【计算机网络】信号处理接口 Signal API(3)

          收发信号思想是 Linux 程序设计特性之一,一个信号可以认为是一种软中断,通过用来向进程通知异步事件。

        本文讲述的 信号处理内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解信号编程。


信号概述

遵循 POSIX.1,特别说明除外

1.描述

        Linux 机制 POSIX 可靠信号(我们成为标准信号),也支持 POSIX 实时信号。

        信号处置方法

        每个信号都有一个定义如何处理信号的处置方法。

        下面列表中 “行为” 列定义了每个信号的默认处置行为:

信号 默认行为
Term 默认行为是终止进程
Ign 默认行为是忽略这个信号
Core 默认行为是终止进程并转储内核(dump core)
Stop 默认行为是停止进程
Cont 默认行为是在进程停止后继续执行该进程

        进程可以通过 sigaction(2) 或者 signal(2) 接口修改信号的处置方法。(后者在建立信号处理函数时移植性差一些,参考 signal(2) 获得详细信息。)通过这些系统调用,在信号发生时,进程可以选择以下行为处理信号:

  • 使用默认行为
  • 过略信号
  • 使用信号处理函数(signal handler)捕获信号
  • 使用用户自定义的函数

        默认情况下,信号处理函数是在普通进程栈上调用的,不过也可以设置它使用其他可选的栈,参考 sigaltstack(2) 查看一些关于如何使用以及这样做有哪些好处的讨论。

        信号处置是每个进程的属性:在多线程应用中,信号处置函数对于所有线程都是一样的。

        通过 fork(2) 创建的子进程会复制一份父进程的信号处置方法,在 execve(2) 执行的过程中,进程想要处理的信号处置方法会被设置为默认值,而处置方法为忽略的信号保持不变。

        发送信号

        下面系统调用和库函数提供了调用者发送信号的相关接口:

        raise(3)

                向调用线程发送一个信号

        kill(2)

                向指定进程、指定进程组下的所有进程、系统的所有进程发送信号

        pidfd_send_signal(2)

                向 PID 文件描述符指定的进程发送一个信号

        killpg(3)

                向指定进程组所有成员发送一个信号

        pthread_kill(3)

                向调用者所在进程中其他 pthread 线程发送一个信号

        tgkill(2)

                向指定进程中的特定线程发送一个信号(这是用来实现 pthread_kill(3) 的系统调用)

        sigqueue(3)

                向指定进程发送一个福袋数据的实时信号

        等待捕捉信号

        下面系统调用会暂停当前调用线程直至捕捉到一个信号(或者无法处理的信号终止该进程):

        pause(2)

                暂停直至捕捉到一个信号

        sigsuspend(2)

                临时修改信号屏蔽值并暂停执行直至捕捉到任意未屏蔽的信号

        同步接收信号

        除了通过信号处理函数异步处理信号,我们还可以通过同步接收信号,也就是说程序会阻塞运行直至接收到信号,同时也是在这一点内核会返回调用者一些关于信号的信息。通常有两种方式实现同步信号接收:

  • sigwaitinfo(2)、sigtimedwait(2)、sigwait(3) 都会阻塞执行直到收到指定集合里的信号发生,每个调用都会返回接收到信号的信息。
  • signalfd(2) 会返回一个用于读取关于接收信号的文件描述符,每次对文件描述符的 read(2)调用都会一直阻塞直到通过 signalfd(2) 指定的信号集合里的任何一个信号发生。read(2) 返回的 buffer 里包含关于该信号的描述结构。

        信号屏蔽和等待信号

        一个信号是可以被屏蔽的,也就是说只有在打开屏蔽后才会得到分发。其实我们上面说的信号发生更确切的说是信号分发到了进程。那么信号在生成和被分发之前的状态我们成为等待(pending)。

        一个进程中的每个线程都有一个独立的信号屏蔽(signal mask),或者也可以称为掩码,它只是了线程当前屏蔽掉的信号集合。线程可以通过 pthread_sigmask(3) 来操作信号掩码,在传统的单线程应用中可以通过 sigprocmask(2) 来操作信号掩码。

        通过 fork(2) 创建的子进程复制一份父进程的信号掩码,信号掩码在整个 execve(2) 过程中会一直保留。

        一个信号可以是发给进程的也可以是发给线程的。发给进程的信号通常是由内核产生的而不是由特定硬件异常产生的,通常是由类似 kill(2) 或者 sigqueue(3) 产生的。而发给线程的信号可能是因为执行了一些特定机器语言指令导致硬件异常(比如非法内存访问的 SIGSEGV 信号,或者数学错误的 SIGFPE)导致的,或者是线程执行了特定接口,比如 tgkill(2) 或者 pthread_kill(3)。

        发给进程的信号是可以被分发到进程内任何没有屏蔽整个信号的线程的。如果不止一个线程没有屏蔽该信号,那么内核会随便选一个线程分发该信号。

        线程可以通过 sigpending(2) 接口来获得当前处于等待状态的信号集,信号集是一个进程等待信号和线程等待信号的共用体。

        通过 fork(2) 创建的进程初始化为控的等待信号集,在整个 execve(2) 过程中,该等待信号集保持不变。

        信号处理函数执行

        在每次内核模式到用户模式的切换时(也就是一个系统调用返回或者调用一个线程到 CPU 上运行),内核都会检查该进程建立信号处理函数的信号的非屏蔽信号的等待状态。如果有这样的信号存在,那么就会进行以下几步:

        (1)内核会在执行信号处理函数前做一些准备工作:

                (1.1)信号从等待信号集合中移除

                (1.2)如果信号处理函数是通过 sigaction(2) 安装并指定了 SA_ONSTACK 标记,集线程定义了一个候补信号栈(使用 sigaltstack(2)),那么内核就会安装这个栈

                (1.3)信号相关的一些上下文片段会被封装到栈上的特定帧里,保存的信息包括:

                           a)程序计数寄存(也就是处理函数退出后主程序要执行的下一条指令)

                           b)一些为恢复被打断程序运行的架构相关的寄存器

                           c)线程当前的信号掩码

                           d)线程的备用信号栈设置

                          (如果信号是通过 sigaction(2) SA_SIGINFO 标记安装的,那么上述信息是可以通过 ucontex_t 对象获取的,这个对象由信号处理函数的第三个参数指定。

        (2)内核在栈上为信号处理函数构建一个栈帧,内核设置线程的 PC 为信号处理函数的第一条指令,并配置信号处理函数的返回地址到一个称为信号蹦床的用户空间代码片段(在 sigreturn(2) 中有描述)。

        (3)内核将控制权返还给用户空间,这样就会从信号处理函数的开始执行。

        (4)当信号处理函数返回时,控制器交给信号蹦床代码。

        (5)信号蹦床调用 sigreturn(2),sigreturn(2) 是一个系统调用,用来使用步骤 1 中创建的栈帧来恢复信号发生之前的线程状态。信号掩码和备用信号栈也会在这个过程中得到恢复。在完成 sigreturn(2) 调用后,内核将控制权交给用户空间,线程会接着从被信号打断的点继续执行。

        值得注意的是,如果信号处理函数不返回(比如通过 siglongjmp(3) 将控制权交给了处理函数外或者处理函数通过 execve(2) 执行了一个程序),那么最后的步骤就不会执行。这种情况下,程序设计者应该特别注意,如果想要恢复在进入信号处理函数时屏蔽掉的信号的话,就必须自己恢复这些信号掩码(使用 sigprocmask(2))。(注意:siglongjmp(3) 可能恢复信号掩码,但是这个要依赖于 sigsetjmp(3) 的 savesigs 值。

        从内核的角度来看,执行信号处理函数和执行其他任何用户空间代码没有什么区别,也就是说内核并不会记录线程当前是否在执行信号处理函数。所有的状态都被保存到了用户空间寄存器和用户空间栈里了,所以信号处理函数的嵌套深度完全取决于用户栈大小(以及合理的软件设计)。

        标准信号

        Linux 支持的标准信号入下表所示。表中第二列标明了信号出现的规范:“P1990” 表示信号是在原先的 POSIX.1-1990 标准中描述的;“P2001” 表示信号是在 SUSv2 和 POSIX.1-2001 中添加的。

信号 标准 行为 备注
SIGABRT P1990 Core abort(3) 的终止信号
SIGALRM P1990 Term alarm(2) 的定时器信号
SIGBUS P2001 Core 总线错误(错误内存访问)
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.2 BSD)
SIGIOT - Core IOT 陷入,同 SIGABRT
SIGKILL P1990 Term 杀死信号
SIGLOST - Term 文件锁丢失(不用了)
SIGPIPE P1990 Term 破坏的管道:写一个没有读者的管道,参考 pipe(7)
SIGPOLL P2001 Term 可查询事件(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()
SIGTERM P1990 Term 终止信号
SIGTRAP P2001 Core 追踪/断电 陷入
SIGTTIN P1990 Stop 后台进程终端输入
SIGTTOU P1990 Stop 后台进程终端输出
SIGUNUSED - Core 同 SIGSYS
SIGURG P2001 Ign 套接字上的紧急事件(4.2 BSD)
SIGUSR1 P1990 Term 用户定义的信号 1
SIGUSR2 P1990 Term 用户定义的信号 2
SIGVTALRM P2001 Term 虚拟定时器时钟(4.2 BSD)
SIGXCPU P2001 Core 超过 CPU 时间限制,参考 setrlimit(2)
SIGXFSZ P2001

Core

文件大小超过限制(4.2 BSD),参考 setrlimit(2)
SIGWINCH - Ign 窗口大小重设信号(4.3 BSD,Sun)

       SIGKILL 和 SIGSTOP 不能被捕捉或者忽略。

        Linux 2.2 前,SIGSYS/SIGXCPU/SIGXFSZ(除了 SPARC 和 MIPS 架构)以及 SIGBUS  的默认行为是终止进程(并没有内核转储)。(在一些 UNIX 的系统上,SIGXCPU 和 SIGXFSZ 的默认行为也是不转储的终止。)Linux 2.4 遵循 POSIX.1-2001 对这些信号的要求,会终止进程并进行内核转储。

        SIGEMT 并没有在 POSIX.1-2001 中描述,但是至少出现在了大多数 UNIX 系统中,并且其默认行为是终止进程并内核转储。

        SIGPWR 也没有在 POSIX.1-2001 中描述,它在起源的一些 UNIX 系统中的默认行为是忽略。

        SIGIO 也一样,在一些 UNIX 系统上的默认行为是忽略。

标准信号的排队和分发语义

        如果多个标准信号正在等待某个进程,那么信号的顺序并没有指定。

        标准信号没有排队,如果一个标准信号的多个实例在信号屏蔽期间到达,那么只有一个信号会被设置为等待状态(信号只能在其取消屏蔽后会分发给进程)。如果一个信号已经在等待,和这个信号关联的 siginfo_t 结构(参考 sigaction(2))并不会被后面的同一信号的其他实例覆盖。因此,进程会接收到第一个达到的信号实例。

标准信号编码

        下表列出了各个标准信号的号码。在表中我们可以看出,一些信号在不同架构上的号码是不一样的。- 表示对应架构上没有该信号。

信号 x86/ARM 以及其他大多数 Alpha/SPARC MIPS PARISC 备注
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 9
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 - - -
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
SIGVTVLRM 26 26 28 20
SIGPROF 27 27 29 21
SIGWINCH 28 28 20 23
SIGIO 29 23 22 22
SIGPOLL
SIGPWR 30 29/- 19 19
SIGINFO - 29/- - -
SIGLOST - - - -
SIGSYS 31 12 12 31
SIGUNUSED 31 - - 31

        注意:

  •         如果有定义,那么 SIGUNUSED 和 SIGSYS 含义相同。但是 glibc 2.26 后,不管是什么架构上都没有 SIGUNUSED 定义了。
  • Alpha 上 29 号信号是 SIGINFO/SIGPWR(含义相同),而在 SPARC 上是 SIGLOST 信号。

实时信号

        从 Linux 2.2 开始,Linux 开始支持 POSIX.1b 实时扩展定义的实时信号(目前包含在 POSIX.1-2001 中)。支持的实时信号范围由宏 SIGRTMIN 和 SIGRTMAX 定义,POSIX.1-2001 要求至少支持 _POSIX_RTSIG_MAX(8) 个实时信号。

        Linux 内核支持 33 个不同的实时信号,号码是从 32 到 64。然而,glibc 的 POSIX 线程使用了 2个(对于 NPTL)或者 3 个(对于 LinuxThreads)实时信号(参考 pthreads(7)),所以调整 SIGRTMIN 为 34 或者 35。正因为实时信号的范围随着 glibc 多线程的实现不同而不同(这个在运行时会根据内核和 glibc 而定),并且事实上也会根据不同 UNIX 系统不同而不同,所以程序不应该使用硬编码值指定实时信号,而应该使用 SIGMIN+n 这种形式来引用实时信号,并且需要在运行时检查其是否超过了 SIGRTMAX。

        和标准信号不同,实时信号没有预定义的含义,即整个实时信号集都可以被应用使用。

        实时信号的默认处理行为是终止接收进程。

        实时信号通过以下方式来区分:

  • 实时信号的多个实例可以排队,这个和标准信号完全相反
  • 如果信号由 sigqueque(3) 发送,可以附带一个这个数值(可以是整数或者指针)。接收进程通过sigaction(2) 的 SA_SIGINFO 来建立该信号的处理函数,然后就可以通过处理函数第二个参数的 siginfo_t 结构的 si_value 来获得这个数值。并且可以通过结构的 si_pid 和 si_uid 可以获得发送信号进程的进程 ID 和用户 ID。
  • 实时信号的分发的顺序是能够得到保证的。同一个类型实时信号的多个实例会按照它们实际的发送顺序分发。多个信号发送到同一个进程,那么会从号码最小的信号依次分发(即号码越小,优先级越高。)相反,多个标准信号处于等待状态,先分发谁是没有定义的。

        如果一个进程有标准信号和实时信号一起处在等待状态,那么 POSIX 并没有指定应该先发送谁。Linux 以及很多其他实现会优先标准信号。

        根据 POSIX 规定,系统实现应该最少支持 _POSIX_SIGQUEQUE_MAX (32) 个实时信号等待,然而 Linux 却采用了不同的方式。Linux 2.6.7 之前,只暴漏了对所有进程总共实时信号数的系统级限制,这个限制可以通过(需要特权)/proc/sys/kernel/rtsig-max 文件查看或者修改。可以通过 /proce/sys/kernel/rtsig-nr 查看当前有多少个实时信号正在等待处理。在 Linux 2.6.8,/proc 接口被 RLIMIT_SIGPENDING 资源限制取代,这是一个每个用户下排队信号的限制,可以参考 setrlimit(2)。

        实时信号要求信号集结构(sigset_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)/waipid(2)
  • 套接字接口:accept(2)/connect(2)/recv(2)/recvfrom(2)/recvmmsg(2)/recvmsg(2)/send(2)/sendto(2)/sendmsg(2),除非套接字设置了超时(下面会提到)
  • 文件锁定接口:flock(2) 以及 fcntl(2) 的 F_SETLKW/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 以及相关 APIs
  • futex(2) FUTEX_WAIT_BITSET
  • POSIX 信号量语义接口:wem_wait(3)/sem_timedwait(3)(Linux 2.6.22 后,之前会失败返回 EINTR)
  • 从 inotify(7) 文件描述符的 read(2)(从 Linux 3.8 后,之前会返回 EINTR)

        下面接口在被信号处理函数打断时不会重启,无论是否设置 SA_RESTART 标记,会返回 EINTR 错误:

  • 使用 setsockopt(2) 设置了超时(SO_RCVTIMEO)的输入套接字接口:accept(2)/recv(2)/recvfrom(2)/recvmmsg(2)(同时有非空 timeout 参数,以及 recvmsg(2)
  • 使用 setsockopt(2) 设置了超时(SO_RCVTIMEO)的输出套接字接口:connect(2)/send(2)/sendto(2)/sendmsg(2)
  • 等待信号的接口:pause(2)/sigsuspend(2)/sigtimedwait(2)/sigwaitinfo(2)
  • 文件描述符多路复用接口:epoll_wait(2)/epoll_pwait(2)/poll(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)

        usleep(3) 在被信号处理函数打断时不会重启,会成功返回睡眠剩余时间的秒数。

        在一些情况下,sseccomp(2) 用户空间通知特性会导致通过 SA_RESTART 配置不会重启的系统调用重启,具体可以参考 seccomp_unotify(2)。

停止信号打断系统调用和库函数调用

        在 Linux 上,即使没有信号处理函数,一些阻塞接口也可能在收到停止信号停止在收到 SIGCONT 信号恢复时返回  EINTR 错误。这个行为并没有受到 POSIX.1 的限制,并不会在其他系统上发生。

        Linux 上会显示出这种行为的接口有:

  • 使用 setsockopt(2) 设置了超时(SO_RCVTIMEO)的输入套接字接口:accept(2)/recv(2)/recvfrom(2)/recvmmsg(2)(同时有非空 timeout 参数,以及 recvmsg(2)
  • 使用 setsockopt(2) 设置了超时(SO_RCVTIMEO)的输出套接字接口:connect(2)/send(2)/sendto(2)/sendmsg(2)
  • epoll_wait(2)/epoll_pwait(2)
  • semop(2)/semtimedop(2)
  • sigtimedwait(2)/gitwaitinfo(2)
  • Linux 3.7 前从 inotify(7) 文件描述符读取的 read(2)
  • Linux 2.6.21 前 futex(2) FUTEX_WAIT/sem_timedwait(3)/sem_wait(3)
  • Linux 2.6.8 前 msgrcv(2)/msgsnd(2)
  • Linux 2.4 前 nanosleep(2)

2.注意

        对于安全的异步信号函数,可以参考 sigal-safety(7)。

        /proc/pid/task/tid/status 文件包含了一些用于显示一个线程正处于阻塞(SigBlk)、捕获(SigCgt)或者忽略(SigIgn)的字段。(进程中所有线程的阻塞和忽略信号相同。)其他字段展示了发给线程(SigPnd)或者进程(ShdPnd)的等待信号。/proc/pid/status 展示了关于主线程的信息。参考 proc(5) 获得详细信息。

3.BUGS

        一共有 6 中关于硬件异常的信号:SIGBUS/SIGEMT/SIGFPE/SIGILL/SIGSEGV/SIGTRAP。至于这些信号哪些会真的分发,文档中并没有给出描述,并且也并不总是有效。

        比如,一个架构非法地址访问可能导致 SIGSEGV,但是在其他架构上可能会导致 SIGBUS,反过来也是一样。

        再比如,使用 x86 的 int 指令携带了禁止参数(除了 3 和 128 的任意数)会导致 SIGSEGV,尽管这时发送 SIGILL 会更合适,因为它是在向内核报告禁止操作。   

4.例程

        下面是一个 sigaction 的信号处理例程。

       #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);
       }

你可能感兴趣的:(计算机网络,信号处理,KILL,SIGNAL,SIGBUS)