Linux 信号学习

news2024/11/24 22:29:10

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 命令可查看到所有信号,如下所示:

image-20230504154409075

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); 

}

运行测试:

image-20230504184540462

信号集

通常我们需要有 一个能表示多个信号(一组信号)的数据类型——信号集(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); 
}

运行结果:

image-20230504190910919

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); 
}

测试结果:

image-20230504191250598

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); 
}

运行结果:

image-20230504192000599

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

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

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

  • 当应用程序调用 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 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。流程图如下:

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

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

image-20230504193636710

阻塞等待

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_handler
    if (-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); 
}

运行结果:

image-20230504204309882

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


参考文献

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




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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/489463.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

玩转传感器----理解时序和数据采集(DHT11)

该文章以DHT11模块进行分析 目录 1.模块复位&#xff08;时序图&#xff09; 2.DHT11的应答信号 3.读取1bit数值&#xff08;比较高电平的时间是否大于40us&#xff09; 4.读取一个字节 5.把读取的字节放入单片机 6. 寄存器设置IO口方向 1.模块复位&#xff08;时序图&a…

22.Java多线程

Java多线程 一、进程和线程 进程是程序的一次动态执行过程&#xff0c;它需要经历从代码加载&#xff0c;代码执行到执行完毕的一个完整的过程&#xff0c;这个过程也是进程本身从产生&#xff0c;发展到最终消亡的过程。多进程操作系统能同时达运行多个进程&#xff08;程序…

使用Python接口自动化测试post请求和get请求,获取请求返回值

目录 引言 请求接口为Post时&#xff0c;传参方法 获取接口请求响应数据 引言 我们在做python接口自动化测试时&#xff0c;接口的请求方法有get,post等&#xff1b;get和post请求传参&#xff0c;和获取接口响应数据的方法&#xff1b; 请求接口为Post时&#xff0c;传参方法…

C++系列二:数据类型

C数据类型 1. 原始类型2. 复合类型3. 类型转换3.1 隐式类型转换3.2 显式类型转换 4. 总结&#xff08;手稿版&#xff09; 1. 原始类型 C 中的原始类型包括整型&#xff08;integral types&#xff09;、浮点型&#xff08;floating-point types&#xff09;、字符型&#xff…

涨薪60%,从小厂逆袭,坐上美团技术专家(面经+心得)

前言 大多数情况下&#xff0c;程序员的个人技能成长速度&#xff0c;远远大于公司规模或业务的成长速度。所以&#xff0c;跳槽成为了这个行业里最常见的一个词汇。 实际上&#xff0c;跳槽的目的无非是为了涨薪或是职业发展&#xff0c;我也不例外。普通本科毕业后&#xf…

计算机网络基础知识(一)计算机发展史、网络设备、网络结构及拓扑

文章目录 01 | 网络设备02 | 网络结构 && 拓扑 网络发展史可以追溯到20世纪60年代&#xff0c;当时美国国防部高级研究计划署&#xff08;ARPA&#xff09;启动了一个名为 ARPANET 的项目&#xff0c;旨在建立军事目的的分布式通信网络&#xff0c;使得网络中的任何一台…

【redis】redis红锁Redlock算法和底层源码分析

【redis】redis红锁Redlock算法和底层源码分析 文章目录 【redis】redis红锁Redlock算法和底层源码分析前言一、当前代码为8.0版&#xff0c;接上一步分布式锁的主要考点lock加锁关键逻辑unlock解锁关键逻辑 二、redis分布式锁-Redlock红锁主页说明:目前所写的分布式锁还有什么…

c++自学笔记(陆续更新)

本笔记为从菜鸟教程边学边记录的笔记---》C 教程 | 菜鸟教程 面向对象程序设计 封装&#xff08;Encapsulation&#xff09;&#xff1a;封装是将数据和方法组合在一起&#xff0c;对外部隐藏实现细节&#xff0c;只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。…

C语言入门教程||C语言 头文件||C语言 强制类型转换

C语言 头文件 头文件是扩展名为 .h 的文件&#xff0c;包含了 C 函数声明和宏定义&#xff0c;被多个源文件中引用共享。有两种类型的头文件&#xff1a;程序员编写的头文件和编译器自带的头文件。 在程序中要使用头文件&#xff0c;需要使用 C 预处理指令 #include 来引用它…

USART串口接收

文章目录 运行环境&#xff1a;1.1 串口接收代码分析1)开启接收中断和空闲中断2)接收存储变量声明和定义3)中断处理函数 2.1实验效果 运行环境&#xff1a; ubuntu18.04.melodic 宏基暗影骑士笔记本 stm32f427IIH6 stlink 9-24v可调电源 usb转串口 杜邦线转4pin 1.1 串口接收…

Python | 人脸识别+活体检测+背景模糊+关键点检测系统(Face_Recognition+dlib+OpenCV+MediaPipe+PyQt)

本博客为人脸识别系统项目简介 项目GitHub完整源代码地址&#xff1a; 一、运行环境 本系统能够运行在基于PC操作系统Windows环境下&#xff0c;要求Windows操作系统安装Python 3.9 及以上环境&#xff0c;且已安装MySQL数据库。 Python3.9 安装&#xff1a;Python 3.9安装教程…

【UE】坦克开火

1. 添加开火的操作映射 2. 创建一个actor蓝图类&#xff0c;添加一个静态网格体组件 添加发射物移动组件 设置初始速度和最大速度 发射物重力范围设为0.05 添加音频组件 设置音效 3. 打开炮管的静态网格体 在插槽管理器中创建插槽 将创建的插槽放到炮口位置 4. 打开“BP_BaseT…

B-Tree (多路查找树)分析-20230503

B-Tree (多路查找树)学习-20230503 前言 B-树是一类多路查询树&#xff0c;它主要用于文件系统和某些数据库的索引&#xff0c;如果采用二叉平衡树访问文件里面的数据&#xff0c;最坏情况下&#xff0c;磁头可能需要进行O(h)次对磁盘的读写&#xff0c;其中h为树的高度&…

探究Android插件化开发的新思路——Shadow插件化框架

Shadow插件化框架是什么&#xff1f; Shadow是一种Android App的插件化框架&#xff0c;它利用类似于ClassLoader的机制来实现应用程序中的模块化&#xff0c;并让这些模块可以在运行时灵活地进行加载和卸载。Shadow框架主张将一个大型的Android App拆分成多个小模块&#xff…

提升V-Ray渲染效率的五个实用技巧!

Chaos V-Ray是一个 3D渲染插件&#xff0c;可用于所有主要的 3D设计和 CAD程序。V-Ray可以和 3ds Max、Cinema 4D、Houdini、Maya、Nuke、Revit、Rhino、SketchUp和 Unreal无缝协作。艺术家和设计师也可以通过使用 V-Ray的实时光线来追踪探索和分享自己的项目&#xff0c;并渲染…

SpringBoot调取OpenAi接口实现ChatGpt功能

很高兴和大家分享我实现的一个小项目&#xff0c;利用 Spring Boot 实现了一个 ChatGpt 对话系统。在本文中&#xff0c;我将详细介绍这个项目的实现步骤&#xff0c;以及代码实现。 什么是 ChatGpt ChatGpt 是一种基于 GPT 技术的对话系统&#xff0c;能够生成连贯、流畅、…

V-Ray怎么快速渲染_渲染加速小技巧

很多小伙伴在使用V-Ray渲染器的时候都想要更加快速的出图&#xff0c;今天Renderbus瑞云渲染就给大家分享一下V-Ray渲染加速的小技巧。 在了解渲染加速技巧之前&#xff0c;我们首先要了解渲染制作主要受两个因素影响——图像质量和渲染时间&#xff0c;高图像质量是以长渲染时…

k8s集群搭建(3主2从)

目录 kubeadm 和二进制安装 k8s 适用场景分析 多 master 节点高可用架构图 集群环境准备 部署过程 修改主机内核参数&#xff08;所有节点&#xff09; 配置阿里云的repo源&#xff08;所有节点&#xff09; 配置国内安装 docker 和 containerd 的阿里云的 repo 源 配置…

day11 TCP连接管理与UDP协议

目录 ​编辑 连接的建立——”三次握手” 连接的释放——“四次挥手” 保活计时器 用户数据报协议 UDP​编辑 连接的建立——”三次握手” TCP 建立连接的过程叫做握手。 采用三报文握手&#xff1a;在客户和服务器之间交换三个 TCP 报文段&#xff0c;以防止已失效的连接…

SPSS如何进行多重响应分析之案例实训?

文章目录 0.引言1.多重响应变量频率分析2.对多重响应变量集进行交叉表分析3.使用表过程研究多重响应变量集 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对SPSS进行了学习&#xff0c;本文通过《SPSS统计分析从入门到精通》及其配套素材结合网上相关资料进行学习笔记…