【进程控制】-3-进程等待

进程等待

1、为什么要进程等待

父进程一定要等待子进程。原因有二:

  1. 子进程的僵尸状态需要被回收。

    • 子进程因为某种原因而退出,状态变成z,子进程本身变成僵尸进程。如果不处理僵尸进程就会引发内存泄漏问题。同时由于僵尸进程已经"死了",再怎么执行kill -9 [pid]命令也无法杀死一个已经"死了"的进程。
  2. 子进程的退出信息需要被获取。

    • 父进程需要知道子进程把父进程交付的任务完成得怎么样,所以父进程就必须知道子进程的退出信息。

2、什么是进程等待

进程等待,就是父进程通过系统调用wait waitpid,对已退出的子进程做两件事:

  • 释放子进程的僵尸状态;
  • 获取子进程的退出信息。

2.1、解决僵尸进程问题

我们先来认识系统调用wait。

  • 参数wstatus:wstatus是一种输出型参数。用户在wait外定义一个status然后传status的指针进wait,如果wait执行成功,就会返回子进程的退出信息。在这里,我们暂时不需要获取退出信息,可以传入NULL。
  • 返回值:
    • 执行成功,wait就会返回终止子进程的pid;
    • 执行失败,返回-1。

我们来看一段代码,演示父进程通过wait回收僵尸子进程的全过程:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(1); // 此时子进程还没有,相当于父子共有1秒

    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程。pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            sleep(1);
            cnt--;
        }
        sleep(5);
        printf("子进程退出。\n");
        exit(1); // 子进程循环大约5秒,休眠大约5秒再退出,总共大约10秒
    }

    sleep(15); // 
    // 父进程(没有循环)休眠大约15秒
    // 中间就有5秒空窗期,此时父进程休眠(s),子进程变僵尸(z)

    pid_t rid =  wait(NULL); // 僵尸子进程消失(被回收)
    if (rid > 0)
    {
        printf("父进程等待成功。\n");
        sleep(5);// 父进程大约5秒后也退出
    }

    return 0;
}

运行代码,然后同步监控进程:

这样一来,我们就完成了僵尸进程的回收。

我们也可以得出结论:

  1. 原则上,我们设计代码的时候,必须保证父进程是最后退出的;
  2. 父进程可以通wait,完成对僵尸子进程的回收;
  3. 如果子进程还未退出(还未变成僵尸进程,也就是z状态),而父进程已经执行到了wait系统调用,那么父进程就会阻塞在wait处,直到子进程退出。

对于结论(3),我们可以把上面代码中标记的sleep(15)去掉,再执行代码并同步监控,发现父进程确实阻塞在wait处了。

2.2、获取子进程的退出信息

实际上,

  • wait:只能等待任意子进程。也就是说当有多个子进程存在时,哪个子进程先退出,wait就只会等待哪个进程。
  • waitpid:可以等待指定的进程。

我们先来认识系统调用waitpid:

  • waitpid的参数列表:
    • pid:指定子进程的pid。未来输入指定子进程的pid,父进程就可以等待指定的子进程;而输入"-1",父进程就等待任意的进程,与wait一样。
    • wstatus:输出型参数。未来输入一个int变量(status)指针,waitpid就会提取子进程的退出信息,写入到status中。
    • options:等待进程的方式。等待进程的方式分为默认阻塞等待(0) 、非阻塞等待(WNOHANG)。
  • waitpid的返回值:
    • 目前我们只需要知道,waitpid在默认阻塞等待方式下,如果等待成功,就会返回被等待的子进程的pid(返回值 > 0)。

我们就可以得出:
waitpid(-1, NULL, 0) == wait(NULL)

以后我们就可以使用waitpid等待进程,因为waitpid的选项更多。

2.2.1、status接收退出信号的原理

我们修改上面的代码,然后看看能不能通过新定义的status,拿到子进程的退出码exit(1):

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		int cnt = 5;
		while (cnt)
		{
			printf("我是子进程。pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
			sleep(1);
			cnt--;
		}
		sleep(5);
		printf("子进程退出。\n");
		exit(1);
	}
	
	//sleep(15); //
	int status = 0;
	pid_t rid =  waitpid(id, &status, 0);
	if (rid > 0)
	{
		printf("父进程等待成功。status: %d\n", status); // status为多少?能不能拿到子进程的退出码1?
		sleep(5);
	}
	
	return 0;
}

status结果是256?这是怎么回事?

实际上,int类型的status,其二进制表示的32位中,

  • 左16位不使用;
  • 对于右16位:
    • 右7位:存退出信号------exit signal
    • 第8位:core dump------(我们暂时留个印象)
    • 右8位:存退出码------exit code

我们可以认为,此时status被当作了位图来使用。

那么我们就可以通过对status进行位运算,提取出指定子进程的退出信息:

  • 退出码:(status >> 8) & 0xFF
  • 退出信息:status & 0x7F

但是,对于存储子进程退出信息的status,我们还要自己进行位操作提取退出码和退出信号?未免太麻烦了点?

Linux已经为我们准备好了三个宏,直接用就可以:

宏 含义
WIFEXITED(status) 判断进程是否正常退出,正常为真,异常为假
WEXITSTATUS(status) 若进程正常退出,提取退出码
WTERMSIG(status) 若进程异常退出(因信号终止),提取退出信号

小贴士1


有人会讲:为什么我们不可以定义一个全局变量status(main函数外),当子进程将要结束的时候,就修改status的内容变成子进程的退出码;然后父进程就可以通过全局status拿到子进程的退出码?

答案是不行的,原因是写时拷贝。当子进程企图修改status时,触发写时拷贝,操作系统就会将"父子进程共用一个status变量"的现实,变成"父子进程各有一个物理地址不同的status变量"的现实。那么父进程就不能通过全局status拿到子进程的退出码。

我们也可以小小总结一下:

  • 退出码的取值范围是0, 256。因为status只分配了8的比特位存储退出码,所以退出码就只有 28 = 256 种,即0, 256。

2.2.2、不同情况下,退出信息的获取

2.2.2.1、等待成功

正常退出(但是退出码是自定义的):

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		int cnt = 5;
		while (cnt)
		{
			printf("我是子进程。pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
			sleep(1);
			cnt--;
		}
		sleep(5);
		printf("子进程退出。\n");
		exit(13); // 退出码自定义为13
	}
	
	//sleep(15); //
	int status = 0;
	pid_t rid =  waitpid(id, &status, 0);
	if (rid > 0)
	{
		printf("父进程等待成功。status: %d, exit code: %d, exit signal: %d\n", status, (status >> 8)&0xFF, status & 0x7F);
		sleep(5);
	}
	
	return 0;
}

我们如何演示出异常退出?

我们可以使子进程:

处于正常死循环是,发送9号终止信号

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		while (1) // 死循环(然后发送9号信号)
		{
			printf("我是子进程。pid: %d, ppid: %d\n", getpid(), getppid());
			sleep(1);
		}
	}
	
	//sleep(15); //
	int status = 0;
	pid_t rid =  waitpid(id, &status, 0);
	if (rid > 0)
	{
		printf("父进程等待成功。status: %d, exit code: %d, exit signal: %d\n", status, (status >> 8)&0xFF, status & 0x7F);
		sleep(5);
	}
	
	return 0;
}

除0操作

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		while (1)
		{
			printf("我是子进程。pid: %d, ppid: %d\n", getpid(), getppid());
			int i = 10 / 0; // 除0操作
			sleep(1);
		}
	}
	
	//sleep(15); //
	int status = 0;
	pid_t rid =  waitpid(id, &status, 0);
	if (rid > 0)
	{
		printf("父进程等待成功。status: %d, exit code: %d, exit signal: %d\n", status, (status >> 8)&0xFF, status & 0x7F);
		sleep(5);
	}
	
	return 0;
}

访问野指针

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		while (1)
		{
			printf("我是子进程。pid: %d, ppid: %d\n", getpid(), getppid());
			int *p = NULL;
			*p = 10; // 访问NULL
			sleep(1);
		}
	}
	
	//sleep(15); //
	int status = 0;
	pid_t rid =  waitpid(id, &status, 0);
	if (rid > 0)
	{
		printf("父进程等待成功。status: %d, exit code: %d, exit signal: %d\n", status, (status >> 8)&0xFF, status & 0x7F);
		sleep(5);
	}
	
	return 0;
}
2.2.2.2、等待失败

可以对waitpid输入一个不存在的进程pid:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		int cnt = 5;
		while (cnt)
		{
			printf("我是子进程。pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
			sleep(1);
			cnt--;
		}
		sleep(5);
		printf("子进程退出。\n");
		exit(13);
	}
	
	//sleep(15); //
	int status = 0;
	pid_t rid =  waitpid(id + 1, &status, 0); // 输入一个不存在的pid
	if (rid > 0)
	{
		printf("父进程等待成功。status: %d, exit code: %d, exit signal: %d, rid: %d\n", status, (status >> 8)&0xFF, status & 0x7F, rid);// 顺便看看waitpid的返回值
		sleep(5);
	}
	else
	{
		printf("父进程等待失败。status: %d, exit code: %d, exit signal: %d, rid: %d\n", status, (status >> 8)&0xFF, status & 0x7F, rid);
		sleep(5);
	}
	
	return 0;
}

当然,我们看到了此时waitpid返回值为"-1"。实际上,waitpid如果等到了子进程,会返回子进程的pid;如果等待失败,则返回"-1"。

2.2.2.3、总结

所以,父进程需要获取退出子进程的退出信息,而退出信息的构成:
退出信息 = 退出码 + 退出信号

2.2.3、获取退出信息的原理的简单表述,以及总结

1、子进程退出时,退出信息保存在PCB中

2、父进程"wait"子进程,本质上是获取已退出子进程的退出信息

父进程会遍历自己的所有孩子进程,查找有没有z状态的子进程:

如果找到了z状态的子进程,就读取其退出信息,再拷贝下来,经过一系列转化写入status中。

所以,"父进程等待子进程的大概方式"是:

  • 父进程遍历检查子进程z状态,获取子进程PCB内部记录的退出信息。

3、非阻塞等待

还记得吗?waitpid的第三个参数:

系统调用waitpid的第三个参数options,决定的是进程的等待方式:

  • 输入0:默认的阻塞等待
  • 输入WNOHANG:非阻塞等待

我们再看看Linux文档关于waitpid返回值的描述:

翻译成中文,大致意思是:

  • 成功的情况下,就会返回状态已经改变(成z状态)的子进程,的pid;
  • 如果options参数传入的是WNOHANG,而子进程的状态还未改变,立即返回0;
  • 失败的情况下,返回-1。

对于目前的解释,我们是似懂非懂的。比如"子进程的状态还未改变,立即返回0",我们是不太懂的。

options参数传入的是WNOHANG,就会启用"非阻塞调用",所以我们就必须了解非阻塞调用,以及阻塞调用。

3.1、非阻塞等待的感性认识

对于非阻塞等待,我们有一个小故事:

你是李四。你的班级里有一个叫张三的努力型学霸,张三上课的时候,笔记做得非常好。

快期末了,明天要考《C语言》,你觉得自己啥也没复习,考试可能要完蛋。你想起了张三,还有张三精美的笔记。于是你来到张三所在寝室的楼下:

你来到楼下,向张三打电话,问张三能不能带上自己的C语言笔记,然后和你一起去图书馆帮你复习。张三答应了,不过张三说再等等他,因为张三他要再看会儿书。

这时,李四你并没有一直接电话。你立马挂断了电话,过了一段时间你又给张三打起了电话。而在第一次挂断和第二次打电话的期间,李四你也没闲着,干了这几件事:

  • 装作看看书;
  • 玩玩手机;
  • 来回走两圈看看风景。

就这样,李四你接连打了好几个电话,每次电话挂断到下一次打电话的期间,你都没闲着,干着这几件事:

直到张三看完书,带着笔记下楼和你一起去图书馆。

在这个故事中,

  • 李四------父进程;
  • 张三------子进程;
  • 打电话------调用waitpid
  • 电话里头,张三说等一下,李四不维持着通话状态,立即挂断电话------函数调用立即返回;
  • 李四打一通电话,
    • 有两种结果:
      • 张三答应并马上下楼,与李四一起去自习室;
      • 张三说再等等,李四立马挂电话;
    • 而这两种结果,都有一个共同点:挂电话。所以从打电话到挂电话------一次非阻塞等待(检测 / 调用)。
  • 在一通电话与另一通电话之间的时间窗口内,李四可以做其它的事;同理,在一个非阻塞等待到另一个非阻塞等待之间的时间窗口内,父进程也可以做其它的事。
  • 李四打了很多次电话------非阻塞轮询过程,即非阻塞轮询过程包含了多次非阻塞等待。

3.2、阻塞等待的感性认识

对于阻塞等待,我们可以继续刚刚的小故事:

学校《C语言》考试结束了。李四你很庆幸自己还好有张三帮忙复习,C语言考试你有十足的把握。

可是,李四你听到同学的闲聊,听到明天又有《操作系统》考试。于是你赶紧又跑到张三楼下,请求张三带上他的笔记,和你到图书馆自习:

李四你向张三打电话,张三在电话答应了你的请求,然后果不其然,张三说再等等他,他要看10min书。

但是,这一次张三你没有立即挂断电话,而是要求张三一直保持通话,直到张三真正下楼才挂断。而在此期间,你什么也没干,就是守着电话。

那么,这一整个打电话到挂电话的过程------阻塞等待。

所以,非阻塞等待与阻塞等待的一大区别是:

等待方式 区别描述
非阻塞等待 父进程等待子进程期间,可以做其他事
阻塞等待 父进程等待子进程期间,不能做其他事

3.3、非阻塞等待的代码

我们可以改造上面的代码,设计出一个进行"非阻塞等待"的过程。不过有两个细节:

  • 我们需要设计父进程非阻塞等待过程中做的其他事。比如我们定义一些行为函数,然后借助函数指针:
    • 定义函数指针;
    • 定义函数指针数组;
    • 非阻塞等待时,就可以通过访问函数指针数组里的函数指针,从而回调函数。
  • 由于非阻塞等待下waitpid认为子进程还未改变状态,就直接返回"0"结束了,所以我们就必须循环重复非阻塞等待,直到父进程等待成功。
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>

void Printlog()
{
	printf("我要打印日志。\n");
}
void SyncMySQL()
{
	printf("我要访问数据库。\n");
}
void Download()
{
	printf("我要下载核心数据。\n");
}

typedef void(*task_t)();

task_t task[3] = {
	Printlog,
	SyncMySQL,
	Download
}; 

int main()
{
	printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
	sleep(1);
	
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程
		int cnt = 5;
		while (cnt)
		{
			printf("我是子进程。pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
			sleep(1);
			cnt--;
		}
		sleep(15);
		printf("子进程退出。\n");
		exit(13);
	}
	
	int status = 0;
	while (1)
	{
		pid_t rid =  waitpid(id, &status, WNOHANG);
		if (rid > 0)
		{
			printf("父进程等待成功。rid: %d\n", rid);
			sleep(1);
			if (WIFEXITED(status))
				printf("子进程正常退出。exit_signal: %d, exit_code: %d\n", WTERMSIG(status), WEXITSTATUS(status));
			else
				printf("子进程异常退出。exit_signal: %d\n", WTERMSIG(status));

			exit(1);
		}
		else if (rid == 0)
		{
			printf("父进程非阻塞等待中。rid: %d\n", rid);
			for (int i = 0; i < 3; ++i)
				task[i]();
			sleep(1);
		}
		else
		{
			printf("父进程等待失败。rid: %d\n", rid);
			exit(1);
		}
	}
	sleep(1);
	
	return 0;
}

3.4、总结

目前我们的最佳实践是,有阻塞等待就够了。

4、编写代码,以演示多进程等待

编写代码,以创建一批子进程,然后全部管理起来,再依次等待。为了更好地管理,我们可以使用C++的标准库。

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int g_num = 5;

void Work()
{
    for (int cnt = 0; cnt < g_num; ++cnt)
        printf("我要开始工作了。cnt: %d\n", cnt);
}

int main()
{
    printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(1);
    std::vector<pid_t> children; // 存的东西是pid,类型要匹配

    for (int i = 0; i < g_num; ++i)
    {
        sleep(1);

        pid_t id = fork();
        if (id < 0)
        {
            perror("fork failed.\n");
            exit(1);
        }
        else if (id == 0)
        {
            //children.push_back(getpid()); // 不要这样入,因为有写时拷贝,父进程的children才需要存pid
            printf("我是子进程。pid: %d, ppid: %d, i: %d\n", getpid(), getppid(), i);
            Work();
            printf("我要退出了。pid: %d\n", getpid()); // 子进程可以做一点任务
            exit(1);
        }
        else
        {
            children.push_back(id);
        }
    }

    sleep(3);

    int status = 0;
    for (size_t i = 0; i < children.size(); ++i)
    {
        pid_t rid = waitpid(children[i], &status, 0);
        if (rid > 0)
        {
            printf("父进程等待成功。rid: %d\n", rid);
            sleep(1);
            if (WIFEXITED(status))
                printf("子进程正常退出。子进程的pid: %d, exit_code: %d\n", children[i], WEXITSTATUS(status));
            else
                printf("子进程异常退出。子进程的pid: %d, exit_signal: %d\n", children[i], WTERMSIG(status));
        }
        else if (rid < 0)
            printf("父进程等待失败。rid: %d\n", rid);

        sleep(1);
    }

    return 0;
}

这里有几个细节:

  1. 差错处理;
  2. 父进程for循环创建下一个子进程之前,可以sleep停顿一下,以保证前一个子进程的工作做完;
  3. 父进程创建完子进程时,可以再sleep停顿一下,以保证所有子进程都完成了任务;
  4. 其实子进程退出之前,也可以
  5. 用vector记录退出子进程的pid时,必须是父进程记录,不能是子进程,因为有写时拷贝。

小贴士2


我用的vim配置好像有点奇怪(好多红叉,尽管代码是对的)。也许我该花时间了解一下vim的不同配置?但也没必要?因为已经有了AI和vs code?

相关推荐
_upupup1 小时前
包管理器和vim
linux·服务器
一颗小树x2 小时前
NVIDIA 驱动故障排查实录:内核升级导致 nvidia-smi 失效
linux·服务器·nvidia驱动·启动失败·内核升级
_smart_boy__2 小时前
I.MX6U开发板Uboot无法ping Ubuntu问题解决方案(二)
linux·运维·ubuntu
DianSan_ERP2 小时前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化
Android系统攻城狮2 小时前
Linux Gstreamer深度解析之gst_audio_decoder_get_parse_state调用流程与实战(四十九)
linux·音视频·gstreamer音视频·gstreamer音视频进阶
~光~~2 小时前
【嵌入式Linux学习】GFP_NOIO / GFP_NOFS(Linux 内核 gfp 分配标志)
linux·运维·学习
坤坤子吖2 小时前
ELF加载、动态链接、GOT与PLT
linux·笔记·学习
android阿杜3 小时前
Ubuntu 26.04 LTS 系统每次重启后屏幕亮度都会增加一点
linux·运维·ubuntu
懿路向前3 小时前
【唤醒实战笔记】2026-09-29 | 智能体提示词迭代史(v1 → v8)
linux·笔记·算法