9深入理解Linux内核(第3版)

中断和异常

中断处理

中断异常软中断工作队列

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第9章:中断和异常(Interrupts and Exceptions)

一、导读

9.1 本章学习目标

第9章讲解Linux内核的中断和异常处理机制。中断和异常是操作系统响应异步事件和同步错误的核心机制——键盘按键、网卡收包、定时器到期、缺页异常、除零错误等,都通过中断和异常机制通知内核。理解中断处理的工作原理,是编写设备驱动程序、诊断系统问题、以及理解内核并发模型的基础。

本章的核心学习目标包括:

  • 区分中断和异常:硬件中断、软件中断、异常的区别和联系
  • 理解中断描述符表(IDT):中断向量如何映射到处理函数
  • 掌握中断处理的完整流程:从硬件触发到处理函数执行再到返回
  • 理解上半部和下半部的分离:为什么需要延迟处理,如何实现
  • 掌握软中断、tasklet和工作队列:三种下半部机制的区别和适用场景
  • 了解中断的并发控制:中断上下文中的锁和原子操作

9.2 为什么本章如此重要

中断是操作系统与硬件交互的主要方式。没有中断机制,CPU只能通过轮询(polling)检查设备状态,这将浪费大量CPU时间。中断允许设备在需要服务时主动通知CPU,使得CPU可以在等待I/O时执行其他任务。

异常处理则是系统安全和稳定性的保障。缺页异常实现了虚拟内存的按需分配,通用保护故障(GPF)阻止了非法的内存访问,系统调用通过异常(软中断)实现。理解异常处理机制,对于理解内存管理、进程保护和系统调用都至关重要。

对于设备驱动开发者而言,中断处理是最核心的技能之一。正确的中断处理需要平衡响应速度和处理时间,合理使用上半部/下半部分离、软中断和工作队列等机制。

9.3 核心问题引导

在阅读本章时,请带着以下问题思考:

当键盘按键时,从硬件到内核到用户程序,经历了哪些步骤?

中断处理和进程调度有什么关系?中断可以抢占当前运行的进程吗?

为什么中断处理函数要尽量短小?长时间的中断处理会导致什么问题?

软中断、tasklet和工作队列有什么区别?在什么场景下使用哪种?

为什么在中断处理函数中不能调用可能睡眠的函数?

多核系统中,中断如何在CPU之间分配?什么是中断亲和性?


二、核心概念详解

2.1 中断和异常的分类

Linux中的中断和异常可以分为以下几类:

中断和异常分类:

1. 中断(Interrupt)—— 异步事件
   ├── 硬件中断(Hardware Interrupt)
   │   ├── 可屏蔽中断(Maskable Interrupt)
   │   │   └── 通过 INTR 引脚,可被 IF 标志屏蔽
   │   │   └── 由外部设备触发(键盘、网卡、定时器等)
   │   └── 不可屏蔽中断(NMI)
   │       └── 通过 NMI 引脚,不能被屏蔽
   │       └── 用于硬件故障等紧急情况
   │
   └── 软件中断(Software Interrupt)
       └── INT n 指令触发
       └── 系统调用(INT 0x80 或 SYSCALL)

2. 异常(Exception)—— 同步事件
   ├── 故障(Fault)
   │   └── 可恢复,指令重新执行
   │   └── 缺页异常(Page Fault, #PF)
   │   └── 除零异常(Division Error, #DE)
   │
   ├── 陷阱(Trap)
   │   └── 可恢复,执行下一条指令
   │   └── 断点异常(Breakpoint, #BP)
   │   └── 溢出异常(Overflow, #OF)
   │
   └── 终止(Abort)
       └── 不可恢复,必须终止进程
       └── 机器检查异常(Machine Check)
c
/* x86 异常向量表(部分) */
/*
 * 向量号  助记符  描述                类型
 * 0       #DE    除零错误             故障
 * 1       #DB    调试异常             故障/陷阱
 * 2       ---    NMI                  中断
 * 3       #BP    断点                 陷阱
 * 4       #OF    溢出                 陷阱
 * 5       #BR    BOUND范围超出         故障
 * 6       #UD    无效操作码            故障
 * 7       #NM    设备不可用           故障
 * 8       #DF    双重故障             终止
 * 10      #TS    无效TSS             故障
 * 11      #NP    段不存在            故障
 * 12      #SS    栈段故障            故障
 * 13      #GP    通用保护故障         故障
 * 14      #PF    缺页异常            故障
 * 16      #MF    x87浮点异常         故障
 * 17      #AC    对齐检查            故障
 * 18      #MC    机器检查            终止
 * 19      #XM    SIMD浮点异常        故障
 * 20      #VE    虚拟异常            故障
 */

2.2 中断描述符表(IDT)

IDT(Interrupt Descriptor Table)存储了所有中断和异常处理函数的入口地址。CPU根据中断向量号索引IDT,找到对应的处理函数。

c
/* IDT 条目(门描述符) */
/* arch/x86/include/asm/desc.h */
struct gate_struct {
    u16 offset_low;       /* 入口地址低16位 */
    u16 segment;          /* 代码段选择符 */
    unsigned ist : 3,     /* 中断栈表索引 */
             zero0 : 4,
             zero1 : 1,
             type : 5,    /* 门类型 */
             dpl : 2,     /* 特权级 */
             p : 1;       /* 存在位 */
    u16 offset_middle;    /* 入口地址中16位 */
    u32 offset_high;      /* 入口地址高32位 */
    u32 reserved;
} __attribute__((packed));

/* IDT 初始化(简化) */
/* arch/x86/kernel/idt.c */
void __init idt_setup_early_traps(void) {
    /* 设置异常处理函数 */
    set_intr_gate(X86_TRAP_DE, divide_error);          /* 0: 除零 */
    set_intr_gate(X86_TRAP_DB, debug);                 /* 1: 调试 */
    set_intr_gate(X86_TRAP_NMI, nmi);                  /* 2: NMI */
    set_intr_gate(X86_TRAP_BP, int3);                  /* 3: 断点 */
    set_intr_gate(X86_TRAP_OF, overflow);              /* 4: 溢出 */
    set_intr_gate(X86_TRAP_UD, invalid_op);            /* 6: 无效操作码 */
    set_intr_gate(X86_TRAP_NM, device_not_available);  /* 7: 设备不可用 */
    set_intr_gate(X86_TRAP_DF, double_fault);          /* 8: 双重故障 */
    set_intr_gate(X86_TRAP_TS, invalid_TSS);           /* 10: 无效TSS */
    set_intr_gate(X86_TRAP_NP, segment_not_present);   /* 11: 段不存在 */
    set_intr_gate(X86_TRAP_SS, stack_segment);         /* 12: 栈段故障 */
    set_intr_gate(X86_TRAP_GP, general_protection);    /* 13: 通用保护 */
    set_intr_gate(X86_TRAP_PF, page_fault);            /* 14: 缺页异常 */
    set_intr_gate(X86_TRAP_MF, coprocessor_error);     /* 16: 浮点异常 */
    set_intr_gate(X86_TRAP_AC, alignment_check);       /* 17: 对齐检查 */
    set_intr_gate(X86_TRAP_MC, machine_check);         /* 18: 机器检查 */
    set_intr_gate(X86_TRAP_XM, simd_coprocessor_error);/* 19: SIMD异常 */

    /* 加载 IDT */
    load_idt(&idt_descr);
}

/* 设置硬件中断处理 */
void __init idt_setup_apic_and_irq_gates(void) {
    /* 从向量 32 开始是硬件中断 */
    for (int i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++) {
        set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
    }
}

2.3 中断处理的完整流程

当一个硬件中断到达时,CPU和内核协作完成以下处理流程:

硬件中断处理流程:

1. 设备发出中断信号
     ↓
2. 中断控制器(APIC)接收并路由到CPU
     ↓
3. CPU完成当前指令,检查中断
     ↓
4. CPU根据中断向量号查找IDT
     ↓
5. CPU保存当前上下文(EFLAGS, CS, RIP)到内核栈
     ↓
6. CPU跳转到中断处理函数
     ↓
7. 内核保存更多寄存器到栈上
     ↓
8. 调用设备的中断处理函数(上半部)
     ↓
9. 发送EOI(End of Interrupt)给中断控制器
     ↓
10. 恢复寄存器,执行 iret 指令返回
     ↓
11. CPU恢复之前的执行
c
/* 中断处理函数的注册 */
/* include/linux/interrupt.h */

/* 中断处理函数类型 */
typedef irqreturn_t (*irq_handler_t)(int, void *);

/* 注册中断处理函数 */
int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev_id);

/* 释放中断 */
void free_irq(unsigned int irq, void *dev_id);

/* 中断处理函数返回值 */
typedef enum {
    IRQ_NONE        = (0 << 0),  /* 中断不是我的 */
    IRQ_HANDLED     = (1 << 0),  /* 中断已处理 */
    IRQ_WAKE_THREAD = (1 << 1),  /* 唤醒中断线程 */
} irqreturn_t;

/* 中断标志 */
#define IRQF_SHARED      (1 << 2)  /* 允许共享中断线 */
#define IRQF_TIMER       (1 << 5)  /* 定时器中断 */
#define IRQF_PERCPU      (1 << 6)  /* 每CPU中断 */

/* 使用示例(设备驱动中) */
static irqreturn_t my_device_handler(int irq, void *dev_id) {
    struct my_device *dev = (struct my_device *)dev_id;

    /* 检查是否是我们的设备触发的中断 */
    u32 status = readl(dev->base + STATUS_REG);
    if (!(status & INTERRUPT_PENDING))
        return IRQ_NONE;  /* 不是我们的中断 */

    /* 清除中断标志 */
    writel(status, dev->base + STATUS_REG);

    /* 上半部处理:只做最关键的操作 */
    /* 例如:确认中断、保存硬件状态 */

    /* 调度下半部处理(如果需要) */
    schedule_work(&dev->work);

    return IRQ_HANDLED;
}

static int my_device_probe(struct platform_device *pdev) {
    struct my_device *dev;
    int irq, ret;

    irq = platform_get_irq(pdev, 0);

    ret = request_irq(irq, my_device_handler,
                      IRQF_SHARED, "my_device", dev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
        return ret;
    }

    return 0;
}

2.4 上半部和下半部

中断处理分为上半部(Top Half)和下半部(Bottom Half):

上半部(Top Half):
- 在中断上下文中执行
- 关中断(或只关当前中断线)
- 必须快速完成
- 只做最关键的操作:确认中断、保存硬件状态
- 不能睡眠、不能做耗时操作

下半部(Bottom Half):
- 延迟执行
- 开中断(允许其他中断)
- 可以做更多工作
- 处理数据、调用协议栈、更新统计等

为什么要分离?
- 中断处理期间,CPU不能响应同优先级或低优先级的中断
- 如果中断处理时间太长,其他中断会被延迟
- 可能导致数据丢失(如网卡缓冲区溢出)
- 降低系统的响应性

Linux提供了三种下半部机制:

c
/* 1. 软中断(Softirq) */
/* 静态分配,编译时确定 */
/* include/linux/interrupt.h */
enum {
    HI_SOFTIRQ = 0,       /* 高优先级软中断 */
    TIMER_SOFTIRQ,        /* 定时器软中断 */
    NET_TX_SOFTIRQ,       /* 网络发送 */
    NET_RX_SOFTIRQ,       /* 网络接收 */
    BLOCK_SOFTIRQ,        /* 块设备 */
    IRQ_POLL_SOFTIRQ,     /* 中断轮询 */
    TASKLET_SOFTIRQ,      /* Tasklet */
    SCHED_SOFTIRQ,        /* 调度 */
    HRTIMER_SOFTIRQ,      /* 高精度定时器 */
    RCU_SOFTIRQ,          /* RCU */
    NR_SOFTIRQS
};

/* 注册软中断处理函数 */
/* 只能在编译时注册,不能动态添加 */
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

/* 触发软中断 */
raise_softirq(NET_RX_SOFTIRQ);

/* 软中断的特点:
 * - 可以在多个CPU上同时运行(同类型可以)
 * - 不能睡眠
 * - 执行时机:硬中断返回前、ksoftirqd线程
 */

/* 2. Tasklet */
/* 基于软中断实现,动态调度 */
struct tasklet_struct {
    struct tasklet_struct *next;
    unsigned long state;     /* TASKLET_STATE_SCHED */
    atomic_t count;          /* 引用计数 */
    void (*func)(unsigned long);
    unsigned long data;
};

/* 定义 tasklet */
void my_tasklet_func(unsigned long data) {
    struct my_device *dev = (struct my_device *)data;
    /* 下半部处理... */
}

DECLARE_TASKLET(my_tasklet, my_tasklet_func, (unsigned long)dev);

/* 在中断处理函数中调度 tasklet */
static irqreturn_t my_handler(int irq, void *dev_id) {
    /* 上半部处理... */
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}

/* tasklet 的特点:
 * - 同一 tasklet 不会在多个CPU上同时运行
 * - 不能睡眠
 * - 可以动态创建和调度
 */

/* 3. 工作队列(Workqueue) */
/* 在进程上下文执行,可以睡眠 */
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;       /* 处理函数 */
};

/* 定义工作 */
void my_work_func(struct work_struct *work) {
    struct my_device *dev = container_of(work, struct my_device, work);
    /* 下半部处理... */
    /* 可以调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock 等) */
}

INIT_WORK(&dev->work, my_work_func);

/* 在中断处理函数中调度工作 */
static irqreturn_t my_handler(int irq, void *dev_id) {
    /* 上半部处理... */
    schedule_work(&dev->work);
    return IRQ_HANDLED;
}

/* 工作队列的特点:
 * - 在进程上下文执行
 * - 可以睡眠
 * - 可以重新调度
 * - 有延迟,不如软中断及时
 */

2.5 中断的并发控制

中断处理函数运行在中断上下文中,不能使用可能睡眠的操作(如mutex_lock、kmalloc(GFP_KERNEL))。内核提供了专门的机制来处理中断上下文中的并发问题。

c
/* 中断上下文中的并发控制 */

/* 1. 原子操作 */
atomic_t counter = ATOMIC_INIT(0);
atomic_inc(&counter);          /* 原子递增 */
atomic_dec_and_test(&counter); /* 原子递减并测试 */
atomic_read(&counter);         /* 原子读取 */

/* 2. 自旋锁 */
spinlock_t my_lock;
spin_lock_init(&my_lock);

/* 在中断处理函数中使用 */
spin_lock(&my_lock);
/* 临界区 */
spin_unlock(&my_lock);

/* 如果临界区也被进程上下文访问 */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);  /* 关中断 + 加锁 */
/* 临界区 */
spin_unlock_irqrestore(&my_lock, flags);  /* 解锁 + 恢复中断 */

/* 3. 每CPU变量 */
/* 如果数据只在单个CPU上使用 */
DEFINE_PER_CPU(int, my_counter);

/* 在中断处理函数中 */
int cpu = smp_processor_id();
per_cpu(my_counter, cpu)++;

/* 4. RCU(Read-Copy-Update) */
/* 适用于读多写少的场景 */
rcu_read_lock();
/* 读取共享数据 */
rcu_read_unlock();
bash
# 查看系统的中断分布
cat /proc/interrupts
#            CPU0       CPU1       CPU2       CPU3
#   0:         23          0          0          0  IR-IO-APIC   2-edge      timer
#   1:          3          0          0          0  IR-IO-APIC   1-edge      i8042
#   8:          1          0          0          0  IR-IO-APIC   8-edge      rtc0
#   9:          0          0          0          0  IR-IO-APIC   9-fasteoi   acpi
#  12:         56          0          0          0  IR-IO-APIC  12-edge      i8042
#  16:       1234        567        890        345  IR-IO-APIC  16-fasteoi   ahci[0000:00:1f.2]
#  23:      56789      34567      23456      12345  IR-IO-APIC  23-fasteoi   xhci_hcd
# NMI:        12         10          8          6   Non-maskable interrupts
# LOC:    123456     123456     123456     123456   Local timer interrupts

# 查看软中断统计
cat /proc/softirqs
#            CPU0       CPU1       CPU2       CPU3
# HI:             0          0          0          0
# TIMER:     123456     123456     123456     123456
# NET_TX:       123        456        789        012
# NET_RX:     34567      23456      12345       6789
# BLOCK:       5678       4567       3456       2345
# IRQ_POLL:       0          0          0          0
# TASKLET:     1234        567        890        345
# SCHED:      56789      45678      34567      23456

三、实战案例

3.1 编写简单的中断处理内核模块

c
/* irq_test.c - 简单的中断处理演示模块 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>

MODULE_LICENSE("GPL");

static struct work_struct my_work;

/* 下半部处理函数(工作队列) */
static void bottom_half_handler(struct work_struct *work) {
    printk(KERN_INFO "Bottom half: processing deferred work\n");
    /* 这里可以做耗时的工作 */
    /* 可以睡眠、可以调用可能阻塞的函数 */
}

/* 模拟中断处理(通过软件触发) */
static void simulate_interrupt(void) {
    printk(KERN_INFO "Interrupt triggered!\n");

    /* 上半部:快速处理 */
    printk(KERN_INFO "Top half: quick response\n");

    /* 调度下半部 */
    schedule_work(&my_work);
}

static int __init irq_test_init(void) {
    INIT_WORK(&my_work, bottom_half_handler);

    printk(KERN_INFO "irq_test module loaded\n");

    /* 模拟触发中断 */
    simulate_interrupt();

    return 0;
}

static void __exit irq_test_exit(void) {
    flush_work(&my_work);
    printk(KERN_INFO "irq_test module unloaded\n");
}

module_init(irq_test_init);
module_exit(irq_test_exit);

3.2 分析中断分布

bash
# 查看中断分布,判断是否有中断不平衡
cat /proc/interrupts

# 如果某个CPU的中断数量远多于其他CPU
# 说明中断分布不均衡,可能影响性能

# 设置中断亲和性(将中断绑定到特定CPU)
echo 1 > /proc/irq/16/smp_affinity      # 绑定到 CPU 0
echo 2 > /proc/irq/16/smp_affinity      # 绑定到 CPU 1
echo f > /proc/irq/16/smp_affinity      # 绑定到所有 4 个 CPU

# 查看中断亲和性
cat /proc/irq/16/smp_affinity
# 0000000f

# 使用 irqbalance 守护进程自动平衡中断
systemctl status irqbalance

3.3 观察软中断行为

bash
# 持续监控软中断
watch -n 1 'cat /proc/softirqs | head -5'

# 在高网络负载下观察 NET_RX 软中断的增长
# 终端1:启动网络流量
iperf3 -c server -t 60

# 终端2:观察软中断
watch -n 1 'cat /proc/softirqs | grep -E "NET|TIMER"'

# 查看 ksoftirqd 线程的CPU使用率
top -H -p $(pgrep ksoftirqd)

四、本章小结

本章深入讲解了Linux内核的中断和异常处理机制。中断是异步事件,由外部设备触发;异常是同步事件,由CPU在执行指令时检测到。IDT(中断描述符表)将中断向量号映射到处理函数,CPU根据向量号自动跳转到对应的处理入口。

中断处理分为上半部和下半部。上半部在中断上下文中快速执行,只做最关键的操作;下半部延迟执行,处理耗时的工作。软中断、tasklet和工作队列是三种下半部机制,各有适用场景:软中断性能最高但灵活性最低,工作队列可以在进程上下文执行但延迟较大。

中断处理是内核并发的重要来源。中断上下文不能使用可能睡眠的操作,需要使用原子操作、自旋锁等机制处理并发问题。理解中断处理机制,是编写设备驱动和诊断系统性能问题的基础。


五、思考与练习

思考题

如果中断处理函数执行时间过长,会导致什么问题?如何检测和解决?

为什么软中断可以在多个CPU上同时运行,而tasklet不能?

什么是"中断风暴"?如何防止设备产生过多的中断?

缺页异常处理函数为什么可能睡眠?这与普通中断处理有什么不同?

编程练习

编写一个内核模块,注册一个虚拟的中断处理函数(使用软件中断),实现上半部和下半部的分离。

编写一个程序,持续监控/proc/interrupts和/proc/softirqs,统计各中断和软中断的频率。

编写一个内核模块,使用工作队列处理"中断"的延迟工作,验证工作队列在进程上下文中执行。


9/10中断和异
0%