【Linux】进程控制(一) 进程创建、终止与等待概念与实战讲解

文章目录


一、进程创建

进程创建话题我们在上一个大章节进程概念部分已经讲了大部分,这里小编又拿出来是为了做归纳总结,并补充一部分新知识。

以我们目前的了解创建进程有两种方式:

1、命令行中./程序名,或者直接输入指令

2、fork函数

fork函数

在 Linux 中,fork 函数是一个非常重要的函数,它能从已存在的进程中创建一个新进程。其中,新创建的进程称为子进程,而原来的进程则称为父进程。

cpp 复制代码
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,⽗进程返回⼦进程id,出错返回-1

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork 返回,开始由调度器进行调度
    当一个进程调用 fork 之后,会产生两个二进制代码相同的进程。而且它们都会运行到相同的位置,但此后每个进程都将开始自己独立的执行路径,看如下程序:
cpp 复制代码
int main()
{
	pid_t pid;
	printf("Before: pid is %d\n", getpid());
	if ((pid = fork()) == -1)perror("fork()")
		exit(1);

	printf("After:pid is %d, fork return %d\n", getpid(), pid);
	sleep(1);
	return 0;
}
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

观察相关程序可以看到三行输出:一行 "before",两行 "after"。进程 43676 先打印 "before" 消息,然后它又打印 "after";另一个 "after" 消息由进程 43677 打印。需要注意的是,进程 43677 没有打印 "before",这是为什么呢?如下图所示:

fork 之前父进程独立执行,fork 之后,父子两个执行流分别执行。注意,fork 之后,谁先执行完全由调度器决定。
fork函数返回值:

⼦进程返回0。

⽗进程返回的是⼦进程的pid。

写时拷贝

关于写时拷贝小编需要补充一些知识,我们上一节已经知道了写时拷贝的触发条件,当父子进程同时对同一变量做写入时会发生,我们再来聊一聊写时拷贝的作用原理,它本质是通过一种名叫缺页中断的概念完成的,但是本节小编不打算详聊缺页中断,而是以一种更为简洁的阐述来描述写时拷贝作用原理。

操作系统有个规定只有父进程运行时进程虚拟内存中已初始化、未初始化数据区在页表中的权限默认是可读可写的,代码区是只读的,但父子进程同时运行的情况下,操作系统会把父进程页表中的数据区权限由可读可写修改为只读,子进程会继承父进程的虚拟地址空间和页表进而子进程页表数据区权限也是只读(为了后续写入时让操作系统进入报错周期),当父子进程其中一方尝试对它们在数据区的共享变量做写入操作时,操作系统就会"报错",并对当前情况进行判断分类:

1、如果该数据区的虚拟地址在页表中并没有映射关系,虚拟地址并没有把内存分配该地址,也就是访问了不该访问的区域,是一种野指针操作,那么操作系统会判断该行为是真的错误,会终止该进程。

2、如果该数据区的虚拟地址在页表中有映射关系,并且访问了你本应访问的区域,只不过暂时被页表中的读写权限给限制了,那么操作系统会做以下操作:1、对父子共享内存做写时拷贝。2、把页表中对应权限改回可读可写。
父子进程不论先后顺序都对共享内存进行写入时,会发生两次写时拷贝:

有关写时拷贝还有两个子问题,其中问题一我们在上一节已经回答过了:

1、为什么创建子进程后不直接把父进程的数据给子进程拷贝一份,直接实现进程独立性呢?

答:该方案技术上可以实现,但是没必要,因为子进程一般不会把父进程全部数据都用到,直接拷贝就会浪费系统内存资源,所以写实拷贝本质是一种惰性申请,按需获取内存资源,提高整机内存使用率。

2、为什么开辟空间后还要把数据拷贝到新空间,不是只开辟空间? 答:写入操作不是只有赋值写入,如 a = 20,还有可以在原数据基础上对其进行写入:如a++。
扩展问题: 从今天开始,你在 C/C++ 上申请空间 malloc or new 的时候,操作系统会立即在物理内存里面开辟空间吗?

答:不会,操作系统只会为你开辟虚拟内存空间,也就是创建一个vm_area_struct,当你真正用对应空间的时候,操作系统才给你在物理内存里申请空间,并构建完整的从虚拟到物理的映射关系。所以new、malloc操作也是一种惰性空间开辟。

fork常规用法

⼀个是⽗进程希望子进程复制⾃⼰,让子进程执行父进程代码的一部分,然后⽗⼦进程同时执⾏父进程原有的不同的代码段。例如,⽗进程等待客⼾端请求,⽣成⼦进程来处理请求。(子承父业)
⼀个是子进程要执⾏⼀个与父进程完全不同的全新的程序。例如⼦进程从fork返回后,调⽤exec函数。(重新创业)

fork调用失败的原因

  • 系统中有太多的进程,内存空间不足导致失败。
  • 一般在系统层面上对特定用户能创建的子进程数目是有限制的,实际用户的进程数超过了限制导致失败。

二、进程终止

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码,是进程创建的逆过程。

进程退出场景

  • 代码运⾏完毕,结果正确
  • 代码运⾏完毕,结果不正确
  • 代码异常终止

判断进程终止后属于前两种情况的哪种情况有进程退出码来判定,当进程异常退出时属于第三种情况,退出码本身就没有意义了,我们可以把进程类比成一个学生,退出码类比学生的考试成绩,当退出码为0时表示学生考了满分,当退出码不为0表示学生考试不及格,进程异常了类比学生考试作弊了,那它不管考了多少分这个分数本身都没有意义了。

退出码

1、退出码是什么? 我们平时main函数的 return 0 就表示对应main函数进程的退出码。

2、进程终止后操作系统会把进程退出码写到对应进程task_struct内的变量exit_code中,然后退出码会被父进程拿到。

3、退出码普遍是正数。

4、使用指令 echo $? 可以查看最近一次的进程退出码,因为变量?里存储了最近一次的进程退出码。

5、退出码存在的意义是让你知道子进程把父进程交给它的事办的怎么样,退出码为0表示运行成功,非0表示失败,在我们的潜意识中一件事办成了是不需要理由的,可如果失败是需要知道失败原因的,除0以外的退出码一般都表明了不同的失败原因。

6、关于不同的退出码代表的含义,标准C/C++已经为我们提供了一些内置的原因,可以通过函数strerror查看详细信息,它可以把错误码转化为具体错误的字符串描述:

我们可以用下面的代码尝试把所有内置错误码信息打印一遍:

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

int main()
{
    int i = 0;
    for(i; i < 150; i++)
    {
        printf("%d: %s\n", i, strerror(i));
    }
    return 0;
}

最后带退出码0一共有134个退出码有内置原因,下面是运行结果:

return/exit/_exit

  • return一般表示函数调用结束,若在main函数中return是一种特殊情况,表示进程退出。
  • exit()是库函数,man手册第三章为库函数:

exit是进程退出的最佳实践,括号中的参数表示退出码,等同于return的数值,在代码的任何地方调用都会导致进程退出。

  • _exit()是系统调用,man手册第三章为系统调用:

功能和exit类似,只不过它和exit()的区别是它在进程退出的时候不会刷新缓冲区,而return和exit()在进程退出的时候,会自动刷新缓冲区。(补充缓冲区:我们在讲进度条小程序的时候提到过缓冲区的概念,我们用printf打印数据时会先默认打到缓冲区,通过某种方式刷新缓冲区后才会把打印的数据刷新到硬件显示器中)

进程终止本质会修改内核的数据结构和代码,所以进程终止必定会调用系统调用,必须让操作系统完成真正的进程删除退出,因为操作系统是计算机软硬件资源的管理者,进程是操作系统内的软件资源,进程需要调用系统调用函数fork,故而进程退出也必然需要调用系统调用_exit来回收资源,所以库函数exit底层一定在底层调用了_exit,库和系统调用是上下层关系,所以这里我们可以肯定一点,那就是缓冲区本身一定不在操作系统内核中,因为如果缓冲区在操作系统exit()内部,那么不论是调用exit()还是_exit()都会刷新缓冲区,而事实是只有调用库函数exit()时刷新了缓冲区,而exit()底层会调用_exit(),所以我们可以大概判断一下缓冲区应该在库当中,等后续我们学了文件系统再来详聊其中的细节。

三、进程等待

进程等待小编分三步介绍,分别是 为什么要有进程等待?进程等待是什么?如何在代码中使用进程等待?

进程等待必要性

为什么?:

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼" 的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程,这句话的意思是在我们用户层面无法处理僵尸进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源(必要),获取子进程退出信息(可选)。

是什么?:

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

怎么办?:

wait

wait是实现进程等待的方法之一,但它不是实现进程等待的最佳实践,我们这里先用它来演示进程等待如何回收子进程资源。
父进程调用wait表示父进程等待任意一个子进程:

1、如果子进程没有退出,父进程就会阻塞等待

2、如果子进程退出,父进程就会wait返回,让系统自动解决子进程的僵尸问题。
参数和返回值:

wait参数status为输出型参数,它的作用是把子进程的退出信息带出来交由父进程处理,在wait这里我们不关心status,在实践的时候会把它置为NULL,后面介绍waitpid时再详细status参数。

当父进程调用wait等待子进程成功wait返回子进程pid,等待失败返回-1。
下面是父进程调用wait进程等待回收子进程的示例代码:

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 = 3;
        while(i--)
        {
            printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
            sleep(1);
        }
        exit(117);
    }

    //子进程已退出,下面代码由父进程独享
    printf("父进程等待中...\n");
    sleep(5);
    pid_t rid = wait(NULL);
    if(rid > 0)
    {
        printf("父进程等待子进程成功,回收子进程,子进程pid:运行结果: %d\n", rid);
    }
    sleep(2);

    return 0;
}

运行结果:

下面小编在编写一份多进程的代码,一次生成10个子进程,为了演示wait函数调用一次只能回收一个子进程,要回收10个子进程就需要把wait函数调用10次。

cpp 复制代码
#define N 10

int main()
{
    //创建10个子进程
    int i = 0;
    for(; i < N; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 3;
            while(i--)
            {
                printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
                sleep(1);
            }
            exit(117);
        }
    }

    //子进程已退出,下面代码由父进程独享
    printf("父进程等待中...\n");
    sleep(5);
    for(i = 0; i < N; i++)
    {
        pid_t rid = wait(NULL);
        if(rid > 0)
        {
            printf("父进程等待子进程成功,回收子进程,子进程pid: %d\n", rid);
        }
    }

    sleep(2);

    return 0;
}

运行结果:

补充:在多进程场景下,父进程往往最先创建,最后退出,因为子进程需要父进程fork,退出后还需要由父进程回收。

waitpid

waitpid是我们未来进行进程等待的最佳实践,它的功能比wait更强大,详细解释如下:

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相等的⼦进程。(waitpid是父进程调用的,父进程能拿到子进程pid)

status:

输出型参数,等同于wait的参数,带出子进程退出码

WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(提取进程退出信号,查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)

options:

默认为0,表⽰阻塞等待,进程不仅可以等待硬件资源,也可以等待软件资源,子进程就是软件资源,每个进程的PCB里都可以包含一个等待队列。

WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。

status参数

waitpid的第一个参数和第三个参数都很好理解,主要是第二个status和我们以前碰到的参数使用方式和含义不太一样,小编来详细解释一下。

status是输出型参数,它不是像普通参数一样是把参数到函数体内部给函数用,而要从函数中带出数据给调用函数方用。
我们直接来看下面的代码示例,注意子进程的退出码是1:

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

#define N 10

int main()
{
    //创建10个子进程
    int i = 0;
    for(; i < N; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 3;
            while(i--)
            {
                printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
                sleep(1);
            }
            exit(1);
        }
    }
    //子进程已退出,下面代码由父进程独享
    printf("父进程等待中...\n");
    sleep(5);
    for(i = 0; i < N; i++)
    {
        int status = 0;
        pid_t rid = waitpid(-1, &status, 0); //等待任意一个进程,阻塞等待
        if(rid > 0)
        {
            printf("父进程等待子进程成功,回收子进程,子进程pid: %d, status: %d\n", rid, status);
        }
    }
    sleep(2);
    return 0;
}

运行结果:

我们看到waitpid带出的status值竟然是256而不是子进程的退出码1,这里有什么猫腻?

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位):

进程正常终止:

我们先研究子进程正常终止的情况,status的8-15位用来表示子进程退出码,所以一般而言,子进程退出码范围是[0,255]。

再看子进程异常退出的情况,我们前面说过,当进程异常退出时,它的退出码就没有意义了,所以我们不看它的8-15,第七位我们目前还无法解释,等后面讲到信号章节时再补充。0-6位表示子进程被哪个信号所杀,所以它表示信号编号。
有了上面的知识我们就可以解释为什么之前明明子进程退出码是1,而status的值是256了,因为status的低16位值是 00000001 00000000,1后面跟8个0是2的8次方,自然就是256。 那我们要是就想拿到进程退出码就可以通过以下算式:

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

status右移8位表示把status的高8位移到低8位了,再与上16进制数0xFF,相当于二进制11111111,那么status右移后的低8位原来是多少与完后还是多少,其他位全部置0,这样就把status右移后的低8位提取出来了。示例:

cpp 复制代码
printf("父进程等待子进程成功,子进程pid: %d, status: %d, exit code:%d\n", rid, status, (status >> 8) & 0xFF);


进程异常终止:

我们首先要知道进程为什么会出现异常,是因为程序出现了问题,操作系统给进程发送信号了。我们看下面所有表征杀死进程的信号:

会发现没有0号信号,是因为当信号数字等于0时表示进程正常退出,所以我们现在可以用2个数字来表征子进程的三种执行情况:

进程退出信号为0, 退出码为0,表示进程运行完毕,结果正确

进程退出信号为0, 退出码非0,表示进程运行完毕,结果不正确

进程退出信号非0,退出码无意义,表示进程异常终止
下面我们用代码来验证一下上面的内容,要获取进程退出信号的算式:

cpp 复制代码
status & 0x7F

我们先让所有创建出来的子进程不是等三秒后退出,而是死循环打印,我们用kill -9 随机挑选一个进程杀掉,看现象:

我们可以看到确实退出信号就是9号。
我们再尝试让子进程除0使代码异常,看父进程的等待结果:

cpp 复制代码
int main()
{
    //创建10个子进程
    int i = 0;
    for (; i < N; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            //子进程
            int i = 3;
            while (1) //死循环
            {
                printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
                sleep(3);
                int a = 10;
                a = a / 10;
                sleep(1);
            }
            printf("子进程退出了\n");
            //模拟子进程结果不正确
            int ret = 1;
            if (ret == 0)
            {
                exit(0);
            }
            else
            {
                exit(1);
            }
        }
    }
    //子进程已退出,下面代码由父进程独享
    printf("父进程等待中...\n");
    sleep(5);
    for (i = 0; i < N; i++)
    {
        int status = 0;
        pid_t rid = waitpid(-1, &status, 0); //等待任意一个进程,阻塞等待
        if (rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid: %d, status: %d, exit code:%d, exit signal: %d\n",
                rid, status, (status >> 8) & 0xFF, status & 0x7F);
        }
    }
    sleep(2);
    return 0;
}

我们看到子进程除0后确实进程异常退出了,退出信号为8号,浮点数错误。
最后我们再让子进程空指针解引用,出现野指针异常:

cpp 复制代码
while (1) //死循环
{
    printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
    sleep(3);
    int* a = NULL;
    25 * a = 100;
    sleep(1);
}

我们看到子进程空指针解引用后进程异常退出,退出信号为11号,段错误。

进程等待代码完善

第一步我们先实现等待特定进程,下面我们让waitpid的第一个参数不是-1,而是特定子进程pid,实现等待特定子进程的效果:

cpp 复制代码
int main()
{
    std::vector<int> subids;
    //创建10个子进程
    int i = 0;
    for(; i < N; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 3;
            while(i--)
            {
                printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
                sleep(1);
            }
            printf("子进程退出了\n");
            //模拟子进程结果不正确
            int ret = 1;
            if(ret == 0)
            {
                exit(0);
            }
            else
            {
                exit(1);
            }
        }
        //把所有子进程pid都存入subids
        subids.push_back(id);
    }
    //子进程已退出,下面代码由父进程独享
    sleep(5);
    for(auto& wid : subids)
    {
        printf("父进程等待特定子进程,子进程pid: %d\n", wid);
        int status = 0;
        pid_t rid = waitpid(wid, &status, 0); //等待任意一个进程,阻塞等待
        if(rid > 0)
        {
            printf("父进程等待子进程成功,子进程pid: %d, status: %d, exit code:%d, exit signal: %d\n", 
                   rid, status, (status >> 8) & 0xFF, status & 0x7F);
        }
    }
    sleep(2);
    return 0;
}

运行结果:

下面我们再通过父进程拿到子进程的status值判断子进程退出的三种情况,最后的 waitpid error 表示waitpid本身调用异常,可能是等待了一个压根不存在的子进程。

cpp 复制代码
int main()
{
    std::vector<int> subids;
    //创建10个子进程
    int i = 0;
    for(i; i < N; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            int i = 3;
            while(i--)
            {
                printf("我是子进程,pid: %d, ppid: %d, count: %d\n", getpid(), getppid(), i);
                sleep(1);
            }
            printf("子进程退出了\n");
            //模拟子进程结果不正确
            int ret = 0;
            if(ret == 0)
            {
                exit(0);
            }
            else
            {
                exit(1);
            }
        }
        //把所有子进程pid都存入subids
        subids.push_back(id);
    }
    //子进程已退出,下面代码由父进程独享
    sleep(5);
    for(auto& wid : subids)
    {
        printf("父进程等待特定子进程,子进程pid: %d\n", wid);
        int status = 0;
        pid_t rid = waitpid(wid, &status, 0); //等待任意一个进程,阻塞等待
        if(rid > 0)
        {
            int exit_code, exit_signal;
            exit_code = (status >> 8) & 0xFF;                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
            exit_signal = status & 0x7F;
            if(exit_code == 0 && exit_signal == 0)
            {
                printf("子进程运行成功,结果正确\n");
            }
            else if(exit_code != 0 && exit_signal == 0)
            {
                printf("子进程运行成功,结果不正确:%s\n", strerror(exit_code));
            }
            else{
                printf("子进程运行异常,退出信号:%d\n", exit_signal);
            }
        }
        else{
            printf("waitpid error!\n");
        }
    }
    sleep(2);
    return 0;
}

进程等待的最佳实践(status宏)

我们前面通过算式获取status内的子进程退出信息只是为了了解它的原理,实际开发中并不需要我们手动获取status的推出信息,系统已经为我们准备了相关的宏来获取status的退出信息:
WIFEXITED(status) : 若为正常终⽌⼦进程返回的status,则为真。(提取子进程退出信号,查看进程是否是正常退出)
WEXITSTATUS(status) :若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)

下面是进程等待最佳实践的示例代码,不需要用多进程了:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int i = 3;
        while(i--)
        {
            printf("子进程pid: %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }                                                                                                                                                                                                                            
    //父进程
    int status = 0;
    pid_t wid = waitpid(id, &status, 0);
    if(wid > 0)
    {
        printf("等待子进程成功\n");
        if(WIFEXITED(status))
        {
            printf("子进程正常运行结束,退出码:%d\n", WEXITSTATUS(status));
        }
        else{
            printf("子进程异常终止\n");
        }
    }
    return 0;
}

进程等待流程总结

一、 1、父进程fork子进程。

2、子进程执行自己的代码和数据,执行完毕return或者exit的时候会把退出码写到子进程的task_struct的exit_code中,然后设置自己task_struct中的退出信号exit_signal。

3、父进程调用waitpid的时候本质就是获取子进程内部的属性数据如task_struct和exit_signal,和getpid没区别,waitpid调用完毕的时候就会让操作系统释放目标的task_struct。
二、为什么父进程waitpid可以等待特定子进程也可以等待任意子进程?

每个进程的task_struct都会维护一份等待队列,父进程可以拿着特定进程的pid去该进程的等待队列中等,除此之外,每个进程还会维护一份自己的子进程列表,当把waitpid的pid参数置为-1时父进程的工作就是检查所有子进程的task_struct,看哪个子进程task_struct状态是Z就把哪个子进程释放。

阻塞与非阻塞等待

cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);

我们先回顾一下之前介绍的waitpid,如果把第三个参数设置为0表示表⽰阻塞等待,若设置为WNOHANG表示非阻塞等待,这里的HANG就表示我们平常说的卡住了,如app,云服务器你怎么点都没有反应,若app,云服务器直接挂掉了,我们一般叫做宕机了,名词性概念小编不过多解释。
其实阻塞与非阻塞等待的概念很好理解,阻塞等待就是父进程只做等待子进程这一件事,直到子进程推出后父进程才会等待返回,非阻塞等待也是检测子进程状态,只不过它不会一直等,而是只查询一次,若查询到子进程退出了就回收子进程,若没退出也会立即返回,因为非阻塞等待只会查询一次,所以我们需要循环调用非阻塞等待,这种循环调用非阻塞等待的方式叫做非阻塞轮询方案。
当把options设置为WNOHANG时返回值就不是只有两个返回值了,而是有三个:

1、waitpid等待成功并且子进程退出,返回子进程pid。

2、waitpid等待成功并且子进程未退出,返回0。

3、waitpid出现异常,等待失败(比如等待的子进程pid不是你的,或者等待时出现了某种错误),返回值小于0。
下面我们用代码来感受一下:

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

int main()         
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程      
        int i = 3;
        while(i--)
        {       
            printf("子进程pid: %d\n", getpid());
            sleep(1);
        }
        exit(100);                                                                                                                                                             
    }                
    //父进程
    while(1)
    {
        pid_t wid = waitpid(id, NULL, WNOHANG);
        if(wid > 0)
        {
            printf("wait success!\n");         
            break; 
        }
        else if(wid == 0)             
        {         
            printf("child no quit\n");
            sleep(1);    
        }
        else if(wid < 0)              
        {            
            printf("wait error\n");
            break;      
        }
    }                              
    return 0;
}

我们乍一看上面的代码和阻塞等待也没有什么区别嘛,那为什么会说非阻塞轮询会更高效呢?原因在于非阻塞轮询不会卡住父进程,父进程可以在等待的间隙做其他事情,这样就可以让代码整体更高效。
我们下面就尝试实现一个让父进程在等待的间隙做一些其他事情场景:

1、补充.cc和.hpp文件后缀含义:

.cc表示C++文件后缀,.hpp表示头文件,但是它不是普通的头源分离的头文件,它允许把函数的声明和实现都放在里面。
2、解释特殊写法:

cpp 复制代码
//C++11支持的写法
using func_t = std::function<void()>
//一般写法
typedef std::function<void()> func_t;

其中 std::function<void()> 是< functional >头文件提供的模板类,它是一种可调用对象,其中的void代表无返回值,()里什么都没有代表无返回值,所以代码整体含义就是 "无参数、无返回值的可调用对象"(可以是普通函数、lambda 表达式、函数对象等)。 两行代码的目的相同,都是类型重命名,方便在代码中使用,后续代码中,func_t 就等价于 std::function<void()>,例如:

cpp 复制代码
// 定义一个符合该类型的lambda
func_t f = [](){ std::cout << "hello" << std::endl; };
f();  // 调用,输出 hello

3、实现Task.hpp和Tool.hpp文件:

我们想让父进程在等待子进程的间隙做做其他事情,这些事情包在Task.hpp头文件中(在文中用打印指令模拟),做的时候不想直接调这些任务,因为直接调不灵活,所以我们用一个Tool.hpp文件把这些任务包装起来。

Task.hpp:

cpp 复制代码
#include <iostream>  

void DownLoad()  
{  
    std::cout << "我是一个下载任务\n" << std::endl;  
}  

void PrintLog()  
{  
    std::cout << "我是一个打印日志任务\n" << std::endl;  
}  

void FlushData()  
{  
    std::cout << "我是一个刷新数据任务\n" << std::endl;                                                                                         
}  

Tool.hpp:

cpp 复制代码
#pragma once

#include<iostream>
#include<vector>
#include<functional>

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

class Tool
{
public:
    Tool()
    {}

    void PushFunc(func_t f)
    {
        _funcs.push_back(f);
    }

    void Execute()
    {
        for(auto& f : _funcs)
        {
            f();
        }
    }

    ~Tool()
    {}

private:
    std::vector<func_t> _funcs; //方法集
};

4、合并代码,完成预期目的:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include "Task.hpp"
#include "Tool.hpp"

int main()
{
    //把任务都push进工具箱
    Tool tool;
    tool.PushFunc(DownLoad);
    tool.PushFunc(PrintLog);
    tool.PushFunc(FlushData);

    pid_t id = fork();
    if(id == 0)
    {                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
        //子进程      
        int i = 3;
        while(i--)
        {
            printf("子进程pid: %d\n", getpid());
            sleep(1);
        }
        exit(100);
    }
    //父进程
    while(1)
    {
        pid_t wid = waitpid(id, NULL, WNOHANG);
        if(wid > 0)
        {
            printf("wait success!\n");
            break;
        }
        else if(wid == 0)
        {
            printf("child no quit\n");
            //父进程做其他事情
            tool.Execute();
            sleep(1);
        }
        else if(wid < 0)
        {
            printf("wait error\n");
            break;
        }
    }
    return 0;
}

运行结果:


以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
挺6的还3 小时前
46.NAT、代理服务、内网穿透
linux
存储服务专家StorageExpert4 小时前
NetApp存储基本概念科普:物理层到逻辑层
linux·服务器·网络·netapp存储·存储维护
cnkeysky4 小时前
ubuntu 24.04 从 6.8 内核升级 6.11 网卡加载失败问题
linux·ubuntu
岑梓铭4 小时前
计算机网络第四章(4)——网络层《IPV6》
服务器·网络·计算机网络·考研·408
十五年专注C++开发4 小时前
通信中间件 Fast DDS(三) :fastddsgen的安装与使用
linux·c++·windows·中间件·跨平台
tt5555555555554 小时前
Linux 驱动开发与内核通信机制——超详细教程
linux·驱动开发·b树
程序设计实验室5 小时前
在Linux系统上一键配置DoH,解决DNS解析被污染
linux
关关长语5 小时前
Ubuntu 中获取指定软件依赖安装包
linux·运维·ubuntu
山,离天三尺三5 小时前
线程中互斥锁和读写锁相关区别应用示例
linux·c语言·开发语言·面试·职场和发展