认识信号的3张表,理解信号的捕捉与保存
一、理解信号的几个概念
- 实际执行信号的处理动作称为信号递达 (Delivery)
- 信号从产生到递达之间的状态,称为信号未决 (Pending(待处理))。
- 进程可以选择 **阻塞 (Block)**某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
二、信号在内核中的表示

信号在内核中的表示示意图(task_struct 关联的三个表):
block(阻塞表):标记信号是否被阻塞pending(未决表):标记信号是否已产生但未递达- **
handler(**处理动作表):标记信号的处理方式(默认 / 忽略 / 自定义函数)
示例说明:
- **
SIGHUP(1):**未阻塞也未产生过,递达时执行默认处理动作。 - **
SIGINT(2):**已产生但被阻塞,暂不能递达;即使处理动作是忽略,未解除阻塞前仍无法执行。 SIGQUIT(3): 未产生过,一旦产生会被阻塞,处理动作是自定义函数sighandler。
补充规则:如果在进程解除对某信号的阻塞之前,这种信号产生多次:
- POSIX.1 允许系统递送该信号一次或多次。
- Linux 实现:常规信号在递达前产生多次只计一次;实时信号可依次排队(本章不讨论实时信号)。
cpp
// 内核结构 2.6.18
struct task_struct {
...
/* signal handlers */
struct sighand_struct *sighand;
sigset_t blocked;
struct sigpending pending;
...
};
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
struct __new_sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};
struct k_sigaction {
struct __new_sigaction sa;
void __user *ka_restorer;
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending {
struct list_head list;
sigset_t signal;
};
三、对三张表的操作
1. sigset_t
- 每个信号只有一个 bit 的未决标志(0/1,不记录产生次数),阻塞标志同理。
- 未决和阻塞标志可用相同数据类型 sigset_t 存储,该类型称为信号集 ,用每个 bit 表示信号的 "有效 / 无效" 状态:
- 阻塞信号集(信号屏蔽字):bit 为 1 表示该信号被阻塞
- 未决信号集:bit 为 1 表示该信号处于未决状态
- 类比权限中的
umask。
2.信号集操作函数
sigset_t 类型的 bit 存储方式依赖系统实现,用户只能通过以下函数操作,不能直接打印解释:
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号。
函数 sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 置位,表示 该信号
集的有效信号包括系统支持的所有信号。
注意,在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 - 1。
sigismember 是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回 1, 不包含则返回 0, 出错返回 - 1。
3. sigprocmask
- 功能:读取或更改进程的信号屏蔽字(阻塞信号集)。
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 返回值:成功返回 0,出错返回 - 1。
- 参数说明:
- 若
oset非空:原信号屏蔽字会通过oset传出。 - 若
set非空:根据how参数更改信号屏蔽字。 - 若
oset和set都非空:先备份原屏蔽字到oset,再按set和how修改。
- 若
表格
| how 参数 | 作用 | 等价操作 | |
|---|---|---|---|
| SIG_BLOCK | 将 set 中的信号添加到当前屏蔽字 |
mask |= set | |
| SIG_UNBLOCK | 从当前屏蔽字中解除 set 中的信号 |
mask &= ~set |
|
| SIG_SETMASK | 将当前屏蔽字直接设为 set 的值 |
mask = set |
- 补充(细节):若调用 sigprocmask 解除了对未决信号的阻塞,函数返回前至少会递达其中一个信号。
4 sigpending
- 功能:读取当前进程的未决信号集 ,通过
set参数传出。
cpp
#include <signal.h>
int sigpending(sigset_t *set);
返回值:成功返回 0,出错返回 - 1。
5. 使用上面的函数写一个小demo
阻塞2号信号
先把2号信号(ctrl + c)阻塞了,然后发送2号信号,将pending表打印出来,观察pending表的中的信号未决
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending(const sigset_t &pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
int main()
{
// 1.屏蔽2号信号
sigset_t block_set, odd_set;
sigemptyset(&block_set);
sigemptyset(&odd_set);
sigaddset(&block_set, SIGINT);
int n = sigprocmask(SIG_SETMASK, &block_set, &odd_set);
(void)n;
while(true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.获取pending表
int n = sigpending(&pending);
(void)n;
// 3. 打印pending表
PrintPending(pending);
sleep(1);
}
return 0;
}
现象:

可以看到,在pending表中,2号信号是未决状态(待处理状态)
把所有信号都阻塞了
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending(const sigset_t &pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
int main()
{
// 1.屏蔽2号信号
sigset_t block_set, odd_set;
sigemptyset(&block_set);
sigemptyset(&odd_set);
for(int i = 0; i <= 31; i++)
sigaddset(&block_set, i);
int n = sigprocmask(SIG_SETMASK, &block_set, &odd_set);
(void)n;
while(true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.获取pending表
int n = sigpending(&pending);
(void)n;
// 3. 打印pending表
PrintPending(pending);
sleep(1);
}
return 0;
}


可以看到,9号信号,和19号信号,是无法被阻塞的。
四、信号捕捉
1.当block表的阻塞信号被取消后,pending表是在handler调用前恢复成0,还是在handler调用后恢复成0?
简单验证:
可以在handler方法内部,继续获取pending表,打印观察,如果pending表的对应信号位还是1,就说明是在调用后恢复成0,反之,在调用前恢复成0.
代码:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending(const sigset_t &pending)
{
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << "------------------entery handler----------" << std::endl;
sigset_t pending;
sigemptyset(&pending);
// 2.获取pending表
int n = sigpending(&pending);
(void)n;
std::cout << "处理完成" << signo << std::endl;
PrintPending(pending);
std::cout << "------------------leave handler----------" << std::endl;
}
int main()
{
std::cout << "我的pid:" << getpid() << std::endl;
// 0. 捕捉2号信号
signal(2, handler);
// 1.屏蔽2号信号
sigset_t block_set, odd_set;
sigemptyset(&block_set);
sigemptyset(&odd_set);
for (int i = 0; i <= 31; i++)
sigaddset(&block_set, i);
int n = sigprocmask(SIG_SETMASK, &block_set, &odd_set);
(void)n;
int cnt = 0;
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.获取pending表
int n = sigpending(&pending);
(void)n;
// 3. 打印pending表
PrintPending(pending);
if (cnt == 20)
{
int n = sigprocmask(SIG_SETMASK, &odd_set, nullptr);
(void)n;
}
cnt++;
sleep(1);
}
return 0;
}
运行结果:

可以看到,是在调用handler前就恢复成0了
2.信号捕捉的流程:

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
- 用户程序定义了
SIGQUIT信号的处理函数sighandler。 - 当前正在执行
main函数,这时发生中断或异常切换到内核态。 - 在中断处理完毕后要返回用户态的
main函数之前检查到有信号SIGQUIT递达。 - 内核决定返回用户态后不是恢复
main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。- 如果没有新的信号要递达 ,这次再返回用户态就是恢复
main函数的上下文继续执行了。

3.sigaction函数
cpp
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
cpp
The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
**sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0, 出错则返回 - 1。**signo 是指定信号的编号。若 act 指针非空,则根据 act 修改该信号的处理动作。若 oact 指针非空,则通过 oact 传出该信号原来的处理动作。
act 和 oact 指向 sigaction 结构体 :将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号,赋值为常数 SIG_DFL 表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为 void, 可以带一个 int 参数(相当于signal),通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被 main 函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(block表中,该信号0->1) ,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,**则用 sa_mask 字段说明这些需要额外屏蔽的信号,**当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags 字段包含一些选项,本章的代码都把 sa_flags 设为 0,sa_sigaction 是实时信号的处理函数。
代码样例:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void handler(int signo)
{
std::cout << "捕捉一个信号: " << signo << std::endl;
sigset_t pending;
while(true)
{
sigpending(&pending);
for(int i = 31; i >= 1; i--)
{
if(sigismember(&pending, i))
{
std::cout << "1";
}
else
std::cout << "0";
}
sleep(1);
std::cout << std::endl;
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&(act.sa_mask));
sigaddset(&(act.sa_mask), 3);
sigaddset(&(act.sa_mask), 4);
sigaddset(&(act.sa_mask), 5);
act.sa_flags = 0;
act.sa_restorer = nullptr;
sigaction(SIGINT, &act, &oact);
while(true)
{
std::cout << "进程在运行" << getpid() << std::endl;
sleep(1);
}
return 0;
}
运行结果: 
