Linux 修炼:进程控制(一)
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《C++修炼之路》、《Linux修炼:终端之内 洞悉真理》、《Git 完全手册:从入门到团队协作实战》
感谢你打开这篇博客!希望这篇博客能为你带来帮助,也欢迎一起交流探讨,共同成长。
目录
1、写时拷贝
2、进程终止
2.1、退出码
2.2、进程退出
3、进程等待
3.1、进程等待引入
3.2、进程等待的方式
3.2.1、wait
3.2.2、waitpid
3.3、非阻塞等待
通过前面的学习,我们知道了要想创建子进程需要用fork函数。创建完成后fork函数子进程返回值为0,父进程返回值为子进程的pid。需要注意的是,fork函数不能无限次数调用,因为每次调用都会复制页表,虚拟地址空间,pcb等等,导致占用物理内存。
当然,fork创建的进程不仅能执行和父进程一样的代码和数据,还能执行一个不同的程序。不过我们后面再说。
1、写时拷贝
通过前面的学习,我们已经知道了父子进程与物理内存之间建立的是怎么样的链接。
数据段和代码段默认情况是只读的。当子进程想要修改数据段的时候,系统会进行"报错"。对于这个报错,系统要做一个检查。如果我们是由于访问野指针而造成的报错,那就直接终止进程。如果是因为权限而造成的报错,这时候系统首先会进行写时拷贝,再更改权限,使权限变为读写。
那么我们为什么要写时拷贝呢?为什么不能在创建子进程是直接先拷贝一份呢?因为我们子进程未必会修改这部分内容,如果我们不管子进程修不修改,直接拷贝一份,无疑是白白浪费空间的。写时拷贝,本质是按需获取。
在C/C++上使用malloc/new申请空间时,其实也是首先开辟虚拟地址空间。当真正用的时候,系统才给你做内存级的申请。
2、进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出,无非就三种情况:
(1)代码跑完,结果对
(2)代码跑完,结果不对
(3)代码没跑完,进程异常了
其中,代码跑完后结果对不对,需要用退出码来决定。
2.1、退出码
在我们之前写代码时,main函数的最后要return 0。其实这个0就是退出码,表示成功执行,没有发生错误。当然,我们也可以返回其他的退出码。
我们可以使用命令查看最近一次进行的进程的退出码:
echo $?
我们把code中的退出码改为10再查看一下:
我们连续执行两次echo $?,第二次的打印值变为了0,是因为我们echo $?本身是一个命令,而命令也是一个进程,0表示运行成功。
退出码是有实际意义的,他可以表示当前进程运行的结果。这个退出码会被系统获得。0表示成功,非0表示失败。每个退出码都有自己表达的含义,
我们可以执行以下代码,查看所有的退出码:
#include <stdio.h>
#include<string.h>
int main()
{int i=0;for(;i<134;i++) {printf("%d:%s\n",i,strerror(i));}return 0;
}
一共134个退出码,太多了我就不展示了。
2.2、进程退出
进程退出一共有三种方法,除了上面的main函数return,还有两种方法(还有其他方法以后再说),分别是调用exit和_exit。exit和_exit括号里面传的是退出码。
return 和 exit的区别是,只有main函数return才会退出进程,而只要调用了exit,不管是main函数还是其他函数,都会直接退出进程。
exit和_exit的区别是,exit是标准C库函数,执行后会刷新缓冲区(return也会刷新缓冲区),_exit是系统调用,不会刷新缓冲区。
要想退出进程,必须要系统调用,所以说exit底层封装了_exit。
3、进程等待
3.1、进程等待引入
进程等待就是让父进程通过等待的方式,回收子进程PCB,如果需要,获取子进程的退出信息。
为什么要进程等待呢?如果子进程退出,父进程不管不顾,就可能造成僵尸进程,进程一旦变成僵尸进程,就无法被kill杀掉。僵尸进程会造成内存泄漏。所以父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
3.2、进程等待的方式
3.2.1、wait
那么如何让父进程等待呢?我们可以使用wait函数使父进程进行等待。让父进程调用wait之后,如果子进程没有退出,父进程wait的时候就会阻塞。如果子进程退出,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题。等待成功就会返回子进程的pid,失败时返回-1。
父进程调用wait,就会等待任意一个子进程,此时父进程会阻塞。
我们执行以下代码
#include <stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>int main()
{pid_t id=fork();if(id==0){int i=10;while(i){ printf("子进程,pid:%d,ppid:%d\n",getpid(),getppid());sleep(1); i--; } printf("子进程退出了\n"); exit(1); } sleep(10);//这时候子进程为僵尸状态 printf("父进程等待子进程\n");pid_t rid=wait(NULL);if(rid>0) { printf("等待成功,子进程pid:%d\n",rid);} sleep(5); return 0;
}
当子进程已经退出而父进程还没退出的时候,我们可以查看到子进程的状态为僵尸状态(Z状态),当等待成功后,子进程成功被释放。经过验证我们可以通过wait来释放子进程的僵尸状态。
父进程也可以释放多个子进程。我们执行以下代码,父进程的wait循环十次,释放十个子进程:
#include <stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#define N 10 int main()
{ int i=0; for(;i<N;i++) { pid_t id=fork(); if(id==0) { int i=10; while(i) { printf("子进程,pid:%d,ppid:%d\n",getpid(),getppid());sleep(1);i--; } printf("子进程退出了\n");exit(1); } } printf("父进程等待子进程\n");sleep(15);for(i=0;i<N;i++){pid_t rid=wait(NULL);if(rid>0){printf("父进程等待子进程成功,子进程pid:%d\n",rid);}}return 0;
}
多进程中,往往父进程最先创建,最后退出。
除了wait以外,还有一个更强大的函数waitpid。 waitpid的功能比wait更多。
3.2.2、waitpid
函数原型
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *wstatus, int options);
参数详解
-
pid_t pid
:指定要等待哪个子进程。-
pid > 0
:等待进程ID等于pid
的特定子进程。 -
pid = -1
:等待任意一个子进程结束,行为类似于wait()
。 -
pid = 0
:等待与调用进程(父进程)属于同一个进程组的任意子进程。 -
pid < -1
:等待进程组ID等于pid
绝对值的任一子进程。(例如pid = -1234
会等待进程组ID为1234
的任一子进程)。
-
-
int *wstatus
:一个指向整数的指针,用于存储子进程的退出状态信息。你不能简单地把它当做一个整数来看,必须使用下面提供的宏来解析它。如果不需要详细信息,可以传入NULL
。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是 否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的 退出码) -
int options
:用于修改函数的行为,可以通过按位或|
组合多个选项。最重要的选项是:-
0
:默认行为。阻塞调用,即父进程会一直挂起等待,直到有符合条件的子进程状态改变。 -
WNOHANG
(Wait No Hang):非阻塞。如果没有任何符合条件的子进程已经结束,则立即返回0,而不是阻塞父进程。这是实现“轮询”检查的关键。 -
WUNTRACED
:也报告那些被信号暂停(但未终止)的子进程状态。 -
WCONTINUED
:报告那些之前被暂停、后又恢复运行的子进程状态。
-
返回值
-
成功:返回状态已改变的那个子进程的PID。
-
如果使用了
WNOHANG
且没有子进程退出:返回0
。 -
失败(例如没有符合条件的子进程):返回
-1
,并设置errno
(常见的是ECHILD
)。
现在我们使用waitpid,实现和wait一样的效果:
#include <stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#define N 10int main()
{int i=0;for(;i<N;i++){pid_t id=fork();if(id==0){int i=10;while(i){printf("子进程,pid:%d,ppid:%d\n",getpid(),getppid());sleep(1);i--;}printf("子进程退出了\n");exit(1);}}printf("父进程等待子进程\n");sleep(15);for(i=0;i<N;i++){int status=0;pid_t rid=waitpid(-1,&status,0);if(rid>0){printf("父进程等待子进程成功,子进程pid:%d,status:%d\n",rid,status);}}return 0;
}
当所有子进程退出后,我们发现所有子进程的退出信息是256。这是因为status不仅仅是退出码。status是个int类型整数,其中他的高16个bit位我们不使用,我们只使用低16位。如果子进程正常终止,子进程的8到15bit位(次低8位)是子进程的退出码。如果进程是被信号所杀,那么次低8位就不看了,他的低7位表示这个进程被几号信号所杀,还有一位是core dump标志。
如果我们想获取退出码(假设正常终止),我们用以下方式访问:
(status>>8)&0xFF
如果status表示信号的数字是0,那就说明进程正常运行结束,此时我们再看运行结果对还是不对,对与不对由退出码决定。如果status表示信号的数字不是0,说明出现异常,进程没有正常退出。
当程序出现异常,系统会给这个进程发送信号,此时父进程的waitpid等待到了子进程,释放子进程,同时结束父进程的阻塞状态,我们可以通过访问status来查看信号。
当程序退出时,退出码,退出信号会作为两个变量写入到子进程的PCB中,由于退出码退出信号还需要被父进程访问,所以处于僵尸状态的子进程,其test_struct并不能释放。接着呢,父进程通过waitpid,访问子进程的test_struct,把这两个整形变量合并成一个整数(status)带出去。
现在我们对上面写的代码进行升级:
#include <stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<iostream>
#include<vector>
#include <unistd.h>
#define N 10int main()
{std::vector<pid_t> subids;int i=0;for(;i<N;i++){pid_t id=fork();if(id==0){int i=10;while(i){printf("子进程,pid:%d,ppid:%d\n",getpid(),getppid());sleep(1);i--;}int *p=nullptr;*p=1;printf("子进程退出了\n");exit(0);}subids.push_back(id);}printf("父进程等待子进程\n");sleep(15);for(auto& sid:subids){int status=0;printf("父进程开始等待子进程...,%d\n",sid);pid_t rid=waitpid(sid,&status,0);if(rid>0){int exit_code=(status>>8)&0xFF;int exit_signal=status&0x7F;if(exit_code==0&&exit_signal==0){printf("子进程运行完毕,结果正确\n");}else if(exit_code>0&&exit_signal==0){printf("子进程运行完毕,结果错误\n");}else{printf("子进程出现异常,exit_signal:%d\n",exit_signal);}//printf("父进程等待子进程成功,子进程pid:%d,status:%d,exit code:%d,exit signal:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);}else{printf("等待失败\n");}}return 0;
}
在这段代码中,我们让父进程等待特定的子进程,并打印出子进程的退出状态。
当然,我们其实不用写的这么麻烦,我们可以运用status特定的宏来判断是否正常运行:
...
for(auto& sid:subids){int status=0;printf("父进程开始等待子进程...,%d\n",sid);pid_t rid=waitpid(sid,&status,0);if(rid>0){ printf("等待成功\n");if(WIFEXITED(status)){printf("正常运行结束:%d\n",WEXITSTATUS(status));}else{printf("进程异常\n");}//printf("父进程等待子进程成功,子进程pid:%d,status:%d,exit code:%d,exit signal:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);}else{printf("等待失败\n");}}
3.3、非阻塞等待
在以上的代码中,我们把option设置为0(默认值),这时是阻塞等待。直观的感受就是,如果在等待的过程中,子进程一直不退出,那么父进程始终为阻塞状态。
如果我们把option设置为WNOHANG,此时等待方式为非阻塞等待。
那么什么是非阻塞等待呢?在父进程等待子进程的过程中,父进程检查到子进程没有结束,便直接返回,过一段时间,父进程再调用waitpid,如果检查到子进程没有结束,同样直接返回,一直进行这一操作直到子进程结束。非阻塞等待本质上就是检测子进程状态,如果退出了,那就回收子进程,如果没有退出,那就立即返回。这种方案我们叫做非阻塞轮询方案。
那么为什么要使用非阻塞等待呢?在父进程等待子进程的过程中,父进程可以在父进程的检测子进程的时间间隙去做其他工作,这也就意味着非阻塞等待更高效一些。
非阻塞等待时,waitpid的返回值有三种,第一种是返回值等于等待的子进程的pid,第二种是0,此时说明子进程没有退出,第三种是返回值小于0,说明等待失败。
我们写一段程序实现非阻塞等待:
#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 cnt=10;while(cnt--){printf("子进程在运行:%d\n",cnt);sleep(1);}exit(0);}while(1){pid_t rid=waitpid(id,NULL,WNOHANG);if(rid==id){printf("等待成功\n");break;}else if(rid==0){printf("子进程还未退出\n");sleep(1);}else{printf("等待错误\n");break;}}return 0;
}
好了,这期的内容就分享到这,我们下期再见!