【Linux 驱动开发】七. 中断下半部

中断下半部

  • [1. 中断下半部与相关机制](#1. 中断下半部与相关机制)
    • [1.1 方式一:使用 `tasklet` 实现](#1.1 方式一:使用 tasklet 实现)
      • [1.1.1 原理](#1.1.1 原理)
      • [1.1.2 实现](#1.1.2 实现)
    • [1.2 方式二:工作队列实现中断下半部](#1.2 方式二:工作队列实现中断下半部)
      • [1.2.1 原理](#1.2.1 原理)
      • [1.2.2 实现](#1.2.2 实现)
    • [1.3 方式三:使用中断线程实现下半部(Threaded IRQ)](#1.3 方式三:使用中断线程实现下半部(Threaded IRQ))
      • [1.3.1 在申请中断时创建下半部线程](#1.3.1 在申请中断时创建下半部线程)
      • [1.3.2 中断处理函数中唤醒 下半部](#1.3.2 中断处理函数中唤醒 下半部)
      • [1.3.3 实现下半部代码](#1.3.3 实现下半部代码)
  • [2. mmap实现](#2. mmap实现)
  • [3. 多路复用](#3. 多路复用)
    • [3.1 概念](#3.1 概念)
    • [3.2 实现驱动 `.poll`](#3.2 实现驱动 .poll)
  • [4. 定时器](#4. 定时器)
    • [4.1 内核函数](#4.1 内核函数)
    • [4.2 定时器时间单位](#4.2 定时器时间单位)
    • [4.3 实例](#4.3 实例)
      • [4.3.1 在`probe`中初始化定时器](#4.3.1 在probe中初始化定时器)
      • [4.3.2 在中断处理函数中,设置超时时间(启动定时器)](#4.3.2 在中断处理函数中,设置超时时间(启动定时器))
      • [4.3.3 实现超时函数](#4.3.3 实现超时函数)

摘要:本文全面梳理 Linux 中断下半部的三种实践:tasklet 强调快速上下文并提示退出需配合 tasklet_kill;工作队列结合私有 workqueue 与 cancel_work_sync 保证任务顺序回收;线程化中断阐明 IRQF_ONESHOT 与 IRQ_WAKE_THREAD 的联用以及可睡眠优势。文中进一步补充 mmap 映射时的物理内存选取策略、poll 读后重置 have_data、定时器初始化与 del_timer_sync 的释放流程,并罗列常见陷阱、同步要点与消抖策略,形成从 probe 阶段资源管理到用户态多路复用交互的闭环驱动开发指南。

1. 中断下半部与相关机制

中断下半部的目标是把「可以延迟处理」的工作从硬中断上下文中转移出去,降低中断响应时间,减少锁竞争,并让复杂逻辑在可睡眠的上下文中完成。为了使中断处理过程尽可能快的结束,可以把中断处理中不太重要的代码放到其他地方来执行,把这种方式称为中断下半部。

在linux中,中断下半部有三种实现方式:

机制 上下文 是否可睡眠 调度粒度 典型场景
Tasklet Softirq CPU 共享队列 高频率、小工作量、不可阻塞逻辑
Workqueue 进程上下文 内核线程(kworker 需要睡眠、访问慢速外设、IO 同步
Threaded IRQ 内核线程 专用线程/共享线程 中断后必须立即睡眠或调用阻塞 API
  • Tasklet 与 Softirq 共享上下文,不能调用可能睡眠的函数;同一 tasklet 在同一时刻只会在一个 CPU 上执行。
  • 系统工作队列 system_wq 会在不同 CPU 上并发执行;若需要顺序执行,可使用 INIT_WORK() + alloc_workqueue() 构建私有单线程队列。
  • Threaded IRQ 仍需要一个「快速」的硬中断入口;若硬中断入口为 NULL 时必须结合 IRQF_ONESHOT 确保控制器不会再次触发。

1.1 方式一:使用 tasklet 实现

1.1.1 原理

1.1.2 实现

驱动中:

1》初始化 tasklet 对象

c 复制代码
 void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), 
                                                                 unsigned long data)
 //参数1 -----tasklet对象地址
 //参数2 -----中断下半部执行函数的指针
 //参数3 -----传给中断下半部函数的参数,类似于pthread_create的最后一个参数

例如:

c 复制代码
 tasklet_init(&key_dev->tesklet, key_irq_fun_down, 120);

2》实现中断下半部函数

c 复制代码
 void key_irq_fun_down(unsigned long data)
 {
     printk("--------^_^ %s---------\n",__FUNCTION__);
     key_dev->have_data = 1;
     //唤醒阻塞的进程
     wake_up_interruptible(&key_dev->wq_head);
     printk("data = %d\n",data);
 }

3》在中断处理函数中启动中断下半部

c 复制代码
 static inline void tasklet_schedule(struct tasklet_struct *t)
 例如: 
 irqreturn_t key_irq_fun(int irqno, void * dev_data)
 {
     int value;
     printk("--------^_^ %s---------\n",__FUNCTION__);
 
     //获取中断引脚的数据
     value = gpiod_get_value(key_dev->gpioa);
 
     //根据value判断按键松开还是按下
     if(value){  //松开
         printk("key1 up\n");
         key_dev->key_data.code = KEY_1;
         key_dev->key_data.value = 0;
     }else{   //按下
         printk("key1 pressed");
         key_dev->key_data.code = KEY_1;
         key_dev->key_data.value = 1;
     }
 
     //启动中断下半部
     tasklet_schedule(&key_dev->tasklet);
 
     return IRQ_HANDLED;
 }

Tasklet 实现(方式一):创建与销毁:

c 复制代码
#include <linux/interrupt.h>

static void key_tasklet_fn(unsigned long data);
DECLARE_TASKLET(key_tasklet, key_tasklet_fn, 0);
/* 或在 probe 中:
 * tasklet_setup(&key_dev->tasklet, key_tasklet_fn);
 */

static void key_tasklet_fn(unsigned long data)
{
    struct key_device *key = (struct key_device *)data;

    /* 不可睡眠,尽量缩短时间 */
    key->have_data = true;
    wake_up_interruptible(&key->wq_head);
}

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_device *key = dev_id;

    /* 采样硬件状态、清除中断 */
    key->key_data.value = !gpiod_get_value(key->gpio);
    tasklet_schedule(&key->tasklet);

    return IRQ_HANDLED;
}

static void key_free_resources(struct key_device *key)
{
    tasklet_kill(&key->tasklet);   /* 退出前确保 tasklet 不在运行 */
}
  • 使用 tasklet_kill()tasklet_disable_sync() 做退出同步。
  • 在 SMP 环境下,tasklet 默认只在一个 CPU 上运行;如果对延迟敏感,可考虑使用 tasklet_hi_schedule()

1.2 方式二:工作队列实现中断下半部

1.2.1 原理


核心结构:

如图 struct work_structfunc 指针由内核线程调度执行。

  • INIT_WORK() 只初始化 work;真正的调度通过 schedule_work()(加入系统缺省队列)或 queue_work(custom_wq, &work) 完成。
  • 如果任务需要持久存在,必须在模块退出时调用 cancel_work_sync()flush_work(),确保执行完毕。

1.2.2 实现

c 复制代码
struct work_struct {
 /*  atomic_long_t data; */
     unsigned long data;
 
     struct list_head entry;
     work_func_t func;
 #ifdef CONFIG_LOCKDEP
     struct lockdep_map lockdep_map;
 #endif
 };

1》初始化work_struct对象

c 复制代码
 INIT_WORK(struct work_struct *_work, work_func_t _func) 
 例如:
 INIT_WORK(&key_dev->work, key_irq_fun_down);

2》实现中断下半部执行函数

c 复制代码
 void key_irq_fun_down(struct work_struct *work)
 {
     printk("--------^_^ %s---------\n",__FUNCTION__);
     key_dev->have_data = 1;
     //唤醒阻塞的进程
     wake_up_interruptible(&key_dev->wq_head);
 
 }

3》启动中断下半部 ----在中断处理函数中

c 复制代码
 schedule_work(&key_dev->work);

1.3 方式三:使用中断线程实现下半部(Threaded IRQ)

1.3.1 在申请中断时创建下半部线程

c 复制代码
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)

例如:

c 复制代码
ret = request_threaded_irq(key_dev->irqno, key_irq_fun, key_irq_fun_down, IRQF_TRIGGER_FALLING |IRQF_TRIGGER_RISING, "key1_irq", NULL);
if(ret < 0){
    printk("request_irq error");
    goto err_gpio_put;
}

1.3.2 中断处理函数中唤醒 下半部

c 复制代码
irqreturn_t key_irq_fun(int irqno, void * dev_data)
{
    printk("--------^_^ %s---------\n",__FUNCTION__);

    。。。。。。
    return IRQ_WAKE_THREAD;   //返回IRQ_WAKE_THREAD表示启动下半部
}

1.3.3 实现下半部代码

c 复制代码
irqreturn_t key_irq_fun_down(int irqno, void * dev_data)
{
    printk("--------^_^ %s---------\n",__FUNCTION__);
    key_dev->have_data = 1;
    //唤醒阻塞的进程
    wake_up_interruptible(&key_dev->wq_head);

    return IRQ_HANDLED;
}

2. mmap实现

bash 复制代码
应用层:
     void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
     //参数1 ----- 指定要映射的虚拟空间起始地址,一般为NULL,系统自动分配
     //参数2 ----- 映射的空间大小
     //参数3 ----- 空间的访问权限:PROT_EXEC  PROT_READ PROT_WRITE PROT_NONE 
     //参数4 ----- 是否可以被共享:MAP_SHARED,MAP_PRIVATE
     //参数5 ----- 文件描述符
     //参数6 ----- 相对物理内存起始位置的偏移量
     //返回值 -----成功:映射的虚拟空间起始地址,失败:-1

关键要点:

  • virt_to_phys() 仅对线性映射地址(kmalloc/dma_alloc_coherent 返回的内存)有效;对 vmalloc 区域必须使用 page_to_pfn()virt_to_phys(page_address(...))
  • 建议为用户映射的内存使用 dma_alloc_coherent()devm_kmalloc() + get_order() 保证连续。
  • 结束前调用 munmap(),驱动侧无需专门回收,但若使用 dma_alloc_coherent() 应在 remove() 中释放。

驱动:

c 复制代码
     struct file_operations {
         int (*mmap) (struct file *, struct vm_area_struct *);
     }
     
     例如:
     int key_drv_mmap(struct file *filp, struct vm_area_struct *vma)
     {
         unsigned long addr;
         printk("---------^_^ %s--------------\n",__FUNCTION__);
 
         //1,获取物理内存
         addr = virt_to_phys(key_dev->virt);
 
         //2,映射物理内存到应用程序的虚拟空间去
         vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
         return io_remap_pfn_range(vma, vma->vm_start, addr>> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot);
     }
     long key_drv_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
     {
         int ret;
         printk("---------^_^ %s--------------\n",__FUNCTION__);
         switch(cmd){
             case KEY_GET_IOC_DATE:
                 ret = copy_to_user((void __user  *)args, key_dev->virt, strlen(key_dev->virt));
                 if(ret > 0){
                     printk("copy_to_user error\n");
                     return -EINVAL;
                 }
                 break;
             default:
                 printk("unknow cmd\n");
                 break;
         }
 
         return 0;
  }

3. 多路复用

3.1 概念

bash 复制代码
在linux应用开发中:
     需要同时监测多个IO端口是否有数据时,可以使用多路复用来监测,比如:网络编程中的:select 和 poll

3.2 实现驱动 .poll

1》应用层:

c 复制代码
  #include <poll.h>
  int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  //参数1 ----- 结构体指针:struct pollfd * ,要监测的文件描述及事件
  		  struct pollfd {
                 int   fd;         /* file descriptor */
                 short events;     /* requested events */
                 short revents;    /* returned events */
             };
  //参数2 ---- 要监测的文件描述符个数
  //参数3 ---- 超时监测:
  				 0  ---不阻塞,立即返回
  				-1 ---一直阻塞,直到有IO事件产生
  				>0 -----当timeout时间到时没有OI事件产生,则立即返回
  //返回值 ----- 成功:事件个数,失败:-1,超时:0

例如: 
  struct pollfd fds[2];
  //将需要监测的IO添加到fds中
  fds[0].fd =  STDIN_FILENO;
  fds[0].events = POLLIN;

  fds[1].fd = fd;
  fds[1].events = POLLIN;

while(1){
      //多路复用
      if(poll(fds,sizeof(fds)/sizeof(fds[0]),-1) < 0){
          perror("poll");
          exit(1);
      }
      //判断标准输入是否有数据
      if(fds[0].revents & POLLIN){
         	//进行IO操作
      }
      //判断按键是否有数据
      if(fds[1].revents & POLLIN){
      	//进行IO操作
      }

2》驱动:

将当前进程的等待队列头注册到内核中

c 复制代码
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
//参数1 ----- struct file结构体指针
//参数2 ----- 等待队列头
//参数3 ----- 列表

例如: 
__poll_t key_drv_poll(struct file *filp, struct poll_table_struct *wait)
{
    __poll_t mask = 0;
    printk("---------^_^ %s--------------\n",__FUNCTION__);

    //将当前的等待队列头注册到轮询表中
    poll_wait(filp, &key_dev->wq, wait);

    if(key_dev->have_data)
        mask |= POLLIN;

    return mask;
 }

4. 定时器

4.1 内核函数

bash 复制代码
所谓定时器,就是闹钟,时间到后你就要做某些事。有2个要素:时间、做事,换成程序员的话就是:超时时间、函数。
在内核中使用定时器很简单,涉及这些函数(参考内核源码include\linux\timer.h):
① timer_setup(timer, callback, flags) 设置定时器,主要是初始化timer_list结构体,设置其中的函数、flags(一般设为0)。
② void add_timer(struct timer_list *timer) 向内核添加定时器。timer->expires表示超时时间。 当超时时间到达,内核就会调用这个函数:timer->function(timer->data)。
③ int mod_timer(struct timer_list *timer, unsigned long expires) 修改定时器的超时时间,

它等同于:del_timer(timer); timer->expires = expires; add_timer(timer)但是更加高效。
④ int del_timer(struct timer_list *timer) 删除定时器。

4.2 定时器时间单位

编译内核时,可以在内核源码根目录下用 "ls -a" 看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:CONFIG_HZ=100

这表示内核每秒中会发生 100 次系统滴答中断(tick),这就像人类的心跳一样,这是 Linux 系统的心跳。每发生一次tick中断,全局变量 jiffies 就会累加1。

bash 复制代码
CONFIG_HZ=100表示每个滴答是10ms。
定时器的时间就是基于jiffies的,我们修改超时时间时,一般使用这2种方法:

① 在add_timer之前,直接修改:
timer.expires = jiffies + xxx;   // xxx表示多少个滴答后超时,也就是xxx*10ms
timer.expires = jiffies + 2*HZ;  // HZ等于CONFIG_HZ,2*HZ就相当于2秒

② 在add_timer之后,使用mod_timer修改:
mod_timer(&timer, jiffies + xxx);   // xxx表示多少个滴答后超时,也就是xxx*10ms
mod_timer(&timer, jiffies + 2*HZ);  // HZ等于CONFIG_HZ,2*HZ就相当于2秒

4.3 实例

4.3.1 在probe中初始化定时器

c 复制代码
//初始化定时器
timer_setup(&key1_dev->timer, key_timer_fun, 0);

//将定时器对象加入内核中
add_timer(&key1_dev->timer);

4.3.2 在中断处理函数中,设置超时时间(启动定时器)

c 复制代码
irqreturn_t key1_irq_fun(int irqno, void *dev_data)
{
    printk("---------%s----------\n",__FUNCTION__);

    //延时10ms,再读取io口的电平 -//消除按键抖动 (jiffies: 滴答定时器每触发一次中断,jiffies就会加1, HZ=100表示频率)
    //mod_timer(&key1_dev->timer,jiffies + 1);
    mod_timer(&key1_dev->timer, jiffies + msecs_to_jiffies(10));

    return IRQ_HANDLED;
}

4.3.3 实现超时函数

c 复制代码
//定时器超时时,会被系统调用
void key_timer_fun(struct timer_list * timer)
{
    int value;
    printk("---------%s----------\n",__FUNCTION__);
    //获取gpiof_9的电平
    value = gpiod_get_value(key1_dev->gpiono);

    key1_dev->data.code = KEY_1;
    //判断电平高低
    if(value){  //松开
        printk("key1  ----> up\n");
        key1_dev->data.value = 0;
    }else{   //按下
        printk("key ----->pressed\n");
        key1_dev->data.value = 1;
    }
    //唤醒阻塞的进程
    wake_up_interruptible(&key1_dev->wq_head);
    key1_dev->have_data = 1;
}

综上。本文介绍了任务上下文差异、同步方法、资源回收、线程化中断以及 mmap/poll/定时器的关键细节。通过完善的样板代码与注意事项,我们可以在实际驱动中安全地选择合适的下半部机制,实现可靠的消抖、数据上报与用户态接口。

相关推荐
cyber_两只龙宝2 小时前
LVS-DR模式实验配置及原理详解
linux·网络·云原生·智能路由器·lvs·dr模式
好好学习啊天天向上7 小时前
C盘容量不够,python , pip,安装包的位置
linux·python·pip
li_wen017 小时前
文件系统(八):Linux JFFS2文件系统工作原理、优势与局限
大数据·linux·数据库·文件系统·jffs2
wypywyp7 小时前
2.虚拟机一直显示黑屏,无法打开,可能是分配的硬盘空间不够
linux·运维·服务器
SongYuLong的博客8 小时前
TL-WR710N-V2.1 硬改刷机OpenWRT源码编译固件
linux·物联网·网络协议
AlfredZhao8 小时前
Docker 快速入门:手把手教你打包 Python 应用
linux·docker·podman
HIT_Weston9 小时前
107、【Ubuntu】【Hugo】搭建私人博客:模糊搜索 Fuse.js(三)
linux·javascript·ubuntu
沃尔特。9 小时前
直流无刷电机FOC控制算法
c语言·stm32·嵌入式硬件·算法
CW32生态社区9 小时前
CW32L012的PID温度控制——算法基础
单片机·嵌入式硬件·算法·pid·cw32