收发信号思想是 Linux 程序设计特性之一,一个信号可以认为是一种软中断,通过用来向进程通知异步事件。
本文讲述的 信号处理内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解信号编程。
遵循 POSIX.1 - 2008
标准 c 库,libc, -lc
#include
int sigaction(int signum,
const struct sigaction *_Nullable restrict act,
struct sigaction *_Nullable restrict oldact);
依赖特性测试宏:
sigaction():
_POSIX_C_SOURCE
siginfo_t:
_POSIX_C_SOURCE >= 199309L
sigaction() 系统调用用来改变进程对指定信号采取的行动。(参考 signal(7) 来总体看下各种信号。)
signum 指定了信号对应的编号,这个值可以是 SIGKILL 和 SIGSTOP 以外任何值。
如果 act 是非 NULL,那就就会从 act 给 signum 指定的信号安装新的 action。如果 oldact 是非 NULL,那么之前的 action 会被保存在 oldact 中。
sigaction 结构体定义类似:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
在一些架构上,会包含一个 union 共同体,不能给 sa_handler 和 sa_sigacton 同时赋值。
sa_restorer 字段不是给应用程序用的。(POSIX 并没有指定这个字段。) 该字段设计意图的详细信息可以参考 sigreturn(2)。
sa_handler 指定了和 signum 关联的行为,它可以是以下三个之一:
如果 SA_SIGINFO 在 sa_flags 中指定了,那么 sa_sigaction (取代 sa_handler)指定了 signum 信号的处理函数。这个函数接收 3 个参数,参数描述如下。
sa_mask 指定了在信号处理函数执行过程中哪些信号将会被屏蔽(即,加入到信号处理函数调用线程的信号屏蔽字里面)。另外,出发信号处理函数的信号本身也会被屏蔽,除非使用了 SA_NODEFER 标记。
sa_flags 指定了一些列能够改变信号行为的标记,它是下面各个标记的位或值:
SA_NOCLDSTOP
如果信号是 SIGCHLD,那么当子进程处理停止(SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU 中的一个)或者恢复(SIGCONT)(参考 wait(2))信号时,不会接收通知。
SA_NOCLDWAIT(Linux 2.6 后)
如果 signum 是 SIGCHLD,那么在其结束(terminate)时不会在子进程结束时将其转换为僵尸进程。参考 waitpid(2)。这个标记只对建立 SIGCHLD 信号处理函数或者将其设置为 SIG_DFL 时有意义。
如果建立 SIGCHLD 信号处理函数时指定了 SA_NOCLDWAIT 标记,POSIX 并没有指定子进程结束时是否产生 SIGCHLD 信号。Linux 上 SIGCHLD 会产生,而其他实现上不会。
SA_NODEFER
在信号处理函数执行时,不把该信号添加到线程的信号屏蔽字里,除非这个信号由 act.sa_mask 指定了。这样,新信号实例在信号处理函数执行时仍然可以分发到该线程。这个信号只对建立一个信号处理函数有意义。
SA_NOMASK 过时了,它是这个标记的非标准同义词。
SA_ONSTACK
在可选的信号栈上调用信号处理函数,这个可选的信号栈由 sigaltstack(2) 提供。如果可选信号栈不可用,那么会使用默认的栈。这个标记也是只对建立信号处理函数有意义。
SA_RESETHAND
在进入信号处理函数入口时,将信号 action 恢复为默认值。这个标记只对建立信号处理函数有意义。
SA_ONESHOT 过时了,它是这个标记的非标准同义词。
SA_RESTORER
不是给应用程序用的。这个标记是 C 库用来指示 sa_restorer 字段包含了“信号床”地址。参考 sigreturn(2) 获取更多信息。
SA_SIGINFO(Linux 2.2 后)
信号处理函数携带 3 个参数,而不是一个。这种情况下,sa_sigaction 会代替 sa_handler。这个标记只有在建立信号处理函数时才有意义。
SA_UNSUPPORTED(Linux 5.11 后)
用来动态探测标记位支持。
如果这个标记和其他内核可能不支持的标记一起在 act->sa_flags 里成功注册了,后面马上调用了 sigaction() 并指定相同的信号值(非空 oldact,oldact->sa_flags 字段的 SA_UNSUPPORTED 位没设置),那么oldact->sa_flags 可以充当掩码,描述那么可能不支持但是实际上支持的标记。参考下面的“动态标记支持检测”。
SA_EXPOSE_TAGBITS(Linux 5.11 后)
正常情况下,在分发一个信号时,siginfo_t 中 si_addr 字段中架构相关的标记位会被清零。如果设置了这个标记位,那么就就保留这些位。
SA_SIGINFO 的 siginfo_t 参数
当 SA_SIGINFO 标记在 act.sa_flags 里指定了,那么信号处理函数的地址会在 act.sa_sigaction 字段中设置。这个函数携带三个参数如下:
void
handler(int sig, siginfo_t *info, void *ucontext)
{
...
}
这三个参数描述如下:
sig 导致该处理函数调用的信号编号
info 指向 siginfo_t 指针,这个结构包含关于信号的描述信息,后面会提到
ucontext
这个指针指向 ucontext_t 结构,转换成了 void *。这个结构包含了信号上下文信息,这些信息是由内核保存到用户空间栈上的。可以参考 sigreturn(2) 获取更多信息。关于 ucontext_t 结构更多信息可以参考 getcontext(2) 和 signal(7)。通常情况下,处理函数不会用到第三个参数。
siginfo_t 数据类型是一个结构体类型,如果所示:
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
union sigval si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_lower; /* Lower bound when address violation
occurred (since Linux 3.19) */
void *si_upper; /* Upper bound when address violation
occurred (since Linux 3.19) */
int si_pkey; /* Protection key on PTE that caused
fault (since Linux 4.6) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
si_signo、si_errno 和 si_code 适用于所有信号(Linux 上通常不用 si_errno)。剩下的部分可以是共用体,每个信号只读对其有意义的字段:
si_code 字段
siginfo_t 参数会传递给 SA_SIGINFO 信号处理函数,里面的 si_code 字段是一个指示信号发送原因的数值(不是位掩码)。对于 ptrace(2) 事件,si_code 会包含 SIGTRAP 并在高字节里包含 ptrace 事件:
(SIGTRAP | PTRACE_EVENT_foo << 8).
对于非 ptrace(2) 事件,si_code 中包含的数值会在后面章节描述。glibc 2.20 以后,这些符号的大多数定义都可以在
对于 TRAP_* 常量,只有在前两种宏定义下会定义相关的符号。在 glibc 2.20 之前,就不需要定义这些宏就可以获得这些符号。
对于常规信号,下面表列出了可以填充到任何信号的 si_code 中的值,和发生信号原因一起:
SI_USER
kill(2)
SI_KERNEL
由内核发送
SI_QUEUE
sigqueque(3)
SI_TIMER
POSIX 定时器超时
SI_MESGQ(Linux 2.6.6 之后)
POSIX 消息队列状态发生变化,参考 mq_notify(3)
SI_ASYNCIO
AIO 完成
SI_SIGIO
队列 SIGIO( Linux 2.2 之前使用,Linux 2.4 后 SIGIO/SIPOLL 像后面描述那样填充 si_code 字段)
SI_TKILL(Linux 2.4.19 后)
tkill(2) 或者 tgkill(2)
对于 SIGILL 信号,下面数值可以填充到 si_code 中:
ILL_ILLOPC
非法操作码
ILL_ILLOPN
非法操作
ILL_ILLADR
非法寻址模式
ILL_ILLTRP
非法陷入
ILL_PRVOPC
特权操作码
ILL_PRVREG
特权寄存器
ILL_COPROC
协处理器错误
ILL_BADSTK
内部栈错误
下面是 SIGFPE 可以填充到 si_code 的值:
FPE_INTDIV
整数除零
FPE_INTOVF
整型溢出
FPE_FLTDIV
浮点数除零
FPE_FLTOVF
浮点数溢出
FPE_FLTUND
浮点数下溢
FPE_FLTRES
浮点数不精确结果
FPE_FLTINV
浮点数不合法操作
FPE_FLTSUB
下标超出范围
下面是 SIGSEGV 信号可以填充 si_code 的值:
SEGV_MAERR
地址没有映射到一个对象上
SEGV_ACCEERR
映射的对象访问权限不合法
SEGV_BNDERR(Linux 3.19 后)
地址边界检查失败
SEGV_PKUERR(Linux 4.6 之后)
内存保护秘钥错误导致访问拒绝。参考 pkeys(7),用于本次访问的秘钥由 si_pkey 指定
下面是 SIGBUS 信号可以填充的 si_code 值:
BUS_ADRALN
不合法的地址对齐
BUS_ADRERR
不存在的物理地址
BUS_OBJERR
对象特定的硬件错误
BUS_MCEERRR_AR(Linux 2.6.32 后)
硬件内存发生错误,在机器检查中消耗了,需要处理。
BUS_MCEERR_AO
进程检查到了硬件内存错误,但是没有消耗,可以不用处理。
下面是信号 SIGTRAP 可以填充的 si_code 值:
TRAP_BRKPT
进程断点
TRAP_TRACE
进程跟踪陷入
TRAP_BRANCH(Linux 2.4 后,IA64)
进程发生分支陷入
TRAP_HWBKPT(Linux 2.4 后,IA64)
硬件断点/观测点
下面是信号 SIGCHLD 可以填充的 si_code:
CLD_EXITED
子进程退出
CLD_KILLED
子进程被杀死了
CLD_DUMPED
子进程异常退出
CLD_TRAPPED
被追踪的子进程陷入了
CLD_STOPPED
子进程停止
CLD_CONTINUED(Linux 2.6.9 后)
停止的子进程继续运行
下面是信号 SIGIO/SIGPOLL 可以填充的 si_code 值:
POLL_IN
数据输入可用
POLL_OUT
输出缓冲区可用
POLL_MSG
输入消息可用
POLL_ERR
I/O 错误
POLL_PRI
高优先级输入可用
POLL_HUP
设备断开
下面是信号 SIGSYS 可以填充的 si_code 值:
SYS_SECCOMP(Linux 3.5 后)
seccomp(2) 过滤规则触发
标记位支持动态检测
Linux 上 sigaction() 系统调用对于 act->sa_flags 中的未知标记位是全部接收而不报告错误。Linux 5.11 后的内核会在第二次调用 sigaction() 时清除 oldact->sa_flags 里不认识的标记位。不过历史原因,第二次嗲用仍然会遗留这些位。
这就意味着对于新标记的支持并不能简单通过对 sa_flags 的测试实现,必须对 sa_flags 进行测试,将 SA_UNSUPPORTED 清除后才可以相信 sa_flags。
只有检查通过,否则信号处理函数的行为是没办法保证的。比较明智的做法是要么在注册信号处理函数以及做检查时,先屏蔽会受到影响的信号,要么对于同步信号来讲信号处理函数本身无法发送第二个 sigaction()。
对于内核不支持特定标记的场景,内核的行为就相当于这个标记没有设置,即使我们在 act->sa_flags 中设置了。
SA_NOCLDSTOP/SA_NOCLDWAIT/SA_SIGINFO/SA_ONSTACK/SA_RESTART/SA_NODEFER/SA_RESETHAND 以及一些架构定义的 SA_RESTORER,上述机制可能并没有那么可靠,因为这些信号是在 Linux 5.11 之前引入的。然后,通常情况下程序是可以假定这些标记是支持的,因为从 Linux 2.6 开始就已经支持它们了,2.6 发布于 2003 年。
可以参考下面的例子来看 SA_UNSUPPORTED 用法演示。
sigaction() 成功时返回 0。发生错误时,返回 -1,并设置 errno 来提示具体错误。
错误值定义如下:
EFAULT | act 或者 oldact 指向的内存不是进程地址空间中合法的地址 |
EINVAL | 指定了非法的信号。如果尝试修改 SIGKILL 或者 SIGSTOP 的行为,也会报告者错误,这两个信号无法被捕捉或者忽略 |
C 库和内核差异
glibc 对 sigaction() 的封装函数会在尝试修改两个实时信号的处置函数时报出 EINVAL 错误,这两个实时信号是由 NPTL 多线程实现使用的,参考 nptl(7) 查看更多细节。
在一些架构上信号床位于 C 库中,glibc 的 sigaction() 封装函数会将信号床代码地址放到 act.sa_restorer 字段并设置 act.sa_flags 字段为 SA_RESTORER。参考 sigreturn(2)。
原来的 Linux 系统调用被命名为 sigaction(),然后随着 Linux 2.2 增加了实时信号,固定长度的 sigset_t(32 位)就不是用于这个目的了。最终,添加了一个新的系统调用 rt_sigaction() 来支持更大的 sigset_t 类型。新的系统调用有携带了第四个参数(size_t sigsetsize),通过 act.sa_mask 和 oldact.sa_mask 指定了信号集的字节大小。glibc 封装函数隐藏了这些细节,内核支持实时信号的情况下就会透明的跳动 rt_sigaction()。
POSIX.1-2001,SVr4。
POSIX.1-1990 不允许设置 SIGCHLD 的处置函数为 SIG_IGN。POSIX.1-2001 以及后面的标准允许这样做来实现防止创建僵尸进程(参考 wait(2))。而且,历史上 BSD 和 System V 忽略 SIGCHLD 的行为也是不一致的,所以从移植性考虑,我们应该捕获 SIGCHLD 信号然后进行 wait(2) 或者类似的行为来防止子进程变为僵尸进程。
POSIX.1-1990 只指定了 SA_NOCLDSTOP,POSIX.1-2001 增加了 SA_NOCLDSTOP/SA_NOCLDWAIT/SA_NODEFER/SA_ONSTACK/SA_RESETHAND/SA_RESTART/SA_SIGINFO。在 sa_flags 里面使用后面这些标记会导致对老的 UNIX 实现的兼容性问题。
SA_RESETHAND 标记和 SVr4 标记同名兼容。
SA_NODEFER 标记在 内核 1.3.9 后和 SVr4 同名标记兼容。更老的 Linux 内核实现允许接收任何信号,不只是我们安装的这些(会覆盖 sa_mask 设置)。
通过 fork(2) 创建的子进程会从父进程集成一份信号处置的拷贝。在执行 execve(2) 时,这些处置会被复位到默认值,被忽略信号的处置不变。
根据 POSIX 要求,一个进程忽略了非 kill(2) 和 raise(3) 产生的 SIGFPE/SIGILL/SIGSEGV 信号后的行为是未知的。整数除零会产生未定义的结果。一些架构上它会产生 SIGFPE 信号。(用 -1 去除最小负整数也可能产生 SIGFPE。)忽略这些信号可能会导致程序进入无限循环。
sigaction() 在将第二个参数指定为 NULL 时,可以用来查询当前的信号处理函数。也可以通过将第二、三个参数都设置为 NULL 来查询当前机器是否支持指定信号。
我们无法屏蔽 SIGKILL 和 SIGSTOP 信号(通过 sa_mask 指定),这些尝试会被忽略掉。
参考 sigsetops(3) 来查阅操作信号集的详细信息。
参考 signal-safety(7) 来查看异步信号安全函数列表,这些函数可以在信号处理函数内部安全的调用。
未归档
在引入 SIG_SIGINFO 之前,也可以使用一些方法来获得信号的额外信息。通过指定 sa_handler 信号处理函数的第二个参(struct sigcontex),这个结构和sa_sigaction 信号处理函数的第三个参数 ucontext 的uc_mcontext 具有同样的结构。参考相关内核源码来详细了解,这个目前已经过时了。
当向 SA_SIGINFO 信号处理函数分发信号时,内核并不会在 siginfo_t 的字段内提供所有信号相关的信息。
Linux 2.6.13 后,在 sa_flags 中指定 SA_NODEFER 不仅会在信号处理函数过程中屏蔽信号,也会屏蔽 sa_mask 中指定的信号。这个 bug 在 Linux 2.6.14 总修复了。
参考 mprotect(2)。
标记支持检测
下面的程序会在检查到支持 SA_EXPOSE_TAGBITS 时以 EXIT_SUCCESS 状态退出,否则以 EXIT_FAILURE 状态退出。
#include
#include
#include
#include
void
handler(int signo, siginfo_t *info, void *context)
{
struct sigaction oldact;
if (sigaction(SIGSEGV, NULL, &oldact) == -1
|| (oldact.sa_flags & SA_UNSUPPORTED)
|| !(oldact.sa_flags & SA_EXPOSE_TAGBITS))
{
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
int
main(void)
{
struct sigaction act = { 0 };
act.sa_flags = SA_SIGINFO | SA_UNSUPPORTED | SA_EXPOSE_TAGBITS;
act.sa_sigaction = &handler;
if (sigaction(SIGSEGV, &act, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
raise(SIGSEGV);
}