--- title: 中断下半部处理 tags: [Linux驱动, 中断, softirq, tasklet, workqueue, 嵌入式] created: 2026-09-17 updated: 2026-09-17 pdf_ref: "57_第五十一章 Linux中断实验.pdf" --- # 中断下半部处理 > 💡 **关联知识**: [[02-嵌入式Linux内核基础/04-进程调度与中断管理]] | [[FreeRTOS学习笔记/07-中断管理与临界段]] --- ## 一、中断处理概述 ### 1.1 为什么需要下半部 Linux内核中的中断处理必须快速完成,因为中断会打断当前正在执行的代码。如果中断处理函数执行时间过长,会导致系统响应延迟,甚至丢失其他中断。因此,Linux内核将中断处理分为两部分: - **上半部(Top Half)**:硬中断处理程序,执行最关键、最紧急的操作,不可睡眠,时间极短 - **下半部(Bottom Half)**:延迟处理程序,执行耗时操作,可以在稍后执行 ### 1.2 中断处理流程 ```mermaid graph TD A[硬件中断触发] --> B[执行上半部] B --> C{是否需要下半部?} C -->|是| D[调度下半部] C -->|否| E[中断处理完成] D --> F[软中断/Tasklet] D --> G[工作队列] D --> H[线程化中断] F --> I[执行下半部处理] G --> I H --> I I --> J[中断处理完成] ``` ### 1.3 下半部机制概览 | 机制 | 执行上下文 | 可睡眠 | 适用场景 | | --------------------- | ------------ | ------ | ------------------ | | 软中断(Softirq) | 软中断上下文 | 否 | 高频事件、网络收发 | | Tasklet | 软中断上下文 | 否 | 快速处理、简单任务 | | 工作队列(Workqueue) | 进程上下文 | 是 | 耗时操作、需要睡眠 | | 线程化中断 | 内核线程 | 是 | 复杂处理、需要互斥 | --- ## 二、软中断(Softirq) ### 2.1 软中断原理 软中断是Linux内核提供的一种下半部实现机制。内核使用`softirq_action`结构体表示软中断,定义在`include/linux/interrupt.h`中: ```c // 示例51.1.2.1 softirq_action结构体 struct softirq_action { void (*action)(struct softirq_action *); }; ``` 在`kernel/softirq.c`文件中,定义了一个包含10个软中断的数组: ```c // 示例51.1.2.2 softirq_vec数组 static struct softirq_action softirq_vec[NR_SOFTIRQS]; ``` ### 2.2 软中断类型 `NR_SOFTIRQS`是枚举类型,定义在`include/linux/interrupt.h`中: ```c // 示例51.1.2.3 软中断类型枚举 enum { HI_SOFTIRQ=0, /* 高优先级软中断 */ TIMER_SOFTIRQ, /* 定时器软中断 */ NET_TX_SOFTIRQ, /* 网络数据发送软中断 */ NET_RX_SOFTIRQ, /* 网络数据接收软中断 */ BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, /* tasklet软中断 */ SCHED_SOFTIRQ, /* 调度软中断 */ HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */ RCU_SOFTIRQ, /* RCU软中断 */ NR_SOFTIRQS }; ``` ### 2.3 软中断API 要使用软中断,需要使用`open_softirq`注册软中断处理函数: ```c // 注册软中断处理函数原型 void open_softirq(int nr, void (*action)(struct softirq_action *)) ``` 参数说明: - `nr`:要注册的软中断,从上述枚举中选择一个 - `action`:软中断对应的处理函数 注册软中断以后,需要通过`raise_softirq`触发软中断: ```c // 触发软中断原型 void raise_softirq(unsigned int nr) ``` ### 2.4 软中断初始化 软中断在系统启动时初始化,`softirq_init`函数定义在`kernel/softirq.c`文件中: ```c // 示例51.1.2.4 softirq_init函数内容 void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); } ``` 从示例51.1.2.4可以看出,`softirq_init`函数默认会注册`TASKLET_SOFTIRQ`和`HI_SOFTIRQ`这两个软中断。 ### 2.5 使用场景 软中断适用于: - 网络数据收发(NET_TX_SOFTIRQ、NET_RX_SOFTIRQ) - 定时器处理(TIMER_SOFTIRQ) - 块设备I/O(BLOCK_SOFTIRQ) - RCU回调(RCU_SOFTIRQ) --- ## 三、Tasklet ### 3.1 Tasklet原理 tasklet是基于软中断实现的一种下半部机制。在中断和tasklet之间,推荐使用tasklet。Linux内核使用`tasklet_struct`结构体来表示tasklet: ```c // 示例51.1.2.5 tasklet_struct结构体 struct tasklet_struct { struct tasklet_struct *next; /* 下一个tasklet */ unsigned long state; /* tasklet状态 */ atomic_t count; /* 引用计数,记录tasklet是否被禁止 */ void (*func)(unsigned long); /* tasklet执行的函数 */ unsigned long data; /* 传递给func的参数 */ }; ``` ### 3.2 Tasklet API 要使用tasklet,首先需要定义一个tasklet,然后使用`tasklet_init`初始化tasklet: ```c // tasklet_init函数原型 void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); ``` 参数说明: - `t`:要初始化的tasklet - `func`:tasklet的处理函数 - `data`:传递给func的参数 也可以使用宏`DECLARE_TASKLET`一次性完成tasklet的定义和初始化: ```c // DECLARE_TASKLET宏(include/linux/interrupt.h) DECLARE_TASKLET(name, func, data) ``` 参数说明: - `name`:要定义的tasklet名称(会自动创建一个`tasklet_struct`类型的变量) - `func`:tasklet的处理函数 - `data`:传递给func的参数 ### 3.3 调度Tasklet 当需要调度tasklet执行时,使用`tasklet_schedule`函数: ```c // tasklet_schedule函数原型 void tasklet_schedule(struct tasklet_struct *t) ``` 参数说明: - `t`:要调度的tasklet,也就是`DECLARE_TASKLET`宏定义的name ### 3.4 Tasklet使用示例 ```c // 示例51.1.2.7 tasklet使用示例 /* 定义tasklet */ struct tasklet_struct testtasklet; /* tasklet处理函数 */ void testtasklet_func(unsigned long data) { /* tasklet具体的处理内容 */ } /* 中断处理函数 */ irqreturn_t test_handler(int irq, void *dev_id) { /* 启动tasklet */ tasklet_schedule(&testtasklet); } /* 驱动入口函数 */ static int __init xxxx_init(void) { /* 初始化tasklet */ tasklet_init(&testtasklet, testtasklet_func, data); /* 注册中断处理函数 */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); } ``` ### 3.5 Tasklet注意事项 1. **同一tasklet不能并发执行**:同一个tasklet只能在一个CPU上运行 2. **不同tasklet可以并发**:不同的tasklet可以在不同CPU上同时运行 3. **不能睡眠**:tasklet运行在软中断上下文,不能调用可能导致睡眠的函数 4. **引用计数**:`count`成员用于控制tasklet的启用和禁用 --- ## 四、工作队列(Workqueue) ### 4.1 工作队列原理 工作队列是另一种下半部执行方式,工作在进程上下文执行,可以将工作推迟到一个内核线程去执行。因为工作队列工作在进程上下文,所以允许睡眠。如果推迟的工作需要睡眠,那么就只能选择工作队列。 ### 4.2 工作队列数据结构 Linux内核使用`work_struct`结构体表示一个工作: ```c // 示例51.1.2.8 work_struct结构体 struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; /* 工作队列处理函数 */ }; ``` 这些工作组织成队列,使用`workqueue_struct`结构体表示工作队列: ```c // 示例51.1.2.9 workqueue_struct结构体(简化版) struct workqueue_struct { struct list_head pwqs; struct list_head list; struct mutex mutex; int work_color; int flush_color; atomic_t nr_pwqs_to_flush; struct wq_flusher *first_flusher; struct list_head flusher_queue; struct list_head flusher_overflow; struct list_head maydays; struct worker *rescuer; int nr_drainers; int saved_max_active; struct workqueue_attrs *unbound_attrs; struct pool_workqueue *dfl_pwq; char name[WQ_NAME_LEN]; struct rcu_head rcu; unsigned int flags ____cacheline_aligned; struct pool_workqueue __percpu *cpu_pwqs; struct pool_workqueue __rcu *numa_pwq_tbl[]; }; ``` Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作。Linux内核使用`worker`结构体表示工作者线程: ```c // 示例51.1.2.10 worker结构体 struct worker { union { struct list_head entry; struct hlist_node hentry; }; struct work_struct *current_work; work_func_t current_func; struct pool_workqueue *current_pwq; bool desc_valid; struct list_head scheduled; struct task_struct *task; struct worker_pool *pool; struct list_head node; unsigned long last_active; unsigned int flags; int id; char desc[WORKER_DESC_LEN]; struct workqueue_struct *rescue_wq; }; ``` 从示例51.1.2.10可以看出,每个worker都有一个自己的任务队列,工作者线程负责将自己的任务队列中的所有工作都执行完毕。实际上工作队列中,我们只需要做的是创建工作(work_struct),在工作队列和工作者线程机制出来之前我们不用去管。简单来说就是,我们只需要定义一个work_struct结构体变量即可,然后使用`INIT_WORK`宏对其进行初始化: ```c // INIT_WORK宏定义 #define INIT_WORK(_work, _func) ``` 参数说明: - `_work`:要初始化的工作 - `_func`:工作对应的处理函数 也可以使用`DECLARE_WORK`宏一次性完成工作的定义和初始化: ```c // DECLARE_WORK宏定义 #define DECLARE_WORK(n, f) ``` 参数说明: - `n`:定义的工作(work_struct) - `f`:工作对应的处理函数 ### 4.3 工作队列API 和tasklet一样,工作也需要调度才能执行,工作的调度函数为`schedule_work`: ```c // schedule_work函数原型 bool schedule_work(struct work_struct *work) ``` 参数说明: - `work`:要调度的工作 返回值:0 成功,其他值 失败。 ### 4.4 工作队列使用示例 ```c // 示例51.1.2.11 工作队列使用示例 /* 定义工作(work) */ struct work_struct testwork; /* work处理函数 */ void testwork_func_t(struct work_struct *work) { /* work具体的处理内容 */ } /* 中断处理函数 */ irqreturn_t test_handler(int irq, void *dev_id) { /* 调度work */ schedule_work(&testwork); } /* 驱动入口函数 */ static int __init xxxx_init(void) { /* 初始化work */ INIT_WORK(&testwork, testwork_func_t); /* 注册中断处理函数 */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); } ``` ### 4.5 创建和销毁工作队列 除了使用系统默认的工作队列,也可以自己创建工作队列: ```c // 创建单线程工作队列 struct workqueue_struct *create_singlethread_workqueue(const char *name); // 创建多线程工作队列 struct workqueue_struct *alloc_workqueue(const char *name, unsigned int flags, int max_active); // 调度指定工作队列中的工作 bool queue_work(struct workqueue_struct *wq, struct work_struct *work); // 销毁工作队列 void destroy_workqueue(struct workqueue_struct *wq); ``` ### 4.6 工作队列与Tasklet的选择 | 场景 | 选择 | | -------------------- | ---------- | | 快速处理,不需要睡眠 | Tasklet | | 耗时操作,需要睡眠 | 工作队列 | | 高频事件 | 软中断 | | 复杂处理,需要互斥 | 线程化中断 | --- ## 五、线程化中断(Threaded IRQ) ### 5.1 线程化中断原理 线程化中断是Linux内核提供的一种中断处理机制,将中断处理分为硬中断上下文和内核线程上下文两部分。线程化中断允许在下半部中睡眠,适合复杂的中断处理场景。 ### 5.2 线程化中断API 使用`request_threaded_irq`函数申请线程化中断: ```c // request_threaded_irq函数原型 int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id); ``` 参数说明: - `irq`:要申请的中断号 - `handler`:上半部处理函数(硬中断上下文,不可睡眠) - `thread_fn`:下半部处理函数(内核线程上下文,可以睡眠) - `irqflags`:中断标志 - `devname`:中断名称 - `dev_id`:传递给中断处理函数的参数 ### 5.3 线程化中断处理函数原型 ```c // 上半部处理函数原型 irqreturn_t (*irq_handler_t)(int irq, void *dev_id); // 下半部处理函数原型 irqreturn_t (*irq_handler_t)(int irq, void *dev_id); ``` ### 5.4 线程化中断使用示例 ```c // 上半部处理函数 static irqreturn_t key_irq_handler(int irq, void *dev_id) { /* 上半部:快速处理 */ printk("IRQ handler\r\n"); return IRQ_WAKE_THREAD; /* 唤醒下半部 */ } // 下半部处理函数(线程上下文) static irqreturn_t key_thread_fn(int irq, void *dev_id) { /* 下半部:可以睡眠 */ msleep(50); printk("Thread handler\r\n"); return IRQ_HANDLED; } // 申请线程化中断 static int __init key_init(void) { request_threaded_irq(irq, key_irq_handler, key_thread_fn, IRQF_TRIGGER_FALLING, "key", dev); } ``` ### 5.5 使用场景 线程化中断适用于: - 需要互斥的中断处理 - 耗时操作需要睡眠 - 复杂的中断处理逻辑 - 需要使用互斥锁、信号量等同步机制 --- ## 六、完整源码分析 ### 6.1 实验概述 本实验基于I.MX6ULL开发板,使用KEY0按键,通过中断方式检测按键按下和释放。实验代码位于`09、文档教程(非常重要)/【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1_拆分/57_第五十一章 Linux中断实验.pdf`。 ### 6.2 设备树节点 在设备树中添加key节点: ```dts // 示例51.3.1.1 key节点信息 key { #address-cells = <1>; #size-cells = <1>; compatible = "atkalpha-key"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_key>; key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */ interrupt-parent = <&gpio1>; interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */ status = "okay"; }; ``` ### 6.3 中断类型定义 在`include/linux/irq.h`中定义了中断类型: ```c // 示例51.3.1.2 中断状态枚举 enum { IRQ_TYPE_NONE = 0x00000000, IRQ_TYPE_EDGE_RISING = 0x00000001, IRQ_TYPE_EDGE_FALLING = 0x00000002, IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING), IRQ_TYPE_LEVEL_HIGH = 0x00000004, IRQ_TYPE_LEVEL_LOW = 0x00000008, IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW | IRQ_TYPE_LEVEL_HIGH), // ... }; ``` `IRQ_TYPE_EDGE_BOTH`表示上升沿和下降沿同时有效,相当于KEY0按下和释放都会触发中断。 ### 6.4 驱动源码分析 #### 6.4.1 数据结构定义 ```c /* 中断IO描述结构体 */ struct irq_keydesc { int gpio; /* gpio */ int irqnum; /* 中断号 */ unsigned char value; /* 按键对应的键值 */ char name[10]; /* 名字 */ irqreturn_t (*handler)(int, void*); /* 中断处理函数 */ }; /* imx6uirq设备结构体 */ struct imx6uirq_dev{ dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ struct device_node *nd; /* 设备节点 */ atomic_t keyvalue; /* 有效的按键值 */ atomic_t releasekey; /* 标记按键是否松开 */ struct timer_list timer; /* 定义一个定时器 */ struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述信息数组 */ unsigned char curkeynum; /* 当前按键号 */ }; struct imx6uirq_dev imx6uirq; /* irq设备 */ ``` #### 6.4.2 中断处理函数 ```c // 中断处理函数,启动定时器 static irqreturn_t key0_handler(int irq, void *dev_id) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; dev->curkeynum = 0; dev->timer.data = (volatile long)dev_id; mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); return IRQ_RETVAL(IRQ_HANDLED); } ``` #### 6.4.3 定时器处理函数 ```c // 定时器处理函数,完成按键消抖 void timer_function(unsigned long arg) { unsigned char value; unsigned char num; struct irq_keydesc *keydesc; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; num = dev->curkeynum; keydesc = &dev->irqkeydesc[num]; value = gpio_get_value(keydesc->gpio); /* 读取IO值 */ if(value == 0){ /* 按下按键 */ atomic_set(&dev->keyvalue, keydesc->value); } else{ /* 松开按键 */ atomic_set(&dev->keyvalue, 0x80 | keydesc->value); atomic_set(&dev->releasekey, 1); /* 标记松开按键 */ } } ``` #### 6.4.4 IO初始化函数 ```c // IO初始化函数 static int keyio_init(void) { unsigned char i = 0; int ret = 0; imx6uirq.nd = of_find_node_by_path("/key"); if (imx6uirq.nd == NULL){ printk("key node not find!\r\n"); return -EINVAL; } /* 获取GPIO */ for (i = 0; i < KEY_NUM; i++) { imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd, "key-gpio", i); if (imx6uirq.irqkeydesc[i].gpio < 0) { printk("can't get key%d\r\n", i); } } /* 初始化key所使用的IO,并且设置为中断模式 */ for (i = 0; i < KEY_NUM; i++) { memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name)); sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name); gpio_direction_input(imx6uirq.irqkeydesc[i].gpio); imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map( imx6uirq.nd, i); printk("key%d:gpio=%d, irqnum=%d\r\n", i, imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].irqnum); } /* 初始化中断 */ imx6uirq.irqkeydesc[0].handler = key0_handler; imx6uirq.irqkeydesc[0].value = KEY0VALUE; for (i = 0; i < KEY_NUM; i++) { ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq); if(ret < 0){ printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum); return -EFAULT; } } /* 创建定时器 */ init_timer(&imx6uirq.timer); imx6uirq.timer.function = timer_function; return 0; } ``` #### 6.4.5 驱动入口函数 ```c static int __init imx6uirq_init(void) { /* 1、创建设备号 */ if (imx6uirq.major) { imx6uirq.devid = MKDEV(imx6uirq.major, 0); register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME); } else { alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME); imx6uirq.major = MAJOR(imx6uirq.devid); imx6uirq.minor = MINOR(imx6uirq.devid); } /* 2、注册字符设备 */ cdev_init(&imx6uirq.cdev, &imx6uirq_fops); cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT); /* 3、创建类 */ imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME); if (IS_ERR(imx6uirq.class)) { return PTR_ERR(imx6uirq.class); } /* 4、创建设备 */ imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME); if (IS_ERR(imx6uirq.device)) { return PTR_ERR(imx6uirq.device); } /* 5、初始化原子变量 */ atomic_set(&imx6uirq.keyvalue, INVAKEY); atomic_set(&imx6uirq.releasekey, 0); keyio_init(); return 0; } ``` #### 6.4.6 驱动出口函数 ```c static void __exit imx6uirq_exit(void) { unsigned int i = 0; /* 删除定时器 */ del_timer_sync(&imx6uirq.timer); /* 释放中断 */ for (i = 0; i < KEY_NUM; i++) { free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq); gpio_free(imx6uirq.irqkeydesc[i].gpio); } cdev_del(&imx6uirq.cdev); unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT); device_destroy(imx6uirq.class, imx6uirq.devid); class_destroy(imx6uirq.class); } module_init(imx6uirq_init); module_exit(imx6uirq_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai"); ``` ### 6.5 应用程序源码 ```c // 应用程序 imx6uirqApp.c #include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" int main(int argc, char *argv[]) { int fd; int ret = 0; char *filename; unsigned char data; if (argc != 2) { printf("Error Usage!\r\n"); return -1; } filename = argv[1]; fd = open(filename, O_RDWR); if (fd < 0) { printf("Can't open file %s\r\n", filename); return -1; } while (1) { ret = read(fd, &data, sizeof(data)); if (ret < 0) { /* 数据读取错误 */ } else { if (data) /* 读取到数据 */ printf("key value = %#X\r\n", data); } } close(fd); return ret; } ``` --- ## 七、实验验证 ### 7.1 编译方法 #### 1. 编译驱动模块 编写Makefile文件: ```makefile KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek # ... 其他内容 obj-m := imx6uirq.o clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean ``` 编译驱动模块: ```bash make -j32 ``` 编译成功后会生成`imx6uirq.ko`驱动模块文件。 #### 2. 编译应用程序 ```bash arm-linux-gnueabihf-gcc imx6uirqApp.c -o imx6uirqApp ``` 编译成功后会生成`imx6uirqApp`应用程序。 ### 7.2 测试步骤 #### 1. 加载驱动模块 ```bash depmod # 第一次加载时需要执行 modprobe imx6uirq # 加载驱动 ``` #### 2. 查看中断注册 ```bash cat /proc/interrupts ``` 从中可以看到KEY0的中断信息: - 中断类型:Edge(边沿触发) - 中断号:49 #### 3. 运行测试程序 ```bash ./imx6uirqApp /dev/imx6uirq ``` 按下开发板上的KEY0,终端就会输出按键值。 #### 4. 卸载驱动 ```bash rmmod imx6uirq.ko ``` --- ## 八、下半部机制对比 ### 8.1 四种机制详细对比 | 特性 | 软中断(Softirq) | Tasklet | 工作队列 | 线程化中断 | | ---------- | ----------------- | ------------------- | ---------- | ---------- | | 执行上下文 | 软中断上下文 | 软中断上下文 | 进程上下文 | 内核线程 | | 可睡眠 | 否 | 否 | 是 | 是 | | 并发性 | 同一软中断可并发 | 同一tasklet不可并发 | 可并发 | 可并发 | | 执行时间 | 极短 | 短 | 可以较长 | 可以较长 | | 复杂度 | 高 | 低 | 中 | 中 | | 适用场景 | 高频事件、网络 | 快速处理 | 耗时操作 | 复杂处理 | | 系统开销 | 最小 | 小 | 较大 | 较大 | ### 8.2 选择指南 ```mermaid graph TD A[需要中断下半部?] --> B{需要睡眠?} B -->|是| C{需要互斥?} B -->|否| D{处理时间?} C -->|是| E[线程化中断] C -->|否| F[工作队列] D -->|短| G[Tasklet] D -->|长| H[工作队列] G --> I{高频事件?} I -->|是| J[软中断] I -->|否| K[Tasklet] ``` ### 8.3 性能对比 | 机制 | 延迟 | 吞吐量 | CPU占用 | 内存占用 | | ---------- | ---- | ------ | ------- | -------- | | 软中断 | 最低 | 最高 | 最低 | 最低 | | Tasklet | 低 | 高 | 低 | 低 | | 工作队列 | 中等 | 中等 | 中等 | 中等 | | 线程化中断 | 较高 | 较低 | 较高 | 较高 | --- ## 九、跨平台对比 ### 9.1 I.MX6ULL vs STM32 vs RK3568 | 特性 | I.MX6ULL | STM32 | RK3568 | | ---------- | ----------------------- | --------------- | ----------------------- | | 中断控制器 | GIC-400 | NVIC | GIC-400 | | 中断优先级 | 256级 | 16级 | 256级 | | 嵌套中断 | 支持 | 支持 | 支持 | | 下半部机制 | 软中断/Tasklet/工作队列 | 无 | 软中断/Tasklet/工作队列 | | 中断号获取 | irq_of_parse_and_map | HAL_NVIC_GetIRQ | irq_of_parse_and_map | | 设备树支持 | 完整 | 无 | 完整 | ### 9.2 中断控制器对比 | 特性 | GIC(ARM) | NVIC(Cortex-M) | PLIC(RISC-V) | | ---------- | ---------- | ---------------- | -------------- | | 中断数量 | 1020+ | 240+ | 1024+ | | 优先级位数 | 8位 | 4位 | 可配置 | | 中断嵌套 | 支持 | 支持 | 支持 | | 软件触发 | 支持 | 不支持 | 支持 | ### 9.3 中断处理差异 | 平台 | 中断入口 | 下半部 | 特点 | | ----------------- | ------------- | ----------------------- | ------------------ | | I.MX6ULL | Linux内核管理 | 软中断/Tasklet/工作队列 | 完整的中断分层机制 | | STM32(裸机) | 中断向量表 | 无或简单任务切换 | 直接、简单 | | STM32(FreeRTOS) | 中断向量表 | 任务通知 | 简单的延迟处理 | | RK3568 | Linux内核管理 | 软中断/Tasklet/工作队列 | 与I.MX6ULL类似 | --- ## 十、面试精选 ### 题目1:中断上半部和下半部有什么区别?为什么要这样设计? **考察点**: 中断处理机制 **参考答案**: | 特性 | 上半部 | 下半部 | | ---------- | -------------- | ------------------ | | 执行上下文 | 硬中断上下文 | 软中断/进程上下文 | | 执行时间 | 极短(微秒级) | 可以较长(毫秒级) | | 可睡眠 | 不可 | 可以(工作队列) | | 优先级 | 最高 | 较低 | | 中断屏蔽 | 会屏蔽其他中断 | 不会 | **设计原因**: 1. 硬件中断需要快速响应,不能长时间占用CPU 2. 耗时操作会延迟其他中断的处理 3. 分层设计提高了系统响应速度 4. 下半部可以使用更多系统资源(如睡眠、互斥) ### 题目2:tasklet和workqueue有什么区别?如何选择? **考察点**: 下半部机制 **参考答案**: | 特性 | tasklet | workqueue | | ---------- | ------------------- | ---------- | | 执行上下文 | 软中断 | 进程上下文 | | 可睡眠 | 不可 | 可以 | | 并发性 | 同一tasklet不能并发 | 可以并发 | | 执行效率 | 高 | 较低 | | 适用场景 | 快速处理 | 耗时操作 | **选择指南**: - 快速处理、不需要睡眠 → tasklet - 耗时操作、需要睡眠 → workqueue - 高频事件 → 软中断 - 需要互斥 → 线程化中断 ### 题目3:工作队列是如何实现的?为什么可以睡眠? **考察点**: 工作队列原理 **参考答案**: 工作队列通过工作者线程(worker thread)实现: 1. 工作队列维护一个工作链表 2. 工作者线程是一个内核线程,运行在进程上下文 3. 工作者线程从工作队列中取出工作并执行 4. 因为运行在进程上下文,所以可以调用可能导致睡眠的函数 关键数据结构: - `work_struct`:表示一个工作 - `workqueue_struct`:表示工作队列 - `worker`:表示工作者线程 ### 题目4:在中断处理函数中能否调用mutex_lock?为什么? **考察点**: 中断上下文限制 **参考答案**: **不能**。原因: 1. 中断处理函数运行在硬中断上下文 2. `mutex_lock`可能导致睡眠 3. 在中断上下文中睡眠会导致系统崩溃 4. 中断上下文不能被调度,睡眠后无法恢复 **解决方案**: 1. 使用自旋锁(spinlock)代替互斥锁 2. 将耗时操作放到下半部执行 3. 使用线程化中断,下半部可以睡眠 ### 题目5:如何获取设备树中定义的中断号? **考察点**: 设备树中断解析 **参考答案**: 有两种方法: 1. **使用`irq_of_parse_and_map`函数**: ```c unsigned int irq_of_parse_and_map(struct device_node *dev, int index); ``` - `dev`:设备节点 - `index`:中断索引号 - 返回值:中断号 2. **使用`gpio_to_irq`函数**: ```c int gpio_to_irq(unsigned int gpio); ``` - `gpio`:GPIO编号 - 返回值:GPIO对应的中断号 **设备树中断属性**: ```dts interrupt-parent = <&gpio1>; /* 指定中断控制器 */ interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* 中断信息 */ ``` `interrupts`属性格式取决于中断控制器,对于GIC: - 第一个cells:中断类型(0=SPI,1=PPI) - 第二个cells:中断号 - 第三个cells:触发方式 --- **代码来源**: Linux驱动例程 13_irq **最后更新**: 2026-09-17