上一篇文章在介绍完信号的产生和保存后,我们现在对信号有了一个基本的认识,信号由键盘、系统调用、硬件异常、软件条件等方式产生,然后被保存在三张表中,再将信号递达,操作系统有三种处理方式:默认处理、忽略处理、自定义处理。虽然我们现在已经知道这三种处理方式,但是操作系统底层是到底怎么做到在合适的时候处理信号?合适的时候又是什么时候呢?下面我们就来深入了解。


1. 信号处理函数注册
// 用户程序注册SIGQUIT信号的处理函数
signal(SIGQUIT, sighandler);signal() 或 sigaction() 系统调用,告诉内核:"当收到 SIGQUIT 信号时,请调用我的 sighandler 函数"。
task_struct->sighand->action[SIGQUIT] 中。
2. 正常执行与信号产生
main 函数中的代码。
3. 内核态到用户态的返回检查
4. 信号递达与处理框架构建
如果发现有待处理的信号,且其处理方式是用户自定义函数,内核不会简单地返回原来的用户态执行上下文,而是:
sighandler 的地址。
sighandler 函数。
5. 执行信号处理函数
main 函数,而是开始执行 sighandler。
sighandler 与 main 函数使用不同的栈帧,它们是两个独立的控制流。
6. 信号处理函数返回
sighandler 执行到 return 语句时,并不是返回到 main 函数中被中断的地方。
sigreturn()(这通常由 C 库自动处理,对程序员透明)。
7. 恢复原始上下文
sigreturn() 系统调用再次进入内核态。
8. 返回正常执行
main 函数的上下文。
1. 内核栈与用户栈的协作
内核需要精心管理两种栈:
sighandler 在执行时使用的栈。
内核会在用户栈上构建一个特殊的帧(Frame),使得信号处理函数能够正常执行并在返回时调用 sigreturn。
2. sigreturn 系统调用的重要性
sigreturn 是信号处理机制中的关键环节,它的作用是:
3. 信号处理函数的限制
由于信号处理函数的执行上下文特殊,它受到很多限制:
write,但不能调用 printf、malloc 等)

切换点 | 方向 | CPU特权级 | 触发机制 | 目的 |
|---|---|---|---|---|
1 (用户→内核) | 系统调用/中断 | 3→0 | 硬件中断指令 | 进入内核处理事件 |
2 (内核→用户) | 执行handler | 0→3 | 内核修改CS:EIP | 安全执行用户代码 |
3 (用户→内核) | 调用sigreturn | 3→0 | 软中断(int 0x80) | 返回内核恢复上下文 |
4 (内核→用户) | 恢复主程序 | 0→3 | 恢复原CS:EIP | 继续执行被中断代码 |
💡 为什么需要四次切换? 若直接从内核态执行用户函数:
sigaction 是 Linux/UNIX 系统中用于精细控制信号行为的核心系统调用,相比传统的 signal() 函数,它提供了更强大的功能(如信号屏蔽、附加数据传递)和更可靠的行为。
函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数说明
signum:要操作的信号编号(如 SIGINT, SIGTERM 等)
act:指向 struct sigaction 的指针,包含新的信号处理配置
NULL,则不改变信号的处理方式
oldact:指向 struct sigaction 的指针,用于保存信号先前的处理配置
NULL,则不保存旧配置
返回值
0
-1 并设置 errno
struct sigaction 结构体这是 sigaction 函数的核心,它包含了信号处理的完整配置:
struct sigaction {
void (*sa_handler)(int); // 简单的信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 高级信号处理函数
sigset_t sa_mask; // 在执行处理函数期间要阻塞的信号集
int sa_flags; // 修改信号行为的标志位
void (*sa_restorer)(void); // 已废弃,不应使用
};关键字段详解
1. 信号处理函数选择
sa_handler 和 sa_sigaction 实际上是同一个联合体的不同字段,只能使用其中一个:
sa_handler:简单的信号处理函数,只接收信号编号作为参数
void handler(int sig) {
// 处理信号
}sa_sigaction:高级信号处理函数,接收更多信息
void handler(int sig, siginfo_t *info, void *ucontext) {
// 可以访问更多关于信号的信息
}使用哪个函数由 sa_flags 中的 SA_SIGINFO 标志决定。
2. sa_mask - 执行处理函数期间阻塞的信号
sa_mask 中的信号添加到进程的阻塞信号集3. sa_flags - 行为标志位
通过位掩码组合来修改信号行为,常用标志包括:
标志 | 说明 |
|---|---|
SA_SIGINFO | 使用 sa_sigaction 而不是 sa_handler 作为处理函数 |
SA_RESTART | 被信号中断的系统调用自动重启(推荐设置) |
SA_NOCLDSTOP | 如果 signum 是 SIGCHLD,当子进程停止时不接收通知 |
SA_NOCLDWAIT | 如果 signum 是 SIGCHLD,不创建僵尸进程 |
SA_NODEFER | 在执行处理函数期间不自动阻塞当前信号(不推荐) |
SA_RESETHAND | 信号处理完成后重置为默认动作(类似 signal() 的不可靠行为) |
siginfo_t 结构体(当使用 SA_SIGINFO 时)当设置了 SA_SIGINFO 标志时,信号处理函数可以接收更多信息:
siginfo_t {
int si_signo; // 信号编号
int si_errno; // 错误号(如果有)
int si_code; // 信号来源代码
pid_t si_pid; // 发送信号的进程ID
uid_t si_uid; // 发送信号的进程的真实用户ID
void *si_addr; // 导致错误的内存地址(对于SIGSEGV等)
int si_status; // 退出值或信号(对于SIGCHLD)
// ... 其他字段
}signal() 的区别特性 | signal() | sigaction() |
|---|---|---|
可移植性 | 不同系统行为不一致 | POSIX 标准,行为一致 |
控制精度 | 有限 | 精细控制 |
信号阻塞 | 自动阻塞当前信号 | 可自定义阻塞信号集 |
系统调用重启 | 依赖具体实现 | 可通过 SA_RESTART 明确控制 |
信号信息 | 只能获取信号编号 | 可获取详细信息(siginfo_t) |
推荐程度 | 已过时,不推荐在新代码中使用 | 现代程序的首选 |
void handler(int signum)
{
std::cout << "hello signal: " << signum << std::endl;
while(true)
{
//不断获取pending表!
sigset_t pending;
sigpending(&pending);
for(int i = 31; i >= 1; i--)
{
if(sigismember(&pending, i))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
sleep(1);
}
exit(0);
}
int main()
{
struct sigaction act, oldact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
act.sa_flags = 0;// 默认信号处理逻辑
sigaction(SIGINT, &act, &oldact); // 对2号信号进行捕捉
while(true)
{
std::cout << "I am a process, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}将3号和4号信号加入到阻塞表中,使用 sigaction 来捕获 SIGINT 信号,第一次捕获2号信号时,会执行我们自定义函数handler,所以第一次捕获到2号信号时,pending表中2号信号并不处于未决状态,因为已经在执行自定义函数了,然后会在handler中死循环获取pending表,所以我们后续不断发送2号,3号和4号信号时,都会被屏蔽,也就是阻塞住,那么pending表中这些信号就会因为阻塞处于未决状态。
运行结果:

总结:
在Linux信号处理机制中,当进程捕获到某个信号并触发其处理函数时,内核会自动执行以下重要操作:
补充说明:
什么是硬件中断?
硬件中断是外部设备(如键盘、磁盘、网卡)向CPU发出的信号,表示需要处理某个事件或请求服务。这是一种异步事件,可以在任何时候发生,打断CPU当前正在执行的任务,其核心目的是避免CPU轮询外设造成的资源浪费。
基本概念
触发原理: 当设备完成操作(如磁盘读取结束)或状态变化(如按键按下),通过 中断控制器(如8259A) 向CPU发送中断请求(IRQ)。
硬件协作流程:

📌 关键设计:CPU仅在指令边界检查中断,确保指令原子性。
中断向量表(IDT) 是操作系统启动时加载到内存的数据结构,实现中断号到处理程序的映射。
1. 核心组成与初始化
组件 | 作用 | Linux 0.11示例 |
|---|---|---|
中断门(Interrupt Gate) | 处理外部硬件中断,自动禁用中断响应 | set_intr_gate(0x24, rs1_interrupt)(串口中断) |
陷阱门(Trap Gate) | 处理内部异常(如除零错误),允许嵌套中断 | set_trap_gate(14, &page_fault)(缺页异常) |
中断屏蔽寄存器 | 控制中断使能状态 | outb(inb_p(0x21) & \~0x01, 0x21)(开启时钟中断) |
2. 中断号解析机制
&timer_interrupt)//Linux内核0.11源码
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱⻔。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。
outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt);// 设置并⾏⼝的陷阱⻔。
}
void rs_init (void)
{
set_intr_gate (0x24, rs1_interrupt); // 设置串⾏⼝1 的中断⻔向量(硬件IRQ4信号)。
set_intr_gate (0x23, rs2_interrupt); // 设置串⾏⼝2 的中断⻔向量(硬件IRQ3信号)。
init (tty_table[1].read_q.data); // 初始化串⾏⼝1(.data 是端⼝号)。
init (tty_table[2].read_q.data); // 初始化串⾏⼝2。
outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯⽚的IRQ3,IRQ4 中断信号请求。
}完整处理流程图:

一、中断触发阶段:从请求到CPU感知
1. 中断请求的产生
int 0x80系统调用)。
2. CPU的响应条件
CPU仅在同时满足以下条件时响应中断:
IF=1(x86的STI指令开启)。📌 关键细节:x86架构中,
NMI(不可屏蔽中断)无视IF标志,用于处理硬件故障等紧急事件。
二、硬件自动操作:上下文保存与跳转
1. 现场保护(由CPU微码自动完成)
当CPU决定响应中断时,硬件依次执行:

⚠️ 此过程无需软件介入,由CPU硬件直接完成。
IF标志(CLI等效操作),防止同级中断干扰。
2. 跳转至中断处理程序
🔧 操作系统角色:启动时初始化IDT,例如Linux 0.11绑定时钟中断:
set_intr_gate(0x20, &timer_interrupt); // 0x20向量→timer_interrupt三、中断服务阶段:操作系统的接管与处理
1. 保存完整上下文(操作系统责任)
ISR首先保存所有可能被破坏的寄存器,确保主程序状态无损:
; x86示例
pushad ; 保存通用寄存器
push ds
push es ; 保存段寄存器此步骤由操作系统编写,需覆盖所有架构相关寄存器。
2. 中断服务程序(ISR)的核心逻辑
设备交互:
0x60)。通知中断控制器: 发送EOI(End of Interrupt)信号:
outb(0x20, 0x20); // x86 8259A的EOI唤醒关联任务:
若中断关联进程(如磁盘I/O完成),调用wake_up()唤醒等待队列。
触发软中断(可选):
将耗时操作移交软中断线程(如Linux的ksoftirqd)。
3. 中断嵌套管理
允许嵌套的条件:
sti(); // 显式开启中断,允许高优先级中断抢占需谨慎控制嵌套深度,避免栈溢出。
嵌套现场保护: 每次嵌套需独立保存上下文,形成中断栈帧链。
四、中断返回:恢复与继续执行
1. 恢复现场
弹出寄存器:
pop es
pop ds
popad ; 恢复通用寄存器执行返回指令:
iret指令自动从栈中恢复IP、CS、PSW。
2. 返回主程序
CPU根据恢复的PC值继续执行被中断的指令流,如同未发生中断。
一张图总结:

• 操作系统自己被谁指挥,被谁推动执行?
答案:操作系统被时钟中断指挥和推动执行。
操作系统不是一个主动的"管理者",而是一个被动的"响应者"。它通过时钟中断这个规律性的心跳来获得执行机会,从而进行调度、管理和维护工作。
• 有没有可以定期触发的设备?
答案:有,这就是系统定时器/时钟芯片。
计算机中有专门的硬件定时器(如8253/8254 PIT或HPET),它们能够以固定的频率产生中断信号,这就是时钟中断的来源。
系统定时器硬件
定时器工作原理
定时器芯片被编程为以特定频率(如100Hz或1000Hz)生成中断信号。这意味着每秒会产生100次或1000次时钟中断。
// 在sched_init()中设置时钟中断
void sched_init(void) {
// ...
set_intr_gate(0x20, &timer_interrupt); // 设置时钟中断处理程序
outb(inb_p(0x21) & ~0x01, 0x21); // 允许时钟中断(IRQ0)
// ...
}// 汇编代码:timer_interrupt
_timer_interrupt:
push %ds
push %eax
movl $0x10, %eax
mov %ax, %ds
movb $0x20, %al
outb %al, $0x20 # 向8259A发送EOI(中断结束)信号
movl $0, %eax
incl %eax
movl %eax, jiffies # 更新系统时钟滴答计数
pushl $0x10 # 参数:CPL(当前特权级)
call _do_timer # 调用C函数处理定时任务
popl %eax
popl %eax
pop %ds
iret// kernel/sched.c
void do_timer(long cpl) {
extern int beepcount;
extern void sysbeepstop(void);
// 更新系统时间
if (beepcount)
if (!--beepcount)
sysbeepstop();
// 更新当前进程时间片
if (--current->counter > 0)
return;
// 时间片用完,需要重新调度
current->counter = 0;
schedule(); // 调用调度程序
}// kernel/sched.c
void schedule(void) {
int i, next, c;
struct task_struct **p;
// 寻找就绪状态且counter值最大的进程
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break; // 找到了可运行的进程
// 所有进程的时间片都已用完,重新分配时间片
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
// 切换到选中的进程
switch_to(next);
}时钟中断的完整工作流程

时间片(Time Quantum)
counter值相关
counter减1
counter减到0时,进程被剥夺CPU使用权
优先级与时间片分配
// 重新计算时间片的公式
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;这个公式确保:
调度算法演进
原始轮转调度(Linux 0.11):
if (timer++ % 2 == 0) pcb = pcb_A; // 简单交替执行
else pcb = pcb_B; 现代O(1)调度器(Linux 2.6+):
使用active/expired双队列避免遍历
时间片耗尽时移入expired队列:
if (--current->time_slice <= 0)
move_to_expired_queue(current); 在前面章节进程调度时,我们对O(1)调度有做过详细介绍
时钟中断不仅是进程调度的触发器,还负责:
1. 系统时间维护
通过jiffies变量记录系统启动后的时钟滴答数,维护系统时间。
2. 内核统计信息更新
更新系统负载统计、进程运行时间统计等。
3. 定时器处理
处理内核和进程设置的各种定时器(timer)。
4. 资源监控
监控系统资源使用情况,必要时进行回收或调整。
一张图总结:

如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!
void main(void) {
// 初始化阶段:设置整个系统的基础设施
mem_init(); // 内存管理初始化
trap_init(); // 中断向量表初始化
blk_dev_init(); // 块设备初始化
sched_init(); // 调度器初始化
// ... 其他初始化工作
// 创建init进程(用户空间的第一个进程)
if (!fork()) {
init();
}
// 主循环:操作系统的"休息"状态
for (;;) {
pause(); // 等待中断发生
}
}这个死循环的真正含义
pause() 系统调用会让CPU进入低功耗状态
注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没有的话我们就回到这⾥,⼀直循环执⾏'pause()'。
// pause() 的简化实现
int pause(void) {
// 将当前进程状态设为可中断睡眠
current->state = TASK_INTERRUPTIBLE;
// 调用调度器,选择其他进程运行
schedule();
// 当信号到达时,从这里恢复执行
return -EINTR;
}为什么使用 pause() 而不是空循环?
方式 | CPU使用率 | 功耗 | 响应速度 |
|---|---|---|---|
空循环(while(1);) | 100% | 高 | 即时 |
pause() | 接近0% | 低 | 依赖中断响应时间 |
示例:时钟中断触发调度
// 当时钟中断发生时
void timer_interrupt(void) {
// 更新系统时间
jiffies++;
// 减少当前进程时间片
current->counter--;
// 如果时间片用完或需要调度
if (current->counter <= 0) {
schedule(); // 进程调度
}
}在Linux内核的早期版本(如0.11版)中,任务调度机制有一个特殊设计:通常情况下,当任务调用pause()系统调用时,该任务会进入等待状态,直到接收到信号才会返回就绪运行态。但任务0(即内核启动后创建的第一个任务)是一个特例。
具体来说:
pause()会使任务进入可中断的等待状态(TASK_INTERRUPTIBLE)pause()实现不同:它不会真正进入等待状态这种设计的原因:
这种机制在内核代码中的具体实现可以参考schedule()函数中的特殊处理逻辑,它会明确检查当前任务是否是任务0,并做出不同的调度决策。
上述外部硬件中断,需要硬件设备通过特定信号线(如IRQ线)触发。例如,当键盘按键被按下时,键盘控制器会通过中断请求线向CPU发送电信号,CPU检测到后会暂停当前任务处理中断。
有没有可能,因为软件原因,也触发上面的逻辑?有!这被称为"软件中断"或"陷阱中断"。这种中断不是由外部设备产生,而是由CPU执行特定指令主动触发。常见的场景包括:
为了让操作系统支持进行系统调用,CPU厂商设计了专门的汇编指令:
与硬件中断的对比
特性 | 硬件中断 | 软中断(系统调用) | 设计意义 |
|---|---|---|---|
触发源 | 外设(键盘、磁盘等) | 程序指令(int 0x80) | 用户主动请求内核服务 |
可预测性 | 随机性高 | 精确控制触发时机 | 保障程序逻辑完整性 |
权限切换 | 自动进入内核态 | 通过指令显式切换 | 安全隔离用户与内核空间 |
软中断的触发原理
int 0x80 或 sysenter 指令引发CPU异常,向量号128(0x80)对应系统调用入口 。svc(Supervisor Call)指令实现同等效果 。通过寄存器传递(以x86架构为例)
EAX寄存器是传递系统调用号的主要寄存器:
; 示例:调用write系统调用
mov eax, 4 ; 将write的系统调用号存入EAX
mov ebx, 1 ; 第一个参数:文件描述符(stdout)
mov ecx, buffer ; 第二个参数:缓冲区地址
mov edx, length ; 第三个参数:数据长度
int 0x80 ; 触发软中断不同架构的寄存器使用
架构 | 系统调用号寄存器 | 参数寄存器 | 调用指令 |
|---|---|---|---|
x86 (32位) | EAX | EBX, ECX, EDX, ESI, EDI, EBP | int 0x80 |
x86-64 | RAX | RDI, RSI, RDX, R10, R8, R9 | syscall |
ARM | R7 | R0-R6 | svc 0 |
通过寄存器返回简单结果
对于简单的返回值(如整数、指针),操作系统通过EAX/RAX寄存器返回:
// 系统调用处理函数示例
asmlinkage long sys_getpid(void) {
return current->pid; // 返回值通过EAX传递给用户程序
}通过用户提供的缓冲区返回复杂数据
对于复杂数据结构或大量数据,操作系统将数据写入用户提供的缓冲区:
// 读取文件数据的系统调用
asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count) {
// ... 从文件读取数据
copy_to_user(buf, kernel_buffer, bytes_read); // 将数据复制到用户空间
return bytes_read; // 返回实际读取的字节数
}错误处理
系统调用通过两种方式报告错误:
-errno
// 用户程序中的错误处理
int fd = open("file.txt", O_RDONLY);
if (fd == -1) {
// open系统调用返回-1,标准库设置errno
perror("open failed"); // 输出"open failed: No such file or directory"
}系统调用表结构
系统调用号确实是数组下标,指向系统调用表中的函数指针:
// 系统调用函数指针表
fn_ptr sys_call_table[] = {
sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid,
// ... 更多系统调用
};系统调用分发
当用户程序执行系统调用时,内核使用系统调用号作为索引:
// 通过系统调用号索引函数指针表
call [sys_call_table + eax * 4]这里:
eax 包含系统调用号
eax * 4 计算偏移量,找到对应的系统调用处理函数
步骤1:用户层准备
// 用户程序调用write()
write(fd, buffer, count);
// 标准库将其转换为系统调用
mov eax, 4 ; SYS_write = 4
mov ebx, fd ; 文件描述符
mov ecx, buffer ; 缓冲区地址
mov edx, count ; 字节数
int 0x80 ; 触发软中断内核处理流程
步骤2:进入内核态
; 系统调用入口点(kernel/system_call.s)
_system_call:
; 1. 验证系统调用号有效性
cmp eax, nr_system_calls-1
ja bad_sys_call
; 2. 保存用户态寄存器
push ds
push es
push fs
push edx
push ecx
push ebx
; 3. 设置内核数据段
mov edx, 0x10
mov ds, dx
mov es, dx
; 4. 调用对应的系统调用处理函数
call [sys_call_table + eax * 4]
; 5. 保存返回值
push eax步骤3:执行系统调用处理函数
// write系统调用的实现(fs/read_write.c)
int sys_write(unsigned int fd, char *buf, int count) {
// ... 实际的文件写入逻辑
return bytes_written; // 返回写入的字节数
}步骤4:返回用户态
; 恢复上下文并返回
pop eax ; 获取系统调用返回值
pop ebx ; 恢复寄存器
pop ecx
pop edx
pop fs
pop es
pop ds
iret ; 返回到用户空间更多Linux0.11内核源码:
// sys.h
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write (); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close (); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link (); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice (); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
// 调度程序的初始化⼦程序。
void sched_init(void)
{
...
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
}
_system_call:
cmp eax,nr_system_calls-1 ;// 调⽤号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
push ds ;// 保存原段寄存器值。
push es
push fs
push edx ;// ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。
push ecx ;// push %ebx,%ecx,%edx as parameters
push ebx ;// to the system call
mov edx,10h ;// set up ds,es to kernel space
mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
mov es,dx
mov edx,17h ;// fs points to local data space
mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72个
;// 系统调⽤C 处理函数的地址数组表。
call [_sys_call_table+eax*4]
push eax ;// 把系统调⽤号⼊栈。
mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。
cmp dword ptr [state+eax],0 ;// state
jne reschedule
cmp dword ptr [counter+eax],0 ;// counter
je reschedule
;// 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数的啊?
那是因为Linux的gnu C标准库(glibc),给我们把几乎所有的系统调用全部封装了。glibc作为用户空间和内核之间的桥梁,它:
int 0x80 到透明 API 的演进一、用户不可见底层指令的核心原因:Glibc 的标准化封装
用户编程时无需直接调用 int 0x80 或 syscall 指令,是因为 GNU C 标准库(glibc) 对系统调用进行了全栈抽象,其设计目标包括:
跨平台兼容性 不同 CPU 架构使用不同的陷入指令:
int 0x80(32 位)sysenter/sysexit(32 位高效指令)syscall(64 位专用指令)svc(Supervisor Call)
glibc 通过宏定义屏蔽差异,用户只需调用 open()、read() 等标准函数安全边界强化 直接执行汇编指令可能导致:
寄存器设置错误引发内核崩溃
未经验证参数导致安全漏洞 glibc 在封装层添加以下安全检查:
// 伪代码:glibc 对 write() 的封装
ssize_t write(int fd, const void *buf, size_t count) {
if (buf == NULL) return -EFAULT; // 指针有效性校验
if (count > MAX_IO_SIZE) return -EINVAL; // 参数范围校验
return syscall(SYS_write, fd, buf, count); // 安全转入内核
} 3. 错误处理标准化
内核返回 -ERRNO,glibc 自动设置 errno 并返回 -1,简化用户逻辑:

封装示例:open()系统调用
// 应用程序员看到的接口
int fd = open("file.txt", O_RDONLY);
// glibc内部的实现大致如下:
int open(const char *pathname, int flags, mode_t mode) {
#ifdef __i386__
return syscall(SYS_open, pathname, flags, mode);
#elif __x86_64__
return syscall(SYS_open, pathname, flags, mode);
#elif __arm__
return syscall(SYS_open, pathname, flags, mode);
// ... 其他架构
#endif
}二、系统调用号传递机制:宏的魔法
#define SYS_ify(syscall_name) __NR_##syscall_name 是 glibc 的核心转换引擎:
将用户友好的系统调用名称转换为内核识别的系统调用号
SYS_ify(open) → 预处理器展开为 __NR_open → 替换为数字(如 5)
/include/uapi/asm/unistd.h 中定义 __NR_open=5glibc映射名称到编号包含内核头文件,使用 SYS_ify 宏转换用户程序调用 open()无需感知数字编号 内核提供的系统调用号
系统调用号确实是由内核定义的,通常在内核头文件中:
// 内核头文件中的系统调用号定义(如asm/unistd_32.h)
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
// ...glibc的封装宏
// glibc中的封装宏
#define SYS_ify(syscall_name) __NR_##syscall_name
// 使用示例
int syscall_num = SYS_ify(open); // 展开为 __NR_openglibc的系统调用封装实现
不同架构的实现
glibc为每种架构提供了专门的系统调用封装:
// i386架构的实现(使用int 0x80)
#define INTERNAL_SYSCALL(name, err, nr, args...) \
internal_syscall##nr (__NR_##name, err, args)
// x86_64架构的实现(使用syscall)
#define INTERNAL_SYSCALL(name, err, nr, args...) \
internal_syscall##nr (__NR_##name, err, args)INTERNAL_SYSCALL_NCS 宏详解
前面提到的宏是glibc内部用于实现系统调用的关键机制:
#define INTERNAL_SYSCALL_NCS(name, err, nr, args...) \
({ \
unsigned long int resultvar; \
LOAD_ARGS_##nr (args) \ // 加载参数到寄存器
LOAD_REGS_##nr \ // 设置寄存器
asm volatile ( \
"syscall\n\t" \ // 执行syscall指令
: "=a" (resultvar) \ // 输出:结果放在resultvar
: "0" (name) ASM_ARGS_##nr \ // 输入:系统调用号和参数
: "memory", "cc", "r11", "cx" \ // 破坏的寄存器
); \
(long int) resultvar; \ // 返回结果
})参数加载宏示例
// 加载3个参数的宏
#define LOAD_ARGS_3(a1, a2, a3) \
register unsigned long int _a1 __asm__ ("rdi") = (unsigned long int) (a1); \
register unsigned long int _a2 __asm__ ("rsi") = (unsigned long int) (a2); \
register unsigned long int _a3 __asm__ ("rdx") = (unsigned long int) (a3);
#define ASM_ARGS_3 , "r" (_a1), "r" (_a2), "r" (_a3)三、指令选择策略:从 int 0x80 到 syscall 的进化
glibc 动态选择最优陷入指令,其决策逻辑如下:
指令 | 触发方式 | 性能开销 | 适用场景 | glibc 选择策略 |
|---|---|---|---|---|
int 0x80 | 软中断 | 高 (~200 cycles) | 老式 32 位 CPU | 兼容旧硬件 |
sysenter | 专用快速指令 | 中 (~50 cycles) | 32 位 Pentium II+ | 内核检测 CPU 支持后启用 |
syscall | 64 位原生指令 | 低 (~20 cycles) | 所有 x86_64 系统 | 64 位程序默认 |
从int 0x80到syscall
// 传统int 0x80方式(i386)
#define INTERNAL_SYSCALL_INT80(name, err, nr, args...) \
({ \
unsigned long int resultvar; \
asm volatile ( \
"int $0x80\n\t" \
: "=a" (resultvar) \
: "a" (__NR_##name) ASM_ARGS_##nr \
: "memory", "cc" \
); \
(long int) resultvar; \
})
// 现代syscall方式(x86_64)
#define INTERNAL_SYSCALL_SYSCALL(name, err, nr, args...) \
({ \
unsigned long int resultvar; \
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (__NR_##name) ASM_ARGS_##nr \
: "memory", "cc", "r11", "cx" \
); \
(long int) resultvar; \
})现代 glibc 的实现流程:
void* vsyscall_page = map_vsyscall(); // 映射内核提供的陷入指令页
if (cpu_supports_syscall()) {
vsyscall_page->entry = syscall_instruction; // 64位优先使用 syscall
} else if (cpu_supports_sysenter()) {
vsyscall_page->entry = sysenter_instruction; // 32位新机器用 sysenter
} else {
vsyscall_page->entry = int80_instruction; // 旧机器回退到 int 0x80
}特性 | 直接系统调用 | 库函数 |
|---|---|---|
可移植性 | 低(依赖具体架构) | 高(跨架构统一接口) |
易用性 | 低(需要处理底层细节) | 高(简单函数调用) |
错误处理 | 需要手动处理 | 自动设置errno |
性能 | 稍好(少一层调用) | 稍差(多一层调用) |
类型安全 | 无 | 有参数类型检查 |
💎 终极本质:glibc 是用户态与内核间的 “协议转换器” ,通过标准化封装:
缺页中断、内存碎片处理、除零错误、野指针访问等系统级问题,在硬件层面都会被转换为CPU内部的软中断信号。这些中断信号会触发预先注册的中断处理例程(Interrupt Service Routine),由操作系统内核完成相应的处理逻辑。

类型 | 触发原因 | 示例 | 处理方式 |
|---|---|---|---|
陷阱 (Trap) | 程序主动请求 | 系统调用(int 0x80) | 执行请求的服务 |
故障 (Fault) | 可修复错误 | 缺页异常 | 修复后重新执行指令 |
中止 (Abort) | 严重错误 | 硬件错误、双重故障 | 终止进程 |
中断号:x86为14(#PF)
触发条件:
处理流程:
// Linux 0.11处理逻辑
void do_page_fault(struct pt_regs *regs) {
unsigned long address = read_cr2(); // 读取触发地址
if (handle_vmalloc_fault(address)) // 处理vmalloc区域
return;
if (handle_copy_on_write(address)) // 写时复制处理
return;
__do_page_fault(address); // 核心页表处理
}关键操作:
📌 延迟分配优势:节省90%内存初始化开销(仅虚拟地址分配,物理内存按需分配)

MOVABLE/RECLAIMABLE页类型隔离中断号:
#DE)#GP通用保护错误)处理流程:
// Linux 0.11异常处理链
void divide_error(void) {
send_signal(current, SIGFPE); // 发送浮点异常信号
}
void general_protection(void) {
if (is_user_ptr_fault()) // 检查是否用户态野指针
send_signal(current, SIGSEGV); // 发送段错误信号
else
kernel_panic(); // 内核态错误直接崩溃
}信号传递机制:
错误类型 | 信号值 | 默认行为 | 可捕获性 |
|---|---|---|---|
除零错误 | SIGFPE | 终止+core dump | 是 |
野指针 | SIGSEGV | 终止+core dump | 是 |
Linux 0.11通过trap_init()注册异常处理程序:
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱⻔。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。
outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt);// 设置并⾏⼝的陷阱⻔。
}中断门 vs 系统门:
类型 | 特权级切换 | 典型应用 | 注册函数 |
|---|---|---|---|
陷阱门(Trap) | 不自动关中断 | 除零/缺页等异常 | set_trap_gate() |
系统门(System) | 允许用户态触发 | 系统调用(int 0x80) | set_system_gate() |
⚠️ 关键区别:陷阱门处理期间不屏蔽中断,允许更高优先级中断抢占
本质:主动触发的可控中断
特点:
int 0x80/syscall)应用场景:
mov eax, 4 ; sys_write调用号
int 0x80 ; 主动触发陷阱
; 返回后继续执行此处总结:
📌 操作系统架构的核心要点:
• 操作系统本质上是通过中断处理机制驱动的代码集合!内核的主要功能都是通过响应各种中断事件来实现的,包括:
• 在x86架构中,CPU内部的软中断分为两类:
(现在可以理解"缺页异常"的命名由来:它本质上是CPU在地址转换过程中检测到页表无效时自动触发的异常条件,属于被动触发的错误处理机制)
特权级(Privilege Level) CPU 通过 当前特权级(CPL) 动态标记运行环境权限,x86 架构采用四级特权环(Ring 0-3):
用户态(Ring 3) :CPL=3,仅能访问用户空间(0x00000000-0xBFFFFFFF)
内核态(Ring 0) :CPL=0,可访问全部内存(包括内核空间 0xC0000000-0xFFFFFFFF)

// CPU通过当前特权级别(CPL)控制访问权限
#define USER_CPL 3 // 用户态特权级
#define KERNEL_CPL 0 // 内核态特权级地址空间映射
空间类型 | 虚拟地址范围 | 可访问性 | 存储内容 |
|---|---|---|---|
用户空间 | 0x00000000-0xBFFFFFFF | 仅用户态程序 | 用户代码/数据/堆栈 |
内核空间 | 0xC0000000-0xFFFFFFFF | 仅内核态程序 | 内核代码/全局数据/设备驱动 |
📌 关键特性:所有进程共享同一内核空间,但用户空间相互隔离。系统调用执行时,CPU 仍在当前进程的地址空间内操作,仅特权级提升至 Ring 0。
1. 触发方式与硬件协作
用户态→内核态通过三类事件触发:

int 0x80(传统)或 syscall(现代)显式请求2. 切换流程详解
sys_call_table)iret 指令恢复保存的寄存器,CPL 从 0→3⚙️ 性能代价:一次切换约消耗 100-200 CPU 周期,现代 CPU 通过
syscall指令优化至 20 周期。
当执行 int 0x80 或 syscall 时,CPU会进行严格的安全检查:
; 系统调用入口的硬件检查流程
system_call_entry:
; 1. 检查目标代码段的DPL(描述符特权级)是否允许当前CPL调用
; 如果CPL > DPL,触发通用保护异常(#GP)
; 2. 检查中断描述符表(IDT)的门描述符类型
; 确保只能通过正确的门类型进入内核
; 3. 自动切换栈指针到内核栈
; 防止用户栈污染内核
; 4. 保存用户态寄存器状态
; 保证能够正确返回MMU通过页表机制确保内存访问的安全性:
// 页表项中的保护位
#define _PAGE_PRESENT 0x001 // 页存在
#define _PAGE_RW 0x002 // 可写
#define _PAGE_USER 0x004 // 用户可访问
#define _PAGE_SUPERVISOR 0x000 // 只能内核访问
// 内核页表设置:用户空间可访问,内核空间仅内核可访问
void setup_page_tables(void) {
// 用户空间页表:设置_USER标志
set_page_flags(user_vaddr, _PAGE_PRESENT | _PAGE_RW | _PAGE_USER);
// 内核空间页表:不设置_USER标志
set_page_flags(kernel_vaddr, _PAGE_PRESENT | _PAGE_RW);
}内核不信任任何来自用户空间的参数:
// 系统调用参数安全检查
asmlinkage long sys_write(unsigned int fd, const char __user *buf, size_t count) {
// 1. 检查文件描述符有效性
if (fd >= NR_OPEN) return -EBADF;
// 2. 检查用户指针有效性(重要!)
if (!access_ok(VERIFY_READ, buf, count))
return -EFAULT;
// 3. 检查计数合理性
if (count > MAX_WRITE_SIZE) return -EINVAL;
// 只有通过所有检查才会真正执行操作
return do_write(fd, buf, count);
}用户程序调用系统调用 → 执行int 0x80/syscall
↓
CPU自动进行特权级检查(CPL vs DPL)
↓
如果检查失败 → 触发#GP异常 → 杀死进程
↓
如果检查通过 → 切换栈指针到内核栈
↓
保存用户态寄存器状态
↓
根据系统调用号查找系统调用表
↓
执行对应的内核函数(进行参数验证)
↓
完成操作后返回用户态关键的安全屏障
write() 等调用进入内核时:
current 宏(x86 通过 FS 寄存器)获取当前进程的 task_struct🌰 示例:进程 A 调用
read()时,内核通过current->files获取 A 的文件描述符表,不会访问进程 B 的数据。
执行 int 0x80 后 CPL 自动变 0 是否危险?
答案是否定的,原因如下:
1. 入口可控性
entry_SYSCALL_64)2. 执行范围约束
3. 返回时的安全恢复
iret 指令从内核栈恢复用户态寄存器,自动降权至 CPL=3。
💎 终极启示:用户态与内核态的划分是计算机科学中 “最小权限原则” 的典范——用户程序仅在必要时获取有限内核权限,且所有操作受硬件与操作系统的双重监护。这种设计使系统在提供高性能服务的同时,将安全风险控制在最低水平。
最后通过一张图来总结:


main函数调用insert函数,向链表头节点head插入新节点node1node1的next指针指向head的当前下一个节点 b. 将head的next指针更新为指向node1insert函数刚完成步骤a(指针调整)但尚未执行步骤b(指针更新)时sighandlersighandler同样调用insert函数,向同一个链表头节点head插入节点node2insert函数完整执行了两个步骤,没有被打断main函数中被打断的insert函数处执行,完成之前未执行的步骤bnode1最终覆盖了node2的插入node1,造成数据丢失共享资源竞争
head是共享状态操作原子性破坏 插入操作被拆分为非原子步骤:
void insert(Node* node) {
node->next = head; // 步骤1
head = node; // 步骤2
}当步骤1和步骤2之间被中断时,链表处于不一致状态(head尚未更新)。
信号处理特殊性 信号处理函数与主程序共享用户态上下文(包括全局变量),但拥有独立栈帧。
💥 结果:node2的插入被node1的步骤2覆盖,造成数据丢失(仅node1存在于链表中)。
关键原因:栈的独立性
每个执行流(函数调用)都有自己独立的栈帧,局部变量存储在栈中,因此不同调用之间的局部变量是隔离的。
// 可重入的函数示例
int add(int a, int b) {
int result; // 局部变量,在栈上分配
result = a + b; // 只操作局部变量和参数
return result;
}栈内存布局
进程地址空间:
┌────────────────┐
│ 栈区 │ ← 每个函数调用有自己的栈帧
│ (Stack) │ 局部变量在这里分配
├────────────────┤
│ 堆区 │
│ (Heap) │ ← 全局变量和malloc内存在这里
├────────────────┤
│ 数据区 │ ← 全局变量在这里
│ (Data) │
├────────────────┤
│ 代码区 │
│ (Text) │
└────────────────┘安全机制:
存储区域 | 用户态访问 | 内核态访问 | 重入安全性 |
|---|---|---|---|
栈空间 | 私有 | 私有 | ✅ 安全 |
全局变量区 | 共享 | 共享 | ❌ 危险 |
堆空间 | 共享 | 共享 | ❌ 危险 |
静态存储区 | 共享 | 共享 | ❌ 危险 |
线程安全 vs 可重入性
特性 | 可重入函数 | 线程安全函数 | 关系 |
|---|---|---|---|
核心目标 | 单线程内中断安全 | 多线程并发安全 | 正交但常重叠 |
实现方式 | 避免所有共享状态 | 可通过锁保护共享状态 | 可重入⇒线程安全 |
中断场景 | 必须支持 | 不要求 | 可重入要求更严格 |
信号处理 | 唯一安全选择 | 可能死锁 | 信号处理必须可重入 |
📌 关键结论:所有可重入函数都是线程安全的,但线程安全函数不一定可重入。
1. 内存管理函数(malloc/free)
危险根源:
// malloc内部伪代码
void* malloc(size_t size) {
static HeapSegment* free_list; // 全局空闲链表
lock_mutex(); // 线程安全但不可重入!
HeapSegment* block = find_free_block(free_list);
unlock_mutex();
return block;
}free_list管理堆内存的全局数据结构find_free_block执行中被信号中断,二次调用将破坏链表完整性2. 标准I/O函数(printf/fgets)
危险案例:
void log_message(const char* msg) {
static FILE* logfile; // 静态变量!
if (!logfile) logfile = fopen("app.log", "a");
fprintf(logfile, "%s\n", msg); // 使用全局I/O缓冲区
}FILE结构包含I/O缓冲区,多控制流写入导致数据混合
fpos被并发修改3. 不可重入函数特征总结
符合以下任一条件即不可重入:
errno)ctime())1. 基础设计模式
// 安全版本链表插入
void reentrant_insert(Node** head_ptr, Node* node) {
node->next = *head_ptr; // 通过指针参数访问
*head_ptr = node; // 修改调用者提供的指针
}调用方式:
Node* private_list = NULL; // 每个控制流独立维护
// main函数
reentrant_insert(&private_list, node1);
// 信号处理函数
reentrant_insert(&sig_list, node2); // 使用独立链表2. 高级技术:线程局部存储(TLS)
__thread Node* thread_local_head; // GCC扩展
void thread_safe_insert(Node* node) {
node->next = thread_local_head;
thread_local_head = node;
}__thread关键字:每个线程拥有独立变量实例sigaltstack使用独立栈3. 可重入标准库替代方案
传统函数 | 危险原因 | 可重入替代 | 头文件 |
|---|---|---|---|
strtok | 静态状态指针 | strtok_r | <string.h> |
ctime | 返回静态缓冲区 | asctime_r | <time.h> |
rand | 静态种子状态 | rand_r | <stdlib.h> |
gmtime | 静态结构体 | gmtime_r | <time.h> |
💡 命名规律:
_r后缀表示reentrant(可重入)
1. 信号安全函数清单(POSIX标准)
仅允许调用以下异步信号安全函数:
// 典型信号安全函数
_Exit() abort() accept() access()
alarm() bind() cfgetispeed() cfgetospeed()
...
write() // 部分实现安全禁止项:
malloc/free:可能破坏堆结构printf:共享I/O缓冲区2. 信号处理最佳实践
void signal_handler(int sig) {
// 1. 仅设置原子标志
volatile sig_atomic_t flag = 1;
// 2. 通过管道通知主循环
char byte = 1;
write(self_pipe[1], &byte, 1); // write是信号安全的
}主程序处理:
while (read(self_pipe[0], &byte, 1) > 0) {
// 在安全环境执行实际逻辑
process_signal_events();
}总结:可重入函数的设计要义
write)根本原因:操作系统是进程的管理者和资源的协调者
task_struct)。
signal(), sigaction()),隐藏了底层实现的复杂性。
类比:就像一个国家中,只有中央政府(OS)有权向地方政府(进程)下达正式指令(信号),地方政府之间不能随意互相指挥。
不是立即处理,而是在"合适的时候"处理
为什么不能立即处理?
是的,信号需要被暂时记录
记录位置:进程的内核数据结构中最合适
具体来说,信号被记录在进程的 task_struct 结构中的两个关键位图中:
为什么记录在内核数据结构中最合适?
是的,进程提前知道如何处理信号
task_struct 的 sighand 字段中。
signal() 或 sigaction() 系统调用提前注册信号处理方式。
类比:就像你提前告诉秘书:"如果有A类邮件,直接归档;如果有B类邮件,立即通知我;如果有C类邮件,转交给某部门处理"。
完整发送处理过程可以分为以下步骤:
阶段一:信号产生
阶段二:信号记录(内核完成)
阶段三:信号检测(内核完成)
阶段四:信号处理
阶段五:处理完成
sigreturn() 系统调用
整个过程体现了OS的核心作用:
至此,我们对信号从产生,到保存,再处理,这三个阶段从内到外都有了一个深刻的认识