22 信号高级特性
核心转储
核心转储的产生
有非常多的原因会导致核心转储不发生,最容易出现的是没有用 ulimit
对进程设置核心转储大小上限(默认是 0)。此外,对核心转储路径没有写权限、set-group-ID 或 set-user-ID 程序由非属组 / 主执行、对可执行文件没有读权限等原因都会导致核心转储不发生。
在 Linux 2.4 之后,prctl
函数的 PR_SET_DUMPABLE
操作可以为进程设置 dumpable 标志,如果有这个标志,set-group-ID 或 set-user-ID 程序在非属组 / 主执行时也能核心转储。Linux 2.6.13 之后,/proc/sys/fs/suid_dumpable 为此标志提供系统级的控制,默认值是 0。
Linux 2.6.23 之后,/proc/PID/coredump_filter 能控制对应进程核心转储中可以包含的内存类型。文件的值是 4 位掩码,表示私有匿名映射、私有文件映射、共享匿名映射以及共享文件映射是否受允许,默认值是仅允许匿名映射、不允许文件映射。如今,我的 WSL 内核版本是 5.15.153.1-microsoft-standard-WSL2,该文件中存储的是 00000033
。
核心转储的路径
可以用 /proc/sys/kernel/core_pattern 查看核心转储文件的存储路径。
系统对一些特殊信号的处理
SIGKILL 和 SIGSTOP 的行为无法被改变,调用 signal
或 sigaction
以图改变其行为总是会返回错误。
SIGCONT 总是会让处于停止状态的进程恢复运行,即使该进程已经阻塞了 SIGCONT 信号。阻塞 SIGCONT 只是会让 SIGCONT 的处理器函数暂时无法执行而已。另外,如果进程收到了 SIGCONT,那么处于等待状态的停止信号(SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 等)会被丢弃,这是为了防止进程刚被唤醒就再次停止。
如果程序发现终端相关的信号处理方式被设置为 SIG_IGN,则一般不应该去更改其设置。这不是硬性规定,是建议遵守的协定。
不可中断的睡眠
睡眠的状态分为以下几种:
- TASK_INTERRUPTIBLE:对应 ps(1) 中 STAT 字段的字母 S。例子:正在等待终端输入,等待数据写入当前的空管道,或者等待 System V 信号量值的增加。如果进程接受到信号,那么进程被唤醒,系统调用会中断(是否会进行重试要看用户的调用方式,见 系统调用的中断和重启)。
- TASK_UNINTERRUPTIBLE:对应 STAT 的字母 D。一般和硬件有关,比如磁盘 I/O。极少数情况下进程会因为硬件故障而一直挂起,此时 SIGKILL 也无法杀死进程。
- TASK_KILLABLE:内核 2.6.25 开始,此信号和 TASK_UNINTERRUPTIBLE 类似,区别是在收到 SIGKILL 时会唤醒进程。首个使用 TASK_KILLABLE 改造的内核模块是 NFS。
硬件产生的信号
硬件产生的信号如果忽略、阻塞,或者从处理器函数返回,则行为未定义。
信号的同步生成和异步生成
信号一般是异步产生的,但是如果进程因为执行了某条指令而产生了硬件异常,或者通过 raise()
、kill()
、killpg()
向自身发送信号,则信号会立即发送(除非信号被阻塞)。对前面提到的几个系统调用来说,“立即发送信号”指的是在函数返回之前信号就已经发出。
信号传递的时机和顺序 ‼️
如果是同步产生的信号,那么信号会立即传递。如果是异步信号,那么信号的传递发生在进程从内核态到用户态的下一次切换时。举例:
- 根据时间片轮转算法再次获得时间片,刚被调度时。
- 一个系统调用完成后。
(用 sigprocmask
函数)解除对多个信号的阻塞时,等待的信号会立即传递。SUSv3 中规定:标准信号的传递顺序由系统实现决定,而 Linux 在成书之时是按照信号序号从低到高的顺序传递的;实时信号的传递顺序则必须得到保障。
当多个解除了阻塞的信号正在等待传递时,如果在信号处理器函数执行期间发生了内核态和用户态之间的切换,那么将中断此处理器函数的执行,转而去调用第二个信号处理器函数。这说明“内核态到用户态切换会触发异步信号的处理”这一点在信号处理器函数运行时也是如此。如图:
Note
要注意信号处理器函数是在用户态调用的,所以一个信号处理器在运行过程中还是可能发生内核态和用户态的切换。
signal()
的实现及可移植性
早期的 signal()
实现是不可靠的。比如:
- 在进入信号处理器的时候会将该信号的处理设置为默认行为(相当于
SA_RESETHAND
),可以防止信号再次进入,但是默认行为可能不是开发者想要的。 - 在信号处理器执行期间,不会对新的信号阻塞(相当于
SA_NODEFER
),如果同类信号再次光顾,信号处理器可能发生递归调用。 - 没有提供重启(
SA_RESTART
)功能。
4.2 BSD 对可靠信号的实现纠正了这些限制,然而早期语义仍然存在于 System V 的很多实现中。Linux 内核的 signal
函数是系统调用,提供老的、不可靠的语义。Glibc 中的 signal
是用 sigaction
重新实现的,如果想要坚持使用不可靠的语义,可以使用 sysv_signal
函数。
若编译程序时并未定义 _BSD_SOURCE
特性测试宏,则 glibc 会隐式将所有 signal()
调用重新定义为 sysv_signal()
调用,亦即启用 signal()
的不可靠语义。_BSD_SOURCE
是会被默认定义的,除非有其他特性测试宏被定义,且会和 _BSD_SOURCE
冲突。
鉴于诸多问题,处理信号时应该选择 sigaction
函数,功能多、可移植性强。
实时信号
实时信号的介绍
实时信号的好处:
- 范围扩大,可以给程序自定义的信号变多了。标准信号中,只有两个(SIGUSR1 和 SIGUSR2)可以给用户自定义。
- 队列化管理。如果一个实时信号的多个实例被发送给同一个进程,那么即便进程来不及处理,也不会把多个信号合并成一个,而是将信号入队。
- 发送实时信号可以传递伴随数据:一个整数或者指针。这样一来实时信号可以比标准信号携带更多信息。
- 不同实时信号的优先级不同。序号小的实时信号始终更加优先,同一序号的各个信号(和它们的伴随数据)是 FIFO。(如果不考虑伴随数据的话,FIFO 这个概念也就没有意义了。)
SUSv3 要求,实现所提供的各种实时信号不得少于
_POSIX_RTSIG_MAX
(定义为 8)个。 Linux 内核则定义了 32 个不同的实时信号,编号范围为 32~63。<signal.h> 头文件所定义的RTSIG_MAX
常量则表征实时信号的可用数量,而此外所定义的常量SIGRTMIN
和SIGRTMAX
则分别表示可用实时信号编号的最小值和最大值。
使用 LinuxThreads 线程实现的系统占用了前三个实时信号,采用 NPTL 线程实现的系统占用了前两个实时信号,因此它们都将
SIGRTMIN
变量的值增加了占用实时信号的数量。
关于 LinuxThreads 和 NPTL 两种线程实现可以参考文章 https://www.cnblogs.com/kaleidoscope/p/9626458.html 。后者(Native POSIX Thread Library)是比较新的。
在我的 WSL Debian 中看,SIGRTMIN
是 34,应该是采用了 NPTL 线程实现。SIGRTMIN
(像 errno
一样)也被定义成对一个函数的调用:#define SIGRTMIN (__libc_current_sigrtmin ())
。用函数实现大概是方便链接到不同线程库时有不同的行为。由于没有定义为整数常量,对于它们的检查不能使用预处理器,只能在运行时完成。
Caution
和标准信号不同,使用实时信号不能对下标写死,要用 SIGRTMIN + x
的形式。
实时信号排队的数量是有限的,可以用 sysconf(_SC_SIGQUEUE_MAX)
来获取实时信号队列的最大长度。
正确使用实时信号
- 发送进程需要使用
sigqueue()
系统调用来发送实时信号及伴随数据。用其他信号发送函数也能发送实时信号,但是不保证排队。 - 接受者要对实时信号注册处理器函数,而且在调用
sigaction()
时需要带有SA_SIGINFO
标志。如果没有这个标志,SUSv3 标准中不保证对信号排队(依赖具体实现会对移植性产生影响)。
sigqueue
函数和 kill
函数比较相似,有一点区别是:sigqueue
只能指定具体的 pid,不能指定其为 0 或负值。如果进程的该信号已经排队满了,则系统调用失败,并返回 EAGAIN
。
处理实时信号
可以像标准信号一样用单参数处理器来处理实时信号,也可以通过带 3 个参数的信号处理器函数来处理实时信号。
其他信号处理方式
这里提到的信号处理方式基本都是要和 sigprocmask
结合使用的。
用 pause
或者 sigsuspend
挂起进程并等待信号
这一节的重点是 sigsuspend
,但是为了理解它的重要性,需要先讲一下 pause
:用 pause
系统调用可以挂起进程,直到进程接受到可以终止进程、或者会被信号处理器函数捕获的信号。在后一种情况下,进程会被唤醒并进入信号处理器函数,从信号处理器函数返回之后会从 pause
中返回。其实阻塞过程也是一种同步吧,只是因为这种等待方式用到了信号处理器函数,还是要考虑异步信号安全问题。
Tip
这里的 pause
系统调用是 UNIX 有的,和 Windows 上的 system("pause")
用途不同。
如果我们要等待特定的信号,更好的方式是 sigsuspend
系统调用。
SYNOPSIS
#include <signal.h>
int sigsuspend(const sigset_t *mask);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
sigsuspend():
_POSIX_C_SOURCE
sigsuspend
系统调用和 pause
很像,也是阻塞直到收到让进程终止或者是被信号处理器函数捕获的信号。区别是它同时进行两个操作:它在阻塞进程 / 线程之前短暂地将信号掩码设置为给定的参数,然后开始阻塞,如果从阻塞中恢复就将信号掩码恢复原状。这些操作放在同一个系统调用里面实现保证了原子性。它相当于使用不可中断的方式实现了以下函数:
为什么非得要提供 sigsuspend
实现的原子操作?设想先用 sigprocmask
来屏蔽掉某些信号,然后执行临界区的代码。然后使用 sigprocmask
将之前的信号恢复,再 pause
等待之前被屏蔽的信号。如果在两次 sigprocmask
之间有被屏蔽信号已经到达,则会在第二次 sigprocmask
解除屏蔽之后立即处理,等到执行 pause
的时候,信号就处理完了。如果进程不再收到信号,则进程将永远挂起!而如果使用 sigsuspend
(相当于第二个 sigprocmask
和 pause
结合在一起),则可以保证如果有对应信号在等待的情况下,处理后不再挂起程序。
Note
注意 sigsuspend
的参数的含义,只有掩码中的信号放行,其他信号被屏蔽。
用 sigwaitinfo
以同步方式等待信号
Tip
sigwaitinfo
和 sigsuspend
都是要配合 sigprocmask
来用的。
#include <signal.h>
int sigwaitinfo(const sigset_t *restrict set,
siginfo_t *_Nullable restrict info);
int sigtimedwait(const sigset_t *restrict set,
siginfo_t *_Nullable restrict info,
const struct timespec *restrict timeout);
该系统调用可以以同步的方式来等待信号。先是阻塞程序,然后等待信号。等待得到信号之后,不会触发信号处理器函数,而是将信号信息以返回值和输出参数的形式提供给调用线程。这样可以免去编写信号处理器函数的负担,而且处理速度也比异步信号要快。siginfo_t *
类型的参数是为了支持实时信号的伴随信息而设计的。
Caution
SUSv3 规定,如果在调用 sigwaitinfo
之前对于其参数 set
中的信号没有阻塞,行为未定义。要在 sigwaitinfo
调用前阻塞这些信号,是为了屏蔽它们的默认处置方式。
sigtimedwait
在 man 手册中有个特殊说明:如果 timeout
参数中的两个字段都被设置成了 0,那么相当于轮询中的单次检查(poll
),不会有阻塞的过程。如果 timeout
参数这个指针值为空,有的 UNIX 实现会将其视为一次 poll 并立即返回,有些 UNIX 会将其看作等同于 sigwaitinfo
,标准对此没有规定。
通过文件描述符来获取信号
始于内核 2.6.22,Linux 提供了(非标准的)signalfd()
系统调用。利用该机制可以创建一个文件描述符,信号可以从其中读取,这样也实现了同步接受信号(没有信号处理器函数在交替执行就是同步的)。为了此系统调用正常工作,需要将要读取的信号先用 sigprocmask
屏蔽。
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
如果 fd
为 -1,则会创建一个新的文件描述符;如果 fd
不为 -1,则需要是之前已经通过 signalfd
创建的文件描述符,这种情况下可以对之前的设置做出一些修改。早期的 flags
参数必须是 0,而 Linux 从版本 2.6.27 开始支持 SFD_CLOEXEC
和 SFD_NONBLOCK
两个参数。
读取信号的过程这里就省略了:
看起来和 inotify
很像,也是从文件描述符中读取字节流信息。由于使用了文件描述符,可以用 select
/ poll
/ epoll
对其进行监控。
Tip
signalfd
和 sigwaitinfo
都是同步处理,但是前者不必立即处理,后者则是调用时立即处理。如果没有指定 SFD_NONBLOCK
标志,我想 signalfd
也是能够起到阻塞效果的(不过 read
系统调用可能被中断,需要处理一下重启)。