linux中断和响应
1. 驱动中断函数基础
中断分为向量中断和非向量中断。向量中断为不同的中断分配不同的中断号,中断到来后自动跳转到地址执行,是硬件提供中断服务程序入口地址;非向量中断是多个中断程序共享一个入口地址,进入入口后再根据标志判断是那个中断。
-
中断上下文(Interrupt Context):当硬件中断发生时,CPU 暂停当前执行的进程,跳转到内核预先注册的中断处理函数。此时 CPU 处于中断上下文,它不属于任何进程,没有 task_struct 保存上下文信息,current 宏指向被中断的进程,但该进程与中断处理无关
-
在中断上下文中:
-
中断被屏蔽(至少当前中断线被屏蔽,有些架构会屏蔽所有中断)
-
不能睡眠:因为睡眠需要进程调度,而中断上下文没有自己的进程结构,无法进行上下文切换
-
不能访问用户空间内存
-
只能使用原子操作或自旋锁来保护共享数据
-
如果所有中断处理都在中断上下文中完成会导致:
-
关中断时间过长:其他中断(包括更紧急的)可能无法及时响应,导致数据丢失或系统响应变慢。
-
实时性下降:实时系统要求中断延迟尽可能短,长时间占用中断上下文会破坏实时性。
因此,内核将中断处理拆分为:
-
上半部(top half):在中断上下文中执行,完成必须立即处理的工作(如读取硬件状态、清除中断标志、拷贝少量数据到缓冲区)。属于硬中断。
-
下半部(bottom half):推迟执行那些可以稍后处理的耗时任务(如协议解析、数据拷贝到用户空间)。下半部执行运行在软中断上下文,中断可以重新启用,从而提高系统吞吐量。
- 如果没有上半部和下半部的区分,下半部处理时间过长时可能会导致嵌套中断,但分开挂到软中断队列中就可以避免。例如在1秒内瞬间到来1000个tcp中断,上半部只把包从硬件 FIFO 拷贝到内核缓冲区,然后调度 tasklet。tasklet 在稍后执行协议解析,而此时中断已经返回,新包可以继续被上半部快速接收。这样,接收包和解析包可以流水线化,大大提高了吞吐量。
1.1 request_irq
申请IRQ函数
/* * irq:中断号 * handler:回调函数,发生中断时,系统调用该函数 * flags:中断处理属性1.3 * devname:用于在/proc/interrupts标记,便于调试 * dev_id:私有指针,内核不直接使用,但在中断发生时会原样传递给处理函数。在共享中断中,此参数是唯一能区分不同设备的手段,一般传入本设备结构体或NULL
* 成功返回0 */ int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id); /* * int irq:触发的中断号(同一个处理函数可以处理多个中断线时有用) * void *dev_id:设备标识符,即 request_irq 传入的 dev_id 参数 * 返回值 irqreturn_t: IRQ_NONE:中断不是本设备产生的(用于共享中断时判断) IRQ_HANDLED:中断已被本设备处理 IRQ_WAKE_THREAD:仅在线程化中断中使用,表示需要唤醒中断线程 */ typedef irqreturn_t (*irq_handler_t)(int, void*);
中断标志 flags 用于控制中断的行为,可组合使用(位或),常用标志包括:
| 标志 | 含义 |
|---|---|
IRQF_SHARED |
允许多个设备共享同一中断线。所有共享该中断的驱动必须设置此标志,并且处理函数必须能通过 dev_id 区分自己设备的中断。 |
IRQF_ONESHOT |
用于线程化中断。确保中断处理线程执行期间,该中断线被屏蔽,线程执行完后再自动重新使能。通常与线程化中断配合使用。 |
IRQF_TRIGGER_RISING / FALLING / HIGH / LOW |
指定中断触发电平(上升沿、下降沿、高电平、低电平)。这些标志通常由平台代码或设备树解析后自动设置,但驱动也可以显式指定。 |
IRQF_DISABLED |
(过时)旧内核用于要求在关中断上下文中执行处理函数。现代内核中,中断处理函数默认在关中断(本地 CPU 关中断)下执行,此标志已无意义。 |
IRQF_NO_SUSPEND |
系统休眠时不禁用该中断。 |
IRQF_FORCE_RESUME |
系统唤醒时强制恢复该中断。 |
IRQF_NO_THREAD |
禁止将该中断强制线程化(即使系统开启 force_irqthreads)。 |
IRQF_EARLY_RESUME |
在早期恢复阶段(syscore 阶段)恢复中断。 |
1.2 free_irq
释放IRQ函数
void free_irq(unsigned int irq, void* dev_id);
1.3 使能和屏蔽中断
释放IRQ函数
void disable_irq(int irq); // 等待目前的中断处理完成后返回,由于该函数等待指定中断处理完,如果在中断的顶半段调用该函数,会引起系统死锁 void disable_irq_nosync(int irq); // 立即返回 void enable_irq(int irq);
1.4 DECLARE_TASKLET
该函数是 Linux 内核中用于静态定义并初始化一个 tasklet 的宏。Tasklet 是一种常用的软中断机制,用于实现中断处理的下半部(bottom half),允许在稍后的安全时间执行延迟的工作,且保证在中断上下文中运行,不会睡眠
#include <linux/interrupt.h> /* * name:要定义的 tasklet 变量名 * func:tasklet 处理函数,类型为 void (*func)(unsigned long) * data:传递给 func 的参数,类型为 unsigned long,通常用于传递设备结构体指针或标志 */ DECLARE_TASKLET(name, func, data); DECLARE_TASKLET_DISABLED(name, func, data); // 初始为禁用状态 // 该宏展开后类似如下 struct tasklet_struct name = { .func = func, .data = data, .state = 0, // 对于 DECLARE_TASKLET // 其他成员由内核初始化 };
最后通过 tasklet_schedule(&xxx_tasklet) 运行,这里实际上会将下半程人物挂载在当前cpu的待执行软中断队列,该队列会在返回到中断前的进程时,或者队列长时间未被调度时执行。
/* * 该模块模拟一个简单的设备驱动:使用内核定时器周期性地触发“中断”, * 中断上半部:调度一个 tasklet * 中断下半部:tasklet 执行实际工作(增加计数器并打印) */ #include <linux/module.h> #include <linux/interrupt.h> // tasklet 相关 #include <linux/timer.h> // 定时器模拟硬件中断 #include <linux/slab.h> // kzalloc #include <linux/spinlock.h> // 自旋锁 #define DRIVER_NAME "tasklet_second_dev" /* 设备结构体,包含所有相关资源 */ struct my_device { struct tasklet_struct tasklet; // tasklet 对象 struct timer_list timer; // 用于模拟硬件中断的定时器 spinlock_t lock; // 保护共享数据 counter unsigned long counter; // 统计 tasklet 执行次数 }; static struct my_device *my_dev; // 全局设备指针 /* tasklet 处理函数(下半部) */ static void my_tasklet_handler(unsigned long data) { struct my_device *dev = (struct my_device *)data; /* tasklet 运行在软中断上下文,不能睡眠,只能使用原子操作或自旋锁 */ spin_lock(&dev->lock); dev->counter++; spin_unlock(&dev->lock); pr_info("Tasklet executed, counter = %lu\n", dev->counter); /* 此处不能调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)) */ } /* 中断上半部处理程序(由定时器回调调用) */ static void simulated_interrupt(struct timer_list *t) { struct my_device *dev = container_of(t, struct my_device, timer); pr_info("Simulated interrupt, scheduling tasklet\n"); /* 调度 tasklet:将 tasklet 挂入队列,稍后在软中断中执行 */ tasklet_schedule(&dev->tasklet); /* 硬件中断处理还需要: * 1. 读取中断状态寄存器,确认是否是自己的设备产生的中断 * 2. 清除硬件中断标志 * 3. 如果是共享中断,返回 IRQ_NONE 表示不是自己的中断 */ /* 重新启动定时器,实现周期性中断(如每秒一次) */ mod_timer(&dev->timer, jiffies + HZ); } /* 模块初始化 */ static int __init tasklet_second_init(void) { int ret; /* 1. 分配设备结构体内存 */ my_dev = kzalloc(sizeof(struct my_device), GFP_KERNEL); if (!my_dev) return -ENOMEM; /* 2. 初始化自旋锁 */ spin_lock_init(&my_dev->lock); /* 3. 初始化 tasklet:绑定处理函数,传入设备指针作为参数 */ tasklet_init(&my_dev->tasklet, my_tasklet_handler, (unsigned long)my_dev); /* 4. 初始化定时器,用于模拟硬件中断 */ timer_setup(&my_dev->timer, simulated_interrupt, 0); mod_timer(&my_dev->timer, jiffies + HZ); // 1 秒后触发第一次 pr_info("Tasklet sample module loaded, timer started.\n"); return 0; } /* 模块退出 */ static void __exit tasklet_second_exit(void) { if (!my_dev) return; /* 1. 删除定时器,确保不会再调用模拟中断 */ del_timer_sync(&my_dev->timer); /* 2. 杀死 tasklet:确保 tasklet 不会再次被调度,并等待正在运行的实例完成 */ tasklet_kill(&my_dev->tasklet); /* 此时可以安全地释放资源,因为不会有任何 tasklet 在运行 */ pr_info("Tasklet killed, final counter = %lu\n", my_dev->counter); /* 3. 释放设备结构体 */ kfree(my_dev); pr_info("Tasklet sample module unloaded\n"); } module_init(tasklet_second_init); module_exit(tasklet_second_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("cear");
1.5 INIT_WORK
该函数是 Linux 内核中用于动态初始化一个工作项(work_struct)的宏。工作项是工作队列机制的核心,允许将任务延迟到进程上下文中执行,从而可以睡眠、持有互斥锁等,适用于处理较复杂或可能阻塞的操作(如大量的数据拷贝、文件系统操作等)
#include <linux/workqueue.h> /* * work:指向要初始化的 struct work_struct 变量的指针 * func:工作处理函数,将在工作项被调度时执行 */ INIT_WORK(struct work_struct *work, work_func_t func); // 宏展开后类似如下 struct work_struct { atomic_long_t data; // 内部状态和标志 struct list_head entry; // 用于挂入队列的链表节点 work_func_t func; // 工作处理函数 };
最后通过 schedule_work(&my_work)调度工作队列执行
/* * - 动态初始化工作项(INIT_WORK) * - 使用系统默认工作队列调度工作(schedule_work) * - 在模拟中断(定时器回调)中调度工作 * - 工作函数在进程上下文中执行,可以睡眠 * - 模块卸载时确保工作项被取消(cancel_work_sync) */ #include <linux/module.h> #include <linux/workqueue.h> // work_struct 相关 #include <linux/timer.h> // 定时器模拟中断 #include <linux/slab.h> // kmalloc #include <linux/spinlock.h> // 自旋锁 #include <linux/delay.h> // msleep (仅用于演示可睡眠) #define DRIVER_NAME "workqueue_sample" /* 设备结构体,包含工作项、定时器、计数器及锁 */ struct my_device { struct work_struct work; // 工作项 struct timer_list timer; // 模拟中断的定时器 spinlock_t lock; // 保护 counter unsigned long counter; // 统计工作项执行次数 bool stop; // 停止标志,用于退出循环(可选) }; static struct my_device *my_dev; // 全局设备指针 /* 中断下半部:工作队列处理函数(运行在进程上下文,可以睡眠) */ static void my_work_handler(struct work_struct *work) { struct my_device *dev = container_of(work, struct my_device, work); unsigned long flags; /* 可以进行需要睡眠的操作(例如长时间计算、I2C 读写等) */ msleep(10); /* 更新计数器(需要保护,因为可能与模块卸载路径并发) */ spin_lock_irqsave(&dev->lock, flags); dev->counter++; spin_unlock_irqrestore(&dev->lock, flags); pr_info("Work handler executed, counter = %lu\n", dev->counter); } /* 中断上半部:定时器回调(软中断上下文,不能睡眠) */ static void simulated_interrupt(struct timer_list *t) { struct my_device *dev = from_timer(dev, t, timer); pr_info("Simulated interrupt, scheduling work\n"); /* 调度工作项到系统默认工作队列 */ schedule_work(&dev->work); /* 重新启动定时器,模拟周期性中断(每2秒一次) */ mod_timer(&dev->timer, jiffies + msecs_to_jiffies(2000)); } /* 模块初始化 */ static int __init workqueue_sample_init(void) { int ret; /* 1. 分配设备结构体内存 */ my_dev = kzalloc(sizeof(struct my_device), GFP_KERNEL); if (!my_dev) return -ENOMEM; /* 2. 初始化自旋锁 */ spin_lock_init(&my_dev->lock); /* 3. 初始化工作项:绑定处理函数,注意处理函数参数是 work_struct 指针 */ INIT_WORK(&my_dev->work, my_work_handler); /* 4. 初始化定时器,用于模拟中断 */ timer_setup(&my_dev->timer, simulated_interrupt, 0); mod_timer(&my_dev->timer, jiffies + msecs_to_jiffies(2000)); // 2秒后首次触发 pr_info("Workqueue sample module loaded, timer started (period 2s)\n"); return 0; } /* 模块退出 */ static void __exit workqueue_sample_exit(void) { if (!my_dev) return; pr_info("Unloading module, stopping timer and cancel work...\n"); /* 1. 删除定时器,确保不再触发新的工作调度 */ del_timer_sync(&my_dev->timer); /* 2. 取消可能正在执行或待处理的工作项 * cancel_work_sync 会等待正在工作项完成 * 并确保之后不会再有该工作项被调度 * 返回 true 表示工作项在取消前尚未开始执行 * false 表示工作项已经开始执行或已经完成 */ if (cancel_work_sync(&my_dev->work)) pr_info("Work was pending and has been cancelled.\n"); else pr_info("Work was not pending or already executed.\n"); /* 此时可以安全地访问 counter(无并发工作项) */ pr_info("Final counter = %lu\n", my_dev->counter); /* 3. 释放设备结构体 */ kfree(my_dev); pr_info("Workqueue sample module unloaded\n"); } module_init(workqueue_sample_init); module_exit(workqueue_sample_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("cear");

浙公网安备 33010602011771号