朋友们、伙计们,我们又见面了,本期来给大家解读一下有关Linux进程等待的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
1. 进程等待
通过系统调用接口(wait/waitpid)的方式,让父进程对子进程进行资源回收的等待过程。
2. 进程等待的必要性
1. 在之前的僵尸进程这一章节提到过,子进程在退出之后,父进程不进行回收资源,会造成内存泄露的问题,简而言之就是要解决僵尸进程带来的内存泄露问题。
2. 父进程创建子进程的目的就是为了让子进程做一些事情,那么父进程是需要知道子进程把事情完成的怎么样了,就需要通过等待来获取进程退出的信息(两个数字),获取这两个数字并不是必须的,但是系统需要提供这样的基础功能!
3. 进程等待的方法
可以使用系统调用接口:wait()或者waitpid()
3.1 wait
wait可以等待任意一个进程。
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status); //返回值: //成功返回被等待进程pid,失败返回-1。 //参数: //输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
我们先不关心它的参数,直接设置为NULL,先来看一下能否回收掉子进程的僵尸问题:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/types.h> void Worker() { int cnt = 5; while (cnt--) { sleep(1); printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); } } int main() { pid_t id = fork(); if (id == 0) { // child Worker(); exit(10); // 退出码设置为10 } else { // parent sleep(10); pid_t rid = wait(NULL); if (rid == id) // 等待成功 { printf("wait success, pid: %d, rpid: %d, \n", getpid(), rid); } sleep(5); } return 0; }
上面这段代码我们期望看到的效果是:刚开始有两个进程在运行,5s之后子进程变成了僵尸状态,再过5s,父进程等待回收子进程,就剩下一个进程在运行。
如果子进程不退出我们就开始等待会发生什么呢?
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/types.h> void Worker() { int cnt = 5; while (cnt--) { sleep(1); printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); } } int main() { pid_t id = fork(); if (id == 0) { // child Worker(); exit(10); // 退出码设置为10 } else { // parent //sleep(10); printf("wait befor\n"); pid_t rid = wait(NULL); printf("wait after\n"); if (rid == id) // 等待成功 { printf("wait success, pid: %d, rpid: %d, \n", getpid(), rid); } sleep(5); } return 0; }
通过实验可以看到,如果子进程没有退出,那么父进程就必须在wait上进行阻塞等待,直到子进程结束,wait自动回收并返回!
一般而言,父子进程谁先运行并不确定,但可以确定的是,父进程一定是最后退出的。
3.2 waitpid
waitpid可以等待指定的进程。
#include<sys/types.h> #include<sys/wait.h> pid_ t waitpid(pid_t pid, int *status, int options); //返回值: //当正常返回的时候waitpid返回收集到的子进程的进程ID; //如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; //如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:等待指定的进程id,设置为-1表示等待任意进程。
*status:获取子进程的状态,不关注可以设置为NULL。
optons:等待的方法,设置为0默认为阻塞式等待。
3.2.1 *status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
通过等待的方式解决了僵尸进程导致的内存泄漏问题,那么如何获取子进程的退出结果呢?
*status是输出型参数,我们可以定义一个status,然后传递给wait/waitpid,然后将返回信息带给我们。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/types.h> void Worker() { int cnt = 5; while (cnt--) { sleep(1); printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); } } int main() { pid_t id = fork(); if (id == 0) { // child Worker(); exit(10); // 退出码设置为10 } else { // parent sleep(10); // pid_t rid = wait(NULL); int status = 0; // 定义退出信息 pid_t rid = waitpid(id, &status, 0); if (rid == id) // 等待成功 { printf("wait success, pid: %d, rpid: %d, exit code: %d\n", getpid(), rid, status); } sleep(5); } return 0; }
可以看到明明我们设置的退出码是10,为什么打印出了2560呢?
status是一个整形指针变量,有32个比特位,我们不能简单的当做整形来看待,要当做位图来看待,我们研究status只观察低16位:
正常结束:次低8位(8 ~ 15)表示的是进程退出码;
异常终止:低8位(0 ~ 7)表示的是异常终止信号。
正常结束时,退出码为10对应的status就是0000 1010 0000 0000转化为10进制就是2560
所以我们要正确的得到进程的退出码需要给status右移8位,按位于0xFF((status >> 8) & 0xFF);
正确得到进程终止信号需要给status按位于0x7F(status & 0x7F)。
int main() { pid_t id = fork(); if (id == 0) { // child Worker(); exit(10); // 退出码设置为10 } else { // parent sleep(10); // pid_t rid = wait(NULL); int status = 0; // 定义退出信息 pid_t rid = waitpid(id, &status, 0); if (rid == id) // 等待成功 { printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status & 0x7F, (status >> 8) & 0xFF); } sleep(5); } return 0; }
关于status参数对退出码的提取还有一种简便的方式:现在外面定义status。
WIFEXITED(status) // 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status) // 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
if (WIFEXITED(status)) // 正常退出 { printf("child process normal quit, exit code : %d\n", WEXITSTATUS(status)); } else // 异常终止 { printf("child process quit except!\n"); }
① 当一个进程异常终止,此时的退出码就毫无意义。
② 如何判断有没有收到信号呢?只要终止信号为0(终止信号范围在1 ~ 64),就表示程序运行正常。
为什么不用全局变量来获取子进程的退出信息呢?而要用系统调用?
当父子进程要对全局变量写入时,会发生写时拷贝,又因为进程具有独立性,所以父进程是无法直接获得子进程的退出信息的。
3.2.2 options
该参数表示等待的方式:
0表示阻塞式等待;
WNOHANG表示非阻塞式等待。
通过一个例子来理解一下阻塞与非阻塞等待:
如果我们要下载一个游戏,那么在下载的这段时间我们就啥也不干,直勾勾的盯着它,直到它下载好,这种行为就叫做阻塞式等待;
当游戏下载的这段时间,我们可以先看看它下没下好,没下好我们就可以看看视频,听听音乐,然后再看看它下没下好,没下好我们就再干干别的事情,直到它下载好,这种行为就叫做非阻塞式等待。
对比等待进程:
- 阻塞式调用:使用wait/waitpid等待子进程时,如果子进程不退出,wait/waitpid不返回。
- 非阻塞式调用:使用waitpid等待子进程时,如果我们等待的条件不满足,不阻塞,而是直接返回!
非阻塞等待往往需要重复调用,以非阻塞轮询的方案进行进程等待,它的好处就在于,当我们进行进程等待的过程中,可以顺便做一下自己占据时间并不多的事情。
当options设置为WNOHANG时,waitpid的返回值有三种情况:
- rid > 0:等待成功
- rid == 0:等待成功,但是对方还有退出
- rid < 0:等待失败(pid填写错误)
下面我们就来用代码的方式来演示一下非阻塞等待:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/types.h> void Worker() { int cnt = 5; while (cnt--) { printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); sleep(2); } } int main() { pid_t id = fork(); if (id == 0) { // child Worker(); exit(0); // 退出码设置为10 } else { while (1) // 非阻塞轮询 { int status = 0; // 定义退出信息 pid_t rid = waitpid(id, &status, WNOHANG); if (rid > 0) // 等待成功 { printf("child quit success, pid: %d, exit sig: %d, exit code: %d\n", getpid(), status & 0x7F, (status >> 8) & 0xFF); break; } else if (rid == 0) // 等待成功,但并未退出 { printf("child is alive, wait again, father do other thing....\n"); // 可以设置一些任务... } else // 等待失败 { printf("wait failed!\n"); break; } sleep(1); } } return 0; }
3.3 OS层面的进程等待
根据冯诺依曼体系结构,wait和waitpid属于系统调用,而进程在OS的内存中运行,我们再进行进程等待的时候,使用系统调用接口,要拿到子进程的退出信息,所以我们在外部定义了一个变量status,当子进程执行完毕之后,OS会执行它的退出程序,将进程的Z状态修改为X状态,并将子进程的的退出码和退出信号写入到它的PCB中,这时就可以通过父进程中的status指针来获取子进程的退出信息。
创建多进程循环fork,等待多进程循环waitpid,waitpid第一个参数设置为-1,表示等待任意一个进程。
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!