【IPC 通信】信号处理接口 Signal API(3)

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

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


sigaction

遵循 POSIX.1 - 2008

1.库

标准 c 库,libc, -lc

2.头文件

<signal.h>

3.接口定义

       #include <signal.h>

       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

4.接口描述

       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 关联的行为,它可以是以下三个之一:

  • SIG_DFL,用来指定默认行为。
  • SIG_IGN,用来指定忽略该信号。
  • 一个指向信号处理函数的指针,这个函数的唯一参数是信号编号。

        如果 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)。剩下的部分可以是共用体,每个信号只读对其有意义的字段:

  • kill(2) 和 sigqueque(3) 发送的信号填写 si_pid 和 si_uid 字段。此外,sigqueque(3) 还会填写 si_int 和 si_ptr 字段为信号的发送者,可以参考 sigqueque(3) 查阅详细信息
  • POSIX.1b 定时器(Linux 2.6 以后)发送的信号会填充 si_overrun 和 si_timerid 字段。si_timerid 字段是内核内部用于识别定时器的标识符,它和 timer_create(2) 返回的 id 不同。si_overrun 字段是定时器超时计数器,这个和通过 timer_getoverrun(2) 获取信息差不多,这些字段不是标准的 Linux 扩展。
  • 消息队列通知信号(参考 mq_notify(3) 的 SIGEV_SIGNAL  描述)填充 si_int/si_ptr 字段,填充的值是 mq_notify(3) 提供的 sigev_value 值。si_pid 为消息发送方的进程 ID,si_uid 为发送方的真实用户 ID。
  • 信号 SIGCHLD 填充 si_pid、si_uid、si_status、si_utime 以及 si_stime 字段来提供关于子进程的信息。si_pid 是子进程的进程 ID,si_uid 是子进程的真实用户 ID。si_status 字段包含了子进程的退出信息(如果 si_code 是 CLD_EXITED),或者导致进程状态变化的信号编号。si_utime 和 si_stime 包含子进程使用的用户空间和系统的 CPU 时间,这些字段不包含 waited-for 子进程使用的时间(不像 getrusage(2) 和 times(2))。直到 Linux 2.6(自从 Linux 2.6.27),这些字段报告的 CPU 时间单位是 sysconf(_SC_CLK_TCK)。在 2.6.27 之前的 2.6 版本中,这些字段报告的时间单位为可配置的系统 jiffy 存在 bug。
  • SIGILL/SIGFPE/SIGSEGV/SIGBUS/SIGTRAP 使用错误地址填充 si_addr 字段。在一些架构上,这些信号也会填充 si_trapno 字段。SIGBUS 的一些子错误,尤其是 BUS_MCEERR_AO 和 BUS_MECERR_AR,也会填充 si_addr_lsb。这个字段指示了报告地址的最低有效位,也就是破坏的程度。比如,如果整个页面都破坏了,si_addr_lsb 包含 log2(sysconf(_SC_PAGESIZE)。当 ptrace(2) 事件(PTRACE_EVENT_foo)分发 SIGTRAP 信号后,si_addr 没有填充,而 si_pid 和 si_udi 填充了负责分发该信号对应的进程 ID 和 用户 ID。在 seccomp(2) 情况下,被 trace 方充当时间的分发方。BUS_MCEERR_* 和 si_addr_lsb 是 Linux 的扩展。SEGV_BNDERR 是 SIGSEGV 的字错误,它会填充 si_lower 和 si_upper 字段。SEGV_PKUERR 是 SIGSEGV 的字错误,填充 si_pkey 字段。
  • SIGIO/SIGPOLL(这两个在 Linux 是同义词)填充 si_band 和 si_fd。si_band 事件是一个位掩码,包含的值和 poll(2) 的 revents 字段填充的值相同。si_fd 字段知识了发生 I/O 崩溃事件的文件描述符。更多信息,可以参考 fcntl(2) 的F_SETSIG 信号。
  • SIGSYS,(Linux 3.5 后) seccomp 过滤器返回 SECCOMP_RET_TRAP 时会发生这个信号,这个信号会填充 si_call_addr 、si_syscall、si_arch、si_errno 以及 seccomp(2) 提到的其他字段。

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 以后,这些符号的大多数定义都可以在 <signal.h> 中看到,不过需要定义一些特性测试宏(在包含头文件之前):

  • _XOPEN_SOURCE 的值为 500 或者更大
  • _XOPEN_SOURCE 和 _XOPEN_SOURCE_EXTENDED
  • _POSIX_C_SOURCE 的值为 200809L 或者更大

        对于 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 用法演示。

5.返回值

        sigaction() 成功时返回 0。发生错误时,返回 -1,并设置 errno 来提示具体错误。

        错误值定义如下:

EFAULTact 或者 oldact 指向的内存不是进程地址空间中合法的地址
EINVAL指定了非法的信号。如果尝试修改 SIGKILL 或者 SIGSTOP 的行为,也会报告者错误,这两个信号无法被捕捉或者忽略

6.版本

       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()。

7.历史

        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 设置)。 

 8.注意

        通过 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 具有同样的结构。参考相关内核源码来详细了解,这个目前已经过时了。

9.BUGS

        当向 SA_SIGINFO 信号处理函数分发信号时,内核并不会在 siginfo_t 的字段内提供所有信号相关的信息。

        Linux 2.6.13 后,在 sa_flags 中指定 SA_NODEFER 不仅会在信号处理函数过程中屏蔽信号,也会屏蔽 sa_mask 中指定的信号。这个 bug 在 Linux 2.6.14 总修复了。

10.代码

        参考 mprotect(2)。

标记支持检测

        下面的程序会在检查到支持 SA_EXPOSE_TAGBITS 时以 EXIT_SUCCESS 状态退出,否则以 EXIT_FAILURE 状态退出。

       #include <signal.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>

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

下一篇 【IPC 通信】信号处理接口 Signal API(4)

  • 109
    点赞
  • 92
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 118
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 118
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

书香度年华

创作不易

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值