Linux下多进程通讯之信号机制详解

引言:本文主要介绍 Linux 下多进程通讯之信号机制:信号是 Linux 进程间通信的最古老的方式,也是 Linux 下编程最常用的知识点之一,温故而知新,本文带你重新全面了解信号机制。

Linux下多进程通讯之信号机制详解

一、信号的概述

(1)信号的概念

信号全称是软件中断信号,是 Linux 进程间通信的最古老的方式,是事件发生时对进程的通知机制。它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

看了上面的概念,你可能还是比较晕,不妨举个例子:“中断”其实在我们生活中经常遇到,譬如,我正在房间里打游戏,突然有一个电话、者短信或者敲门声,通知你去取快递或者外卖,无奈,我只能把正在玩游戏的我给暂停了,然后去签收快递,处理完成后再继续玩我的游戏。

  • 那这个电话或者短信就相当于信号
  • 暂停游戏相当于中断
  • 签收快递相当于处理中断

Linux下多进程通讯之信号机制详解_第1张图片

我们学习的“信号”也是类似的。我们在终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)

(2)信号的目的与特点

与外卖小哥想让你知道外卖已到达,需要你快来取走外卖的目的类似,使用信号的两个主要目的是:

  • 让进程知道已经发生了一个特定的事情:信号可以直接进行用户空间进程和内核空间进程的交互,所以内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

  • 强迫进程执行它自己代码中的信号处理程序(通常函数实现)。因此通常一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

    Linux下多进程通讯之信号机制详解_第2张图片

    【注意】:这里信号的产生,注册,注销时信号的内核的机制,而不是信号的函数实现。

同时看到上面的例子,我们不难看出信号有一下特点

  • 简单。【注意】这里的简单是指使用简单,但是信号的内核实现机制是非常复杂的。
  • 不能携带大量信息;
  • 满足某个特设条件才发送;
  • 优先级比较高,即需要先处理信号,再处理其他任务。

二、信号四要素

每个信号必备4要素,分别是:1、编号 ;2、名称 ;3、事件 ;4、默认处理动作

信号编号

linux 中信号种类很多,为了方便使用和管理,操作系统给它们分别编号入库,即系统定义的信号列表。

我们可以使用 Linux 提供的命令查看系统定义的信号列表,查看相应的信号:kill –l (“l” 为字母)

Linux下多进程通讯之信号机制详解_第3张图片

如上图展示的 1) SIGHUP1 是该信号的编号,SIGHUP就是该信号的名称,所以一共有 62 种信号,不存在编号为 0 的信号:

  • 其中编号为 1-31 的信号称之为常规信号(也叫普通信号或标准信号)前 32 个信号名字各不相同;
  • 编号为 34-64 的信号称之为实时信号(不常用),驱动编程与硬件相关。这些信号目前还没有使用,将来可能会使用,且这些信号名字上区别不大;
  • 编号为 31、32 的信号不存在。

通过Linux提供的 man 文档可以查询所有信号的详细信息:

# 查看man文档的信号描述
$ man 7 signal

Linux下多进程通讯之信号机制详解_第4张图片

【注意】仔细观察上图,可以发现有些信号对应着多个编号,这是因为不同的操作系统定义了不同的系统信号:第一个值通常对 alpha 和 sparc 架构有效,中间值针对 x86、arm 和其他架构,最后一个应用于 mips 架构。一个‘-’表示在对应架构上尚未定义该信号。

现在,我们的 linux 操作系统基本上都是Intel X86架构或 ARM 架构,所以需要看中间一列的编号即可。

Linux常规信号一览表 ,【注意】高亮的信号是常用的信号:

编号 信号 对应事件 默认动作
1 SIGHUP 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号 终止进程
2 SIGINT 当用户按下了 组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 终止进程
3 SIGQUIT 用户按下 组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 终止进程
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 停止终端交互进程的运行。按下 组合键时发出这个信号 暂停进程
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文件
34~64 SIGRTMIN ~ SIGRTMAX LINUX的实时信号,它们没有固定的含义(可以由用户自定义) 终止进程

信号默认动作与信号状态

信号有 5 中默认处理动作:

  • Term:终止进程;

  • Ign: 忽略信号 :当前进程忽略掉这个信号;

  • Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)

  • Stop:停止(暂停)进程

  • Cont:继续运行进程

信号有三种状态:产生、未决、递达:

  • 信号的产生:通常发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

    • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如

      • 终端上按 “Ctrl+c” 组合键通常产生中断信号 SIGINT;
      • 终端上按 “Ctrl+\” 组合键通常产生中断信号 SIGQUIT;
      • 终端上按 “Ctrl+z” 组合键通常产生暂停信号 SIGSTOP 等。
    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域;

    • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出;

    • 调用系统函数(如:kill、raise、abort)将发送信号。【注意】此时接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

    • 运行 kill 命令。调用 kill 命令实际上是使用 kill 函数来发送信号。常用此命令终止一个失控的后台进程。

  • 信号未决状态:没有被处理(即信号产生之后,到信号还没有被处理前的这段时间的信号状态)

  • 信号递达状态:信号被处理了(即信号产生之后,信号处理被处理的这段时间的信号状态)

【注意】SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

三、信号产生函数

kill 函数

kill 既一个函数也是一个命令

#include 
#include 

int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死,比如有些信号是忽略信号)

参数:
    pid : 取值有 4 种情况 :
        pid > 0:   将信号传送给进程 ID 为pid的进程。
        pid = 0 :  将信号传送给当前进程所在进程组中的所有进程。
        pid = -1 : 将信号传送给系统内所有的进程。
        pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
    sig : 信号的编号(即数字编号),也可以填信号的宏值(即信号名字)。
          不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
          如果 sig 为 0 表示不发送任何信号。

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

对于 kill ,super 用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的(没有权限)。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 普通用户只能向自己创建的进程发送信号。

#include 
#include 
int main() 
{
    pid_t pid = fork();
    if (-1 == pid)
    {
        perror("fork");
        return 1;
    }
    
    if (0 == pid)			//子进程
    {	
        int i = 0;
        for (i = 0; i< 5; i++)
        {
            printf("in son process\n");
            sleep(1);
        }
    }
    else					//父进程
    {
        printf("in father process\n");
        sleep(2);
        printf("kill son process now \n");
        kill(pid, SIGINT);
    }

    return 0;
}

raise 函数

#include 

int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:
    sig:信号的编号(即数字编号),也可以填信号的宏值(即信号名字)。
         不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
返回值:
    成功:0;失败:非0值
#include 
#include 
#include 

int main(void)
{
    int i = 0;
    while (1)
    {
        printf("do working %d\n", i);
     	if (4 == i)
        {
            raise(SIGTERM);
        }
        i++;
        sleep(1);
    }
	return 0;   
}

abort函数

#include 

void abort(void);
功能:发送异常终止信号 SIGABRT 给当前进程,并产生core文件,
     默认是杀死当前进程,等价于 kill(getpid(), SIGABRT);

参数:无

返回值:无
#include 
#include 
#include 

int main(void)
{
    int i = -1;
    while (1)
    {
        printf("do working %d\n", i);
     	if (4 == i)
        {
            abort(); // 给自己发送一个编号为6的信号,默认的行为就是终止进程
        }
        i++;
        sleep(1);
    }
	return 0;   
}

alarm函数(闹钟)

即当检测到某种软件条件已发生,并将其通知有关进程时产生信号,类似于闹钟的功能。

#include 

unsigned int alarm(unsigned int seconds);
功能:设置定时器 (闹钟)。在指定 seconds(秒)后,内核会给当前进程发送 SIGALRM信号。
     进程收到该信号的默认动作是终止当前进程。 【注意】alarm 不会阻塞当前进程。
	     
参数:
    seconds:指定的时间,以秒为单位。如果参数为0,定时器无效(不进行倒计时,也不会发送信号)
    取消一个定时器通过 alarm(0),此时返回旧闹钟余下秒数。
    
返回值:
    之前没有定时器返回0;之前有定时器则返回剩余的秒数
    
    
每一个进程有且仅有唯一的一个定时器,比如:
	alarm(20);// 返回0
	过了一秒钟
	alarm(5); // 返回19,此时此处定义的定时器会覆盖上面的定时器(上一个定时器失效)并开始5秒的倒计时 

【注意】定时器与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸……无论进程处于何种状态,alarm都计时。

#include 
#include 

int main()
{
    int seconds = 0;

    seconds = alarm(5);
    printf("seconds = %d\n", seconds);  // seconds 值为 0

    sleep(2);
    seconds = alarm(5);    				// 之前没有超时的闹钟被新的设置的闹钟给覆盖了
    printf("seconds = %d\n", seconds);  // seconds 值为 3

    while (1);
    return 0;
}

setitimer函数(定时器)

#include 

int setitimer(int which,  const struct itimerval *new_value, 
              					struct itimerval *old_value);
功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
    which:指定定时方式(即定时器以什么时间计时):
        a) 自然定时:ITIMER_REAL → 时间到了发送 SIGALRM 信号,计算自然时间,最常用;
        b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 时间到了发送 SIGVTALRM 信号,
    														只计算进程占用 cpu 的时间;
        c) 运行时计时(用户 + 内核):ITIMER_PROF → 时间到了发送 SIGPROF 信号,
    													计算占用 cpu 及执行系统调用的时间;
    
    new_value:负责设定 timeout 时间(即时间到了,触发定时器),使用结构体表示:struct itimerval
        struct itimerval {					// 定时器的结构体
            struct timerval it_interval; 	// 闹钟触发周期:每个阶段的时间,间隔时间
            struct timerval it_value;    	// 闹钟触发时间:延迟多长时间执行定时器
            // 过10秒后,每隔2秒定是一次:10秒指 it_value;2秒指 it_interval
        };
        struct timeval {			// 时间的结构体
            long tv_sec;            // 秒数
            long tv_usec;           // 微秒
        }

    old_value: 存放上一次定时的 timeout 值,一般不使用,所以常指定为NULL
        
返回值:
    成功:0
    失败:返回 -1 并设置错误号
            
【注意】:setitimer 函数和 alarm 一样都是非阻塞的函数
#include 
#include 
#include 

int main()
{
    struct itimerval new_value;

    //定时周期,每隔 1 秒钟
    new_value.it_interval.tv_sec = 1;
    new_value.it_interval.tv_usec = 0;

    //第一次触发的时间
    new_value.it_value.tv_sec = 2;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); //定时器设置
	if(ret == -1) 
    {
        perror("setitimer");
        exit(0);
    }
    
    while (1);

    return 0;
}

四、信号集

一个用户进程常常需要对多个信号做出处理,为了方便对多个信号进行处理,在 Linux 系统中引入了信号集(信号的集合),信号集用数据结构 sigset_t 来表示。这个信号集有点类似于我们的 QQ 群,一个个的信号相当于 QQ 群里的一个个好友。

进程的虚拟地址空间,分为用户区和内核区,内核区中有一个 PCB 进程控制块, 是一个结构体:task_struct,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要是两个信号集:阻塞信号集和未决信号集

阻塞信号集和未决信号集

在 PCB 中的两个非常重要的信号集:一个称之为 “阻塞信号集” ,另一个称之为“未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。

  • 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。信号产生,未决信号集中描述该信号的标志立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。

  • 阻塞信号集也称信号屏蔽集、信号掩码。每个进程都只有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。阻塞信号集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。

    信号的阻塞就是让系统暂时保留信号留待以后发送,所以信号阻塞并不是禁止传送信号,也不是阻止信号产生, 而是暂缓信号的传送,那么该信号的处理也将推后(处理推迟到解除阻塞之后)。若将被阻塞的信号从阻塞信号集中删除,进程将会收到相应的信号。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

  • 我们只能设置阻塞信号集(可以读,可以设置),不能设置未决信号集(只能读,不能设置,由内核自动设置),因为未决信号集由内核完成 。

Linux下多进程通讯之信号机制详解_第5张图片

阻塞信号集和未决信号集发生过程举例

  1. 用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
  2. 信号产生但是没有被处理,处于未决状态
    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    • SIGINT 信号状态被存储在第二个标志位上
      • 这个标志位的值为0, 说明信号不是未决状态
      • 这个标志位的值为1, 说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    • 阻塞信号集默认不阻塞任何的信号,所以阻塞信号集中所有标志位默认都是 0
    • 如果想要阻塞某些信号需要用户调用系统的 API,调用 API 后,该信号的标志位值为1
  4. 在处理信号的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

Linux下多进程通讯之信号机制详解_第6张图片

自定义信号集函数

信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。sigset_t 实际上就是一个64位的整数(位图),因为有 64 号信号编号(实际上31、32号缺失)。既然是一个集合,就需要对集合进行添加/删除等操作。相关函数说明如下:

#include   

int sigemptyset(sigset_t *set);       	
	功能:将 set 集合置空,即将信号集中的所有的标志位置为 0
    参数:传出参数,需要操作的信号集
    返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);          			
 	功能:将所有信号加入 set 集合,即将信号集中的所有的标志位置为 1
    参数:传出参数,需要操作的信号集
    返回值:成功返回0, 失败返回-1  
int sigaddset(sigset_t *set, int signo); 	 		
	功能:将 signo 信号加入到set集合,即设置 signo 信号对应的标志位为1,表示阻塞这个信号
    参数:
    	set:传出参数,需要操作的信号集
    	signo:需要设置阻塞的那个信号
    返回值:成功返回0, 失败返回-1 
int sigdelset(sigset_t *set, int signo);   			
	功能:从set集合中移除signo信号,即设置 signo 信号对应的标志位为 0,表示不阻塞这个信号
    参数:
        set:传出参数,需要操作的信号集
    	signo:需要设置不阻塞的那个信号
    返回值:成功返回0, 失败返回-1 
int sigismember(const sigset_t *set, int signo); 	
	功能:判断某个信号是否存在,即判断某个信号是否阻塞
    参数:传出参数,需要操作的信号集
        set:传出参数,需要操作的信号集
    	signo:需要设置阻塞的那个信号
    返回值:
        1 : signum被阻塞
        0 : signum不阻塞
        -1 : 失败
                
sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

【注意】由于只能设置阻塞信号集不能设置未决信号集,所以 sigaddset() 相当于阻塞某个信号,sigdelset() 表示不阻塞某个信号

#include 
#include 

int main()
{
    sigset_t set;   // 定义一个信号集变量
    int ret = 0;

    sigemptyset(&set); // 清空信号集的内容

    // 判断 SIGINT 是否在信号集 set 里
    ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT is not a member of set \nret = %d\n", ret);
    }

    sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 set
    sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set

    // 判断 SIGINT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 1)
    {
        printf("SIGINT is a member of set \nret = %d\n", ret);
    }

    sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除

    // 判断 SIGQUIT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT is not a member of set \nret = %d\n", ret);
    }

    return 0;
}

sigprocmask 函数

我们可以通过 sigprocmask() 修改当前的阻塞信号集来改变信号的阻塞情况。

#include 

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:将自定义信号集中的数据设置到内核中:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞信号集进行修改,新的信号阻塞集由 set 指定,	 而原先的信号阻塞集合由 oldset 保存(保留旧的阻塞集是为了方便还原)。

参数:
    how : 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中:向信号阻塞集合中添加 set 信号集,
        	新的阻塞信号集是 set 和旧阻塞信号集的并集。相当于 mask = mask|set。
        SIG_UNBLOCK:从当前内核的阻塞信号集中去除 set 中的信号。相当于 mask = mask & ~ set。
        	被去除的信号相当于解除了阻塞。
        SIG_SETMASK:将内核中原有阻塞信号集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。
        	相当于mask = set。
    set : 要操作的信号集地址。
        若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
    oldset : 保存原先信号阻塞集地址,可以为 NULL。

返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。
#include 
#include 
#include 
#include 

int main() 
{

    // 设置2、3号信号阻塞
    sigset_t set;
    sigemptyset(&set);
    // 将2号和3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;

    while(1) 
    {
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);

        // 遍历前32位
        for(int i = 1; i <= 31; i++) 
        {
            if(sigismember(&pendingset, i) == 1) 
            {
                printf("1");
            }else if(sigismember(&pendingset, i) == 0) 
            {
                printf("0");
            }else 
            {
                perror("sigismember");
                exit(0);
            }
        }

        printf("\n");
        sleep(1);
        if(num == 10) 
        {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }

    }

    return 0;
}

sigpending 函数

本函数不常用,了解即可。

#include 

int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
    set:未决信号集
返回值:
    成功:0
    失败:-1

五、信号捕捉

信号处理方式

一个进程收到一个信号的时候,可以用如下方法进行处理:

  1. 执行系统默认动作:对大多数信号来说,系统默认动作是用来终止该进程。

  2. 忽略此信号(丢弃):接收到此信号后没有任何动作。

  3. 执行自定义信号处理函数(捕获,也叫注册):用用户定义的信号处理函数处理该信号。

    【注意】:SIGKILL 和 SIGSTOP 不能更改信号的处理方式(这两个信号即不可以被阻塞、也不能被捕捉,也不可以被忽略),因为它们向用户提供了一种使进程终止的可靠方法。

内核实现信号捕捉过程

Linux下多进程通讯之信号机制详解_第7张图片

本文将介绍三种信号捕捉 API,基本可以覆盖日常工作需求。

信号捕捉的特性

  1. 当正在处理某个信号的信号处理函数时,该信号默认会被屏蔽:如果此时又来了一个相同类型的信号,此时新来的信号的处理将会被阻塞,直至正在处理的信号的处理函数被处理完。

  2. 内核中存在阻塞信号集,在信号处理函数被处理的过程中,会使用一个临时的阻塞信号集合,当处理完成之后,会恢复到内核的阻塞信号集。

  3. 常规信号不支持排队:未决信号集和阻塞信号集中的每个信号只有一个标志位,只能同时记录一个相同类型信号的状态,如果同时发送了很多相同类型的信号,最终只能记录一个,其他的都被丢弃。

    实时信号支持排队

信号捕捉的特性可以参考信号捕捉的特性及实例一文。

signal 函数

#include 

typedef void(*sighandler_t)(int);  // 函数指针
sighandler_t signal(int signum, sighandler_t handler);
功能:
    设置某个信号的捕捉行为:注册信号处理函数,即确定收到信号后处理函数的入口地址。此函数不会阻塞。

参数:
    signum:要捕捉的信号,可以是编号(即数字编号),也可以填信号的宏值(即信号名字)。
         不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
    
    handler : 捕捉到的信号如何处理,取值有 3 种情况:
          SIG_IGN:忽略该信号
          SIG_DFL:执行系统默认动作
          信号处理函数名:自定义信号处理函数(回调函数)
        		这个函数不是程序员调用,而是当信号产生,由内核调用;
        		程序员只负责写这个函数(函数的类型根据实际需求,看函数指针的定义);
          		写的内容为:捕捉到信号后如何处理信号,如:func 回调函数的定义如下:
                void func(int signo)
                {
                    // signo 为触发的信号,为 signal() 第一个参数的值
                }

返回值:
    成功:第一次返回 NULL,下一次返回此信号的上一次注册的信号处理函数的地址。
    		如果需要使用此返回值,必须在前面先声明此函数指针的类型。
    失败:返回 SIG_ERR,设置错误号
    
【注意】SIGKILL SIGSTOP不能被捕捉,不能被忽略。

【注意】该函数由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中可能有不同的行为。因此应该尽量避免使用它,取而代之使用 sigaction 函数

#include 
#include 
#include 
#include 

// 信号处理函数
void signal_handler(int signo)
{
    if (signo == SIGINT)
    {
        printf("recv SIGINT\n");
    }
    else if (signo == SIGQUIT)
    {
        printf("recv SIGQUIT\n");
    }
}

int main()
{
    printf("wait for SIGINT OR SIGQUIT\n");

    /* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
    // 信号注册函数
    signal(SIGINT, signal_handler);
    signal(SIGQUIT, signal_handler);

    while (1); //不让程序结束

    return 0;
}

sigaction 函数

#include 

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    检查或修改指定信号的设置(或同时执行这两种操作),即信号捕捉。

参数:
    signum:要操作的信号。可以是编号(即数字编号),也可以填信号的宏值(即信号名字)。
         不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
    
    act:   捕捉到信号之后的处理动作,即设置对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式,一般不使用,传递NULL(传出参数)。
    		如果 act 指针非空,则要改变指定信号的处理方式(设置);
    		如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。

返回值:
    成功:0
    失败:-1
    
【注意】SIGKILL SIGSTOP不能被捕捉,不能被忽略。

【注意】由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中,signal 函数可能有不同的行为。因此应该尽量避免使用它,取而代之使用 sigaction 函数

struct sigaction结构体:

struct sigaction {
    void(*sa_handler)(int);  // 信号处理函数指针
    void(*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数指针,不常用
    sigset_t   sa_mask;      // 临时信号阻塞集:在信号捕捉函数执行过程中,临时阻塞某些信号。
    int        sa_flags;     // 信号处理的方式,即使用哪一个信号处理对捕捉到的信号进行处理
    							// 这个值可以是0,表示使用sa_handler;
    							// 这个值也可以是 SA_SIGINFO 表示使用 sa_sigaction
    void(*sa_restorer)(void);// 已弃用
};
  • sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler 两者之一赋值,其取值如下:
  • sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。
  • sa_flags:用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以是一下值的“按位或”组合:
    • SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
    • SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
    • SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
    • SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
    • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
    • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

信号处理函数:

void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:
    signum:信号的编号。
    info:记录信号发送进程信息的结构体。
    context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文。
#include 
#include 
#include 
#include 

void myalarm(int num) 
{
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() 
{

    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask);  // 清空临时阻塞信号集
   
    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    if(ret == -1) 
    {
        perror("setitimer");
        exit(0);
    }

    // getchar();
    while(1);

    return 0;
}

sigqueue 函数

本函数不常用,了解即可。

#include 

int sigqueue(pid_t pid, int sig, const union sigval value);
功能:
    给指定进程发送信号。
参数:
    pid : 进程号。
    sig : 信号。可以是编号(即数字编号),也可以填信号的宏值(即信号名字)。
         不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
        
    value : 通过信号传递的参数。union sigval 类型如下:
            union sigval
            {
                int   sival_int;
                void *sival_ptr;
            };
返回值:
    成功:0
    失败:-1

向指定进程发送指定信号的同时,携带数据。但如果传地址需注意:不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。

下面我们做这么一个例子,一个进程在发送信号,一个进程在接收信号的发送。

/*******************************************************
*功能:     发 SIGINT 信号及信号携带的值给指定的进程
*参数:        argv[1]:进程号 argv[2]:待发送的值(默认为100)
*返回值:   0
********************************************************/
// 发送信号示例代码如下

int main()
{
    if (argc >= 2)
    {
        pid_t pid, pid_self;
        union sigval tmp;

        pid = atoi(argv[1]); // 进程号
        if (argc >= 3)
        {
            tmp.sival_int = atoi(argv[2]);
        }
        else
        {
            tmp.sival_int = 100;
        }

        // 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去
        sigqueue(pid, SIGINT, tmp);

        pid_self = getpid(); // 进程号
        printf("pid = %d, pid_self = %d\n", pid, pid_self);
    }

    return 0;
}

接收信号示例代码如下:

// 信号处理回调函数
void signal_handler(int signum, siginfo_t *info, void *ptr)
{
    printf("signum = %d\n", signum); // 信号编号
    printf("info->si_pid = %d\n", info->si_pid); // 对方的进程号
    printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息
}

int main()
{
    struct sigaction act, oact;

    act.sa_sigaction = signal_handler; //指定信号处理回调函数
    sigemptyset(&act.sa_mask); // 阻塞集为空
    act.sa_flags = SA_SIGINFO; // 指定调用 signal_handler

    // 注册信号 SIGINT
    sigaction(SIGINT, &act, &oact);

    while (1)
    {
        printf("pid is %d\n", getpid()); // 进程号

        pause(); // 捕获信号,此函数会阻塞
    }

    return 0;
}

两个终端分别编译代码,一个进程接收,一个进程发送。

SIGCHLD 信号捕捉

SIGCHLD 信号产生的条件

  1. 子进程终止时
  2. 子进程接收到 SIGSTOP 信号停止时
  3. 子进程处在停止态,接受到SIGCONT后唤醒时

【注意】以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号

SIGCHL 信号用途:解决多进程中僵尸进程的问题。

子进程结束的时候,父进程有责任回收子进程的资源,一般是在父进程不断循环的调用 wait 或者 waitpid 去回收子进程的资源。这就导致一个问题:父进程需要不断地循环回收处理,而且wait函数是阻塞的,但是父进程也需要做自己的事情,不能一直阻塞等待子进程结束回收资源。如果当子进程结束的时候,给父进程发送一个 SIGCHLD信号,父进程中会默认忽略该信号,但是我们可以捕捉该信号,当捕捉到信号时,说明有子进程结束,此时可以调用 wait 或者 waitpid 回收子进程资源。实例如下:

#include 
#include 
#include 
#include 
#include 
#include 

void myFun(int num) 
{
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
	// 1、需要添加循环,否则只能回收少量子进程资源,因为常规信号不支持排队
    // 2、不使用 wait,因为 wait 会导致阻塞,而要使用 waitpid,因为 waitpid 可以设置为非阻塞
  
    while(1) 
    {
       int ret = waitpid(-1, NULL, WNOHANG);
       if(ret > 0) 
       {
           printf("child die , pid = %d\n", ret);
       } else if(ret == 0) 
       {
           // 说明还有子进程或者
           break;
       } else if(ret == -1) 
       {
           // 没有子进程
           break;
       }
    }
}

int main() 
{

    // 提前设置好阻塞信号集,阻塞 SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建一些子进程
    pid_t pid;
    for(int i = 0; i < 20; i++) 
    {
        pid = fork();
        if(pid == 0) 
        {
            break;
        }
    }

    if(pid > 0) 
    {
        // 父进程

        // 捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        // 注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while(1) 
        {
            printf("parent process pid : %d\n", getpid());
            sleep(2);
        }
    } else if( pid == 0) 
    {
        // 子进程
        printf("child process pid : %d\n", getpid());
    }

    return 0;
}

六、总结

文章的最后,稍微总结一下:在 Linux 系统(以及其他类 Unix 操作系统)中,信号被用于进程间的通信,所以理解信号的相关概念并学会使用信号是非常重要的。希望与大家一起共勉学习进步。

你可能感兴趣的:(linux,c语言,c++)