OS24.【Linux】进程等待 (上)
介绍Linux系统中僵尸进程的清理方法及进程等待机制。主要内容包括:1)验证僵尸进程无法被kill命令终止;2)清理僵尸进程的两种方法:终止父进程或父进程主动收尸;3)详细讲解wait和waitpid系统调用的使用,包括等待单个/多个子进程、阻塞特性等;4)深入分析wstatus参数的位图结构,解释如何通过位运算获取子进程退出状态;5)说明进程退出信息的存储位置和waitpid可能失败的情况。文章
目录
1.知识回顾
参见OS20.【Linux】进程状态(2) 僵尸进程、孤儿进程和进程优先级文章复习
2.验证僵尸进程无法用kill杀死
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t ret_id = fork();
if (ret_id == 0)
{
printf("the child process's PID:%d\n",getpid());
exit(0);//父进程不收尸
}
else
{
while(1);
}
return 0;
}
运行结果:
结论:僵尸进程无法用kill杀死,因为谁也没有办法杀死一个已经死去的进程
但只要Ctrl+C僵尸进程就被清理了,原因在"清理僵尸进程常见的两种方法"中讲
3.清理僵尸进程常见的两种方法
杀死父进程
上面提到了"但只要Ctrl+C僵尸进程就被清理了",在linuxjournal how-kill-zombie-processes-linux文章有提到:
如果父进程无法收尸,那么就要杀死或者重启父进程,而Ctrl+C就是杀死父进程的一种方式
见下图画线句
父进程收尸
进程等待
定义
父进程通过进程等待的方式来回收子进程资源和获取子进程退出信息,否则子进程会变僵尸进程,僵尸进程的危害参见OS20.【Linux】进程状态(2) 僵尸进程、孤儿进程和进程优先级文章
系统调用wait和waitpid的简介
父进程调用wait或waitpid(这里不讨论waitid)可以对子进程进行状态检测与回收,即wait或waitpid可以等待任意一个进程的退出
父进程获取子进程退出信息必须经过系统调用
错误做法: 定义一个区域,子进程退出前向区域写入数据,父进程负责读取区域数据,这样就可以不经过系统调用
错因:进程具有独立性
返回值:
等待单个子进程
参数status暂时不填,置为NULL
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)//子进程
{
printf("子进程的pid:%d, ppid:%d\n", getpid(), getppid());
exit(0);
}
else//父进程得到子进程的PID
{
sleep(2);
printf("父进程准备回收子进程\n");
sleep(2);
pid_t ret = wait(NULL);
if(ret < 0)//返回-1等待失败
{
perror("wait failed");
}
if (ret == id)
{
printf("父进程回收子进程成功,子进程的pid:%d\n", ret);
}
sleep(2);
}
return 0;
}
注:1.调用fork()后,父进程得到子进程的PID,那么父进程就可以通过分析ret是否等于id来判断父进程是否成功回收子进程 2.wait返回值:如果调用成功,wait返回成功退出的子进程的PID;如果调用失败,返回-1
运行结果:
等待多个子进程
父进程可以用循环来等待多个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
void run_child()
{
printf("子进程的pid: %d, ppid:%d\n", getpid(), getppid());
}
void collect_child()
{
for(int i = 0; i < 5; i++)
{
sleep(1);
pid_t ret = wait(NULL);//回收任意一个子进程
if(ret < 0)
{
perror("wait failed");
}
else
{
printf("父进程回收子进程成功,子进程的pid:%d\n", ret);
}
}
}
int main()
{
for(int i = 0; i < 5; i++)
{
pid_t id = fork();
if(id == 0)
{
run_child();
exit(0);//执行run_child后,最子进程会退出
}
}
sleep(1);
printf("父进程准备回收子进程\n");
collect_child();
return 0;
}
运行结果:可以看到创建子进程的顺序不一定就是父进程回收的顺序
如果子进程不退出呢?
如果子进程不退出,那么父进程默认在等待的时候,调用这个系统调用的时也就不返回,即父进程默认处于阻塞状态(参见文章OS19.【Linux】进程状态(1)复习),例如以下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
for(int i = 0; i < 5; i++)
{
pid_t id = fork();
if(id == 0)
{
for(;;);//子进程一直不退出
}
else
{
sleep(1);
printf("父进程准备回收子进程\n");
sleep(1);
pid_t ret = wait(NULL);//父进程一直等待,处于阻塞状态
if(ret < 0)
{
perror("wait failed");
}
else
{
printf("父进程回收子进程成功,子进程的pid:%d\n", ret);
}
}
}
return 0;
}
wait是waitpid的子集
对于waitpid(pid_t pid, int *_Nullable wstatus, int options)
Pid==-1,可等待任何一个子进程,此时与wait 效,例如wait(NULL)和waitpid(-1.NULL,0)等价
Pid>0,等待其进程ID与pid形参相等的子进程
解释wstatus参数
子进程的退出状态(退出状态的定义参见OS23.【Linux】进程终止文章)可以通过wstatus的值知晓
pid_t wait(int *_Nullable wstatus);
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
1.wstatus的类型为int*,是一个输出型参数(wait或waitpid通过wstatus修改int类型的变量)
2.用_Nullable编译器指示符修饰,顾名思义,告诉编译器: wstatus指针变量可能为NULL
实验1: wstatus的位图
运行以下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
printf("子进程的pid:%d, ppid:%d\n", getpid(), getppid());
exit(1);//修改了这里
}
else
{
int status;
pid_t ret = wait(&status);
if(ret < 0)
{
perror("wait failed");
}
if (ret == id)
{
printf("父进程回收子进程成功,子进程的pid:%d\n", ret);
printf("status :%d\n", status);//修改了这里
}
}
return 0;
}
运行结果:status的值不是exit(1)的1,而是256
256是2的整数倍,换算成二进制数:
猜测二进制数最高位的1就是exit(1)的1
验证猜测:
将exit(1)改成exit(0b1011),0b1011指的是1011是以二进制数表示的,运行结果:
猜测是正确的
进一步分析: 子进程的退出状态(退出状态的定义参见OS23.【Linux】进程终止文章)可以通过wstatus的值知晓,例如:
1. 子进程是否异常退出
2. 如果没有异常,那么运行结果对吗?
3.如果运行结果不对,那么原因(看退出码)? 不同的退出码表示不同的出错原因
显然*wstatus要拆分成多个部分(位图)
wstatus的位图的详细说明
glibc 2.9库的bits/waitstatus.h中有wstatus各个位的说明:
/* Everything extant so far uses these same bits. */
/* If WIFEXITED(STATUS), the low-order 8 bits of the status. */
#define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
/* If WIFSIGNALED(STATUS), the terminating signal. */
#define __WTERMSIG(status) ((status) & 0x7f)
/* If WIFSTOPPED(STATUS), the signal that stopped the child. */
#define __WSTOPSIG(status) __WEXITSTATUS(status)
/* Nonzero if STATUS indicates normal termination. */
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)
/* Nonzero if STATUS indicates termination by a signal. */
#define __WIFSIGNALED(status) \
(((signed char) (((status) & 0x7f) + 1) >> 1) > 0)
/* Nonzero if STATUS indicates the child is stopped. */
#define __WIFSTOPPED(status) (((status) & 0xff) == 0x7f)
/* Nonzero if STATUS indicates the child continued after a stop. We only
define this if <bits/waitflags.h> provides the WCONTINUED flag bit. */
#ifdef WCONTINUED
# define __WIFCONTINUED(status) ((status) == __W_CONTINUED)
#endif
/* Nonzero if STATUS indicates the child dumped core. */
#define __WCOREDUMP(status) ((status) & __WCOREFLAG)
/* Macros for constructing status values. */
#define __W_EXITCODE(ret, sig) ((ret) << 8 | (sig))
#define __W_STOPCODE(sig) ((sig) << 8 | 0x7f)
#define __W_CONTINUED 0xffff
#define __WCOREFLAG 0x80
__WEXITSTATUS: 退出状态,在wstatus从右往左数的第二个字节,即退出码,前提是__WIFEXITED(status)为真,
(((status) & 0xff00) >> 8)只要从右往左数的第二个字节
__WTERMSIG: 终止信号,在wstatus的低7位
((status) & 0x7f)中0x7f的二进制表示为0111 1111,只有低7位为1
__WSTOPSIG: 和退出状态是同一个意思
__WCOREDUMP: COREDUMP标志,在wstatus的低8位最高位
((status) & __WCOREFLAG)==((status) & 0x80),0x80的二进制表示只有一个1
注:
1.core dump 指的是核心转储,也可以称之为 "吐核"
操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写入到一个磁盘文件中
目前只需要知道: 该信息是用于调试的
2.信号的编号从1开始,因此只需要检测中止信号字段是否为0,就能检测是否异常
其他宏:
__WIFEXITED(status)检测进程是否正常退出,如果进程正常退出,则宏值为真;如果进程异常退出,,则宏值为假
提醒:前面也可以不加下划线,<sys/wait.h>含有不带下划线的版本的宏
(摘自https://www.gnu.org/software/libc/manual/html_node/Process-Completion-Status.html)
其实 /usr/include/sys/wait.h中:
/* This will define all the `__W*' macros. */ # include <bits/waitstatus.h> # define WEXITSTATUS(status) __WEXITSTATUS (status) # define WTERMSIG(status) __WTERMSIG (status) # define WSTOPSIG(status) __WSTOPSIG (status) # define WIFEXITED(status) __WIFEXITED (status) # define WIFSIGNALED(status) __WIFSIGNALED (status) # define WIFSTOPPED(status) __WIFSTOPPED (status) # ifdef __WIFCONTINUED # define WIFCONTINUED(status) __WIFCONTINUED (status)
所以WEXITSTATUS(status) == WEXITSTATUS (status)、WTERMSIG(status) == __WTERMSIG (status)、......
可以得出分布图,其中一个格子表示wstatus的1个比特位:
可以分两种情况看上面这个图:
1.进程正常终止
2.进程被信号所杀
实验2:手动提取wstatus的位
做个测试:
#include <bits/waitstatus.h>//必须写,否则要手动复制粘贴宏
#include <stdio.h>
int main()
{
int wstatus = 0x12345678;
printf("wstatus: 0x%X\n", wstatus);
printf("__WEXITSTATUS: 0x%X\n", __WEXITSTATUS(wstatus));
printf("__WTERMSIG: 0x%X\n", __WTERMSIG(wstatus));
printf("__WSTOPSIG: 0x%X\n", __WSTOPSIG(wstatus));
printf("__WIFEXITED: 0x%X\n", __WIFEXITED(wstatus));
printf("__WIFSIGNALED: 0x%X\n", __WIFSIGNALED(wstatus));
printf("__WIFSTOPPED: 0x%X\n", __WIFSTOPPED(wstatus));
printf("__WCOREDUMP: 0x%X\n", __WCOREDUMP(wstatus));
return 0;
}
运行结果:
注: *wstatus的最低8位表示是否异常,暂时不管高16位
结论: wstatus指针指向的int区域是以位图的方式存储进程的退出状态的(这里讲的退出状态不单指__WEXITSTATUS,而是退出码、终止信号和COREDUMP标志
实验3: 测试进程异常退出
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <bits/waitstatus.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
int val = 2/0;
exit(0);
}
else
{
int wstatus;
pid_t ret = wait(&wstatus);
if(ret < 0)
{
perror("wait failed");
}
if (ret == id)
{
printf("wstatus: 0x%X\n", wstatus);
printf("__WEXITSTATUS: 0x%X\n", __WEXITSTATUS(wstatus));
printf("__WTERMSIG: 0x%X\n", __WTERMSIG(wstatus));
printf("__WSTOPSIG: 0x%X\n", __WSTOPSIG(wstatus));
printf("__WIFEXITED: 0x%X\n", __WIFEXITED(wstatus));
printf("__WIFSIGNALED: 0x%X\n", __WIFSIGNALED(wstatus));
printf("__WIFSTOPPED: 0x%X\n", __WIFSTOPPED(wstatus));
printf("__WCOREDUMP: 0x%X\n", __WCOREDUMP(wstatus));
}
}
return 0;
}
运行结果:wstatus为0x00 00 00 88,只有最低8位有数据,显然没有退出码,但退出信号为0x8
对应除0错误:
4.退出信息保存的位置
退出信息保存在子进程的PCB中,完整的task_struct的源代码参见OS17.【Linux】进程基础知识(1)文章,这里展示一部分
//......
struct mm_struct* mm;
struct mm_struct* active_mm;
struct address_space* faults_disabled_mapping;
int exit_state;
int exit_code;
int exit_signal;
/* The signal sent when the parent dies: */
int pdeath_signal;
//......
使用wait时,函数会讲将exit_code、exit_signal和exit_state进行位运算合并到wstatus中
5.waitpid等待失败的情况举例
waitpid等待失败的其中一种情况:父进程等待的不是自己的子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
exit(0);
}
else
{
pid_t ret = waitpid(1,NULL,0);//PID=1是init进程
if(ret < 0)
{
perror("wait failed");
}
if (ret == id)
{
printf("wait success\n");
}
}
return 0;
}
或者利用__WIFEXITED(status)宏,具体作用见上方说明
运行结果:
7.参考资料
redhat.com killing-zombies-linux-style
askubuntu what-is-a-defunct-process-and-why-doesnt-it-get-killed
stackoverflow i-used-waitstatus-and-the-value-of-status-is-256-why
有关waitpid的options参数和非阻塞的问题下一篇讲
更多推荐
所有评论(0)