Linux:进程信号

生活角度的信号

a.信号在生活中,随时可以产生(信号的产生和我是异步的)

b.你能认识这个信号

c.我们知道信号产生了,我能识别这个信号,信号该怎么处理

d.我们可能正在做着更重要的事情,把到来的信号暂不处理(1.我记得这个事 2.合适的时候处理)

信号介绍

在bash上执行命令kill -l便可看到系统定义的所有信号

我们只研究前31个信号,后面31个是实时信号这里不做研究

每个信号都有一个编号和一个宏定义名称,这些宏定义都可以在signal.h中找到,在man手册中还可以找到各种信号的详细信息

man 7 signal

这里具体介绍了信号在什么时候产生,处理的动作是什么

信号概念的基本储备

信号:Linux系统提供的一种,向指定进程发送特定事件的方式,做识别和处理。

信号产生是异步的。

信号处理常见方式

1、忽略该信号

2、执行信号的默认处理动作(终止自己、暂停、忽略.....)

3、提供一个信号处理函数,要求内核在处理信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个异常

Core和Term:默认动作都是终止,但Core能产生Core文件 

如何理解信号的发送和保存?

进程---task_struct---struct---成员变量---用位图来保存收到的信号

uint32_t signals;

0000 0000 0000 0000 0000 0000 0000 0000

发送信号:修改指定进程pcb中的信号指定位图,0->1,写信号

pcb:内核数据结构对象,只有OS有资格修改内核结构对象中的值

信号产生具体过程

kill命令

通过kill命令向指定进程发送指定信号

kill -数字 进程号

通过终端按键来产生信号

ctrl+c  2)SIGINT 向当前进程发送2号信号

ctrl+\  3)SIGQUIT向当前进程发送3号信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并Core Dump,我们在Linux环境下来验证一下,先来了解一下什么是Core Dump

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存在磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有BUG,比如非法访问内存导致段错误,事后可以用调试器检查core文件以查清楚错误原因,这叫做事后调试,一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中),默认是不允许改变这个限制,允许产生core文件。首先用ulimit命令来改变shell进程的Resource Limit,允许core文件最大为1024k

通过信号返回值我们发现有一个core dump位。

云服务器默认关闭这个core文件功能

ulimit -c 1024 //打开这个文件功能

写一个死循环程序

编译并执行程序:

看到的现象是先打印出pid然后一直在死循环,按下组合键ctrl+\后退出并提示core dumped

test程序也会core  dump的原因是我们先修改了shell的Resource Limit值,而test进程是由shell产生的所以test进程的PCB也是由shell复制而来,所以test进程和shell就具有相同的Resource Limit值,所以就会产生core  dump了。

如图所示就是产生的core文件

最新的Linux版本所生成的core文件没有后缀,同一个文件如果多次执行多次异常终止,那么生成的core文件还是一份。这样就避免了一份文件生成多份core文件占内存。

调用系统函数来向进程发信号

kill函数

参数解释:

第一个参数进程id

第二个参数信号标号

返回值:成功返回0失败返回-1

./mykill 2 1234
 int main(int argc, char *argv[])
 {
     if(argc != 3)
     {
         std::cerr << "Usage: " << argv[0] << " signum pid" << std::endl;
         return 1;
     }

     pid_t pid = std::stoi(argv[2]);
     int signum = std::stoi(argv[1]);
     kill(pid, signum);
 }

raise函数

发信号给自己 == kill(getpid(), sig)

只能向当前进程发送信号

参数解释:

信号标号

返回值:成功返回0失败返回-1

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

int main()
{
        printf("raise befor.");
        raise(9);//结束自己。相当于_exit
        printf("raise after.\n");
        return 0;
}
CLC@Embed_Learn:~/linux_io/02/02/seven$ ./a.out 
Killed       //并未打印出raise befor.,所以相当于_exit

信号处理函数:

 abort函数

函数功能:使当前进程接收到信号而异常终止

参数:无参数

返回值:无返回值

void handler(int sig)
 {
     std::cout << "get a sig: " << sig << std::endl;
 }

 int main()
 {
     int cnt = 0;

      signal(SIGABRT, handler);
   
     while (true)
     {
         sleep(1);
         std::cout << "hello bit, pid: " << getpid() << std::endl;

          abort();
     }
 }

信号虽被捕捉,但abort正常接收终止进程

signal函数

void (*signal(int signum, void (*handler)(int)))(int);

令A= void (*handler)(int) = 函数指针变量。

void (*signal(int signum, A))(int);
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函数;
这个函数的返回值是一个函数指针。

void handler(int sig)
 {
     std::cout << "get a sig: " << sig << std::endl;
 }

int main()
{


signal(SIGABRT, handler);


 return 0;
}

捕捉到SIGABRT信号后,可以执行我们所定义的函数,并将signum传入函数sig

注意:

该函数设置一次,进程不结束可以无限捕捉

并不是所有的信号都可以捕捉,9号信号不允许自定义捕捉

软件条件产生

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动 作是终止当前进程。

函数功能:

设定一个闹钟,告诉内核在seconds秒后给当前进程发送一个SIGALRM信号,该信号的默认处理动作是终止当前进程

函数参数解释:闹钟的时间是多少秒

函数返回值:这个函数的返回值是0或者闹钟剩下的秒数,

当你一直不修改闹钟,直到闹钟响这时的返回值是0

当在设定的秒数之内修改了闹钟的秒数就会返回上个闹钟剩下的时间,将seconds值设为零表示取消闹钟

void handler(int sig)
{
    std::cout << "get a sig: " << sig << std::endl;
}

int main()
 {
     signal(SIGALRM, handler);

     alarm(3); // 设定1S后的闹钟 -- 1S --- SIGALRM
      int n =  alarm(0): 取消闹钟, 上一个闹钟的剩余时间
      std::cout << "n : " << n << std::endl;//3
      sleep(10);
 }

OS对闹钟如何做管理?先描述再组织  

struct alarm

{

time_t expired;//未来的超时时间=seconds+Now();时间戳

pid_t pid;

fun_t f;

.....

}

这里是用大堆或者小堆将结构体链式存储起来的

硬件异常产生信号

程序为什么会崩溃???非法访问和操作,导致OS给进程发信号

非法访问:SIGSEGV信号

非法操作:SIGFPE信号

崩溃了为什么会退出?可以不退出吗,可以。

默认是终止进程,捕捉信号即可不退出,但最好终止退出进程释放进程的上下文数据,包括溢出标志数据或者其他异常数据。

int main()
{
    // 程序为什么会崩溃???非法访问、操作(?), 导致OS给进程发送信号啦!! --- 为什么
    // signal(SIGSEGV, handler);
    // signal(SIGFPE, handler);
    //  崩溃了为什么会退出?默认是终止进程
    //  可以不退出吗?可以,捕捉了异常, 推荐终止进程(为什么?) --- 为什么?

    // int *p = nullptr;
    // *p = 100; // SIGSEGV
    int a = 10;
    a /= 0;      // 8) SIGFPE
    while (true)
    {
        std::cout << "hello bit, pid: " << getpid() << std::endl;

        sleep(1);
    }
}

阻塞信号

信号在内核中的表示

信号在内核中一般有三种状态:

(1)信号递达(Delivery):实际执行信号的处理动作称为信号递达(处理)(终止,忽略,暂停)

(2)信号未决(Pending):信号从产生到递达之间的状态;

(3)信号阻塞(Block):被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;

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

一个信号的阻塞和它有没有未决没有关系

下面来看一下操作系统为每个进程提供的一套信号机制:

上图的三张表分别为:阻塞表(Block)未决表(Pending)递达表
这三张表分别对应三种不同的状态:信号阻塞、信号未决、信号递达之后的自定义捕捉
前两张表都是通过位图来存储的(决定了当前是否能收到信号),信号被阻塞就将相应位置置1,否则就置0。而在pending表中,当前位是1时表示信号存在,置0时表不存在。(pending表中的数据是判断信号是否存在的重要因素)

pending底层原理:

位图 int 0000 0000 0000 0000 0000 0000 0000 0000

比特位的位置:代表信号编号

比特位的内容:代表信号是否收到(存在)

handler数组

信号的编号就是数组的下标,可以采用信号编号,索引信号处理方法

signal(2,handler)

block底层原理:

一张位图和pengding类型完全一样

0000 0000 0000 0000 0000 0000 0000 0000

比特位的位置:代表信号编号

比特位的内容:代表信号是否阻塞

SIGHUP信号(也就是(1)号信号)未阻塞也未产生过,当它抵达时执行默认处理工作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号从未产生过,一旦产生,SIGQUIT信号将被阻塞,它的处理动作是用户自定义的函数singhandler。

总结:两张位图+一张函数指针数组 == 让进程识别信号

小问题:

倘若在进程解除对某种信号的阻塞之前这种信号产生过多次,将会如何处理?

解析:Linux下的实现方式:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

小知识点:普通信号允许丢失、实时信号是不允许的

sigset_t  

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略

本质是一个长整型数组(每个4字节)组成的结构体,用它作位图

 struct bits
 {
     uint32_t bits[400]; // 400*32
 };
 40
 40/(sizeof(uint32_t)*8) = 1 -> bits[1]
 40%(sizeof(uint32_t)*8) = 8 -> bits[1]:8

sigset_t Linux给用户提供的一个用户级的数据类型, 禁止用户直接修改位图

信号集操作函数

#include <signal.h>
// 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信息
int sigemptyset(sigset_t  *set)
// 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信息包含系统支持的所有信号
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置位,表示该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
返回值:
这四个函数都是成功返回0,出错返回-1。
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

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

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

(1)若oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
(2)若set是非空指针,则更改进程的信号屏蔽字,参数how至少如何更改
(3)若oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字

how参数:

* SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
* SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
* SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set

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

sigpending

#include <signal.h>
int sigpending(sigset_t *set);

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

实验:

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "curr process[" << getpid() << "]pending: ";
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

void handler(int signo)
{
    std::cout << signo << " 号信号被递达!!!" << std::endl;

}

int main()
{
    // 0. 捕捉2号信号
    signal(2, handler); // 自定义捕捉
    signal(2, SIG_IGN); // 忽略一个信号
    signal(2, SIG_DFL); // 信号的默认处理动作

    // 1. 屏蔽2号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, SIGINT); // 我们有没有修改当前进行的内核block表呢???1 0
    // 1.1 设置进入进程的Block表中
    sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽!

    int cnt = 15;
    while (true)
    {
        // 2. 获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);

        // 3. 打印pending信号集
        PrintPending(pending);
        cnt--;

        // 4. 解除对2号信号的屏蔽
        if (cnt == 0)
        {
            std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }

        sleep(1);
    }
}

由上图实现结果我们看到SIGINT信号被阻塞,所以按kill之后SIGINT信号处于未决状态,此时信号集数据由0表1。经过15s后,我们将其捕捉处理,未决状态清0。

补充:

1.解除屏蔽,一般会立即处理当前被解除的信号(如果被pending)

2.pending位图对应的信号也要被清0,在递达之前。

捕捉信号

自定义处理信号可能不会被立即处理,而是在合适的时候处理

进程从内核态返回到用户态的时候进行检测和处理

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。

内核态和用户态在地址空间上的理解

谈谈键盘输入数据的过程

我们学习的信号就是模拟中断实现的

信号:纯软件

中断:软件+硬件

谈谈理解OS如何正常运行

1.如何理解系统调用

我们只要找到特定数组下标(系统调用号)的方法,就能执行系统调用了

pid_t fork()

{

   mov 2 eax;//系统调用号放入寄存器中

   int 0x80;用户态->内核态

}

2.OS是如何运行的

操作系统本质就是一个死循环+时钟中断 不断调度系统的任务的

OS不相信任何用户,用户无法直接跳转到[3,4]GB内核空间

必须在特定的条件下才能跳转过去(3->0)(硬件cpu配合)

sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体;
  • sa_handler 赋值为常数 SIG_IGN传给sigaction表示忽略信号 , 赋值为常数SIG_DFL 表示执行系统默认动作, 赋值为一个函数指针表示用自定义函数捕捉信号 , 或者说向内核注册了一个信号处理函数 , 该函数返回值为void, 可以带一个 int 参数 , 通过参数可以得知当前信号的编号 , 这样就可以用同一个函数处理多种信号。显然, 这也是一个回调函数 , 不是被 main 函数调用 , 而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}
  • sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
  • sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
  • sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。 

SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

// 当前如果正在对n号信号进行处理,默认n号信号会被自动屏蔽
// // 对n号信号处理完成的时候,会自动解除对n号信号的屏蔽
// // 为什么?
 void handler(int signum)
 {
     std::cout << "get a sig: " << signum << std::endl;
     while(true)
     {
         sigset_t pending;
         sigpending(&pending);

         Print(pending);

        
          sleep(30);
          break;
     }
     // exit(1);
 }

 int main()
 {
     struct sigaction act, oact;
     act.sa_handler = handler;
     sigemptyset(&act.sa_mask); 
// 如果你还想处理2号(OS对2号自动屏蔽),同时对其他信号也进行屏蔽
     sigaddset(&act.sa_mask, 3);
     act.sa_flags = 0;

sigaction(2, &act, &oact);

     while(true)
     {
         std::cout << "I am a process, pid: " << getpid() << std::endl;
         sleep(1);
     }
     return 0;
 }

for(int i = 0; i <= 31; i++)
    sigaction(i, &act, &oact);

9号信号不能被捕捉处理

可重入函数

main 函数调用 insert 函数向一个链表 head 中插入节点 node1, 插入操作分为两步 , 刚做完第一步的 时候 , 因为硬件中断使进程切换到内核, 再次回用户态之前检查到有信号待处理 , 于是切换 到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2, 插入操作的 两步都做完之后从sighandler返回内核态 , 再次回到用户态就从 main 函数调用的 insert 函数中继续 往下执行 , 先前做第一步之后被打断, 现在继续做完第二步。结果是 ,main 函数和 sighandler 先后向链表中插入两个节点 , 而最后只有一个节点真正插入链表中了。
像上例这样 ,insert 函数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数 , 这称为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为 不可重入函数 , 反之 , 如果一个函数只访问自己的局部变量或参数, 则称为可重入 (Reentrant) 函数。想一下 , 为什么两个不同的控制流程调用同一个函数, 访问它的同一个局部变量或参数就不会造成错乱 ?
如果一个函数符合以下条件之一则是不可重入的:
  • 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

该关键字在 C 当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
 volatile int gflag = 0;

 void changedata(int signo)
 {
     std::cout << "get a signo:" << signo << ", change gflag 0->1" << std::endl;
     gflag = 1;
 }

 int main() // 没有任何代码对gflag进行修改!!!
 {
     signal(2, changedata);

     while(!gflag); // while不要其他代码
     std::cout << "process quit normal" << std::endl;
 }

如果没有volatile,变量gflag将被优化,默认取0

但实际情况,我们收到信号改变了gflag的值,后面的代码不会被执行

SIGCHLD信号

进程一章讲过用 wait waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻 塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不 能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。其实, 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。
请编写一个程序完成以下功能 : 父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数, 在其中调用wait获得子进程的退出状态并打印。
事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 :
父进程调用sigactionSIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的, 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可用。请编写程序验证这样做不会产生僵尸进程。
void notice(int signo)
 {
     std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
         pid_t rid = waitpid(-1, nullptr, 0); 
         
         if (rid > 0)
         {
             std::cout << "wait child success, rid: " << rid << std::endl;
         }
     
 }

void DoOtherThing()
 {
     std::cout << "DoOtherThing~" << std::endl;
 }
 int main()
 {
     signal(SIGCHLD, notice);
     for (int i = 0; i < 10; i++)
     {
         pid_t id = fork();
         if (id == 0)
         {
             std::cout << "I am child process, pid: " << getpid() << std::endl;
             sleep(3);
             exit(1);
         }
     }
     // father
     while (true)
     {
         DoOtherThing();
         sleep(1);
     }

     return 0;
 }

问题1: 如果一共有10个子进程,且同时退出呢?

答:同时退出只会收到一个信号,然后依次回收

问题2: 如果一共有10个子进程, 5个退出,5个永远不退出呢?

答:5个会回收,然后另外5个子进程会阻塞等待,也可以不阻塞等待

void notice(int signo)
 {
     std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
     while (true)
     {
         pid_t rid = waitpid(-1, nullptr, WNOHANG); // 阻塞啦!!--> 非阻塞方式
         if (rid > 0)
         {
             std::cout << "wait child success, rid: " << rid << std::endl;
         }
         else if (rid < 0)
         {
             std::cout << "wait child success done " << std::endl;
             break;
         }
         else
         {
             std::cout << "wait child success done " << std::endl;
             break;
         }
     }
 }

void DoOtherThing()
 {
     std::cout << "DoOtherThing~" << std::endl;
 }
 int main()
 {
     signal(SIGCHLD, notice);
     for (int i = 0; i < 10; i++)
     {
         pid_t id = fork();
         if (id == 0)
         {
             std::cout << "I am child process, pid: " << getpid() << std::endl;
             sleep(3);
             exit(1);
         }
     }
     // father
     while (true)
     {
         DoOtherThing();
         sleep(1);
     }

     return 0;
 }

对信号进行忽略即可不用回收 

int main()
{
    signal(SIGCHLD, SIG_IGN); // 收到设置对SIGCHLD进行忽略即可
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            std::cout << "child running" << std::endl;
            cnt--;
            sleep(1);
        }

        exit(1);
    }
    while (true)
    {
        std::cout << "father running" << std::endl;
        sleep(1);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你好,赵志伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值