imx6ull linux中断*
内核中断
裸机的中断
之前我们在裸机中,要想使用中断,需要经过3个门槛:
- 外设中断使能
- GIC开中断,设置中断优先级(处理外部中断)
- 内核控制寄存器打开全局中断使能
这样我们才能使用中断。
在裸机代码中,我们需要自己处理GIC的寄存器,内核中断使能的寄存器(CPSR)等等这些,需要实际操作底层的硬件寄存器。但是移植性非常差。
linux内核中断
和前面的gpio子系统一样,linux内核也实现了中断管理子系统,提供了完善的中断框架。
- 在这个框架之上,我们的驱动开发,只需要申请中断,注册中断处理函数即可。
- 在这个框架之下,由原厂的工程师,替linux内核实现具体的操作底层寄存器。
我们主要在这一节学习linux内核的中断框架api,底层如何实现,还需要对子系统有点了解才行
中断API
裸机实验中断的处理方法:
涉及的外设:
- 使用中断的设备(uart)
- GIC
- 使能中断,初始化相应的寄存器
- 注册中断服务函数
- 进入IRQ中断服务函数,判断中断源,执行相应处理函数
中断号
linux内核的中断子系统,每个中断都有一个中断号,使用一个int变量表示中断号(具体参考gpio中断那一个裸机实验)
request_irq
中断子系统规定,如果要想使用某个中断,是需要申请的。
request_irq
- 用于申请中断
- 会导致进程睡眠阻塞
- 不能在中断上下文/禁止阻塞的代码中,使用request_irq
- 实际作用:
- 使能中断

- 使用:


GPIO1_IO18, 按下低电平,可以设置下降沿触发中断,多个标志可以用
|来组合
free_irq
这个就是释放掉相应的中断了,就是关掉GIC的中断使能
如果中断没有共享,那么free_irq会:
- 删除中断处理函数
- 禁止中断
使用如下:
中断处理函数
使用request_irq函数申请中断的时候,就需要设置中断处理函数。
irqreturn_t (*irq_handler_t) (int, void *) |
中断使能/禁止函数
使能/失能 指定中断
void enable_irq(unsigned int irq) |
要求:
- 要等当前正在执行的中断处理函数执行完才返回
- 使用者要保证不会产生新的中断,并且确保已经开始的中断处理程序全部退出。
- 另一个中断禁止函数
void disable_irq_nosync(unsigned int irq)- 调用后会立即返回,不会等待当前中断处理程序执行完毕。
---
上面三个都是使能/失能某一个中断。有时候我们需要**关闭当前处理器的整个中断系统**。
```c
local_irq_enable() //使能当前处理器中断系统
local_irq_disable() //禁止当前处理器的中断系统
//当出现多个任务要关闭中断,由于没有嵌套,所以容易误开中断,所以第二个任务只能恢复以前的中断状态。
local_irq_save(flags)
local_irq_restore(flags)
上半部 下半部
上半部:就是中断服务函数
- 要求越短越好
- 建议仅响应中断,清除中断标志位即可
触摸屏:
IC通过中断通知SOC有触摸事件,I2C读取触摸数据。
下半部:就是将耗时的代码提取出来,交给下半部执行。
- 此时中断处理函数已经退出,SOC能够再次响应中断,但仍处于内核态。
相当于在中断(内核态)返回进程上下文(用户态/内核态)之间,新增了一个内核态的部分,用来执行耗时操作。
Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出:
- 放到上半部:
- 对时间敏感
- 执行速度快的操作
- 不希望处理内容被打断
- 处理的任务和硬件相关
- 剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行
下半部的实现机制
linux内核提供了多种下半部的机制。
软中断
linux内核提供结构体softirq_action表示软中断。定义在include/linux/interrupt.h
struct softirq_action |
kernel/softirq.c中定义了10个软中断
static struct softirq_action softirq_vec[NR_SOFTIRQS]; |
是说相当于进入一个软件中断,发送在中断处理函数和上下文之间?
可以看到,一共10个软中断。因此NR_SORTIRQS为10, 数组softirq_vec有10个元素。里面每个元素都是对应的软中断的服务函数softirq_action,
相当于自己搞了一套软件中断号和对应的中断向量表
所以各个CPU(多核)所执行的软件中断服务函数都是相同的(和中断向量表一样嘛)。
要使用软中断,必须要open_softirq函数注册对应的软中断处理函数
注册好软中断后,需要通过raise_softirq函数来软件触发

软中断必须在编译的时候,静态注册
内核使用softirq_init来初始化软中断, 定义在kernel/softirq.c中
这里具体不是很明白,但是更常用的是tasklet
tasklet
软中断是底层框架,而 Tasklet 是基于软中断实现的更易用的“高级封装”。
区别和联系:
linux内核使用tasklet_struct结构体表示tasklet
struct tasklet_struct |
func函数就是tasklet要执行的处理函数。由用户来定义函数内容。就相当于我们的中断处理函数。
使用:
先定义一个tasklet, 然后tasklet_init初始化tasklet
在上半部(我们实际的中断处理函数)中,调用tasklet_schedule就能使tasklet在合适的时间运行
就相当于自己开了一个软件上的可调度的任务处理链表,类似rtos
tasklet示例:
/* 定义 taselet */ |
tasklet流程图解
┌─────────────────────────────────────┐ |
工作队列
工作队列是另一种下半部的执行方式。在进程上下文执行(对比前面tasklet,还没有返回进程下文)
工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度
概念梳理
概念梳理:
- 内核:
- 是整个操作系统的 “地基 + 规则制定者”,它管理 CPU、内存、硬件,提供所有进程能运行的 “基础设施”;
- 本身是一套代码 + 数据结构 + 硬件管理逻辑,它不是 “执行单元”,而是 “管理执行单元的规则集”
- 进程:
- 地基上 “按规则干活的独立个体”(不管是
用户进程还是内核线程,本质都是 “执行流”) - 是有独立上下文、有生命周期、可被调度的 “执行单元”
- 地基上 “按规则干活的独立个体”(不管是
- 内核线程
- 跑在内核态的特殊进程,归内核管理,没有用户态的地址空间的轻量化进程
- 和普通用户进程对比:

- 内核线程的典型例子,
ps -ef |grep k, k开头的进程kthreadd:所有内核线程的 “父线程”,负责创建其他内核线程;kswapd0:内存交换守护线程,负责内存页的换入换出;kworker/0:0:中断下半部(工作队列)的执行线程;khubd:USB集线器管理线程;它们是内核用来异步处理自身任务的 “执行载体”,比如你之前学的中断下半部里的
workqueue,本质就是靠内核线程来执行延后的任务
内核线程和中断下半部,tasklet和工作队列对比的关系
工作队列流程图解
┌─────────────────────────────────────┐ |
api
linux内核使用work_struct结构体表示一个工作
struct work_struct { |
这些工作,组织成一个工作队列,工作队列使用:workqueue_struct结构体表示
struct workqueue_struct { |
linux内核使用工作者线程(worker_thread)来处理工作队列中的各种工作, linux内核用worker结构体表示工作者线程
struct worker { |
可以看到,每个工作者线程worker,都有一个工作队列。工作者线程处理自己的工作队列中的任务。
在实际的驱动开发中,我们只需要定义工作(work_struct)即可,工作队列workqueue和工作者线程worker 都不用去管
创建工作:
调度任务(开始让worker工作者线程执行他的工作队列)
示例:
/* 定义工作(work) */ |
work就相当于回调处理函数,逻辑肯定都是固定好的,比如拷贝数据任务。
中断里面判断,如果需要,就开启调度,挂上去一个任务
使用注意
- kworker工作者线程
- 不是只有一个,而是按 CPU 核心 / 工作队列类型,有多个 kworker 线程(甚至成百个),Linux 会动态创建 / 销毁,核心目的是并行处理任务、避免单线程瓶颈
- CPU核心:
- kworker/0:0、kworker/0:1 对应 CPU0,kworker/1:0 对应 CPU1)

- 工作队列类型:
- 线程分为 “普通”(处理默认队列)、“高优先级”(处理紧急任务)、“绑定 CPU”(处理指定核心任务)等类型;
- 动态管理:
- 系统负载高时,内核会动态创建更多 kworker;负载低时,会销毁闲置的 kworker
ps -ef | grep kworker | wc -l
- CPU核心:
- 为什么分类这么多:
- 不是只有一个,而是按 CPU 核心 / 工作队列类型,有多个 kworker 线程(甚至成百个),Linux 会动态创建 / 销毁,核心目的是并行处理任务、避免单线程瓶颈
- schedule,把work挂到queue
- 每个work里,都有一个状态位(
pending)- 0: work空闲,未在queue
- 1: work挂入queue, 处于待处理状态
- schedule_work的核心逻辑:
- 原子性检查pending标志位
- pending = 0, 置1, 挂入队列queue, 唤醒kworker线程
- pending = 1, 直接返回, 不会重复挂入
- 原子性检查pending标志位
所以如果一个任务已经被挂入了,就无法再次被挂入queue
- 这样设计,避免重复执行同一个任务。防止出现 “同一个任务被多次调度、重复处理数据” 的 BUG:
- 比如你用
my_work处理网卡收包,如果允许重复挂入,可能会导致同一个数据包被处理两次 - 原子性的
test_and_set_bit操作,保证了即使多 CPU/core 同时调用 schedule_work(&my_work),也只会有一次成功
- 比如你用
- 每个work里,都有一个状态位(
- 需要重复挂任务的情况(中断快速进入两次)
中断上半部 → 调用schedule_work(&my_work) → my_work还在执行(pending=1)→ 又来中断 → 再次调用schedule_work(&my_work) → 失败 → 担心数据丢失?- **方案1**: queue_work + delay_work
- 任务即使重复触发,也只需执行一次最新的(网卡收包)
- 定义 `struct delayed_work`(**带延迟的 work**)
- 调用 `mod_delayed_work`:如果任务已在队列中,会更新执行时间(不是新增任务),保证只执行一次最新的
- ```c
// 定义延迟work
struct delayed_work my_dwork;
// 初始化:绑定处理函数
INIT_DELAYED_WORK(&my_dwork, my_handler);
// 中断上半部调用(重复调用也安全)
// 参数3:延迟0ms执行(立刻执行)
mod_delayed_work(system_wq, &my_dwork, 0);- 方案2, 用完work后手动重置
- 必须执行多次,一次不能少,采集数据
- 定义多个work (work数组)
- 每次中断选择一个空闲的work挂入队列
- work执行完后,重置为空闲状态
// 定义3个work,应对高频中断 #define WORK_NUM 3 struct work_struct my_works[WORK_NUM]; // 标记每个work是否空闲 atomic_t work_idle[WORK_NUM] = {ATOMIC_INIT(1), ATOMIC_INIT(1), ATOMIC_INIT(1)}; // 初始化所有work void init_my_works() { for (int i=0; i<WORK_NUM; i++) { INIT_WORK(&my_works[i], my_handler); atomic_set(&work_idle[i], 1); } } // 中断上半部调用:找空闲work挂入 void irq_handler() { for (int i=0; i<WORK_NUM; i++) { // 原子性检查并占用空闲work if (atomic_cmpxchg(&work_idle[i], 1, 0) == 1) { schedule_work(&my_works[i]); break; } } } // work处理函数:执行完后重置为空闲 void my_handler(struct work_struct *work) { // 处理数据拷贝等任务 process_data(); // 找到当前work的索引,重置空闲标记 int idx = work - my_works; atomic_set(&work_idle[idx], 1); }
- 方案3: 用workqueue_create创建专属队列(并非解决快速重挂工作)
- 任务优先级高,不想和其他内核任务抢kworker
// 创建专属队列,会有独立的kworker线程 struct workqueue_struct *my_wq = create_workqueue("my_drv_wq"); // 挂任务到专属队列 queue_work(my_wq, &my_work);- 使用专属队列的好处
- 有一个好处是可以定制优先级,这样你这个专属队列对应的worker线程有更高的优先级,可以进行抢占。

-
- worker工作者线程,work_queue工作队列,work工作的对应关系
- 工作队列(workqueue):
- 内核预设了多个「默认队列」,驱动也可创建「专属队列」,是存放 work 的 “任务容器”;



不同调度函数,可以把work挂到不同的队列

- 工作者线程(kworker):
- 每个队列对应一组 kworker 线程池(按 CPU 核心划分),是执行 work 的 “工人”;
- 工作任务(work_struct):
- 来自不同驱动的异步任务(比如网卡收包、磁盘刷页),是 “待干的活”,可挂到任意队列中,由该队列的 kworker 执行。
- 三者关系:
- 工作队列(workqueue):
- 创建专属队列
- 调用队列创建函数,内核会自动为每个CPU核心创建对应的kworker线程

#include <linux/workqueue.h> // 定义队列指针 struct workqueue_struct *my_drv_wq; // 驱动初始化时创建队列 int my_drv_init(void) { // 方式1:创建普通队列,每个CPU一个kworker my_drv_wq = create_workqueue("my_drv_wq"); // 方式2:创建单线程队列(所有CPU共享一个kworker) // my_drv_wq = create_singlethread_workqueue("my_drv_wq_single"); if (!my_drv_wq) { pr_err("Failed to create workqueue\n"); return -ENOMEM; } return 0; } // 驱动退出时销毁队列 void my_drv_exit(void) { destroy_workqueue(my_drv_wq); }
- 怎么 “知道” 自己的 worker 是谁?
- 方式1:看进程名
- 内核线程就是一个特殊进程,
ps -ef | grep kworker 
- 内核线程就是一个特殊进程,
- 方式2:用调试接口
- 查看
/sys/kernel/debug/workqueue/workqueues文件,能看到队列和其线程池的对应关系: 
- 查看
- 方式1:看进程名
- 怎么控制创建多少 worker?
- 通过
alloc_workqueue()的参数来精细控制线程数量和行为 

- 通过
dts中断拓破关系梳理
- /
cpussoc- 中断父控制器 = gpc
- uart
- gpiox
- 中断控制器
- gpc
- 中断控制器, 中断父控制器 = intc
clocksintc:- GIC设备节点, 中断控制器
所以整个中断信号的级联如下所示
+---------------------------------------+ |
之前编写裸机gpio中断时,没有关心过gpc,原因是:
节点中断属性
#interrupt-cells= <3>- 指明interrupts属性中的元素个数
- interrupts = <中断类型(0=SPI,1=PPI), 中断号, 标志(中断触发类型,PPI中断的CPU掩码)>
interrupt-controller- 为空,表面当前节点为中断控制器
interrupt-parent- 设置使用的上一级的中断控制器
- 假设父中断控制器是gpio5,说明该设备的中断信号INT线接到gpio5组上,当发送中断,gpio5组直接告知gpc,这组gpio有中断
interrupts- 设置该设备具体走的什么中断信息,联合你上面的中断控制器
- <gpio号 触发方式> = <0 8>, 表面GPIO5_IO00, 8表示低电平触发
- 触发方式的具体宏定义在
include/dt-bindings/interrupt-controller/irq.h
- 触发方式的具体宏定义在
在dts中设置好了我们的中断线连接之后,我们下一步就是要让linux内核的驱动们知道我们的中断pin,肯定是OF函数:irq_of_parse_and_map

如果我们的外设节点,他的中断引脚接到gpio上来上传中断信号,也可以使用gpio_to_irq来获取gpio对应的中断号(因为我们知道接在哪个gpio上,而每组gpio的中断号是固定的)
实验
实验设计:
- 用户态程序(消费者)读取按键值(原子变量),并打印。
- 检查完整计数 是否 == 1
- ==1,校验前面的记录键值,也可以直接返回,完整计数归0
- !=1, 失败读取,返回错误值。
- 检查完整计数 是否 == 1
- key0按键驱动,采用中断双边沿触发的方式,,并用定时器消抖。生产按键检测
- 按下 + 释放 = 一次完整的按键
- 按下:
- 下降沿本身触发中断,此时已经是0了,
- 中断内无需读取,直接开启定时器10ms
- 定时器中断处理函数,再读一次,若还是0,置为00000001
- 释放:
- 上升沿本身触发中断,此时已经是1了
- 中断内无需读取,直接开启定时器10ms
- 定时器中断处理函数,再读一次,若还是1,置为10000001, 完整按键计数 = 1
这里还学习了一下,如何用面向对象的思想,去编写我们的一个设备的驱动,我自己手写了一遍,还是有很多收获的,用对象的思维,去描述,比较有条理。
//通用 |



















