Linux-进程控制

**摘要:**本文系统讲解 Linux 进程控制的核心机制,围绕进程创建、进程终止与进程等待三大主题展开。首先介绍 fork 函数的基本用法、返回值与写时拷贝原理,并分析 fork 的常规使用场景及调用失败原因;随后深入探讨进程退出的三种场景、退出码的含义以及 return、exit、_exit 三种退出方式的区别,并结合缓冲区刷新机制说明库函数与系统调用的差异;最后重点讲解进程等待的必要性与实现方法,包括 wait 与 waitpid 的用法、退出码与退出信号的解析、阻塞等待与非阻塞轮询的对比,并通过 C/C++ 代码示例演示如何正确回收子进程资源、获取子进程退出信息,帮助读者彻底理解并掌握 Linux 下进程控制的完整流程。

目录

1.进程创建

1.1fork函数初识

1.2fork函数返回值

1.3写时拷贝

1.4fork的常规用法

1.5fork调用失败的原因

2.进程终止

2.1背景

2.2进程退出

2.3进程退出场景

2.4退出码

3.进程等待

3.1进程等待必要性

3.2进程等待定义

3.3进程等待的方法

3.3.1wait方法

3.3.2waitpid方法

3.3.2.1waitpid基本用法

3.3.2.2waitpid进程异常用法

3.3.2.3结合C++使用waitpid

3.2.2.4waitpid最佳实践

3.3.3阻塞等待与非阻塞等待


1.进程创建

1.1fork函数初识

在Linux中我们创建子进程需要使用fork函数,如:

cpp 复制代码
#include <unistd.h>
pid_t fork(void);

fork函数会从已存在的进程中创建一个新进程。新进程为子进程,而原进程为父进程。

fork函数有两个返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1。

进程调用fork,当控制转移到内核中的fork代码后,内核做:

①分配新的内存块和内核数据结构给子进程;

②将父进程部分数据结构拷贝至子进程;(子进程以父进程为模板)

③添加子进程到系统进程列表中;

④fork返回,开始调度器调度。

当一个进程fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但是可能会因为if或else来执行两段不同的代码!

1.2fork函数返回值

①给子进程返回0(代表已经成功创建子进程);

②给父进程返回子进程的pid。

1.3写时拷贝

在程序地址空间博客中我们了解到,通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,类似这样:

其中的虚拟内存就是之前的虚拟地址空间,这个图相当于之前博客中的简化版本了!

但是对于父进程来讲,实际上的数据段是可读可写的:

当fork创建子进程的时候,父进程的代码段权限不变,因为本来是只读的;但对于数据段,操作系统会对父子进程的页表当中数据段的权限由可读可写改为只读,而子进程的页表从父进程来,因此也认为为只读的!

当父子进程中有一个进程尝试写入,此时操作系统会"报错"。此时操作系统要对当前情况进行判断分类:①真的是野指针(虚拟地址空间中不存在该地址)?真的报错,终止进程;②对于本来可以访问的区域,只是权限不允许,操作系统会:a.写时拷贝;b.更改回权限(把父子进程的数据段的权限都改为:可读可写)

问题1:为什么创建子进程之后,直接把数据分开呢?直接拷贝不就可以了,为什么要写时拷贝?

问题2:为什么要拷贝,直接开辟对应的空间不就好了?

回答问题1:①我们确实可以创建子进程之后把数据分开,并把数据拷贝过来,但是没必要,假设父进程的数据有1GB,那么子进程不可能把这1GB数据全部拷贝过来,浪费了系统的时间还浪费了空间,我们的写时拷贝本质上是一种:按需获取 。如果直接全部都拷贝,那么代价是很大的!②当数据没有修改时,就先不拷贝,先把这多余的空间给其他进程使用更好,它也是一种惰性申请,提高了内存的使用率!因此采用写时拷贝!

回答问题2:假设我们写入不是覆盖式写入,如:a = 20,而是在原来基础上对数据进行修改,如:a++,所以我们需要拷贝,不能单纯开辟空间!

扩展问题:从今天开始,在C/C++上malloc or new的时候,需要在物理内存里面开辟空间吗?

实际上,我们使用malloc或者new的时候,只需要开辟虚拟地址空间即可,当真正想用那块申请的地址空间的时候,操作系统才进行内存级的申请,此时才会在物理内存开辟空间,并构建映射关系,整个过程对用户来讲,完全透明,也就是说用户不知道,也不需要知道。

在malloc或new至内存级申请的时候,这个过程叫做:缺页中断!(malloc或者new本质上也是一种惰性空间开辟)

1.4fork的常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。如:父进程等待客户端请求,生成子进程来处理请求;

一个进程要执行一个不同的程序。例如:子进程从fork返回之后,调用exec函数。(让子进程执行全新的程序)

1.5fork调用失败的原因

创建一个子进程,就需要创建进程PCB等,要消耗内存,我们可以创造无限个子进程,但是内存是有限的,只要创建达到上限,就一定会出现失败的情况!

①系统有太多的进程;

②实际用户的进程数超过了限制。(对某些用户只能创建有限个进程)

2.进程终止

2.1背景

首先提出问题:

①进程终止,操作系统要做什么?

我们知道进程创建是需要创建PCB等,那么进程终止就是进程创建的反过程,在进程状态我们讲到,当进程终止时,不能直接释放空间,需要知道执行结果,因此,我们需要先把进程置为Z状态(僵尸状态),此时需要保留进程的PCB,而类似进程地址空间和页表这类的空间都需要释放!直到其父进程回收了该进程的PCB才会完全释放!这个都是之前在进程状态讲过的,因此不做过多讲解(想要更多了解,见博客:Linux-进程状态)

②在main函数中要return 0,其中return 0是什么意思?给谁了?

我们写下如下代码:

cpp 复制代码
#include<stdio.h>

int main()
{
    int a = 10;
    int b = 20;

    int c = a + b;
    printf("%d+%d=%d\n",a,b,c);
    return 0;
}

我们很容易知道,该代码的运行结果为:

为什么我们会在main函数里写return 0,为什么不return 1/2/3?

return 0,我们叫做:进程退出时的退出码 ,当我们return 0时,未来会被"系统"获得,未来系统会拿这个退出码来辨别当前进程的执行情况。

怎么证明呢?

如果我们把return值改为2026,即把代码改为:

cpp 复制代码
#include<stdio.h>

int main()
{
    int a = 10;
    int b = 20;

    int c = a + b;
    printf("%d+%d=%d\n",a,b,c);
    //return 0;
    return 2026;
}

运行完后有:

如何看到这个2026呢?

使用

bash 复制代码
echo $?

这个指令的意思是:在系统层面上查询最近一个命令行在转化为进程的时候的退出码,因此结果为:

为什么不为2026?我们改为其他的数,如:115,运行有:

这是因为:

Linux Shell(Bash)规定,退出状态码(Exit Code)的有效范围是 0~255。当你返回 2026 时,系统只取了它的"低 8 位"数值,即 2026 % 256 = 234

为什么为234?

在 Linux 内核中,exit() 系统调用只接收一个 int 类型的状态值,但父进程(Shell)通过 waitpid() 获取子进程终止状态时,只会读取 status 变量的低 8 位 (即 0-255)。

计算过程如下:

  • 2026 的二进制2026 = 7 * 256 + 234

  • 取低 8 位2026 & 0xFF = 234

所以,当你 return 2026; 时,Shell 实际收到的退出码是 234 。这就是你第一次看到 234 的根本原因,而不是因为"旧程序残留"。

为什么第二次为115?

因为在第二次修改代码时,改成了 return 115;

  • 115 小于 255,在有效范围内。

  • 所以低 8 位就是 115 本身,Shell 直接收到了 115

我们可以通过return 256,它最终结果为0:

回到最开始的问题,我们知道echo a它会打印a变量的值,也就是说echo ?它会打印?变量的值,也就是说,当我们return返回的值,会储存在?这个变量里面,我们只要知道?会保存进程的退出码。

但是我们如果多查一次(把代码改为return 115):

为什么第二次查的时候为0?

因为echo ?也是一个命令,第二次查的时候,它查的是echo ?的退出码,由于它能够成功返回,因此,它会return 0,所以return 0,代表:程序成功执行,因此,C/C++都会reuturn 0代表程序成功运行结束!

为什么一定要有退出码?

我们创建这么多子进程的目的就是让子进程给我们父进程办事,子进程把父进程给子进程的事情办得怎么样,父进程得知道,因此,需要有退出码,通常退出码为0,代表成功了。但一旦退出码不为0,代表失败,但是代表失败的退出码有很多 ,而且失败我们还需要原因,因此,非0的时候,还需要进一步知道失败的原因!1,2,3,4,5......代表不同的失败原因!

因此,我们怎么知道,a + b 是否等于c,因此,我们可以这样写:

cpp 复制代码
#include<stdio.h>

int main()
{
    int a = 10;
    int b = 20;

    int c = a + b;
    //printf("%d+%d=%d\n",a,b,c);
    if(c == 30)
        return 0;
    else
        return 1;
    //return 0;
    //return 2026;
    //return 115;
    //return 256;
}

因此,我们可以这样知道程序是否成功执行:

虽然./a.out没有输出结果,但是我们仍然可以通过echo $?来知道是否程序成功运行!为0,代表成功运行;非0,代表没成功运行。

但是我们怎么知道非0代表哪些错误信息呢?

C/C++语言已经提供了一些内置的错误原因。在C语言里,提供了一个全局变量errno,系统提供了一个strerror来查看错误信息,我们可以用:

bash 复制代码
man strerror

来查看对应的详细信息:

翻译一下有:

其中,我们可以在C语言使用char *strerror(int errnum);函数来知道错误码对应的信息,我们传入一个数字,它会返回错误信息,虽然我们不知道1代表什么意思,但是我们可以用这个函数来知道对应的错误信息,如:

cpp 复制代码
#include<stdio.h>
#include<string.h>

int main()
{
    int i = 0;
    for(;i<256;i++)
    {
        printf("%d:%s\n",i,strerror(i));
    }
    //int a = 10;
    //int b = 20;

    //int c = a + b;
    ////printf("%d+%d=%d\n",a,b,c);
    //if(c == 30)
    //    return 0;
    //else
    //    return 1;
    //return 0;
    //return 2026;
    //return 115;
    //return 256;
}

运行有:

实际上Linux的错误码一共有133个,退出码一共有256个(之前说过)。

因此我们可以这样使用:

由于当前目录下不存在这个ocweoro文件,因此,会提示错误信息:cannot access 'ocweoro': No such file or directory,然后我们可以通过echo $?得到退出码是2,2代表:

因此ls的错误信息是根据自己的实际情况来提示的,并不是完全一样!

因此,main函数中return退出码,它会提示程序的运行成功与否,退出码范围为0,255。实际上退出码在进程的PCB当中,因此在Linux内核中有int exit_code:

实际上,操作系统获得到进程的退出码后,会把这个退出码写到进程PCB里,未来父进程会获得这个退出码的(bash)!

2.2进程退出

进程退出只有三种情况:

①代码运行完毕,结果正确;

②代码运行完毕,结果不正确;

③代码异常终止。

对于①②代码运行完毕,看结果是否正确,我们可以使用退出码来判断(我们在return 0之前先做一下判断)。

对于③,退出码本身有意义吗?

这就好比,如果有人考试投机取巧去作弊,那么它的考试成绩有意义吗?那肯定是没有的,因此退出码本身没意义,因此,我们需要把重点放在:为什么会异常,即代码哪里有问题上面,这个话题会在"进程等待"里进行讲解!因此,操作系统要知道为什么会异常,操作系统知道异常后一般会把该进程杀掉,操作系统一般通过信号来杀掉这个进程(如:kill -9),因此,我们需要把重点放在信号编号上了!

2.3进程退出场景

我们在myproc.c里写下如下代码:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("hello world!\n");
    sleep(1);
    return 1;
}

运行后有:

因此,进程退出方法1:main函数进行return n;n表示该进程的退出码

还有方法2,使用exit函数,我们可以用:

bash 复制代码
man 3 exit

来查看具体信息:

翻译一下有:

exit函数能使一个正常的进程退出。其中:

cpp 复制代码
void exit(int status);

这个status等同于main函数的return status,也就是说:status就是退出码

因此,我们可以把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
    printf("hello world!\n");
    sleep(1);
    //return 1;
    exit(2);
}

此时我们查看执行并查看退出码有:

因此,进程退出方法2:直接调用exit(n);n表示该进程的退出码。

在系统中也存在一种系统调用,叫做_exit,其中_exit也是引起调用进程终止,谁调用该函数,调用该函数的进程就会终止,使用:

bash 复制代码
man 2 _exit

来查看对应的信息:

翻译过来有:

因为它在二号手册(man n xxx)中的n代表它在n号手册,它的参数status和exit一模一样,即status就是进程退出的退出码。

方法3:直接调用_exit(n);

如:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
    printf("hello world!\n");
    sleep(1);
    //return 1;
    //exit(2);
    _exit(3);
}

运行并查看进程退出码有:

进程退出不止这几种方法,更多方法在进程信号中会讲到!

问题1:return 方式和 exit 方式区别?

问题2:exit 方式和 _exit 方式区别?

回答问题1:

除了main函数外,每个函数都会return,return语句通常表示函数调用结束,只是main函数return表示进程退出;exit表示的是进程结束,在代码中任意地方调用,都会导致进程退出!

我们可以把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
void print()
{
    printf("hello print()\n");
    return;
}
int main()
{
    printf("我的进程开始运行了\n");
    sleep(1);
    print();
    printf("我的进程正常运行结束了\n");
    return 4;
}

那么此时的代码运行路线为:

此时的运行结果为:

此时的退出码为:

因此,因为会执行print()语句后面的代码(实际上print()可以替换为打印一行并return),因此可以证明,return只是函数调用结束,并不会结束进程!

但是我们代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void print()
{
    printf("hello print()\n");
    //return;
    exit(10);
}
int main()
{
    printf("我的进程开始运行了\n");
    sleep(1);
    print();
    printf("我的进程正常运行结束了\n");
    return 4;
}

此时的运行结果和退出码为:

这就说明,任意地方调用exit都会导致进程退出!

因此,终止进程最佳实践:exit

回答问题2:

我们把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    printf("我是一个进程\n");
    exit(1);
}

我们很容易知道,此时的退出码为1:

如果我们把main函数的exit改为_exit有:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    printf("我是一个进程\n");
    //exit(1);
    _exit(1);
}

那么此时的退出码也为1:

这些都没有问题。

如果我们把main函数的printf语句去掉\n,我们知道**,由于缓冲区的存在,"我是一个进程"这个语句并不会很快显示出来,而是先在缓冲区里,但是因为我们后面直接执行了exit,因此,输出的字符串会在缓冲区暂存下来,只有当我们return时或者我们主动用fflush强制刷新出来才能看到**,因此我们可以多休眠几秒验证一下,代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    //printf("我是一个进程\n");
    printf("我是一个进程");
    sleep(3);
    exit(1);
    //_exit(1);
}

如果我们运行,最开始为:

等进程结束时为:

因此,我们得到一个结论:输出缓冲区,在进程结束时,会自动被刷新!

当然我们在main函数中return也是一样的效果!

但是,如果我们把exit改为_exit呢?把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    //printf("我是一个进程\n");
    printf("我是一个进程");
    sleep(3);
    //exit(1);
    _exit(1);
}

此时我们运行最开始为:

但是到最后为:

我们发现,没有输出结果啊!

exit和_exit的区别是:一个是C库函数;一个是系统调用。其中exit会刷新缓冲区,而_exit直接终止进程,不会刷新缓冲区!

虽然我们最好使用exit函数,但是_exit不是要我们在代码中使用的,而是为后面做准备!库函数和系统调用是有上下层关系的,即库函数在上,系统调用在下**,系统调用本质是操作系统内为了能够保证自己的安全,又能为上层提供对应的服务,因此系统调用就是像函数一样的东西可以进行调用**。

问题:终止进程是不是在修改内核数据的相关代码?

进程终止时,需要把当前进程的PCB从调度队列中移除并释放空间,因此,进程终止,必定要调用系统调用,必须要让操作系统完整真正的进程删除退出

也就是说,exit也必须要调用系统调用,我们能大胆猜测,exit底层封装了_exit!!!因为只有封装了_exit,才能完成真正的进程终止!

那么输出缓冲区在哪里?

虽然我不知道缓冲区在哪,但是我知道它一定是一段内存空间,将来打印的东西写入到该空间,等刷新的时候再提取出来。

反过来问,这个缓冲区,一定不在哪里?

一定不在操作系统内部的缓冲区!因为如果在,那么不管调用的是exit还是_exit,刚刚的"我是一个进程"这个字符串都应该刷新到显示器上,但是_exit没有!因此,所说的缓冲区在库里面,因此,输出缓冲区在库缓冲区里!

我们知道,printf打印的时候,往往像stdout那样标准输出去打印,在C语言中我们讲过,一个程序启动时,默认会打开三个输入输出流:stdin,stdout,stderr。因此,printf默认是往标准输出即stdout上面打。

如,我们使用指令:

bash 复制代码
man 3 printf

能看到:

我们注意到还有一个fprintf函数,其中的FILE* stream就是传入一个FILE文件,表示我们打印该在哪里打印,如:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void fprint()
{
    fprintf(stdout,"hello fprint()\n");
    return;
}
int main()
{
    fprint();
    return 0;
}

此时代码的运行结果为:

其中printf打印的结果是往stdout文件流中去打印。可以:

bash 复制代码
man stdout

看到stdout详细信息:

也就是说,显示器也是一个文件,因此,显示器就是文件

而FILE是C语言提供的结构体,那FILE在哪里?

而这个FILE在每个返回值为FILE*类型的函数里面会定义这个FILE,且这个FILE是**C函数自己申请的,自己维护的!因此,FILE可以存在一个很大的内存空间,**因此,实际上,stdout是char outbuffer1024;所以本质上的stdout就是一个数组,因此当我们printf的时候,打印到的缓冲区就是这个FILE里面了,本质上的缓冲区就是数组!

当我们在C++里写输入cin,输出cout也是有缓冲区的,其中cout也有一个缓冲区。

后面的需要等到后面讲解!

2.4退出码

Linux Shell中主要的退出码:

3.进程等待

3.1进程等待必要性

进程等待的必要性本质就是为什么要进行进程等待的问题:

①之前我们说过,如果子进程退出,父进程不管子进程,就可能造成"僵尸进程"的问题,进而导致内存泄漏。

②另外,进程一旦变成僵尸状态,那就刀枪不入,连"杀人不眨眼"的kill -9也无能为力,因为谁也没有办法杀掉已经死去的进程;

③最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是 不对,或者是否正常退出。

④⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息

如何评定子进程把任务完成的怎么样?

因为有退出码,因此我们要父进程得到子进程的退出码,才能知道任务完成得怎么样。

因此为什么要进行进程等待:

①通过进程等待,我们希望回收子进程的僵尸状态,从而回收子进程所占用的资源;(必要功能)

②如果需要,我们需要获取子进程的退出信息,来判断子进程把任务完成得怎么样。(可选功能)

3.2进程等待定义

什么叫做进程等待?

进程等待就是让父进程通过等待的方式,回收子进程PCB,Z,如果需要,获取子进程的退出信息。

如何进行进程等待?

这个时候就需要用很多方法了!

3.3进程等待的方法

3.3.1wait方法

我们可以使用系统调用wait,用:

cpp 复制代码
man 2 wait

来查看具体信息:

cpp 复制代码
pid_t wait(int *wstatus);

wait可以用来进行进程等待,也可以用来获取进程退出码,但是我们现在只用它来验证解决僵尸问题。

wait这个函数代表:我们要等待任意一个进程,父进程调用wait表示:父进程等待任意一个子进程:①如果子进程没有退出,父进程wait的时候,就会阻塞(父进程一直等子进程结束后才继续执行后续代码,期间什么事情都不干);②如果子进程退出,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题。

wait函数在解决子进程后,会返回等待的子进程的pid,但是只有等待成功的时候,才会返回子进程的pid!如果等待失败,则返回-1。

对于它的参数,我们暂时设定为NULL,后面会用新的方法来进行进程等待。

我们把代码改为:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int i = 7;
        while(i)
        {
            printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
            sleep(1);
            i--;
        }
        printf("子进程退出了\n");
        exit(115);
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    pid_t rid = wait(NULL);
    if(rid > 0)
    {
        printf("父进程等待子进程成功,子进程pid:%d\n",rid);
        sleep(1);
    }
    return 0;
}

然后我们新开一台机器,执行如下代码:

bash 复制代码
while :; do ps ajx | head -1 && ps ajx | grep myproc; sleep 1; done

运行有:

如果想要看到子进程变成僵尸后,再进行回收,我们需要先把子进程的执行时间缩短(不然太多循环的话开始的结果和最后的结果不在一个界面),然后让父进程开始就直接休眠比子进程所有代码执行后还有富余的时间即可,如:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int i = 4;
        while(i)
        {
            printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
            sleep(1);
            i--;
        }
        printf("子进程退出了\n");
        exit(115);
    }
    //printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(6);
    pid_t rid = wait(NULL);
    if(rid > 0)
    {
        printf("父进程等待子进程成功,子进程pid:%d\n",rid);
        sleep(1);
    }
    return 0;
}

然后此时我们执行代码有:

我们可以看到,在执行4秒后,子进程(pid为3115)变成僵尸进程,在执行6秒后,父进程启动,发现子进程已经结束,因此直接执行,不会进行等待了!

因此,我们可以使用wait来回收子进程的僵尸状态!

但是如果有多个进程怎么办,如何回收多进程?

由于wait是等待任意一个子进程结束,所以我们不仅需要创建多个子进程,还要wait等待多次!假设创建10个子进程,那么就需要循环10次wait才能全部回收子进程!

因此,代码改为(循环次数太多,可适当改少):

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(i)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            exit(115);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        pid_t rid = wait(NULL);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d\n",rid);
            //sleep(1);
        }
    }
    return 0;
}

有如下结果:

我们观察发现子进程退出后,会全部变成Z状态,然后父进程会依次回收创建的子进程(虽然不是按照打印顺序来,但是后面回收子进程的顺序是有序的)。

因此,多进程中,往往父进程最先创建,最后退出,因为它要把所有子进程回收完成!

可是wait不够,还有其他方法,如:waitpid,它的功能更强大!

3.3.2waitpid方法

我们仍然可以通过使用:

bash 复制代码
man 2 wait

来看到waitpid函数:

实际上waitpid函数是未来我们实现进程等待的最佳实践

cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
    当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
    如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
    如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
    pid:
        Pid=-1,等待任⼀个⼦进程。与wait等效。
        Pid>0.等待其进程ID与pid相等的⼦进程。
    status: 输出型参数
        WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
        WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
    options:默认为0,表⽰阻塞等待
3.3.2.1waitpid基本用法

如果我们传递的pid为-1,那么就会等待任意一个子进程,和wait一样的效果;如果传递的pid大于0,那么就等这个pid的子进程。options:选项,我们默认设为0,也是最常见的,默认阻塞等待。如:scanf当我们键盘没有输入时,它就会进入进程的等待队列里,此时进程就阻塞了。因此,进程不仅仅可以等待进程资源就绪,还可以等待软件资源就绪!如果选项设为其他数字,那么就处于其他状态,之后再说。返回值:pid_t,如果返回成功了,会返回子进程的PID;如果失败了,返回-1。

waitpid的第二个参数status等同于wait的参数status,这个参数是输出型参数,我们传递的int类型的地址,将来操作系统会把子进程的退出信息,写到status里,然后我们就可以通过实参看到对应的信息。

输出型参数:它不以把参数传递给函数为目的,它的目的是通过参数的传递来带出来东西,即把该函数内部的数据带出来。

因此,我们可以把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(i)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            //exit(115);
            exit(1);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        //pid_t rid = wait(NULL);
        int status = 0;
        pid_t rid = waitpid(-1,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d\n",rid,status);
            //sleep(1);
        }
    }
    return 0;
}

我们把退出码改为了1,并且把wait函数改为了waitpid函数,但是这个waitpid函数功能和wait一样,因此运行有:

我们观察一下结果,在创建完所有进程之后,子进程执行2秒后退出,但是我们发现:所有的子进程的status是256,不是1,我们刚刚写的代码明明是exit(1)啊!

**这是因为status不仅仅是退出码!**status是整数,int类型,它占32bit,它的高16位不考虑,即它最前面的16个比特位不使用!如果程序(子进程)正常终止,其中剩余的次低8位即第8-15个比特位叫做当前进程的退出状态,也就是子进程的退出码。即:如果一个子进程正常终止,它的32个比特位只有低16位即第0-15比特位会使用,而低16位中的次低8位叫做子进程的退出码。

所以,一般而言,进程的退出码取值范围为0,2550,2\^8-1)。(进程正常结束的退出码范围)

**如果一个进程被信号所杀掉或进程异常退出,它的次低8位依旧表示进程的退出码,但是被信号杀掉了进程,它的进程退出码没意义,所以可以不用管(如:一件事情完成了当然没事,如果中途被终止了,那么最后的结果是怎么样就没任何意义了)。而被信号所杀,那么此时的低7位代表它是被几号信号杀掉的,即表示信号编号。**还有一位代表core dump标志,当前没法讲解。

我们分析一下代码,在子进程退出的时候,它的退出码为1,因此它的次低8位为0000 0001,因为它是正常终止的,所以没有信号编号,因此为0000 0000:

因此,此时status即为(前16位)0000 0001 0000 0000,因此此时转化为十进制为2^8 = 256。

因此,如果我们想要获取子进程的退出码,可以直接对status右移8位即:

cpp 复制代码
status >> 8;

然后我们再提取最低8位:

cpp 复制代码
(status >> 8) & 0xFF

其中0xFF转化为二进制为1111 1111。

所以我们要把真正的退出码得到,可以把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(i)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            exit(115);
            //exit(1);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        //pid_t rid = wait(NULL);
        int status = 0;
        pid_t rid = waitpid(-1,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d\n",rid,status,(status>>8)&0xFF);
            //sleep(1);
        }
    }
    return 0;
}

此时的运行结果为:

3.3.2.2waitpid进程异常用法

但是这是进程正常的情况,但是如果进程异常了呢?

为什么会出现异常呢?要么就是除0或野指针,导致进程中断,但这些只是基本原因。根本原因是:因为出现了问题,导致了操作系统给进程发信号了!

在Linux中我们可以通过:

bash 复制代码
kill -l

查看所有的信号:

仔细观察这些信号,没有0号信号。(因为0号信号代表程序正常运行结束)

如果我想拿到进程的退出信号,因为进程退出时的信号编号会存放在status低7位中,因此可以用这种方式来获取信号编号:

cpp 复制代码
status&0x7F;//0x7F:0111 1111

因此,如果进程正常结束,那么status表示信号的数字 == 0 就行!

因此,我们需要先判断进程正常结束才能再判断退出码,由二者结合才能判断进程终止的三种情况中的哪一种!

因此,我们用两个数字来代表子进程的执行 情况:

1.进程退出信号;2.进程退出码。

因此,完善的代码为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(i)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            //exit(115);
            //exit(1);
            exit(0);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        //pid_t rid = wait(NULL);
        int status = 0;
        pid_t rid = waitpid(-1,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
            //sleep(1);
        }
    }
    return 0;
}

代码运行结果为:

如果我们想要进程异常退出,可以使用kill -9 xxx,来杀掉进程pid为xxx的进程。

所以我们可以让这个子进程的循环为死循环,然后随机杀掉一个进程即可,代码为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            //while(i)
            while(1)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            //exit(115);
            //exit(1);
            exit(0);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        //pid_t rid = wait(NULL);
        int status = 0;
        pid_t rid = waitpid(-1,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
            //sleep(1);
        }
    }
    return 0;
}

此时我们把代码运行起来:

此时父进程创建完子进程就一直在等待,我们可以在另外一台机器上执行(可以把1831换成任意一个子进程的pid):

bash 复制代码
kill -9 1831

然后观察该机器有:

因为1831进程是被9号信号导致的退出,所以才会显示退出信号为9!

除此之外,我们还可以在代码中进行除0操作,也能导致异常有:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            //while(i)
            while(1)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(3);
                int a = 10;
                a /= 0;
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            //exit(115);
            //exit(1);
            exit(0);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        //pid_t rid = wait(NULL);
        int status = 0;
        pid_t rid = waitpid(-1,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
            //sleep(1);
        }
    }
    return 0;
}

因此,此时的代码编译会warning:

但是不会影响生成可执行文件:

我们运行./myproc有:

退出信号为8号信号,我们的8号信号是谁?

SIGFPE它的全称是 Signal Floating-Point Exception(浮点异常信号)。

但请注意一个极其重要的历史遗留陷阱SIGFPE 不仅仅指浮点数错误,它实际上涵盖了"所有算术运算错误",包括整数运算。

什么情况会触发SIGFPE?

在你的 C/C++ 程序中,以下代码行为会导致操作系统向进程发送 SIGFPE 信号:

  • 整数除以零 (最常见):int a = 10 / 0;

  • 整数取模零int a = 10 % 0;

  • 整数溢出 (某些体系结构下触发):如有符号整数溢出(INT_MAX + 1,但注意在 x86 上不一定总触发,取决于编译优化和 CPU 标志位)。

  • 浮点异常 :例如浮点数除以零(1.0 / 0.0)通常返回 inf 而不一定触发信号,但在特定的 FPU 控制字设置下也可能触发;无效浮点运算(如 sqrt(-1))也可能触发。

默认行为 :收到 SIGFPE 的进程通常会立即异常终止(Terminate) ,并且默认会产生 core dump(核心转储文件),方便事后调试。

为什么名字叫"FPE"却管整数错误?

这是因为 Unix 早期(PDP-11 时代)浮点运算单元(FPU)是外置硬件,当算术出错时,由 FPU 发一个中断,内核统一标记为"浮点异常"。后来 CPU 集成了整数和浮点单元,但信号名字沿用了下来,变成了"算术异常"的代名词。

当然,如果有野指针,也会有问题:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
int main()
{
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            //while(i)
            while(1)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(3);
                int* p = NULL;
                *p = 100;//野指针
                //int a = 10;
                //a /= 0;
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            //exit(115);
            //exit(1);
            exit(0);
        }
    }
    printf("父进程开始等待子进程ing...\n");
    //父进程
    sleep(4);
    for(i=0;i<N;i++)
    {
        //pid_t rid = wait(NULL);
        int status = 0;
        pid_t rid = waitpid(-1,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
            //sleep(1);
        }
    }
    return 0;
}

此时的代码运行有:

11号信号是:

SIGSEGV是 11 号信号 ,全称 Segmentation Violation(段错误)。

它是 Linux 编程中最常见、也最令初学者头疼的崩溃信号。它直接翻译成人话就是:"你的程序试图访问它无权访问的内存地址,或者试图以错误的方式访问合法地址(比如往只读内存里写东西)。"

哪些操作会引起段错误(11号信号)?

  1. 空指针解引用(最经典)int *p = NULL; *p = 10;(试图往 0 地址写数据)。

  2. 访问已释放的内存(Use-After-Free)free(ptr); int a = *ptr;(野指针悬挂)。

  3. 往只读内存区域写数据(常考)

    cpp 复制代码
    char *s = "hello";   // "hello" 存储在只读数据段(rodata)
    s[0] = 'H';          // 试图修改只读内存 -> SIGSEGV
  4. 栈溢出(Stack Overflow):无限递归函数调用,把栈帧空间耗完,触及非法地址。

  5. 数组越界严重访问int arr[10]; arr[10000] = 1;(如果偏移量过大,越过了当前进程的堆/栈边界,会触发)。

问题1:进程如果执行完,如果结果不对,要不要知道是什么原因不对吗?

用退出码(exit code),有退出编号,用退出编号可以知道退出原因。

问题2:如果进程出现异常,如何知道什么原因导致的异常?

所有信号有不同的数字,我们可以通过数字来了解为什么进程退出!

子进程会在进程退出的时候,把两个数字分别写到自己的退出码(exit_code)和退出信号(exit_signal)写到进程的PCB当中!

在Linux的内核源码task_struct里有这两个:

因此,为什么Z状态不释放task_struct?

因为它的退出信息(退出信号+退出码)还没有传递给父进程。

因此,waitpid就是把子进程的exit_code和exit_signal合并成一个status传递出去,因此waitpid本质是从子进程task_struct内的属性数据,和getpid没区别。当前,它调用完毕的时候,也会让操作系统释放目标task_struct。

因此,task_struct里还有一个children链表,来表示所有的子进程:

3.3.2.3结合C++使用waitpid

此时我们就需要加一下C++的代码了,因此,需要把文件名改一下,改为myproc.cpp,并把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<vector>
#define N 10
int main()
{
    std::vector<pid_t> subids;
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(1)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(3);
                int* p = nullptr;
                *p = 10;//野指针
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            exit(0);
        }
        subids.push_back(id);//把子进程的pid都放入顺序表中
    }
    //父进程
    sleep(4);
    for(auto& sid : subids)
    {
        int status = 0;
        printf("父进程开始等待子进程ing...,等待:%d\n",sid);
        pid_t rid = waitpid(sid,&status,0);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
        }
    }
    return 0;
}

此时的Makefile改为:

此时代码运行结果为:

如果我们想要知道更详细的信息,可以把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<vector>
#include<string.h>
#define N 10
int main()
{
    std::vector<pid_t> subids;
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(1)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                sleep(3);
                int* p = nullptr;
                *p = 10;//野指针
                sleep(1);
                i--;
            }
            printf("子进程退出了\n");
            exit(0);
        }
        subids.push_back(id);//把子进程的pid都放入顺序表中
    }
    //父进程
    sleep(4);
    for(auto& sid : subids)
    {
        int status = 0;
        printf("父进程开始等待子进程ing...,等待:%d\n",sid);
        pid_t rid = waitpid(sid,&status,0);
        if(rid > 0)
        {
            int exit_code = (status>>8)&0xFF;//退出码
            int exit_signal = status & 0x7F;//退出信息
            if(exit_code == 0 && exit_signal == 0)
                printf("子进程运行完毕,结果正确\n");
            else if(exit_code > 0 && exit_signal == 0)
                printf("子进程运行完毕,结果不正确: %d:%s\n",exit_code,strerror(exit_code));
            else
                printf("子进程出现异常,exit_signal: %d\n",exit_signal);
        }
        else
            printf("等待失败\n");//等待失败只有在我们waitpid第一个参数传递的不是自己的子进程时会失败!(或者不存在这个进程)
        //if(rid > 0)
        //{
        //    printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
        //}
    }
    return 0;
}

如果我们把代码改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<vector>
#include<string.h>
#define N 10
int main()
{
    std::vector<pid_t> subids;
    int i = 0;
    for(;i<N;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 2;
            while(1)
            {
                printf("我是一个子进程,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),i);
                //sleep(3);
                //int* p = nullptr;
                //*p = 10;//野指针
                sleep(1);
                i--;
                break;
            }
            printf("子进程退出了\n");
            exit(1);
            //exit(0);
        }
        subids.push_back(id);//把子进程的pid都放入顺序表中
    }
    //父进程
    sleep(4);
    for(auto& sid : subids)
    {
        int status = 0;
        printf("父进程开始等待子进程ing...,等待:%d\n",sid);
        pid_t rid = waitpid(sid,&status,0);
        if(rid > 0)
        {
            int exit_code = (status>>8)&0xFF;//退出码
            int exit_signal = status & 0x7F;//退出信息
            if(exit_code == 0 && exit_signal == 0)
                printf("子进程运行完毕,结果正确\n");
            else if(exit_code > 0 && exit_signal == 0)
                printf("子进程运行完毕,结果不正确: %d:%s\n",exit_code,strerror(exit_code));
            else
                printf("子进程出现异常,exit_signal: %d\n",exit_signal);
        }
        else
            printf("等待失败\n");//等待失败只有在我们waitpid第一个参数传递的不是自己的子进程时会失败!(或者不存在这个进程)
        //if(rid > 0)
        //{
        //    printf("父进程等待子进程成功,子进程pid:%d,status:%d,退出码:%d,退出信号:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
        //}
    }
    return 0;
}

我们让子进程退出时的退出码为1,此时(父进程)就会执行else if语句:

3.2.2.4waitpid最佳实践

实际上,我们还是用C语言来实现waitpid的最佳实践更好,因此,我们waitpid最基本的代码为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define N 10
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(cnt)
        {
            cnt--;
            printf("子进程: %d\n",getpid());
        }
        exit(0);
    }
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
        printf("wait sucess\n");

    }
}

我们实际操作时,只要关心进程是否异常或者代码是否执行完毕就行,没必要用status来提取退出码,因此,waitpid给了我们宏,来帮助我们检测退出信号,检测退出码等等:

WIFEXITED用来检测进程是否正常退出,即查看我们进程是否正常结束的;WEXITSTATUS用来查看进程的退出码,看我们的程序运行是否正确!当信号值为0时,它会返回为1;当信号值不为0时,它会返回为0。对二者都同理!

因此,代码可以改为:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define N 10
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(cnt)
        {
            cnt--;
            printf("子进程: %d\n",getpid());
        }
        exit(0);
    }
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
        printf("wait sucess\n");
        if(WIFEXITED(status))
            printf("正常运行结束:%d\n",WEXITSTATUS(status));
        else
            printf("进程异常了\n");
    }
}

此时的运行结果为:

如果我们把退出码改为10再运行:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define N 10
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(cnt)
        {
            cnt--;
            printf("子进程: %d\n",getpid());
        }
        //exit(0);
        exit(10);
    }
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
        printf("wait sucess\n");
        if(WIFEXITED(status))
            printf("正常运行结束:%d\n",WEXITSTATUS(status));
        else
            printf("进程异常了\n");
    }
}

此时的运行结果为:

因此这就是最佳实践!

实际上我们还可以把status传递一个NULL,我们可以不关心子进程的退出信息,因此也可以在waitpid第二个参数传递NULL!

3.3.3阻塞等待与非阻塞等待

之前我们传递waitpid的时候一般是这样:

cpp 复制代码
waitpid(id,&status,0);

我们之前从来没有关心过最后一个参数为什么传0,最开始我们说:传0代表让父进程阻塞等待,对我们最直观的感受是父进程在等,只要子进程不退出,那么父进程就不会执行后续代码。

如果我们把它的第三个参数设置成:

cpp 复制代码
WNOHANG

WNOHANGwaitpid 系统调用中一个极其重要的选项标志,它的全称是 "Wait No HANG" (不挂起/不阻塞)。它称为:非阻塞等待

它的核心作用是:告诉内核"如果子进程还没结束,不要在这里死等(阻塞),立刻返回 0 让我去做别的事"。

当你设置 waitpid(pid, &status, WNOHANG); 时:

  • 如果子进程还在运行waitpid 不会阻塞 ,而是立刻返回 0。父进程可以继续执行后续的代码。

  • 如果子进程已经退出 :行为跟普通一样,回收子进程,返回子进程的 PID(大于 0)。

  • 如果出错 (比如 pid 不存在或参数非法):返回 -1 ,并设置 errno

举个例子,期末考试的时候,张三找李四去复习C语言,他到李四楼下的时候,打了一个电话,让他下来,但是过了一段时间,李四没有下来,于是张三又打了电话,直到打了10几个电话,李四才下来(每个电话都接通了),然后他们两个就一起到自习室去复习C语言了;第二次的时候,要考数据结构,张三到李四楼下,打电话说和他一起复习数据结构,但是因为上次挂电话导致产生了很多的电话费,因此让李四不要挂电话,张三想要关注李四实时的动态,张三啥都不干,就一直听着李四在干什么,等到李四下来的时候,才挂掉电话,一起去自习室了!

张三是一个进程,李四是操作系统,张三要求李四提供复习的服务,也就是张三这个进程要进行系统调用比如:waitpid,当张三在等的时候,调用waitpid,一旦调用waitpid就会阻塞住,相当于"不要挂电话",此时就是阻塞式调用。张三进程打一次电话给操作系统就是调用一次waitpid函数,询问操作系统自己的子进程退了吗,如果得到:没有退的结果,直接挂掉电话,并要waitpid返回0,等过一段时间就会再打一次电话。**此时的waitpid不会因为条件没有就绪(子进程没有退出),而是立即返回,这个状态叫做:非阻塞状态,非阻塞状态本质:检测子进程状态,如果子进程退出了,回收子进程;如果没有退出,立即返回。**因此,看似我们只调用了一次waitpid,实际上我们调用了很多次waitpid!

这种大量调用非阻塞等待检测子进程的状态我们叫做:非阻塞轮询方案!

如何让父进程进入非阻塞等待呢?

因此必须把最后一个参数设置成WNOHANG,但是还不够,阻塞等待时要么等待成功,要么等待失败,所以它的返回值要么大于0,要么小于0就是成功和失败。

但是非阻塞等待时,要么等待成功,子进程退了,回收子进程;要么等待失败,你等待的进程并不是自己的子进程;还有一种是,在等待期间,发现子进程并没有退出,此时会直接返回0。因此waitpid此时有三种返回值,如果返回-1,代表等待失败(返回值<0);返回的是子进程的PID,证明等待是成功的,子进程退出并且等待成功(返回值>0);子进程没有退出,waitpid的等待还是成功的。

因此代码改为:

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 10;
        while(cnt--)
        {
            printf("子进程在运行:%d\n",cnt);
            sleep(1);
        }
        exit(0);
    }
    pid_t rid = waitpid(id,NULL,WNOHANG);
    if(rid > 0)
    {
        //证明等待成功了,子进程已经退出
        printf("wait child success\n");
    }
    else if(rid == 0)
    {
        //证明等待中,子进程并没有退出
        printf("child not quit\n");
    }
    else
    {
        //等待失败
        printf("wait error\n");
    }
    return 0;
}

此时我们用另外一个机器使用如下指令运行:

bash 复制代码
while :; do ps ajx | head -1 && ps ajx | grep myproc; sleep 1; done

运行有:

我们观察发现,父进程一启动创建完子进程之后,打印完child not quit并退出(打印了命令行提示符)后就退出了,并且

①只有一个进程的父进程PID为1的进程在运行了;

②我并没有看到父进程在等待;

③为什么直接输出了一行child not quit呢?

我们在程序开始运行之后,父进程执行完fork就直接执行后面的代码了,并不会等待子进程的结束,先执行waitpid函数,发现:子进程还没退出,此时的rid为0,因此执行else if语句后就退出了。虽然waitpid会一直等待,但是父进程执行完后续代码直接结束进程了,相当于:张三找李四,打了一个电话得到李四在干什么之后就直接不等了,直接走了,但是李四还需要过10秒才能下楼(因为子进程需要运行10秒),但是父进程却没等了!

因此,如果想要保证非阻塞等待,必须要保证:父进程最后一个结束的,即必须要等所有子进程结束,父进程才能结束!因此要把非阻塞等待改为非阻塞轮询等待。

这三个现象很容易解释,因此这里不过多说明了!

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 10;
        while(cnt--)
        {
            printf("子进程在运行:%d\n",cnt);
            sleep(1);
        }
        exit(0);
    }
    //父进程-非阻塞轮询等待
    while(1)
    {
        pid_t rid = waitpid(id,NULL,WNOHANG);
        if(rid > 0)
        {
            //证明等待成功了,子进程已经退出
            printf("wait child success\n");
            break;//成功直接退出
        }
        else if(rid == 0)
        {
            //证明等待中,子进程并没有退出
            printf("child not quit\n");
            sleep(1);//不要一直去轮询了,一秒钟轮询一次
        }
        else
        {
            //等待失败
            printf("wait error\n");
            break;//失败直接退出即可
        }
    }
    return 0;
}

此时我们就能做到轮询等待了,我们需要每隔1秒钟轮询一次!

但是这样的代码没意义,这样还不如阻塞等待,为什么要用非阻塞轮询呢?

按照之前的例子,如果张三在每次等待期间都不干啥事情干等着也没办法,因此还会玩手机或者看一下复习资料这种。因此**:使用非阻塞轮询方案,不会卡住父进程,如果是阻塞等待子进程,子进程不退,那么父进程就要卡着,但是有非阻塞轮询的话,父进程就能在等待间隙可以做其他的事情了!因此,非阻塞轮询会高效一些,因为它会让父进程做更多的其他事情!**

我们可以把代码改为C++的形式,我们需要创建一个hpp文件:

在C语言/C++中我们写过很多的头源分离的代码,其中这个hpp文件会把类或者函数或各种数据的声明和实现可以写在一起,并且可以被别人直接include,即可以直接使用它了!本质上,这个头源写在一起就和之前我们写源文件一样,把类的声明和类的实现写在一起,并且还可以像头文件一样被include进来,这种hpp的写法更多的是在开放源代码的项目当中即开源项目里有更多的使用这种做法,这种写法未来我们不用写库,而是使用header only 方式使用其他的方法,实际上.hpp就是.h的头文件!

因此我们在otherThing.hpp写(#include<functional>包含这个头文件,我们可以定义函数对象,类似仿函数对象):

这行代码等同于(C++14或C++17):

cpp 复制代码
typedef std::function<void()> func_t;

相当于我们定义了一个函数对象,它的返回值为void,参数为空。把OtherThing.hpp重命名为Tool.hpp,并在里面写下如下代码:

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include<functional>

using func_t = std::function<void()>;
//typedef std::function<void()> func_t;

class Tool
{
public:
    Tool(){}//构造函数
    void PushFunc(func_t f)
    {
        _funcs.push_back(f);//允许外部向内部插入更多的方法!
    }
    void Execute()
    {
        for(auto& f : _funcs)
        {
            f();//调用一次Execute,执行一次_funcs里面的所有方法
        }
    }
    ~Tool(){}//析构函数
private:
    std::vector<func_t> _funcs; //方法集
};

也就是说,我们的Tool将来作为我们的工具包,我们可以通过写入工具来完善这个工具包!

再创建一个Task.hpp,写入以下内容:

cpp 复制代码
#pragma once
#include<iostream>

void DownLoad()
{
    std::cout << "我是一个下载任务" << std::endl;
}
void PrintLog()
{
    std::cout << "我是一个打印日志的任务" << std::endl;
}
void FlushData()
{
    std::cout <<"我是一个刷新数据的任务" << std::endl;
}

并且我们把myproc.cc的代码改为:

cpp 复制代码
//让父进程在等待期间做做其他事情
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include "Task.hpp"
#include "Tool.hpp"

int main()
{
    Tool tool;
    tool.PushFunc(DownLoad);
    tool.PushFunc(PrintLog);
    tool.PushFunc(FlushData);
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 10;
        while(cnt--)
        {
            printf("子进程在运行:%d\n",cnt);
            sleep(1);
        }
        exit(0);
    }
    //父进程-非阻塞轮询等待
    while(1)
    {
        pid_t rid = waitpid(id,NULL,WNOHANG);
        if(rid > 0)
        {
            //证明等待成功了,子进程已经退出
            printf("wait child success\n");
            break;//成功直接退出
        }
        else if(rid == 0)
        {
            //证明等待中,子进程并没有退出
            printf("child not quit\n");
            //父进程去做一下其他事情
            tool.Execute();
            sleep(1);//不要一直去轮询了,一秒钟轮询一次
        }
        else
        {
            //等待失败
            printf("wait error\n");
            break;//失败直接退出即可
        }
    }
    return 0;
}

并且把Makefile的代码改为:

因此会有:

因此,如果我们想要父进程进行对应的任务,可以在Task.hpp里新增对应的任务即可,然后把任务放到对应工具箱Tool里即可。虽然我们确实没必要这么麻烦,但是这确实是一个示例,如果我们直接写为一个文件,那么需要改父进程的代码,如果分开写到时候可以随意添加!

相关推荐
上火的金鱼妹1 小时前
K8S基础组件作用和关系整理
linux·运维·服务器·kubernetes
Vcaker1 小时前
Linux学习25-harbor私有仓库部署
linux·运维·学习
刃神太酷啦1 小时前
Redis 进阶核心:持久化 (RDB/AOF)、事务与主从复制全解析----《Hello Redis!》(5)
linux·c语言·数据库·c++·redis·缓存·bootstrap
编程的一拳超人2 小时前
DeepSeek Harness Linux/macOS/Windows 本地下载、启动与模型配置指南
linux·windows·macos·deepseek
醉颜凉2 小时前
网络安全必学:粘性MAC地址(Sticky MAC)原理与应用全解析
运维·服务器·网络·安全·web安全
时凌云.2 小时前
【2026最新】JDK 下载安装与环境配置全教程(Windows/Mac/Linux 三平台,零基础友好)
java·linux·macos
不懂的浪漫2 小时前
ToDesk 连接 Linux 后分辨率过低的解决方法
linux·运维·数据库
晊晌_h2 小时前
嵌入式从0到精通——Linux C|TCP 并发服务器实现方案详解
服务器·开发语言·tcp/ip
姚不倒3 小时前
etcd 学习系列(七):排障指南 —— 常见问题与解决方案
运维·etcd