Linux 信号处理

信号被传递给进程,用于软件中断。操作系统使用信号将异常情况报告给运行中的程序。某些信号用于报告错误,例如:引用了无效的内存地址。而另一些信号则用于报告异步事件,例如:网线断开了。

GNU C 库定义了各种信号类型,每种信号类型都用于特定的事件。

某些事件会使程序无法照常运行,因为,相应的信号会中止程序。而报告无害事件的信号,在默认情况下,是被忽略的。

如果某个事件一定会引发特定类型的信号,那么,我们可以定义一个 handler 函数,并告知操作系统,在特定类型的信号到达时,运行该 handler 函数。

一个进程可以发送信号给另一个进程。这就代表:允许父进程终止子进程、允许两个相关进程进行通信和同步。

信号的基本概念

信号的种类

产生一个信号代表了一件异常事件的发送。以下这些事件都是会引发(产生、发送)信号。

  • 程序错误。例如:除零、内存地址越界。
  • 用户请求中断、终止程序。大部分环境都允许让用户通过键入 C-z 来暂停程序,或键入 C-c 终止程序。
  • 子进程终止。
  • 计时器、警报器到期。
  • 来自当前进程 kill、raise 调用。
  • 来自其他进程 kill 调用。
  • 尝试执行一个无法完成的 I/O 操作。例如:从一个没有数据写入的 pipe 中读取数据并写入到终端。

以上事件(除去明确要调用 killraise 的事件)都会产生其特定的信号。

信号的产生

通常,产生信号的事件可以分为三大类:错误、外部事件、显式请求。

  • 错误:代表程序完成了某些无效的事情,无法继续执行。

    • 不过,也并非所有类型的错误都会产生信号(实际上,大部分错误都不会产生信号)。

      • 例如:打开一个不存在的文件,这个错误仅仅只是 open 函数返回 -1 值,而不会产生任何信号。
    • 通常,发生库函数相关的错误时,都会返回一个值(代表发生了某一类错误事件)。
    • 会产生信号的错误,在程序中的任何地方都可能存在。

      • 例如:除零、无效的内存地址。
  • 外部事件:通常与 I/O 或其他进程有关。

    • 例如:输入到达、计时器到期以及子进程终止。
  • 显式请求:使用了 kill 之类的库函数,其目的就是为了产生信号。

信号可以同步、也可以异步生成。

  • 同步信号:与程序中的特定动作有关,并且在该动作执行期间被传递(除非信号阻塞了)。

    • 大多数错误都是同步生成信号。
    • 进程对自身显式请求所生成的信号也是同步信号。
    • 在某些机器上,某些类型的硬件错误(通常是浮点异常)并不完全同步,一些说明可能会有所延迟。
  • 异步信号:由接收该信号的进程,在其控制范围之外的事件生成的。

    • 在进程运行期间,这些信号的到达时间是随机的。
    • 外部事件异步生成信号,相当于是对其他进程进行显式请求产生信号。

任何类型的信号,既可能同步、也可能异步生成。例如:错误产生的信号通常是同步生成。但是,任何类型的信号都可以通过显式请求同步、异步生成。

信号的传递

信号在生成时,默认为 pending 状态,然后传递给目标进程。

如果信号被阻塞了,那么该信号会一直保持为 pending 状态直到阻塞被解除。

在信号传递成功以后,目标进程将对该信号采取指定的动作。

  • 对于某些信号,例如:SIGKILLSIGSTOP,采取的是固定动作。
  • 对于大多数信号,程序可选的动作有:忽略信号、执行指定 handler 函数、执行该类信号的默认动作。

    • 忽略信号:丢弃已生成的(包括阻塞中的)同一类信号。
    • 执行该类信号的默认动作:未选择要采取的动作时,执行该类信号的默认动作。(对于大多数信号,默认动作是结束进程,对于表示“无害”事件的某些信号,则默认动作不执行任何操作)。
  • 程序通过 singalsigaction 函数选择需要采取的动作。
  • 当程序正在为信号执行动作时,后续的同类信号将被阻塞。

当一个进程被信号终止时,其父进程可以通过检查 wait、waitpid 函数返回的退出状态码,确定程序的退出是由哪一类信号造成的。如果 shell 程序被信号终止,那么 shell 通常会输出一些错误信息。

通常,程序错误的信号都有一种特殊的属性:当此类信号在终止进程时,还会向 core dump 文件写入该进程在终止时的状态。而后,可以使用调试器检查 core dump 文件,以便排查错误的原因。

通过显式请求产生的程序错误信号,不仅能导致进程终止,同时生成 core dump 文件内容。

标准信号

本节列出了各种标准信号的名称,并描述了这些信号意味着什么事件。

每个信号名称都是一个宏,代表一个正整数,即该信号的信号编号。

在程序中,请勿直接使用信号编号进行编程判断,而应该始终是引用信号名称。这是因为,信号编号可能因系统而异,但是信号名称的含义是统一标准化的。

信号名称在头文件 signal.h 中定义。

Macro: int NSIG

  • 信号总数。
  • 由于信号编号是连续分配的,因此 NSIG = 最大信号编号 + 1。

程序错误信号

当操作系统或计算机本身检测到严重的程序错误时,将产生此类信号。

此类信号的默认动作是终止进程。

大部分程序会为此类信号选择执行 handler 函数,其目的是为了让程序在终止前进行整理。不过,需要注意的是,在 handler 函数体内,需要先将此类信号设置为执行默认动作(即终止),然后再编写具体逻辑,最后在 handler 函数尾部主动引发该类信号。这样就确保,程序执行了 handler 函数,然后终止。

由于此类信号的默认动作是终止进程。如果主动阻塞、忽略此类信号,或者把它们当成正常信号去处理,那么当此类信号发生时,程序可能会严重崩溃。除非,这些信号是通 raise、kill 生成的,而不是由程序错误引发的。

此类信号在终止进程时,同时会向核心转储文件写入进程被终止前的状态。核心转储文件名默认为 core,并且被保存在该进程运行时的任何目录中。(在 GNU/Hurd 系统上,可以使用环境变量 COREFILE 为核心转储文件重命名。)可以通过调试器检查 core 文件,以确认程序错误的具体原因。

Macro: int SIGFPE

  • 全写:floating-point exception。
  • 虽然,信号名称的含义是浮点异常,但是,该信号实际上代表了所有的算术错误,包括:除零和溢出。
  • 如果程序将整数用于浮点运算,通常会导致 invalid operation 异常,因为处理器无法将数据识别为浮点数。(实际上,浮点异常是一个非常复杂的主题,因为有许多类型的异常含义各有不同,SIGFPE 信号无法区分这些类型。)

Macro: int SIGILL

  • 全写:illegal instruction。
  • 表示程序正在尝试执行垃圾回收、特权指令。
  • 由于 C 编译器只会生成有效指令,因此产生 SIGILL 也就说明可执行文件已损坏、尝试执行数据。
  • 当堆栈溢出、系统无法运行信号 handler 函数时,也会产生 SIGILL。

Macro: int SIGSEGV

  • 全写:segmentation violation。
  • 当程序尝试读写未分配的内存、写入只读内存时,将产生此信号。

Macro: int SIGBUS

  • 全写:bus error。
  • 当取消引用无效的指针时,将引发此信号。

Macro: int SIGABRT

  • 全写:abort。
  • 表示程序本身检测到错误,并通过调用 abort 函数报告错误。

Macro: int SIGIOT

  • 由 PDP-11 iot 指令生成。在大多数计算机上,这只是 SIGABRT 的别名。

Macro: int SIGTRAP

  • 全写:trap。
  • 由计算机的断点指令以及其他可能的 trap 指令生成。
  • 该信号由调试器使用。
  • 如果程序以某种方式执行了错误的指令,则可能只会看到 SIGTRAP。

Macro: int SIGEMT

  • 全写:emulator trap。
  • 由于某些未实现的指令(可能会在软件中进行仿真)或操作系统未能正确地对其进行仿真而导致的。

Macro: int SIGSYS

  • 全写:system call。
  • 系统调用错误。

终止信号 - Termination Signals

此类信号用于告诉进程以某种方式终止。

通常,处理此类信号,是为了让程序在真正终止之前前进行适当的整理。例如:处理完正在处理的数据、删除临时生成的文件等。

此类信号的默认动作都是终止进程。

Macro: int SIGTERM

  • 终止程序的通用信号。
  • 此信号可以被阻塞、被处理、被忽略,常用于:平滑退出进程。
  • 在默认情况下,shell kill 命令产生 SIGTERM 信号。

Macro: int SIGINT

  • 全写:program interrupt。
  • 当用户键入 INTR 特殊字符(通常为 C-c,即 ctrl+c)时,将发送 SIGINT 信号。

Macro: int SIGQUIT

  • SIGQUIT 与 SIGINT 相似,不同之处在于 SIGQUIT 是由 QUIT 特殊字符(通常为 C-)控制的,并在进程终止时产生 core dump(就像程序错误信号一样)。
  • 在用户发现了程序错误是,可以通过发送 SIGQUIT 信号主动让程序终止。

Macro: int SIGKILL

  • 立即终止程序。
  • 此信号不能被处理、忽略、阻塞,是一个致命的信号。
  • 只能显式请求生成。由于无法被处理,因此,在确定必须发送 SIGKILL 信号之前,最好先尝试其他终止信号(例如:C-c、SIGTERM)。
  • 如果某个进程不响应其他终止信号,则向其发送 SIGKILL 信号强制使其终止。

Macro: int SIGHUP

  • 全写 hang-up。
  • 用于报告用户终端已断开连接,这可能是因为网络连接断开了。
  • 此信号可以有效断开会话中的所有进程以及控制终端的连接。

警报信号 - Alarm Signals

警报信号用于表明计时器时间到期。

警报信号的默认行为是终止程序。不过,这个默认行为很少会使用到。通常都需要定义 handler 函数。

Macro: int SIGALRM

  • 表示计时器已到期,该计时器测量实际或时钟时间。

Macro: int SIGALRM

  • 全写:virtual time alarm。
  • 表示计时器已到期,该计时器测量当前进程使用的 CPU 时间。

Macro: int SIGPROF

  • 全写:profiling。
  • 表示计时器已到期,该计时器既测量当前进程使用的 CPU 时间,也可以测量系统代表该进程使用的 CPU 时间。
  • 这样的计时器主要用于代码分析,因此信号名称的全写是 profiling。

异步 I/O 信号 - Asynchronous I/O Signals

异步 I/O 信号必须与异步 I/O 设备一起使用。

必须显示调用 fcntl 函数,使特定的文件描述符能够生成异步 I/O 信号。

异步 I/O 信号的默认动作是忽略。

Macro: int SIGIO

  • 当文件描述符准备执行输入、输出时,发送此信号。
  • 在大多数操作系统上,终端和套接字是唯一可以生成 SIGIO 的文件。其他类型的文件(包括普通文件),即便是主动要求,它们也不会生成 SIGIO。
  • 在 GNU 系统上,如果能够成功使用 fcntl 设置异步模式,则 SIGIO 将会始终正确生成。

Macro: int SIGURG

  • 全写 urgent。
  • 当紧急或外部数据到达套接字时,发送此信号。

Macro: int SIGPOLL

  • 这是 System V 信号名称,与 SIGIO 差不多。为兼容而定义。

作业控制信号 - Job Control Signals

除非真正了解作业控制的工作原理,否则一般情况下,是不用理会作业控制信号。

Macro: int SIGCHLD

  • 每当子进程之一终止、暂停时,都会将此信号发送到父进程。
  • 此信号的默认动作是忽略信号。

Macro: int SIGCLD

  • SIGCHLD 弃用的名称。

Macro: int SIGCONT

  • 将 SIGCONT 信号发送给进程使其继续运行。
  • 此信号比较特殊 - 如果进程是暂停的状态,那么此信号会先让进程恢复继续运行,然后再传递。
  • 默认行为是什么也不做。
  • 无法阻塞此信号。
  • 即使给 SIGCONT 设置 handler 函数,其仍然会使进程恢复继续运行。

Macro: int SIGSTOP

  • 暂停进程。
  • 无法被处理、忽略、阻塞。

Macro: int SIGTSTP

  • 交互式暂停进程。
  • 此信号可以被处理、忽略。
  • 当用户键入 SUSP 特殊字符(通常为 C-z)时,将生成此信号。

Macro: int SIGTTIN

  • 进程在作为后台作业运行时无法从用户终端读取。当后台作业中的任何进程尝试从终端读取时,作业中的所有进程都会发送 SIGTTIN 信号。
  • 此信号的默认操作是中止进程。

Macro: int SIGTTOU

  • 类似 SIGTTIN,但是在后台作业中的进程尝试写入终端、设置终端模式时生成。
  • 默认操作是暂停进程。
  • 仅当设置了 TOSTOP 输出模式时,才会生成 SIGTTOU,以尝试写入终端;否则,将不进行任何操作。

操作错误信号 - Operation Error Signals

用于报告程序执行的操作所产生的各种错误。

这类信号不一定表示程序中有编程错误,而是表示阻止操作系统调用完成的错误。

此类信号的默认动作:终止进程。

Macro: int SIGPIPE

  • pipe 破损。
  • 如果使用 pipe、FIFO,则必须设计应用程序,以便一个进程打开 pipe 进行读取,然后另一个进程开始写入 pipe。如果读取过程从未开始、意外终止,则写入 pipe、FIFO 会引发 SIGPIPE 信号。如果 SIGPIPE 被阻塞、处理、忽略,则有问题的调用将失败,而是使用 EPIPE。

Macro: int SIGLOST

  • 资源丢失。
  • 如果在 NFS 文件上存在 advisory lock 时,会生成此信号,并且 NFS 服务器将重启并忽略 advisory lock。
  • 在 GNU/Hurd 系统上,当任何服务器程序意外终止时,都会生成 SIGLOST。通常可以忽略此信号。

Macro: int SIGXCPU

  • 超过了 CPU 时间限制。当进程超过其对 CPU 时间的软资源限制时,将生成此信号。

Macro: int SIGXFSZ

  • 超出文件大小限制。当进程尝试扩展文件时,会产生此信号,因此该信号超出了进程对文件大小的软资源限制。

其他信号 - Miscellaneous Signals

此类信号用于实现自定义的功能。

除非为此类信号指定了 handler 函数,否则,它们不会对程序产生任何影响。

Macro: int SIGUSR1
Macro: int SIGUSR2

  • 保留信号,供用户实现自定义的功能。

Macro: int SIGWINCH

  • Window size change
  • 窗口大小发生改变了。
  • 默认动作:忽略。

Macro: int SIGINFO

  • 信息请求。在 4.4 BSD、GNU、Hurd 系统上,当用户以规范模式键入 STATUS 特殊字符时,此信号将发送到控制终端的前台进程组中的所有进程。

参考资料

添加评论

验证码: