ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OS25.【Linux】进程等待 (上)

OS25.【Linux】进程等待 (上) 目录1.知识回顾2.验证僵尸进程无法用kill杀死3.清理僵尸进程常见的两种方法杀死父进程父进程收尸进程等待定义系统调用wait和waitpid的简介等待单个子进程等待多个子进程如果子进程不退出呢?wait是waitpid的子集解释wstatus参数实验1: wstatus的位图wstatus的位图的详细说明实验2:手动提取wstatus的位实验3: 测试进程异常退出4.退出信息保存的位置5.waitpid等待失败的情况举例6.从task_struct看父子进程之间的联系7.参考资料8.反思: wait和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 processs PID:%d\n,getpid()); exit(0);//父进程不收尸 } else { while(1); } return 0; }运行结果:结论:僵尸进程无法用kill杀死,因为谁也没有办法杀死一个已经死去的进程但只要CtrlC僵尸进程就被清理了,原因在清理僵尸进程常见的两种方法中讲3.清理僵尸进程常见的两种方法杀死父进程上面提到了但只要CtrlC僵尸进程就被清理了,在linuxjournal how-kill-zombie-processes-linux文章有提到:如果父进程无法收尸,那么就要杀死或者重启父进程,而CtrlC就是杀死父进程的一种方式见下图画线句父进程收尸进程等待定义父进程通过进程等待的方式来回收子进程资源和获取子进程退出信息,否则子进程会变僵尸进程,僵尸进程的危害参见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)等价Pid0等待其进程ID与pid形参相等的子进程解释wstatus参数子进程的退出状态(退出状态的定义参见OS23.【Linux】进程终止文章)可以通过wstatus的值知晓pid_t wait(int *_Nullablewstatus);pid_t waitpid(pid_t pid, int *_Nullablewstatus, 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,而是256256是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 0088,只有最低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中其实子进程退出时其实会向父进程发送信号SIGCHLD,这个之后的文章会分析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);//PID1是init进程 if(ret 0) { perror(wait failed); } if (ret id) { printf(wait success\n); } } return 0; }或者利用__WIFEXITED(status)宏,具体作用见上方说明运行结果:6.从task_struct看父子进程之间的联系父进程怎么知道自己的子进程是谁呢?答: 父进程task_struct存储了父进程的所有子进程的指针,即进程间的关系存放在进程描述符task_struct中在《Linux内核的设计和实现 第三版》中的 第3章 进程管理 的 3.2 进程描述符及任务结构 的 3.2.6 进程家族树 中是这样说的:3.2.6 进程家族树Unix 系统的进程之间存在一个明显的继承关系在 Linux 系统中也是如此。所有的进程都是 PID 为 1 的 init 进程的的后代。内核在系统启动的最后阶段启动 init 进程。该进程读取系统的初始化脚本initscript并执行其他的相关程序最终完成系统启动的整个过程。系统中的每个进程必有一个父进程相应的每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个 task_struct 都包含一个指向其父进程 tast_struct、叫做 parent 的指针还包含一个称为 children 的子进程链表。所以对于当前进程可以通过下面的代码获得其父进程的进程描述符struct task_struct *my_parent current-parent;同样也可以按以下方式依次访问子进程struct task_struct *task; struct list_head *list; list_for_each(list, current-children) { task list_entry(list, struct task_struct, sibling); /* task 现在指向当前的某个子进程 */ }init 进程的进程描述符是作为 init_task 静态分配的。下面的代码可以很好地演示所有进程之间的关系struct task_struct *task; for (task current; task ! init_task; task task-parent); /* task 现在指向 init */实际上你可以通过这种继承体系从系统的任何一个进程出发查找到任意指定的其他进程。但大多数时候只需要通过简单的重复方式就可以遍历系统中的所有进程。这非常容易做到因为任务队列本来就是一个双向的循环链表。对于给定的进程获取链表中的下一个进程list_entry(task-tasks.next, struct task_struct, tasks)获取前一个进程的方法与之相同list_entry(task-tasks.prev, struct task_struct, tasks)这两个例程分别通过 next_task(task) 宏和 prev_task(task) 宏实现。而实际上for_each_process(task) 宏提供了依次访问整个任务队列的能力。每次访问任务指针都指向链表中的下一个元素struct task_struct *task; for_each_process(task) { /* 它打印出每一个任务的名称和 PID*/ printk(%s[%d]\n, task-comm, task-pid); }特别提醒 在一个拥有大量进程的系统中通过重复来遍历所有的进程的代价是很大的。因此如果没有充足的理由或者别无他法别这样做。可以看看Linux v6.19.10的task_struct中关于父子进程的相关字段:/* * Pointers to the (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p-father can be replaced with * p-real_parent-pid) */ /* Real parent process: */ struct task_struct __rcu *real_parent; /* Recipient of SIGCHLD, wait4() reports: */ struct task_struct __rcu *parent; /* * Children/sibling form the list of natural children: */ struct list_head children;7.参考资料redhat.com killing-zombies-linux-styleaskubuntu what-is-a-defunct-process-and-why-doesnt-it-get-killedstackoverflow i-used-waitstatus-and-the-value-of-status-is-256-whygnu libc Process-Completion-Status有关waitpid的options参数和非阻塞的问题下一篇讲8.反思: wait和waitpid的作用只是等待子进程退出吗?从本文上方的介绍可以看到,wait和waitpid可以用于父进程等待子进程的退出,但wait和waitpid的作用只是等待子进程退出吗?答: 其实并不是,wait和waitpid也有其它的功能! 手册上是这样写的:wait forstate changesin a child of the calling process,也就是说等待调用进程的子进程发生状态变化(等待即阻塞),可以选择是否获取状态变化如果子进程已经发送状态变化,那么wait或waitpid会立即返回
返回列表