当前位置:首页>Linux>OS25.【Linux】进程等待 (上)

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

  • 2026-10-11 05:51:29
OS25.【Linux】进程等待 (上)

目录

1.知识回顾

2.验证僵尸进程无法用kill杀死

3.清理僵尸进程常见的两种方法

杀死父进程

父进程收尸

进程等待

定义

系统调用wait和waitpid的简介

等待单个子进程

等待多个子进程

如果子进程不退出呢?

wait是waitpid的子集

解释wstatus参数

实验1: wstatus的位图

wstatus的位图的详细说明

实验2:手动提取wstatus的位

实验3: 测试进程异常退出

4.退出信息保存的位置

5.waitpid等待失败的情况举例

6.从task_struct看父子进程之间的联系

7.参考资料


1.知识回顾

参见OS20.【Linux】进程状态(2) 僵尸进程、孤儿进程和进程优先级文章复习

2.验证僵尸进程无法用kill杀死

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(){	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>intmain(){    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>voidrun_child(){    printf("子进程的pid: %d, ppid:%d\n", getpid(), getppid());}voidcollect_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);        }    }}intmain(){    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>intmain(){    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>intmain(){    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>intmain(){    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中

其实子进程退出时其实会向父进程发送信号SIGCHLD,这个之后的文章会分析

5.waitpid等待失败的情况举例

waitpid等待失败的其中一种情况:父进程等待的不是自己的子进程

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/wait.h>intmain(){    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)宏,具体作用见上方说明

运行结果:

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-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

gnu libc Process-Completion-Status

有关waitpid的options参数和非阻塞的问题下一篇讲

原文首发于CSDN,点击阅读全文即可查看

最新文章

随机文章