所学来自百问网
目录
1. 嵌入式中断系统
2. 中断处理流程
3. 异常向量表
4. Linux系统对中断的处理
4.1 ARM 处理器程序运行的过程
4.2 保护现场
5. Linux 系统对中断处理的演进
5.1 硬件中断和软件中断
5.2 中断拆分(上半部和下半部)
5.2.1 tasklet
5.2.2 工作队列
5.2.3 新技术 threaded irq
6. Linux中断系统中的重要数据结构
6.1 irq_desc 数组
6.2 irqaction 结构体
6.3 irq_data 结构体
6.4 irq_domain 结构体
6.5 irq_chip 结构体
7. 设备树中的中断语法
8. 驱动程序基石
8.1 休眠函数
8.2 唤醒函数
8.3 休眠与唤醒
8.4 poll机制
8.5 异步通知
8.6 阻塞与非阻塞
1. 嵌入式中断系统
CPU 在运行的过程中,也会被各种“异常”打断。这些“异常”有:
1.指令未定义
2.指令、数据访问有问题
3.SWI(软中断)
4.快中断
5.中断
中断也属于一种“异常”,导致中断发生的称为“中断源“,这些“中断源”很多,比如:按键、UART发送完数据、收到数据等等
这些众多的“中断源”,汇集到“中断控制器“,由“中断控制器”选择优先级最高的中断并通知CPU。
2. 中断处理流程
1.初始化:
a) 设置中断源,让它可以产生中断
b) 设置中断控制器(可以屏蔽某个中断,优先级)
c) 设置CPU总开关(使能中断)
2.执行其他程序:正常程序
3.产生中断:比如按下按键--->中断控制器--->CPU
4.CPU 每执行完一条指令都会检查有无中断/异常产生
5.CPU发现有中断/异常产生,开始处理。
对于不同的异常,跳去不同的地址执行程序。
这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。
6.函数执行
a) 保存现场(各种寄存器)
b) 处理异常(中断):分辨中断源,再调用不同的处理函数
c) 恢复现场
其中345是硬件负责实现,6是软件负责实现
3. 异常向量表
u-boot 或是Linux内核,都有类似如下的代码:
_start: b reset // 产生复位时执行ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq //发生中断时,CPU跳到这个地址执行该指令 **假设地址为0x18** ldr pc, _fiq
这就是异常向量表,每一条指令对应一种异常。
这些指令存放的位置是固定的,比如对于ARM9芯片中断向量的地址是0x18。当发生中断时,CPU就强制跳去执行0x18处的代码。
在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU就会执行向量表中的跳转指令,去调用更复杂的函数。
当然,向量表的位置并不总是从0地址开始,很多芯片可以设置某个vector base寄存器,指定向量表在其他位置,比如设置vector base为0x80000000, 指定为DDR的某个地址。但是表中的各个异常向量的偏移地址,是固定的,如复位向量偏移地址是0,中断是0x18。
4. Linux系统对中断的处理
4.1 ARM 处理器程序运行的过程
ARM 芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
1.对内存只有读、写指令
2.对于数据的运算是在CPU内部实现
3.使用RISC指令的CPU复杂度小一点,易于设计
对于a = a + b的处理流程如下:
CPU 运行时,先去取得指令,再执行指令:
1.把内存a的值读入CPU寄存器R0
2.把内存b的值读入CPU寄存器R1
3.把R0、R1累加,存入R0
4.把R0的值写入内存a
由此可知:CPU处理数据是需要经过寄存器,寄存器可以去保存数据
4.2 保护现场
从上图可知,CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。
这些寄存器的值会被保存在内存中,也就是栈,程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。
这个场景并不局限于中断,下图可以概括程序A、B的切换过程,其他情况是类似的:
函数调用:
a) 在函数A里调用函数B,实际就是中断函数A的执行。
b) 那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;
c) 再去执行函数B;
d) 函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。
中断处理:
a) 进程A正在执行,这时候发生了中断。
b) CPU强制跳到中断异常向量地址去执行。
c) 这时就需要保存进程A被中断瞬间的CPU寄存器值。
d) 可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。
e) 中断处理完毕,要继续运行进程A之前,恢复这些值。
进程切换:
a) 在所谓的多任务操作系统中,我们以为多个程序是同时运行的。
b) 如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。
c) 切换过程是发生在内核态里的,跟中断的处理类似。
d) 进程A的被切换瞬间的CPU寄存器值保存在某个地方;
e) 恢复进程B之前保存的CPU寄存器值,这样就可以运行进程B了
所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。进程的调度也是使用栈来保存、恢复现场:
5. Linux 系统对中断处理的演进
Linux 系统中有硬件中断,也有软件中断。对硬件中断的处理有2个原则: 不能嵌套,越快越好。
5.1 硬件中断和软件中断
Linux 系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为 “硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、 网卡中断的处理函数肯定不一样。
为方便理解,你可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:
当发生A中断时,对应的irq_function_A函数被调用。硬件导致该函数被调用。相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:
注意:以上两图是简化的
对于硬件中断:CPU会优先处理硬件中断事件,在硬件中断中有两条原则,不能嵌套和越快越好
1.不能嵌套
中断A正在处理的过程中,假设又发生了中断B,那么在栈里要保存A的现场,然后处理B,在处理B的过程中又发生了中断C,那么在栈里要保存B的现场,然后处理C。以此类推,如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。
为了防止这种情况,在Linux系统上中断无法嵌套:即当前中断A没处理完之前,不会响应另一个中断B(即使它的优先级更高)。
2.越快越好
在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。
在SMP系统中,假设中断处理很慢,那么正在处理这个中断的CPU上的其他线程也无法执行。
在中断的处理过程中,该CPU是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。
在Linux 系统中使用中断是挺简单的,为某个中断irq注册中断处理函数 handler,可以使用request_irq函数:
对于软件中断:中断由软件决定,对于X号软件中断,只需要把它的flag设置为1就表示发生了该中断。在处理完硬件中断后,再去处理软件中断
在include/linux/interrupt.h中,有以下软件中断:
enum
{HI_SOFTIRQ=0, //高优先级软中断TIMER_SOFTIRQ, //定时器软中断NET_TX_SOFTIRQ, //网络发送软中断NET_RX_SOFTIRQ, //网络接收软中断BLOCK_SOFTIRQ, //块设备软中断IRQ_POLL_SOFTIRQ, //IRQ轮询软中断TASKLET_SOFTIRQ, //tasklet软中断SCHED_SOFTIRQ, //调度软中断HRTIMER_SOFTIRQ, //高精度定时器软中断 RCU_SOFTIRQ, //RCU(Read-Copy Update)软中断,RCU是一种用于同步对共享数据的读取和更新的技术NR_SOFTIRQS //软中断类型的总数
};
5.2 中断拆分(上半部和下半部)
对于硬件中断来说,秉承着中断不能嵌套和越快越好的原则,虽然解决了爆栈的问题,但是处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待几十毫秒消除机械抖动。非紧急的情况就没必要在handler函数里处理,由此将中断拆分为中断上半部和中断下半部
对于中断下半部实现的方式很多,以下主要对:tasklet(小任务)、work queue(工作队列)进行分析
5.2.1 tasklet
tasklet适合用于比较耗时但是能忍受,并且处理比较简单的程序
如下:
使用流程图简化一下:
preempt_count表示中断数
以下使用情景化分析:
-
硬件中断A处理过程中,没有其他中断发生
按流程从上到下执行一遍
-
硬件中断A处理过程中,又再次发生了中断A
程序会先正常执行到第六步,这时preempt_count = 1,当中断打开后,程序会从头执行,preempt_count 加1减1,执行到第四步,这时preempt_count = 1,程序直接完成处理后,处理完中断,程序继续执行中断下半部直至结束
由此可知:同一个中断的上半部、下半部,在执行时是多对一的关系。
-
硬件中断A处理过程中,又再次发生了中断B
程序A会先正常执行到第六步,这时preempt_count = 1,当中断打开后,程序B执行,preempt_count 加1减1,执行到第四步,这时preempt_count = 1,程序B直接完成处理后(这里指的是处理完程序B的上半部),处理完中断,程序会继续执行A、B中断下半部直至结束
由此可知:多个中断的下半部,是汇集在一起处理的
总结:
1.中断的处理可以分为上半部,下半部
2.中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
3.中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
4.中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
5.中断上半部执行完后,触发中断下半部的处理
6.中断上半部、下半部的执行过程中,不能休眠
5.2.2 工作队列
工作队列适用于下半部要做的事情太多并且很复杂十分耗时的程序,这是在内核线程中执行程序,这个内核线程是系统帮我们创建的,一般是kworker线程
使用步骤
1.创建work
你得先写出一个函数,然后用这个函数填充一个work结构体。比如:
2.把work提交给work_queue
上述函数会把work提供给系统默认的work_queue:system_wq,它是一个队列。在中断场景中,可以在中断上半部调用schedule_work函数。schedule_work 函数不仅仅是把 work 放入队列,还会把 kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。
总结:
1.很耗时的中断处理,应该放到线程里去,对应的函数可以休眠
2.在中断上半部调用schedule_work函数,触发work的处理
5.2.3 新技术 threaded irq
你可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。
新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。
6. Linux中断系统中的重要数据结构
6.1 irq_desc 数组
irq_desc 结构体在include/linux/irqdesc.h 中定义,主要内容如下图:
每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action 链表。要理解它们,需要先看中断结构图:
外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC(通用中断控制器)的A号中断,GIC再去中断CPU。那么软件处理时就是反过来,先读取GIC获得中断号A,再细分出GPIO中断B,最后判断是哪一个外部芯片发生了中断。
所以,中断的处理函数来源有三:
1.GIC的处理函数:
假设irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家), 这个函数需要读取芯片的GPIO控制器,细分发生的是哪一个GPIO中断(假设是 B),再去调用irq_desc[B]. handle_irq。
显然中断A是CPU感受到的顶层的中断,GIC中断CPU时,CPU读取GIC状态得到中断A。
注意:irq_desc[A].handle_irq 细分出中断后B,调用对应的 irq_desc[B].handle_irq。
2.模块的中断处理函数:
比如对于GPIO模块向 GIC 发出的中断 B,它的处理函数是 irq_desc[B].handle_irq。
BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或 handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。
注意:导致GPIO中断B发生的原因很多,可能是外部设备1,可能是外部设备 n,可能只是某一个设备,也可能是多个设备。所以irq_desc[B].handle_irq 会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。
3.外部设备提供的处理函数:
这里说的“外部设备”可能是芯片,也可能是简单的按键。它们的处理函数由自己驱动程序提供,它知道如何判断设备是否发生了中断,如何处理中断。
对于共享中断,比如GPIO中断B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。 一旦程序确定发生了GPIO 中断B,那么就会从链表里把那些函数取出来, 一一执行。这个链表就是action链表。
6.2 irqaction 结构体
irqaction 结构体在 include/linux/interrupt.h 中定义,主要内容如下图:
当调用request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个irqaction 结构体。在里面保存name、dev_id 等,最重要的是handler、thread_fn、thread。
handler 是中断处理的上半部函数,用来处理紧急的事情。
thread_fn 对应一个内核线程thread,当 handler执行完毕,Linux内核会唤醒对应的内核线程。在内核线程里,会调用thread_fn函数。
-
可以提供handler而不提供thread_fn,就退化为一般的request_irq函数。
-
可以不提供handler只提供thread_fn,完全由内核线程来处理中断。
-
也可以既提供handler也提供thread_fn,这就是中断上半部、下半部。
在reqeust_irq 时可以传入dev_id,主要作用如下:
1.中断处理函数执行时,可以使用dev_id
2.卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项
所以在共享中断中必须提供dev_id,非共享中断可以不提供。
6.3 irq_data 结构体
irq_data 结构体在include/linux/irq.h 中定义,主要内容如下图:
它就是个中转站,里面有irq_chip指针 irq_domain指针,都是指向别的结构体。 其中irq、hwirq,irq 是软件中断号,hwirq 是硬件中断号。 比如上面我们举的例子,在GPIO中断B是软件中断号,可以找到irq_desc[B] 这个数组项;GPIO里的第x号中断,这就是hwirq。
irq_domain会把本地的hwirq映射为全局的irq,比如GPIO控制器里有第1号中断,UART 模块里也有第1号中断,这两个“第1号中断”是不一样的,它们属于不同的“域”──irq_domain。
6.4 irq_domain 结构体
irq_domain 结构体在include/linux/irqdomain.h 中定义,主要内容如下图:
irq_domain
结构体代表了一个中断域(IRQ domain),它是一个或多个中断控制器的逻辑分组。每个中断域都可以管理一组特定的中断号,并且这些中断号通常是连续分配的。中断域还负责将硬件中断线(hardware interrupt lines)映射到内核使用的中断号上。
在设备树中你会看到这样的属性:
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
它表示要使用gpio1里的第5号中断,hwirq就是5。
我们在驱动中会使用request_irq(irq, handler)这样的函数来注册中断,irq是软件中断号,gpio1中的irq_domain将硬件中断转化为软件中断号
irq_domain 结构体中有一个irq_domain_ops结构体,里面有各种操作函数,
主要是:
-
xlate: 用来解析设备树的中断属性,提取出hwirq、type等信息。
-
map: 把hwirq转换为irq。
6.5 irq_chip 结构体
irq_chip 结构体在include/linux/irq.h 中定义,主要内容如下图:
这个结构体跟“chip”即芯片相关,里面各成员的作用在头文件中也列得很清楚,摘录部分如下:
* @irq_startup: start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
我们在request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的irq_chip里的函数帮我们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用irq_chip中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。
就像上面图里的“外部设备1“、“外部设备n”,外设备千变万化,内核里可没有对应的清除中断操作。
7. 设备树中的中断语法
中断的硬件框图如下:
在硬件上,“中断控制器”只有GIC这一个,而在软件层面,可以将GPIO等其他模块称为“中断控制器”,当将汇聚的起来的中断源向GIC发出中断,一个中断就涉及到GIC的一个hwirq(硬件中断号)
在设备树中,中断控制器节点中必须有一个属性:interrupt-controller, 表明它是“中断控制器”,还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需要多少个cell。当#interrupt-cells=<1>时,表示只需要一个cell来表明使用“哪一个中断“,当#interrupt-cells=<2>时,表示使用两个cell描述中断,一个表示使用”哪一个中断“,另一个则表示”中断触发类型“。
触发类型有以下:
第2个cell的bits[3:0] 用来表示中断触发类型(trigger type and level flags):
1 = low-to-high edge triggered,上升沿触发
2 = high-to-low edge triggered,下降沿触发
4 = active high level-sensitive,高电平触发
8 = active low level-sensitive,低电平触发
设备树中设置中断示例如下:
vic: intc@10140000 { compatible = "arm,versatile-vic"; interrupt-controller; // 中断控制器#interrupt-cells = <1>; // 表示”哪一个中断“reg = <0x10140000 0x1000>;
};
当中断控制器有级联关系,下级中断控制器需要设置“interrupt-parent”和“interrupts”
“interrupt-parent”表示定义中断父节点,在可以产生中断的设备节点中,其中断信号连接到了哪个中断控制器
“interrupts”表示用于描述一个设备的一个或多个中断源,这个属性的值包含了中断号、触发方式(如边缘触发或电平触发)等信息
注意:Interrupts里要用几个cell,由interrupt-parent对应的中断控制器决定。在中断控制器里有“#interrupt-cells”属性,它指明了要用几个cell 来描述中断。
示例:
i2c@7000c000 { gpioext: gpio-adnp@41 { compatible = "ad,gpio-adnp"; interrupt-parent = <&gpio>; interrupts = <160 1>; // 表明中断号为160,使用1号触发方式gpio-controller; #gpio-cells = <1>; interrupt-controller; #interrupt-cells = <2>; };
};
interrupts-extended
一个“interrupts-extended”属性就可以既指定“interrupt-parent”, 也指定“interrupts”,比如:
interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;
以100ASK_IMX6ULL开发板为例,在arch/arm/boot/dts目录下可以看到2个文件:imx6ull.dtsi、100ask_imx6ull-14x14.dts,把里面有关中断的部分内容抽取出来。
8. 驱动程序基石
8.1 休眠函数
在Linux内核中,wait_event
和wait_event_interruptible
及其带有超时参数的版本wait_event_interruptible_timeout
和wait_event_timeout
是用于进程同步和等待特定条件成立的宏。这些宏通常用于等待某个条件变为真,同时允许内核线程或进程在条件不满足时进入睡眠状态。
-
使当前进程或线程在指定的等待队列
wq
上等待,直到condition
变为真。在等待期间,进程会被置于不可中断的睡眠状态。-
void wait_event(wait_queue_head_t *wq, int condition)
-
wq
:指向等待队列的指针,该队列用于管理等待该条件的所有进程。 -
condition
:一个表达式,用于检查是否满足唤醒条件。这个条件在每次唤醒时都会重新检查。
-
-
与
wait_event
类似,但进程在睡眠期间可以被信号中断。如果进程被信号中断,则返回-EINTR
-
int wait_event_interruptible(wait_queue_head_t *wq, int condition)
-
wq
:指向等待队列的指针,该队列用于管理等待该条件的所有进程。 -
condition
:一个表达式,用于检查是否满足唤醒条件。这个条件在每次唤醒时都会重新检查。
-
-
在指定的
timeout
时间内等待condition
变为真。-
long wait_event_interruptible_timeout(wait_queue_head_t *wq, int condition, long timeout)
-
wq
:指向等待队列的指针。 -
condition
:条件表达式。 -
timeout
:等待的最长时间,以jiffies为单位。 -
返回值:如果条件在超时前变为真,则返回剩余的超时时间,如果进程被信号中断,则返回
-EINTR
。
-
-
与
wait_event_interruptible_timeout
类似,但等待期间不能被信号中断-
long wait_event_timeout(wait_queue_head_t *wq, int condition, long timeout)
-
wq
:指向等待队列的指针。 -
condition
:条件表达式。 -
timeout
:等待的最长时间,以jiffies为单位。 -
返回值:如果条件在超时前变为真,则返回剩余的超时时间;如果超时发生且条件仍未满足,则返回0。
-
8.2 唤醒函数
在Linux内核中,wake_up_interruptible
、wake_up_interruptible_nr
、wake_up_interruptible_all
、wake_up
、wake_up_nr
和wake_up_all
是用于唤醒等待队列中进程或线程的宏。这些宏用于在特定条件得到满足时,唤醒那些在该等待队列上等待该条件的进程或线程。
-
唤醒在给定等待队列
x
上等待且处于可中断睡眠状态的所有进程。-
wake_up_interruptible(x)
-
x
是指向等待队列的指针。
-
-
与
wake_up_interruptible
类似,但它尝试唤醒指定数量的(nr
)在给定等待队列x
上等待且处于可中断睡眠状态的进程。-
wake_up_interruptible_nr(x, nr)
-
x
是指向等待队列的指针。 -
nr
是希望唤醒的进程数量。
-
-
在给定等待队列
x
上等待且处于可中断睡眠状态的所有进程都被唤醒。-
wake_up_interruptible_all(x)
-
x
是指向等待队列的指针。
-
-
唤醒在给定等待队列
x
上等待的所有进程,无论它们是否处于可中断睡眠状态。这包括那些处于不可中断睡眠状态的进程。-
wake_up(x)
-
x
是指向等待队列的指针。
-
-
尝试唤醒在给定等待队列
x
上等待的指定数量(nr
)的进程。与wake_up_interruptible_nr
不同,这个宏不区分进程是否处于可中断睡眠状态。-
wake_up_nr(x, nr)
-
x
是指向等待队列的指针。 -
nr
是希望唤醒的进程数量。
-
-
确保在给定等待队列
x
上等待的所有进程都被唤醒,无论它们是否处于可中断睡眠状态。这个宏用于在条件满足时唤醒所有等待的进程。-
wake_up_all(x)
-
x
是指向等待队列的指针。
-
8.3 休眠与唤醒
图解:当APP调用read去读取驱动数据的时候,没有数据则休眠,有数据则将数据记录并唤醒APP
驱动框架
要休眠的线程,放在 wq 队列里,中断处理函数从 wq 队列里把它取出来唤醒。
步骤:
1.初始化wq队列
2.在驱动的read函数中,调用wait_event_interruptible:
它本身会判断event是否为FALSE,如果为FASLE表示无数据,则休眠。
当从wait_event_interruptible 返回后,把数据复制回用户空间。
3.在中断服务程序里: 设置event为TRUE,并调用wake_up_interruptible唤醒线程。
8.4 poll机制
使用休眠-唤醒的方式等待某个事件发生时,有一个缺点:等待的时间可能很久。我们可以加上一个超时时间,这时就可以使用poll机制。
图解:APP不知道驱动程序中是否有数据,可以先调用poll函数查询一下,poll函数可以传入超时时间;当没有数据时则会休眠,若休眠事件超过超时事件则唤醒,当有数据时则会记录数据返回
注意:
1.drv_poll 要把线程挂入队列 wq,但是并不是在 drv_poll 中进入休眠,而是在调用drv_poll之后休眠
2.drv_poll要返回数据状态,判断是获取到数据还是超时,有数据时再去调用read 函数。
3.APP调用一次poll,有可能会导致drv_poll被调用2次
4.线程被唤醒的原因有2:中断发生了或超时时间到了
编程
使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll 函数。在 drv_poll 函数中要做2件事:
1.把当前线程挂入队列wq:poll_wait
这边使用内核函数poll_wait把线程挂入队列可防止drv_poll被调用2次
2.返回设备状态:
APP 调用poll函数时,有可能是查询“有没有数据可以读”:POLLIN,也有可能是查询“你有没有空间给我写数据”:POLLOUT。所以drv_poll要返回自己的当前状态:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
a) POLLRDNORM等同于POLLIN,为了兼容某些APP把它们一起返回。 b) POLLWRNORM等同于POLLOUT ,为了兼容某些APP把它们一起返 回。
APP 调用 poll 后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。
3.APP可以调用poll或select函数,这2个函数的作用是一样的。
poll/select 函数可以监测多个文件,可以监测多种事件:
事件类型 | 说明 |
---|---|
POLLIN | 有数据可读 |
POLLRDNORM | 等同于POLLIN |
POLLPRI | 高优先级数据可读 |
POLLOUT | 可以写数据 |
POLLWRNORM | 等同于POLLOUT |
POLLERR | 发生了错误 |
POLLHUP | 挂起 |
POLLNVAL | 无效的请求,一般是fd未open |
8.5 异步通知
② APP给SIGIO这个信号注册信号处理函数func,以后APP收到SIGIO信号时,这个函数会被自动调用;
③ 把APP的PID(进程ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录PID;
④ 读取驱动程序文件Flag;
⑤ 设置Flag里面的FASYNC位为1:当FASYNC位发生变化时,会导致驱动程序的fasync被调用;
⑥⑦ 调用faync_helper,它会根据 FAYSNC 的值决定是否设置 button_async->fa_file = 驱动文件 filp:驱动文件filp结构体里面含有之前设置的PID。
⑧ APP可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用 kill_fasync 发信号;
⑪⑫⑬ APP收到信号后,它的信号处理函数被自动调用,可以在里面调用 read 函数读取按键。
涉及函数
-
用于向所有通过
fp
指向的fasync_struct
链表注册了异步通知的进程发送信号sig
-
void kill_fasync(struct fasync_struct **fp, int sig, int band)
-
struct fasync_struct **fp
:这是一个指向指针的指针,指向fasync_struct
结构体的链表头部。fasync_struct
结构体用于在内核中维护一个或多个进程对特定文件描述符的异步通知请求。 -
int sig
:这是要发送给所有注册了异步通知的进程的信号编号。如SIGIO或
SIGURG -
int band
:具体结合上下文
-
-
用于启用或禁用对指定文件描述符
fd
的异步通知-
int fasync_helper(int fd, struct file *filp, int on, struct fasync_struct **fapp)
-
int fd
:这是要启用或禁用异步通知的文件描述符。 -
struct file *filp
:这是与fd
关联的文件对象。它包含了文件的状态和操作函数等信息。 -
int on
:这是一个控制标志。如果on
非零,则启用异步通知;如果on
为零,则禁用异步通知。 -
struct fasync_struct **fapp
:这是一个指向指针的指针,指向fasync_struct
结构体链表的头部(或驱动内部维护的类似结构)。这个函数将使用这个指针来更新链表,以添加或删除对fd
的异步通知请求。
-
8.6 阻塞与非阻塞
所谓阻塞,就是等待某件事情发生。比如调用read读取按键时,如果没有按键数据则read函数不会返回,它会让线程休眠等待。
使用poll时,如果传入的超时时间不为0,这种访问方法也是阻塞的。
使用poll时,可以设置超时时间为0,这样即使没有数据它也会立刻返回, 这就是非阻塞方式。
APP 调用open函数时,传入O_NONBLOCK,就表示要使用非阻塞方式;默认是阻塞方式。
注意:
对于普通文件、块设备文件,O_NONBLOCK不起作用。
对于字符设备文件,O_NONBLOCK 起作用的前提是驱动程序针对 O_NONBLOCK 做了处理。