嵌入式linux学习记录十一,tasklet、workqueue、中断下半部分线程化处理
- 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, ¶m);每个中断独占线程
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 直接在软中断里执行开销更大
和其他下半部对比
tasklet workqueue threaded IRQ 执行上下文 软中断 内核线程池 专属内核线程 可以睡眠 ❌ ✅ ✅ 实时性 高 低 中(可调) 资源占用 少 共享线程池 每个IRQ一个线程 代码复杂度 简单 简单 简单 新内核推荐 ❌ ✅ ✅ 如何选择
下半部不能睡眠,需要极快响应 └──→ tasklet(但新内核不推荐) 下半部需要睡眠,多个任务共享 └──→ workqueue 下半部需要睡眠,需要独立优先级控制 └──→ threaded IRQ 实时系统,工业控制 └──→ threaded IRQ + PREEMPT_RT
