tasklet执行时机硬中断结束 │ ▼ 内核检查是否有 pending 的软中断 │ ▼ 有 ──→ 执行 tasklet 回调 │ ▼ 执行完tasklet 停止 等待下次 tasklet_schedule 触发关键特性同一个 tasklet │ ├── 同一时刻只在一个 CPU 上执行 │ 不会并发重入 │ └── 多次 tasklet_schedule 如果上次还没执行不会重复加入队列 只执行一次优点实时性好硬中断结束后立刻执行 不需要等调度器调度 响应速度比 workqueue 快不会并发重入同一个 tasklet 同一时刻只在一个CPU执行 不需要加锁保护 tasklet 本身 比 softirq 使用更安全使用简单tasklet_setup(tasklet, func); // 初始化 tasklet_schedule(tasklet); // 触发 tasklet_kill(tasklet); // 销毁缺点不能睡眠运行在软中断上下文 │ ├── 不能调用 mutex_lock ❌ ├── 不能调用 msleep ❌ ├── 不能调用 kmalloc(GFP_KERNEL) ❌ └── 只能用 kmalloc(GFP_ATOMIC) ✅不能做耗时操作tasklet 占用 CPU 时间过长 │ ▼ 影响其他软中断的执行 │ ▼ 系统响应变差已被新内核不推荐使用内核社区认为 tasklet 能做的事 workqueue 都能做 workqueue 限制更少更灵活 新驱动推荐用 threaded IRQ 或 workqueue 替代 tasklet旧接口类型不安全/* 旧接口unsigned long 传参需要强制转换不安全 */ static void my_tasklet_func(unsigned long data) { struct my_dev *dev (struct my_dev *)data; // 强转不安全 } /* 新接口通过 from_tasklet 获取类型安全 */ static void my_tasklet_func(struct tasklet_struct *t) { struct my_dev *dev from_tasklet(dev, t, tasklet); // 安全 }workqueue:优点可以睡眠void my_work_func(struct work_struct *work) { mutex_lock(my_mutex); // ✅ 可以 msleep(100); // ✅ 可以 kmalloc(size, GFP_KERNEL); // ✅ 可以 }可以处理耗时操作网络数据包处理、文件IO、复杂计算 这些放在中断上半部会导致系统卡顿 放在workqueue完全没问题使用简单// 只需两步 INIT_WORK(work, func); // 初始化 schedule_work(work); // 提交共享线程节省资源系统默认workqueue 所有模块共用worker线程 不需要每个驱动自己创建线程缺点实时性差任务提交后何时执行取决于调度器 worker线程优先级不高 对实时性要求高的场景不适合 tasklet ──→ 中断返回前就执行实时性更好 workqueue ──→ 等调度器调度有延迟共享workqueue可能被拖慢系统默认workqueue 所有人共用 某个任务执行很慢 │ ▼ 后面的任务都要等 │ ▼ 影响其他模块的任务执行并发问题同一个 work 在执行期间再次 schedule_work │ ▼ 不会重复加入队列 │ ▼ 可能丢失一次执行机会 需要自己处理这种情况自定义workqueue消耗资源create_workqueue() // 每个CPU创建一个线程 // CPU多时线程数量可观 create_singlethread_workqueue() // 只有一个线程但串行执行简单例程代码整体说明这个例程目的是同时演示三种下半部机制并不是一个完整的按键驱动所以三种机制的回调函数里只有打印没有实际处理按键数据。三种下半部在哪里触发static irqreturn_t gpio_key_isr(int irq, void *dev_id) { struct gpio_key *gpio_key dev_id; tasklet_schedule(gpio_key-tasklet); // 触发 tasklet mod_timer(gpio_key-key_timer, jiffies HZ/50); // 触发定时器 schedule_work(gpio_key-work); // 触发 workqueue return IRQ_HANDLED; }按键中断发生时三种下半部同时被触发各自独立执行。HZ/50 20ms去抖时间合理。三种下半部的回调tasklet 回调static void key_tasklet_func(unsigned long data) { struct gpio_key *gpio_key data; int val gpiod_get_value(gpio_key-gpiod); printk(key_tasklet_func key %d %d\n, gpio_key-gpio, val); }运行在软中断上下文不能睡眠执行很快。定时器回调static void key_timer_expire(struct timer_list *t) { struct gpio_key *gpio_key from_timer(gpio_key, t, key_timer); int val gpiod_get_value(gpio_key-gpiod); int key (gpio_key-gpio 8) | val; put_key(key); wake_up_interruptible(gpio_key_wait); kill_fasync(button_fasync, SIGIO, POLL_IN); }三种回调里只有定时器回调做了完整处理放入缓冲区、唤醒进程、异步通知。其他两个只是打印演示。workqueue 回调static void key_work_func(struct work_struct *work) { struct gpio_key *gpio_key container_of(work, struct gpio_key, work); int val gpiod_get_value(gpio_key-gpiod); printk(key_work_func: the process is %s pid %d\n, current-comm, current-pid); printk(key_work_func key %d %d\n, gpio_key-gpio, val); }这里打印current-comm和current-pid是刻意为之目的是直观展示 workqueue 运行在内核线程worker的进程上下文里输出结果类似key_work_func: the process is kworker/0:1 pid 23这和 tasklet 运行在中断上下文形成鲜明对比。probe 中的初始化// 定时器使用新接口 timer_setup timer_setup(gpio_keys_100ask[i].key_timer, key_timer_expire, 0); gpio_keys_100ask[i].key_timer.expires ~0; // 暂不触发 add_timer(gpio_keys_100ask[i].key_timer); // tasklet使用旧接口 tasklet_init(gpio_keys_100ask[i].tasklet, key_tasklet_func, gpio_keys_100ask[i]); // workqueue INIT_WORK(gpio_keys_100ask[i].work, key_work_func);定时器用了新接口timer_setup但 tasklet 还是旧接口tasklet_init两者不统一。remove 中的清理free_irq(gpio_keys_100ask[i].irq, gpio_keys_100ask[i]); del_timer(gpio_keys_100ask[i].key_timer); tasklet_kill(gpio_keys_100ask[i].tasklet); // 漏掉了 cancel_work_sync三种机制清理对比定时器 del_timer_sync() ← 原代码用了不安全的 del_timer tasklet tasklet_kill() ← 正确 workqueue cancel_work_sync() ← 原代码漏掉了改进点tasklet 改用新接口/* 原代码 */ tasklet_init(gpio_keys_100ask[i].tasklet, key_tasklet_func, gpio_keys_100ask[i]); static void key_tasklet_func(unsigned long data) { struct gpio_key *gpio_key data; ... } /* 改进 */ tasklet_setup(gpio_keys_100ask[i].tasklet, key_tasklet_func); static void key_tasklet_func(struct tasklet_struct *t) { struct gpio_key *gpio_key from_tasklet(gpio_key, t, tasklet); ... }remove 补全清理/* 改进后的 remove */ for (i 0; i count; i) { free_irq(gpio_keys_100ask[i].irq, gpio_keys_100ask[i]); del_timer_sync(gpio_keys_100ask[i].key_timer); // sync版本更安全 tasklet_kill(gpio_keys_100ask[i].tasklet); cancel_work_sync(gpio_keys_100ask[i].work); // 补上 }read 补全错误处理c/* 改进 */ if (size 4) return -EINVAL; if (wait_event_interruptible(gpio_key_wait, !is_key_buf_empty())) return -EINTR; key get_key(); if (copy_to_user(buf, key, 4)) return -EFAULT; return 4;线程化中断threaded IRQ是为每个中断创建一个专属内核线程来处理下半部。硬中断上半部 │ └──→ return IRQ_WAKE_THREAD │ ▼ 唤醒专属内核线程 irq/xx-设备名 │ ▼ 线程中执行下半部基本使用/* 上半部硬中断上下文 */ static irqreturn_t gpio_key_top_half(int irq, void *dev_id) { // 只做最少的事 // 清中断标志等 return IRQ_WAKE_THREAD; // 唤醒线程 } /* 下半部运行在内核线程 */ static irqreturn_t gpio_key_thread_handler(int irq, void *dev_id) { struct gpio_key *gpio_key dev_id; int val; msleep(20); // 可以睡眠用来去抖 val gpiod_get_value(gpio_key-gpiod); int key (gpio_key-gpio 8) | val; put_key(key); wake_up_interruptible(gpio_key_wait); kill_fasync(button_fasync, SIGIO, POLL_IN); return IRQ_HANDLED; } /* 注册 */ request_threaded_irq( irq, gpio_key_top_half, // 上半部可以为NULL gpio_key_thread_handler, // 下半部线程函数 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpio_key, dev );上半部为 NULL 的情况request_threaded_irq( irq, NULL, // 上半部为NULL gpio_key_thread_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, gpio_key, dev );上半部为 NULL 时必须加 IRQF_ONESHOT没有上半部 硬中断结束 ──→ 重新开中断 ──→ 线程还没执行完 新中断又来了 反复触发中断风暴 加了 IRQF_ONESHOT 硬中断结束 ──→ 保持中断屏蔽 ──→ 线程执行完 ──→ 再开中断验证线程存在# 加载驱动后 $ ps aux | grep irq root irq/45-gpio_key ← 自动创建的专属线程 # 查看优先级 $ chrt -p $(pgrep irq/45) 调度策略: SCHED_FIFO 调度优先级: 50优点可以睡眠static irqreturn_t thread_handler(int irq, void *dev_id) { msleep(20); // ✅ 去抖 mutex_lock(my_mutex); // ✅ 可以加锁 kmalloc(GFP_KERNEL); // ✅ 可以正常分配内存 ... }可以设置实时优先级// 线程默认是 SCHED_FIFO 实时线程优先级50 // 可以根据需要调整满足实时性要求 struct irq_desc *desc irq_to_desc(irq); sched_setscheduler(desc-action-thread, SCHED_FIFO, param);每个中断独占线程gpio_key ──→ irq/45-gpio_key 专属线程 uart ──→ irq/32-uart 专属线程 spi ──→ irq/28-spi 专属线程 互不影响一个慢不影响其他代码结构清晰上半部硬件相关快速处理 下半部业务逻辑随意发挥 职责分明代码易读易维护PREEMPT_RT 实时内核的基础实时内核把几乎所有中断都线程化 让内核完全可抢占 实现硬实时适合工业控制场景缺点每个中断创建一个线程占用资源中断数量多时 irq/1-xxx irq/2-xxx irq/3-xxx ... 线程数量可观占用内存和调度资源线程调度有延迟线程被唤醒 ──→ 等调度器调度 ──→ 才能执行 │ 这段延迟不确定 不如 tasklet 实时性好线程切换开销唤醒线程需要上下文切换 比 tasklet 直接在软中断里执行开销更大和其他下半部对比taskletworkqueuethreaded IRQ执行上下文软中断内核线程池专属内核线程可以睡眠❌✅✅实时性高低中可调资源占用少共享线程池每个IRQ一个线程代码复杂度简单简单简单新内核推荐❌✅✅如何选择下半部不能睡眠需要极快响应 └──→ tasklet但新内核不推荐 下半部需要睡眠多个任务共享 └──→ workqueue 下半部需要睡眠需要独立优先级控制 └──→ threaded IRQ 实时系统工业控制 └──→ threaded IRQ PREEMPT_RT