中断下半部
- [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 实现下半部代码)
- [1.1 方式一:使用 `tasklet` 实现](#1.1 方式一:使用
- [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 实现超时函数)
- [4.3.1 在`probe`中初始化定时器](#4.3.1 在
摘要:本文全面梳理 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_struct 的 func 指针由内核线程调度执行。
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/定时器的关键细节。通过完善的样板代码与注意事项,我们可以在实际驱动中安全地选择合适的下半部机制,实现可靠的消抖、数据上报与用户态接口。