信号管理:从基础到高级
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
的详细介绍,我们可以更深入地理解信号处理机制,从而在实际编程中更好地运用这些知识来实现复杂的信号管理功能。
超级会员免费看

被折叠的 条评论
为什么被折叠?



