Linux 信号学习

  • 信号量的基本概念
    • 信号产生的条件
    • 信号如何被处理
    • 信号的异步特质
  • 信号的分类
    • 可靠信号/不可靠信号
    • 实时信号/非实时信号
  • 常见信号与默认行为
  • 信号处理
    • `signal()` 函数
    • `sigaction()`函数
  • 向进程发送信号
    • `kill()` 函数
    • `raise()` 函数
  • alarm()和pause()函数
    • `alarm()` 定时函数
    • `pause()` 暂停进程函数
    • 通过 `alarm()` 和 `pause()` 模拟sleep功能
  • 信号集
    • `sigemptyset()` 初始化空信号集
    • `sigfillset()` 初始化满信号集
    • `sigaddset()` 向信号集添加信号
    • `sigdelset()` 从信号集移除信号
    • `sigismember()` 测试信号是否属于信号集
  • 信号描述信息
    • `sys_siglist` 信号描述信息数组
    • `strsignal()` 输出描述信息函数
    • `psignal()` 输出描述信息到标准错误函数
  • 信号掩码(阻塞信号传递)
    • `sigprocmask()` 添加/移除/设置信号集至信号掩码
  • 阻塞等待
    • `sigsuspend()` 阻塞等待信号
  • 实时信号
    • `sigpending()` 读取等待信号集
    • `sigqueue()` 发送实时信号
  • 异常退出
    • `abort()` 终止进程
  • 参考文献

信号量的基本概念

信号是事件发生时对进程的通知机制,也可以把它称为 软件中断 。 信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。

信号产生的条件

一个具有合适权限的进程能够向另一个进程发送信号 ,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号可以由“谁”发出呢?以下列举的很多情况均可以产生信号:

  • 硬件发生异常 ,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括 执行一条异常的机器语言指令 ,诸如, 除数为0数组访问越界导致引用了无法访问的内存区域 等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
  • 在终端下输入能够产生信号的特殊字符 。譬如在终端上按下 CTRL + C 组合按键可以产生 中断信号(SIGINT) ;按下 CTRL + Z 组合按键可以产生 暂停信号(SIGCONT)
  • 进程调用 kill() 系统调用将任意信号发送给另一个进程或进程组 。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root超级用户。
  • 用户可以通过 kill 命令 将信号发送给其它进程。 kill 命令想必大家都会使用,通常我们会通过 kill 命令来“杀死”(终止)一个进程,譬如在终端下执行 "kill -9 xxx" 来杀死PID为xxx的进程,这里的 -9 其实指的就是发送编号为9的信号,也就是 SIGKILL 信号。
  • 发生了软件事件 ,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的 定时器已经超时进程执行的CPU时间超限进程的某个子进程退出 等等情况)。

信号如何被处理

信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:

  • 忽略信号 。也就是说, 当信号到达进程后,该进程并不会去理会它、直接忽略 ,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但 有两种信号却决不能被忽略 ,它们是 SIGKILLSIGSTOP ,这两种信号不能被忽略的原因是: 它们向内核和超级用户提供了使进程终止或停止的可靠方法 。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
  • 捕获信号当信号到达进程后,执行预先绑定好的信号处理函数 。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了 signal() 系统调用可用于注册信号的处理函数,将会在后面向大家介绍。
  • 执行系统默认操作进程不对该信号事件作出处理,而是交由系统进行处理 ,每一种信号都会有其对应的系统默认的处理方式,8.3小节中对此有进行介绍。需要注意的是, 对大多数信号来说,系统默认的处理方式就是终止该进程

信号的异步特质

信号是异步事件的经典实例 ,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号, 这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数 ,这就是异步处理方式。

信号的分类

可靠信号/不可靠信号

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,**把那些建立在早期机制上的信号叫做"不可靠信号",Linux 中信号值小于 SIGRTMIN(34) 的信号都是 不可靠信号 **。不可靠信号的来源主要是:

  • 进程每次处理信号后,就将对信号的响应设置为默认动作 。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
  • 信号可能丢失 ,后面将对此详细阐述。 如果在进程对某个信号进行处理时, 这个信号发生多次 ,对后到来的这类信号不排队,那么 仅传送该信号一次 ,即发生了信号丢失。因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此, Linux下的不可靠信号问题主要指的是信号可能丢失

随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种UNIX版本分别在这方面进行了研究,力图实现"可靠信号"。 由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号(SIGRTMIN~SIGRTMAX),并在一开始就把它们定义为可靠信号 ,在Linux系统下使用 kill -l 命令可查看到所有信号,如下所示:

Tips:括号" ) "前面的数字对应该信号的编号,编号131所对应的是不可靠信号,编号3464对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+NSIGRTMAX-N 的方式来表示。

可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数 sigqueue()信号绑定函数 sigaction()

实时信号/非实时信号

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的, 非实时信号都不支持排队,都是不可靠信号实时信号都支持排队,都是可靠信号 。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX标准的一部分,可用于应用进程。

一般我们也把非实时信号(不可靠信号)称为标准信号,如果文档中用到了这个词,那么大家要知道,这里指的就是非实时信号(不可靠信号)。

实时信号较之于标准信号,其优势如下:

  • 实时信号的信号范围有所扩大 ,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用: SIGUSR1SIGUSR2
  • 内核对于实时信号所采取的是队列化管理 。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反,对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。
  • 当发送一个实时信号时,可为信号指定伴随数据 (一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。
  • 不同实时信号的传递顺序得到保障 。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。

Linux内核定义了31个不同的实时信号,信号编号范围为34~64 ,使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX 表示编号最大的实时信号, 其它信号编号可使用这两个宏加上一个整数或减去一个整数

应用程序当中使用实时信号,需要有以下的两点要求:

  • 发送进程使用 sigqueue() 系统调用向另一个进程发送实时信号以及伴随数据,见后文。
  • 接收实时信号的进程要为该信号建立一个信号处理函数,使用 sigaction 函数为信号建立处理函数,并加入 SA_SIGINFO ,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用 sa_sigaction 指针指向的处理函数,而不是 sa_handler ,当然允许应用程序使用 sa_handler ,但这样就不能获取到实时信号的伴随数据了。

常见信号与默认行为

信号本质上是 int 类型的数字编号 ,这就好比硬件中断所对应的中断号。 内核针对每个信号,都给其定义了一个唯一的整数编号 ,从数字1开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样, 所以在程序当中一般都使用信号的符号名 (也就是宏定义)。

这些信号在<signum.h>头文件中定义,每个信号都是以SIGxxx开头。Linux下对标准信号(不可靠信号、非实时信号)的编号为1~31:

Linux信号总结

信号名称  编号  描述  系统默认操作  SIGINT  2 终端中断符  term  SIGQUIT  3 终端退出符  term+core  SIGILL  4 非法硬件指令  term+core  SIGABRT  6 异常终止 (abort)  term+core  SIGBUS  7 内存访问错误  term+core  SIGFPE  8 算术异常  term+core  SIGKILL  9 终极终止信号  term  SIGUSR1  10 用户自定义信号  1 term  SIGSEGV  11 无效的内存引用  term+core  SIGUSR2  12 用户自定义信号  2 term  SIGPIPE  13 管道关闭  term  SIGALRM  14 定时器超时 (alarm)  term  SIGTERM  15 终止进程  term  SIGCHLD/SIGCLD  17 子进程终止或停止  ignore  SIGCONT  18 使停止状态的进程继续运行  cont  SIGSTOP  19 停止进程  stop  SIGTSTP  20 终端停止符  stop  SIGXCPU  24 超过 CPU 限制  term+core  SIGVTALRM  26 虚拟定时器超时  term  SIGWINCH  28 终端窗口尺寸发生变化  ignore  SIGPOLL/SIGIO  29 异步 I/O  term/ignore  SIGSYS  31 无效系统调用  term+core  \begin{array}{|c|c|c|c|} \hline \text { 信号名称 } & \text { 编号 } & \text { 描述 } & \text { 系统默认操作 } \\ \hline \text { SIGINT } & 2 & \text { 终端中断符 } & \text { term } \\ \hline \text { SIGQUIT } & 3 & \text { 终端退出符 } & \text { term+core } \\ \hline \text { SIGILL } & 4 & \text { 非法硬件指令 } & \text { term+core } \\ \hline \text { SIGABRT } & 6 & \text { 异常终止 (abort) } & \text { term+core } \\ \hline \text { SIGBUS } & 7 & \text { 内存访问错误 } & \text { term+core } \\ \hline \text { SIGFPE } & 8 & \text { 算术异常 } & \text { term+core } \\ \hline \text { SIGKILL } & 9 & \text { 终极终止信号 } & \text { term } \\ \hline \text { SIGUSR1 } & 10 & \text { 用户自定义信号 } 1 & \text { term } \\ \hline \text { SIGSEGV } & 11 & \text { 无效的内存引用 } & \text { term+core } \\ \hline \text { SIGUSR2 } & 12 & \text { 用户自定义信号 } 2 & \text { term } \\ \hline \text { SIGPIPE } & 13 & \text { 管道关闭 } & \text { term } \\ \hline \text { SIGALRM } & 14 & \text { 定时器超时 (alarm) } & \text { term } \\ \hline \text { SIGTERM } & 15 & \text { 终止进程 } & \text { term } \\ \hline \text { SIGCHLD/SIGCLD } & 17 & \text { 子进程终止或停止 } & \text { ignore } \\ \hline \text { SIGCONT } & 18 & \text { 使停止状态的进程继续运行 } & \text { cont } \\ \hline \text { SIGSTOP } & 19 & \text { 停止进程 } & \text { stop } \\ \hline \text { SIGTSTP } & 20 & \text { 终端停止符 } & \text { stop } \\ \hline \text { SIGXCPU } & 24 & \text { 超过 CPU 限制 } & \text { term+core } \\ \hline \text { SIGVTALRM } & 26 & \text { 虚拟定时器超时 } & \text { term } \\ \hline \text { SIGWINCH } & 28 & \text { 终端窗口尺寸发生变化 } & \text { ignore } \\ \hline \text { SIGPOLL/SIGIO } & 29 & \text { 异步 I/O } & \text { term/ignore } \\ \hline \text { SIGSYS } & 31 & \text { 无效系统调用 } & \text { term+core } \\ \hline \end{array}  信号名称  SIGINT  SIGQUIT  SIGILL  SIGABRT  SIGBUS  SIGFPE  SIGKILL  SIGUSR1  SIGSEGV  SIGUSR2  SIGPIPE  SIGALRM  SIGTERM  SIGCHLD/SIGCLD  SIGCONT  SIGSTOP  SIGTSTP  SIGXCPU  SIGVTALRM  SIGWINCH  SIGPOLL/SIGIO  SIGSYS ​ 编号 2346789101112131415171819202426282931​ 描述  终端中断符  终端退出符  非法硬件指令  异常终止 (abort)  内存访问错误  算术异常  终极终止信号  用户自定义信号 1 无效的内存引用  用户自定义信号 2 管道关闭  定时器超时 (alarm)  终止进程  子进程终止或停止  使停止状态的进程继续运行  停止进程  终端停止符  超过 CPU 限制  虚拟定时器超时  终端窗口尺寸发生变化  异步 I/O  无效系统调用 ​ 系统默认操作  term  term+core  term+core  term+core  term+core  term+core  term  term  term+core  term  term  term  term  ignore  cont  stop  stop  term+core  term  ignore  term/ignore  term+core ​​

  • term 表示终止进程;
  • core 表示生成核心转储文件,核心转储文件可用于调试,这个便不再给介绍了;
  • ignore 表示忽略信号;
  • cont 表示继续运行进程;
  • stop 表示停止进程(注意停止不等于终止,而是暂停)。

具体介绍各信号作用如下:

信号名称 信号作用
SIGINT 当用户在终端按下中断字符(通常是CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一个进程。 该信号的系统默认操作是终止进程的运行。 所以通常我们都会使用CTRL + C来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。
SIGQUIT 当用户在终端按下退出字符(通常是CTRL + \)时,内核将发送 SIGQUIT 信号给前台进程组中的每一个进程。 该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。 进程如果陷入无限循环、或不再响应时,使用SIGQUIT信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符CTRL + C、也可以按下退出字符CTRL + \来终止,当然前提条件是,此进程会将SIGINT信号或SIGQUIT信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。
SIGILL 如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行。
SIGABRT 当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT信号。该信号的系统默认操作是终止进程、并生成核心转储文件。
SIGBUS 产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。
SIGFPE 该信号因特定类型的算术错误而产生,譬如除以0。该信号的系统默认操作是终止进程。
SIGKILL 此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。使用SIGINT信号和SIGQUIT信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,如果使用SIGINT或SIGQUIT无法终止进程,那就使用“必杀信号”SIGKILL吧。Linux下有一个kill命令,kill命令可用于向进程发送信号, 我们会使用"kill -9 xxx"命令来终止一个进程(xxx表示进程的pid),这里的-9其实指的就是发送编号为9的信号,也就是SIGKILL信号。
SIGUSR1 该信号和SIGUSR2信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。
SIGSEGV 这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对内存无效引用的原因很多,C语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程。
SIGUSR2 与SIGUSR1信号相同。
SIGPIPE 涉及到管道和socket,当进程向已经关闭的管道、FIFO或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。
SIGALRM 与系统调用alarm()或setitimer()有关,应用程序中可以调用alarm()或setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送SIGALRM信号给该应用程序,关于alarm()或setitimer()函数的使用,后面将会进行讲解。该信号的系统默认操作是终止进程。
SIGTERM 这是用于终止进程的标准信号,也是kill命令所发送的默认信号(kill xxx,xxx表示进程pid),有时我们会直接使用"kill -9 xxx"显式向进程发送SIGKILL信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获SIGTERM信号、并为其绑定一个处理函数,当该进程收到SIGTERM信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用SIGKILL信号终止进程,从而跳过了SIGTERM信号的处理函数,通常SIGKILL终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用SIGTERM,实在不行再使用最后手段SIGKILL。
SIGCHLD 当父进程的某一个 子进程终止时 ,内核会 向父进程发送该信号 。当父进程的某一个 子进程因收到信号而停止或恢复时 ,内核也可能 向父进程发送该信号 。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。
SIGCLD 与SIGCHLD信号同义。
SIGCONT 将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。
SIGSTOP 这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。
SIGTSTP 这也是一个停止信号,当用户在终端按下停止字符(通常是CTRL + Z),那么系统会将SIGTSTP信号发送给前台进程组中的每一个进程,使其停止运行。
SIGXCPU 当进程的CPU时间超出对应的资源限制时,内核将发送此信号给该进程。
SIGVTALRM 应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程。
SIGWINCH 在窗口环境中,当终端窗口尺寸发生变化时(譬如用户手动调整了大小,应用程序调用ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号。
SIGPOLL/SIGIO 这两个信号同义。这两个信号将会在高级IO章节内容中使用到,用于提示一个异步IO事件的发生,譬如应用程序打开的文件描述符发生了I/O事件时,内核会向应用程序发送SIGIO信号。
SIGSYS 如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。

信号处理

signal() 函数

signal() 函数是Linux系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:

#include <signal.h>
typedef void (*sig_t)(int);// signum: 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
// handler: sig_t类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数。
sig_t signal(int signum, sig_t handler);

信号处理参数handler既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGNSIG_DFL

  • SIG_IGN 表示此进程需要忽略该信号

  • SIG_DFL 则表示设置为系统默认操作

#include <signal.h>
/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);
/* Fake signal functions.  */
#define SIG_ERR  ((__sighandler_t) -1)  /* Error return.  */
#define SIG_DFL  ((__sighandler_t)  0)  /* Default action.  */
#define SIG_IGN  ((__sighandler_t)  1)  /* Ignore signal.  */

sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号, 可将多个信号绑定到同一个信号处理函数上 ,此时就可通过此参数来判断当前触发的是哪个信号。

返回值 :此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR ,并会设置 errno

sigaction()函数

sigaction() 允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:

#include <signal.h>// signum:需要设置的信号,除了SIGKILL信号和SIGSTOP信号之外的任何信号。
// act:act参数是一个struct sigaction类型指针,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数act不为NULL,则表示需要为信号设置新的处理方式;如果参数act为NULL,则表示无需改变信号当前的处理方式。
// oldact:oldact参数也是一个struct sigaction类型指针。如果参数oldact不为NULL,则会将信号之前的处理方式等信息通过参数oldact返回出来;如果无意获取此类信息,那么可将该参数设置为NULL。
int sigaction (int sig, const struct sigaction *restrict act, struct sigaction *restrict oact)

struct sigaction 结构体如下所示:

struct sigaction {union {__sighandler_t _sa_handler;                         void (*_sa_sigaction)(int, struct siginfo *, void *);   } _u;sigset_t sa_mask;                                      unsigned long sa_flags;void (*sa_restorer)(void);
};
  • sa_handler指定信号处理函数 ,与signal()函数的handler参数相同。
  • sa_sigaction :也用于指定信号处理函数, 这是一个替代的信号处理函数,提供了更多的参数 ,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取,稍后介绍该数据结构; sa_handlersa_sigaction 是互斥的,不能同时设置 ,对于标准信号来说,使用 sa_handler 就可以了,可通过 SA_SIGINFO 标志进行选择。
  • sa_mask :参数 sa_mask 定义了一组信号,当 进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再将这组信号从信号掩码字段中删除 。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现, 如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞 。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成(此参数是sigset_t类型变量,关于该类型的介绍信息下文有详细介绍),信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。
  • sa_flags :参数 sa_flags 指定了一组标志, 这些标志用于控制信号的处理过程 ,可设置为如下这些标志(多个标志使用位或" | "组合):
  • SA_NOCLDSTOP
    如果 signumSIGCHLD ,则 子进程停止时 (即当它们接收到 SIGSTOPSIGTSTPSIGTTINSIGTTOU 中的一种时)或 恢复时 (即它们接收到 SIGCONT不会收到 SIGCHLD 信号
  • SA_NOCLDWAIT
    如果 signumSIGCHLD ,则在 子进程终止时不要将其转变为僵尸进程
  • SA_NODEFER
    不要阻塞从某个信号自身的信号处理函数中接收此信号 。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞。
  • SA_RESETHAND
    执行完信号处理函数之后,将信号的处理方式设置为系统默认操作
  • SA_RESTART
    被信号中断的系统调用,在信号处理完成之后将自动重新发起
  • SA_SIGINFO
    如果设置了该标志,则 表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler ,关于 sa_sigaction 信号处理函数的参数信息。
  • sa_restorer :该成员已过时,不要再使用了。

一般而言,将信号处理函数设计越简单越好 ,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗CPU时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

向进程发送信号

kill 命令相类似,Linux系统提供了 kill() 系统调用,一个进程可通过 kill() 向另一个进程发送信号;除了 kill() 系统调用之外,Linux系统还提供了系统调用 killpg() 以及库函数 raise() ,也可用于实现发送信号的功能。

kill() 函数

kill() 系统调用可 将信号发送给指定的进程或进程组中的每一个进程 ,其函数原型如下所示:

#include <sys/types.h>
#include <signal.h> // pid:参数pid为正数的情况下,用于指定接收此信号的进程pid;除此之外,参数pid也可设置为0或-1以及小于-1等不同值,稍后给说明。
// sig:参数sig指定需要发送的信号,也可设置为0,如果参数sig设置为0则表示不发送信号,但仍执行错误检查,这通常可用于检查参数pid指定的进程是否存在。
int kill(pid_t pid, int sig);

返回值:成功返回0;失败将返回-1,并设置 errno

参数pid不同取值含义:

  • 如果pid为正,则信号sig将发送到 pid 指定的进程
  • 如果pid等于0,则将sig发送到当前进程的 进程组中的每个进程
  • 如果pid等于-1,则将sig发送到当前进程 有权发送信号的每个进程 ,但进程1(init)除外。
  • 如果pid小于-1,则将sig发送到ID为 -pid 的进程组中的每个进程

进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是 发送者进程的实际用户ID或有效用户ID必须等于接收者进程的实际用户ID或有效用户ID

从上面介绍可知,当 sig 为0时 ,仍可进行正常执行的错误检查,但不会发送信号,这通常可 用于确定一个特定的进程是否存在 。如果向一个 不存在的进程 发送信号, kill() 将会返回 -1errno 将被设置为 ESRCH ,表示进程不存在。

raise() 函数

有时 进程需要向自身发送信号raise() 函数可用于实现这一要求, raise() 函数原型如下所示(此函数为C库函数):

#include <signal.h> // sig:需要发送的信号
int raise(int sig);     // 等价于 kill(getpid(), sig);

alarm()和pause()函数

alarm() 定时函数

使用 alarm() 函数可以设置一个定时器(闹钟) ,当定时器定时时间到时,内核会向进程发送 SIGALRM 信号,其函数原型如下所示:

#include <unistd.h> // seconds:设置定时时间,以秒为单位;如果参数seconds等于0,则表示取消之前设置的alarm闹钟。
unsigned int alarm(unsigned int seconds);

返回值:如果在调用 alarm() 时,之前已经为该进程设置了闹钟还没有超时,则该闹钟的剩余值作为本次 alarm() 函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0。

每个进程只能设置一个 alarm 闹钟 ;虽然SIGALRM信号的系统默认操作是终止进程,但是如果程序当中设置了 alarm 闹钟,但大多数使用闹钟的进程都会捕获此信号。

需要注意的是 alarm 闹钟并不能循环触发,只能触发一次 ,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm() 函数设置定时器。

pause() 暂停进程函数

pause() 系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止只有执行了信号处理函数并从其返回时, pause() 才返回 ,在这种情况下, pause() 返回-1,并且将 errno 设置为 EINTR 。其函数原型如下所示:

#include <unistd.h> int pause(void);

通过 alarm()pause() 模拟sleep功能

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> static void sig_handler(int sig) {puts("Alarm timeout");
}int main(int argc, char *argv[]) {struct sigaction sig = {0};int second; /* 检验传参个数 */ if (2 > argc) exit(-1); /* 为SIGALRM信号绑定处理函数 */ sig.sa_handler = sig_handler; sig.sa_flags = 0; if (-1 == sigaction(SIGALRM, &sig, NULL)) {perror("sigaction error");exit(-1);} /* 启动alarm定时器 */ second = atoi(argv[1]);printf("定时时长: %d秒\n", second);alarm(second); /* 进入休眠状态 */ pause();puts("休眠结束");exit(0); }

运行测试:

信号集

通常我们需要有 一个能表示多个信号(一组信号)的数据类型——信号集(signal set) ,很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如 sigaction() 函数、 sigprocmask() 函数、 sigpending() 函数等。本小节向大家介绍信号集这个数据类型。

信号集其实就是 sigset_t 类型数据结构:

typedef struct {unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;

使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中,当然Linux系统了用于操作 sigset_t 信号集的API,譬如 sigemptyset()sigfillset()sigaddset()sigdelset()sigismember() ,接下来向大家介绍。

sigemptyset() 初始化空信号集

sigemptyset() 初始化信号集,使其不包含任何信号:

#include <signal.h> // set:指向需要进行初始化的信号集变量
int sigemptyset(sigset_t *set);

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例:

sigset_t sig_set;
sigemptyset(&sig_set);

sigfillset() 初始化满信号集

sigfillset() 函数初始化信号集,使其包含所有信号(包括所有实时信号):

#include <signal.h> // set:指向需要进行初始化的信号集变量
int sigfillset(sigset_t *set);

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例:

sigset_t sig_set;
sigfillset(&sig_set);

sigaddset() 向信号集添加信号

sigaddset() 向信号集添加信号:

#include <signal.h> // set:指向需要进行初始化的信号集变量
// signum:需要添加的信号
int sigaddset(sigset_t *set, int signum);

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例:

sigset_t sig_set; sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);

sigdelset() 从信号集移除信号

sigaddset() 从信号集移除信号:

#include <signal.h> // set:指向需要进行初始化的信号集变量
// signum:需要移除的信号
int sigdelset(sigset_t *set, int signum);

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例:

sigset_t sig_set; sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);

sigismember() 测试信号是否属于信号集

使用 sigismember() 函数可以测试某一个信号是否在指定的信号集中,函数原型如下所示:

#include <signal.h> // set:指向需要进行初始化的信号集变量
// signum:需要进行测试的信号
int sigismember(sigset_t *set, int signum);

返回值:如果信号 signum 在信号集 set 中,则返回1,否则返回0;失败则返回-1,并设置errno。

使用示例:

sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT)) puts("信号集中包含SIGINT信号");
else if (!sigismember(&sig_set, SIGINT)) puts("信号集中不包含SIGINT信号");

信号描述信息

sys_siglist 信号描述信息数组

在Linux下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于 sys_siglist 数组中, sys_siglist 数组是一个 char * 类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。譬如, 可以使用 sys_siglist[SIGINT] 来获取对 SIGINT 信号的描述 。我们编写一个简单地程序进行测试:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h> int main(void) {printf("SIGINT描述信息: %s\n", sys_siglist[SIGINT]);printf("SIGQUIT描述信息: %s\n", sys_siglist[SIGQUIT]);printf("SIGBUS描述信息: %s\n", sys_siglist[SIGBUS]);exit(0);
}

运行结果:

strsignal() 输出描述信息函数

除了直接使用 sys_siglist 数组获取描述信息之外,还可以使用 strsignal() 函数。较之于直接引用 sys_siglist 数组,更推荐使用 strsignal() 函数,其函数原型如下所示:

#include <string.h> // sig:指定的信号
// s: 用户添加的输出信息
char *strsignal(int sig);

调用 strsignal() 函数将会获取到参数 sig 指定的信号对应的描述信息,返回该描述信息字符串的指针 ;函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息。

测试如下:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> int main(void) {printf("SIGINT描述信息: %s\n", strsignal(SIGINT));printf("SIGQUIT描述信息: %s\n", strsignal(SIGQUIT));printf("SIGBUS描述信息: %s\n", strsignal(SIGBUS));printf("编号为1000的描述信息: %s\n", strsignal(1000));exit(0);
}

测试结果:

psignal() 输出描述信息到标准错误函数

psignal() 可以在 标准错误(stderr) 上输出信号描述信息 ,其函数原型如下所示:

#include <signal.h> // sig:指定的信号
void psignal(int sig, const char *s);

所以整个输出信息由 字符串s + 冒号 + 空格 + 描述信号编号sig的字符串 + 尾随的换行符 组成。

测试如下:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) { psignal(SIGINT, "SIGINT信号描述信息"); psignal(SIGQUIT, "SIGQUIT信号描述信息"); psignal(SIGBUS, "SIGBUS信号描述信息");exit(0);
}

运行结果:

信号掩码(阻塞信号传递)

内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。 当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理 ,那么内核会将其阻塞, 直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理

向信号掩码中添加一个信号,通常有如下几种方式:

  • 当应用程序调用 signal()sigaction() 函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction() 而言,是否会如此,需要根据 sigaction() 函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
  • 使用 sigaction() 函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过 sa_mask 参数进行设置,参考前面所述。
  • 除了以上两种方式之外,还可以使用 sigprocmask() 系统调用,随时可以显式地向信号掩码中添加/移除信号。

sigprocmask() 添加/移除/设置信号集至信号掩码

#include <signal.h> // how:参数how指定了调用函数时的一些行为。
// set:将参数set指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数set为NULL,则表示无需对当前信号掩码作出改动。
// oldset:如果参数oldset不为NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在oldset所指定的信号集中;如果为NULL则表示不获取当前的信号掩码。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

返回值:成功返回0;失败将返回-1,并设置errno。

参数 how 可以设置为以下宏:

  • SIG_BLOCK :将参数 set所指向的信号集内的所有信号添加到进程的信号掩码中 。换言之,将信号掩码设置为当前值与set的并集。
  • SIG_UNBLOCK :将参数 set指向的信号集内的所有信号从进程信号掩码中移除
  • SIG_SETMASK :将参数 set指向的信号集直接设置为进程信号掩码

下面编写一个简单的测试代码,验证信号掩码的作用:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> static void sig_handler(int sig)
{ printf("执行信号处理函数...\n");
}int main(void)
{ struct sigaction sig = {0}; sigset_t sig_set; /* 注册信号处理函数 */ sig.sa_handler = sig_handler; sig.sa_flags = 0; if (-1 == sigaction(SIGINT, &sig, NULL)) exit(-1); /* 信号集初始化 */ sigemptyset(&sig_set);sigaddset(&sig_set, SIGINT);/* 向信号掩码中添加信号 */ if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL)) exit(-1); /* 向自己发送信号 */ raise(SIGINT); /* 休眠2秒 */ sleep(2); printf("休眠结束\n"); /* 从信号掩码中移除添加的信号 */ if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL)) exit(-1); exit(0);
}

上述代码中,我们为 SIGINT 信号注册了一个处理函数 sig_handler ,当进程接收到该信号之后就会执行它;然后调用 sigprocmask 函数将 SIGINT 信号添加到信号掩码中,然后再调用 raise(SIGINT) 向自己发送一个 SIGINT 信号,如果信号掩码没有生效、也就意味着 SIGINT 信号不会被阻塞,那么调用 raise(SIGINT) 之后应该就会立马执行 sig_handler 函数,从而打印出"执行信号处理函数…"字符串信息;如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在2秒后才执行,因为程序中使用 sleep(2) 休眠了2秒钟之后,才将 SIGINT 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。流程图如下:

#mermaid-svg-Ahqd0SBIYwPbvjs5 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .error-icon{fill:#552222;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .marker.cross{stroke:#333333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .cluster-label text{fill:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .cluster-label span{color:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .label text,#mermaid-svg-Ahqd0SBIYwPbvjs5 span{fill:#333;color:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .node rect,#mermaid-svg-Ahqd0SBIYwPbvjs5 .node circle,#mermaid-svg-Ahqd0SBIYwPbvjs5 .node ellipse,#mermaid-svg-Ahqd0SBIYwPbvjs5 .node polygon,#mermaid-svg-Ahqd0SBIYwPbvjs5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .node .label{text-align:center;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .node.clickable{cursor:pointer;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .arrowheadPath{fill:#333333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .cluster text{fill:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 .cluster span{color:#333;}#mermaid-svg-Ahqd0SBIYwPbvjs5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ahqd0SBIYwPbvjs5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

信号掩码失效
信号掩码生效
调用sigaction()
SIGINT信号注册处理函数sig_handler()
调用sigprocmask()
将SIGINT信号添加到信号掩码
调用raise(SIGINT)
向自己发送SIGINT信号
直接执行sig_handler(),然后再延时2s
1. 打印“执行信号处理函数...”
2. 再打印“休眠结束”
先延时2s,移除信号掩码后再执行sig_handler()
1. 打印“休眠结束”
2. 再打印“执行信号处理函数...”

运行的结果如下,证明信号掩码生效:

阻塞等待

sigsuspend() 阻塞等待信号

前面已经说明,更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的关键代码段。如果希望 对一个信号解除阻塞后,然后调用 pause() 以等待之前被阻塞的信号的传递 ,这将如何?譬如有如下代码段:

sigset_t new_set, old_set; /* 信号集初始化 */
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT); /* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set)) exit(-1); /* 受保护的关键代码段 */
......
/**********************/ /* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL)) exit(-1); /* 等待信号唤醒 */
pause();

执行受保护的关键代码时不希望被 SIGINT 信号打断,所以在执行关键代码之前将 SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。最后调用了 pause() 阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从 pause 返回继续执行;

考虑这样一种情况:如果信号的传递恰好发生在第二次调用 sigprocmask() 之后、 pause() 之前,就会产生一个问题:信号传递过来会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到 pause() 被阻塞,直到下一次信号发生时才会被唤醒,这有违代码的本意。

虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能,这必然是一个缺陷,要避免这个问题, 需要将恢复信号掩码和 pause() 挂起进程这两个动作封装成一个原子操作 ,这正是 sigsuspend() 系统调用的目的所在, sigsuspend() 函数原型如下所示:

#include <signal.h> // mask:参数mask指向一个信号集
int sigsuspend(const sigset_t *mask);

返回值: sigsuspend() 始终返回-1,并设置 errno 来指示错误(通常为 EINTR ),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT

sigsuspend() 函数会用参数 mask 所指向的信号集来替换进程的信号掩码, 也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒 (如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回, sigsuspend() 会将进程的信号掩码恢复成调用前的值。

调用 sigsuspend() 函数相当于以不可中断(原子操作)的方式执行以下操作:

sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);

使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> static void sig_handler(int sig)
{printf("执行信号处理函数...\n");
} int main(void)
{ struct sigaction sig = {0}; sigset_t new_mask, old_mask, wait_mask; /* 信号集初始化 */ sigemptyset(&new_mask); sigaddset(&new_mask, SIGINT); sigemptyset(&wait_mask); /* 注册信号处理函数 */ sig.sa_handler = sig_handler; sig.sa_flags = 0; if (-1 == sigaction(SIGINT, &sig, NULL))exit(-1); /* 向信号掩码中添加信号 */ if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))exit(-1); /* 执行保护代码段 */ puts("执行保护代码段"); /******************/ /* 挂起、等待信号唤醒 */ if (-1 != sigsuspend(&wait_mask)) exit(-1); /* 恢复信号掩码 */ if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL)) exit(-1); exit(0);
}

在上述代码中,我们希望执行受保护代码段时不被 SIGINT 中断信号打断,所以在执行保护代码段之前将 SIGINT 信号添加到进程的信号掩码中, 执行完受保护的代码段之后,调用 sigsuspend() 挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态

实时信号

如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中, 为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending() 函数获取

sigpending() 读取等待信号集

函数原型如下:

#include <signal.h> // set:处于等待状态的信号会存放在参数set所指向的信号集中
int sigpending(sigset_t *set);

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例,判断 SIGINT 信号当前是否处于等待状态:

/* 定义信号集 */
sigset_t sig_set; /* 将信号集初始化为空 */
sigemptyset(&sig_set);/* 获取当前处于等待状态的信号 */
sigpending(&sig_set); /* 判断SIGINT信号是否处于等待状态 */
if (1 == sigismember(&sig_set, SIGINT)) puts("SIGINT信号处于等待状态");
else if (!sigismember(&sig_set, SIGINT)) puts("SIGINT信号未处于等待状态");

sigqueue() 发送实时信号

等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之, 如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一

可以使用 sigqueue() 函数发送实时信号,其函数原型如下所示:

#include <signal.h> // pid:指定接收信号的进程对应的pid,将信号发送给该进程
// sig:指定需要发送的信号。与kill()函数一样,也可将参数sig设置为0,用于检查参数pid所指定的进程是否存在
// value:参数value指定了信号的伴随数据,union sigval数据类型
int sigqueue(pid_t pid, int sig, const union sigval value);

返回值:成功将返回0;失败将返回-1,并设置 errno

union sigval 数据类型(共用体)如下所示:

typedef union sigval
{ int sival_int;void *sival_ptr;
} sigval_t;

携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针

使用举例:

发送进程使用 sigqueue() 系统调用向另一个进程发送实时信号:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h> int main(int argc, char *argv[])
{ union sigval sig_val; int pid; int sig; /* 判断传参个数 */ if (3 > argc) exit(-1); /* 获取用户传递的参数 */ pid = atoi(argv[1]); sig = atoi(argv[2]); printf("pid: %d\nsignal: %d\n", pid, sig); /* 发送信号 */ sig_val.sival_int = 10; //伴随数据 if (-1 == sigqueue(pid, sig, sig_val)) {perror("sigqueue error"); exit(-1); } puts("信号发送成功!"); exit(0);
}

接收进程使用 sigaction() 函数为信号绑定处理函数:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> static void sig_handler(int sig, siginfo_t *info, void *context)
{ sigval_t sig_val = info->si_value;printf("接收到实时信号: %d\n", sig); printf("伴随数据为: %d\n", sig_val.sival_int);
}int main(int argc, char *argv[])
{ struct sigaction sig = {0}; int num;/* 判断传参个数 */ if (2 > argc) exit(-1); /* 获取用户传递的参数 */ num = atoi(argv[1]); /* 为实时信号绑定处理函数 */ sig.sa_sigaction = sig_handler; sig.sa_flags = SA_SIGINFO;     // 选择 sa_sigaction 作为信号处理函数,而不是 sa_handlerif (-1 == sigaction(num, &sig, NULL)) { perror("sigaction error"); exit(-1); } /* 死循环 */ for ( ; ; ) sleep(1); exit(0);
}

异常退出

abort() 终止进程

不同于 exit()_exit()_Exit() 这些函数用于正常退出应用程序, 对于异常退出程序,则一般使用 abort() 库函数 。使用 abort() 终止进程运行,会生成核心转储文件,可用于判断程序调用 abort() 时的程序状态。

abort() 函数原型如下所示:

#include <stdlib.h> void abort(void);

函数 abort() 通常产生 SIGABRT 信号来终止调用该函数的进程, SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件。

使用实例如下:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> static void sig_handler(int sig)
{ printf("接收到信号: %d\n", sig);
} int main(int argc, char *argv[])
{struct sigaction sig = {0}; sig.sa_handler = sig_handler; sig.sa_flags = 0; if (-1 == sigaction(SIGABRT, &sig, NULL)) { perror("sigaction error"); exit(-1); } sleep(2); abort(); // 调用abort for ( ; ; ) sleep(1); exit(0);
}

运行结果:

从打印信息可知,即使在我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止, 无论阻塞或忽略 SIGABRT 信号, abort() 调用均不收到影响,总会成功终止进程

参考文献

1:【正点原子】STM32MP1嵌入式Linux C应用编程指南V1.4 - 第八章



如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
Copyright © 2023.05 by Mr.Idleman. All rights reserved.


Linux 信号学习相关推荐

  1. linux信号学习02

    未决信号集与阻塞信号集(信号屏蔽字) 阻塞信号集: 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后) 未决信号集: a. 信号产生,未决信号集中描述该 ...

  2. Linux shell 学习笔记(12)— linux 信号、后台运行脚本、作业控制、定时运行任务

    1. 处理信号 1.1 Linux 信号 常见的 Linux 信号如下表所示: 信号 值 描述 1 SIGHUP 挂起进程 2 SIGINT 终止进程 3 SIGQUIT 停止进程 9 SIGKILL ...

  3. 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】Linux信号机制分析

    Linux信号机制分析 Sailor_forever  sailing_9806@163.com 转载请注明 http://blog.csdn.net/sailor_8318/archive/2008 ...

  4. Linux内核学习--内存管理模块

    Linux内核学习--内存管理模块 首先,Linux内核主要由五个部分组成,他们分别是:进程调度模块.内存管理模块.文件系统模块.进程间通信模块和网络接口模块. 本部分所讲的内存是内存管理模块,其主要 ...

  5. Linux驱动学习--USB接口wifi/BT芯片开发之BT开发(BlueDroid框架)

    目录 一.引言 二.整体框架分析(结合实际芯片分析) 三.内核中的相关配置 四.厂家驱动分析 五.蓝牙BlueDroid协议 一.引言 之前我们简单分析过BlueDroid框架,今天来结合源码,挑重点 ...

  6. linux所有信号,【转载】Linux 信号列表

    信号及其简介 信号是一种进程通信的方法,他应用于异步事件的处理.信号的实现是一种软中断.它被发送为一个正在运行的进程,已告知进程某个事件发生了. SIGHUP 本信号在用户终端连接(正常或非正常)结束 ...

  7. Linux命令学习笔记

    Linux命令学习笔记 1 前言 1.1 系统版本 1.2 终端切换 1.3 快捷键 1.4 帮助文档 1.5 关机重启 2 目录命令 2.1 当前目录 pwd 2.2 切换目录 cd 2.3 查看目 ...

  8. linux -- 信号

    信号及信号来源 使用kill命令杀死进程的实质,是向目标进程发送了一个信号,当目标进程接收到这个信号后,会根据信号的处理函数,执行指 定动作. 比如:keil 杀死进程就是使用的9号信号 使用&quo ...

  9. linux pipe 命名管道,linux 进程学习笔记-named pipe (FIFO)命名管道

    与"无名管道"不同的是,FIFO拥有一个名称来标志它,所谓的名称实际上就是一个路径,比如"/tmp/my_fifo",其对应到磁盘上的一个管道文件,如果我们用f ...

最新文章

  1. 设计模式在外卖营销业务中的实践
  2. java srs 推流_srs安装与ffmpeg推流
  3. 2019年上半年收集到的人工智能循环神经网络干货文章
  4. PMCAFF微课堂|诸葛理财联合创始人:互联网金融产品的那些坑
  5. cocos2d-x游戏实例(15)-纵版射击游戏(2)
  6. 游戏组件——挑战:创建NextBlock游戏组件
  7. 刷排名优优软件_QQ群拉人了,排名后置了?申诉案例
  8. window 写txt 换行
  9. printf参数的问题
  10. spark sql练习之join操作
  11. 树形dp 没有上司的舞会
  12. DELMIA软件物流仿真:操作对象显示与隐藏功能介绍与使用方法
  13. node_注册页面+连接数据库+加密
  14. matplotlib折线图(标记点、标记点大小、标记点边颜色、标记点边宽)
  15. 0x77A26B83 (ntdll.dll) 处有未经处理的异常: 0xC0000374: 堆已损坏。
  16. 解决import cv2找不到指定模块问题
  17. 3D打印人体骨骼?这家哈佛大学孵化的公司要为中国医生开发自己的软件平台...
  18. python好用的软件_比python好用的分析软件_财务分析报表怎么做_企业财务报表分析软件-帆软...
  19. Springboot出现@Value注解无法读取配置文件的解决方法
  20. go http post请求

热门文章

  1. vue点击按钮打开新页签,并传参
  2. JSP 【基础】连接数据库的登录及验证
  3. c语言象棋教程下载,C语言程序源代码中国象棋.doc-资源下载在线文库www.lddoc.cn...
  4. uni-app点击按钮弹出提示框-uni.showModal(OBJECT),选择确定和取消
  5. 购买企业邮箱价格多少钱?企业邮箱注册申请哪个好?
  6. Python为何会发展的如此之快?带你纵观全球Python趋势!
  7. ARM64之内存屏障
  8. mac单节点安装Erda实践
  9. win10 t480耳机没有声音处理方法
  10. android1f21ec,polaris.tmall.com