Linux中断处理机制:上半部与下半部协作模型深度剖析

Linux中断处理机制:上半部与下半部协作模型深度剖析 Linux中断处理机制上半部与下半部协作模型深度剖析中断是外设与CPU通信的核心通道。网卡收到数据包、磁盘完成DMA传输、定时器超时触发都需要中断来通知内核。中断处理有两个约束响应要快但处理逻辑可能很重。这两者之间的矛盾催生了Linux中断的两阶段设计。一、中断为何需要两阶段设计当硬件产生中断信号时CPU需要立刻响应。中断处理函数运行期间同优先级或更低优先级的中断会被屏蔽。如果处理函数执行时间过长系统将丢失后续中断表现为丢包、IO延迟飙升。但实际处理逻辑往往很重。以网卡为例收到中断后需要从DMA环形缓冲区拷贝数据包、解析以太网头、走协议栈、送达socket。这些操作在中断上下文中执行会长时间关闭中断造成不可接受的影响。sequenceDiagram participant HW as 硬件设备 participant APIC as APIC中断控制器 participant CPU as CPU核心 participant TH as 上半部(硬中断) participant BH as 下半部(softirq/tasklet/workqueue) HW-APIC: 1. 产生中断信号 APIC-CPU: 2. 路由中断到目标CPU CPU-TH: 3. IDT查中断向量→do_IRQ() TH-TH: 4. 关中断执行注册的处理函数 TH--BH: 5. 调度下半部 Note over TH,BH: 中断上下文禁止睡眠 TH-CPU: 6. 开中断返回 BH-BH: 7. 宽松环境下执行耗时操作 BH-BH: 8. 拷贝数据、协议处理、通知用户态所以Linux将中断处理拆为两部分。上半部工作在中断上下文只做最紧急的事应答中断、搬运关键数据。下半部在更宽松的环境中被调度执行完成耗时的实际处理。两阶段设计在不丢中断的前提下保证了处理的完整性。二、硬中断上半部的执行路径上半部Top Half即硬中断处理程序。它的入口由中断描述符表IDT定义。外部中断到达时CPU通过中断向量号索引IDT最终调用do_IRQ()分发到驱动注册的处理函数。内核提供标准API来注册和释放中断#include linux/interrupt.h int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); void free_irq(unsigned int irq, void *dev);irq为中断号handler是处理函数指针。函数签名为irqreturn_t handler(int irq, void *dev_id)返回IRQ_HANDLED或IRQ_NONE。dev_id用于共享中断线时区分设备。中断处理函数运行在中断上下文必须遵守三条铁律禁止调用可能阻塞的函数msleep、copy_from_user、mutex_lock、禁止访问用户空间内存、执行时间必须极短。以下是PCIe字符设备的上半部实现#include linux/interrupt.h #include linux/pci.h #include linux/slab.h struct my_device { void __iomem *bar0; struct tasklet_struct tasklet; spinlock_t lock; }; static irqreturn_t my_irq_handler(int irq, void *dev_id) { struct my_device *dev dev_id; u32 status; if (!dev || !dev-bar0) return IRQ_NONE; /* 读中断状态确认中断来源 */ status ioread32(dev-bar0 IRQ_STATUS_REG); if (!(status IRQ_DATA_READY)) { dev_warn(((struct pci_dev *)dev_id)-dev, spurious interrupt: status0x%x\n, status); return IRQ_NONE; } /* 应答中断写1清除 */ iowrite32(status, dev-bar0 IRQ_CLEAR_REG); /* 调度下半部 */ tasklet_schedule(dev-tasklet); return IRQ_HANDLED; }三、下半部三剑客softirq、tasklet与workqueue下半部机制有三种选择差别在于执行环境和并发语义。Softirq是最底层机制在内核编译时静态定义。每个CPU维护一个pending位图。硬中断返回前内核检查__softirq_pending在do_softirq()中遍历执行。网络子系统的NET_RX_SOFTIRQ和块设备的BLOCK_SOFTIRQ是典型场景。Softirq在同一CPU上可重入需要自旋锁保护共享数据。驱动开发一般不直接使用softirq而是基于它构造高层机制。Tasklet建立在softirq之上TASKLET_SOFTIRQ对开发者更友好。内核保证同一tasklet不会同时在多CPU上执行简化锁设计。不同tasklet之间仍然可并发。Tasklet始终运行在中断上下文同样不能睡眠。以下是tasklet下半部示例static void my_tasklet_handler(unsigned long data) { struct my_device *dev (struct my_device *)data; u32 *buf; unsigned long irq_flags; buf kmalloc(DATA_BUF_SIZE, GFP_ATOMIC); if (!buf) { dev_err(dev-pdev-dev, tasklet: alloc failed\n); return; } spin_lock_irqsave(dev-lock, irq_flags); memcpy_fromio(buf, dev-bar0 DATA_FIFO_REG, DATA_BUF_SIZE); spin_unlock_irqrestore(dev-lock, irq_flags); /* 通过环形缓冲区交给用户态 */ ring_buffer_push(dev-rb, buf, DATA_BUF_SIZE); kfree(buf); wake_up_interruptible(dev-read_wq); }Workqueue与前两者根本不同它在内核线程中执行允许睡眠。当处理逻辑需要持有互斥锁、操作文件系统、或分配大块内存时workqueue是唯一选择。代价是调度延迟更高。#include linux/workqueue.h static void my_work_handler(struct work_struct *work) { struct my_device *dev container_of(work, struct my_device, work); struct file *fp; loff_t pos 0; fp filp_open(/var/log/device.log, O_WRONLY | O_CREAT | O_APPEND, 0644); if (IS_ERR(fp)) { dev_err(dev-pdev-dev, open log failed: %ld\n, PTR_ERR(fp)); return; } kernel_write(fp, dev-log_buf, dev-log_len, pos); filp_close(fp, NULL); }三者对比如下特性SoftirqTaskletWorkqueue定义方式编译时静态运行时动态运行时动态执行上下文中断上下文中断上下文进程上下文可睡眠否否是并发语义同softirq可并发同tasklet串行无限制典型用户网络、块层设备驱动文件、阻塞IO四、实战完整的驱动中断协作模型综合上下半部下面是一个完整的PCIe字符设备驱动。它注册中断处理函数用tasklet处理数据搬运用workqueue写日志。中断流程图展示从硬件到下半部的完整协作路径。#include linux/module.h #include linux/pci.h #include linux/interrupt.h #include linux/cdev.h #include linux/workqueue.h struct my_device { struct pci_dev *pdev; void __iomem *bar0; int irq_line; struct tasklet_struct tasklet; struct work_struct work_log; struct workqueue_struct *wq; spinlock_t lock; u32 log_buf[256]; int log_len; }; static void my_tasklet_fn(unsigned long data) { struct my_device *dev (struct my_device *)data; u32 *tmp; unsigned long flags; tmp kmalloc(PAGE_SIZE, GFP_ATOMIC); if (!tmp) return; spin_lock_irqsave(dev-lock, flags); memcpy_fromio(tmp, dev-bar0 DATA_REG, PAGE_SIZE); spin_unlock_irqrestore(dev-lock, flags); /* 数据取出后调度日志写入 */ memcpy(dev-log_buf, tmp, min_t(size_t, sizeof(dev-log_buf), PAGE_SIZE)); dev-log_len 128; queue_work(dev-wq, dev-work_log); kfree(tmp); } static void my_work_fn(struct work_struct *work) { struct my_device *dev container_of(work, struct my_device, work_log); dev_info(dev-pdev-dev, data done: %08x\n, dev-log_buf[0]); } static irqreturn_t my_isr(int irq, void *data) { struct my_device *dev data; u32 status; status ioread32(dev-bar0 STATUS_REG); if (!(status IRQ_DATA_READY)) return IRQ_NONE; iowrite32(status, dev-bar0 IRQ_CLEAR); tasklet_schedule(dev-tasklet); return IRQ_HANDLED; } static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct my_device *dev; int ret; dev kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev-pdev pdev; ret pcim_enable_device(pdev); if (ret) goto err_free; ret pcim_iomap_regions(pdev, BIT(0), mydev); if (ret) goto err_free; dev-bar0 pcim_iomap_table(pdev)[0]; pci_set_master(pdev); spin_lock_init(dev-lock); tasklet_init(dev-tasklet, my_tasklet_fn, (unsigned long)dev); dev-wq alloc_workqueue(mydev_wq, 0, 0); if (!dev-wq) { ret -ENOMEM; goto err_free; } INIT_WORK(dev-work_log, my_work_fn); ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (ret 0) goto err_wq; dev-irq_line pci_irq_vector(pdev, 0); ret request_irq(dev-irq_line, my_isr, 0, mydev, dev); if (ret) goto err_vectors; pci_set_drvdata(pdev, dev); return 0; err_vectors: pci_free_irq_vectors(pdev); err_wq: destroy_workqueue(dev-wq); err_free: kfree(dev); return ret; } static void my_remove(struct pci_dev *pdev) { struct my_device *dev pci_get_drvdata(pdev); free_irq(dev-irq_line, dev); pci_free_irq_vectors(pdev); tasklet_kill(dev-tasklet); destroy_workqueue(dev-wq); kfree(dev); } static const struct pci_device_id my_ids[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_ids); static struct pci_driver my_driver { .name mydev, .id_table my_ids, .probe my_probe, .remove my_remove, }; module_pci_driver(my_driver); MODULE_LICENSE(GPL);完整时序图如下sequenceDiagram participant HW as 硬件 participant APIC as APIC participant CPU as CPU participant ISR as 上半部(my_isr) participant TL as Tasklet(my_tasklet_fn) participant WQ as Workqueue(my_work_fn) HW-APIC: 数据就绪拉高中断线 APIC-CPU: INT#路由到目标核 CPU-ISR: 关中断do_IRQ→my_isr ISR-ISR: 读状态寄存器确认中断源 ISR-ISR: 写CLEAR寄存器应答 ISR--TL: tasklet_schedule() ISR-CPU: 开中断返回 CPU-CPU: 检查__softirq_pending CPU-TL: do_softirq→TASKLET_SOFTIRQ TL-TL: BAR0 DMA拷贝数据 TL--WQ: queue_work() WQ-WQ: 内核线程中写日志五、总结中断处理分上半部和下半部上半部在中断上下文极速运行下半部异步完成耗时操作。Softirq是内核级静态机制网络和块层直接使用驱动开发建议用tasklet或workqueue。Tasklet基于softirq同类型串行化执行简化锁设计始终运行在中断上下文。Workqueue运行在进程上下文允许睡眠适合阻塞操作场景代价是调度延迟较高。中断处理函数必须返回IRQ_HANDLED或IRQ_NONE共享中断线需检查设备状态。生产级驱动需正确处理request_irq失败路径、tasklet_kill同步和workqueue销毁顺序。