深入剖析Linux——进程信号

致前行的人:

                要努力,但不着急,繁花锦簇,硕果累累都需要过程!

目录

1.信号概念

1.1生活角度的信号

2. 技术应用角度的信号

3.Linux操作系统中查看信号

4.常用信号发送

4.1通过键盘发送信号

4.2调用系统函数发送信号

4.3硬件异常产生信号

4.4软件条件产生信号

5.核心转储

6.不同信号的意义

7.阻塞信号

7.1信号常见概念:

7.2信号在内核中的表示

7.3sigset_t

7.4信号操作函数

7.5sigprocmask

7.6sigpending

 7.7实例代码

8.捕捉信号

8.1如何实现信号的捕捉

8.2sigaction

9.可重入函数

10.volatile

11.SIGCHLD信号


1.信号概念

1.1生活角度的信号

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

2. 技术应用角度的信号

如何将上面的概念,迁移到进程中呢?

1.首先我们都知道,信号是给进程发的,那进程是如何识别信号的呢?因为进程本身就是程序员编写的属性的逻辑的集合,所以进程识别信号是通过程序员编码完成的;2.当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理;3.进程必须对信号具有保存能力;4.进程在处理信号的时候,会有三种动作(默认动作,自定义动作,忽略动作),当进程收到信号时就称之为信号捕捉

上面我们知道进程必须对信号具有保存能力,而进程要保存,应该保存在哪里呢?通过之前的学习我们知道,当一个进程被加载到内存的时候,操作系统会创建PCB对象,保存进程的相关属性,其中就一个字段是用来保存进程是否收到信号,当进程收到信号的时候就将该字段的对应信号的比特位置为1,所以发送信号并保存信号的本质是修改PCB中的信号位图。

PCB的管理者是OS,既然是要修改PCB中的内容,只能由操作系统自己进行修改,所以在上层我们进行发送信号一定要调用操作系统提供的对应接口,所以在使用kill命令进行发送信号的时候一定在底层调用了对应的系统调用接口

 3.Linux操作系统中查看信号

kill-l 查看所有的信号:如图所示

 【1,31】称为称为普通信号【34,64】称为实时信号

4.常用信号发送

4.1通过键盘发送信号

1. 用户输入命令,在Shell下启动一个前台进程。
用户按下
Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
 

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    while(true)
    {
        cout << "我是一个进程" << getpid() << endl;
    }
    return 0;
}


按Ctrl + c时本质上就是给进程发送2号信号,操作系统从键盘上捕捉到信号 ,然后终止前台正在运行的进程。

验证ctrl + c是2号信号 

下面介绍一个函数是专门进行捕捉信号:signal

函数原型:

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

参数介绍:

signum:要进行捕捉的信号

handler:捕捉信号之后采用回调的方式执行要采取的动作

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;


void handler(int signao)
{
    cout<< "捕捉了一个信号,信号编号是:"<< signao << endl;
}
int main()
{
    signal(2,handler);
    while(true)
    {
        cout << "我是一个进程" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

2. 用户输入命令,在Shell下启动一个前台进程。
用户按下
Ctrl-\ ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    while(true)
    {
        cout << "hello world" << getpid() <<endl;
        sleep(1);
    }
    return 0;
}

 Ctrl + \对应的就是3号信号

4.2调用系统函数发送信号

1.调用函数kill

 参数介绍:

第一个参数:要终止进程的pid

第二个参数:要发送的信号

返回值:On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately.

使用介绍:调用kill函数终止掉任意进程

//mysignal.cc
static void Usage(const string& proc)
{
    cout << "\nUsage: " << proc << " pid signo\n" << endl;
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        //使用手册说明
        Usage(argv[0]);
        exit(1);
    }
    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int n = kill(pid,signo);
    if(n != 0)
    {
        perror("kill fail:");
    }
    return 0;
}
//mytest.cc

int main()
{
    while(true)
    {
        cout << "我是一个正在运行的进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

 2.调用函数raise

参数介绍:

sig:要发送的信号 

返回值:returns 0 on success, and nonzero for failure.

使用说明:给自己发送任意信号

//给自己发送任意信号:
int main()
{
    int count = 0;
    while(count <= 10)
    {
        printf("%d\n",count++);
        if(count >= 5)
            raise(9);
    }
    return 0;
}

3.调用函数abort 

使用说明:给自己发送指定信号

int main()
{
    int count = 0;
    while(count <= 10)
    {
        printf("%d\n",count++);
        if(count >= 5)
            abort();
    }
    return 0;
}

 调用abort函数终止了该进程,但是给进程发送的是几号信号呢?下面我们来验证一下:

 通过验证发现调用abort函数是给进程发送了6号信号

4.3硬件异常产生信号

信号产生不一定是用户显示的发信号,也有可能是硬件异常,操作系统主动发送信号

1.如下面这段代码:

int main()
{
    printf("运行中.....\n");
    sleep(1);
    int a = 10;
    a /= 0;
    return 0;
}

当在代码出现除0的操作时,报了一个浮点数错误,并且终止掉了该进程 

那为什么会终止进程呢?

这里是因为收到了OS系统发的信号,那操作系统发的是哪个信号呢?通过验证发现操作系统发的是8号信号:SIGFPE

那8号信号是什么呢?

 通过调用signal函数验证8号信号:

void catchSig(int signo)
{
    cout << "获取到一个信号,信号编号是:" << signo << endl;
    sleep(1);
}
int main()
{
    signal(SIGFPE,catchSig);
    while(true)
    {
        printf("运行中.....\n");
        sleep(1);
        int a = 10;
        a /= 0;
    }
    return 0;
}

 通过验证也发现,当除0的时候,操作系统确实给该进程发送8号信号

此时就有了新的疑问,当进行一次/=0的时候,为什么会一直收到信号呢?,除此之外,操作系统是如何得知我/0了呢?

关于上面的两个问题,其实是跟硬件有关系的,下面我们具体来看看:

此时我们就回答了上面的一个问题,操作系统如何得知我/0了,对于另外一个问题/0一次,会一直捕捉

这是因为收到信号不一定会引起进程退出,没有退出,就有可能被调到,CPU内部的寄存器只有一份,但是寄存器中的内容,是属于进程的上下文,当进程被切换的时候,就有无数次寄存器被保存和恢复的过程,所以每一次恢复的时候,让OS系统识别到了CPU内部状态寄存器的溢出标志位为1,所以就出现了上面的现象,在/一次0之后,就会一直捕捉到8号信号

2.如下面的这段代码:

int main()
{
    while(true)
    {
        printf("运行中.....\n");
        sleep(1);
        int* p = nullptr;
        *p = 100; //野指针错误
    }
    return 0;
}

当出现野指针问题的时候,操作系统发的是几号信号呢?

void catchSig(int signo)
{
    cout << "获取到一个信号,信号编号是:" << signo << endl;
    exit(1);
}
int main()
{
    signal(SIGFPE,catchSig);
    while(true)
    {
        printf("运行中.....\n");
        sleep(1);
        int* p = nullptr;
        *p = 100; //野指针错误
    }
    return 0;
}

通过验证发现,当出现野指针错误的时候操作系统发送的是11号信号

还是和上面相同的问题,操作系统是如何得知我写的代码有空指针的问题呢?

当空指针解引用从虚拟地址映射到物理空间的时候,CPU中有一个MMU的内存管理单元,当遇到空指针解引用的时候MMU就会出现异常,然后被操作系统得知,操作系统向进程发送信号 

4.4软件条件产生信号

1.在管道通信的时候,当读端关闭,写端一直写的时候,OS系统就会向写端发送13号信号SIGPIPE终止掉写端,把这种产生信号的方式就被称为由软件条件产生的信号

2.调用函数alarm

使用说明:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

int main()
{
    alarm(1);
    int cnt = 0;
    while(true)
    {
        printf("%d\n",cnt++);
    }
    return 0;
}

验证alarm给进程发送SIGALRM信号:

int cnt = 0;
void catchSig(int signo)
{
    cout << "获取到一个信号" << cnt << endl;
    exit(1);
}
int main()
{
    signal(SIGALRM,catchSig);
    alarm(1);
    while(true)
    {
        cnt++;
    }
    return 0;
}

注:alarm(0)可以取消闹钟 

如何理解“闹钟”是软件条件产生的信号:

任意一个进程,都可以通过alarm系统调用在内核中设置闹钟,OS系统内可能存在着很多闹钟,那操作系统该如何管理这些闹钟:

所以说闹钟是软件条件产生的信号。 

信号产生总结:

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

OS是进程的管理者
信号的处理是否是立即处理的?在合适的时候信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

需要被暂时记录下来,记录在进程的PCB中
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

知道,就如同在没有看到红灯之前就已经知道,看到红灯之后应该如何处理

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

当操作系统向进程发送信号时,本质上是在修改进程PCB的位图。

5.核心转储

概念:当进程异常退出的时候,进程在对应的时刻,将内存中的有效数据转储到磁盘中。

现象:

进程在退出的时候,一般会有两种情况,一种是Term表示正常退出,一种是core表示异常退出:

如图所示:

在云服务器上,默认进程如果是以core退出的,暂时看不到明显的现象,如果想要看到需要打开core file选项:

int main()
{
    int a[10] = { 0 };
    a[10000] = 100;
    return 0;
}

 数组越界访问时进程会异常退出:

当打开core file选项的时候就会产生一个临时文件:文件名=core + 进程的pid  

该文件是可被调试的,那如何进行调试呢?

如图所示:

 通过这种方式就能快速定位进程异常的位置了

6.不同信号的意义

通过上面的学习,我们已经了解了信号产生的几种方式,虽然产生信号的方式有所不同,但是大部分信号,默认处理动作都是终止进程,既然默认处理动作都是终止进程,那为什么还需要分那么多的信号呢?这是因为信号不同,代表不同的时间,当进程被终止的时候,能够更快的定位到进程是因为什么而终止的

注:kill -9信号是管理员信号,即使所有的信号被捕捉时,无法终止进程,9号信号也能终止掉该进程:

例:

void catchSig(int signo)
{
    cout << "获取到一个信号" << signo << endl;
}
int main()
{
    for(int signo = 1; signo <= 31; signo++)
    {
        signal(signo,catchSig);
    }
    while(true)
    {
        printf("我正在运行>>%d\n",getpid());
        sleep(1);
    }
    return 0;
}

7.阻塞信号

7.1信号常见概念:

实际执行信号的动作称为信号递达

信号从产生到递达之间的状态称为信号未决

进程可以阻塞某个信号

被阻塞的信号在产生时将保持未决状态,直到信号解除对此信号的阻塞,才执行递达的动作

注:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是信号是在递达之后的一种可选的处理动作

7.2信号在内核中的表示

信号在内核的表示示意图:

 执行逻辑:

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志,当信号产生时,信号被阻塞,所以暂时不递达,当信号没有被阻塞,并且该信号处于未决状态,该信号被递达

7.3sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

7.4信号操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

7.5sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。 

SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于 maks = mask | set

SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~ set

SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask = set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

7.6sigpending

#include <signal.h>

int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

 7.7实例代码

#include<iostream>
#include<vector>
#include<signal.h>
#include<unistd.h>
#define MAX_SIGNUM 31
using namespace std;

static vector<int> sigarr = {2};
static void show_pending(const sigset_t &pending)
{
    for(int signo = MAX_SIGNUM; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else cout << "0";
    }
    cout << "\n";
}
static void myhandler(int signo)
{
    cout << signo <<"号信号被递达" << endl;
}
int main()
{
    //执行自定义动作
    for(const auto& sig : sigarr)
    {
        signal(sig,myhandler);
    }
    //屏蔽指定信号:
    sigset_t block,oblock,pending;
    //初始化:
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    //添加要屏蔽的信号:
    for(const auto& sig : sigarr)
    {
        sigaddset(&block,sig);
    }
    //开始屏蔽,设置进入内核:
    sigprocmask(SIG_SETMASK,&block,&oblock);
    //遍历打印pending信号集:
    int cnt = 10;
    while(true)
    {
        //初始化:
        sigemptyset(&pending);
        //获取:
        sigpending(&pending);
        //打印:
        show_pending(pending);
        sleep(1);
        if(cnt-- == 0)
        {
            //对特定信号解除屏蔽,让操作系统递达该信号
            sigprocmask(SIG_SETMASK,&oblock,&block);
            cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
        }
    }
    return 0;
}

运行截图:

8.捕捉信号

8.1如何实现信号的捕捉

信号在产生的时候,不会立即被处理,而是在合适的时候进行处理,所谓合适的时候就是从内核态返回到用户态的时候进行处理。

对于内核态和用户态又该如何理解呢?

 如何标识当前进程的身份是用户态还是内核态:

进程是如何进入操作系统执行对应的方法呢?

每一个进程都有自己的地址空间,内核空间被映射到了[3,4]G的空间,当进程想要访问操作系统的接口时,只需要在自己的地址空间进行跳转就可以了

注:进入内核态的方式,第一种是在调用系统调用接口,第二种是进程切换的时候

捕捉信号的具体过程:

逻辑抽象:

8.2sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:

 

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void handler(int signo)
{
    cout<< "get a signo" << signo << endl;
}
int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    //捕捉2号信号
    sigaction(SIGINT,&act,&oact);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

运行截图:

当某个信号处理的函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段。

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
void handler(int signo)
{
    cout<< "get a signo" << signo<< "正在处理中……"<< endl;
    Count(20);
}
int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    //当正在处理某一种信号,也想屏蔽其它信号的时候,可以将屏蔽的信号加入sa_mask中
    sigaddset(&act.sa_mask,3);
    //捕捉2号信号
    sigaction(SIGINT,&act,&oact);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

 运行截图:

9.可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。 

如果在main函数和在handler函数中insert函数被重复进入,出现问题,该函数就被称为是不可重入函数

如果在main函数和在handler函数中insert函数被重复进入,没有出现问题,该函数就被称为是可重入函数

如果一个函数符合以下条件之一则是不可重入的:

1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

10.volatile

#include<stdio.h>

int quit = 0;
void handler(int signo)
{
    printf("%d号信号正在被捕捉\n",signo);
    printf("quit:%d\n",quit);
    quit = 1;
    printf("->%d\n",quit);
}
int main()
{
    signal(2,handler);
    while(!quit);
    printf("我是正常退出的!!\n");
    return 0;
}

标准情况下,键盘输入CTRL+c,2号信号被捕捉,执行自定义动作,修改quit = 1,while(条件不满足),退出循环,进程退出。

运行截图:

将该进程进行优化为-O3 

mysignal:mysignal.c
	gcc -o $@ $^ -O3 #-std=c++11
.PHONY:clean 
clean:
	rm -rf mysignal 

运行截图:

优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 quit=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显quit肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的quit,并不是内存中最新的quit,这就存在了数据二异性的问题。 while 检测的quit其实已经因为优化,被放在了CPU寄存器当中。

CPU处理数据的过程分为:取指令,分析指令,执行指令,将结果写回到内存中

未优化前:

优化后:因为编译器识别到在main函数内部中并没有对quit进行修改,而是每次做一个判断,所以直接将quit变量中的内容写入到寄存器中,下一次再判断的时候直接使用寄存器变量中的值,所以while()条件一直为假,进程也就没有退出

如何解决呢?很明显需要 volatile 

运行截图:

volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作 

11.SIGCHLD信号

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD(17)信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait,清理子进程即可。

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

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
void handler(int signo)
{
    //1.有非常多的子进程,在同一时刻退出了
    //处理方案:waitpid(-1) while(1) 采用循环的方式等待任意一个进程
    //2.有非常多的子进程,在同一时刻退出了一部分:
    //处理方案:采用非阻塞式的等待方式等待任意一个进程
    // while(1)
    // {
    //     pid_t ret = waitpid(-1,NULL,WNOHANG);
    //     if(ret == 0) break;
    // }
    printf("pid:%d,%d号信号正在被捕捉\n",getpid(),signo);

}
int main()
{
    signal(17,handler);
    printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
    pid_t id = fork();
    if(id == 0)
    {
        printf("我是子进程,pid:%d,ppid:%d,我要退出了\n",getpid(),getppid());
        Count(5);
        exit(1);
    }
    while(1) sleep(1);
    return 0;
}

运行截图:

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

运行截图:5秒过后子进程自动被回收了 

猜你喜欢

转载自blog.csdn.net/qq_65307907/article/details/129179238