进程在执行完之后,需要调用exit()系统调用退出,然后操作系统会清理进程相关的资源:
例如申请的内存、打开的文件等。
打开man手册,可以看到exit系统调用的声明如下:
void exit(int status);
一般情况下,进程正常退出使用exit(0),异常退出使用exit(-1)。
参数status是进程退出时的状态标志,可以被父进程使用wait系统调用获取到。
例如,写一个c程序:
int main()
{
return -1;
}
编译后假设可执行文件为a.out,使用命令./a.out执行,则可用echo $?命令获取./a.out的退出标志,即main()函数最后返回的值,输出为255,即uint8_t型的-1。
在命令行执行时,shell(假设为bash)为父进程,a.out退出时的status被shell获得,而shell命令echo $?可以获取上次执行命令的退出状态。
在man手册的exit函数的返回值说明中,有一句话“the exit() function do not return”,即exit函数不再返回。实际上,exit()也没法返回了,它是一个单向旅行,调用它就会导致进程的死亡。
exit的处理过程大概如下:
1,进入内核,设置进程状态为“不可打断睡眠”,也就是说进程不会再次被调度执行,
2,关闭打开的文件,清理用户态的内存等资源,总之就是能释放的全部释放,能关闭的全部关闭。到了这里,实际上已经回不去了,因为用户态的内存已经被释放了。
还剩下的就是进程在内核的task_struct结构体,以及进程的“内核页表”了。
task_struct,就是进程在内核的描述结构,而且还和内核栈绑定在一起,暂时是没法释放的。代码的执行,必须要用栈来保存临时变量、返回地址等关键数据。
页表,关系到内存线性地址到物理地址的映射。进入内核态了,就可以把用户态的释放掉,反正不会返回用户态了,但是内核页表是不能释放的。
正是因为task_struct、内核栈、内核页表的存在,进程的“僵尸状态”才会存在。也可以叫“僵死状态”,英文都是zombie。
僵尸进程,就是处于zombie状态的进程。
到了这里,可以把进程设置为zombie状态了。
zombie状态的进程,也会占用系统的内存资源。如果不及时清理,那么随着进程的不断创建和僵死,内存会被消耗尽的。
显然,内核页表和内核栈正处于使用状态时,是没法清理的。只有等不使用了,才可以清理。也就是说,只有当“该进程”不是current进程---即CPU不处于“该进程的上下文”----时才可以清理。
A,怎么办呢?当然是让别人成为current进程啊。
B,让谁来清理呢?谁fork,谁清理,当然是父进程。
C,父进程咋知道什么时候该清理?给它发SIGCHILD信号。
D,父进程如果挂了呢?让init进程来,反正都是它的后代。
E,init进程挂了呢?机箱上有reset键:(
3,根据以上ABCD四项原则,现在操作如下:
把exit函数的参数status设置到task_struct的exit_code项里:
current->exit_code = status;
给父进程发SIGCHILD信号:
kill(current->ppid, SIGCHILD);
进程切换,退出当前进程上下文,从那些没zombie的里选一个执行:
schedule();
这次调度之后,该进程永远也不会回来了。
4,父进程收到SIGCHILD信号后,使用wait()系统调用,清理zombie状态的子进程的task_struct、内核栈、内核页表。
打开man手册,wait系统调用的声明为:
pid_t wait(int* status);
被清理的子进程的pid由wait返回,退出状态由指针变量status传出。
要在代码里执行一个命令,可以这么实现:
pid_t pid = fork();
if (-1 == pid) {
//出错
} else if (0 == pid) {
//子进程
execve(); //execve执行成功后是不会返回的
exit(-1); //到了这里,必然出错,返回-1
} else {
//父进程,
int status;
pid_t pid2;
do {
pid2 = wait(&status);
} while (pid2 != pid);
//输出信息status信息
}