https://man7.org/linux/man-pages/man7/signal.7.html
名称
信号 - 信号的概述
描述
Linux支持POSIX的可靠信号(下文的标准信号)和POSIX的实时信号。
信号描述
每一个信号都有当前的处置,用来决定当前的进程在接收到这个信号投递的时候如何处理。
“行为”词条的那一行中指定了对于每一个信号的默认操作,如下:
Term 默认行为是中止进程
Ign 默认行为是忽略信号
Core 默认行为是中止进程并且输出core信息
Stop 默认行为是停止进程
Cont 默认行为是如果当前停止了,继续进程
进程可以通过sigaction
或者signal
来修改信号的行为。(后者跨平台性较差,如果绑定了信号句柄的话,可以参考signal
查看详细信息)。使用这些系统调用,一个进程可以选择一个如下的行为来应对信号的投递:执行默认行为;忽略信号;或者使用信号句柄捕获这个信号,程序定义的函数会被自动的调用,当信号投递的时候。
默认情况下,一个信号句柄会在进程堆栈上正常调用。设置信号句柄使用交替的堆栈也是可行的;查看sigaltstack
讨论如何这样做将会是有用的。
信号的处理是属于进程的:在多线程中,信号的处理对于每个线程是一样的。
通过fork
创建的子进程会拷贝父进程的信号处理。通过execve
,信号处理句柄会被设置为默认值;忽略处置的信号会被保持,不会修改。
发送信号
下面的这些系统调用和类库函数允许调用者发送信号:
raise
发送一个信号到调用的线程
kill
发送一个信号到指进程,或者到所有的进程组,或者到所有的系统上的进程
pidfd_send_signal
发送一个信号到通过PID文件描述符指定的进程
killpg
发送一个信号到一个指定群组的所有进程
pthread_kill
发送一个信号到指定的POSIX线程在同一个进程调用
tgkill
发送信号到指定的进程的一个线程。这个是系统调用,用来实现pthread_kill
sigqueue
发送一个实时的信号,附带额外的数据到指定的进程
等待捕获的一个信号
下面的系统调用会阻塞线程,知道对应的信号被捕获或者一个未被处理的信号中断了进程:
pause(2)
中断执行直到任何一个信号捕获
sigsuspend(2)
临时修改信号标识位(如下),中断执行直到任何一个未设置的信号捕获
同步接收信号
出了通过信号句柄异步的捕获信号,也可以同步的接收信号,就是阻塞执行,直到信号分发,内核返回关于信号的信息到调用的位置。有两个方法可以这样做:
sigwaitinfo
,sigtimedwait
和sigwait
挂起执行直到定意的信号簇中其中一个触发。任何一个调用都会返回触发信号的信息。
signalfd
返回一个文件描述符用作读取相关触发信号的信息。每一个read
从这个文件描述符读取内容都会阻塞直到由signalfd
定义的一组信号中的一个触发。由read
返回的数据包含一个结构体描述这个信号。
信号的标识位和挂起信号
一个信号可以被锁定,就是直到过一会解除锁定,这个信号都不会被派送。当这个信号已经产生,但是还没有派送,这个期间叫做挂起。
每一个进程中的线程都有一个独立的信号标识位,用来指明一系列信号,这些信号用来表明当前线程已经阻塞。一个线程可以通过pthread_sigmask
操作自己的信号标识符。在一个传统的信号线程程序中,sigprocmask
可以被用作操作信号标识符。
通过fork
创建的子进程会继承父进程的信号描述符;通过execve
创建的也会被保留
一个信号可以是指向进程的,也可以是指向线程的。一个指向进程的信号是附加到进程所有的目标或者挂起进程。一个信号可能是进程指定的,因为它是由内核因为某些原因而不是由硬件问题触发的;或者是由kill
或者sigqueue
发送的。
一个线程指向的信号目标是一个特定的线程。一个信号可能是线程指向的,因为可能由特定的机器语言定义的产物触发了一个硬件中断。比如SIGSEGV是非法内存访问,或者SIGFPE是算术错误,或者由一些线程接口比如tgkill
或pthread_kill
创建。
一个进程指向的信号可能会被发送到任何一个线程,而没有被立马挂起。如果超过一个线程拥有这个线程,并且解锁,内核会选择一个线程来分发信号。
一个线程可以包含一组信号,由sigpending
挂起。这一组信号包含进程指向的挂起的一组信号的合集和调用线程挂起的一组信号。
通过fork
创建的子进程初始化包含空的挂起信号集;通过execve
创建的保留挂起的信号集。
信号句柄的执行
不管什么时候,都有一个从内核态向用户态传输的执行,比如通过系统调用或者线程基于CPU时序的时序的返回,内核检测是否有一个挂起的解锁的信号给已经建立好信号句柄的进程。如果有,下面的步骤就会触发:
-
内核处理必要的准备工作用作执行如下步骤:
-
a) 信号从挂起集合移除
-
b) 如果信号句柄已经通过
sigaction
添加了,并且设定了SA_ONSTACK标识,并且线程定义了交替的信号堆栈(使用sigaltstack
),这个堆栈就被安装了。 -
c) 有关信号内容的各种数据都被保存在特定的在堆栈上创建的片段中。保存的信息包括:
-
程序计数器寄存器。比如下一个在主程序中需要执行返回的消息句柄的地址
-
特定结构寄存器的状态,需要在恢复打断程序的时候使用。
-
线程当前的信号标识位
-
线程交替信号堆栈设定
-
如果信号句柄通过
sigaction
安装了SA_SIGINFO标识,上面的信息可以通过ucontext_t结构体获得,在信号句柄的第三个参数中。
-
-
d) 任何信号通过
sigprocmask
在act->sa_mask注册的标识符都会被添加到线程的信号标识符中。当使用sigprocmask
注册句柄,会被添加到线程信号标识符中。派送的信号同样会被添加到信号标识符中,除非在注册句柄的时候定义了SA_NODEFER。当句柄执行的时候,信号会被阻塞。
-
-
内核设计了一个结构体为信号句柄在堆栈上。内核设置程序技术为线程,指向第一个信号句柄函数的命令,并且配置返回地址对于这个函数,指向一片用户空间的代码,这就是信号弹跳,在sigreturn中会介绍。
-
当在信号句柄函数开始执行的时候,内核会把控制交给用户层。
-
当信号句柄返回,控制会传递给信号弹跳代码
-
这个信号弹跳调用
sigreturn
,是一个系统调用,使用在第一步创建堆栈结构中的信息来还原线程到它自己的堆栈中,在信号句柄被调用的时候。这个线程的信号标识位和交替信号堆栈设置被保存到程序中。在完成sigreturn
调用之前,内核把权限交给用户层,线程从这个被信号打断的位置重新执行。
注意,如果信号句柄没有返回,比如通过siglongjmp
把控制权限从句柄中跳出,或者句柄通过execve
执行了一个新程序,最后一个步骤就不会完成。特别是,在这种情境下,程序需要保存信号标识符(使用sigprocmsk
),如果它设计的是解锁被信号句柄阻塞的信号的情况。注意siglongjmp
是否保存信号标识符,取决于通过调用sigsetjmp
定义的变量savesigs。
从内核的角度看,执行信号句柄的代码和执行用户层所有的代码都是一样的。也就是,内核不会保存任何特殊的状态信息,用来指定线程当前在信号句柄中执行。所有必须的状态信息都是保存在用户层寄存器和用户堆栈中。信号句柄嵌套调用的限制仅仅是由用户堆栈举鼎,或者说软件的设计决定。
标准信号
Linux支持的标准信号如下。第二列表明是哪个标准定义的这个信号: P1990表明是POSIX.1-1990标准;P2001表明信号是在SUSv2和POSIX.1-2001添加的。
信号 | 标准 | 行为 | 注释 |
---|---|---|---|
SIGABRT | P1990 | Core | 通过abort(3)中断信号 |
SIGALRM | P1990 | Term | 通过alarm(2)定时信号 |
SIGBUS | P2001 | Core | Bus错误(错误的内存访问) |
SIGCHLD | P1990 | Ign | 孩子停止或者中止 |
SIGCLD | - | Ign | SIGCHLD另一个写法 |
SIGCONT | P1990 | Cont | 如果暂停了,那么继续 |
SIGEMT | - | Term | 仿真器陷阱 |
SIGFPE | P1990 | Core | 浮点数异常错误 |
SIGHUP | P1990 | Term | 由于控制中断或者控制进程死亡导致的挂起 |
SIGILL | P1990 | Core | 非法的指令 |
SIGINFO | - | SIGPWR的另一种写法 | |
SIGINT | P1990 | Term | 键盘中断 |
SIGIO | - | Term | I/O可用(4.2BSD) |
SIGIOT | - | Core | IOT陷阱。SIGABRT的另一种写法 |
SIGKILL | P1990 | Term | Kill信号 |
SIGLOST | - | Term | 文件锁丢失(不可用) |
SIGPIPE | P1990 | Term | 损坏的管道:写入管道,另一端没有读取;参考pipe(7) |
SIGPOLL | P2001 | Term | 可以poll的事件(Sys V);与SIGIO一样 |
SIGPROF | P2001 | Term | 仿真计时器过期 |
SIGPWR | - | Term | 电源故障(System V) |
SIGQUIT | P1990 | Core | 键盘退出 |
SIGSEGV | P1990 | Core | 非法的内存引用 |
SIGSTKFLT | - | Term | 堆栈在协同处理器上出错(不可用) |
SIGSTOP | P1990 | Stop | 停止进程 |
SIGTSTP | P1990 | Stop | 在终端上停止打字 |
SIGSYS | P2001 | Core | 不好的系统调用(SVr4);查看seccomp(2) |
SIGTERM | P1990 | Term | 停止信号 |
SIGTRAP | P2001 | Core | 跟踪/断电捕捉器 |
SIGTTIN | P1990 | Stop | 终端后台输入 |
SIGTTOU | P1990 | Stop | 终端后台输出 |
SIGUNUSED | - | Core | 与SIGSYS一样 |
SIGURG | P2001 | Ign | 套接字上的紧急情况(4.2BSD) |
SIGUSR1 | P1990 | Term | 用户定义的信号1 |
SIGUSR2 | P1990 | Term | 用户定义的信号2 |
SIGVTALRM | P2001 | Term | 虚拟闹钟定时器(4.2BSD) |
SIGXCPU | P2001 | Core | CPU时间超过限制(4.2BSD);查看setrlimit(2) |
SIGXFSZ | P2001 | Core | 文件大小超过限制(4.2BSD);查看setrlimit(2) |
SIGWINCH | - | Ign | 窗口调整大小信号(4.3BSD, Sun) |
信号SIGKILL和SIGSTOP不能被捕获,阻塞或者忽略。
从Linux 2.2(包括)开始,SIGSYS,SIGXCPU,SIGXFSZ的默认行为和(在架构上,出了SPARC和MIPS)SIGBUS都是结束进程(没有core dump)。(在一些其他UNIX系统中,SIGXCPU和SIGXFSZ默认行为是结束进程,没有core dump。)Linux 2.4符合POSIX.1-2001标准,对于这个信号,是结束进程,产生core dump。
SIGEMT并没有在POSIX.1-2001中定义,但是出现在绝大多数的UNIX系统上,默认行为是结束进程,产生core dump。
SIGPWR(没有在POSIX.1-2001中定义)如果有的UNIX支持的话,默认行为是忽略。
SIGIO(没有在POSIX.1-2001中定义)如果有UNIX支持的话,默认行为是忽略。
对于标准信号排序与分发
如果很多标准信号挂起到进程上,信号分发的顺序是无法定义的。
标准信号是没有队列的。如果因为阻塞某一个标准信号的实例被创建了很多次,只有一个信号被标记为挂起(这个信号在解锁时会被分发一次)。在这种情况下,如果一个信号已经被挂起,与这个信号相关的siginfo_t结构体不会因为后续的同样信号的实例到达而重写。因此,进程会接收到第一次实例信号的信息
标准信号的序号
每一个信号的序号在下面表格中给出了。正如表格中显示的一样,每一个信号在不同的架构下都有着不同的数字。第一列是x86,ARM和绝大多数架构;第二列是Alpha和SPARC;第三列是MIPS;最后一列是PARISC。短横线标识在对应的架构上没有。
Signal | x86/ARM/most others | Alpha/SPARC | MIPS | PARISC | Notes |
---|---|---|---|---|---|
SIGHUP | 1 | 1 | 1 | 1 | |
SIGINT | 2 | 2 | 2 | 2 | |
SIGQUIT | 3 | 3 | 3 | 3 | |
SIGILL | 4 | 4 | 4 | 4 | |
SIGTRAP | 5 | 5 | 5 | 5 | |
SIGABRT | 6 | 6 | 6 | 6 | |
SIGIOT | 6 | 6 | 6 | 6 | |
SIGBUS | 7 | 10 | 10 | 10 | |
SIGEMT | - | 7 | 7 | - | |
SIGFPE | 8 | 8 | 8 | 8 | |
SIGKILL | 9 | 9 | 9 | 9 | |
SIGUSR1 | 10 | 30 | 16 | 16 | |
SIGSEGV | 11 | 11 | 11 | 11 | |
SIGUSR2 | 12 | 31 | 17 | 17 | |
SIGPIPE | 13 | 13 | 13 | 13 | |
SIGALRM | 14 | 14 | 14 | 14 | |
SIGTERM | 15 | 15 | 15 | 15 | |
SIGSTKFLT | 16 | - | - | 7 | |
SIGCHLD | 17 | 20 | 18 | 18 | |
SIGCLD | - | - | 18 | - | |
SIGCONT | 18 | 19 | 25 | 26 | |
SIGSTOP | 19 | 17 | 23 | 24 | |
SIGTSTP | 20 | 18 | 24 | 25 | |
SIGTTIN | 21 | 21 | 26 | 27 | |
SIGTTOU | 22 | 22 | 27 | 28 | |
SIGURG | 23 | 16 | 21 | 29 | |
SIGXCPU | 24 | 24 | 30 | 12 | |
SIGXFSZ | 25 | 25 | 31 | 30 | |
SIGVTALRM | 26 | 26 | 28 | 20 | |
SIGPROF | 27 | 27 | 29 | 21 | |
SIGWINCH | 28 | 28 | 20 | 23 | |
SIGIO | 29 | 23 | 22 | 22 | |
SIGPOLL | Same as SIGIO | ||||
SIGPWR | 30 | 29/- | 19 | 19 | |
SIGINFO | - | 29/- | - | - | |
SIGLOST | - | -/29 | - | - | |
SIGSYS | 31 | 12 | 12 | 31 | |
SIGUNUSED | 31 | - | - | 31 |
注意这两点:
-
在有SIGUNUSED定义的系统中,这个信号的行为与SIGSYS一致。从gibc 2.26开始,SIGUNUSED在所有的架构下就被抛弃了。
-
信号29是SIGINFO/SIGPWR(这两个是同一个信号)在Alpha,但是在SPARC上是SIGLOST。
实时信号
从2.2版本起,Linux开始支持在POSIX.1b中定义的实时扩展(现在包含在POSIX.1-2001中)的实时信号。支持的实时信号的范围由SIGRTMIN和SIGRTMAX两个宏限定。POSIX.1-2001要求规定必须至少支持_POSIX_RTSIG_MAX (8)的实时信号。
Linux内核支持至少33个不同的实时信号,从32到64。但是glibc POSIX线程的实现内部使用了信号二(NPTL)或者信号三(LinuxThreads)作为实时信号(参考pthreads(7)),并且把SIGRTMIN数值合理的设定为34或者35。因为可用的实时信号的数值依赖于glibc线程实现(并且这个变化会在运行时根据核和glibc而变化),并且真是的实时信号的范围在不同的UNIX系统上也不一样,程序永远不应该使用实时信号的模数字,而应该使用实时信号的符号SIGRTMIN+n这种样式,包括可以用作检查。SIGRTMIN+n不包括SIGRTMAX。
与标准信号不一样,实时信号没有提前定义的含义:所有的实时信号都可以用作应用自己定义使用。
为定义行为句柄的实时信号会中止进程。
实时信号可以通过下面来辨别:
-
多个实时信号实例是有队列的。明显的区别就是,如果多个标准信号实例分发,当信号阻塞时,只会有一个实例被放入队列。
-
如果一个信号通过
sigqueue
发送,(3),一个附带的数据(整数或者指针)都可以通过信号发送。如果接收进程通过一个使用SA_SIGINFO标识来设置sigaction的句柄绑定,他可以通过传递给句柄的第二个参数siginfo_t结构体的si_value获得这个数据。除此之外,可以通过结构体的si_pid和si_uid获得发送进程的PID和UID。 -
实时信号是按照顺序分发的。多个实时信号的同一个类型也是按照它们发送的顺序分发的。如果不同的实时信号发送到进程,它们根据信号的数字大小排序。比如,小的数字的信号有着更高的权限。与之不同的是,如果多个标准信号挂起到进程,它们分发的顺序是未知的。
如果标准信号和实时信号都附加到进程,POSIX并没有规定它们的分发顺序。Linux通常会给标准信号权限。
对于POSIX,一个实现必须保证_POSIX_SIGQUEUE_MAX (32) 的实时信号在一个进程的队列中。但是Linux做法不同。在内核2.6.7以及更高的版本上,Linux对于实时信号的限制是全系统公用的,对于每一个进程。这个限制可以通过/proc/sys/kernel/rtsig-max file查看和修改。/proc/sys/kernel/rtsig-nr文件可以用作查看多少实时信号当前在队列中。在Linux 2.6.8中,/proc的接口被RLIMIT_SIGPENDING资源限制替换,针对每一个用户做了队列信号限制;参考setrlimit(2)获得更多信息。
附加的实时信号需要扩充信号结构体(igset_t)从32位到64位。
因此,很多系统调用被新的接口取代,用来支持更大的信号集。对应的列表如下:
Linux 2.0 and earlier | Linux 2.2 and later |
---|---|
sigaction(2) | rt_sigaction(2) |
sigpending(2) | rt_sigpending(2) |
sigprocmask(2) | rt_sigprocmask(2) |
sigreturn(2) | rt_sigreturn(2) |
sigsuspend(2) | rt_sigsuspend(2) |
sigtimedwait(2) | rt_sigtimedwait(2) |
系统调用和类库函数中断的信号处理句柄
如果一个信号句柄被系统调用或者类库函数阻塞,那么:
-
调用在信号处理句柄返回的时候会自动开始
-
调用接收到一个EINTR错误
这两种行为依赖于这个接口,是否设置了信号句柄的SA_RESTART标识(参考sigaction(2))。细节在UNIX系统上有区别,下面是Linux系统的情况:
如果一个阻塞调用的接口被信号句柄打断,调用会在设置了SA_RESTART标识的信号句柄返回时自动开始;不然就会获得一个EINTR错误:
-
read(2)
readv(2)
write(2)
writev(2)
ioctl(2)
在“慢”设备上调用。“慢”设备就是I/O调用,永久阻塞,比如,终端,管道或者套接字。如果I/O调用在慢设备上已经传输了一些数据,这时候被信号句柄中断,这个调用会返回一个成功的状态(正常是传输了多少数据)。注意在这里(本地)硬盘不是慢设备;对硬盘设备的I/O操作不会被信号中断。 -
open(2)
,如果可以被阻塞(比如,打开FIFO;参考fifo(7))。 -
wait(2)
wait3(2)
wait4(2)
waitid(2)
waitpid(2)
-
套接字接口:
accept(2)
connect(2)
recv(2)
recvfrom(2)
recvmmsg(2)
recvmsg(2)
send(2)
sendto(2)
sendmsg(2)
,也属于上面的情况,除非设置了超时时间(参考下面的内容) -
文件锁定接口:
flock(2)
和F_SETLKW和fcntl
的F_OFD_SETLKW操作 -
POSIX消息队列接口:
mq_receive(3)
mq_timedreceive(3)
mq_send(3)
mq_timedsend(3)
-
futex(2)
FUTEX_WAIT(从Linux 2.6.22起;以前的版本一致错误返回EINTR) -
getrandom(2)
-
pthread_mutex_lock(3)
pthread_cond_wait(3)
还有相关APIs -
futex(2)
FUTEX_WAIT_BITSET -
POSIX信号量接口:
sem_wait(3)
sem_timedwait(3)
(从Linux 2.6.22开始,以前版本一直返回EINTR)。 -
read(2)
从一个inotify(7)文件描述符(从Linux 3.8开始,以前版本一直返回EINTR)
下面的接口永远不会在信号句柄打断的时候重新开始,除非使用了SA_RESTART;永远都是在打断的时候返回EINTR错误信息:
-
“输入”套接字接口,当一个超时(SO_RCVTIMEO)设置到套接字通过
setsockopt(2)
:accept(2)
recv(2)
recvfrom(2)
recvmmsg(2)
(同样没有NULL超时参数)和recvmsg(2)
-
“输出”套接字接口,当一个超时(SO_RCVTIMEO)通过
setsockopt(2)
设置到套接字上:connect(2)
send(2)
sendto(2)
sendmsg(2)
-
等待信号的接口:
pause(2)
sigsuspend(2)
sigtimedwait(2)
sigwaitinfo(2)
-
文件描述符的复用接口:
epoll_wait(2)
epoll_pwait(2)
poll(2)
ppoll(2)
select(2)
pselect(2)
-
System V IPC 接口:
msgrcv(2)
msgsnd(2)
semop(2)
semtimedop(2)
-休眠接口: clock_nanosleep(2)
nanosleep(2)
usleep(3)
io_getevents(2)
sleep(3)
同样不会重新开始,当一个打断通过句柄,但是会返回成功,返回睡眠了多久
中断系统调用和类库函数的停止信号
在Linux上,即使没有信号句柄,某一个阻塞的接口也可以在进程因为一个信号中断再通过SIGCONT继续的时候返回EINTR错误。这个行为在POSIX.1中并没有规定,在其他系统上不用考虑。
Linux接口描述这个行为是:
-
"输入" 套接字接口,当使用
setsockopt
把超时(SO_RCVTIMEO)套接字上面:accept(2)
recv(2)
recvfrom(2)
recvmmsg(2)
(传入的超时参数不是NULL)recvmsg(2)
-
"输出" 套接字接口,当通过
setsockopt(2)
把超时(SO_RCVTIMEO)设置到套接字上:connect(2)
send(2)
sendto(2)
sendmsg(2)
,发送超时(SO_SNDTIMEO)设置在上面。 -
epoll_wait(2)
epoll_pwait(2)` -
semop(2)
semtimedop(2)
-
sigtimedwait(2)
sigwaitinfo(2)
-
Linux 3.7或者更早:
read(2)
从inotify(7)
文件描述符 -
Linux 2.6.21或者更早:
futex(2)
FUTEX_WAITsem_timedwait(3)
sem_wait(3)
-
Linux 2.6.8或者更早:
msgrcv(2)
msgsnd(2)
-
Linux 2.4或者更早:
nanosleep(2)
遵循规范
POSIX.1,出了特别提示的内容之外
注意
更短关于异步信号安全的函数,参考signal-safety(7)
/proc/[pid]/task/[tid]/status文件包含很多字段,展示线程阻塞的信号(SigBlk)、捕获的信号(SigCgt)或者忽略的信号(SigIgn)。这一系列信号被捕获或者忽略对于一个进程下所有的线程是一样的。其他一些字段展示直接发送给特定线程的挂起的信号(SigPnd)或者直接发送给进程的挂起的信号(ShdPnd)。相关的/proc/[pid]/status展示主线程的信息。参考proc(5)
。
问题
有六个信号是有关硬件异常的:SIGBUS, SIGEMT, SIGFPE, SIGILL, SIGSEGV, SIGTRAP。这其中的任何一个信号由于硬件异常被分发,不需要关心,因为没有文档记录行为。
比如,一个内存错误到达,导致SIGSEGV信号在CPU架构上,可能也会产生SIGBUS在另外一个架构上。
另外举个例子,在x86架构上,int指令如果使用了禁止的命令(任何数字,除了3或者128),导致SIGSEGV信号,即使SIGILL可能有更大作用,因为CPU报告了禁止操作对内核。