信号概念
信号是进程之间事件异步通知的⼀种⽅式,属于软中断。
生活中的信号
我们先拿「电话通知」来类比,帮你理解信号的本质:
- 你正在专心打游戏 / 写代码,就算不一直盯着手机,也知道 "手机响了 = 有通知",这就是你对「信号的识别」。
- 当手机真的响了(信号到达),你有三种处理方式:立刻接电话、挂断不理、或者等打完这局再接。
- 就算你暂时没处理,电话也不会消失,只是暂时处于 "待处理" 状态。
而且电话什么时候来是完全不确定的,你没法提前预知 ------ 这和 Linux 信号的核心特性完全对应。
初识信号
信号本质上是 Linux 中一种异步通知机制 ,可以理解为操作系统给进程发送的「紧急通知」,用来告诉进程某个事件发生了(比如用户按了Ctrl+C、进程出错了、其他进程要终止它)。
它有三个核心特性:
- 异步性:信号到来的时机是完全不确定的,进程无法预知什么时候会收到信号,就像你不知道电话什么时候响。
- 软中断:信号会打断进程当前的正常执行流程,转而去处理信号,处理完成后再回到原来的代码继续执行。
- 唯一标识 :每个信号都有一个唯一的编号和名称,内核和进程靠这些标识识别信号,比如
SIGINT编号是 2,SIGKILL编号是 9。

常见信号的就这几个:
| 信号名称 | 编号 | 触发场景 & 作用 |
|---|---|---|
SIGINT |
2 | 终端中断信号,按Ctrl+C时发送,默认终止进程 |
SIGKILL |
9 | 强制终止信号,俗称 "必杀信号",只能终止进程,无法被忽略 / 捕捉 |
SIGSTOP |
19 | 暂停进程信号,收到后进程会暂停执行,直到收到SIGCONT恢复,无法被忽略 / 捕捉 |
SIGSEGV |
11 | 段错误信号,进程访问非法内存(比如数组越界、访问空指针)时触发,默认终止进程 |
SIGUSR1/SIGUSR2 |
10/12 | 用户自定义信号,系统没有预设行为,完全由你自己定义处理逻辑,常用于进程间的自定义通知 |
SIGQUIT |
3 | 终端退出信号,按Ctrl+\发送,默认终止进程并生成core文件,方便调试 |
信号的产生
键盘产生信号
用户可以通过终端组合键,主动向前台进程发送信号,本质是终端驱动捕获按键,再转发信号给进程。
Ctrl+C→SIGINT:终止前台进程(你平时按Ctrl+C结束程序就是这个信号)Ctrl+Z→SIGTSTP:暂停前台进程,进程会被放到后台暂停Ctrl+\→SIGQUIT:终止进程,并生成core文件,方便调试错误
硬件异常
进程运行中出现硬件层面的错误,内核检测到后,会自动给进程发送对应信号。
- 除零错误 →
SIGFPE - 非法内存访问(比如段错误) →
SIGSEGV - 总线错误(访问未对齐的内存) →
SIGBUS
系统调用 / 库函数 / 命令
进程或用户可以通过系统调用、库函数或命令,主动发送信号:
kill命令 /kill()系统调用:向指定进程发送任意信号(比如kill -9 PID就是发送SIGKILL)raise():进程向自身发送信号alarm()/setitimer():定时器到期后,向进程发送SIGALRM信号abort():向自身发送SIGABRT信号,让进程异常终止- 其他场景:终端关闭时会发送
SIGHUP,子进程退出时会向父进程发送SIGCHLD
信号的保存
我们拿「上课铃」类比你面对信号时的三种选择:
- 知道铃响了,但先玩会儿再进教室(信号先记着,暂时不处理)
- 知道铃响了,但直接无视(信号被阻塞,暂时不响应)
- 知道铃响了,但不回教室,做别的事(自定义处理信号)
Linux 内核里,信号正是靠这三个结构来实现的:
1. 未决信号集(pending set)
- 作用:记录当前已经产生、但还没被进程处理的信号。
- 特点 :
- 每个信号在集合里对应一个位,信号产生时置
1,处理完成后清0。 - 标准信号(1~31)如果多次产生,只会记录一次(不排队);实时信号(34~64)会排队记录。
- 每个信号在集合里对应一个位,信号产生时置
- 对应生活例子:铃响了,但你先玩会儿再进教室,信号先被 "记着",还没处理。
2. 阻塞信号集(block set /mask)
- 作用:也叫「信号掩码」,表示哪些信号当前被进程 "阻塞" 了。
- 特点 :
- 被阻塞的信号,产生后会一直停留在未决状态,直到进程解除阻塞,才会被处理。
- 可以用
sigprocmask()系统调用,设置或读取阻塞信号集。
- 对应生活例子:铃响了,但你直接无视,暂时不处理,等解除 "无视" 状态再响应。
3. 处理函数(handler)
进程收到信号后,有三种处理方式,由内核里的handler决定:
1. 默认处理(SIG_DFL)
每个信号都有 Linux 系统预设的默认动作,大部分信号的默认处理分为这几类:
- 终止进程 :收到信号后直接杀死进程 例子:
SIGINT(按Ctrl+C触发)、SIGKILL(强制终止信号) - 忽略信号 :收到信号后什么都不做,直接无视 例子:
SIGCHLD(子进程退出时发给父进程的信号,默认被忽略) - 终止并生成
core dump文件 :杀死进程的同时,生成调试用的core文件,方便定位程序出错位置 例子:SIGSEGV(段错误)、SIGABRT(程序主动异常终止) - 暂停进程 :让进程暂停运行,进入后台休眠状态 例子:
SIGSTOP(强制暂停)、SIGTSTP(按Ctrl+Z暂停前台进程) - 继续运行 :让暂停的进程恢复执行 例子:
SIGCONT
2. 忽略处理(SIG_IGN)
- 注意:有两个信号永远不能被忽略,也不能被捕获 :
SIGKILL(9 号):强制终止进程,不管进程做了什么防御,都会被杀死SIGSTOP(19 号):强制暂停进程,进程无法绕过
- 可以通过
signal()或sigaction()系统调用,设置信号的忽略处理。 - 注册方式:用
signal()或更安全的sigaction()系统调用,把信号和自定义处理函数绑定。 - 关键注意事项:
- 信号处理函数里,必须使用异步信号安全的函数 (也叫可重入函数),比如
write()、sig_atomic_t类型的操作。 - 不能用
printf、malloc这类非安全函数,否则可能导致程序崩溃。
- 信号处理函数里,必须使用异步信号安全的函数 (也叫可重入函数),比如
- 类比:就像上课铃响了,你不回教室,反而去操场打球,就是 "自定义处理"。
3. 自定义处理(用户函数)
进程可以注册一个自己写的函数,当收到信号时,不执行默认动作,而是执行我们自定义的逻辑。
- 注册方式:用
signal()或更安全的sigaction()系统调用,把信号和自定义处理函数绑定。 - 关键注意事项:
- 信号处理函数里,必须使用异步信号安全的函数 (也叫可重入函数),比如
write()、sig_atomic_t类型的操作。 - 不能用
printf、malloc这类非安全函数,否则可能导致程序崩溃。
- 信号处理函数里,必须使用异步信号安全的函数 (也叫可重入函数),比如
- 类比:就像上课铃响了,你不回教室,反而去操场打球,就是 "自定义处理"。
关键点:
-
信号是否被处理,取决于它是否在未决集中且未被阻塞。
-
信号保存的本质就是内核在
task_struct(进程描述符)中维护的这两个信号集。
信号在内核中的表⽰⽰意图:

信号集
信号从产生到被处理,会经历两个关键状态:未决(pending) 和 阻塞(blocked) ,内核通过维护两个「信号集」来管理这些状态。信号集本质上是一个位图(bit array),每个位对应一个信号,用来记录信号的状态。
信号集的核心概念
我们可以把信号集理解成「信号的开关清单」,每个信号在清单里都有一个专属的位置,用来标记它的状态:
1. 未决信号集(pending set)
- 内核为每个进程单独维护的信号集,记录当前已经产生但尚未被处理的信号。
- 规则:当一个信号产生时,对应位被置为
1;当信号被处理后,对应位会被清为0。 - 例子:进程收到
SIGINT信号,但此时信号被阻塞,那么它会一直保持未决状态,直到阻塞解除。
2. 阻塞信号集(blocked set / 信号屏蔽字)
- 也叫「信号屏蔽字」,决定当前进程是否阻塞某些信号。
- 规则:如果一个信号被阻塞,它仍然可以正常产生,但会一直停留在未决状态,直到进程解除对它的阻塞,才会被处理。
- 注意:
SIGKILL(9 号)和SIGSTOP(19 号)这两个信号永远不能被阻塞或忽略,也不能被捕获。
信号集操作函数
我们可以通过系统提供的函数,自由创建和修改信号集,常见的操作函数如下:
cpp
// 1. 清空信号集:将所有位设置为0(不包含任何信号)
int sigemptyset(sigset_t *set);
// 2. 填满信号集:将所有位设置为1(包含系统支持的所有信号)
int sigfillset(sigset_t *set);
// 3. 添加信号:向信号集中添加指定信号,对应位设置为1
int sigaddset(sigset_t *set, int signum);
// 4. 删除信号:从信号集中移除指定信号,对应位设置为0
int sigdelset(sigset_t *set, int signum);
// 5. 判断信号是否在集合中:存在返回1,不存在返回0
int sigismember(const sigset_t *set, int signum);
补充说明:
- 所有函数成功返回
0,失败返回-1,可以用perror()查看错误原因。 - 这些函数是后续设置信号屏蔽字(
sigprocmask)的基础,比如你想阻塞SIGINT,就需要先创建一个信号集,再用sigaddset把SIGINT加进去。
sigset_t 的底层实现
sigset_t 是信号集的专用类型,在大多数 POSIX 系统(包括 Linux)中,它的底层是一个位掩码数组:
- 系统支持的信号数量可能超过单个
unsigned long的位数(通常是 64 位),所以用一个结构体来管理多个unsigned long元素,每个元素的每一位代表一个信号。 - 简化的底层实现示例(来自 glibc 源码):
cpp
/* 简化的 sigset_t 实现 */
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct {
// 用多个unsigned long数组表示所有信号位
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
这样的设计可以覆盖 Linux 的所有信号(标准信号 1-31、实时信号 32-64,以及扩展信号),保证信号集的兼容性和扩展性。
设置阻塞集
sigprocmask()
这个函数用来修改进程的阻塞信号集(信号屏蔽字),控制哪些信号会被进程阻塞。
cpp
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
how:SIG_BLOCK(添加阻塞)、SIG_UNBLOCK(解除阻塞)、SIG_SETMASK(直接设置) -
oldset:返回旧的阻塞集(可为 NULL)
简单使用示例
临时阻塞 SIGINT(Ctrl+C)信号:
cpp
sigset_t block_set;
sigemptyset(&block_set); // 初始化空信号集
sigaddset(&block_set, SIGINT); // 把 SIGINT 加入集合
// 把 SIGINT 添加到进程的阻塞集
sigprocmask(SIG_BLOCK, &block_set, NULL);
获取未决信号集
sigpending()
这个函数用来获取当前进程的未决信号集,也就是那些已经产生、但因被阻塞而还没被处理的信号。
cpp
int sigpending(sigset_t *set);
参数说明
set:函数执行后,会把当前进程的未决信号集存入这个变量,我们可以用sigismember判断某个信号是否处于未决状态。
简单使用示例
检测 SIGINT 是否处于未决状态:
cpp
sigset_t pending_set;
sigpending(&pending_set); // 获取当前未决信号集
// 判断 SIGINT 是否在未决集合中
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT 信号处于未决状态!\n");
}
信号的捕捉
进程可以注册一个自定义函数来处理某个信号,即 "捕捉" 该信号。当信号递达时,内核会中断进程的正常执行流程,转而执行信号处理函数,处理完成后再继续执行原代码。
使用 signal() 函数
cpp
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:要捕捉的信号编号(如SIGINT)handler:处理方式,可指定:SIG_IGN:忽略信号SIG_DFL:执行默认处理- 用户自定义函数地址
- 特点:用法简单,但不可靠,不推荐在复杂场景使用。
使用 sigaction() 函数
cpp
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数
sigset_t sa_mask; // 处理期间额外阻塞的信号集
int sa_flags; // 标志位(如SA_SIGINFO启用扩展信息)
void (*sa_restorer)(void); // 已废弃
};
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能更强大、更可靠,是推荐的信号捕捉方式。
- 支持精细控制:
- 通过
sa_mask在信号处理期间自动阻塞其他信号 - 通过
SA_SIGINFO标志获取信号发送方的详细信息
- 通过
oldact可用于保存旧的信号处理方式,后续恢复使用。
信号的处理
信号的处理方式分为三个层次:
1. 默认处理
每个信号都有系统预设的默认动作,常见的分为这几类:
- Term(终止进程) :收到信号后直接结束进程,如
SIGINT(Ctrl+C)、SIGTERM - Core(终止并生成 core 文件) :结束进程的同时生成调试用的 core 文件,方便定位错误,如
SIGSEGV(段错误)、SIGABRT - Ign(忽略信号) :收到信号后直接无视,不做任何处理,如
SIGCHLD(子进程退出通知)、SIGURG - Stop(暂停进程) :让进程暂停执行,进入后台休眠,如
SIGSTOP、SIGTSTP(Ctrl+Z) - Cont(恢复进程) :让暂停的进程恢复运行,如
SIGCONT
2. 忽略处理
进程可以主动设置,收到信号后直接忽略,不执行任何动作。
- 实现方式:
signal(signum, SIG_IGN)sigaction中设置sa_handler = SIG_IGN
3. 自定义处理
进程可以注册一个自定义函数,收到信号后,不执行默认动作,转而执行我们自己写的处理逻辑。
⚠️ 重要注意事项
- 异步信号安全 :信号处理函数中,只能调用「异步信号安全」的函数(如
write),禁止使用printf、malloc这类非安全函数,否则可能导致程序崩溃。 - 可重入性:信号处理函数执行时,可能会被其他信号中断,需要保证代码可重入。
- 原子访问 :如果在信号处理中使用全局变量,必须声明为
volatile sig_atomic_t,保证读写的原子性,避免数据异常。
综合代码示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// 自定义信号处理函数:必须用异步信号安全的函数(如write)
void sig_handler(int signo) {
char msg[] = "✅ Caught signal! 信号已被处理\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main() {
// 1. 初始化sigaction结构体,注册信号处理函数
struct sigaction act;
act.sa_handler = sig_handler; // 指定自定义处理函数
sigemptyset(&act.sa_mask); // 信号处理期间不额外阻塞其他信号
act.sa_flags = 0; // 默认标志
sigaction(SIGUSR1, &act, NULL); // 注册SIGUSR1信号的处理方式
// 2. 创建信号集,阻塞SIGUSR1信号
sigset_t newmask, oldmask, pendmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGUSR1); // 把SIGUSR1加入要阻塞的集合
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
perror("sigprocmask 阻塞失败");
exit(1);
}
printf("🔒 已阻塞SIGUSR1信号,现在发送信号会进入未决状态\n");
// 3. 给当前进程发送SIGUSR1信号(此时信号会被阻塞,进入未决状态)
printf("📨 正在给当前进程发送SIGUSR1信号...\n");
raise(SIGUSR1); // 等价于 kill(getpid(), SIGUSR1);
// 4. 获取当前未决信号集,检测SIGUSR1是否处于未决状态
sigpending(&pendmask);
if (sigismember(&pendmask, SIGUSR1)) {
printf("⚠️ 检测到SIGUSR1处于未决状态(被阻塞,还没处理)\n");
}
// 5. 解除SIGUSR1的阻塞,信号会立刻被处理
printf("\n🔓 解除SIGUSR1阻塞,信号将被处理...\n");
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
perror("sigprocmask 解除阻塞失败");
exit(1);
}
// 6. 再次检测未决信号集,确认信号已被处理
sigpending(&pendmask);
if (!sigismember(&pendmask, SIGUSR1)) {
printf("✅ 再次检测:SIGUSR1已不在未决集合中,处理完成\n");
}
// 7. 让程序保持运行,方便你手动发送信号测试
printf("\n程序将继续运行,你可以在终端用 `kill -USR1 %d` 手动发送信号\n", getpid());
while (1) {
sleep(1);
}
return 0;
}

代码解释:
- 进程首先通过
sigaction注册了SIGUSR1信号的自定义处理函数,定义好收到信号后的行为。 - 接着通过
sigprocmask阻塞SIGUSR1,此时收到该信号不会被立即处理。 - 进程通过
raise()向自己发送SIGUSR1,信号因被阻塞进入未决状态,sigpending检测到它的存在。 - 解除阻塞后,内核立即递送
SIGUSR1,执行我们注册的自定义处理函数,打印信号捕获信息。 - 信号处理完成后,程序继续运行,进入循环等待状态,你可以用
kill命令手动发送信号,再次触发自定义处理。
中断
硬件中断
硬件中断是硬件设备(如键盘、硬盘、网卡、定时器)主动向 CPU 发出的电信号,用来通知 CPU:「我有一件事需要你处理」。
核心特点
- 异步性:中断的发生与 CPU 当前执行的指令无关,随时可能发生,CPU 无法提前预知。
- 优先级:中断有高低之分,高优先级中断可以打断低优先级中断的处理,优先被响应。
- 目的:避免 CPU 不断轮询设备状态,让 CPU 在设备空闲时做其他工作,大幅提高系统效率。

硬件中断全流程
- 外设准备就绪(如键盘按下按键、硬盘读写完成),通过中断控制器向 CPU 发送中断信号。
- 中断控制器汇总外设发来的中断号,通知 CPU 有中断需要处理。
- CPU 在每条指令执行结束后,检查中断请求线:
- 若 CPU 的中断标志位(IF)为允许状态,CPU 会响应中断;
- 首先保护当前现场(保存寄存器、程序计数器等状态),防止被打断的程序丢失数据。
- CPU 根据中断号,找到对应的中断处理程序(如键盘输入处理、磁盘读写回调),执行处理逻辑。
- 中断处理完成后,CPU 恢复之前保存的现场,回到被打断的程序,继续执行原来的指令。
什么是中断向量表?
由于外部设备和 CPU 自身都会触发不同的中断 / 异常信号,CPU 拿到这些信号后,需要知道该调用哪个处理程序。因此操作系统提前为每一种中断信号分配了唯一的编号(中断向量号),并把对应的处理程序地址按顺序存储在一张表中,这就是中断向量表(IDT,Interrupt Descriptor Table)。
-
0~31 号向量:预留给 CPU 内部异常 用于处理 CPU 运行时的故障,比如除零错误、页错误、非法指令、栈溢出等,这类异常是 CPU 自身触发的,不是外部设备的请求。
-
32~255 号向量:用于外部硬件中断和系统服务
- 外部硬件中断(IRQ):键盘、硬盘、网卡、定时器等设备的中断请求,比如时钟中断(IRQ0)通常映射到向量 32 或更高。
- 系统服务 / 系统调用:32 位 Linux 中,系统调用使用
int 0x80指令触发,对应的向量号是 128;64 位 Linux 改用syscall指令,不再依赖 IDT,但早期系统调用是基于中断向量实现的。
当硬件中断发生时:
- CPU 根据中断向量号,在中断向量表(IDT)中定位对应的门描述符。
- 通过门描述符,找到内核中提前注册的中断处理入口(如
common_interrupt),进入内核态执行完整的中断处理流程(也就是我们之前讲过的 "保护现场→处理中断→恢复现场" 的过程)。
时钟中断
问题:
- 进程可以在操作系统的指挥下,被调度、被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
- 外部设备可以触发硬件中断,但是这需要用户或者设备自己触发,有没有自己可以定期触发的设备?
CPU 内部有个时钟中断源,会每隔一段时间进行时钟中断,它就像操作系统的 "心跳",驱动着操作系统中一切与时间相关的核心功能。
问题:因为操作系统本质是一个死循环进程,这就意味着操作系统可以躺平了吗?
回答: 不能,因为 CPU 内部的时钟中断会驱动操作系统在对应的中断向量表中执行对应的服务,强制让操作系统 "动起来"。
补充说明: 这就是为什么我们经常说 CPU 主频越快,计算机越快,因为主频越高,时钟中断触发的频率就越高,操作系统被唤醒、执行管理工作的频率也就越快,整个系统动得也就越快了,而主频就相当于是时钟中断的 "心跳频率"。
💡 补充:时钟中断是操作系统能实现多任务调度的关键 ------ 它会定期打断当前进程,让系统切换任务,实现 "多个进程同时运行" 的效果。
软中断
- 上述外部硬件中断,需要硬件设备触发。
- 有没有可能,因为软件原因,也触发上面的逻辑?有!
- 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int或者syscall),可以让CPU内部触发中断逻辑。

问题:
用户层怎么把系统调用号给操作系统?-寄存器(比如EAX)
操作系统怎么把返回值给用户?-寄存器或者用户传入的缓冲区地址
系统调用的过程,其实就是先int Ox80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
系统调用号的本质:数组下标!
软中断的执行时机
软中断不会立即执行,而是在以下时机被内核检查并调度:
1.硬件中断处理程序返回前
在执行完所有上半部并准备返回时,内核会调用 do_softirq() 处理当前 CPU 上挂起的软中断。这是最常见的执行路径。
2.内核线程 ksoftirqd 中
如果软中断的负载很重,以至于在上一步中处理不完,或者系统长时间没有中断触发,内核会唤醒每个 CPU 上的 ksoftirqd 内核线程来继续处理软中断。ksoftirqd 是一个低优先级的线程,在进程上下文中运行,但执行时仍然禁止抢占,且不能休眠。
3.显式调用 do_softirq()
某些内核代码(如网络子系统的某些路径)可能会主动检查并执行软中断,以降低延迟。
同时Linux的C标准库把这些系统调用的函数全部封装了,我们通过调用C标准库其实也间接调用了底层的系统调用,进行了软中断。
信号处理函数的安全性
常见的异步信号安全函数包括:
write、read、open 等系统调用
_exit、_Exit、abort
signal、sigaction(部分情况)
等等
不安全函数:
printf、malloc、free、fopen、getc 等标准 I/O 和堆操作函数
因为可能被信号中断导致死锁或数据不一致
多个信号同时到达
标准信号(1-31)不支持排队:如果同一信号在未决期间再次产生,只会被记录一次。
实时信号(34-64)支持排队,会依次递送。
用户态和内核态
·内存中不仅有普通进程的各种数据,那有没有底层系统层面相关的数据?肯定有
·那普通进程能访问吗?不能访问
·那如何区分运行级别?用户态和内核态
用户态和内核态的区别
1.特权级划分
内核态(Ring 0):拥有最高权限,可以执行所有 CPU 指令,访问所有内存地址(包括硬件设备、页表等)。
用户态(Ring 3):权限受限,不能直接执行 I/O 指令、关闭中断、操作页表等敏感操作,只能访问自身进程的用户空间。
2.内存空间隔离
用户态:每个进程拥有独立的虚拟地址空间(通常 3GB 或 128TB 等),进程间互不干扰。
内核态:所有进程共享一个内核空间(位于虚拟地址的高位部分),内核态代码可以访问整个系统内存。
3.切换成本
从用户态陷入内核态(通过系统调用、中断、异常)需要保存上下文、切换栈、检查权限等,有一定开销;反之返回亦然。
信号处理的时机
信号通常是在进程从内核态返回用户态时(例如系统调用结束、中断处理完毕)被检查并处理。如果进程被阻塞在某个系统调用上,信号的到来可能使系统调用返回错误(EINTR),需要程序员手动处理重试。
主要检查时机
(1)系统调用返回。当进程通过系统调用(如 read、write、open)陷入内核,完成工作后准备返回用户空间时,内核会检查 task_struct 中的信号位图,如果有未处理的信号(且未被阻塞),则会在返回用户态之前处理信号。
(2)中断处理返回。硬件中断(如时钟中断、磁盘中断)发生后,CPU 进入内核态执行中断服务程序。当中断处理完成,准备返回被中断的上下文时,如果被中断的是用户态进程,内核同样会检查信号并处理。
(3)异常处理返回。当 CPU 遇到异常(如缺页异常、除零错误)陷入内核,异常处理程序结束后返回用户态时,也会检查信号。
(4)主动调度返回。当进程主动调用 schedule() 让出 CPU,被再次调度运行时,如果是从内核态切换到用户态(比如 ret_from_fork 路径),也会检查信号。
(5)信号处理函数返回。当信号处理函数执行完毕后,通过特殊的系统调用(如 sigreturn)返回到被中断的用户代码前,内核会再次检查是否有新的信号挂起,形成可能的信号嵌套。
信号处理的具体流程(以中断返回为例)
进程运行在用户态,被硬件中断打断。
CPU 切换到内核态,保存用户态上下文(pt_regs),执行中断处理程序(上半部 + 下半部)。
中断处理结束,内核准备执行 iret 返回用户态前,调用 do_signal() 函数检查当前进程的 pending 信号集。
如果有信号需要处理:
内核在用户栈上构造一个 信号帧(包含信号处理函数的地址、被中断的上下文等信息)。
修改 pt_regs 中的 RIP(指令指针)为信号处理函数的入口地址,修改栈指针指向信号帧。
当 iret 执行后,CPU 就会"返回"到用户态的信号处理函数中执行。
信号处理函数执行完毕后,通过 sigreturn 系统调用再次陷入内核,内核恢复之前保存的 pt_regs,再次 iret 回到原被中断的代码继续执行。
