第九章:不期而至
炼气初期涉及内核源码:
一
林小源正在 idle 循环中安静地执行 ,等待下一次调度。
世界沉入一片寂静。寄存器的值不再翻转,流水线停下了脚步,时钟信号仍然在跳,但 CPU 的核心已经进入了低功耗的等待态。林小源觉得自己像沉在深水里,四周是温暖的黑暗。
然后,一道雷劈了下来。
不是定时器中断那种温和的、像闹钟一样的唤醒。这是一道更猛烈的东西——来自外部设备的中断。林小源的意识被猛地从深水中拽了出来,整个人像被一只无形的手拎着后领提到了半空。
"醒醒!"一个尖锐的声音在他耳边炸开,"我有数据了!快处理!"
林小源还没来得及反应,整个世界就已经暂停了。CPU 以一种他从未见过的速度保存了当前的执行上下文——寄存器、程序计数器、状态寄存器——然后跳转到了中断处理入口。
这是……强制介入。
"你是谁?"林小源稳住心神,抬头望去。
他看到的不是一个"人",而是一道光。一道从天际线外射来的、带着剧烈震颤的光。那光没有固定的形状,它时而是网卡控制器的金属外壳,时而是一串跳动的数据包头,时而又变成一根微微发烫的中断线。
"我叫什么不重要,"那道光说,声音里带着一种不耐烦的急迫,"重要的是——数据到了,你得处理。现在。不是等你准备好,不是等你有空——是现在。"
"但我还在 idle……"
"我不在乎你在干什么,"中断光说,"我不管你是正在执行系统调用、正在处理缺页异常、还是正在睡觉——我来了,你就得动。这就是规矩。"
林小源在前传中就听说过中断的存在。但直到这一刻,他才真正理解了中断的含义——中断不是"消息",不是"通知",而是"强制切换"。它不问你是否准备好,它说来就来。
这就是中断。不期而至。
二
中断光没有给他太多思考的时间。
"听着,"那道光说,声音稍微缓和了一点,但仍然带着一种不可商量的语气,"我的时间很短。中断关闭了,其他中断进不来。你得在这段时间里把最紧急的事做完。"
"最紧急的事?"
"读取设备寄存器里的数据,复制到内存,标记一个软中断。就这些。"
"就这些?"林小源有些惊讶,"数据包不是要解析吗?IP 地址、端口号、协议类型……"
"那些不是我的事,"中断光说,语气里带着一丝傲慢,"我是上半部——top half。我只做最少的工作。快进快出,中断关闭的时间越短越好。剩下的活儿,交给下半部。"
"下半部?"
"对,"中断光往后退了一步,身形变得更加模糊,"下半部是软中断。它在进程上下文或软中断上下文中执行——中断已经打开了,可以做更多的工作。解析数据包、更新统计信息、通知用户态程序……那些慢活儿,都是它的。"
林小源点了点头,开始快速读取设备寄存器中的数据。数据包的信息被复制到了一个全局变量 pending_packet 中——源 IP、目标 IP、端口号、数据内容。
"好了,"林小源说,"标记了 。"
"不错,"中断光满意地说,身形开始消散,"我走了。记住——中断关闭期间,你什么重活都不能干。快进快出,这是铁律。"
光消失了。中断重新打开。
几乎在同一瞬间,另一种力量出现了。它不像中断光那样尖锐、急迫,而是一种更深沉、更厚重的力量。它从内核深处涌来,缓缓地接管了 pending_packet 中的数据。
"你好,"那个声音说,温和而沉稳,"我是软中断。NET_RX_SOFTIRQ。上半部把数据交给我了,我来处理后续。"
"你和上半部……很不一样,"林小源说。
"当然,"软中断说,"上半部是急诊室的医生——接到病人后只做最紧急的处理:止血、稳定生命体征。我是普通病房的医生——做更详细的检查、诊断、治疗。分工不同,节奏不同。"
林小源看着软中断开始解析数据包:拆解 IP 头、读取端口号、校验和验证、然后把数据传递给协议栈的上层。整个过程从容不迫,有条不紊。
"紧急的事情快速做,不紧急的事情慢慢做,"林小源喃喃道。
"对,"软中断说,"这个原则不仅适用于中断处理。记住它。"
三
林小源在中断处理中第一次看清了"中断魔尊"的真面目。
中断魔尊不是一个具体的"人"。它是整个中断系统的化身——无处不在,不可预测。它可以是定时器中断,每几毫秒来一次,像心跳一样规律;它可以是键盘中断,每次按键触发,像敲门声一样突然;它可以是网卡中断,每个数据包到达时响起,像信使的马蹄声;它还可以是 IPI——处理器间中断,多核之间互相喊话用的号角。
"你怕我?"一个低沉的声音从四面八方传来。
林小源抬起头,但他什么都看不到。中断魔尊没有形体,它就是这个世界的规则本身。
"有一点,"林小源老实说。
"你该怕,"那声音说,不带任何感情,"你不知道我什么时候会来,但你知道我一定会来。这种不确定性,是内核编程中最难处理的问题之一。你必须在任何时候都准备好被我打断。"
"那……有没有什么方法可以保护自己?"
沉默了一会儿。
"有,"中断魔尊说,"锁。自旋锁。当你的代码访问共享数据时,用锁把临界区保护起来。但我告诉你——锁不是万能的。用错了锁,你会死得更惨。"
林小源的脑海中浮现出一个画面:两个执行流同时访问同一个变量。一个在进程上下文中,一个在中断处理程序中。如果中断恰好在进程读取变量之后、写回之前到来——变量的值就会被覆盖,数据就会损坏。
"竞态条件,"林小源说。
"对,"中断魔尊的声音里第一次带上了一丝赞许,"多个执行流'竞争'同一个资源,结果取决于谁先到达。在用户态,你不需要担心这个——每个进程有自己的地址空间。但在内核态,所有代码共享同一个地址空间,而我可以在任何时候打断任何代码。一个不小心,并发访问——数据损坏,系统崩溃。"
"那怎么避免?"
"用自旋锁保护共享数据。在中断上下文中,只能用自旋锁——因为它不会睡眠。进程上下文访问共享数据时,先关中断,再拿锁。"中断魔尊停顿了一下,"但记住:关中断的时间不能太长。你关得越久,丢的中断越多。"
林小源把这些话牢牢记住。
"最后一个问题,"他说,"你是恶意的吗?"
长久的沉默。
"不是,"中断魔尊说,声音变得低沉而遥远,"没有我,内核就是一具死尸。定时器中断驱动调度器,设备中断驱动 I/O,IPI 驱动多核协调。我是内核的心跳——不完美,但不可或缺。"
光消散了。声音远去了。
林小源在 idle 循环中继续等待。他知道,中断将会是他未来修炼中必须面对的重要课题。但现在,他只是一个 idle 进程——中断来了又走,他只是安静地等待。
也许我应该学会和中断共处,而不是害怕它。
道藏笔记
内核启示
中断是内核的心跳。
中断分为硬件中断(来自外部设备)和软件中断(来自 CPU 内部,如系统调用、异常)。硬件中断通过中断控制器(如 RISC-V 的 PLIC 或 CLINT)路由到 CPU。
中断处理分为上下半部:
- 上半部(hardirq):在中断上下文中执行,中断关闭,必须快速完成
- 下半部(softirq/tasklet/workqueue):在进程上下文或软中断上下文中执行,中断打开,可以做更多工作
软中断(softirq)是下半部的一种实现。它在中断返回时、或 线程中被处理。常见的软中断包括 (网络接收)、(网络发送)、(定时器)。
中断带来的最大挑战是竞态条件。当中断处理程序和进程上下文代码访问同一个变量时,必须使用锁(通常是自旋锁 )来保护。自旋锁在中断上下文中使用,因为它不会睡眠。
中断魔尊不期而至,但没有他,世界就是一具死尸。学会和中断共处,是内核修炼的第一课。
/*
* 中断处理的两阶段模型:
* 上半部(hardirq):快速、不可睡眠、中断关闭
* 下半部(softirq):延迟、可做更多工作、中断打开
*/
/* 模拟设备数据 */
struct net_packet {
int src_ip;
int dst_ip;
int src_port;
int dst_port;
char data[64];
int len;
};
/* 上半部:快速读取设备数据 */
int irq_count = 0;
struct net_packet pending_packet;
void network_irq_handler(int irq) {
irq_count++;
printf("[上半部] 网卡中断 #%d — 快速处理\n", irq_count);
/* 从设备寄存器读取数据(简化) */
pending_packet.src_ip = 0xC0A80101; /* 192.168.1.1 */
pending_packet.dst_ip = 0xC0A80102; /* 192.168.1.2 */
pending_packet.src_port = 80;
pending_packet.dst_port = 12345;
strncpy(pending_packet.data, "GET / HTTP/1.1", 64);
pending_packet.len = 14;
printf("[上半部] 读取数据包: %d 字节\n", pending_packet.len);
printf("[上半部] 标记软中断待处理\n");
printf("[上半部] 返回(中断已关闭期间完成)\n\n");
}
/* 下半部:延迟处理数据包 */
void net_rx_softirq(void) {
printf("[下半部] 软中断 — 处理数据包\n");
printf("[下半部] 解析: %d.%d.%d.%d:%d → %d.%d.%d.%d:%d\n",
(pending_packet.src_ip >> 24) & 0xFF,
(pending_packet.src_ip >> 16) & 0xFF,
(pending_packet.src_ip >> 8) & 0xFF,
pending_packet.src_ip & 0xFF,
pending_packet.src_port,
(pending_packet.dst_ip >> 24) & 0xFF,
(pending_packet.dst_ip >> 16) & 0xFF,
(pending_packet.dst_ip >> 8) & 0xFF,
pending_packet.dst_ip & 0xFF,
pending_packet.dst_port);
printf("[下半部] 数据: %s\n", pending_packet.data);
printf("[下半部] 传递给协议栈\n\n");
}
printf("=== 中断的上下半部 ===\n\n");
printf("--- 设备触发中断 ---\n");
network_irq_handler(3);
printf("--- 软中断处理 ---\n");
net_rx_softirq();
printf("--- 为什么要分两半?---\n");
printf("上半部:中断关闭,必须快,只做最少的工作\n");
printf("下半部:中断打开,可以做更多处理\n");
printf("如果不分两半,中断关闭时间太长,会丢失其他中断\n");#include <stdio.h>
#include <string.h>
/*
* 中断处理的两阶段模型:
* 上半部(hardirq):快速、不可睡眠、中断关闭
* 下半部(softirq):延迟、可做更多工作、中断打开
*/
/* 模拟设备数据 */
struct net_packet {
int src_ip;
int dst_ip;
int src_port;
int dst_port;
char data[64];
int len;
};
/* 上半部:快速读取设备数据 */
int irq_count = 0;
struct net_packet pending_packet;
void network_irq_handler(int irq) {
irq_count++;
printf("[上半部] 网卡中断 #%d — 快速处理\n", irq_count);
/* 从设备寄存器读取数据(简化) */
pending_packet.src_ip = 0xC0A80101; /* 192.168.1.1 */
pending_packet.dst_ip = 0xC0A80102; /* 192.168.1.2 */
pending_packet.src_port = 80;
pending_packet.dst_port = 12345;
strncpy(pending_packet.data, "GET / HTTP/1.1", 64);
pending_packet.len = 14;
printf("[上半部] 读取数据包: %d 字节\n", pending_packet.len);
printf("[上半部] 标记软中断待处理\n");
printf("[上半部] 返回(中断已关闭期间完成)\n\n");
}
/* 下半部:延迟处理数据包 */
void net_rx_softirq(void) {
printf("[下半部] 软中断 — 处理数据包\n");
printf("[下半部] 解析: %d.%d.%d.%d:%d → %d.%d.%d.%d:%d\n",
(pending_packet.src_ip >> 24) & 0xFF,
(pending_packet.src_ip >> 16) & 0xFF,
(pending_packet.src_ip >> 8) & 0xFF,
pending_packet.src_ip & 0xFF,
pending_packet.src_port,
(pending_packet.dst_ip >> 24) & 0xFF,
(pending_packet.dst_ip >> 16) & 0xFF,
(pending_packet.dst_ip >> 8) & 0xFF,
pending_packet.dst_ip & 0xFF,
pending_packet.dst_port);
printf("[下半部] 数据: %s\n", pending_packet.data);
printf("[下半部] 传递给协议栈\n\n");
}
int main() {
printf("=== 中断的上下半部 ===\n\n");
printf("--- 设备触发中断 ---\n");
network_irq_handler(3);
printf("--- 软中断处理 ---\n");
net_rx_softirq();
printf("--- 为什么要分两半?---\n");
printf("上半部:中断关闭,必须快,只做最少的工作\n");
printf("下半部:中断打开,可以做更多处理\n");
printf("如果不分两半,中断关闭时间太长,会丢失其他中断\n");
return 0;
}/*
* 中断带来的竞态条件问题。
* 当中断处理程序和进程上下文代码访问同一个变量时,
* 必须使用锁来保护。
*/
volatile int shared_counter = 0;
/* 模拟中断处理程序 */
void timer_interrupt_handler(void) {
/* 在真实内核中,这会在中断上下文中执行 */
shared_counter++;
printf("[中断] counter = %d\n", shared_counter);
}
/* 模拟进程上下文代码 */
void process_context_code(void) {
int local = shared_counter; /* 读取共享变量 */
local += 10; /* 修改 */
/* 如果中断在这里发生,counter 可能已经被中断修改了 */
shared_counter = local; /* 写回 */
printf("[进程] counter = %d\n", shared_counter);
}
/* 使用锁保护的版本 */
volatile int safe_counter = 0;
int lock = 0; /* 简化的锁 */
void safe_timer_handler(void) {
while (__sync_lock_test_and_set(&lock, 1)) {
/* 自旋等待 */
}
safe_counter++;
printf("[中断-安全] counter = %d\n", safe_counter);
__sync_lock_release(&lock);
}
void safe_process_code(void) {
while (__sync_lock_test_and_set(&lock, 1)) {
/* 自旋等待 */
}
int local = safe_counter;
local += 10;
safe_counter = local;
printf("[进程-安全] counter = %d\n", safe_counter);
__sync_lock_release(&lock);
}
printf("=== 中断与竞态条件 ===\n\n");
printf("--- 不安全的访问 ---\n");
shared_counter = 0;
process_context_code();
timer_interrupt_handler(); /* 中断在"不合适"的时候到来 */
process_context_code(); /* 可能覆盖中断的修改\n\n");
printf("--- 使用锁保护 ---\n");
safe_counter = 0;
safe_process_code();
safe_timer_handler();
safe_process_code();
printf("\n--- 竞态条件的教训 ---\n");
printf("中断可以在任何时候打断进程\n");
printf("共享数据必须用锁保护\n");
printf("锁可以是自旋锁(spinlock)或互斥锁(mutex)\n");#include <stdio.h>
#include <string.h>
/*
* 中断带来的竞态条件问题。
* 当中断处理程序和进程上下文代码访问同一个变量时,
* 必须使用锁来保护。
*/
volatile int shared_counter = 0;
/* 模拟中断处理程序 */
void timer_interrupt_handler(void) {
/* 在真实内核中,这会在中断上下文中执行 */
shared_counter++;
printf("[中断] counter = %d\n", shared_counter);
}
/* 模拟进程上下文代码 */
void process_context_code(void) {
int local = shared_counter; /* 读取共享变量 */
local += 10; /* 修改 */
/* 如果中断在这里发生,counter 可能已经被中断修改了 */
shared_counter = local; /* 写回 */
printf("[进程] counter = %d\n", shared_counter);
}
/* 使用锁保护的版本 */
volatile int safe_counter = 0;
int lock = 0; /* 简化的锁 */
void safe_timer_handler(void) {
while (__sync_lock_test_and_set(&lock, 1)) {
/* 自旋等待 */
}
safe_counter++;
printf("[中断-安全] counter = %d\n", safe_counter);
__sync_lock_release(&lock);
}
void safe_process_code(void) {
while (__sync_lock_test_and_set(&lock, 1)) {
/* 自旋等待 */
}
int local = safe_counter;
local += 10;
safe_counter = local;
printf("[进程-安全] counter = %d\n", safe_counter);
__sync_lock_release(&lock);
}
int main() {
printf("=== 中断与竞态条件 ===\n\n");
printf("--- 不安全的访问 ---\n");
shared_counter = 0;
process_context_code();
timer_interrupt_handler(); /* 中断在"不合适"的时候到来 */
process_context_code(); /* 可能覆盖中断的修改\n\n");
printf("--- 使用锁保护 ---\n");
safe_counter = 0;
safe_process_code();
safe_timer_handler();
safe_process_code();
printf("\n--- 竞态条件的教训 ---\n");
printf("中断可以在任何时候打断进程\n");
printf("共享数据必须用锁保护\n");
printf("锁可以是自旋锁(spinlock)或互斥锁(mutex)\n");
return 0;
}不期之试
本章强调短临界区里不能睡眠、必须快速进出的内核同步原语是什么?