Linux(9)——进程(控制篇——下)

发布于:2025-05-31 ⋅ 阅读:(27) ⋅ 点赞:(0)

目录

三、进程等待

1)进程等待的必要性

2)获取子进程的status

3)进程的等待方法

wait方法

waitpid方法 

多进程创建以及等待的代码模型  

非阻塞的轮训检测

四、进程程序替换

1)替换原理

2)替换函数

3)函数解释 

4)命名理解 


三、进程等待

1)进程等待的必要性

  1. 之前提过子进程退出,父进程如果不读取子进程的退出信息,就可能造成“僵尸进程”的问题,从而造成内存泄漏的问题。
  2. 再者,一旦子进程进入了僵尸状态,那就连kill -9都杀不亖他,因为没有谁能够杀亖一个死去的进程。
  3. 最后,父进程创建子进程是要获取子进程的完成任务的情况的。
  4. 父进程需要通过等待的方式来回收子进程的资源,获取子进程的退出信息。

2)获取子进程的status

下面进程等待使用的两个方法wait方法和waitpid方法都有一个status参数,这是一个输出型参数(输出型参数是函数中用于返回结果或修改调用者变量的参数,通常通过引用或指针实现。如void func(int *output)。),由操作系统进行填写。

如果向status中传递的是NULL,那就表示用户不关心子进程的退出状态。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

我们从图中可见,status的低16比特位中,高8位表示进程的退出状态,即退出码。当进程被信息杀亖时,则低7位表示终止信息,第8位时core dump标志。

我们可以通一系列的位操作来得出进程的退出码和退出信号。

exitcCode = (status >> 8) & 0xFF; //退出码

exitSignal = status & 0x7F;

对于这两个操作,系统提供了两宏来获取退出码以及退出信号。分别是:

  • WIFEXITED(status):用于查看是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status); //是否正常退出

exitCode = WEXITSTATUS(status); //获取退出码

敲黑板:

当一个进程是非正常退出的时候,那么该进程的退出码将毫无意义。

3)进程的等待方法

wait方法

函数类型:pid_t wait(int* status);

返回值:成功返回被等待进程pid,失败返回-1。

参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL 

作用:等待任意子进程 

创建子进程后,父进程使用wait方法等待子进程,直到子进程的退出信息被读取,我们可以写个代码验证一下:

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/wait.h>    
#include <sys/types.h>    
    
int main()    
{    
    pid_t id = fork();    
    if(id == 0){ //子进程    
        int count = 10;    
        while(count--)    
        {    
            printf("我是子进程,PID:%d, PPID:%d\n", getpid(), getppid());    
            sleep(1);    
        }    
        exit(0);    
    }    
    //父进程                                                                                                                                                               
    int status = 0;    
    pid_t ret = wait(&status);    
    if(ret > 0){    
        printf("等待成功...\n");    
        if(WIFEXITED(status)){    
            printf("退出码:%d\n", WEXITSTATUS(status));    
        }    
    }    
    sleep(3);    
    return 0;    
}

然后我们可以在开一个会话用来监控进程的状态:

while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;echo "============================================================";sleep 1;done

 在下面这图中我们可以看到,当子进程退出,父进程读取到了子进程的退出信息时,子进程就不会变成僵尸状态了。

waitpid方法 

函数原型:pid_t waitpid(pid_t pid, int *status, int options);

返回值:

  1. 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  2. 如果设置了选项WNOHANG(option),而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 

参数:

  1. pid:当pid=-1,等待任意一个子进程,与wait等效。当pid>0.等待其进程ID与pid相等的子进程。
  2. status:输出型参数,用来获取子进程的退出状态,不关心可以设置成NULL。
  3. options:默认为0,表示阻塞等待;当设置为WNOHANG时,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

返回值:等待任意子进程(可以指定)退出 

我们可以写个代码来验证一下,创建子进程后,父进程可以使用waitpid函数一直等待子进程,直到子进程退出后读取子进程的退出信息。

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/wait.h>    
#include <sys/types.h>    
    
int main()    
{    
    pid_t id = fork();    
    if(id == 0){ //子进程    
        int count = 10;    
        while(count--)    
        {    
            printf("我是子进程,PID:%d, PPID:%d\n", getpid(), getppid());    
            sleep(1);    
        }    
        exit(0);    
    }    
    //父进程    
    int status = 0;    
    //pid_t ret = wait(&status);    
    pid_t ret = waitpid(id, &status, 0);    
    if(ret >= 0){    
        printf("等待成功...\n");    
        if(WIFEXITED(status)){    
            printf("退出码:%d\n", WEXITSTATUS(status));    
        }else{    
         printf("被信号杀?:%d\n",status & 0x7F);    
        }                                                                            
    }    
    sleep(3);    
    return 0;    
}

在父进程运行过程中,我们可以使用kill -9命令来将子进程杀亖,这个时候父进程也能成功等待子进程。

敲黑板:

被信号杀亖的进程的退出码是没有意义的。

多进程创建以及等待的代码模型  

上面演示的都是父进程的创建以及等待一个子进程,那么接下来我们可以同时创建多个子进程,然后让父进程进程依次等待子进程退出。

下面我们可以同时创建10个子进程,将子进程的pid放到一个id数组中,并将这10个子进程的退出时的退出码设置为该子进程pid对应数组中的下标,之后父进程使用waitpid等待这10个子进程。

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
int main()    
{    
    pid_t ids[10];    
    for(int i = 0; i < 10; i++){    
        pid_t id = fork();    
        if(id == 0){    
            printf("子进程创建成功:PID:%d\n", getpid());    
            sleep(3);    
            exit(i);    
        }    
        ids[i] = id;    
    }    
    for(int i = 0; i < 10; i++){    
        int status = 0;    
        pid_t ret = waitpid(ids[i], &status, 0);    
        if(ret >= 0){    
            printf("等待子进程成功:PID:%d\n", ids[i]);    
            if(WIFEXITED(status)){    
                printf("退出码:%d\n", WEXITSTATUS(status));    
            }    
            else{    
                printf("被信号杀亖:%d\n", status & 0x7F);    
            }    
    }    
    }                                                                                                                                                                               
    return 0;    
} 

运行完代码,我发现父进程同时创建了了多个子进程,当子进程退出后,父进程再以此读取这些子进程的退出信息。 

杀亖进程,父进程依然可以获取子进程的退出信息。

非阻塞的轮训检测

上面的方案,其实是有缺点的,那就是在父进程等待子进程的时候,父进程什么也干不了,这样的等待就是阻塞等待。

而实际上,我们是可以让我们的父进程在等待子进程退出的过程中做一些自己的事情的,这样的等待就是非阻塞等待了。

其实想要实现非阻塞等待也很简单,我们在上面说waitpid时就提过了,在想这个函数第三个参数传入WNOHANG时就可以使得在等待子进程时,如果waitpid函数直接返回0,就不予等待,而等待的子进程若是正常结束,就返回该子进程的pid。

比如,父进程可以选择地调用wait函数,若是等待的子进程还没有退出,那么父进程就可以先去做一些其他的事情了,过一段时间在调用waitpid函数读取子进程的退出信息。

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
int main()    
{    
    pid_t id = fork();    
    if(id == 0){    
        int count = 3;    
        while(count--){    
            printf("子进程在运行:PID:%d, PPID:%d\n", getpid(), getppid());    
            sleep(3);    
        }    
        exit(0);    
    }    
    while(1){    
        int status = 0;    
        pid_t ret = waitpid(id, &status, WNOHANG);    
        if(ret > 0){    
            printf("等待子进程成功。\n");    
            printf("退出码是:%d\n", WEXITSTATUS(status));    
            break;    
        }    
        else if(ret == 0){    
            printf("父进程做了其他事情。\n");    
            sleep(1);    
        }    
        else{    
            printf("等待错误。\n");    
            break;    
        }    
    }    
    return 0;                                                                          
}

代码的运行结果就是,父进程每隔一段时间就去看看子进程是否退出,如果没就去做自己的事情,知道子进程退出后读取子进程退出的信息。

 

四、进程程序替换

1)替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

 回答两个关于进程程序替换的文题:

问题一:

当进行进程替换时,有没有创建新的进程呢?

进程程序替换之后,该进程的PCB、进程的地址空间以及页表等数据结构都没有发生改变,而只是对原来在物理内存上的代码和数据进行了替换,所有根本没有创建新的进程,而且进程的程序替换前后其pid也是没有改变的。

问题二:

子进程进行进程的程序替换时,是否会影响父进程的代码和数据呢?

不会影响,一开始子进程被父进程创建时代码和数据是和父进程共享的,但是一旦子进程要进行程序替换操作,就意味着子进程需要对代码和数据进行写入操作了,这时就需要对父进程的代码和数据进行拷贝了(写时拷贝),从这里开始子进程和父进程的代码和数据就分离了,所有子进程进行进程的程序替换时不会影响父进程的代码和数据。

2)替换函数

这里的替换函数都是以exec开头的,所以称之为exec函数,总共有六种:

1️⃣int execl(const char *path,const char *arg,...); 

相关参数的说明:第一参数是要执行程序的路径,第二个参数是可变参数列表,表示具体如何执行这个程序,同时以NULL结尾。

写一个执行ls程序:

execl("/usr/bin/ls", "ls", "-l", NULL);

2️⃣int execlp(const char *file, const char *arg,...); 

相关参数的说明:第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行,也是以NULL结尾。

写一个执行ls程序:

execle("ls", "ls", "-l", NULL); 

3️⃣int execle(const char *path, const char *arg,..., char *const envp[]); 

相关参数的说明:第一个参数是要执行的程序的路径,第二个参数是可变参数列表,表示如何执行这个程序,也是以NULL结尾。第三个参数是自己设置的环境变量。

这里我们可以设置MYVAL的环境变量,在test中可以使用这个环境变量了。

char* myenp[] = {"MYVAL=2025", NULL};
execle("./test", "test", NULL, myenvp);

4️⃣int execv(const char *path, char *const argv]); 

相关参数的说明:第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组中的就是你要如何执行这个程序,数组也是以NULL结尾的。

写一个ls程序:

char* myargv[] = {"ls", "-l", NULL);
execv("/usr/bin/ls", myargv);

5️⃣int execvp(const char *file,char *const argv[]);

相关参数的说明:第一个是要执行的程序的名字,第二个参数是一个指针数组, 数组中的就是你要如何执行这个程序,数组也是以NULL结尾的。

写一个ls程序:

char* myargv[] = {"ls", "-l", NULL};
execvp("ls", myargv);

6️⃣int execve(const char *path,  char *const argvl],  char *const envp[]);

相关参数的说明: 第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组中的就是你要如何执行这个程序,数组也是以NULL结尾的,第三个参数是自己设置的环境变量。

这里我们可以设置MYVAL的环境变量,在test中可以使用这个环境变量了。

char* myargv[] = {"mycmd", NULL};
char* myenvp[] = {"MYVAL=2025", NULL};
execve("./test", test, myenvp);

3)函数解释 

  1. 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  2. 如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值。

4)命名理解 

其实我们仔细观察就会发现这六个函数都是有规律的,只要掌握了规律是很好记的。

  1. l(list):表示参数采用列表的形式
  2. v(vector):参数用数组的形式
  3. p(path):有p自动搜索环境变量PATH
  4. e(env):表示自己维护环境变量
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,须自己组装环境变量
execv 数组 不是
execvp 数组
execve 数组 不是 不是,须自己组装环境变量

事实上,我们打开man手册就可以发现execve在man手册第2节其它函数在man手册第3节。也就是 只有execve是真正的系统调用,其它五个函数最终都调用execve。


网站公告

今日签到

点亮在社区的每一天
去签到