中断和阻塞/非阻塞IO的实现
- 1. 中断
- 1.1 概念
- 1.1.1 中断源的概念
- 1.2 设备树中描述中断信息
- 1.3 中断实例---以按键为例
- 1.3.1 在设备树中添加结点
- 1.3.2 驱动实现
- 1.4 使用gpio子系统实现中断处理
- 1.4.1 旧的gpio方式
- 1.4.2 新的gpio方式
- 2. 阻塞IO和非阻塞IO的实现
- 2.1 阻塞IO的实现
- 2.2 非阻塞IO的实现
博客阅读地图:
1. 认识中断模型与上下半部2. 在设备树中描述中断资源3. 基于 GPIO 的按键中断驱动(旧/新 GPIO API)4. 阻塞与非阻塞 read()的内核实现5. poll /select/ epoll 的多路复用支持6. 关键调试方法、常见踩坑与进阶补充(threaded IRQ、下半部处理策略等)阻塞IO和非阻塞IO:
中断下半部:
| 部分 | 触发时机 | 典型动作 | 时延要求 | 常用 API |
|---|---|---|---|---|
| 上半部 (Top Half) | 中断到来立刻执行 | 快速响应、清除中断源、收集最小数据 | 极短 | request_irq()注册的 handler |
| 下半部 (Bottom Half) | 延迟执行 | 大量处理、与用户空间交互 | 允许延迟 | Softirq、Tasklet、Workqueue、Threaded IRQ |
中断下半部:
| 机制 | 触发方式 | 适用场景 | API |
|---|---|---|---|
| Softirq | 静态分配 | 网络、块设备 | open_softirq() |
| Tasklet | Softirq 的封装 | 简易延迟执行 | tasklet_init()/tasklet_schedule() |
| Workqueue | 内核线程执行 | 允许睡眠操作 | INIT_WORK()/schedule_work() |
| Threaded IRQ | request_threaded_irq() | 需要在中断上下文睡眠 | handler为快速函数,threadfn为线程 |
上半部禁止阻塞和耗时操作;复杂逻辑下沉至下半部或专用内核线程。
多路复用poll
1. 中断
为什么需要中断?
轮询缺陷:CPU 主动查询硬件状态,空转浪费。
中断机制:外设在事件发生时主动向 CPU 发出信号,请求立即处理。
软/硬中断:
- 硬件中断(不可预见):GPIO、电源管理、外设 DMA 等。
- 软件中断(计划内):系统调用、raise() 等借助特殊指令触发。
1.1 概念
首先需要了解一下中断的概念:为了提高 CPU 和 外围硬件(硬盘,键盘,鼠标等等) 之间协同工作的性能,引入了中断的机制。
没有中断的话,CPU 和 外围设备之间协同工作可能只有轮询这个方法:CPU定期检查硬件状态,需要处理时就处理,否则就跳过。
当硬件忙碌的时候,CPU很可能会做许多无用功(每次轮询都是跳过不处理)。中断机制是硬件在需要的时候向CPU发出信号,CPU暂时停止正在进行的工作,来处理硬件请求的一种机制。
一个 “中断” 仅仅是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。内核维护了一个中断信号线的注册表,该注册表类似于I/O端口的注册表。
在处理器中,所谓中断,是一个过程,即 CPU 在正常执行程序的过程中,遇到外部/内部的紧急事件需要处理,暂时中断(中止)当前程序的执行,而转去为事件服务,待服务完毕,再返回到暂停处(断点)继续执行原来的程序。为事件服务的程序称为中断服务程序或中断处理程序。严格地说,上面的描述是针对硬件事件引起的中断而言的。用软件方法也可以引起中断,即事先在程序中安排特殊的指令,CPU 执行到该类指令时,转去执行相应的一段预先安排好的程序,然后再返回来执行原来的程序,这可称为软中断。把软中断考虑进去,可给中断再下一个定义:中断是一个过程,是 CPU 在执行当前程序的过程中因硬件或软件的原因插入了另一段程序运行的过程。因硬件原因引起的中断过程的出现是不可预测的,即随机的,而软中断是事先安排的。
1.1.1 中断源的概念
仔细研究一下生活中的中断,对于理解中断的概念也很有好处。什么可以引起中断,生活中很多事件可以引起中断:
有人按门铃了,电话铃响了,你的闹钟响了,你烧的水开了……诸如此类的事件。我们把可以引起中断的信号源称为中断源。硬件上任何可以触发中断信号的模块都可以视为中断源,例如:键盘、GPIO、定时器、DMA 控制器等。内核为中断控制器建立注册表,用于分发并管理这些中断源。
1.2 设备树中描述中断信息
1》当某个设备要使用中断时,需要在设备树中描述中断信息:
设备的中断源是哪个(物理中断号),中断发送给哪个中断控制器 所以在设备树节点中需要描述两个属性: interrupts ------ //表示设备要使用哪个中断,中断的触发方法等 interrupt-parent ---- //这个中断要挂在哪个设备上?中断控制器是谁 或 interrupts-extended=<&gpiof9IRQ_TYPE_EDGE_BOTH>;//参数1-中断控制器,参数2-中断号,参数3-触发方式2》上述两个属性的值如何确定
interrupts ----用多少个u32位的整数表示应该由它的中断控制器结点来描述 在中断控制器中,至少有两个属性: interrupt-controller;//表示当前节点是个中断控制器#interrupt-cells //表示子设备节点中应该有几个u32位整数来描述中断信息最终在设备树中描述的中断信息被内核转换为platform_device中的中断资源,从中可以获取虚拟中断号。
3》设备树中中断信息处理
中断控制器分为:root controller, gpio irq controller root controller ---设备树和内核中芯片公司已做好 gpio irq controller 设备树描述信息 ------pinctrl节点中 驱动中 -------内核通过pinctl子系统来处理 具体的设备中断 设备树文件中的描述 ------该设备中断是哪个中断控制器的哪个中断,以及中断触发方式 在驱动中 ------从platform_device中获取对应的中断资源,进而获取中断号1.3 中断实例—以按键为例
1.3.1 在设备树中添加结点
key1_test1{compatible="stm32mp157,key1_test1";interrupts=<9,IRQ_TYPE_EDGE_BOTH>;interrupt-parent=<&gpiof>;key1-name="key01";//设备节点名称key1-minor=<2>;//次设备号};1.3.2 驱动实现
1》申请中断
intrequest_irq(unsignedintirq,irq_handler_thandler,unsignedlongflags,constchar*name,void*dev)//参数1 --中断号(虚拟中断号)//参数2 -- 中断处理函数:typedef irqreturn_t (*irq_handler_t)(int, void *);//参数3 --- 触发方式:#defineIRQF_TRIGGER_RISING0x00000001#defineIRQF_TRIGGER_FALLING0x00000002#defineIRQF_TRIGGER_HIGH0x00000004#defineIRQF_TRIGGER_LOW0x00000008//参数4 ---- 描述信息,自定义//参数5 ---- 传给中断处理函数的参数//返回值 ---- 成功:0,失败:错误码例如: ret=request_irq(key_dev->irqno,key_irq_fun,IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,"gpiof_7/key1",NULL);if(ret){printk("request_irq error");gotoerr_misc_deregister;}2》实现中断处理函数
irqreturn_txxx_irq_handler(int,void*){//处理中断的过程}例如:irqreturn_tkey_irq_fun(intirqno,void*dev_id){intvalue;printk("-----------^_^ %s-------------\n",__FUNCTION__);returnIRQ_HANDLED;}3》释放中断
constvoid*free_irq(unsignedintirq,void*dev_id)//参数1 ----中断号//参数2 ----- 必须和request_irq的最后一个参数保持一致1.4 使用gpio子系统实现中断处理
1.4.1 旧的gpio方式
1》在设备树中添加结点
key_test2{compatible="stm32mp157,key_test2";dev-name="key01";//设备结点名称minor=<7>;gpio=<&gpiof7GPIO_ACTIVE_HIGH>;};2》在驱动中
//获取key1对应的gpio引脚编号key_dev->gpiono=of_get_gpio(key_dev->np,0);//获取key1对应的虚拟中断号key_dev->irqno=gpio_to_irq(key_dev->gpiono);//申请中断ret=request_irq(key_dev->irqno,key_irq_fun,IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,"key1",NULL);if(ret){printk("request_irq error");gotoerr_misc_deregister;}//实现中断处理函数irqreturn_tkey_irq_fun(intirqno,void*dev_id){intvalue;printk("-----------^_^ %s-------------\n",__FUNCTION__);//获取gpio引脚状态value=gpio_get_value(key_dev->gpiono);//判断key按下还是松开if(value){//松开printk("released key1\n");}else{//按下printk("pressed key1\n");}returnIRQ_HANDLED;}1.4.2 新的gpio方式
1》在设备树文件中定义结点
key_test3{compatible="stm32mp157,key_test3";dev-name="key03";//设备结点名称minor=<7>;key-gpios=<&gpiof7GPIO_ACTIVE_HIGH>;};2》实现驱动
//获取key1对应的gpio引脚编号key_dev->gpiof_7=devm_gpiod_get_index(&pdev->dev,"key",0,GPIOD_OUT_HIGH);//获取key1对应的虚拟中断号key_dev->irqno=gpiod_to_irq(key_dev->gpiof_7);//申请中断ret=request_irq(key_dev->irqno,key_irq_fun,IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,"key1",NULL);if(ret){printk("request_irq error");gotoerr_misc_deregister;}//实现中断处理函数irqreturn_tkey_irq_fun(intirqno,void*dev_id){intvalue;printk("-----------^_^ %s-------------\n",__FUNCTION__);//获取gpio引脚状态value=gpiod_get_value(key_dev->gpiof_7);//判断key按下还是松开if(value){//松开printk("released key1\n");}else{//按下printk("pressed key1\n");}returnIRQ_HANDLED;}2. 阻塞IO和非阻塞IO的实现
2.1 阻塞IO的实现
在linux应用编程中,有许多函数,比如:read(),accept(),connet(),recv(),recvfrom()默认都是阻塞IO函数,在内核中需要实现这些阻塞IO函数
//先定义返回的数据包structstm32mp157_key_data{intcode;//键值intvalue;//松开 value=0,按下 value = 1};1》创建并初始化等待队列头
init_waitqueue_head(structwait_queue_head*wq_head)2》实现阻塞IO函数 -----实现read接口
//根据条件变量condition,决定是否让当前进程挂起wait_event_interruptible(structwait_queue_headwq_head,intcondition)//参数1 ----等待队列头//参数2 ---- 挂起的条件,condition=0,则挂起,否则,直接返回。例如:ssize_tkey_drv_read(structfile*filp,char__user*buf,size_tsize,loff_t*flags){intret;printk("--------^_^ %s---------\n",__FUNCTION__);//如果have_data为0,则进程阻塞,返回直接返回wait_event_interruptible(key_dev->wq_head,key_dev->have_data);//将内核数据转为应用数据ret=copy_to_user(buf,&key_dev->key_data,size);if(ret>0){printk("copy_to_user error");return-EINVAL;}//have_data置零key_dev->have_data=0;returnsize;}3》唤醒阻塞的进程 — 中断处理函数中
wake_up_interruptible(structwait_queue_head*x)例如:irqreturn_tkey_irq_fun(intirqno,void*dev_data){intvalue;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;}key_dev->have_data=1;//唤醒阻塞的进程wake_up_interruptible(&key_dev->wq_head);returnIRQ_HANDLED;}2.2 非阻塞IO的实现
应用层: 打开文件:fd=open("/dev/key01",O_RDWR);//默认以阻塞IO的方式读取数据打开文件:fd=open("/dev/key01",O_RDWR|O_NONBLOCK);//默认以非阻塞IO的方式读取数据read();//上面两种不同的打开方式,使得read在读数据时,如果没有数据,阻塞的方式打开,则read会使进程阻塞,非阻塞的方式打开,则read会立即返回驱动:ssize_tkey_drv_read(structfile*filp,char__user*buf,size_tsize,loff_t*flags){intret;printk("--------^_^ %s---------\n",__FUNCTION__);//判断应用层打开文件时,是否以非阻塞方式读取IOif((key_dev->have_data==0)&&(filp->f_flags&O_NONBLOCK))return-EAGAIN;//如果have_data为0,则进程阻塞,返回直接返回wait_event_interruptible(key_dev->wq_head,key_dev->have_data);//将内核数据转为应用数据ret=copy_to_user(buf,&key_dev->key_data,size);if(ret>0){printk("copy_to_user error");return-EINVAL;}//have_data置零key_dev->have_data=0;returnsize;}综上。本篇内容包含:
- 中断机制论述 —— 为什么需要中断、上下半部的职责。
- 设备树描述 —— interrupts / interrupts-extended、中断控制器属性配置。
- 驱动实现 —— 获取 GPIO、映射虚拟 IRQ、request_irq/devm_request_irq、中断处理流程。
- 阻塞/非阻塞 I/O —— wait_event_interruptible、wake_up_interruptible、O_NONBLOCK 逻辑。
- 多路复用支持 —— .poll 实现及应用层示例。
- 中断下半部进阶 —— Threaded IRQ、Workqueue、调试与常见问题解决。
本文覆盖了从设备树到驱动、从阻塞/非阻塞读到 poll/select 的完整链路,同时给出了关键 API 的使用示例以及实际开发中不可忽视的同步、资源管理与调试技巧,可直接作为 Linux 中断驱动实战的操作手册。