LINUX基础 [三] - 进程控制

目录

前言

 进程创建的初次了解(创建进程的原理)

什么是fork函数?

初识fork函数

写时拷贝

fork函数存在的意义

fork调用失败的原因 

进程终止

运行完毕结果不正确

 main函数返回

库函数函数exit 

系统调用接口_exit

进程异常终止

进程等待

进程等待是什么

进程等待为什么要进行

进程等待怎么做

阻塞和非阻塞轮询


前言

上节我们已经讲了进程的概念了,大家应该对进程有感悟同时也有更深入的思考,上节课介绍了那么多进程,但是进程该怎么创建呢,今天就来给大家讲解一下 进程的创建

 进程创建的初次了解(创建进程的原理)

创建新进程在Linux的下是由父进程来完成的,创建完成的新进程是子进程。
新进程的地址空间有两种可能性:

子进程是父进程的复制品(除了PID和task_struct是子进程自己的,其余的都从父进程复制而来)
子进程装入另一个程序。
在Linux下的fork函数用于创建一个新的进程,使用fork函数来创建一个进程时,子进程只是完全复制父进程的资源。这样得到的子进程和父进程是独立的,具有良好的并发性。但是进程间通信需要专门的机制。

什么是fork函数?

之前我们在Linux下启动一个进程的时候利用的是./可执行程序那是否有其他办法去启动一个进程呢? 

初识fork函数

当然是有的,那就是使用fork()这个函数。在使用之前呢我们要先去查看一下这个函数该如何使用------ 使用man 手册查询一下 fork 函数的使用

man 2 fork
  • 可以看到,这个函数的功能就是去创建一个子进程,其返回值为pid_t
  • 注意:这里的 pid_t 类型 是无符号整数

 函数说明:

  • 通过复制调用进程创建一个新进程。

  • fork 有两个返回值。

  • 父子进程代码共享,数据各自私有一份(采用写时拷贝)。

接下来,我们来测试一段代码:

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("begin:我是一个进程, pid:%d,ppid:%d\n", getpid(), getppid());
    pid_t id = fork();
    if (id == 0)
    {
        while(1)
        {
            printf("我是子进程, pid:%d,ppid:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if (id > 0)
    {
        while(1)
        {
            printf("我是父进程, pid:%d,ppid:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        // fork失败的错误处理
        printf("进程创建失败\n");
        return 1;
    }
    return 0;
}

 运行之后我们会发现一个非常惊奇的现象:竟然if和else if同时在进行!!!

       而后我们就要根据以上两个现象,我们大概可以知道fork其实创建了一个子进程,而后我们要针对这个现象进行系统地分析!

为什么会需要子进程?

之所以会多此一举搞一个子进程,其实是为了让父和子同时执行不同的事情。因此我们就要想办法让父和子执行不同的代码块,解决方法就是fork要有两个返回值!所以返回不同的值的意义是为了区分不同的执行流,让父进程和子进程分别执行不同的代码块!!

fork为什么要给子进程返回0,给父进程返回子进程pid?

因为一个父亲可以有多个子女,但是一个子女只能有一个父亲。所以对于父进程来说他未来可能需要去控制子进程,所以就需要子进程的PID用来标定子进程的唯一性。而子进程只需要用getppid就知道其父进程了!所以我们返回个0就可以了(找到父进程基本不耗费什么成本)。

fork函数究竟都干了什么?

调用fork函数后,内核做了下面的工作:

1、创建了一个子进程的PCB结构体、并拷贝一份相同的进程地址空间和页表(PCB结构体中的一个指针指向该空间)

2、子进程和父进程起初共享代码和数据,并且页表中的虚拟地址和物理地址的映射关系是一样的,所以也指向相同的物理空间。    

3、fork返回后将子进程添加到系统的进程列表中,由调度器调用(每个进程开始自己的旅程)

4、一旦其中任意一方尝试修改数据,那么就会发生写时拷贝,会开辟一块新的物理内存,然后改变页表的映射关系。


写时拷贝

 通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。 

当父进程形成子进程之后,子进程写入,发生写时拷贝,重新申请空间,进行拷贝,修改页表(OS)

但是,我们怎么知道发生了写时拷贝呢?写时拷贝的内容都是由操作系统来完成的 

其实父进程创建子进程的时候首先将自己的读写权限改成只读然后再创建子进程。

此时是操作系统在做,用户并不知道,而且用户可能会对某一数据进行写入,这时页表转换就会出现问题,操作系统就会介入,就触发了我们重新申请内存拷贝内容的策略机制

一个函数是如何做到返回两次的?

父子的代码是共享的,所以return ret也是属于代码,因此父子进程各返回了一次! 

 一个变量为什么会有不同的内容? 

 

原因是因为子进程要修改父进程的数据的时候,发生了写时拷贝,所以该数据其实有两份内容,然后因为进程的在运行的时候是具有独立性的,所以此时父进程和子进程通过if else 分流去执行共享的代码。 但是要注意的是,子进程被创建好之后,究竟是先运行子进程还是先运行父进程,其实是由调度器(因为CPU只有一个,所以他的作用就是在当前进程中选一个合适的放到CPU中,进程之间会竞争CPU资源,所以调度器会遵循着自己的一套原则来保证进程之间的公平性)去决定的!!

通过fork来理解bash命令行是如何工作的 

bash本身就是一个进程,当你输入相关指令的时候,他会为指令创建子进程,然后由子进程去执行对应的指令,这样即使子进程失败了也不会影响到bash——>目的是为了让bash可以专注于命令行解释的工作!!bash进程是在我们打开机器的时候就创建好的! 

fork函数存在的意义

fork函数常规用法:

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

2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。 (进程替换

fork调用失败的原因 

系统中有太多的进程

实际用户的进程数超过了限制

进程终止

问题引入:为什么main函数要返回0?返回多少的意义是什么???

成功只有一种情况,但是失败可以有无数的原因和理由!! 所以main函数的本质是进程运行时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因

进程退出场景:

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

运行完毕结果不正确

正常终止(可以通过 echo $? 查看进程退出码):       $?->保存最后一次进程退出的退出码

1. 从main返回

2. 调用exit

3. _exit

 main函数返回

 进程中,谁会关心我的运行情况呢??——>父进程 !

我们之前写代码中,main函数只能return 0吗?

答案是肯定不是!

在多进程环境中,我们创建子进程的目的就是协助父进程办事,但是父进程怎么知道子进程把事情办得怎么样?所以父进程要知道子进程办的怎么样,就有了退出码,而main函数的返回值,就是进程的退出码!

其实main函数本质上也是一个被别人调用的函数,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,

也就是说main函数是间接性被操作系统所调用的。所以他return的结果其实是想告诉他的父进程自己的运行情况。

退出码概念

父进程可以通过拿到子进程的退出码,不同的退出码分别代表的是不同的原因!

为什么需要有退出码呢??遇到问题我直接printf输出一下错误原因,或者是直接看结果不就可以了吗?

没有人规定代码程序必须得打印!比如说该错误并不是从显示器打印而是向网络写入

错误码适合计算机去看,但是不适合人去看,所以我们是否可以将他转换成字符串的错误信息解读??

我们可以通过 strerror 函数来直接查看每个数字代表的意义

返回 0 就表示成功,其他数字就表示进程失败的原因,每个不同的数字代表不同的原因!

它可以返回描述错误码的字符串

#include<stdio.h>
#include<string.h>

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

我们打印结果来看看 

退出码 0 正好对应的是成功!

当我们134位置处时,发现已经没有错误信息了。

实际上Linux中的ls、pwd等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。
可以看到,这些命令成功执行后,其退出码也是0。

但是命令执行错误后,其退出码就是非0的数字,该数字具体代表某一错误信息。

注意: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

注意:错误码我们可以自己自定义!

main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果

int main()
{
	return 31;
}

我们可以直接用 echo $? 指令查看进程的退出码: 

我们可以发现指令echo $?  返回的是上一个进程的错误码。当读取了一次之后,再读取就变成了0

我们的退出码其实使用的是系统提供的错误码体系,那么我们可不可以自己造一个退出码体系呢?

该体系是C标准库提供的,但是我们写的代码一般不是纯C写的,所以一般会自己搞一个退出码体系 

const char *errString[] = {
    "success",
    "error 1",
    "error 2",
    "error 3",
    "error 4",
    "error 5"
};

父进程为啥要关心子进程的运行状况呢?

父进程创建子进程的目的就是为了让子进程执行和自己不一样的代码流来完成某些特定的任务,父进程本身也就是一个跑腿的,因为代码是用户写的,所以真正关心的是用户,用户需要知道子进程将自己的工作完成得怎样了

全局变量erron

保存最后一次执行的错误码

这样的写法既可以直接在进程返回前知道错误码,然后再变成错误信息打印出来,并且也可以在进程结束后让父进程知道运行的情况 

库函数函数exit 

exit和return的区别:return和exit在main函数里是等价的,因为exit表示退出进程,而main函数恰好执行完return也会退出进程但是return在其他函数中代表的是函数返回。 

系统调用接口_exit

#include void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

 _exit和exit的区别:一个是系统调用接口(更底层)一个是库函数,其实exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

  • 执行用户通过 atexit或on_exit定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入(缓存被清理了)
  • 调用_exit

exit与_exit的区别

首先他们二者都可以让进程终止,并且使用方法也一样,那他们到底有什么区别呢?我们用代码来一探究竟!

//代码一:
int main()
{
	printf("Hello");
	exit(0); 
}

......
//代码二:
int main()
{
	printf("Hello");
    _exit(0); 
}

 为什么会出现这种情况呢?

printf打印如果不使用\n换行的话,数据会被存储到缓冲区里。exit函数会帮助我们刷新缓冲区的数据,然而_exit函数不会。因为exit函数在调用exit之前将所有缓存数据都写入了,所以在终止进程时,会将数据打印在屏幕上!

exit比_exit多做了一层最重要的工作就是刷新缓存我们还可以得出另一个结论就是:缓冲区绝对不在内核区!!因为如果在内核区的话,系统调用的_exit在终止的时候也必然会把缓冲区刷新一下,因为现代操作系统不做任何浪费时间和空间的事情,所以肯定不是由内核维护缓存区,而是由用户区在维护!!(_exit压根看不到缓冲区,所以这个工作只能有exit去完成)

return、exit和_exit之间的联系

执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。

使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。

进程异常终止

 用退出码可以告诉父进程自己的执行情况,那如果是异常中止了呢??那就连运行完毕这个条件都完成不了,更别谈结果是否正确了,所以我们可以知道异常必然是最先需要被知道的!因为一旦异常了,一般代码都没跑完,即使跑完了,错误码也不能让人相信,此时退出码就没有意义了!

举个例子:就好比我们平时考试一样,你考不好的时候大家会关心你为啥考不好,但如果你作弊了,性质就变了,即考得再好都让人觉得不可相信。

所以进程结束后应该优先判断该进程是否异常了,然后才能确定退出码能不能用!! 

// 当我们在运行这样的代码时

int a = 100;
a /= 0;
......
int *p = NULL;
*p = 100;
......

第一种情况: Floating point exception
第二种情况: Segmentation fault

当然不止这两个情况,但是它们都会让程序进程异常终止!

其实一旦程序出现了异常,操作系统就是通过 信号 的方式来杀掉这个进程!

而我们的前面两种情况正好对应了kill -8 kill -11

类似除0、野指针这样的错误,会触发一些硬件级别的错误,比如除0,cpu的状态寄存器会出现溢出的错误,而野指针,也就是们即将访问的虚拟地址在页表中找不到对应的映射,或者是建立的映射关系只有只读权限,反正最终会转化成一些硬件级别的信号来给操作系统。

所以,父进程需要关心子进程为什么异常,以及发生何种异常,系统会通过信号来告诉我们的进程发生了异常!! 

while(1)
{
	printf("hello Linux, pid: %d\n", getpid());
	sleep(1);
}

 所以我们最关键的是要看父进程是否收到了信号,如果没有收到就没有异常(具体如何收到,就涉及到进程等待的知识)

进程等待

进程等待是什么

首先在开始之前我们提个问题,到底什么是进程等待? - 是什么

进程等待的概念:

  • 我们通常说的进程等待其实是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程,父进程必须等待这个子进程结束后,处理它的代码和数据!

进程等待为什么要进行

在了解完进程等待的概念后,新的问题出现了,我们为什么要进行进程等待?-为什么

  • 在前面的文章中讲过,子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而会造成内存泄露。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的 kill -9 指令也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如:子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息。

进程等待怎么做

我们进行了进程等待分析,发现进程等待非常的有必要。那么进程等待具体是怎么做的? - 如何做

父进程通过调用wait/waitpid方法来解决僵尸进程回收问题,以及获取子进程退出情况

wait方法

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);
  • 返回值:成功,返回被等待进程的 pid,失败返回-1。

  • 参数:输出型参数,获取子进程的退出状态。

  • status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位)

父进程等待,我希望获取子进程的哪些信息呢?

(1)子进程的代码是否异常??(2)没有异常,结果对吗,不对的原因是什么?

父进程为什么不定义全局变量的status,而必须用wait等系统调用来获取状态呢?

用全局变量的话,因为进程具有独立性!!所以子进程再怎么去改自己的status,父进程都看不到!(虽然表面上是一份代码),所以这个过程比如要通过系统调用接口来让操作系统帮助我们获取子进程的一些数据!!(因为OS不相信任何人) 

为什么int被分为好几个部分?

我们不仅需要知道是否发生异常,还需要知道退出状态,所以这个int需要拆分成bit位 

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。

exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

对于此,系统当中提供了两个宏来获取退出码和退出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码

父进程只等待一个进程(阻塞式等待) 

#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)    
    {    
        perror("fork");    
        return 1;    
    }    
    else if(id == 0)    
    {    
        // child    
        int cnt = 5;    
        while(cnt)    
        {    
            printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);    
            sleep(1);    
        }    
        exit(0);    
    }    
    else    
    {    
        int cnt = 10;    
        // parent    
        while(cnt)    
        {    
            printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);    
            sleep(1);    
        }    
    
        int ret = wait(NULL);    
        if(ret == id)    
        {    
            printf("wait success!\n");    
        }    
        sleep(5);                                                                                                                                               
    }    
    
    return 0;    
}

前五秒父子进程同时运行,紧接着子进程退出变成僵尸状态,五秒钟后父进程对子进程进行了等待,成功将子进程释放掉,最后再五秒钟后父进程也退出,整个程序执行结束。 

 父进程等待多个子进程(阻塞式等待)

一个 wait 只能等待任意一个子进程,因此父进程如果要等待多个子进程可以通过循环来多次调用 wait 实现等待多个子进程。

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
#define N 5    
// 父进程等待多个子进程    
void RunChild()    
{    
    int cnt = 5;    
    while(cnt--)    
    {    
        printf("I am child, pid:%d, ppid:%d\n", getpid(), getppid());    
        sleep(1);    
    }    
    return;    
}    
int main()    
{    
    for(int i = 0; i < N; i++)    
    {    
        pid_t id = fork();// 创建一批子进程    
        if(id == 0)    
        {    
            // 子进程    
            RunChild();    
            exit(0);    
        }    
        // 父进程    
        printf("Creat process sucess:%d\n", id);    
    }    
    
    sleep(10);    
    
    for(int i = 0; i < N; i++)    
    {    
        pid_t id = wait(NULL);                                                                                
        if(id > 0)    
        {    
            printf("Wait process:%d, success!\n", id);    
        }    
    }    
    
    sleep(5);    
    return 0;    
}

如果子进程不退出,父进程在执行 wait 系统调用的时候也不返回(默认情况),默认叫做阻塞状态。由此可以看出,一个进程不仅可以等待硬件资源,也可以等待软件资源,这里的子进程就是软件。

waitpid方法

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int* status, int options);

返回值:

  • 当正常返回的时候 waitpid 返回等待到的子进程的进程 ID;
  • 如果设置了选项 WNOHANG,而调用的过程中没有子进程退出,则返回0;
  • 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在。

pid:

  • pid = -1 表示等待任意一个子进程。与 wait 等效;
  • pid > 0 表示等待进程 ID 与 pid 相等的子进程。

status:

  • WIFEXITED(status):查看子进程是否正常退出。若为正常终止子进程返回的状态,则为真;WEXITSTATUS(status):查看进程的退出码。若非零,提取子进程的退出码。

options:

  • 0:表示父进程以阻塞的方式等待子进程,即子进程如果处在其它状态,不处在僵尸状态(Z状态),父进程会变成 S 状态,操作系统会把父进程放到子进程 PCB 对象中维护的等待队列中,以阻塞的方式等待子进程变成僵尸状态,当子进程运行结束,操作系统会检测到,把父进程重新唤醒,然后回收子进程;
  • WNOHANG:非阻塞轮询等待,若 pid 指定的子进程没有结束,处于其它状态,则 waitpid() 函数返回0,不予等待。若正常结束,则返回该子进程的 ID。

小Tipswait 和 waitpid 都只能等待该进程的子进程,如果等待了其它的进程那么就会出错。

例如,创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
	pid_t id = fork();
	if (id == 0){
		//child          
		int count = 10;
		while (count--){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	//father           
	int status = 0;
	pid_t ret = waitpid(id, &status, 0);
	if (ret >= 0){
		//wait success                    
		printf("wait child success...\n");
		if (WIFEXITED(status)){
			//exit normal                                 
			printf("exit code:%d\n", WEXITSTATUS(status));
		}
		else{
			//signal killed                              
			printf("killed by siganl %d\n", status & 0x7F);
		}
	}
	sleep(3);
	return 0;
}

 在父进程运行过程中,我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功。

阻塞和非阻塞轮询

如何理解阻塞轮询

如果子进程一直不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态 ——>通过这个我们可以知道阻塞不仅仅只是发生在向硬件发送请求时等待他的状态准备好,还可以发生在父进程在等待子进程结束从而获取他的状态。

如何理解非阻塞轮询呢?

你还有3天就要C语言考试了,但是你不以为然于是先玩了2他,当第3天的时候你慌了因为你平时上课没听而且啥也不懂,所以你找了一个班里的努力型学霸小张(喜欢学习并且做了很多笔记) 于是你走到楼下 但是你又懒得上去,于是你就打电话给小张“你能不能跟我去图书馆帮我复习几个小时,顺便教教我把笔记借我看看呗” 小张说:“好,但是我现在笔记还有几页没看完,你再楼下等等我,我等会就下去……” 然后你就把电话挂了,然后过了5分钟,你发现小张还没下来,然后你又打了电话,但是小张还是说等会就下去,就这样你打了十多个电话,终于小张下来了,于是你们开开心心地去往图书馆了……

 在这个过程中,你就是用户,你打电话的过程就是调用系统调用的过程,而小张就是操作系统,当你打电话询问小张的这个过程其实就是想操作操作系统询问:“你当前的状态准备好了没有?(检查状态)”   小张说等会就下来,于是你挂电话  其实就是你检查不成功,先结束系统调用(系统调用立马返回)  这就是非阻塞!! 而你一直给小张打电话其实就是轮询 (不断询问 有while循环)所以加在一起就是           非阻塞轮询!

最后你考过了,你很开心,而是数据结构老师又告诉你明天要考试,你又没听,于是你想到了找小张,但是历史的经验告诉你肯定得打很多电话,上次手机都打欠费了。于是这次你换了一个思路,在小张告诉你再等会的时候,你就要求他不要挂断电话,直到下楼的时候再挂,这样我可以随时知道你的情况     

这个过程其实就是阻塞!! 也就是系统调用会卡住,会被链接到子进程的一个阻塞队列中等待。

你又过了,你特别开心,但是操作系统明天又要考试了,于是你给小张打电话,但是你也不知道小张不会立马下来,所以你自己也带了本书,在等小张的时候自己也不会闲着没事。可以自己看会书

这个过程描述的就是,阻塞的方式虽然简单且应用较多,但是也比较呆,因为父进程在等待的时候啥也干不了,非阻塞轮询相比较于阻塞来说,可以多做一些自己的事情,比如说我可以做一些检查的工作! 

父进程在非阻塞轮询时可以做什么事,如果这件事任务太重到时没时间等怎么办?

一般来说这种事都是一些比较轻的工作,因为我们核心的任务是等待子进程,所以一般来说都是做一些检查之类的简单任务。 

获取子进程的退出信息(阻塞式等待)

多进程的代码逻辑

1.  如果是多进程的话,waitpid的第一个参数可以用-1,让父进程等待任意一个子进程,然后子进程有多少最好用一个宏,这样父进程可以知道子进程的数目,轮询的时候我们就不能一下子break掉,而是需要维护一个计数器,没等待完一个子进程就去统计一下

2、创建很多的子进程,但是具体哪个先去执行是由调度器决定的,但是我们必须知道的就是最后一个结束的必然就是父进程,因为子进程都是他创建的,所以他理所应当去回收所有的子进程 

3、进程最重要的三个核心:进程创建、进程等待、进程终止。所以我们在需要多进程的时候,我们的代码核心首先要考虑以下要素:(1)需要有循环fork创建子进程 (2)需要在合适的时候让子进程退出(常用exit)(3)父进程必须等待子进程(阻塞就是一直卡住等,非阻塞轮询就是得需要一个while循环 反复调用)  他有义务回收所有子进程!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t ids[10];
	for (int i = 0; i < 10; i++){
		pid_t id = fork();
		if (id == 0){
			//child
			printf("child process created successfully...PID:%d\n", getpid());
			sleep(3);
			exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标
		}
		//father
		ids[i] = id;
	}
	for (int i = 0; i < 10; i++){
		int status = 0;
		pid_t ret = waitpid(ids[i], &status, 0);
		if (ret >= 0){
			//wait child success
			printf("wiat child success..PID:%d\n", ids[i]);
			if (WIFEXITED(status)){
				//exit normal
				printf("exit code:%d\n", WEXITSTATUS(status));
			}
			else{
				//signal killed
				printf("killed by signal %d\n", status & 0x7F);
			}
		}
	}
	return 0;
}

运行代码,这时我们便可以看到父进程同时创建多个子进程,当子进程退出后,父进程再依次读取这些子进程的退出信息

基于非阻塞接口的轮询检测方案

上述所给例子中,当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

做法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。

例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t id = fork();
	if (id == 0){
		//child
		int count = 3;
		while (count--){
			printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(0);
	}
	//father
	while (1){
		int status = 0;
		pid_t ret = waitpid(id, &status, WNOHANG);
		if (ret > 0){
			printf("wait child success...\n");
			printf("exit code:%d\n", WEXITSTATUS(status));
			break;
		}
		else if (ret == 0){
			printf("father do other things...\n");
			sleep(1);
		}
		else{
			printf("waitpid error...\n");
			break;
		}
	}
	return 0;
}

运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。 

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值