• Linux_应用篇(08) 信号-基础


    本章将讨论信号,虽然信号的基本概念比较简单,但是其所涉及到的细节内容比较多,所以本章篇幅也会相对比较长。 事实上,在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法,所以信号机制在 Linux 早期版本中就已经提供了支持,随着 Linux 内核版本的更新迭代,其对信号机制的支持更加完善。
    本章将会讨论如下主题内容。
    ⚫ 信号的基本概念;
    ⚫ 信号的分类、 Linux 提供的各种不同的信号及其作用;
    ⚫ 发出信号以及响应信号,信号由“谁”发送、由“谁”处理以及如何处理;
    ⚫ 进程在默认情况下对信号的响应方式;
    ⚫ 使用进程信号掩码来阻塞信号、以及等待信号等相关概念;
    ⚫ 如何暂停进程的执行,并等待信号的到达。

    基本概念

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

    信号的目的是用来通信的
    一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。 信号可以由“谁”发出呢? 以下列举的很多情况均可以产生信号:
    ⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为 0、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
    ⚫ 用于在终端下输入了能够产生信号的特殊字符。 譬如在终端上按下 CTRL + C 组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程。
    ⚫ 进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组。 当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是 root 超级用户。
    ⚫ 用户可以通过 kill 命令将信号发送给其它进程。 kill 命令想必大家都会使用,通常我们会通过 kill命令来“杀死”(终止)一个进程,譬如在终端下执行"kill -9 xxx"来杀死 PID 为 xxx 的进程。 kill命令其内部的实现原理便是通过 kill()系统调用来完成的。
    ⚫ 发生了软件事件,即当检测到某种软件条件已经发生。 这里指的不是硬件产生的条件(如除数为 0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况)。进程同样也可以向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核。
    以上便是可以产生信号的多种不同的条件,总的来看,信号的目的都是用于通信的,当发生某种情况下,通过信号将情况“告知”相应的进程,从而达到同步、通信的目的。

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

    信号是异步的

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

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

    1. /* Signals. */
    2. #define SIGHUP 1 /* Hangup (POSIX). */
    3. #define SIGINT 2 /* Interrupt (ANSI). */
    4. #define SIGQUIT 3 /* Quit (POSIX). */
    5. #define SIGILL 4 /* Illegal instruction (ANSI). */
    6. #define SIGTRAP 5 /* Trace trap (POSIX). */
    7. #define SIGABRT 6 /* Abort (ANSI). */
    8. #define SIGIOT 6 /* IOT trap (4.2 BSD). */
    9. #define SIGBUS 7 /* BUS error (4.2 BSD). */
    10. #define SIGFPE 8 /* Floating-point exception (ANSI). */
    11. #define SIGKILL 9 /* Kill, unblockable (POSIX). */
    12. #define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
    13. #define SIGSEGV 11 /* Segmentation violation (ANSI). */
    14. #define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
    15. #define SIGPIPE 13 /* Broken pipe (POSIX). */
    16. #define SIGALRM 14 /* Alarm clock (POSIX). */
    17. #define SIGTERM 15 /* Termination (ANSI). */
    18. #define SIGSTKFLT 16 /* Stack fault. */
    19. #define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
    20. #define SIGCHLD 17 /* Child status has changed (POSIX). */
    21. #define SIGCONT 18 /* Continue (POSIX). */
    22. #define SIGSTOP 19 /* Stop, unblockable (POSIX). */
    23. #define SIGTSTP 20 /* Keyboard stop (POSIX). */
    24. #define SIGTTIN 21 /* Background read from tty (POSIX). */
    25. #define SIGTTOU 22 /* Background write to tty (POSIX). */
    26. #define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
    27. #define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
    28. #define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
    29. #define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
    30. #define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
    31. #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
    32. #define SIGPOLL SIGIO /* Pollable event occurred (System V). */
    33. #define SIGIO 29 /* I/O now possible (4.2 BSD). */
    34. #define SIGPWR 30 /* Power failure restart (System V). */
    35. #define SIGSYS 31 /* Bad system call. */
    36. #define SIGUNUSED 31

    不存在编号为 0 的信号,信号编号是从 1 开始的,事实上 kill()函数对信号编号 0 有着特殊的应用,关于这个将会在后面的内容向大家介绍。

    信号的分类

    Linux 系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号,本小节将对这些信号的分类进行简单地介绍。

    可靠信号与不可靠信号

    Linux 信号机制基本上是从 UNIX 系统中继承过来的, 早期 UNIX 系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,它的主要问题是:
    ⚫ 进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新为该信号绑定相应的处理函数。
    ⚫ 早期 UNIX 下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。

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

    Tips:括号" ) "前面的数字对应该信号的编号,编号 1~31 所对应的是不可靠信号,编号 34~64 对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAX-N 的方式来表示。可靠信号支持排队,不会丢失, 同时,信号的发送和绑定也出现了新版本, 信号发送函数 sigqueue()及信号绑定函数 sigaction()。
    早期 UNIX 系统只定义了 31 种信号,而 Linux 3.x 支持 64 种信号,编号 1-64(SIGRTMIN=34,SIGRTMAX=64),将来可能进一步增加,这需要得到内核的支持。前 31 种信号已经有了预定义值,每个信号有了确定的用途、 含义以及对应的名字,并且每种信号都有各自的系统默认操作。如按键盘的 CTRL+C时,会产生 SIGINT 信号,对该信号的系统默认操作就是终止进程, 后 32 个信号表示可靠信号。

    实时信号与非实时信号

    实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的, 非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。 实时信号保证了发送的多个信号都能被接收, 实时信号是 POSIX 标准的一部分,可用于应用进程。一般我们也把非实时信号(不可靠信号)称为标准信号,如果文档中用到了这个词,那么大家要知道,这里指的就是非实时信号(不可靠信号)。 

    常见信号与默认行为

    前面说到, Linux 下对标准信号(不可靠信号、 非实时信号) 的编号为 1~31,接下来将介绍这些信号以及这些信号所对应的系统默认操作。
    ⚫ 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
    如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。以上就是关于这些信号的简单介绍内容,以上所介绍的这些信号并不包括 Linux 下所有的信号,仅给大
    家介绍了一下常见信号, 下表将对这些信号进行总结。

    信号名称编号描述系统默认操作
    SIGINT2终端中断符term
    SIGQUIT3终端退出符term+core
    SIGILL4非法硬件指令term+core
    SIGABRT6异常终止(abort)term+core
    SIGBUS7内存访问错误term+core
    SIGFPE8算术异常term+core
    SIGKILL9终极终止信号term
    SIGUSR110用户自定义信号 1term
    SIGSEGV11无效的内存引用term+core
    SIGUSR212用户自定义信号 2term
    SIGPIPE13管道关闭term
    SIGALRM14定时器超时(alarm)term
    SIGTERM15终止进程term
    SIGCHLD/SIGCLD17子进程终止或停止ignore
    SIGCONT18使停止状态的进程继续运行cont
    SIGSTOP19停止进程stop
    SIGTSTP20终端停止符stop
    SIGXCPU24超过 CPU 限制term+core
    SIGVTALRM26虚拟定时器超时term
    SIGWINCH28终端窗口尺寸发生变化ignore
    SIGPOLL/SIGIO29异步 I/Oterm/ignore
    SIGSYS31无效系统调用term+core

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

    进程对信号的处理

    当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。 Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式,本小节将向大家介绍这两个系统调用的使用方法。

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

    1. #include
    2. typedef void (*sig_t)(int);
    3. sig_t signal(int signum, sig_t handler);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    signum: 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
    handler: sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN 或 SIG_DFL, SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。
    sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。

    1. Tips: SIG_IGN、 SIG_DFL 分别取值如下:
    2. /* Fake signal functions. */
    3. #define SIG_ERR ((sig_t) -1) /* Error return. */
    4. #define SIG_DFL ((sig_t) 0) /* Default action. */
    5. #define SIG_IGN ((sig_t) 1) /* Ignore signal. */

    返回值: 此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno。
    由此可知, signal()函数可以根据第二个参数 handler 的不同设置情况,可对信号进行不同的处理。

    1. #include
    2. #include
    3. #include
    4. static void sig_handler(int sig)
    5. {
    6. printf("Received signal: %d\n", sig);
    7. }
    8. int main(int argc, char *argv[])
    9. {
    10. sig_t ret = NULL;
    11. ret = signal(SIGINT, (sig_t)sig_handler);
    12. if (SIG_ERR == ret) {
    13. perror("signal error");
    14. exit(-1);
    15. }
    16. /* 死循环 */
    17. for ( ; ; ) { }
    18. exit(0);
    19. }

    在上述示例代码中,我们通过 signal()函数将 SIGINT(2) 信号绑定到了一个用户自定的处理函数上sig_handler(int sig), 当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句。当运行程序之后,程序会卡在 for 死循环处,此时在终端按下中断符 CTRL + C,系统便会给前台进程组中的每一个进程发送SIGINT 信号,我们测试程序便会收到该信号。运行测试:

    当运行程序之后,程序会占用终端称为一个前台进程,此时按下中断符便会打印出信息(^C 表示按下了中断符)。平时大家使用 CTRL + C 可以终止一个进程,而这里却不能通过这种方式来终止这个测试程序,原因在于测试程序中捕获了该信号,而对应的处理方式仅仅只是打印一条语句、而并不终止进程。那此时该怎么关闭这个测试程序呢?前面给大家介绍了“一击必杀”信号 SIGKILL(编号为 9),可向该进程发送 SIGKILL 暴力终止该进程,当然一般不推荐大家这样使用,如果实在没办法才采取这种措施。新打开一个终端,使用 ps 命令找到该进程的 pid 号,再使用 kill 命令,如下所示:

    我们再执行一次测试程序,这里将测试程序放在后台运行,然后再按下中断符:

    按下中断符发现进程并没有收到 SIGINT 信号,原因很简单,因为进程并不是前台进程、而是一个后台进程,按下中断符时系统并不会给后台进程发送 SIGINT 信号。 可以使用 kill 命令手动发送信号给我们的进程:

    两种不同状态下信号的处理方式
    通过上面的介绍, 以及我们的测试实验, 不知大家是否出现了一个疑问?如果程序中没有调用 signal()函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到 signal()处,那么这时进程接收到一个信号后是如何处理的呢?带着这个问题来聊一聊。
    ⚫ 程序启动
    当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数) , 通常情况下, 进程对所有信号的处理方式都设置为系统默认操作。所以如果在我们的程序当中,没有调用 signal()为信号设置处理方式,则默认的处理方式便是系统默认操作。所以为什么大家平时都可以使用 CTRL + C 中断符来终止一个进程,因为大部分情况下,应用程序中并不会为 SIGINT 信号设置处理方式,所以该信号的处理方式便是系统默认操作,当接收到信号之后便执行系统默认操作,而 SIGINT 信号的系统默认操作便是终止进程。
    ⚫ 进程创建
    当一个进程调用 fork()创建子进程时,其子进程将会继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中是有意义的。

    sigaction()函数

    除了signal()之外, sigaction()系统调用是设置信号处理方式的另一选择, 事实上,推荐大家使用sigaction()函数。虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报, sigaction()也更具灵活性以及移植性。sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:

    1. #include
    2. int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
    act: act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
    oldact: oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL, 则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
    返回值: 成功返回 0;失败将返回-1,并设置 errno。

    1. struct sigaction {
    2. void (*sa_handler)(int);
    3. void (*sa_sigaction)(int, siginfo_t *, void *);
    4. sigset_t sa_mask;
    5. int sa_flags;
    6. void (*sa_restorer)(void);
    7. };

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

    siginfo_t 结构体

    1. siginfo_t {
    2. int si_signo; /* Signal number */
    3. int si_errno; /* An errno value */
    4. int si_code; /* Signal code */
    5. int si_trapno; /* Trap number that caused hardware-generated signal(unused on most architectures) */
    6. pid_t si_pid; /* Sending process ID */
    7. uid_t si_uid; /* Real user ID of sending process */
    8. int si_status; /* Exit value or signal */
    9. clock_t si_utime; /* User time consumed */
    10. clock_t si_stime; /* System time consumed */
    11. sigval_t si_value; /* Signal value */
    12. int si_int; /* POSIX.1b signal */
    13. void *si_ptr; /* POSIX.1b signal */
    14. int si_overrun; /* Timer overrun count; POSIX.1b timers */
    15. int si_timerid; /* Timer ID; POSIX.1b timers */
    16. void *si_addr; /* Memory location which caused fault */
    17. long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
    18. int si_fd; /* File descriptor */
    19. short si_addr_lsb; /* Least significant bit of address(since Linux 2.6.32) */
    20. void *si_call_addr; /* Address of system call instruction(since Linux 3.5) */
    21. int si_syscall; /* Number of attempted system call(since Linux 3.5) */
    22. unsigned int si_arch; /* Architecture of attempted system call(since Linux 3.5) */
    23. }
    1. #include
    2. #include
    3. #include
    4. static void sig_handler(int sig)
    5. {
    6. printf("Received signal: %d\n", sig);
    7. }
    8. int main(int argc, char *argv[])
    9. {
    10. struct sigaction sig = {0};
    11. int ret;
    12. sig.sa_handler = sig_handler;
    13. sig.sa_flags = 0;
    14. ret = sigaction(SIGINT, &sig, NULL);
    15. if (-1 == ret) {
    16. perror("sigaction error");
    17. exit(-1);
    18. }
    19. /* 死循环 */
    20. for ( ; ; ) { }
    21. exit(0);
    22. }

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

    向进程发送信号

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

    kill()函数

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

    1. #include
    2. #include
    3. int kill(pid_t pid, int sig);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    pid: 参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或-1 以及小于-1 等不同值,稍后给说明。
    sig: 参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但仍执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
    返回值: 成功返回 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()将会返回-1, errno 将被设置为 ESRCH,表示进程不存在。

    raise()

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

    1. #include
    2. int raise(int sig);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    sig: 需要发送的信号。
    返回值: 成功返回 0;失败将返回非零值。
    raise()其实等价于:

    kill(getpid(), sig);

    Tips: getpid()函数用于获取进程自身的 pid。

    alarm()和 pause()函数

    alarm()函数

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

    1. #include
    2. unsigned int alarm(unsigned int seconds);

    函数参数和返回值:
    seconds: 设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。
    返回值: 如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。
    参数 seconds 的值是产生 SIGALRM 信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,每个进程只能设置一个 alarm 闹钟; 虽然 SIGALRM 信号的系统默认操作是终止进程,但是如果程序当中设置了 alarm 闹钟,大多数使用闹钟的进程都会捕获此信号。需要注意的是 alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm()函数设置定时器。

    pause()函数

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

    1. #include
    2. int pause(void);

    信号集

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

    1. # define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
    2. typedef struct
    3. {
    4. unsigned long int __val[_SIGSET_NWORDS];
    5. } sigset_t;

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

    初始化信号集

    sigemptyset()和 sigfillset()用于初始化信号集。 sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),函数原型如下:

    1. #include
    2. int sigemptyset(sigset_t *set);
    3. int sigfillset(sigset_t *set);

    使用这些函数需要包含头文件
    函数参数和返回值含义如下:
    set: 指向需要进行初始化的信号集变量。
    返回值: 成功返回 0;失败将返回-1,并设置 errno。
    使用示例
    初始化为空信号集:

    1. sigset_t sig_set;
    2. sigemptyset(&sig_set);

    初始化信号集,使其包含所有信号:

    1. sigset_t sig_set;
    2. sigfillset(&sig_set);

    向信号集中添加/删除信号

    分别使用 sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号,函数原型如下:

    1. #include
    2. int sigaddset(sigset_t *set, int signum);
    3. int sigdelset(sigset_t *set, int signum);

    函数参数和返回值含义如下:
    set: 指向信号集。
    signum: 需要添加/删除的信号。
    返回值: 成功返回 0;失败将返回-1,并设置 errno。
    使用示例
    向信号集中添加信号:

    1. sigset_t sig_set;
    2. sigemptyset(&sig_set);
    3. sigaddset(&sig_set, SIGINT);

    从信号集中移除信号:

    1. sigset_t sig_set;
    2. sigfillset(&sig_set);
    3. sigdelset(&sig_set, SIGINT);

    测试信号是否在信号集中

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

    1. #include
    2. int sigismember(const sigset_t *set, int signum);

    函数参数和返回值含义如下:
    set: 指定信号集。
    signum: 需要进行测试的信号。
    返回值: 如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno。
    使用示例
    判断 SIGINT 信号是否在 sig_set 信号集中:

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

    获取信号的描述信息

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

    1. #include
    2. #include
    3. #include
    4. int main(void)
    5. {
    6. printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
    7. printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
    8. printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
    9. exit(0);
    10. }

    strsignal()函数

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

    1. #include
    2. char *strsignal(int sig);

    使用 strsignal()函数需要包含头文件,这是一个库函数。调用 strsignal()函数将会获取到参数 sig 指定的信号对应的描述信息,返回该描述信息字符串的指针;函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息。

    psignal()函数

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

    1. #include
    2. void psignal(int sig, const char *s);

    调用 psignal()函数会将参数 sig 指定的信号对应的描述信息输出到标准错误,并且还允许调用者添加一些输出信息,由参数 s 指定;所以整个输出信息由字符串 s、冒号、空格、描述信号编号 sig 的字符串和尾随的换行符组成。

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

    内核为每一个进程维护了一个信号掩码(其实就是一个信号集) ,即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理, 那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。向信号掩码中添加一个信号,通常有如下几种方式:
    ⚫ 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中, 这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
    ⚫ 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中, 当信号处理函数结束返回后,再将这组信号从信号掩码中移除; 通过 sa_mask 参数进行设置
    ⚫ 除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号。

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

    1. #include
    2. int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    how: 参数 how 指定了调用函数时的一些行为。
    set: 将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为NULL,则表示无需对当前信号掩码作出改动。
    oldset: 如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
    返回值: 成功返回 0;失败将返回-1,并设置 errno。
    参数 how 可以设置为以下宏:
    ⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与 set 的并集。
    ⚫ SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
    ⚫ SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。
    使用示例
    将信号 SIGINT 添加到进程的信号掩码中:

    1. int ret;
    2. /* 定义信号集 */
    3. sigset_t sig_set;
    4. /* 将信号集初始化为空 */
    5. sigemptyset(&sig_set);
    6. /* 向信号集中添加 SIGINT 信号 */
    7. sigaddset(&sig_set, SIGINT);
    8. /* 向进程的信号掩码中添加信号 */
    9. ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);
    10. if (-1 == ret) {
    11. perror("sigprocmask error");
    12. exit(-1);
    13. }

    从信号掩码中移除 SIGINT 信号:

    1. int ret;
    2. /* 定义信号集 */
    3. sigset_t sig_set;
    4. /* 将信号集初始化为空 */
    5. sigemptyset(&sig_set);
    6. /* 向信号集中添加 SIGINT 信号 */
    7. sigaddset(&sig_set, SIGINT);
    8. /* 从信号掩码中移除信号 */
    9. ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);
    10. if (-1 == ret) {
    11. perror("sigprocmask error");
    12. exit(-1);
    13. }

    阻塞等待信号 sigsuspend()

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

    1. sigset_t new_set, old_set;
    2. /* 信号集初始化 */
    3. sigemptyset(&new_set);
    4. sigaddset(&new_set, SIGINT);
    5. /* 向信号掩码中添加信号 */
    6. if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set))
    7. exit(-1);
    8. /* 受保护的关键代码段 */
    9. ......
    10. /**********************/
    11. /* 恢复信号掩码 */
    12. if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL))
    13. exit(-1);
    14. pause();/* 等待信号唤醒 */

    执行受保护的关键代码时不希望被 SIGINT 信号打断,所以在执行关键代码之前将 SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。 最后调用了 pause()阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从 pause 返回继续执行;考虑到这样一种情况,如果信号的传递恰好发生在第二次调用 sigprocmask()之后、 pause()之前,如果确实发生了这种情况,就会产生一个问题,信号传递过来就会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到 pause()被阻塞,直到下一次信号发生时才会被唤醒,这有违代码的本意。虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能, 这必然是一个缺陷,要避免这个问题,需要将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作,这正是 sigsuspend()系统调用的目的所在, sigsuspend()函数原型如下所示:

    1. #include
    2. int sigsuspend(const sigset_t *mask);

    使用该函数需要包含头文件#include
    函数参数和返回值含义如下:
    mask: 参数 mask 指向一个信号集。
    返回值: sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR) ,表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT。
    sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起) 、并从信号处理函数返回,一旦从信号处理函数返回, sigsuspend()会将进程的信号掩码恢复成调用前的值。调用 sigsuspend()函数相当于以不可中断(原子操作)的方式执行以下操作:

    1. sigprocmask(SIG_SETMASK, &mask, &old_mask);
    2. pause();
    3. sigprocmask(SIG_SETMASK, &old_mask, NULL);

    实时信号

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

    sigpending()函数

    其函数原型如下所示:

    1. #include
    2. int sigpending(sigset_t *set);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    set: 处于等待状态的信号会存放在参数 set 所指向的信号集中。
    返回值: 成功返回 0;失败将返回-1,并设置 errno。
    使用示例
    判断 SIGINT 信号当前是否处于等待状态

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

    发送实时信号

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

    Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64,使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数。应用程序当中使用实时信号,需要有以下的两点要求:
    ⚫ 发送进程使用 sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。
    ⚫ 接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了。
    使用 sigqueue()函数发送实时信号,其函数原型如下所示:

    1. #include
    2. int sigqueue(pid_t pid, int sig, const union sigval value);

    使用该函数需要包含头文件
    函数参数和返回值含义如下:
    pid: 指定接收信号的进程对应的 pid,将信号发送给该进程。
    sig: 指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在。
    value: 参数 value 指定了信号的伴随数据, union sigval 数据类型。
    返回值: 成功将返回 0;失败将返回-1,并设置 errno。
    union sigval 数据类型(共用体) 如下所示:

    1. typedef union sigval
    2. {
    3. int sival_int;
    4. void *sival_ptr;
    5. } sigval_t;

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

    1. #include
    2. #include
    3. #include
    4. int main(int argc, char *argv[])
    5. {
    6. union sigval sig_val;
    7. int pid;
    8. int sig;
    9. /* 判断传参个数 */
    10. if (3 > argc)
    11. exit(-1);
    12. /* 获取用户传递的参数 */
    13. pid = atoi(argv[1]);
    14. sig = atoi(argv[2]);
    15. printf("pid: %d\nsignal: %d\n", pid, sig);
    16. /* 发送信号 */
    17. sig_val.sival_int = 10; //伴随数据
    18. if (-1 == sigqueue(pid, sig, sig_val)) {
    19. perror("sigqueue error");
    20. exit(-1);
    21. }
    22. puts("信号发送成功!");
    23. exit(0);
    24. }

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

    1. #include
    2. #include
    3. #include
    4. #include
    5. static void sig_handler(int sig, siginfo_t *info, void *context)
    6. {
    7. sigval_t sig_val = info->si_value;
    8. printf("接收到实时信号: %d\n", sig);
    9. printf("伴随数据为: %d\n", sig_val.sival_int);
    10. }
    11. int main(int argc, char *argv[])
    12. {
    13. struct sigaction sig = {0};
    14. int num;
    15. /* 判断传参个数 */
    16. if (2 > argc)
    17. exit(-1);
    18. /* 获取用户传递的参数 */
    19. num = atoi(argv[1]);
    20. /* 为实时信号绑定处理函数 */
    21. sig.sa_sigaction = sig_handler;
    22. sig.sa_flags = SA_SIGINFO;
    23. if (-1 == sigaction(num, &sig, NULL)) {
    24. perror("sigaction error");
    25. exit(-1);
    26. }
    27. /* 死循环 */
    28. for ( ; ; )
    29. sleep(1);
    30. exit(0);
    31. }

    异常退出 abort()函数

    前面大家介绍了应用程序中结束进程的几种方法,譬如使用 exit()、 _exit()或_Exit()这些函数来终止进程,然后这些方法使用于正常退出应用程序,而对于异常退出程序,则一般使用 abort()库函数,使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态。
    abort()函数原型如下所示:

    1. #include
    2. void abort(void);

    函数 abort()通常产生 SIGABRT 信号来终止调用该函数的进程, SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。即使我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止,无论阻塞或忽略 SIGABRT 信号, abort()调用均不受到影响,总会成功终止进程。

  • 相关阅读:
    Lingo求解规划优化问题
    Unity中AssetBundle的变体(Varient)使用教程
    HTML5期末考核大作业,网站——旅游景点。 学生旅行 游玩 主题住宿网页
    力扣(518.377)补7.31
    解决WebStorm中不显示npm任务面板
    黑猫带你学Makefile第5篇:Makefile中函数的使用
    1100 Mars Numbers
    Day814.电商系统表设计优化案例分析 -Java 性能调优实战
    秒杀系统设计
    C# 使用Parallel去执行并行下载
  • 原文地址:https://blog.csdn.net/weixin_41252596/article/details/139092889