前言:接着前面进程终止,话不多说我们进入Linux进程等待的学习,如果你还不了解进程终止建议先了解:
Linux进程终止
本篇主要内容:
什么是进程等待
为什么要进行进程等待
如何进程等待
1. 进程等待的概念
首先在开始之前我们提个问题,到底什么是进程等待?
进程等待的概念:
- 我们通常说的进程等待其实是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程,父进程必须等待这个子进程结束后,处理它的代码和数据!
2. 进程等待必要性
在了解完进程等待的概念后,新的问题出现了,我们为什么要进行进程等待,进程等待的必要性是什么?
进程等待必要性:
- 若子进程退出,而父进程对它不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,谁也没有办法杀死一个已经死去的进程。
- 父进程创建子进程的目的是为了让子进程协助自己完成任务的,而父进程需要知道子进程将任务完成得如何。这就需要通过进程等待的方式,获取子进程的退出信息。
3. 进程等待的方法
3.1 wait方法
我们可以通过系统调用来等待进程:wait
函数
wait
等待任意一个子进程的退出,如果等待成功他将返回子进程的pid,失败则返回-1
我们就用一段代码来看看wait
:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> void Worker() { int cnt = 5; while(cnt) { printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--); sleep(1); } } int main() { pid_t id = fork(); if(id == 0) { // child Worker(); exit(0); } else{ sleep(10); // father pid_t rid = wait(NULL); if(rid == id) { printf("wait success, pid: %d\n", getpid()); } } return 0; }
进程等待:wait函数
我们通过视频发现:进程等待是可以回收子进程僵尸状态的
然后我们将父进程sleep()
取消,看看在子进程退出之前父进程在干什么
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> void Worker() { int cnt = 5; while(cnt) { printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--); sleep(1); } } int main() { pid_t id = fork(); if(id == 0) { // child Worker(); exit(0); } else{ // father printf("wait before:\n"); pid_t rid = wait(NULL); printf("wait after:\n"); if(rid == id) { printf("wait success, pid: %d\n", getpid()); } sleep(10); } return 0; }
观察父进程等待过程
通过这个视频我们又能发现两个进程一起运行,但是在子进程没有退出之前,父进程一直在wait上等待,并且并没有出现子进程僵尸状态而是直接回收了。
结论:如果子进程根本就没有退出,父进程必须在wait上进行阻塞等待。直到子进程僵尸,wait自动回收返回。
3.2 waitpid方法
waitpid
和wait
都是等待进程。waitpid
可以指定等待一个进程,且有三个参数
4. 获取子进程status
父进程想要知道子进程的退出信息,也就是退出码和退出信号,就要用到输出型参数status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
int main() { pid_t id = fork(); if(id == 0) { // child Worker(); exit(10); // 设置成10方便观察现象 } else{ // father printf("wait before:\n"); int status = 0; pid_t rid = waitpid(id, &status, 0); printf("wait after:\n"); if(rid == id) { printf("wait success, pid: %d, status: %d\n", getpid(), status); } } return 0; }
我明明将
exit
的退出结果设置成10,但是为什么他的status会是2560呢?
- 其实status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
因此我们在研究status时,不能整体使用status!!!
获取子进程退出信息
因为我们知道了status不能整体使用因此我们要进行位操作:
exit sig: status&0x7f //获取信号 exit code: (status>>8)&0xff //获取退出结果
- 当我们的程序异常了,exit code 将无任何意义
- exit sig : 0则代表没有收到信号
- 手动杀掉子进程也会获取到信号
但是如果我们每次提取退出信息都要使用繁琐的位运算,这很不方便,因此系统给我们做了一个简单的封装
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
int main() { pid_t id = fork(); if(id == 0) { // child Worker(); exit(1); } else{ // sleep(10); // father printf("wait before:\n"); //pid_t rid = wait(NULL); int status = 0; pid_t rid = waitpid(id, &status, 0); printf("wait after:\n"); if(rid == id) { // 不能对status整体使用 //printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n",getpid(), rid, status&0x7f, (status>>8)&0xff); if(WIFEXITED(status)) { printf("child process normal quit, exit code: %d\n", WEXITSTATUS(status)); } else{ printf("child process quit exept!!!\n"); } } } return 0; }
当我们要获取多个进程的调度信息时,我们给每个进程都要一个编号,我们来观察一下进程是怎样调度的
void Worker(int number) { int cnt = 5; while(cnt) { printf("i am child, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number); sleep(1); } } const int n = 10; int main() { for(int i = 0; i < n; i++) { pid_t id = fork(); if(id == 0) { Worker(i); exit(i); } } // 等待多个子进程 for(int i = 0; i < n; i++) { int status = 0; pid_t rid = waitpid(-1, &status, 0); //pid > 0, -1:等待任意一个进程 if(rid > 0) { printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status)); } } return 0; }
观察进程调度顺序
我们发现明明是按顺序创建的进程,但是在调度时却没有顺序可言,终止的时候也没有顺序,因为进程在调度完全由调度器说的算,所以进程调度的先后我们并不确定,这点在前面我们也提到过。
5. waitpid的第三个参数options
在使用waitpid的第三个参数时,前面我们提到设为0则是默认阻塞等待状态,必须等待子进程的退出,当时如果我们要做自己的事我们就不能使用0而是使用:WNOHANG
options:
- WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 0:默认的阻塞等待状态
**父进程在非阻塞等待时,因为子进程没有结束,就跑去做自己的事情了,但是又要继续等待,所以往往伴随着重复调,轮询,也就是基于非阻塞轮询的等待方案!
**
我们来直接验证一下非阻塞等待:
void Worker(int cnt) { printf("i am child, pid: %d, cnt: %d\n", getpid(), cnt); sleep(1); } int main() { pid_t id = fork(); if(id == 0) { // 子进程 int cnt = 5; while(cnt) { Worker(cnt); sleep(1); cnt--; } exit(1); } // 父进程 while(1) { int status = 0; pid_t rid = waitpid(id, &status, WNOHANG); if(rid > 0) { // 等待成功 printf("wait success, child quit, exit code: %d, exit sig: %d\n", (status>>8)&0xff, status&0x7f); break; } else if(rid == 0) { // 等待成功,但子进程并未退出 printf("wait again, child alive, do other thing\n"); sleep(1); } else{ // 等待失败 printf("wait failed\n"); break; } } return 0; }
非阻塞等待
我们可以看到非阻塞等待可以让我们的父进程在等待时,做自己的事!
6. 总结拓展
拓展一:父进程如何得知子进程的退出信息
父进程调用wait()/waitpid()
来获取子进程的退出信息,调用的接口就传入了一个status参数,而父进程中存在着一个statusp的指针。
而子进程在退出时,操作系统就会将退出信号和退出码写到子进程的PCD中
int exit_code; int exit_signal
而退出信号和退出码将会写到这两个变量中,
当我们调用系统调用时,只需要将这两个变量组合写入到变量里
*statusp = (exit code<<8)| exit siganl • 1
这样父进程就获取到了子进程的退出信息
拓展二:我们为什么不用全局变量获取子进程的退出信息而用系统调用?
这个就是因为进程具有独立性,父进程无法直接获得子进程的退出信息
总结:
进程等待确实非常有用,它既可以回收僵尸进程,避免造成内存泄漏,也能让父进程能够获取到子进程的退出信息,进程等待我们就先了解这么多,进程控制马上就到了我们的最后一步——进程替换,让我们来期待下一篇!
谢谢大家支持本篇到这里就结束了