【Linux系统编程】信号

------------->【Linux系统编程/网络编程】(学习目录汇总) <--------------

1.信号的概念

信号在我们的生活中随处可见, 如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪… 他们都有共性:1. 简单 2. 不能携带大量信息 3. 满足某个特设条件才发送

信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。

Unix 早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T 都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1 对可靠信号例程进行了标准化。

2.信号的机制

A 给 B 发送信号,B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行, 去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称 为“软中断”

信号的特质: 由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

每个进程收到的所有信号,都是由内核负责发送的,内核处理。

3.与信号相关的事件和状态

产生信号:

  1. 按键产生,如:Ctrl+cCtrl+zCtrl+\
  2. 系统调用产生,如:killraiseabort
  3. 软件条件产生,如:定时器 alarm
  4. 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
  5. 命令产生,如:kill 命令

递达: 递送并且到达进程。

未决: 产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

信号的处理方式:

  1. 执行默认动作

  2. 忽略(丢弃)

  3. 捕捉(调用户处理函数)

4.信号屏蔽字和未决信号集

Linux 内核的进程控制块 PCB 是一个结构体,task_struct, 除了包含进程 id,状态,工作目录,用户 id,组 id, 文件描述符表,还包含了信号相关的信息,主要指阻塞信号集未决信号集

阻塞信号集(信号屏蔽字):

本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。

未决信号集:

  1. 本质:位图。
  2. 信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
  3. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

阻塞信号集和未决信号集在内核中的结构是相同的,它们都是一个整形数组 (被封装过的), 一共 128 字节 (int [32] == 1024 bit),1024 个标志位,其中前 31 个标志位,每一个都对应一个 Linux 中的标准信号,通过标志位的值来标记当前信号在信号集中的状态。

在这里插入图片描述

5.信号四要素和常规信号一览

5.1 信号的编号

可以使用 kill –l 命令查看当前系统可使用的信号有哪些。

在这里插入图片描述

不存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号),34-64 称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同。

5.2 信号 4 要素

与变量三要素类似的,每个信号也有其必备 4 要素,分别是:

  1. 编号 2. 名称 3. 事件 4. 默认处理动作

注意: 信号使用之前,应先确定其4要素,而后再用!!!

可通过man 7 signal 查看帮助文档获取。

在这里插入图片描述

在这里插入图片描述

默认动作:

  • Term:终止进程
  • Ign: 忽略信号 (默认即时对该种信号忽略操作)
  • Core:终止进程,生成 Core 文件。(查验进程死亡原因, 用于 gdb 调试)
  • Stop:停止(暂停)进程
  • Cont:继续运行进程

这里特别强调了 9) SIGKILL19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。

另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!

5.3 Linux 常规信号一览表

  1. SIGHUP: 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程

  2. SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。

  3. SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。

  4. SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件

  5. SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止里程 并产生 core 文件。

  6. SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。

  7. SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。

  8. SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件。

  9. SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。

  10. SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。

  11. SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。

  12. SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

  13. SIGPIPE:Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。

  14. SIGALRM: 定时器超时,超时的时间 由系统调用 alarm 设置。默认动作为终止进程。

  15. SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程。

  16. SIGSTKFLT:Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。

  17. SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。

  18. SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。

  19. SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。

  20. SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。

  21. SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。

  22. SIGTTOU: 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

  23. SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。

  24. SIGXCPU:进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。

  25. SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

  26. SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程。

  27. SGIPROF:类似于 SIGVTALRM,它不公包括该进程占用 CPU时间还包括执行系统调用时间。默认动作为终止进程。

  28. SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

  29. SIGIO:此信号向进程指示发出了一个异步 IO 事件。默认动作为忽略。

  30. SIGPWR:关机。默认动作为终止进程。

  31. SIGSYS:无效的系统调用。默认动作为终止进程并产生 core 文件。

  32. SIGRTMIN ~ (64) SIGRTMAX:LINUX 的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

6.信号的产生

6.1 终端按键产生信号

Ctrl + c → 2) SIGINT(终止/中断) “INT” ----Interrupt

Ctrl + z→ 20) SIGTSTP(暂停/停止) “T” ----Terminal 终端。

Ctrl + \→ 3) SIGQUIT(退出)

6.2 硬件异常产生信号

除 0 操作 → 8) SIGFPE (浮点数例外)

非法访问内存 → 11) SIGSEGV (段错误)

总线错误 → 7) SIGBUS

6.4 kill 函数/命令产生信号

函数原型:

#include <signal.h>
// 给某一个进程发送一个信号
int kill(pid_t pid, int sig);

函数参数:

​ pid: > 0:发送信号给指定进程

​ = 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。

​ < -1:取绝对值,发送信号给该绝对值所对应的进程组的所有组员。

​ = -1:发送信号给,有权限发送的所有进程。

​ signum:待发送的信号

函数返回值:

​ 成功: 0

​ 失败: -1 errno

小案例

子进程发送信号kill父进程:

在这里插入图片描述

编译运行,结果如下:

在这里插入图片描述

kill -9 -groupname 杀一个进程组

7.定时器

7.1 alarm()函数

设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14)SIGALRM 信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。

函数原型

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

函数参数:

倒计时 seconds 秒,倒计时完成发送一个信号 SIGALRM , 当前进程会收到这个信号,这个信号默认的处理动作是中断当前进程

函数返回值:

大于 0 表示倒计时还剩多少秒,返回值为 0 表示倒计时完成,信号被发出

小案例:

使用这个定时器函数,检测一下当前计算机 1s 钟之内能数多少个数

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

int main()
{
    // 1. 设置一个定时器, 定时1s
    alarm(1);	// 1s之后会发出一个信号, 这个信号将中断当前进程
    int i = 0;
    while(1)
    {
        printf("%d\n", i++);
    }
    return 0;
}

使用 time 命令查看程序执行的时间。 程序运行的瓶颈在于 IO,优化程序,首选优化 IO。

# 直接通过终端输出
$ time ./a.out
real    0m1.013s		# 实际数数用的总时间
user    0m0.060s		# 用户区代码使用的时间
sys     0m0.324s		# 内核区使用的时间

real = user + sys + 消耗的时间(频率的从用户区到内核区进程切换)


# 不直接写终端, 将数据重定向到磁盘文件中
$ time ./a.out > a.txt
Alarm clock

real    0m1.002s    # 用户实际数数的时间变长了
user    0m0.740s
sys     0m0.236s

实际执行时间 = 系统时间 + 用户时间 + 等待时间

7.2 setitimer()函数

设置定时器(闹钟)。 可代替 alarm 函数。精度微秒 us,可以实现周期定时。

函数原型

// 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, 
              struct itimerval *old_value);

函数参数

  • which:指定定时方式

    ① 自然定时:ITIMER_REAL → 14)SIGLARM 计算自然时间

    ② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用 cpu 的时间

    ③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF 计算占用 cpu 及执行系统调用的时间

  • new_value: 给定时器设置定时秒数,传入参数

  • old_value: 上一次给定时器设置的定时信息,传出参数,如果不需要这个信息,指定为 NULL

函数返回值:

​ 成功: 0

​ 失败: -1 errno

类型:

struct itimerval {
struct timeval {
time_t      tv_sec;         /* seconds */
suseconds_t tv_usec;        /* microseconds */
}it_interval;---> 用于设定两个定时任务之间的间隔时间
struct timeval {
time_t      tv_sec;         
suseconds_t tv_usec;        
}it_value;  ---> 第一次定时秒数
};

可以理解为有2个定时器

  • 一个用于第一个闹钟什么时候触发打印
  • 一个用于之后间隔多少时间再次触发闹钟。

小案例

使用setitimer定时,向屏幕打印信息:

在这里插入图片描述

编译运行,结果如下:

在这里插入图片描述

第一次信息打印是两秒间隔,之后都是5秒间隔打印一次

8.信号集操作函数

内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应 用程序中自定义 set 来改变mask。已达到屏蔽指定信号的目的。因为用户是不能直接操作内核中的阻塞信号集和未决信号集的,必须要调用系统函数,关于阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作。

在这里插入图片描述

8.1 信号集设定

#include <signal.h>
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节

// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);		成功:0;失败:-1
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);		成功:0;失败:-1
// 将set集合中某一个信号(signum)对应的标志位设置为1		
int sigaddset(sigset_t *set, int signum);		成功:0;失败:-1
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);		成功:0;失败:-1
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1(相当于判断某个信号是否在集合中)
int sigismember(const sigset_t *set, int signum);		返回值:在集合:1;不在:0; 出错:-1

sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

8.2 sigprocmask()函数

用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB 中) 严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。

函数原型

#include <signal.h>
// 使用这个函数修改内核中的阻塞信号集
// sigset_t 被封装之后得到的数据类型, 原型:int[32], 里边一共有1024个标志位, 每一个信号对应一个标志位
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

函数参数:
how:

  • SIG_BLOCK: 当 how设置为此值,set 表示需要屏蔽的信号。相当于 mask = mask|set
  • SIG_UNBLOCK: 当 how设置为此,set 表示需要解除屏蔽的信号。相当于 mask = mask & ~set
  • SIG_SETMASK: 使用参数 set 集合中的数据覆盖内核的阻塞信号集数据

set:

传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。

oldset:

传出参数,保存旧的信号屏蔽集,如果不需要可以指定为 NULL

函数返回值:

函数调用成功返回 0,调用失败返回 - 1

8.4 sigpending() 函数

读取当前进程的未决信号集

int sigpending(sigset_t *set); set 传出参数。 返回值:成功:0;失败:-1,设置 errno

小案例

需求:
在阻塞信号集中设置某些信号阻塞, 通过一些操作产生这些信号, 然后读未决信号集, 最后再解除这些信号的阻塞
假设阻塞这些信号:

  • 2号信号: SIGINT: ctrl+c
  • 3号信号: SIGQUIT: ctrl+\
  • 9号信号: SIGKILL: 通过shell命令给进程发送这个信号 kill -9 PID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

int main()
{
    // 1. 初始化信号集
    sigset_t myset;
    sigemptyset(&myset);
    // 设置阻塞的信号
    sigaddset(&myset, SIGINT);  // 2
    sigaddset(&myset, SIGQUIT); // 3
    sigaddset(&myset, SIGKILL); // 9 测试不能被阻塞

    // 2. 将初始化的信号集中的数据设置给内核
    sigset_t old;
    sigprocmask(SIG_BLOCK, &myset, &old);

    // 3. 让进程一直运行, 在当前进程中产生对应的信号
    int i = 0;
    while(1)
    {
        // 4. 读内核的未决信号集
        sigset_t curset;
        sigpending(&curset);
        // 遍历这个信号集
        for(int i=1; i<32; ++i)
        {
            int ret = sigismember(&curset, i);
            printf("%d", ret);
        }
        printf("\n");
        sleep(1);
        i++;
        if(i==10)
        {
            // 解除阻塞, 重新设置阻塞信号集
            //sigprocmask(SIG_UNBLOCK, &myset, NULL);
            sigprocmask(SIG_SETMASK, &old, NULL);
        }
    }
    return 0;
}

编译运行,结果如下:

在这里插入图片描述

通过测试最终得到结论:程序中对 9 号信号的阻塞是无效的,因为它无法被阻塞。

9.信号捕捉

Linux 中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略这个信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。程序中进行信号捕捉可以看做是一个注册的动作,提前告诉应用程序信号产生之后做什么样的处理,当进程中对应的信号产生了,这个处理动作也就被调用了。

9.1 signal()函数

使用 signal() 函数可以捕捉进程中产生的信号,并且修改捕捉到的函数的行为,这个信号的自定义处理动作是一个回调函数,内核通过 signal() 得到这个回调函数的地址,在信号产生之后该函数会被内核调用。

函数原型

#include <signal.h>
// 在程序中什么时候产生信号, 程序猿是不知道的, 因此不能在信号产生之后再去处理
// 在信号产生之前, 提供一个注册函数, 用来捕捉信号
//	  - 假设在将来这个信号产生了, 就委托内核进行捕捉, 这个信号的默认动作就不能被执行
//	  - 执行什么样的处理动作 ==> 在signal函数中指定的处理动作
//	  - 如果这个信号不产生, 回调函数永远不会被调用
sighandler_t signal(int signum, sighandler_t handler);   

函数参数:

signum: 需要捕捉的信号

handler: 信号捕捉到之后的处理动作,这是一个函数指针,函数原型typedef void (*sighandler_t)(int);

这个回调函数是需要程序猿写的,但是程序猿不调用,由内核调用,内核调用回调函数的时候,会给它传递一个实参,这个实参的值就是捕捉的那个信号值。

小案例

下面的测试程序中使用 signal () 函数来捕捉定时器产生的信号 SIGALRM:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include <signal.h>

// 定时器信号的处理动作
void doing(int arg)
{
    printf("当前捕捉到的信号是: %d\n", arg);
    // 打印当前的时间
}

int main()
{
    // 注册要捕捉哪一个信号, 执行什么样的处理动作
    signal(SIGALRM, doing);
    // 1. 调用定时器函数设置定时器函数
    struct itimerval newact;
    // 3s之后发出第一个定时器信号, 之后每隔1s发出一个定时器信号
    newact.it_value.tv_sec = 3;
    newact.it_value.tv_usec = 0;
    newact.it_interval.tv_sec = 1;
    newact.it_interval.tv_usec = 0;
    // 这个函数也不是阻塞函数, 函数调用成功, 倒计时开始
    // 倒计时过程中程序是继续运行的
    setitimer(ITIMER_REAL, &newact, NULL);

    // 编写一个业务处理, 阻止当前进程自己结束, 让当前进程被发出的信号杀死
    while(1)
    {
        sleep(1000000);
    }

    return 0;
}

编译运行,结果如下:

在这里插入图片描述

9.2 sigaction()函数

sigaction () 函数和 signal () 函数的功能是一样的,用于捕捉进程中产生的信号,并将用户自定义的信号行为函数(回调函数)注册给内核,内核在信号产生之后调用这个处理动作。sigaction () 可以看做是 signal () 函数是加强版,函数参数更多更复杂,函数功能也更强一些。

函数原型:

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

函数参数:

signum: 要捕捉的信号

act: 捕捉到信号之后的处理动作

oldact: 上一次调用该函数进行信号捕捉设置的信号处理动作,该参数一般指定为 NULL

函数返回值:

函数调用成功返回 0,失败返回 - 1

该函数的参数是一个结构体类型,结构体原型如下:

struct sigaction {
	void     (*sa_handler)(int);    // 指向一个函数(回调函数)
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;             // 初始化为空即可, 处理函数执行期间不屏蔽任何信号
	int        sa_flags;	        // 0
	void     (*sa_restorer)(void);  //不用
};

sa_restorer:该元素是过时的,不应该使用,POSIX.1 标准将不指定该元素。(弃用)

sa_sigaction:当 sa_flags 被指定为 SA_SIGINFO标志时,使用该信号处理程序。(很少使用)

重点掌握:

sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN表忽略 或 SIG_DFL 表执行默认动作

sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。

sa_flags:通常设置为 0,表使用默认属性。

小案例

通过 sigaction () 捕捉阻塞信号集中解除阻塞的信号

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

// 信号的处理动作
void callback(int num)
{
    printf("当前捕捉的信号: %d\n", num);
}

int main()
{
    // 1. 初始化信号集
    sigset_t myset;
    sigemptyset(&myset);
    // 设置阻塞的信号
    sigaddset(&myset, SIGINT);  // 2
    sigaddset(&myset, SIGQUIT); // 3
    sigaddset(&myset, SIGKILL); // 9 测试不能被阻塞

    // 当阻塞的信号被解除阻塞, 该信号就可以被捕捉到了
    // 如果信号被捕捉到之后, 马上就被处理掉了 --> 递达状态
    struct sigaction act;
    act.sa_handler = callback;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT, &act, NULL);
    // 和sigint的处理动作相同
    sigaction(SIGQUIT, &act, NULL);
    sigaction(SIGKILL, &act, NULL);

    // 2. 将初始化的信号集中的数据设置给内核
    sigset_t old;
    sigprocmask(SIG_BLOCK, &myset, &old);

    // 3. 让进程一直运行, 在当前进程中产生对应的信号
    int i = 0;
    while(1)
    {
        // 4. 读内核的未决信号集
        sigset_t curset;
        sigpending(&curset);
        // 遍历这个信号集
        for(int i=1; i<32; ++i)
        {
            int ret = sigismember(&curset, i);
            printf("%d", ret);
        }
        printf("\n");
        sleep(1);
        i++;
        if(i==10)
        {
            // 解除阻塞, 重新设置阻塞信号集
            //sigprocmask(SIG_UNBLOCK, &myset, NULL);
            sigprocmask(SIG_SETMASK, &old, NULL);
        }
    }
    return 0;
}

编译运行,如下所示:

在这里插入图片描述

9.3 信号捕捉的特性

信号捕捉特性:

  1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
  2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).其他信号不屏蔽,如需屏蔽则调用sigsetadd函数修改
  3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!

10.内核实现信号捕捉简析

在这里插入图片描述

11.SIGCHLD 信号

11.1 SIGCHLD 的产生条件

  • 子进程终止时
  • 子进程接收到SIGSTOP
  • 子进程处于停止态,接收到SIGCONT后唤醒时

11.2 借助 SIGCHLD 信号回收子进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>

// 回收子进程处理函数
void recycle(int num)
{
    printf("捕捉到的信号是: %d\n", num);
    // 子进程的资源回收, 非阻塞
    // SIGCHLD信号17号信号, 1-31号信号不支持排队
    // 如果这些信号同时产生多个, 最终处理的时候只处理一次
    // 假设多个子进程同时退出, 父进程同时收到了多个sigchld信号
    // 父进程只会处理一次这个信号, 因此当前函数被调用了一次, waitpid被调用一次
    // 相当于只回收了一个子进程, 但是是同时死了多个子进程, 因此就出现了僵尸进程
    // 解决方案: 循环回收即可
    while(1)
    {
        // 如果是阻塞回收, 就回不到另外一个处理逻辑上去了
        pid_t pid = waitpid(-1, NULL, WNOHANG);
        if(pid > 0)
        {
            printf("child died, pid = %d\n", pid);
        }
        else if(pid == 0)
        {
            // 没有死亡的子进程, 直接退出当前循环
            break;
        }
        else if(pid == -1)
        {
            printf("所有子进程都回收完毕了, 拜拜...\n");
            break;
        }
    }
}


int main()
{
    // 设置sigchld信号阻塞
    sigset_t myset;
    sigemptyset(&myset);
    sigaddset(&myset, SIGCHLD);
    sigprocmask(SIG_BLOCK, &myset, NULL);

    // 循环创建多个子进程 - 20
    pid_t pid;
    for(int i=0; i<20; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    if(pid == 0)
    {
        printf("我是子进程, pid = %d\n", getpid());
    }
    else if(pid > 0)
    {
        printf("我是父进程, pid = %d\n", getpid());
        // 注册信号捕捉, 捕捉sigchld
        struct sigaction act;
        act.sa_flags  =0;
        act.sa_handler = recycle;
        sigemptyset(&act.sa_mask);
        // 注册信号捕捉, 委托内核处理将来产生的信号
        // 当信号产生之后, 当前进程优先处理信号, 之前的处理动作会暂停
        // 信号处理完毕之后, 回到原来的暂停的位置继续运行
        sigaction(SIGCHLD, &act, NULL);

        // 解除sigcld信号的阻塞
        // 信号被阻塞之后,就捕捉不到了, 解除阻塞之后才能捕捉到这个信号
        sigprocmask(SIG_UNBLOCK, &myset, NULL);

        // 父进程执行其他业务逻辑就可以了
        // 默认父进程执行这个while循环, 但是信号产生了, 这个执行逻辑或强迫暂停
        // 	父进程去处理信号的处理函数
        while(1)
        {
            sleep(100);
        }
    }
    return 0;
}

SIGCHLD 信号注意问题

  1. 子进程继承父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集 spending。
  2. 注意注册信号捕捉函数的位置。
  3. 应该在 fork 之前,阻塞 SIGCHLD 信号。注册完捕捉函数后解除阻塞。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BillySturate

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

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

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

打赏作者

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

抵扣说明:

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

余额充值