一文搞定 Linux 中断:从底层原理到驱动实战

张开发
2026/4/8 5:17:42 15 分钟阅读

分享文章

一文搞定 Linux 中断:从底层原理到驱动实战
做Linux驱动开发的同学都懂中断机制就是系统的“隐形应急队长”——不用全程盯着硬件却能让CPU快速响应各类紧急事件悄悄撑起整个系统的高效运转。它是硬件与CPU的通信核心更是实现多任务并发、提升系统利用率的关键搞懂它才能打通驱动开发的任督二脉。从技术角度来讲中断是一种异步通信信号当硬件设备有紧急事务需要处理时会向 CPU 发送中断信号。比如当你在键盘上敲击一个按键键盘控制器会立刻向 CPU 发送中断请求告知它有新的输入需要处理又或者网卡接收到网络数据包时也会通过中断信号通知 CPU 有数据到来。有了中断机制CPU 就无需像无头苍蝇一样时刻不停地轮询各个硬件设备的状态极大地提高了系统资源的利用率和整体运行效率。可以说中断是 Linux 系统高效管理硬件设备、提升系统并发能力的核心所在。无论是驱动开发人员编写设备驱动程序还是内核优化工程师对系统性能进行调优都离不开对中断机制的深入理解和熟练运用。一、吃透Linux中断的核心原理1.1 中断的本质中断本质就是给CPU发“加急电报”逼它暂停当前任务优先处理紧急事件。不管是硬件触发还是软件触发最终都是让CPU切换执行流程这也是异步处理的核心逻辑。我们先区分硬件中断和软件中断的核心差异再看一段简单代码直观感受软件中断的触发以系统调用为例本质是软中断#include stdio.h #include unistd.h int main() { // read系统调用会触发软中断切换到内核态执行 char buf[1024]; ssize_t ret read(STDIN_FILENO, buf, sizeof(buf)-1); // 触发软中断 if (ret 0) { buf[ret] \0; printf(读取到内容%s\n, buf); } return 0; }这段代码里read函数调用会触发软中断x86架构下是syscall指令让CPU从用户态切换到内核态执行内核的文件读取逻辑完成后再返回用户态——这就是软件中断的典型应用。在理解中断机制时对比轮询机制能让我们更清晰地认识到中断的优势。轮询是 CPU 主动周期性地检查设备状态。就像你每隔一段时间就去看看手机有没有新消息不管有没有消息你都要去检查一下。在计算机中早期的打印机驱动就常采用轮询方式CPU 需要不断查询打印机的状态看它是否完成打印任务是否有卡纸等问题 。这种方式虽然简单直接但缺点也很明显那就是浪费 CPU 资源。因为在大多数情况下打印机可能处于空闲状态CPU 却还在不断地查询做了很多无用功。而中断则不同它是设备主动通知 CPU。还是以手机为例当有新消息来时手机会主动发出提示音中断信号这时你才去查看手机而不是一直不停地去看手机。在现代计算机中像 SSD 硬盘就采用中断方式与 CPU 通信当硬盘完成数据读写操作后会主动向 CPU 发送中断信号通知 CPU 数据已准备好。这样 CPU 就无需在空闲时不断查询硬盘状态大大提高了效率。从本质上讲中断是一种异步机制设备可以在任何时候向 CPU 发送中断请求而轮询是同步机制CPU 按照固定的周期去检查设备状态。中断机制的出现显著提升了系统资源的利用率让 CPU 能够更高效地处理各种任务。1.2 中断的两大分类中断的两大分类——硬件中断和软件中断硬件中断外设发起的“求助”异步触发、优先级高比如键盘敲击、网卡收包、磁盘读写完成。每个硬件中断都有唯一IRQ号由APIC中断控制器管理处理完立即恢复原任务。# 查看所有硬件中断的触发次数、绑定CPU cat /proc/interrupts输出结果中第一列是IRQ号后面是各CPU的中断次数最后一列是设备名称比如eth0对应的就是网卡的硬件中断。软件中断内核或程序主动触发优先级低无硬件参与核心用于系统调用、定时器、软中断处理。除了上面的系统调用示例内核中的软中断还可以通过代码主动触发比如#include linux/interrupt.h #include linux/module.h // 定义软中断处理函数 static void my_softirq_handler(struct softirq_action *action) { printk(KERN_INFO 软中断触发执行处理逻辑\n); } static int __init softirq_init(void) { // 注册软中断HI_SOFTIRQ是高优先级软中断 open_softirq(HI_SOFTIRQ, my_softirq_handler); // 触发软中断 raise_softirq(HI_SOFTIRQ); return 0; } static void __exit softirq_exit(void) { printk(KERN_INFO 软中断模块卸载\n); } module_init(softirq_init); module_exit(softirq_exit); MODULE_LICENSE(GPL);这段内核模块代码注册并触发了一个高优先级软中断适合处理内核内部的紧急异步任务注意软中断运行于中断上下文不能睡眠。1.3 关键机制中断的 “上下半部” 分工协作最核心的考点来了——中断上下半部分工。为啥要拆分很简单既要快速响应中断避免数据丢失又不能让耗时操作阻塞其他中断所以拆成“紧急处理”和“后续收尾”两步先看完整的分工示例代码再拆解细节。上半部即中断服务程序ISRInterrupt Service Routine是中断发生后 CPU 首先执行的代码负责紧急处理读取硬件状态、清除中断标志避免重复触发、保存关键数据到内存。同时需要快速返回尽量缩短执行时间通常几微秒不做复杂操作避免阻塞其他中断。其特点是执行时关闭当前中断同类型中断被屏蔽但可响应更高优先级中断运行在中断上下文非进程上下文无进程调度不能睡眠或阻塞。下半部有3种实现实操中最常用的是tasklet和工作队列案例1Tasklet实现中等耗时、不可睡眠#include linux/interrupt.h #include linux/module.h // 定义tasklet处理函数 static void my_tasklet_handler(unsigned long data) { printk(KERN_INFO Tasklet执行处理耗时操作data: %ld\n, data); // 这里可以做数据包解析、状态更新等中等耗时操作不可睡眠 } // 声明并初始化tasklet DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 123); // 最后一个参数是传递给handler的数据 // 中断上半部ISR static irqreturn_t my_irq_handler(int irq, void *dev_id) { printk(KERN_INFO 中断上半部触发处理紧急任务\n); // 1. 清除中断标志关键避免重复触发 // clear_irq_flag(irq); // 按需调用不同硬件写法不同 // 2. 保存关键数据省略根据硬件需求编写 // 3. 调度tasklet执行下半部 tasklet_schedule(my_tasklet); return IRQ_HANDLED; } static int __init irq_init(void) { int irq 10; // 假设中断号为10需根据实际硬件修改 // 注册中断上半部 if (request_irq(irq, my_irq_handler, IRQF_SHARED, my_irq_dev, my_tasklet)) { printk(KERN_ERR 中断注册失败\n); return -1; } printk(KERN_INFO 中断模块初始化成功\n); return 0; } static void __exit irq_exit(void) { int irq 10; free_irq(irq, my_tasklet); // 释放中断 tasklet_kill(my_tasklet); // 确保tasklet执行完成 printk(KERN_INFO 中断模块卸载成功\n); } module_init(irq_init); module_exit(irq_exit); MODULE_LICENSE(GPL);案例2工作队列实现需睡眠、高耗时#include linux/workqueue.h #include linux/interrupt.h #include linux/module.h // 定义工作队列和工作项 static struct workqueue_struct *my_workqueue; static struct work_struct my_work; // 工作队列处理函数下半部进程上下文可睡眠 static void my_work_handler(struct work_struct *work) { printk(KERN_INFO 工作队列执行可执行睡眠操作\n); msleep(100); // 模拟耗时操作可正常睡眠 printk(KERN_INFO 工作队列处理完成\n); } // 中断上半部ISR static irqreturn_t my_irq_handler(int irq, void *dev_id) { printk(KERN_INFO 中断上半部触发处理紧急任务\n); // 调度工作队列执行下半部 queue_work(my_workqueue, my_work); return IRQ_HANDLED; } static int __init irq_init(void) { int irq 10; // 创建工作队列 my_workqueue create_workqueue(my_workqueue); if (!my_workqueue) { printk(KERN_ERR 工作队列创建失败\n); return -1; } // 初始化工作项 INIT_WORK(my_work, my_work_handler); // 注册中断 if (request_irq(irq, my_irq_handler, IRQF_SHARED, my_irq_dev, my_work)) { printk(KERN_ERR 中断注册失败\n); destroy_workqueue(my_workqueue); return -1; } printk(KERN_INFO 中断模块初始化成功\n); return 0; } static void __exit irq_exit(void) { int irq 10; free_irq(irq, my_work); // 销毁工作队列等待所有工作执行完成 destroy_workqueue(my_workqueue); printk(KERN_INFO 中断模块卸载成功\n); } module_init(irq_init); module_exit(irq_exit); MODULE_LICENSE(GPL);不可睡眠用tasklet需睡眠用工作队列二者搭配上半部就能完美平衡响应速度和处理效率。二、实战从零开始玩转Linux中断2.1 中断的注册与释放request_irq 与 free_irq实操第一步先搞定中断的注册与释放——这是驱动开发的基础操作就像给硬件和CPU“搭通信桥”一步错就可能导致资源泄漏先看完整的实操代码再拆解参数细节。中断注册是驱动开发的第一步它通过 request_irq 函数将中断处理程序与特定的中断号绑定起来。在 Linux 内核中request_irq 函数的原型如下int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);irq这是要注册的中断号每个硬件设备都有对应的中断号它是识别设备中断请求的关键标识。例如在常见的嵌入式开发板中网卡设备可能被分配到中断号 10当网卡有数据接收或发送完成等事件时就会通过这个中断号向 CPU 发送中断请求 。handler指向中断处理函数的指针当中断发生时系统会调用这个函数来处理中断事件。比如编写一个简单的按键中断处理函数在函数内部实现读取按键状态、判断按键是否按下等逻辑 。static irqreturn_t button_irq_handler(int irq, void *dev_id) { // 读取按键状态寄存器 unsigned int button_state read_button_status(); if (button_state BUTTON_PRESSED) { // 处理按键按下的逻辑例如打印信息 printk(KERN_INFO Button pressed!\n); } return IRQ_HANDLED; }flags中断标志决定中断的行为重点记3个常用值IRQF_SHARED允许多个设备共享同一个中断号实操中常用节省IRQ资源IRQF_TRIGGER_RISING上升沿触发比如按键按下时触发中断IRQF_TRIGGER_FALLING下降沿触发比如按键松开时触发。name设备名称会显示在/proc/interrupts中方便调试比如命名为“key_irq”就能快速找到按键中断的信息。dev私有数据指针共享中断时必须唯一用于区分不同设备通常传递设备结构体指针。当设备不再使用中断时就需要调用 free_irq 函数来释放中断资源避免资源泄漏。free_irq 函数的原型如下void free_irq(unsigned int irq, void *dev_id);这里的 irq 参数与 request_irq 中注册的中断号一致dev_id 参数也必须与注册时传入的 dev 参数相同这样系统才能准确地找到要释放的中断资源。比如在驱动模块卸载时需要调用 free_irq 来释放之前注册的中断static int __init my_driver_init(void) { // 注册中断 int ret request_irq(IRQ_NUMBER, my_irq_handler, IRQF_SHARED, my_device, my_dev); if (ret) { printk(KERN_ERR Failed to request IRQ\n); return ret; } // 其他初始化操作 return 0; } static void __exit my_driver_exit(void) { // 释放中断 free_irq(IRQ_NUMBER, my_dev); // 其他清理操作 } module_init(my_driver_init); module_exit(my_driver_exit);在系统运行过程中可以通过 /proc/interrupts 文件来查看系统中断的使用状态该文件会列出每个中断号对应的中断发生次数、中断所属的 CPU 以及设备名称等信息为我们调试和优化中断处理提供了重要依据。2.2 中断处理函数的编写规范中断处理函数是核心写得不好会导致系统卡顿甚至崩溃记住一个核心原则短小精悍、不阻塞、不睡眠先看规范写法再避坑。中断处理函数运行在中断上下文没有进程调度机制一旦调用睡眠函数比如msleep、kmalloc就会导致系统死锁——这是新手最常踩的坑。错误案例禁止这么写static irqreturn_t bad_irq_handler(int irq, void *dev_id) { char *buf kmalloc(1024, GFP_KERNEL); // 错误GFP_KERNEL可能睡眠 if (!buf) return IRQ_HANDLED; msleep(10); // 错误直接睡眠导致死锁 kfree(buf); return IRQ_HANDLED; }正确案例规范写法static irqreturn_t good_irq_handler(int irq, void *dev_id) { // 仅处理紧急任务读取状态、清除中断标志 struct key_dev *dev (struct key_dev *)dev_id; if (readl(dev-reg_base STATUS_REG) IRQ_FLAG) { writel(0, dev-reg_base IRQ_CLEAR_REG); // 清除中断标志 // 耗时操作交给下半部tasklet/工作队列 tasklet_schedule(dev-key_tasklet); return IRQ_HANDLED; } return IRQ_NONE; // 非本设备中断返回NONE }如果必须分配内存可用GFP_ATOMIC标志原子分配不睡眠但尽量减少中断上下文的内存操作优先交给下半部。中断处理函数的返回值也有特定的含义。它的返回类型是 irqreturn_t通常有两种取值IRQ_HANDLED 和 IRQ_NONE。当返回 IRQ_HANDLED 时表示中断已经被当前处理函数成功处理系统会认为这个中断事件已经得到妥善解决而当返回 IRQ_NONE 时一般用于共享中断的场景表示这个中断不是由当前设备触发的系统会继续查找其他可能的中断处理函数来处理这个中断。例如在一个共享中断的驱动程序中中断处理函数可能会这样编写static irqreturn_t shared_irq_handler(int irq, void *dev_id) { // 检查是否是本设备的中断 if (is_my_device_interrupt(dev_id)) { // 处理本设备的中断逻辑如读取硬件状态、清除中断标志等 read_device_status(); clear_interrupt_flag(); return IRQ_HANDLED; } return IRQ_NONE; }在中断处理函数的核心逻辑部分应仅保留那些必须立即执行的关键操作如读取硬件设备的状态寄存器获取设备当前的工作状态清除中断标志避免中断的重复触发确保系统能够正常处理后续的中断请求等。对于那些耗时较长的任务如数据的复杂处理、大量数据的传输等不应该放在中断处理函数中执行而应将其移交给下半部机制来处理以保证中断处理函数能够快速返回使系统尽快恢复对其他中断的响应能力。2.3 上下半部实战配置Tasklet 与工作队列上下半部的选型的核心的是“是否需要睡眠”前面已经给过基础案例这里补充一个“上半部tasklet工作队列”的组合实操案例覆盖复杂场景直接套用即可。Tasklet 是一种基于软中断实现的轻量级下半部机制适用于那些中等耗时且不允许睡眠的任务。它的执行效率较高并且支持在多 CPU 环境下并行运行。在使用 Tasklet 时首先需要通过 DECLARE_TASKLET 宏来声明一个 Tasklet。例如我们定义一个用于处理网络数据包的 Taskletstatic void packet_process_tasklet(unsigned long data) { // 处理网络数据包的逻辑如解析数据包头部、校验数据等 struct network_packet *packet (struct network_packet *)data; parse_packet_header(packet); check_packet_integrity(packet); // 其他处理操作 } DECLARE_TASKLET(packet_process_tasklet, packet_process_tasklet, (unsigned long)packet);当需要调度 Tasklet 执行时调用 tasklet_schedule 函数即可。例如在上半部的中断处理函数中读取完网络数据包后就可以调度 Tasklet 来进行后续的处理irqreturn_t network_irq_handler(int irq, void *dev_id) { // 读取网络数据包到缓冲区 read_network_packet(buffer); // 清除中断标志 clear_network_interrupt_flag(); // 调度Tasklet处理数据包 tasklet_schedule(packet_process_tasklet); return IRQ_HANDLED; }工作队列则适用于那些需要睡眠或执行时间较长的任务它是通过内核线程来执行任务的因此运行在进程上下文可以使用一些在中断上下文中不能使用的函数如 kmalloc、msleep 等。以 I2C 设备的数据读取为例由于 I2C 通信速度相对较慢数据读取操作可能会耗时较长并且在等待数据传输完成的过程中可以睡眠这种情况下就非常适合使用工作队列。首先定义一个工作队列和工作处理函数static void i2c_read_work_handler(struct work_struct *work) { struct i2c_device *i2c_dev container_of(work, struct i2c_device, work); // 进行I2C设备数据读取操作可能会睡眠 i2c_read_data(i2c_dev, buffer); // 处理读取到的数据 process_i2c_data(buffer); } struct workqueue_struct *i2c_workqueue; struct work_struct i2c_read_work;然后在驱动初始化时初始化工作队列和工作项static int __init i2c_driver_init(void) { i2c_workqueue create_workqueue(i2c_workqueue); if (!i2c_workqueue) { printk(KERN_ERR Failed to create workqueue\n); return -ENOMEM; } INIT_WORK(i2c_read_work, i2c_read_work_handler); // 其他初始化操作 return 0; }当需要读取 I2C 设备数据时将工作项提交到工作队列中void trigger_i2c_read(struct i2c_device *i2c_dev) { queue_work(i2c_workqueue, i2c_dev-work); }通过合理选择和配置 Tasklet 与工作队列能够将中断处理中的不同任务分配到最合适的执行环境中充分发挥它们各自的优势从而最大化系统的中断处理效率提升整个系统的性能和稳定性。目标大厂Linux C/C后端岗想找套科学系统的进阶指南避开学习弯路必看【大厂标准】Linux C/C 后端进阶学习路线想挖一挖操作系统底层技术突破自己的技术上限《Linux内核学习指南硬核修炼手册》分享超实用的学习方法适合愿意沉下心钻研的小伙伴三、Linux中断使用的常见问题与解决方法3.1 踩坑点 1中断上下文禁止睡眠实操中最容易踩的坑就是——在中断上下文调用睡眠函数很多新手因为忽略这个规则导致系统死锁先拆解原因再给解决方案和实操修正代码。先明确哪些函数会导致睡眠除了msleep、kmallocGFP_KERNEL还有mutex_lock互斥锁、copy_to_user用户空间拷贝可能阻塞等这些函数在中断上下文绝对不能用。看一个新手常写的错误代码再修正错误代码中断上下文调用互斥锁导致死锁static struct mutex my_mutex; static irqreturn_t bad_irq_handler(int irq, void *dev_id) { mutex_lock(my_mutex); // 错误mutex_lock可能睡眠中断上下文禁止使用 // 处理中断逻辑 mutex_unlock(my_mutex); return IRQ_HANDLED; }正确解决方案用原子锁或移交下半部// 方案1用原子锁适合简单同步不睡眠 static atomic_t my_lock ATOMIC_INIT(1); static irqreturn_t good_irq_handler1(int irq, void *dev_id) { if (atomic_dec_and_test(my_lock)) { // 原子操作不睡眠 // 处理中断逻辑 atomic_set(my_lock, 1); // 释放锁 } return IRQ_HANDLED; } // 方案2移交到工作队列适合复杂同步可睡眠 static struct work_struct my_work; static struct mutex my_mutex; static void work_handler(struct work_struct *work) { mutex_lock(my_mutex); // 工作队列是进程上下文可正常使用 // 处理耗时/阻塞操作 mutex_unlock(my_mutex); } static irqreturn_t good_irq_handler2(int irq, void *dev_id) { queue_work(my_workqueue, my_work); // 调度工作队列 return IRQ_HANDLED; }为了避免这种情况在编写中断处理函数时要严格遵守中断上下文的规则。对于那些可能会阻塞的操作要将它们迁移到工作队列中执行。工作队列是一种基于内核线程的机制它运行在进程上下文可以进行睡眠、阻塞等操作。在中断处理函数中只保留那些必须立即执行的紧急任务如读取硬件状态、清除中断标志等然后将其他耗时较长或可能阻塞的任务交给工作队列处理 。3.2 踩坑点 2中断共享的冲突处理中断资源有限多个设备共享IRQ是常态但处理不当会导致“误响应”——比如设备A触发中断设备B的处理函数却被调用核心解决思路是“唯一标识设备校验”看完整实操代码。在注册共享中断时需要设置 IRQF_SHARED 标志告诉系统这个中断是可以被多个设备共享的。每个设备在注册中断时其 dev 参数必须是唯一的这个参数就像是设备的 “身份证”用于在中断处理时区分不同的设备 。在中断处理函数中首先要做的就是检查设备状态确认这个中断是否是由本设备触发的。通常可以通过读取设备的状态寄存器或者检查设备的特定标识来判断。如果确认是本设备触发的中断就进行相应的处理如果不是就立即返回 IRQ_NONE避免误处理其他设备的中断请求 。以两个SPI设备共享IRQ为例完整规范的驱动代码如下重点关注dev_id的唯一性和设备校验逻辑#include linux/interrupt.h #include linux/module.h // 定义两个SPI设备结构体dev_id唯一 struct spi_device dev1 {.name spi_dev1, .irq 10}; struct spi_device dev2 {.name spi_dev2, .irq 10}; // 设备1的中断处理函数 static irqreturn_t spi_dev1_irq(int irq, void *dev_id) { struct spi_device *dev (struct spi_device *)dev_id; // 关键校验是否是本设备的中断通过设备状态寄存器 if (readl(dev-reg_base IRQ_STATUS) DEV1_IRQ_FLAG) { writel(0, dev-reg_base IRQ_CLEAR); // 清除中断标志 printk(KERN_INFO 设备1中断处理\n); return IRQ_HANDLED; } return IRQ_NONE; // 不是本设备返回NONE } // 设备2的中断处理函数 static irqreturn_t spi_dev2_irq(int irq, void *dev_id) { struct spi_device *dev (struct spi_device *)dev_id; if (readl(dev-reg_base IRQ_STATUS) DEV2_IRQ_FLAG) { writel(0, dev-reg_base IRQ_CLEAR); printk(KERN_INFO 设备2中断处理\n); return IRQ_HANDLED; } return IRQ_NONE; } static int __init shared_irq_init(void) { // 注册共享中断必须设置IRQF_SHAREDdev_id分别传递两个设备 if (request_irq(10, spi_dev1_irq, IRQF_SHARED, spi_dev1, dev1)) { printk(KERN_ERR 设备1中断注册失败\n); return -1; } if (request_irq(10, spi_dev2_irq, IRQF_SHARED, spi_dev2, dev2)) { printk(KERN_ERR 设备2中断注册失败\n); free_irq(10, dev1); return -1; } printk(KERN_INFO 共享中断注册成功\n); return 0; } static void __exit shared_irq_exit(void) { free_irq(10, dev1); // 释放时dev_id必须和注册时一致 free_irq(10, dev2); } module_init(shared_irq_init); module_exit(shared_irq_exit); MODULE_LICENSE(GPL);关键注意点共享中断的所有设备注册时必须设置IRQF_SHARED且dev_id不能相同否则会注册失败或触发误响应。// 设备1的中断处理函数 static irqreturn_t spi_device1_irq_handler(int irq, void *dev_id) { struct spi_device *spi_dev (struct spi_device *)dev_id; // 检查是否是设备1的中断 if (spi_dev spi_device1) { // 处理设备1的中断逻辑如读取SPI数据等 spi_read_data(spi_dev, buffer); return IRQ_HANDLED; } return IRQ_NONE; } // 设备2的中断处理函数 static irqreturn_t spi_device2_irq_handler(int irq, void *dev_id) { struct spi_device *spi_dev (struct spi_device *)dev_id; // 检查是否是设备2的中断 if (spi_dev spi_device2) { // 处理设备2的中断逻辑 spi_write_data(spi_dev, buffer); return IRQ_HANDLED; } return IRQ_NONE; }在上述代码中每个中断处理函数都会先检查传入的 dev_id 参数确认是否是自己设备的中断然后再进行相应的处理这样就能有效地避免中断共享时的冲突问题 。3.3 踩坑点 3软中断 CPU 占用过高高并发场景比如高流量网卡最容易出现软中断CPU占用过高表现为top命令中si%软中断占用率飙升导致系统卡顿先找原因再给3个实操优化方案和代码。以网络收发包软中断为例当网络流量较大时网卡会频繁地产生中断触发软中断进行数据包的处理。如果软中断处理函数中包含大量的复杂计算如数据包的深度解析、复杂的协议转换等就会使 CPU 长时间忙于处理软中断导致其他任务无法及时得到 CPU 资源系统响应变慢 。3个实操优化方案从简单到复杂按需选用方案1优化软中断处理逻辑减少冗余计算// 优化前多次内存拷贝冗余计算 static void bad_softirq_handler(struct softirq_action *action) { char buf[1024]; for (int i 0; i 100; i) { memcpy(buf, net_buf, 1024); // 冗余拷贝 parse_data(buf); // 重复解析 } } // 优化后减少拷贝批量处理 static void good_softirq_handler(struct softirq_action *action) { char buf[1024]; memcpy(buf, net_buf, 1024); // 仅拷贝一次 parse_data_batch(buf, 100); // 批量解析减少循环冗余 }方案2调整中断亲和性分散CPU压力通过命令将不同软中断分配到不同CPU核心避免单个CPU过载# 查看中断亲和性以IRQ 10为例 cat /proc/irq/10/smp_affinity # 设置IRQ 10仅在CPU 0和1上运行十六进制0x3对应二进制11 echo 3 /proc/irq/10/smp_affinity方案3限制软中断单次运行时间内核参数调优# 临时设置限制软中断单次运行最大时间单位jiffies1jiffies≈10ms sysctl -w net.core.netdev_budget100 # 永久设置写入/etc/sysctl.conf echo net.core.netdev_budget100 /etc/sysctl.conf sysctl -p补充netdev_budget默认值是300值越小软中断单次运行时间越短越不容易占用过多CPU但会增加软中断调度次数需根据实际场景调整。四、典型场景Linux中断的“高光时刻”4.1 驱动开发场景触摸屏 / 传感器中断处理嵌入式驱动开发中触摸屏和传感器是中断的高频应用场景核心需求是“快速响应耗时处理分离”这里给一个触摸屏中断的完整驱动代码贴合实际开发场景。触摸屏中断的完整驱动实现上半部工作队列#include linux/module.h #include linux/interrupt.h #include linux/workqueue.h #include linux/platform_device.h // 触摸屏设备结构体 struct touchscreen_dev { int irq; void __iomem *reg_base; struct work_struct work; int x, y; // 触摸坐标 }; static struct touchscreen_dev ts_dev; static struct workqueue_struct *ts_workqueue; // 工作队列处理函数下半部处理耗时操作 static void ts_work_handler(struct work_struct *work) { struct touchscreen_dev *dev container_of(work, struct touchscreen_dev, work); // 1. I2C传输坐标数据耗时可睡眠 dev-x readl(dev-reg_base X_REG); dev-y readl(dev-reg_base Y_REG); // 2. 坐标校准耗时计算 dev-x (dev-x - 100) * 1080 / 2000; // 模拟校准公式 dev-y (dev-y - 100) * 1920 / 3000; // 3. 上报触摸事件省略调用input子系统接口 printk(KERN_INFO 触摸坐标x%d, y%d\n, dev-x, dev-y); } // 中断上半部快速响应 static irqreturn_t ts_irq_handler(int irq, void *dev_id) { struct touchscreen_dev *dev (struct touchscreen_dev *)dev_id; // 1. 屏蔽中断避免重复触发 disable_irq_nosync(dev-irq); // 2. 清除中断标志 writel(0, dev-reg_base IRQ_CLEAR_REG); // 3. 调度工作队列 queue_work(ts_workqueue, dev-work); // 4. 重新使能中断 enable_irq(dev-irq); return IRQ_HANDLED; } static int ts_probe(struct platform_device *pdev) { // 1. 申请IO内存省略根据设备树配置 ts_dev.reg_base ioremap(0x12340000, 0x100); // 2. 获取中断号从设备树获取更贴合实际 ts_dev.irq platform_get_irq(pdev, 0); // 3. 创建工作队列 ts_workqueue create_workqueue(touchscreen_workqueue); if (!ts_workqueue) return -ENOMEM; // 4. 初始化工作项 INIT_WORK(ts_dev.work, ts_work_handler); // 5. 注册中断 if (request_irq(ts_dev.irq, ts_irq_handler, IRQF_TRIGGER_RISING, touchscreen, ts_dev)) { printk(KERN_ERR 触摸屏中断注册失败\n); iounmap(ts_dev.reg_base); destroy_workqueue(ts_workqueue); return -1; } printk(KERN_INFO 触摸屏驱动初始化成功\n); return 0; } static int ts_remove(struct platform_device *pdev) { free_irq(ts_dev.irq, ts_dev); destroy_workqueue(ts_workqueue); iounmap(ts_dev.reg_base); return 0; } static struct platform_driver ts_driver { .probe ts_probe, .remove ts_remove, .driver { .name touchscreen, }, }; module_platform_driver(ts_driver); MODULE_LICENSE(GPL);下半部通过工作队列来实现。工作队列是一种基于内核线程的机制它运行在进程上下文能够执行可能会阻塞的操作。在触摸屏的例子中工作队列会启动一个内核线程该线程负责将上半部获取的原始触摸坐标数据通过 I2C 总线传输给处理器。在传输过程中它可能会遇到总线繁忙等情况需要进行短暂的等待睡眠这在中断上下文中是不允许的但在工作队列中却可以正常执行 。数据传输完成后内核线程还会对坐标数据进行校准处理考虑到触摸屏的硬件特性和显示屏幕的分辨率差异通过特定的算法将原始坐标转换为屏幕上的实际坐标最终将处理好的触摸事件上报给系统供上层应用程序使用。同样在传感器驱动中以加速度传感器为例当传感器检测到加速度的变化超过设定阈值时会触发硬件中断。上半部快速响应读取传感器的状态寄存器和数据寄存器记录下加速度的原始数据然后标记数据有效。下半部的工作队列则负责将这些原始数据进行滤波处理去除噪声干扰再根据传感器的校准参数进行数据校准最后将处理后的加速度数据发送给需要的应用程序如用于运动检测的健身应用或者自动旋转屏幕的系统功能。4.2 网络场景软中断处理数据包收发网络通信中中断的核心作用是“快速收包高效处理”尤其是高并发场景全靠硬件中断软中断的协同这里拆解网卡收包的中断流程补核心代码片段。当网卡接收到网络数据包时首先触发的是硬件中断。硬件中断的优先级较高它会立即打断 CPU 当前正在执行的任务CPU 迅速响应执行网卡的中断服务程序ISR。在这个上半部处理阶段ISR 的主要任务是快速标记数据包的到达状态确保数据包不会丢失 。它会检查网卡的接收队列状态确认数据包已经正确地存储到了内存中的接收缓冲区。为了尽快恢复系统对其他中断的响应能力ISR 并不会对数据包进行深入处理而是简单地设置一个标志位表示有新的数据包到达然后立即触发软中断。网卡收包的中断处理核心代码简化版贴合内核实际逻辑#include linux/netdevice.h #include linux/interrupt.h struct net_device *eth_dev; // 软中断处理函数处理数据包解析 static void net_rx_softirq_handler(struct softirq_action *action) { struct sk_buff *skb; // 循环处理接收队列中的数据包 while ((skb dev_alloc_skb(1500))) { // 1. 解析链路层帧头 struct ethhdr *eth eth_hdr(skb); // 2. 解析网络层IP头 struct iphdr *ip ip_hdr(skb); // 3. 分发到对应协议栈TCP/UDP netif_rx(skb); } } // 网卡中断上半部快速收包 static irqreturn_t eth_irq_handler(int irq, void *dev_id) { struct net_device *dev (struct net_device *)dev_id; struct sk_buff *skb; // 1. 从网卡接收缓冲区读取数据包 skb dev_alloc_skb(1500); if (!skb) return IRQ_HANDLED; // 2. 清除网卡中断标志 writel(0, dev-base_addr IRQ_CLEAR); // 3. 触发软中断处理数据包解析 raise_softirq(NET_RX_SOFTIRQ); return IRQ_HANDLED; } // 驱动初始化简化版 static int eth_driver_init(void) { eth_dev alloc_netdev(0, eth0, ether_setup); if (!eth_dev) return -ENOMEM; eth_dev-irq 11; // 网卡中断号 // 注册中断 request_irq(eth_dev-irq, eth_irq_handler, IRQF_SHARED, eth0, eth_dev); // 注册网络接收软中断内核已定义这里仅演示 open_softirq(NET_RX_SOFTIRQ, net_rx_softirq_handler); return 0; } module_init(eth_driver_init); MODULE_LICENSE(GPL);在高并发的网络通信场景中这种硬件中断与软中断协同工作的模式展现出了强大的优势。通过将数据包的快速接收和复杂处理分开使得网络中断的响应时间能够缩短至微秒级。在一个繁忙的 Web 服务器中大量的客户端同时发送 HTTP 请求网卡不断接收到数据包。硬件中断快速响应及时标记数据包到达软中断则高效地处理这些数据包将它们准确地分发到对应的 Web 应用程序进程中确保服务器能够稳定、高效地处理高并发的网络请求 。4.3 定时器场景上下半部实现精准定时定时器中断是系统定时任务的基础比如按键去抖、周期性巡检核心是“上下半部协同实现精准定时”这里给两个实操案例覆盖短定时和长定时场景。Linux 定时器中断的触发源头是系统时钟系统时钟按照固定的频率产生时钟中断信号这个频率通常是 100Hz 到 1000Hz 不等也就是每 1 毫秒到 10 毫秒产生一次中断 。当定时器中断发生时上半部首先响应。上半部的主要任务是更新系统的全局节拍数jiffies这个节拍数记录了系统自启动以来的时钟中断次数是系统实现定时功能的重要依据 。上半部还会检查是否有定时器到期。它遍历定时器链表对比每个定时器的设定时间和当前的全局节拍数如果发现某个定时器的设定时间已到就标记该定时器到期并触发下半部处理。案例1tasklet实现按键去抖短定时、高实时性#include linux/interrupt.h #include linux/timer.h static struct timer_list key_timer; static int key_state; // tasklet处理函数去抖逻辑 static void key_debounce_tasklet(unsigned long data) { // 再次读取按键状态确认是否稳定 int new_state read_key_state(); if (new_state key_state) { printk(KERN_INFO 按键状态稳定%s\n, new_state ? 按下 : 松开); } } DECLARE_TASKLET(key_tasklet, key_debounce_tasklet, 0); // 定时器中断处理函数上半部 static void key_timer_handler(unsigned long data) { key_state read_key_state(); // 读取按键状态 tasklet_schedule(key_tasklet); // 调度tasklet处理去抖 // 重新启动定时器10ms后再次触发实现持续检测 mod_timer(key_timer, jiffies msecs_to_jiffies(10)); } static int __init key_debounce_init(void) { // 初始化定时器10ms后首次触发 init_timer(key_timer); key_timer.function key_timer_handler; key_timer.expires jiffies msecs_to_jiffies(10); add_timer(key_timer); return 0; } static void __exit key_debounce_exit(void) { del_timer(key_timer); tasklet_kill(key_tasklet); } module_init(key_debounce_init); module_exit(key_debounce_exit); MODULE_LICENSE(GPL);案例2工作队列实现周期性系统巡检长定时、可睡眠#include linux/workqueue.h #include linux/timer.h static struct timer_list check_timer; static struct work_struct check_work; static struct workqueue_struct *check_workqueue; // 工作队列处理函数巡检逻辑可睡眠 static void system_check_work(struct work_struct *work) { printk(KERN_INFO 开始系统巡检...\n); msleep(500); // 模拟巡检耗时操作 // 磁盘空间检查、日志清理等逻辑省略 printk(KERN_INFO 系统巡检完成\n); } // 定时器中断处理函数上半部 static void check_timer_handler(unsigned long data) { queue_work(check_workqueue, check_work); // 调度工作队列 // 1小时后再次触发3600000ms mod_timer(check_timer, jiffies msecs_to_jiffies(3600000)); } static int __init system_check_init(void) { // 初始化工作队列和工作项 check_workqueue create_workqueue(system_check); INIT_WORK(check_work, system_check_work); // 初始化定时器1小时后首次触发 init_timer(check_timer); check_timer.function check_timer_handler; check_timer.expires jiffies msecs_to_jiffies(3600000); add_timer(check_timer); return 0; } static void __exit system_check_exit(void) { del_timer(check_timer); destroy_workqueue(check_workqueue); } module_init(system_check_init); module_exit(system_check_exit); MODULE_LICENSE(GPL);而对于那些执行时间较长、可能需要进行阻塞操作的定时任务如周期性的系统巡检工作队列则是更合适的选择。假设系统需要每小时进行一次全面的磁盘空间检查和系统日志清理等操作这些任务可能涉及大量的文件读写和数据处理耗时较长。在定时器中断的上半部标记任务到期后下半部将这些任务交给工作队列处理。工作队列会启动一个内核线程该线程可以在执行过程中进行睡眠、阻塞等操作不会影响中断上下文的正常运行 。内核线程按照预定的逻辑依次检查磁盘空间清理过期的系统日志文件确保系统的稳定运行和资源的合理利用。

更多文章