Skip to content

第九章:不期而至

炼气初期

涉及内核源码:

林小源正在 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)是下半部的一种实现。它在中断返回时、或 线程中被处理。常见的软中断包括 (网络接收)、(网络发送)、(定时器)。

中断带来的最大挑战是竞态条件。当中断处理程序和进程上下文代码访问同一个变量时,必须使用锁(通常是自旋锁 )来保护。自旋锁在中断上下文中使用,因为它不会睡眠。

中断魔尊不期而至,但没有他,世界就是一具死尸。学会和中断共处,是内核修炼的第一课。


c
/*
 * 中断处理的两阶段模型:
 * 上半部(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");
c
/*
 * 中断带来的竞态条件问题。
 * 当中断处理程序和进程上下文代码访问同一个变量时,
 * 必须使用锁来保护。
 */

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");

破关试炼

不期之试

本章强调短临界区里不能睡眠、必须快速进出的内核同步原语是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道