Linux系统:进程信号的处理
系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、保存信号
- 1-1 sigset_t
- 1-2 信号集操作函数
- 1-3 sigprocmask
- 1-4 sigpending
- 1-5 sigaction
- 1-6 volatile
前言
学习进程的信号是因为信号是 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
的作用就是 禁止编译器优化,保证每次访问变量都直接读写内存。