36、信号管理:从基础到高级

信号管理:从基础到高级

1. 信号集函数

在信号处理中,信号集是一个重要概念,它是一组信号的集合。有几个基础的信号集操作函数:
- sigaddset() :将指定信号 signo 添加到给定的信号集 set 中。成功时返回 0,失败返回 -1,同时 errno 会被设置为 EINVAL ,表示 signo 是无效的信号标识符。
- sigdelset() :从给定的信号集 set 中移除指定信号 signo 。返回值和错误处理与 sigaddset() 相同。
- sigismember() :检查指定信号 signo 是否在给定的信号集 set 中。如果存在返回 1,不存在返回 0,出错返回 -1, errno 同样会被设置为 EINVAL

除了这些 POSIX 标准的函数,Linux 还提供了一些非标准的信号集函数:

#define _GNU_SOURCE
#include <signal.h>
int sigisemptyset (sigset_t *set);
int sigorset (sigset_t *dest, sigset_t *left, sigset_t *right);
int sigandset (sigset_t *dest, sigset_t *left, sigset_t *right);
  • sigisemptyset() :判断信号集 set 是否为空,为空返回 1,否则返回 0。
  • sigorset() :将 left right 两个信号集的并集(按位或)存放在 dest 中。
  • sigandset() :将 left right 两个信号集的交集(按位与)存放在 dest 中。这两个函数成功返回 0,失败返回 -1, errno 会被设置为 EINVAL

这些非标准函数虽然有用,但如果程序需要遵循 POSIX 标准,应避免使用它们。

2. 信号阻塞

在程序执行过程中,有时需要暂时阻止某些信号的传递,这些被阻止的信号称为被阻塞信号。被阻塞的信号在阻塞期间不会被处理,直到阻塞解除。一个进程可以阻塞任意数量的信号,被阻塞信号的集合称为信号掩码。

POSIX 定义了一个用于管理进程信号掩码的函数:

#include <signal.h>
int sigprocmask (int how,
                 const sigset_t *set,
                 sigset_t *oldset);

sigprocmask() 的行为取决于 how 的值,它可以是以下标志之一:
- SIG_SETMASK :将调用进程的信号掩码设置为 set
- SIG_BLOCK :将 set 中的信号添加到调用进程的信号掩码中,即信号掩码变为当前掩码和 set 的并集(按位或)。
- SIG_UNBLOCK :从调用进程的信号掩码中移除 set 中的信号,即信号掩码变为当前掩码和 set 取反后的交集(按位与)。注意,解除未被阻塞的信号是非法操作。

如果 oldset 不为 NULL ,函数会将之前的信号集存放在 oldset 中。如果 set NULL ,函数会忽略 how ,不改变信号掩码,但会将当前信号掩码存放在 oldset 中,这是获取当前信号掩码的方法。

成功调用时返回 0,失败返回 -1, errno 可能被设置为 EINVAL (表示 how 无效)或 EFAULT (表示 set oldset 是无效指针)。需要注意的是,不允许阻塞 SIGKILL SIGSTOP 信号, sigprocmask() 会默默忽略任何将这两个信号添加到信号掩码的尝试。

下面是 sigprocmask() 操作的流程图:

graph TD;
    A[开始] --> B{set是否为NULL};
    B -- 是 --> C[忽略how,将当前信号掩码存于oldset];
    B -- 否 --> D{how的值};
    D -- SIG_SETMASK --> E[将信号掩码设为set];
    D -- SIG_BLOCK --> F[信号掩码变为当前掩码和set的并集];
    D -- SIG_UNBLOCK --> G[信号掩码变为当前掩码和set取反的交集];
    C --> H[返回0];
    E --> H;
    F --> H;
    G --> H;
    H --> I[结束];
    D -- 其他 --> J[返回-1,errno设为EINVAL];
    J --> I;
    B -- 指针无效 --> K[返回-1,errno设为EFAULT];
    K --> I;
3. 获取待处理信号

当内核产生一个被阻塞的信号时,该信号不会立即传递,这样的信号称为待处理信号。当待处理信号的阻塞被解除后,内核会将其传递给进程进行处理。

POSIX 定义了一个函数来获取待处理信号集:

#include <signal.h>
int sigpending (sigset_t *set);

成功调用时,会将待处理信号集存放在 set 中并返回 0,失败返回 -1, errno 会被设置为 EFAULT ,表示 set 是无效指针。

4. 等待一组信号

POSIX 还定义了一个函数,允许进程暂时更改其信号掩码,然后等待一个能终止进程或被进程处理的信号:

#include <signal.h>
int sigsuspend (const sigset_t *set);

如果信号终止了进程, sigsuspend() 不会返回。如果信号被处理, sigsuspend() 在信号处理函数返回后返回 -1,同时 errno 会被设置为 EINTR 。如果 set 是无效指针, errno 会被设置为 EFAULT

常见的 sigsuspend() 使用场景是获取在程序关键区域执行期间可能到达并被阻塞的信号。具体步骤如下:
1. 使用 sigprocmask() 阻塞一组信号,并将旧的信号掩码保存到 oldset 中。
2. 退出关键区域后,调用 sigsuspend() ,并将 oldset 作为参数传递给 set

5. 高级信号管理

早期使用的 signal() 函数功能比较基础,因为它是标准 C 库的一部分,对运行的操作系统能力假设较少,只能提供最基本的信号管理功能。而 POSIX 标准化了 sigaction() 系统调用,它提供了更强大的信号管理能力。

#include <signal.h>
int sigaction (int signo,
               const struct sigaction *act,
               struct sigaction *oldact);

sigaction() 用于改变指定信号 signo (除 SIGKILL SIGSTOP 外)的行为。如果 act 不为 NULL ,系统调用会按照 act 的指定改变信号的当前行为;如果 oldact 不为 NULL ,调用会将信号之前(或当前,如果 act NULL )的行为存放在 oldact 中。

sigaction 结构体的定义如下:

struct sigaction {
        void (*sa_handler)(int);   /* signal handler or action */
        void (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t sa_mask;          /* signals to block */
        int sa_flags;              /* flags */
        void (*sa_restorer)(void); /* obsolete and non-POSIX */
};
  • sa_handler :指定接收到信号时的操作,可以是 SIG_DFL (默认操作)、 SIG_IGN (忽略信号)或指向信号处理函数的指针,处理函数原型为 void my_handler (int signo);
  • sa_sigaction :当 sa_flags 中设置了 SA_SIGINFO 标志时,使用这个函数处理信号,其原型为 void my_handler (int signo, siginfo_t *si, void *ucontext);
  • sa_mask :指定在信号处理函数执行期间需要阻塞的信号集,这有助于避免多个信号处理函数之间的重入问题。除非 sa_flags 中设置了 SA_NODEFER 标志,否则当前正在处理的信号也会被阻塞。注意,不能阻塞 SIGKILL SIGSTOP 信号。
  • sa_flags :是一个位掩码,包含零个、一个或多个标志,用于改变信号的处理方式,常见的标志有:
  • SA_NOCLDSTOP :如果 signo SIGCHLD ,此标志指示系统在子进程停止或恢复时不提供通知。
  • SA_NOCLDWAIT :如果 signo SIGCHLD ,此标志启用自动子进程回收,子进程终止时不会变成僵尸进程,父进程无需(也不能)调用 wait()
  • SA_RESTART :启用 BSD 风格的系统调用重启,即被信号中断的系统调用会自动重启。
  • SA_RESETHAND :启用“一次性”模式,信号处理函数返回后,信号的行为会重置为默认行为。
  • sa_restorer :这个字段已经过时,在 Linux 中不再使用,也不属于 POSIX 标准,应忽略它。

sigaction() 成功时返回 0,失败返回 -1, errno 可能被设置为 EFAULT (表示 act oldact 是无效指针)或 EINVAL (表示 signo 是无效信号、 SIGKILL SIGSTOP )。

下面是 sigaction 结构体各字段的作用总结表格:
| 字段 | 作用 |
| ---- | ---- |
| sa_handler | 指定信号处理方式 |
| sa_sigaction | 当 SA_SIGINFO 标志设置时的信号处理函数 |
| sa_mask | 信号处理期间阻塞的信号集 |
| sa_flags | 改变信号处理方式的标志 |
| sa_restorer | 已过时,忽略 |

信号管理:从基础到高级

6. siginfo_t 结构体

siginfo_t 结构体定义在 <sys/signal.h> 中,它为信号处理函数提供了丰富的信息。其定义如下:

typedef struct siginfo_t {
        int si_signo;      /* signal number */
        int si_errno;      /* errno value */
        int si_code;       /* signal code */
        pid_t si_pid;      /* sending process's PID */
        uid_t si_uid;      /* sending process's real UID */
        int si_status;     /* exit value or signal */
        clock_t si_utime;  /* user time consumed */
        clock_t si_stime;  /* system time consumed */
        sigval_t si_value; /* signal payload value */
        int si_int;        /* POSIX.1b signal */
        void *si_ptr;      /* POSIX.1b signal */
        void *si_addr;     /* memory location that caused fault */
        int si_band;       /* band event */
        int si_fd;         /* file descriptor */
};

该结构体各字段的详细说明如下:
| 字段 | 说明 |
| ---- | ---- |
| si_signo | 信号的编号,在信号处理函数中,第一个参数也会提供该信息 |
| si_errno | 若不为零,表示与该信号关联的错误码,对所有信号都有效 |
| si_code | 解释进程为何以及从何处接收到该信号,不同信号有不同的有效取值 |
| si_pid | 对于 SIGCHLD 信号,是发送信号的进程的 PID |
| si_uid | 对于 SIGCHLD 信号,是发送信号的进程的真实 UID |
| si_status | 对于 SIGCHLD 信号,是进程的退出值或信号 |
| si_utime | 对于 SIGCHLD 信号,是进程消耗的用户时间 |
| si_stime | 对于 SIGCHLD 信号,是进程消耗的系统时间 |
| si_value | 信号的有效负载值,是 si_int si_ptr 的联合 |
| si_int | 对于通过 sigqueue() 发送的信号,是作为整数类型的有效负载 |
| si_ptr | 对于通过 sigqueue() 发送的信号,是作为指针类型的有效负载 |
| si_addr | 对于 SIGBUS SIGFPE SIGILL SIGSEGV SIGTRAP 信号,是导致错误的内存地址 |
| si_band | 对于 SIGPOLL 信号,是文件描述符的带外和优先级信息 |
| si_fd | 对于 SIGPOLL 信号,是操作完成的文件的文件描述符 |

需要注意的是,POSIX 仅保证前三个字段对所有信号都有效,其他字段应在处理相应信号时才进行访问。例如,只有当信号为 SIGPOLL 时,才应访问 si_fd 字段。

7. si_code 的奇妙世界

si_code 字段指示信号的产生原因。对于用户发送的信号,该字段表示信号的发送方式;对于内核发送的信号,该字段表示信号的发送原因。

以下是对任何信号都有效的 si_code 值:
- SI_ASYNCIO :信号因异步 I/O 完成而发送。
- SI_KERNEL :信号由内核产生。
- SI_MESGQ :信号因 POSIX 消息队列的状态变化而发送。
- SI_QUEUE :信号通过 sigqueue() 发送。
- SI_TIMER :信号因 POSIX 定时器到期而发送。
- SI_TKILL :信号通过 tkill() tgkill() 发送。
- SI_SIGIO :信号因 SIGIO 入队而发送。
- SI_USER :信号通过 kill() raise() 发送。

以下是仅对 SIGBUS 有效的 si_code 值,它们表示发生的硬件错误类型:
- BUS_ADRALN :进程发生对齐错误。
- BUS_ADRERR :进程访问了无效的物理地址。
- BUS_OBJERR :进程导致了其他类型的硬件错误。

对于 SIGCHLD 信号,以下值表示子进程产生信号的原因:
- CLD_CONTINUED :子进程停止后恢复。
- CLD_DUMPED :子进程异常终止。
- CLD_EXITED :子进程通过 exit() 正常终止。
- CLD_KILLED :子进程被杀死。
- CLD_STOPPED :子进程停止。
- CLD_TRAPPED :子进程触发了陷阱。

仅对 SIGFPE 有效的 si_code 值,解释了发生的算术错误类型:
- FPE_FLTDIV :进程执行了浮点除零操作。
- FPE_FLTOVF :进程执行了浮点溢出操作。
- FPE_FLTINV :进程执行了无效的浮点操作。
- FPE_FLTRES :进程执行的浮点操作产生了不精确或无效的结果。
- FPE_FLTSUB :进程执行的浮点操作导致下标越界。
- FPE_FLTUND :进程执行的浮点操作导致下溢。
- FPE_INTDIV :进程执行了整数除零操作。
- FPE_INTOVF :进程执行了整数溢出操作。

仅对 SIGILL 有效的 si_code 值,解释了非法指令执行的性质:
- ILL_ILLADR :进程尝试进入非法寻址模式。
- ILL_ILLOPC :进程尝试执行非法操作码。
- ILL_ILLOPN :进程尝试对非法操作数执行操作。
- ILL_PRVOPC :进程尝试执行特权操作码。
- ILL_PRVREG :进程尝试对特权寄存器执行操作。
- ILL_ILLTRP :进程尝试进入非法陷阱。

对于 SIGPOLL 信号,以下值表示产生信号的 I/O 事件:
- POLL_ERR :发生 I/O 错误。
- POLL_HUP :设备挂断或套接字断开连接。
- POLL_IN :文件有数据可供读取。
- POLL_MSG :有消息可用。
- POLL_OUT :文件可进行写入操作。
- POLL_PRI :文件有高优先级数据可供读取。

对于 SIGSEGV 信号,以下代码描述了两种无效内存访问类型:
- SEGV_ACCERR :进程以无效方式访问了有效内存区域,即违反了内存访问权限。
- SEGV_MAPERR :进程访问了无效的内存区域。

对于 SIGTRAP 信号,以下两个 si_code 值表示触发的陷阱类型:
- TRAP_BRKPT :进程触发了断点。

下面是 si_code 不同信号对应值的 mermaid 流程图:

graph LR;
    A[信号类型] --> B{SIGBUS};
    A --> C{SIGCHLD};
    A --> D{SIGFPE};
    A --> E{SIGILL};
    A --> F{SIGPOLL};
    A --> G{SIGSEGV};
    A --> H{SIGTRAP};
    A --> I[其他信号];
    B --> B1[BUS_ADRALN];
    B --> B2[BUS_ADRERR];
    B --> B3[BUS_OBJERR];
    C --> C1[CLD_CONTINUED];
    C --> C2[CLD_DUMPED];
    C --> C3[CLD_EXITED];
    C --> C4[CLD_KILLED];
    C --> C5[CLD_STOPPED];
    C --> C6[CLD_TRAPPED];
    D --> D1[FPE_FLTDIV];
    D --> D2[FPE_FLTOVF];
    D --> D3[FPE_FLTINV];
    D --> D4[FPE_FLTRES];
    D --> D5[FPE_FLTSUB];
    D --> D6[FPE_FLTUND];
    D --> D7[FPE_INTDIV];
    D --> D8[FPE_INTOVF];
    E --> E1[ILL_ILLADR];
    E --> E2[ILL_ILLOPC];
    E --> E3[ILL_ILLOPN];
    E --> E4[ILL_PRVOPC];
    E --> E5[ILL_PRVREG];
    E --> E6[ILL_ILLTRP];
    F --> F1[POLL_ERR];
    F --> F2[POLL_HUP];
    F --> F3[POLL_IN];
    F --> F4[POLL_MSG];
    F --> F5[POLL_OUT];
    F --> F6[POLL_PRI];
    G --> G1[SEGV_ACCERR];
    G --> G2[SEGV_MAPERR];
    H --> H1[TRAP_BRKPT];
    I --> I1[SI_ASYNCIO];
    I --> I2[SI_KERNEL];
    I --> I3[SI_MESGQ];
    I --> I4[SI_QUEUE];
    I --> I5[SI_TIMER];
    I --> I6[SI_TKILL];
    I --> I7[SI_SIGIO];
    I --> I8[SI_USER];

通过上述对信号管理相关函数、结构体和 si_code 的详细介绍,我们可以更深入地理解信号处理机制,从而在实际编程中更好地运用这些知识来实现复杂的信号管理功能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值