系列文章目录
前言
学习进程的信号是因为信号是 Linux 中 进程和内核之间异步通信的重要机制。它可以用来控制进程(如 Ctrl+C 发送 SIGINT 终止程序)、实现进程间事件通知(如子进程退出触发 SIGCHLD)、支持定时器和调试(如 alarm()、gdb 的断点),并帮助程序优雅处理错误和安全退出(如捕获 SIGSEGV、SIGTERM)。掌握信号能让我们理解操作系统如何管理进程,写出健壮的系统级程序,并为并发与网络编程打下基础。
一、保存信号
在学习本篇之前需要学习进程信号的产生明白信号是如何产生的
在task_struct中存在三个数据结构pending和block和handler,当我们发送一个信号后,信号会依次在这三个数据结构中显示,至于怎么显示,我们来详细讲解一下。
在task_struct中pending和block和handler类似于三张表

三者结构和功能
Handler(信号处理函数表)
- 定义:每个信号在内核里都有一个对应的处理动作(默认、忽略、自定义函数)。
- 底层实现:
- 在 内核进程控制块
(task_struct)里,有一张 信号动作表。 - 这张表本质上是一个“数组”或“哈希表”,下标是信号编号,值是一个
struct sigaction结构。
- 在 内核进程控制块
struct sigaction {
void (*sa_handler)(int); // handler 指针: SIG_DFL / SIG_IGN / 自定义函数
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; // 处理信号时临时阻塞的信号集合
int sa_flags; // 标志位 (SA_RESTART 等)
};
- 所以
Handler实际上是:- 数组形式的映射表(每个信号编号 → 一个
struct sigaction结构)。 - 每个信号只能对应一个
handler。
- 数组形式的映射表(每个信号编号 → 一个
Block(阻塞信号集合)
- 定义:告诉内核“哪些信号现在不要立即处理,先等一等”。
- 底层实现:
- 用
位图(bitmask)表示集合。 - 每一位对应一个信号编号,
1表示阻塞,0表示不阻塞。
- 用
- 存放位置:
- 每个进程的
PCB(task_struct)里有一个blocked字段,类型就是sigset_t
- 每个进程的
typedef struct {
unsigned long __val[_SIGSET_NWORDS]; // 位数组
} sigset_t;
- 例如:
- 阻塞 SIGINT (2) → 位图第 2 位 = 1
- 阻塞 SIGQUIT (3) → 位图第 3 位 = 1
Pending(挂起信号集合)
- 定义:信号已经送到了,但由于在
Block集合里,暂时不能处理 → 放到Pending集合里“排队”。 - 底层实现:
- 用
位图(bitmask)表示集合 - 存在进程控制块
task_struct的signal_pending里。
- 用
- 特点:
- 一个信号挂起只会在位图上标
1,不会排队累积(大部分信号只记录一次,不是FIFO队列)。 - 实时信号
(SIGRTMIN ~ SIGRTMAX)例外,它们可以有队列。
- 一个信号挂起只会在位图上标
当一个信号发送给进程时,它会先进入 pending 集合,在位图上标记为 1。如果这个信号没有被阻塞(block mask 里是 0),内核会立刻把它递送给进程,并调用对应的 handler;执行完后,pending 位会被清0。如果这个信号被阻塞 (block mask 里是 1),那么它会留在 pending 集合中,不会被递送。对普通信号来说,不管来多少次,pending 只会保持为 1;对实时信号来说,pending 位=1 的同时,还会把每次信号都排队。当进程解除阻塞后,内核会立即把 pending 里的信号递送给 handler,然后再清除 pending 位。
1-1 sigset_t
sigset_t 是 Linux 系统里定义的一种 数据类型,用来表示一组信号。它本质上是一个 位图(bitmap),里面的每一位对应一个信号(比如 SIGINT、SIGKILL 等)
- 如果某一位是
1,就表示这个信号处于某种“有效”状态; - 如果是
0,就表示这个信号处于“无效”状态。 - 在
未决信号集(pending)里,某位为1表示这个信号已经来了但还没处理 - 在
阻塞信号集(block / signal mask)里,某位为1表示这个信号被阻塞,不能递送给进程
定义
在源码里,它通常是个结构体或整型数组,比如在 glibc 里:
typedef struct {
unsigned long __val[_SIGSET_NWORDS];
} sigset_t;
也就是说,它其实就是一堆 long 类型的数组,每个 bit 对应一个信号。
1-2 信号集操作函数
你可以把 sigset_t 想象成一个黑盒子,里面就是一张“信号表”,但我们看不到内部结构。系统不让我们直接在里面乱涂乱改,而是给了一些“官方函数”,比如“清空”“添加”“删除”“查询”,我们只能按这些按钮来修改它。
- 推荐的方式 是通过系统提供的库函数来操作,比如:
sigemptyset(&set);
sigfillset(&set);
sigaddset(&set, SIGINT);
sigdelset(&set, SIGINT);
sigismember(&set, SIGINT);
各自的作用
- sigemptyset(sigset_t *set)
- 清空一个信号集,把所有位都置为 0。
- 表示“集合里没有任何信号”。
- sigfillset(sigset_t *set)
- 填满一个信号集,把所有位都置为 1。
- 表示“集合里包含所有信号”。
- sigaddset(sigset_t *set, int signo)
- 向信号集里 添加一个指定信号。
- 例如:sigaddset(&set, SIGINT) → 把 SIGINT 加入集合。
- sigdelset(sigset_t *set, int signo)
- 从信号集里 删除一个指定信号。
- 例如:sigdelset(&set, SIGINT) → 把 SIGINT 从集合中去掉。
- sigismember(const sigset_t *set, int signo)
- 判断一个信号是否在集合里。
上述函数都是成功返回 1 ,失败返回0,-1 表示出错
1-3 sigprocmask
sigprocmask的作用就是 修改或查询当前进程的信号屏蔽字(signal mask,也就是阻塞信号集)成功返回 0,失败返回 -1
函数原型:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
-
how:决定如何修改进程的信号屏蔽字,取值有三种:- SIG_BLOCK:把 set 里指定的信号 加入到当前屏蔽字(阻塞它们)。
- SIG_UNBLOCK:把 set 里指定的信号 从屏蔽字中移除(解除阻塞)。
- SIG_SETMASK:用 set 直接替换当前屏蔽字(覆盖原来的)。
-
set:指向一个 sigset_t,表示要修改的信号集- 如果传 NULL,就表示不修改屏蔽字,只查询。
-
oldset:如果不为 NULL,则把修改前的旧屏蔽字保存到这里。- 常用于“保存原来的状态,稍后恢复”。
演示代码:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void print_sigset(const sigset_t*set)
{
for(int i=0;i<31;i++)
{
if(sigismember(set,i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void show_pending()
{
sigset_t pending;
sigpending(&pending);
printf("当前pending信号:");
print_sigset(&pending);
}
void show_block()
{
sigset_t block;
sigprocmask(SIG_BLOCK,NULL,&block);
printf("当前block信号:");
print_sigset(&block);
}
int main()
{
sigset_t set,oldset;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigprocmask(SIG_BLOCK,&set,&oldset);
printf("SIGINT已经被阻塞,按Ctrl+C多次试试...\n");
for(int i=0;i<5;i++)
{
show_pending();
show_block();
sleep(3);
}
sigprocmask(SIG_SETMASK,&oldset,NULL);
printf("SIGINT已恢复,可以被捕捉/递送了,再按Ctrl+C试试...\n");
for(int i=0;i<5;i++)
{
show_pending();
show_block();
sleep(3);
}
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
SIGINT已经被阻塞,按Ctrl+C多次试试...
当前pending信号:1000000000000000000000000000000
当前block信号:1010000000000000000000000000000
^C
当前pending信号:1010000000000000000000000000000
当前block信号:1010000000000000000000000000000
...
当前pending信号:1000000000000000000000000000000
当前block信号:1000000000000000000000000000000
- 我们在程序里把进程的
block集合里把SIGINT (编号2)加进去,相当于把2 号位置成了1。这样,当我们按Ctrl+C触发SIGINT时,这个信号并不会马上交给进程处理,而是会被放到pending集合里,对应的2号位就变成了1。 - 接着,当程序运行到后面,我们把
block集合里的2号位清零(也就是允许SIGINT递送)。这时候,内核会立刻把pending里的SIGINT交给进程的handler去处理。等到handler执行完,对应的pending的2号位就会被清零。最后程序继续往下运行,直到结束。
1-4 sigpending
sigpending 用来查看当前进程有哪些信号处在 pending(未决)状态。
函数原型:
#include <signal.h>
int sigpending(sigset_t *set);
参数说明:
set:传出参数,函数会把当前进程的未决信号集写到 set 里,成功返回 0,失败返回 -1
演示代码:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
printf("捕获到信号 %d\n",signo);
}
int main()
{
signal(SIGINT,handler);
sigset_t blockset;
sigemptyset(&blockset);
sigaddset(&blockset,SIGINT);
sigprocmask(SIG_BLOCK,&blockset,NULL);
printf("请按Ctrl+C(SIGINT)\n");
sleep(5);
sigset_t pendingset;
sigpending(&pendingset);
if(sigismember(&pendingset,SIGINT))
{
printf("SIGINT信号在pending集合中!\n");
}
else
{
printf("SIGINT信号不在pending集合中!\n");
}
sigprocmask(SIG_UNBLOCK,&blockset,NULL);
printf("接触阻塞,SIGINT会立即递达handler\n");
sleep(2);
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
请按Ctrl+C(SIGINT)
^C
SIGINT信号在pending集合中!
捕获到信号 2
接触阻塞,SIGINT会立即递达handler
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
请按Ctrl+C(SIGINT)
SIGINT信号不在pending集合中!
接触阻塞,SIGINT会立即递达handler
程序先阻塞 SIGINT,按下 Ctrl+C,SIGINT 会进入 pending 集合,sigpending 检查到 SIGINT 在 pending 中,解除阻塞后,信号立即传递到 handler
1-5 sigaction
sigaction 是比 signal 更强大、更可控的信号处理接口,用来 检查或修改某个信号的处理方式。在 Linux编程里,推荐使用 sigaction 来替代 signal,成功:0,失败:-1
函数原型:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
参数说明:
signum:要操作的信号编号,比如 SIGINT、SIGTERM 等
act:指定新的处理方式(传入一个 struct sigaction 结构体指针)
oldact:如果不为 NULL,会把之前的处理方式保存到这里 -
struct sigaction 结构体:
struct sigaction {
void (*sa_handler)(int); // 处理函数(简化版)
void (*sa_sigaction)(int, siginfo_t *, void *); // 处理函数(带更多信息)
sigset_t sa_mask; // 处理函数执行期间要屏蔽的信号
int sa_flags; // 行为标志,比如 SA_SIGINFO
};
参数解析:
sa_handler:普通信号处理函数,参数是信号编号sa_sigaction:带扩展信息的处理函数,可以获取信号发送者的 PID、UID 等,需要配合 SA_SIGINFO 使用sa_mask:在处理信号时临时阻塞的信号集sa_flags:行为标志,比如:SA_RESTART:被信号中断的系统调用会自动重启SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler
演示代码:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
printf("捕获到信号:%d\n",signo);
}
int main()
{
struct sigaction act;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
if(sigaction(SIGINT,&act,NULL)==-1)
{
perror("sigaction");
return 1;
}
printf("运行中...请按Ctrl+C\n");
while(1)
{
sleep(1);
}
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
运行中...请按Ctrl+C
^C捕获到信号:2
^C捕获到信号:2
这里我们设置了一个自定义的信号处理,不过进程是死循环的。
1-6 volatile
volatile 是 C/C++ 里的一个 类型修饰符,它主要用来告诉编译器:不要对这个变量进行优化,每次都要从内存里重新读取它的值。
- 为什么要这样做?
一般情况下,编译器会为了优化性能,把变量的值放在寄存器里缓存住,不会每次都去内存读。但有些场景下,变量的值可能会被程序以外的东西修改,如果没有volatile,编译器可能会误以为变量值没变,从而导致逻辑出错。
演示代码:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1; // 在信号处理函数里改变量
}
int main() {
signal(SIGINT, handler);
while (!flag) {
// 如果 flag 不是 volatile,可能优化成死循环
}
printf("收到信号,退出\n");
return 0;
}
flag 不是 volatile,可能优化成死循环,但是这个程序并不是一个死循环,当我们发送SIGINT信号后循环需要结束。
volatile 的作用就是 禁止编译器优化,保证每次访问变量都直接读写内存。

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



