当前位置: 首页 > news >正文

嵌入式linux学习记录十一,tasklet、workqueue、中断下半部分线程化处理

  1. tasklet:
    1. 执行时机

      硬中断结束 │ ▼ 内核检查是否有 pending 的软中断 │ ▼ 有 ──→ 执行 tasklet 回调 │ ▼ 执行完,tasklet 停止 等待下次 tasklet_schedule 触发
    2. 关键特性

      同一个 tasklet │ ├── 同一时刻只在一个 CPU 上执行 │ 不会并发重入 │ └── 多次 tasklet_schedule 如果上次还没执行,不会重复加入队列 只执行一次
    3. 优点

      1. 实时性好

        硬中断结束后立刻执行 不需要等调度器调度 响应速度比 workqueue 快
      2. 不会并发重入

        同一个 tasklet 同一时刻只在一个CPU执行 不需要加锁保护 tasklet 本身 比 softirq 使用更安全
      3. 使用简单

        tasklet_setup(&tasklet, func); // 初始化 tasklet_schedule(&tasklet); // 触发 tasklet_kill(&tasklet); // 销毁
    4. 缺点

      1. 不能睡眠

        运行在软中断上下文 │ ├── 不能调用 mutex_lock ❌ ├── 不能调用 msleep ❌ ├── 不能调用 kmalloc(GFP_KERNEL) ❌ └── 只能用 kmalloc(GFP_ATOMIC) ✅
      2. 不能做耗时操作

        tasklet 占用 CPU 时间过长 │ ▼ 影响其他软中断的执行 │ ▼ 系统响应变差
      3. 已被新内核不推荐使用

        内核社区认为: tasklet 能做的事 workqueue 都能做 workqueue 限制更少,更灵活 新驱动推荐用 threaded IRQ 或 workqueue 替代 tasklet
      4. 旧接口类型不安全

        /* 旧接口: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); // 安全 }
  2. workqueue:

    1. 优点

      1. 可以睡眠

        void my_work_func(struct work_struct *work) { mutex_lock(&my_mutex); // ✅ 可以 msleep(100); // ✅ 可以 kmalloc(size, GFP_KERNEL); // ✅ 可以 }
      2. 可以处理耗时操作

        网络数据包处理、文件IO、复杂计算 这些放在中断上半部会导致系统卡顿 放在workqueue完全没问题
      3. 使用简单

        // 只需两步 INIT_WORK(&work, func); // 初始化 schedule_work(&work); // 提交
      4. 共享线程,节省资源

        系统默认workqueue 所有模块共用worker线程 不需要每个驱动自己创建线程
    2. 缺点

      1. 实时性差

        任务提交后,何时执行取决于调度器 worker线程优先级不高 对实时性要求高的场景不适合 tasklet ──→ 中断返回前就执行,实时性更好 workqueue ──→ 等调度器调度,有延迟
      2. 共享workqueue可能被拖慢

        系统默认workqueue 所有人共用 某个任务执行很慢 │ ▼ 后面的任务都要等 │ ▼ 影响其他模块的任务执行
      3. 并发问题

        同一个 work 在执行期间,再次 schedule_work │ ▼ 不会重复加入队列 │ ▼ 可能丢失一次执行机会 需要自己处理这种情况
      4. 自定义workqueue消耗资源

        create_workqueue() // 每个CPU创建一个线程 // CPU多时线程数量可观 create_singlethread_workqueue() // 只有一个线程,但串行执行
  3. 简单例程:
    1. 代码整体说明

      这个例程目的是同时演示三种下半部机制,并不是一个完整的按键驱动,所以三种机制的回调函数里只有打印,没有实际处理按键数据。

    2. 三种下半部在哪里触发

      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,去抖时间合理。

    3. 三种下半部的回调

      1. 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); }

        运行在软中断上下文,不能睡眠,执行很快。

      2. 定时器回调:

        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); }

        三种回调里只有定时器回调做了完整处理:放入缓冲区、唤醒进程、异步通知。其他两个只是打印演示。

      3. 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->commcurrent->pid刻意为之,目的是直观展示 workqueue 运行在内核线程(worker)的进程上下文里,输出结果类似:

        key_work_func: the process is kworker/0:1 pid 23

        这和 tasklet 运行在中断上下文形成鲜明对比。

    4. 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,两者不统一。

    5. 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() ← 原代码漏掉了
    6. 改进点

      1. 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); ... }
      2. 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); // 补上 }
      3. 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;
  4. 线程化中断(threaded IRQ)是为每个中断创建一个专属内核线程来处理下半部。

    硬中断上半部 │ └──→ return IRQ_WAKE_THREAD │ ▼ 唤醒专属内核线程 irq/xx-设备名 │ ▼ 线程中执行下半部
    1. 基本使用

      /* 上半部:硬中断上下文 */ 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 );
    2. 上半部为 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: 硬中断结束 ──→ 保持中断屏蔽 ──→ 线程执行完 ──→ 再开中断
    3. 验证线程存在

      # 加载驱动后 $ ps aux | grep irq root irq/45-gpio_key ← 自动创建的专属线程 # 查看优先级 $ chrt -p $(pgrep "irq/45") 调度策略: SCHED_FIFO 调度优先级: 50
    4. 优点

      1. 可以睡眠

        static irqreturn_t thread_handler(int irq, void *dev_id) { msleep(20); // ✅ 去抖 mutex_lock(&my_mutex); // ✅ 可以加锁 kmalloc(GFP_KERNEL); // ✅ 可以正常分配内存 ... }
      2. 可以设置实时优先级

        // 线程默认是 SCHED_FIFO 实时线程,优先级50 // 可以根据需要调整,满足实时性要求 struct irq_desc *desc = irq_to_desc(irq); sched_setscheduler(desc->action->thread, SCHED_FIFO, &param);
      3. 每个中断独占线程

        gpio_key ──→ irq/45-gpio_key 专属线程 uart ──→ irq/32-uart 专属线程 spi ──→ irq/28-spi 专属线程 互不影响,一个慢不影响其他
      4. 代码结构清晰

        上半部:硬件相关,快速处理 下半部:业务逻辑,随意发挥 职责分明,代码易读易维护
      5. PREEMPT_RT 实时内核的基础

        实时内核把几乎所有中断都线程化 让内核完全可抢占 实现硬实时,适合工业控制场景
    5. 缺点

      1. 每个中断创建一个线程,占用资源

        中断数量多时: irq/1-xxx irq/2-xxx irq/3-xxx ... 线程数量可观,占用内存和调度资源
      2. 线程调度有延迟

        线程被唤醒 ──→ 等调度器调度 ──→ 才能执行 │ 这段延迟不确定 不如 tasklet 实时性好
      3. 线程切换开销

        唤醒线程需要上下文切换 比 tasklet 直接在软中断里执行开销更大
    6. 和其他下半部对比

      taskletworkqueuethreaded IRQ
      执行上下文软中断内核线程池专属内核线程
      可以睡眠
      实时性中(可调)
      资源占用共享线程池每个IRQ一个线程
      代码复杂度简单简单简单
      新内核推荐
    7. 如何选择

      下半部不能睡眠,需要极快响应 └──→ tasklet(但新内核不推荐) 下半部需要睡眠,多个任务共享 └──→ workqueue 下半部需要睡眠,需要独立优先级控制 └──→ threaded IRQ 实时系统,工业控制 └──→ threaded IRQ + PREEMPT_RT
http://www.jsqmd.com/news/962711/

相关文章:

  • 零售店库存预测实操包:用随机森林算出补货时间点,带交互图表和完整代码
  • 26年吕梁市黄金回收靠谱门店推荐 黄金+K金+白银+铂金回收门店TOP5排行榜+联系方式推荐 - 奢金汇
  • 035、液态镜头技术探索:电压驱动对焦与手机差异化应用的可行性
  • 别再手动记录温度了!用LabVIEW+Excel打造自动化数据采集与存储系统(附完整源码)
  • 植物大战僵尸终极修改器:PvZ Tools 2.7.4 完整使用指南
  • 2026年厦门市上门黄金回收白银回收铂金回收测评,五家全城可上门实体店整理推荐 - 嵩山路大王
  • 副队长HTML教程(1)--序言
  • 技术人如何应对职业文化迁徙:从硅谷到本土的适应策略
  • 明日方舟终极自动化助手:MAA助手的完整使用指南
  • FramePack:如何用13B模型在笔记本GPU上实现超长AI视频生成
  • 3步解锁完整Office:Ohook免费激活Microsoft 365终极方案
  • 2026 合肥黄金回收权威指南:高价变现安全避坑首选合扬 - 开心测评
  • 富士康转型二十年:从代工巨头到产业链突围的八大战略解析
  • 深入LIO-SAM:图解五大核心模块的数据流与ROS话题通信(附消息关系图)
  • MLOps实战:从Notebook到高可用模型服务的工程契约
  • 浏览器中的专业视频编辑:OmniClip如何革新Web端创作体验?
  • Extension Manager全面指南:一站式GNOME扩展管理解决方案
  • GitLens实战指南:在VS Code中高效追溯代码变更源头
  • 终极指南:联想拯救者BIOS高级设置解锁工具完整教程
  • 终极指南:Voron 2.4开源CoreXY 3D打印机如何重新定义DIY打印体验
  • ESP32蓝牙音频终极指南:快速构建蓝牙音乐接收器和发送器
  • 2026 沈阳黄金处置行业白皮书,揭秘本地高价变现靠谱门道 - 开心测评
  • 【20年数字营销老兵亲测】CSDN AI分发前是否需提前绑定?用3组AB测试数据告诉你:延迟绑定导致CTR下降47.6%
  • 用mbedtls给你的STM32物联网设备‘上锁’:从SHA1加密到MQTT over TLS实战构想
  • 遥感小白避坑指南:用GDAL+PyTorch处理6波段.tif影像喂给Faster R-CNN的完整流程
  • 从工程师视角拆解创新力培养:家庭、职场与个人成长
  • S4.3创造而非替代——AI产品的价值主张重构
  • Colmap vs OpenMVG实战:用手机拍鞋子和恐龙,谁的三维重建效果更靠谱?
  • 如何永久保存微信聊天记录:WeChatMsg完整指南让你的数字记忆不再丢失
  • Deep-Live-Cam:3分钟学会实时人脸替换的终极指南