中断的引入
硬件中断是可屏蔽的, 软中断不可屏蔽!
妈妈怎么知道孩子醒了
妈妈怎么知道卧室里小孩醒了?
① 时不时进房间看一下:查询方式
简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
后面的3种方式,都需要“小孩来中断妈妈”:中断她的睡眠、中断她的工作。
实际上,能“中断”妈妈的事情可多了:
① 远处的猫叫:这可以被忽略
② 门铃、小孩哭声:妈妈的应对措施不一样
③ 身体不舒服:那要赶紧休息
④ 有蜘蛛掉下来了:赶紧跑啊,救命
妈妈当前正在看书,被“中断”后她会怎么做?流程如下:
① 妈妈正在看书
② 发生了各种声音
可忽略的远处猫叫
快递员按门铃
卧室中小孩哭了
③ 妈妈怎么办?
a. 先在书中放入书签,合上书 (这其实就是相当于保存现场了)
b. 去处理 (调用中断服务程序)
对于不同的情况,处理方法不同:
对于门铃:开门取快递
对于哭声:照顾小孩
c. 回来继续看书 (恢复现场)
基本概念
/*******************************************************/
/*******************************************************/
什么是异常?
- 正常工作之外的流程都叫异常
- 异常会打断正在执行的工作,并且一般我们希望异常处理完成后继续回来执行原来的工作
- 中断是异常的一种
异常向量表:
- 所有的CPU都有异常向量表,这是CPU设计时就设定好的,是硬件决定的。
- 当异常发生时,CPU会自动动作(PC指针跳转到异常向量处处理异常,有时伴随一些辅助动作)。
- 异常向量表是硬件向软件提供的处理异常的支持。
注意区分中断和中断源:
在Linux中有各种各样的中断:比如GPIO
一个中断类型可能有多个中断源
中断向量:中断的地址的变量;
中断向量表:中断类型号与相应中断源的中断处理程序入口地址之间的连接表;
中断服务程序:中断时所执行的中断代码
- 寄存器是一般是中央处理器CPU的内部组成部分,读写速度和CPU运行速度基本匹配。寄存器是有限存储容量的高速存储部件,可用来存储指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。
- 存储器在CPU外,一般只硬盘、U盘等在切断电源后可以保存资料的设备。
- 内存指的是内存条,由于一半的硬盘读取速度很慢,所以用先将硬盘里面的东西读取到内存条里面,然后在给CPU进行处理,这样是为了加快系统的运行速度。
- CPU计算时,先把数据从硬盘读到内存,然后再把即将要用到的数据读到寄存器。最理想的情况下,CPU要用到的数据都能从寄存器读到,这样读写速度很快,如果寄存器没有要用的数据,就要从内存中甚至从硬盘里读取,这样读取数据的时间比CPU计算的时间还多。
CPU在运行的过程中,也会被各种“异常”打断。这些“异常”有:
① 指令未定义
② 指令、数据访问有问题
③ SWI(软中断)
④ 快中断
⑤ 中断
中断也属于一种“异常”,导致中断发生的情况有很多,比如:
① 按键
② 定时器
③ ADC转换完成
④ UART发送完数据、收到数据
⑤ 等等
这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知CPU。
中断的处理流程
arm对异常(中断)处理过程:
① 初始化:
a. 设置中断源,让它可以产生中断
b. 设置中断控制器(可以屏蔽某个中断,优先级)
c. 设置CPU总开关(使能中断)
② 执行其他程序:正常程序
③ 产生中断:比如按下按键--->中断控制器--->CPU
④ CPU 每执行完一条指令都会检查有无中断/异常产生
⑤ CPU发现有中断/异常产生,开始处理。
对于不同的异常,就会跳去不同的地址执行程序。
这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。(这就和异常向量表联系起来了)
③④⑤都是硬件做的。
⑥ 这些函数做什么事情?
软件做的:
a. 保存现场(各种寄存器)
b. 处理异常(中断):
分辨中断源,再调用不同的处理函数
c. 恢复现场
异常向量表定义的是什么?
×××
定义的就是异常服务程序的跳转指令,因为每个异常向量在异常向量表中只有很少字节的存储空间,所以通常存放跳转指令,使程序跳转到存储器的其他地方,再执行中断处理。这里cpu就可以找异常服务程序
当CPU产生了异常 ,CPU会先判断产生了什么类型的异常 然后就会去往 异常向量表的对应异常类型的地址中 查找接下来要执行的指令 然后跳转到存储该类型异常服务函数的地址 执行异常服务函数
×××
<--这就是ARM的异常向量表图示 左侧为地址(因为ARM是32位所以是4字节对齐)
例如:若发生了普通中断 ,PC指针会去到中断向量表的 IRQ地址处, 然后跳转到 IRQ对应的存储器的某个存放中断服务函数的地址 然后执行中断服务函数
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
这就是异常向量表,每一条指令对应一种异常。
发生复位时,CPU就去 执行第1条指令:b reset。
发生中断时,CPU就去执行“ldr pc, _irq”这条指令。
这些指令存放的位置是固定的,比如对于ARM9芯片中断向量的地址是0x18。
当发生中断时,CPU就强制跳去执行0x18处的代码。
在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU就会执行向量表中的跳转指令,去调用更复杂的函数。
当然,向量表的位置并不总是从0地址开始,很多芯片可以设置某个vector base寄存器,指定向量表在其他位置,比如设置vector base为0x80000000,指定为DDR的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是0,中断是0x18。
关于GIC
注意到异常向量表中和中断相关的中断只有 IRQ和FIQ 但是我们知道在Linux中是有各种各样的类型的中断的,那么CPU是怎么区分各种各样的中断的?
通过GIC。
GIC 是联系外设中断和 CPU 的桥梁,也是各 CPU 之间中断互联的通道(也带有管理功能),它负责检测、管理、分发中断,可以做到:
1、使能或禁止中断;
2、把中断分组到Group0还是Group1(Group0作为安全模式使用连接FIQ ,Group1 作为非安全模式使用,连接IRQ );
3、多核系统中将中断分配到不同处理器上;
4、设置电平触发还是边沿触发方式(不等于外设的触发方式);
5、虚拟化扩展。
ARM CPU 对外的连接只有2 个中断: IRQ和FIQ ,相对应的处理模式分别是一般中断(IRQ )处理模式和快速中断(FIQ )处理模式。所以GIC 最后要把中断汇集成2 条线,与CPU 对接。
相当于是一次中断的产生 需要CPU执行两个中断服务函数:第一次是CPU执行GIC上报的中断的中断服务函数,也就是处理函数。第二次是 GIC的处理函数,去找触发中断的中断源是哪一个 然后再去调用该中断源的中断服务函数
就如上图:
假设GPIO1的中断在GPIO中断中是B号中断,GPIO类型的中断在GIC中是A号中断。既:不管是GPIO1的引脚2中断还是GPIO2的引脚3中断、不管是上升沿还是下降沿触发的中断,在GIC中都是A号中断;随后GIC就会将A号中断上报给CPU,CPU就知道了此时发生了A号中断。
CPU会去执行GIC A号中断的中断服务函数(处理函数) 在处理函数中会去找引发GIC中断A(GPIO中断)的是哪个中断 发现是B号中断 也就是找到对应的irq_dsc数组项 irq_dsc[virq] 上图中是B号中断 也就是找到 irq_dsc[B] 然后会马上执行handle_irq函数 去找到是哪个中断源 然后马上顺序执行irqaction结构体类型的action链表中的每个成员的中断服务函数,如果不是触发中断的中断源的中断服务函数就会马上退出 执行下一个,直到找到对应中断源的中断服务函数 执行该函数。
进程、线程、中断的核心:栈
中断中断,中断谁?
中断当前正在运行的进程、线程。
进程、线程是什么?内核如何切换进程、线程、中断?
要理解这些概念,必须理解栈的作用。
ARM处理器程序运行的过程
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
① 对内存只有读、写指令
② 对于数据的运算是在CPU内部实现
③ 使用RISC指令的CPU复杂度小一点,易于设计
比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:
细看这几个步骤,有些疑问:
① 读a,那么a的值读出来后保存在CPU里面哪里?
② 读b,那么b的值读出来后保存在CPU里面哪里?
③ a+b的结果又保存在哪里?
CPU运行时,先去取得指令,再执行指令(寄存器R15来处理机器指令执行下面的步骤):
① 把内存a的值读入CPU寄存器R0
② 把内存b的值读入CPU寄存器R1
③ 把R0、R1累加,存入R0
④ 把R0的值写入内存a
程序被中断时,怎么保存现场?
从上图可知,CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。
保存在哪里?内存,这块内存就称之为栈。
程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。
(也就是说上下文切换 其实就是切换寄存器的值。)
这个场景并不局限于中断,下图可以概括程序A、B的切换过程,其他情况是类似的:
(上图是程序A被程序B打断的过程,如果程序A被中断打断也是类似的)
a. 函数调用:
在函数A里调用函数B,实际就是中断函数A的执行。
那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;
再去执行函数B;
函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。
b. 中断处理
进程A正在执行,这时候发生了中断。
CPU强制跳到中断异常向量地址去执行,
这时就需要保存进程A被中断瞬间的CPU寄存器值,
可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。
中断处理完毕,要继续运行进程A之前,恢复这些值。
c. 进程切换
在所谓的多任务操作系统中,我们以为多个程序是同时运行的。
如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。
怎么切换?
切换过程是发生在内核态里的,跟中断的处理类似。
进程A的被切换瞬间的CPU寄存器值保存在某个地方;
恢复进程B之前保存的CPU寄存器值,这样就可以运行进程B了。
(所以进程的上下文切换实际上就是切换cpu寄存器中的值)
所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。
进程的调度也是使用栈来保存、恢复现场:
值得注意的地方:
在Linux中:资源分配的单位是进程,调度的单位是线程。
也就是说Linux中线程参与调度,也就是说会涉及保存现场,那么线程就得有自己的栈;也就是线程栈。
(线程栈保存在所属进程的共享区中。(我们在学习进程的时候知道共享区是用来保存 链接的libc库等和线程栈和进程间共享内存等的东西 详情去看写的进程的基本概念的博客))
线程在被调度时(也就是被切换时),此时CPU的寄存器的值会被保存在该线程的线程栈中(保存现场),当下一次切换到该线程时,将线程栈中保存的各寄存器值写入到CPU的寄存器 (这就是恢复现场了)
Linux系统对中断处理的演进
Linux系统中有硬件中断,也有软件中断。(主要学习硬件中断)
Linux内核通过中断号来找到对应的中断服务函数
对硬件中断的处理有2个原则:不能嵌套,越快越好。
Linux对中断的扩展:硬件中断、软件中断
1.
Linux系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
为方便理解,你可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:
注意:上图是简化的,Linux中这个数组复杂多了。
当发生A中断时,对应的irq_function_A函数被调用。硬件导致该函数被调用。
2.
/****************软中断start*****************/
相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:
注意:上图是简化的,Linux中这个数组复杂多了。
问题来了:
a. 软件中断何时生产?
由软件决定,对于X号软件中断,只需要把它的flag设置为1就表示发生了该中断。
b. 软件中断何时处理?
软件中断嘛,并不是那么十万火急,有空再处理它好了。
什么时候有空?不能让它一直等吧?
Linux系统中,各种硬件中断频繁发生,至少定时器中断每10ms发生一次,那取个巧?
在处理完硬件中断后,再去处理软件中断?就这么办!
有哪些软件中断?
查内核源码include/linux/interrupt.h
怎么触发软件中断?最核心的函数是raise_softirq,简单地理解就是设置softirq_veq[nr]的标记位:
怎么设置软件中断的处理函数:
extern void open_softirq(int nr, void (*action) (struct soft_action*));
后面讲到的中断下半部tasklet就是使用软件中断实现的。
/****************软中断end*****************/
中断处理原则1:不能嵌套
官方资料:中断处理不能嵌套
kernel/git/torvalds/linux.git - Linux kernel source tree
中断处理函数需要调用C函数,这就需要用到栈。
中断A正在处理的过程中,假设又发生了中断B,那么在栈里要保存A的现场,然后处理B。
在处理B的过程中又发生了中断C,那么在栈里要保存B的现场,然后处理C。
如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。
所以,为了防止这种情况发生,也是为了简单化中断的处理,在Linux系统上中断无法嵌套:即当前中断A没处理完之前,不会响应另一个中断B(即使它的优先级更高)。
中断处理原则2:越快越好
妈妈在家中照顾小孩时,门铃响起,她开门取快递:这就是中断的处理。她取个快递敢花上半天吗?不怕小孩出意外吗?
同理,在Linux系统中,中断的处理也是越快越好。
在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。
在SMP系统中,假设中断处理很慢,那么正在处理这个中断的CPU上的其他线程也无法执行。
在中断的处理过程中,该CPU是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。
在Linux系统中使用中断是挺简单的,为某个中断irq注册中断处理函数handler,可以使用:
request_irq函数
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
可见request_irq ()其实还是调用了request_pthreaded_irq ()
其实就是不使用线程的 request_pthreaded_irq ()
注意:request_irq和request_threaded_irq的irq参数其实是虚拟中断号
在handler函数中,代码要尽可能高效。
但是,处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在handler中等待吗?对于计算机来说,这可是一个段很长的时间。怎么办?
对于耗时中断的处理办法
1.继续使用requst_irq 但是将中断分为 上半部 和 下半部
2.使用requst_pthreaded_irq 内核线程处理中断
1.上半部、下半部
要处理的事情实在太多,拆分为:上半部、下半部
当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。换句话说,在这段时间内,系统是关中断的。
如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?
在handler函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。
中断下半部的实现有很多种方法,讲2种主要的:a.tasklet(小任务)、b.work queue(工作队列)
1.A. 下半部要做的事情耗时不是太长:tasklet
假设我们把中断分为上半部、下半部。发生中断时,上半部下半部的代码何时、如何被调用?
当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用tasklet来处理下半部。tasklet是使用软件中断来实现。
写字太多,不如贴代码,代码一目了然:
使用流程图简化一下:
假设硬件中断A的上半部函数为irq_top_half_A,下半部为irq_bottom_half_A。
使用情景化的分析,才能理解上述代码的精华。
a. 硬件中断A处理过程中,没有其他中断发生:
一开始,preempt_count = 0;
上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。
b. 硬件中断A处理过程中,又再次发生了中断A:
一开始,preempt_count = 0;
执行到第⑥时,一开中断后,中断A又再次使得CPU跳到中断向量表。
注意:这时preempt_count等于1,并且中断下半部的代码并未执行。
CPU又从①开始再次执行中断A的上半部代码:
在第①步preempt_count等于2;
在第③步preempt_count等于1;
在第④步发现preempt_count等于1,所以直接结束当前第2次中断的处理;
注意:重点来了,第2次中断发生后,打断了第一次中断的第⑦步处理。当第2次中断处理完毕,CPU会继续去执行第⑦步。
可以看到,发生2次硬件中断A时,它的上半部代码执行了2次,但是下半部代码只执行了一次。
所以,同一个中断的上半部、下半部,在执行时是多对一的关系。
c. 硬件中断A处理过程中,又再次发生了中断B:
一开始,preempt_count = 0;
执行到第⑥时,一开中断后,中断B又再次使得CPU跳到中断向量表。
注意:这时preempt_count等于1,并且中断A下半部的代码并未执行。
CPU又从①开始再次执行中断B的上半部代码:
在第①步preempt_count等于2;
在第③步preempt_count等于1;
在第④步发现preempt_count等于1,所以直接结束当前第2次中断的处理;
注意:重点来了,第2次中断发生后,打断了第一次中断A的第⑦步处理。当第2次中断B处理完毕,CPU会继续去执行第⑦步。
在第⑦步里,它会去执行中断A的下半部,也会去执行中断B的下半部。
所以,多个中断的下半部,是汇集在一起处理的。
总结:
a. 中断的处理可以分为上半部,下半部
b. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
c. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
d. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
e. 中断上半部执行完后,触发中断下半部的处理
f. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?
1.B.下半部要做的事情太多并且很复杂:工作队列
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间APP是无法执行的。
假设下半部要执行1、2分钟,在这1、2分钟里APP都是无法响应的。
这谁受得了?
所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行,APP有机会执行,系统不会卡顿。
这个内核线程是系统帮我们创建的,一般是kworker线程,内核中有很多这样的线程:
kworker线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。
那我们怎么使用work、work queue呢?
a. 创建work:
你得先写出一个函数,然后用这个函数填充一个work结构体。比如:
b. 要执行这个函数时,把work提交给work queue就可以了:
上述函数会把work提供给系统默认的work queue:system_wq,它是一个队列。
c. 谁来执行work中的函数?
不用我们管,schedule_work函数不仅仅是把work放入队列,还会把kworker线程唤醒。此线程抢到时间运行时,它就会从队列中取出work,执行里面的函数。
d. 谁把work提交给work queue?
在中断场景中,可以在中断上半部调用schedule_work函数。
总结:
a. 很耗时的中断处理,应该放到线程里去
b. 可以使用work、work queue
c. 在中断上半部调用schedule_work函数,触发work的处理
d. 既然是在线程中运行,那对应的函数可以休眠。
2.新技术:threaded irq
我们前面说过request_irq ( ) 相当于是 request_threaded_irq(irq, handler, NULL, flags, name, dev)
也就是不使用线程的request_threaded_irq 但是我们后面为了处理耗时长的中断服务程序下半部。又去工作队列取出内核线程 (kworker线程)来处理中断下半部,这不是没事找事?不如直接使用request_threaded_irq()算了。
你可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。
注意:request_irq和request_threaded_irq的irq参数其实是虚拟中断号(virq,不过通常直接称为中断号或者软件中断号)
以前用work来线程化地处理中断,一个worker线程只能由一个CPU执行,多个中断的work都由同一个worker线程来处理,在单CPU系统中也只能忍着了。但是在SMP系统中,明明有那么多CPU空着,你偏偏让多个中断挤在这个CPU上?
新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。
也就是说 requst_threaded_irq():1.你可以选择直接将中断服务程序直接全部放到handler参数中(也就是不将中断分成上半部和下半部);2.也可以将中断分成上半部和下半部,上半部正常执行 下半部由新创建的内核线程来执行;3.也可以直接将整个中断服务程序直接全部丢给新创建的内核线程去执行。
Linux中断系统中的重要数据结构
(硬件中断)
引入几个Linux中断子系统下重要数据结构:
1、中断描述符:irq_desc; (desc就是description)
2、中断处理:struct irqaction
3、虚拟中断号virq和硬件中断号hwirq;
4、中断域:irq_domain;
下面内容,可以从request_threaded_irq(include/linux/interrupt.h)函数一路分析得到。
能弄清楚下面这个图,对Linux中断系统的掌握也基本到位了。
最核心的结构体是irq_desc,之前为了易于理解,我们说在Linux内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是irq_desc数组。
注意:如果内核配置了CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替irq_desc数组。SPARSE的意思是“稀疏”,假设大小为1000的数组中只用到2个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。
1. irq_desc数组
每个硬件中断都对应一个中断号对应一个irq_desc数组项!(但是一个中断可能存在多个中断源)
irq_desc结构体在include/linux/irqdesc.h中定义,主要内容如下图:
每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action链表(这个action链表里面保存了多个irqaction结构体 既保存多个中断源的中断服务函数)。
我们知道对于每一个硬件中断都对应一个irq_desc数组项,且irqaction结构体是保存中断源的 name、dev_id等,最重要的是handler(上半部函数)、thread_fn(线程中处理的函数)、thread。
有些问题:
1.为什么需要一个保存irqaction类型结构体的链表?只用一个irqaction类型结构体不可以吗?
有可能有多个中断源共享这个中断,不同的中断源对应不同的name、dev_id等,最重要的是handler(上半部函数)、thread_fn(线程中处理的函数)、thread.
2.irq_desc数组项中的handle_irq和irqaction链表中的handler有什么区别?是一样的吗?
irq_desc数组项中的handle_irq是该中断的入口函数,在该中断触发后就会马上进入这个函数,在这个函数里面做两件事:1、清除该中断的中断标志位 2、调用引起该中断的中断源在该irq_desc数组项中断irqaction链表中的对应节点的handle.(其实就是该中断被触发后马上进入irq_desc数组项中的handle_irq,清除中断标志位 然后顺序执行irqaction链表中的handle如果不是该中断源的handle就马上退出,直到执行到该中断源的handle)
3.我们知道通过irq(virq)我们可以找到对应的 irq_desc[virq] ,但是这里面有一个irqaction结构体链表保存着许多中断源的中断服务函数,那么我们是怎么找到对应的中断源的中断服务函数的呢?
内核只要通过 virq 找到对应的irq_desc数组项 irq_desc[virq] ,然后通过irq_desc[virq]的成员:handle_irq函数 去顺序执行irqaction结构体链表中的每一个节点的handle函数,如果当前节点的handle不是触发中断的中断源的handle 就会马上退出 然后执行下一个节点的handle 再次判断 如果不是就马上退出,直到执行到触发中断的中断源的handle.
那么是怎么判断的呢?
irqaction结构体链表节点中有一个成员 dev_id ,在reqeust_irq时可以传入dev_id,为何需要dev_id?
作用有2:
① 中断处理函数执行时,可以使用dev_id
② 卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项
所以在共享中断中必须提供dev_id,非共享中断可以不提供。
要理解它们,需要先看中断结构图:
ARM CPU 对外的连接只有2 个中断: IRQ和FIQ ,相对应的处理模式分别是一般中断(IRQ )处理模式和快速中断(FIQ )处理模式。所以GIC 最后要把中断汇集成2 条线,与CPU 对接。
外部设备1、外部设备n共享一个GPIO中断B(也就是多个中断源),多个GPIO中断汇聚到GIC(通用中断控制器)的A号中断,GIC再去中断CPU。
那么软件处理时就是反过来,CPU先读取GIC获得中断号A,再细分出GPIO中断B,最后判断是哪一个外部芯片发生了中断!!!
就如上图:
假设GPIO1的中断在GPIO中断中是B号中断,GPIO类型的中断在GIC中是A号中断。既:不管是GPIO1的引脚2中断还是GPIO2的引脚3中断、不管是上升沿还是下降沿触发的中断,在GIC中都是A号中断;随后GIC就会将A号中断上报给CPU,CPU就知道了此时发生了A号中断。
CPU会去执行GIC A号中断的中断服务函数(处理函数) 在处理函数中会去找引发GIC中断A(GPIO中断)的是哪个中断 发现是B号中断 也就是找到对应的irq_dsc数组项 irq_dsc[virq] 上图中是B号中断 也就是找到 irq_dsc[B] 然后会马上执行handle_irq函数 去找到是哪个中断源 然后马上顺序执行irqaction结构体类型的action链表中的每个成员的中断服务函数,如果不是触发中断的中断源的中断服务函数就会马上退出 执行下一个,直到找到对应中断源的中断服务函数 执行该函数。
相当于是一次中断的产生 需要执行两次中断服务函数:第一次是CPU执行GIC上报的中断的中断服务函数,也就是处理函数。第二次是 在GIC的处理函数中 去找触发中断的中断是哪一个 找到对应的irq_dsc[virq] 然后调用该结构体中的handle_irq 去找到是哪个中断源 然后去调用该中断源的中断服务函数
所以,中断的处理函数来源有三:
① GIC的处理函数:
假设irq_desc[A].handle_irq是XXX_gpio_irq_handler(XXX指厂家),这个函数需要读取芯片的GPIO控制器,细分发生的是哪一个GPIO中断(假设是B),再去调用irq_desc[B]. handle_irq。
注意:irq_desc[A].handle_irq细分出中断后B,调用对应的irq_desc[B].handle_irq。
显然中断A是CPU感受到的顶层的中断,GIC中断CPU时,CPU读取GIC状态得到中断A。
② 模块的中断处理函数:
比如对于GPIO模块向GIC发出的中断B,它的处理函数是irq_desc[B].handle_irq。
BSP开发人员会设置对应的处理函数,一般是handle_level_irq或handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。
注意:导致GPIO中断B发生的原因很多,可能是外部设备1,可能是外部设备n,可能只是某一个设备,也可能是多个设备。所以irq_desc[B].handle_irq会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。
③ 外部设备提供的处理函数:
这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”:它知道如何判断设备是否发生了中断,如何处理中断。
对于共享中断,比如GPIO中断B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。
一旦程序确定发生了GPIO中断B,那么就会从链表里把那些函数取出来,一一执行。
这个链表就是action链表。
对于我们举的这个例子来说,irq_desc数组如下:
一定要看这张图!!!!
外部设备1、外部设备n共享一个GPIO中断B,多个GPIO中断汇聚到GIC(通用中断控制器)的A号中断,GIC再去中断CPU。
那么软件处理时就是反过来,先读取GIC获得中断号A,再细分出GPIO中断B,最后判断是哪一个外部芯片发生了中断!!!
我们从上面的描述和图片可以知道,实际上:当一个中断发生了,CPU会首先处理来自于GIC的中断,既执行GIC的中断服务函数 在这个函数里细分到底是什么中断,然后去执行该中断的中断服务函数
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,这就是中断上半部、下半部。
里面还有一个名为sedondary的irqaction结构体,它的作用以后再分析。
在reqeust_irq时可以传入dev_id,为何需要dev_id?作用有2:
① 中断处理函数执行时,可以使用dev_id
② 卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项
所以在共享中断中必须提供dev_id,非共享中断可以不提供。
对应于request_irq()函数中所传递的第五个参数,可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体。 共享中断时所用。
virq(虚拟中断号)和hwriq(硬件中断号)
virq:虚拟中断号。我们通常也把virq称为软件中断号、中断号,或者直接使用irq来表示它。
我们在rquest_threaded_irq中的第一个参数irq指的就是虚拟中断号(软件中断号)。
硬件中断号hwirq是写在芯片手册当中的, 它会明确告诉你这个硬件中断属于哪一个中断控制器管理, 以及哪一个或哪几个GPIO与这个硬件中断是硬件连接的.
中断不仅是软件的概念, 在硬件中断的概念中, 它是实打实的硬件, 我们嵌入式软件中, 驱动层的中断是需要硬件的支撑才可以实现中断的.
何为软件中断号呢? 当硬件中断在被我们软件工程师通过代码所调用时, 我们在代码中不会继续使用硬件中断号(虽然多数情况下硬件中断号和软件中断号是一样的), 这是由Linux系统所决定的, Linux帮我们做了许多操作, 使我们不需要关心硬件中断是如何与我们的软件中断绑定起来的, 我们只需要知道使用这个软件中断号, 如同使用硬件中断号一样!
也就是说:我们通常在驱动代码中使用的中断号irq其实就是虚拟中断号virq,我们在设备树中指定的中断号则是硬件中断号hwirq。这就和我们访问硬件的寄存器时,在驱动程序中是不能直接访问的,必须通过映射ioremap才能访问,在设备树中指定的寄存器才是实打实的硬件的寄存器。
那么既然我们在驱动程序只能使用virq那么我们怎么从设备树中获取hwirq并且将其转换为virq呢?
在使用时, 执行request_irq(virq, handler): 内核根据virq可以知道对应的硬件中断, 然后去设置、使能中断等.
发生硬件中断时: 内核读取硬件信息, 确定hwirq, 反算出virq, 然后调用irq_desc[virq].handle_irq, 最终调用到对应的handler(处理函数).
怎么根据hwirq计算出virq?
硬件上有多个intc(中断控制器), 对于同一个hwirq数值, 会对应不同的virq. 所以在讲hwirq时,应该强调"是哪一个intc的hwirq", 在描述hwirq转换为virq时, 引入一个概念: irq_domain(域), 在这个域里hwirq转换为某一个virq.
使用子中断EINT4的过程:
// step1, 为父中断(intc, 4)设置irq_desc:
找空闲项, virq=4, 保存起来: intc's irq_domain.linear_revmap[4] = 4
设置irq_desc[4].handle_irq = s3c_irq_demux
// step2, 为子中断eint(subintc, 4)设置irq_desc:
找空闲项, virq=5, 保存起来: subintc's irq_domain.linear_revmap[4] = 5
// step3, 驱动程序调用了 request_irq(5, my_handler)
会把my_handler保存到irq_desc[5].action链表中.
// step4, 发生了中断:
内核读取intc, 得到hwirq=4, virq=intc's irq_domain.linear_revmap[4] = 4
调用irq_desc[4].handle_irq, 即s3c_irq_demux
// step5, s3c_irq_demux:
读取subintc, 得到hwirq=4, virq=subintc's irq_domain.linear_revmap[4] = 5
调用irq_desc[5].handle_irq, 它会调用action链表中保存的my_handler
每一个中断控制器都有一个irq_domain结构体;
interrupt-controller中断控制器使用interrupt-controller(空值)来描述自己.
3.irq_data结构体
irq_data结构体在include/linux/irq.h中定义,主要内容如下图:
它就是个中转站,里面有irq_chip指针 irq_domain指针,都是指向别的结构体。
比较有意思的是irq、hwirq,irq是软件中断号(也就是virq虚拟中断号),hwirq是硬件中断号。比如上面我们举的例子,在GPIO中断B是软件中断号,可以找到irq_desc[B]这个数组项;GPIO里的第x号中断,这就是hwirq。
谁来建立irq、hwirq之间的联系呢?由irq_domain来建立。irq_domain会把本地的hwirq映射为全局的irq,什么意思?比如GPIO控制器里有第1号中断,UART模块里也有第1号中断,这两个“第1号中断”是不一样的,它们属于不同的“域”──irq_domain。
4.irq_domain结构体
irq_domain结构体在include/linux/irqdomain.h中定义,主要内容如下图:
当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被转换为irq时,irq_domain将会起到极大的作为。
这里基于入门的解度简单讲讲,在设备树中你会看到这样的属性:
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
它表示要使用gpio1里的第5号中断,hwirq就是5。
但是我们在驱动中会使用request_irq(irq, handler)这样的函数来注册中断,irq是什么?它是软件中断号,它应该从“gpio1的第5号中断”(hwirq)转换得来。
谁把hwirq转换为irq?由gpio1的相关数据结构,就是gpio1对应的irq_domain结构体。
1.irq_domain结构体中有一个irq_domain_ops结构体,里面有各种操作函数,主要是:
① xlate
用来解析设备树的中断属性,提取出hwirq、type等信息。
② map
把hwirq转换为irq。
内核在使用设备树时,会使用map函数来建立virq和hwirq的映射关系
2.irq_domain结构体成员:unsigned int linear_revmap[];
是用来保存virq和hwirq的关系的,内核如果要使用virq,就会通过hwirq在这个数组中找到对应的virq然后就可以使用了
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”,外设备千变万化,内核里可没有对应的清除中断操作。
request_irq和request_threaded_irq详解
当调用request_irq、request_threaded_irq注册中断处理函数时,内核就会构造一个irqaction结构体。在里面保存name、dev_id等,最重要的是handler、thread_fn、thread。
那么问题来了?我们知道irq_desc数组项中的irqaction结构体链表中保存着不同中断源的中断服务函数的。那么调用request_irq、request_threaded_irq是创建一个irq_desc数组项还是如果该中断号对应的irq_desc数组项存在就往里添加irqaction链表节点?
在request_irq、request_threaded_irq时可以传入dev_id,为何需要dev_id?
作用有2:
① 中断处理函数执行时,可以使用dev_id
② 卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项
所以在共享中断中必须提供dev_id,非共享中断可以不提供。
也就是说我们是通过dev_id来区分irqaction结构体链表中的不同中断源的。
对应于request_irq()函数中所传递的第五个参数,可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体。 共享中断时所用。
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname,
void *dev_id)
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)
参数:
1.irq:
要申请的虚拟中断号。
对于不支持设备树的内核:在内核si项目里搜“irqs.h",找对应平台,即可得到。
对于支持设备树的内核:不能直接写硬件中断号,要通过设备树映射中断号
2.handler:
向系统注册的中断处理函数,是回调函数,中断发生时,系统调用这函数,dev_id参数传递给它。 本函数最后要return IRQ_HANDLED;
3.irqflags:
中断处理的属性, //以下老版本意思是linux2.6.24之前
IRQF_DISABLED (老版本中的SA_INTERRUPT)表示中断处理程序是快速处理程序,
快速处理程序被调用时禁止所有中断。慢速处理程序不禁止
IRQF_SHARED (老版本中的SA_SHIRQ)多个设备共享中断,
IRQF_SAMPLE_RANDOM(老版本中的SA_SAMPLE_RANDOM),对系统获取随机数有好处。
...
3.name:
中断名称,通常是设备驱动程序的名称. 在/proc/interrupts 里可以看到。cat /proc/interrupts
4.dev_id:
对应于request_irq()函数中所传递的第五个参数,可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体。 共享中断时所用。也可以当成传参用的。通常传入platform_device的成员dev中的of_node结构体(也就是device_node)来获取设备树的信息
作为中断服务函数(handler)的第二个参数。若中断服务函数用不到更多参数,则设置为NULL。若此项不是NULL,free_irq的第二个参数也要写上对应的值。
返回值
0: 成功
-EINVAL:表示中断号无效或处理函数指针为NULL
-EBUSY:表示中断已经被占用且不能共享
调用request_threaded_irq后内核的数据结构
request_threaded_irq
request_threaded_irq函数,肯定会创建一个内核线程。
源码在内核文件kernel\irq\manage.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)
{
// 分配、设置一个irqaction结构体
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = __setup_irq(irq, desc, action); // 进一步处理
}
__setup_irq函数代码如下(只摘取重要部分):
if (new->thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
setup_irq_thread函数代码如下(只摘取重要部分):
if (!secondary) {
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
} else {
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
new->name);
param.sched_priority -= 1;
}
new->thread = t;
调用request_threaded_irq函数注册中断,调用free_irq卸载中断。
从前面可知,我们可以提供上半部函数,也可以不提供:
① 如果不提供
内核会提供默认的上半部处理函数:irq_default_primary_handler,它是直接返回IRQ_WAKE_THREAD 。会通过这个返回值来唤醒内核线程
② 如果提供的话
返回值必须是:IRQ_WAKE_THREAD。(用来唤醒内核线程)
(如果没提供thread_fn就应该使用IRQ_HANDLED作为返回值)
在thread_fn中,如果中断被正确处理了,thread_fn应该返回IRQ_HANDLED。
如何在设备树中指定中断_在代码中获得中断?
设备树里的中断控制器
中断的硬件框图如下:
在硬件上,“中断控制器”只有GIC这一个,但是我们在软件上也可以把上图中的“GPIO”称为“中断控制器”。很多芯片有多个GPIO模块,比如GPIO1、GPIO2等等。所以软件上的“中断控制器”就有很多个:GIC、GPIO1、GPIO2等等。
GPIO1连接到GIC,GPIO2连接到GIC,所以GPIO1的父亲是GIC,GPIO2的父亲是GIC。
假设GPIO1有32个中断源,但是它把其中的16个汇聚起来向GIC发出一个中断,把另外16个汇聚起来向GIC发出另一个中断。这就意味着GPIO1会用到GIC的两个中断,会涉及GIC里的2个hwirq。
这些层级关系、中断号(hwirq),都会在设备树中有所体现。
1.在设备树中,中断控制器节点中必须有一个属性:interrupt-controller,表明它是“中断控制器”。
2.还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需要多少个cell。
#interrupt-cells的值一般有如下取值:
① #interrupt-cells=<1>
别的节点要使用这个中断控制器时,只需要一个cell来表明使用“哪一个中断”。
② #interrupt-cells=<2>
别的节点要使用这个中断控制器时,需要一个cell来表明使用“哪一个中断”;
还需要另一个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”是谁,用了interrupt-parent”中的哪一个“interrupts”,请看下一小节。
设备树里使用中断
一个外设,它的中断信号接到哪个“中断控制器”的哪个“中断引脚”,这个中断的触发方式是怎样的?
这3个问题,在设备树里使用中断时,都要有所体现。
① interrupt-parent=<&XXXX>
你要用哪一个中断控制器里的中断?
② interrupts
你要用这个中断控制器的哪一个中断?
Interrupts里要用几个cell,由interrupt-parent对应的中断控制器决定。在中断控制器里有“#interrupt-cells”属性,它指明了要用几个cell来描述中断。
比如:
i2c@7000c000 {
gpioext: gpio-adnp@41 {
compatible = "ad,gpio-adnp";
interrupt-parent = <&gpio>;
interrupts = <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,把里面有关中断的部分内容抽取出来。
从设备树反推IMX6ULL的中断体系,如下,比之前的框图多了一个“GPC INTC”:
GPC INTC的英文是:General Power Controller, Interrupt Controller。它提供中断屏蔽、中断状态查询功能,实际上这些功能在GIC里也实现了,个人觉得有点多余。除此之外,它还提供唤醒功能,这才是保留它的原因。
在驱动代码中获得中断(获得中断号)
之前我们提到过,设备树中的节点有些能被转换为内核里的platform_device,有些不能,回顾如下:
A. 根节点下含有compatile属性的子节点,会转换为platform_device
B. 含有特定compatile属性的节点的子节点,会转换为platform_device
如果一个节点的compatile属性,它的值是这4者之一:"simple-bus","simple-mfd","isa","arm,amba-bus",
那么它的子结点(需含compatile属性)也可以转换为platform_device。
C. 总线I2C、SPI节点下的子节点:不转换为platform_device
某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。
1.对于platform_device
一个节点能被转换为platform_device,如果它的设备树里指定了中断属性,那么可以从platform_device中获得“中断资源”,函数如下,可以使用下列函数获得IORESOURCE_IRQ资源,即中断号:
/**
* platform_get_resource - get a resource for a device
* @dev: platform device
* @type: resource type // 取哪类资源?IORESOURCE_MEM、IORESOURCE_REG
* // IORESOURCE_IRQ等
* @num: resource index // 这类资源中的哪一个?
*/
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num);
2.对于I2C设备、SPI设备
对于I2C设备节点,I2C总线驱动在处理设备树里的I2C子节点时,也会处理其中的中断信息。一个I2C设备会被转换为一个i2c_client结构体,中断号会保存在i2c_client的irq成员里,代码如下(drivers/i2c/i2c-core.c):
对于SPI设备节点,SPI总线驱动在处理设备树里的SPI子节点时,也会处理其中的中断信息。一个SPI设备会被转换为一个spi_device结构体,中断号会保存在spi_device的irq成员里,代码如下(drivers/spi/spi.c):
实际上IIC和SPI还是调用了of_irq_get
3.直接调用of_irq_get获得中断号
如果你的设备节点既不能转换为platform_device,它也不是I2C设备,不是SPI设备,那么在驱动程序中可以自行调用of_irq_get函数去解析设备树,得到中断号。
4.对于GPIO
参考:drivers/input/keyboard/gpio_keys.c
因为很多的中断都是来自于GPIO中断所以 内核对于GPIO中断单独提供一些机制
我们甚至不需要在设备树节点添加中断属性 可以直接在驱动程序使用gpio_to_irq或gpiod_to_irq获得中断号。
举例,假设在设备树中有如下节点:
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
user {
label = "User Button";
gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
gpio-key,wakeup;
linux,code = <KEY_1>;
};
};
那么可以使用下面的函数获得引脚和flag:
(1)通过gpio号来获取gpiodescription结构体里面包含gpio的信息
struct gpio_desc *gpio_to_desc(unsigned gpio)
(2)获取gpio号
static inline int of_get_gpio_flags(struct device_node *np, int index,
enum of_gpio_flags *flags)
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);
再去使用gpiod_to_irq获得中断号:
(3)根据gpio号获取中断号
static inline int gpio_to_irq(unsigned int gpio)
irq = gpiod_to_irq(bdata->gpiod);
获得的是虚拟中断号virq
编写使用中断的按键驱动程序
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
struct gpio_key{
int gpio;//GPIO的值
struct gpio_desc *gpiod;/* */
int flag;//flag
int irq;//virq
} ;/* 使用结构体来保存gpio信息和中断信息 */
static struct gpio_key *gpio_keys_100ask;/* 使用动态分配结构体 */
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
int val;
val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
return IRQ_HANDLED;
}
static int gpio_key_probe(struct platform_device *pdev)
{
int i;
int gpio_key_count;
struct device_node *dev_node;
dev_node = pdev->dev.of_node;
enum of_gpio_flags flag;
gpio_key_count = of_gpio_count(dev_node);
/* 根据设备树节点中的gpios中的数量分配结构体数 */
gpio_keys_100ask = kzalloc(sizeof(struct gpio_key)*gpio_key_count, GFP_KERNEL);
for (i = 0; i < gpio_key_count; i++)
{
gpio_keys_100ask[i].gpio = of_get_gpio_flags(dev_node, i, &flag);
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
request_irq(gpio_keys_100ask[i].irq, gpio_key_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]
}
}
static int gpio_key_remove(struct platform_device *pdev)
{
//int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
count = of_gpio_count(node);
for (i = 0; i < count; i++)
{
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.drive = {
.name = ,
.of_match_table = ,
},
};
static int __init gpio_key_init(void)
{
platform_driver_register(gpio_keys_driver);
return 0;
}
static void __exit gpio_key_exit(void)
{
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
代码中注意的地方:
中断服务函数的返回值为:return IRQ_HANDLED;
IRQ_HANDLED means that we did have a valid interrupt and handled it.
* 接收到了准确的中断信号,并且作了相应正确的处理
Linux内核中的等待队列
CPU调度如下图所示:
阻塞和非阻塞
1.阻塞(block)概念:
指进程或线程在执行设备操作或管道或网络操作时,不能获取到资源就被挂起,直到满足可操作的条件后在进行操作,被挂起的进程进入休眠状态,从运行队列移走,直到等待的条件满足才继续执行。也就是执行到某些函数时必须等待某个事件发生函数才返回。
2.非阻塞(non_block):进程就算没有获取到资源或没有等到事件发生时不挂起,通常会直接放弃或不断查询,直到可以进行的位置。也就是函数的执行不必等待事件发生,一旦执行肯定返回,用返回值来反应函数执行情况。
在Linux驱动程序中:1.我们可以使用基于等待队列(wait queue)的休眠唤醒和POLL机制来实现阻塞操作。
2.我们可以使用超时时间为0的POLL机制、异步通知(发信号)来实现阻塞操作
等待队列及其原理
- cpu会调度就绪队列,或者打断执行线程,运行就绪队列
- 创建等待队列头和队列,使用wait event,当condition不满足时,当前线程进入等待队列
- 通过将当前线程加入等待队列中,同时schedule调度(任务调度)走cpu执行别的线程,下次cpu便不会再调度当前线程了
- 当使用wake up后,会将wait线程加入run queue(运行队列)或者就绪队列中,同时:
- 如果condition满足,下次被阻塞的线程会被调度,
- 如果condition不满足,不会执行当前线程,在下次schedule调度(任务调度)当前线程会从run queue(运行队列)或者就绪队列被删除 放回等待队列。
也就是说 1.condition不满足就将线程放入等待队列
2.在使用了wake up后 就会将在等待队列中的线程放在 run queue(运行队列)或者就绪队列中 a.如果此时conditon满足就 当前线程就会被执行
b.如果此时conditon不满足 在下次schedule调度(任务调度)当前线程
会从 run queue(运行队列)或者就绪队列被删除 放回等待队列。
POLL机制休眠唤醒等和中断相关的基本上都是依赖于等待队列的。
等待队列是一种基于资源状态的线程管理的机制,它可以使线程在资源不满足的情况下处于休眠状态,让出CPU资源,而资源状态满足时唤醒线程,使其继续进行业务的处理。等待队列(wait queue)用于使线程等待某一特定的事件发生而无需频繁的轮询,进程在等待期间睡眠,在某件事发生时由内核自动唤醒。它是以双循环链表为基础数据结构,与进程的休眠唤醒机制紧密相联,是实现异步事件通知、跨进程通信、同步资源访问等技术的底层技术支撑。
1.创建等待队列
1.
使用这个宏
DECLARE_WAIT_QUEUE_HEAD(wq);
2.
在Linux内核中,wait_queue_head_t代表一个等待队列,只需要定义一个wait_queue_head_t类型的变量,就表示创建一个等待队列,还需要调用如下接口来初始化此队列:
staitc wait_queue_head_t wq;
init_waitqueue_head(&wq);
具体看一下wait_queue_head_t数据类型:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
就是一个链表和一把自旋锁,链表是用于保存等待该队列的wait_queue_t类型waiter对象(此类型对象内部的private成员保存了当前的任务对象task_struct *),自旋锁是为了保证对链表操作的原子性。这里简单的看一下wait_queue_t数据类型:
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private; // 保存当前任务的task_struct对象地址,为pcb
wait_queue_func_t func; // 用于唤醒被挂起任务的回调函数,该回调函数是将该进程加入run queue,等待cpu调度
struct list_head task_list; // 连接到wait_queue_head_t中的task_list链表
};
2. 让当前进程开始等待
内核提供了如下的接口来让当前进程在条件不满足的情况下,阻塞等待:
wait_event(wq, condition);
wait_event_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition);
wait_event_interruptible_timeout(wq, condition, timeout);
返回值如下:
1) -ERESTARTSYS: 表示被信号激活唤醒
2) > 0: 表示condition满足,返回值表示距离设定超时还有多久
3) = 0: 表示超时发生
其内部实现源码都很类似,只是有些细节不太一样,这里以wait_event_interruptible()为例子,看看其源码:
#define __wait_event_interruptible(wq, condition, ret) \
do { \
// 定义一个waiter对象
DEFINE_WAIT(__wait); \
\
for (;;) { \
// 将waiter对象加入到等待链表中,并设置当前task的状态为TASK_INTERRUPTIBLE
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if (!signal_pending(current)) { \
// 进行任务调度,
schedule(); \
continue; \
} \
ret = -ERESTARTSYS; \
break; \
} \
// 如果condition满足 就将waiter对象从等待链表中删除
finish_wait(&wq, &__wait); \
} while (0)
当我们调用wait_event_interruptible()接口时,会先判断condition是否满足,如果不满足,则会suspend当前task。
这里再看一下DEFINE_WAIT宏的源码,可以发现其private成员总是保存这当前task对象的地址current,还有一个成员func也是非常重要的,保存着task被唤醒前的操作方法,这里暂不说明,待下面的wait_up唤醒等待队列时再进行分析:
#define DEFINE_WAIT(name) \
wait_queue_t name = { \
.private = current, \
.func = autoremove_wake_function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
3. 唤醒此等待队列上的进程:
内核提供了如下的接口:
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
void wake_up_interruptible_all(wait_queue_head_t *q);
这里以分析wake_up_interruptible()函数的源码进行说明唤醒task的原理,因为其他的唤醒过程都是类似的。最后都会调用到__wake_up_common()这个函数:
void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int sync, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
从上面的源码可以看出最终就是调用了等待队列q上的task_list链表上的waiter对象的func方法,在前面又提到过这个方法就是autoremove_wake_function():
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
// 将wait对象private成员保存的task添加到run queue中,便于系统的调度
int ret = default_wake_function(wait, mode, sync, key);
// 将此wait对象从链表中删除
if (ret)
list_del_init(&wait->task_list);
return ret;
}
defailt_wake_function()的源码如下,又看到我们熟悉的private成员
int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
void *key)
{
return try_to_wake_up(curr->private, mode, sync);
}
休眠和唤醒
当应用程序必须等待某个事件发生,比如必须等待按键被按下时,可以使用“休眠-唤醒”机制:
① APP调用read等函数试图读取数据,比如读取按键;
② APP进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
③ 如果APP在内核态,也就是在驱动程序中发现没有数据,则APP休眠;
④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP;
⑤ APP继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。
驱动中有数据时,下图中红线就是APP1的执行过程,涉及用户态、内核态:
驱动中没有数据时,APP1在内核态执行到drv_read时会休眠。所谓休眠就是把自己的状态改为非RUNNING,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒APP1。所以唤醒就是把程序的状态改为RUNNING,这样内核的调度器有合适的时间就会让它运行。当APP1再次运行时,就会继续执行drv_read中剩下的代码,把数据复制回用户空间,返回用户空间。APP1的执行过程如下图的红色实线所示,它被分成了2段:
值得注意的是,上面2个图中红线部分都属于APP1的“上下文”,或者这样说:红线所涉及的代码,都是APP1调用的。但是按键的中断服务程序,不属于APP1的“上下文”,这是突如其来的,当中断发生时,APP1正在休眠呢。
在APP1的“上下文”,也就是在APP1的执行过程中,它是可以休眠的。
在中断的处理过程中,也就是gpio_key_irq的执行过程中,它不能休眠:“中断”怎么能休眠?“中断”休眠了,谁来调度其他APP啊?
所以,请记住:在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数。
1.休眠函数
参考内核源码:include\linux\wait.h。
函数 | 说明 |
wait_event_interruptible(wq, condition) | 休眠,直到condition为真; 休眠期间是可被打断的,可以被信号打断 |
wait_event(wq, condition) | 休眠,直到condition为真; 退出的唯一条件是condition为真,信号也不好使 |
wait_event_interruptible_timeout(wq, condition, timeout) | 休眠,直到condition为真或超时; 休眠期间是可被打断的,可以被信号打断 |
wait_event_timeout(wq, condition, timeout) | 休眠,直到condition为真; 退出的唯一条件是condition为真,信号也不好使 |
比较重要的参数就是:
① wq:waitqueue,等待队列
休眠时除了把程序状态改为非RUNNING之外,还要把进程/进程放入wq中,以后中断服务程序要从wq中把它取出来唤醒。
没有wq的话,茫茫人海中,中断服务程序去哪里找到你?
② condition
这可以是一个变量,也可以是任何表达式。表示“一直等待,直到condition为真”。
第一个参数wq需要使用
2.唤醒函数
参考内核源码:include\linux\wait.h。
函数 | 说明 |
wake_up_interruptible(x) | 唤醒x队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤醒其中的一个线程 |
wake_up_interruptible_nr(x, nr) | 唤醒x队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤醒其中的nr个线程 |
wake_up_interruptible_all(x) | 唤醒x队列中状态为“TASK_INTERRUPTIBLE”的线程,唤醒其中的所有线程 |
wake_up(x) | 唤醒x队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中的一个线程 |
wake_up_nr(x, nr) | 唤醒x队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中nr个线程 |
wake_up_all(x) | 唤醒x队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,唤醒其中的所有线程 |
#define ___wait_event(wq, condition, state, exclusive, ret, cmd) \ ({ \ __label__ __out; \ wait_queue_t __wait; \ long __ret = ret; /* explicit shadow */ \ \ init_wait_entry(&__wait, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \ for (;;) { \ long __int = prepare_to_wait_event(&wq, &__wait, state);\ \ if (condition) \ break; \ \ if (___wait_is_interruptible(state) && __int) { \ __ret = __int; \ goto __out; \ } \ \ cmd; \ } \ finish_wait(&wq, &__wait); \ __out: __ret; \ })
读一下wait_event_interruptible()的源码,不难发现这个函数先将
当前进程的状态设置成TASK_INTERRUPTIBLE,然后调用schedule(),
而schedule()会将位于TASK_INTERRUPTIBLE状态的当前进程从runqueue
队列中删除。从runqueue队列中删除的结果是,当前这个进程将不再参
与调度,除非通过其他函数将这个进程重新放入这个runqueue队列中,
这就是wake_up()的作用了。
由于这一段代码位于一个由condition控制的for(;;)循环中,所以当由
shedule()返回时(当然是被wake_up之后,通过其他进程的schedule()而
再次调度本进程),如果条件condition不满足,本进程将自动再次被设
置为TASK_INTERRUPTIBLE状态,接下来执行schedule()的结果是再次被
从runqueue队列中删除。这时候就需要再次通过wake_up重新添加到
runqueue队列中。
如此反复,直到condition为真的时候被wake_up.
可见,成功地唤醒一个被wait_event_interruptible()的进程,需要满足:
在 1)condition为真的前提下,2) 调用wake_up()。
所以,如果你仅仅修改condition,那么只是满足其中一个条件,这个时候,
被wait_event_interruptible()起来的进程尚未位于runqueue队列中,因
此不会被 schedule。这个时候只要wake_up一下就立刻会重新进入运行调度。
驱动框架
驱动框架如下:
要休眠的线程,放在wq队列里,中断处理函数从wq队列里把它取出来唤醒。
所以,我们要做这几件事:
① 初始化wq队列
② 在驱动的read函数中,调用wait_event_interruptible:
它本身会判断event是否为FALSE,如果为FASLE表示无数据,则休眠。
当从wait_event_interruptible返回后,把数据复制回用户空间。
③ 在中断服务程序里:
设置event为TRUE,并调用wake_up_interruptible唤醒线程。
POLL机制
使用休眠-唤醒的方式等待某个事件发生时,有一个缺点:等待的时间可能很久。我们可以加上一个超时时间,这时就可以使用poll机制:
① APP不知道驱动程序中是否有数据,可以先调用poll函数查询一下,poll函数可以传入超时时间;
② APP进入内核态,调用到驱动程序的poll函数,如果有数据的话立刻返回;
③ 如果发现没有数据时就休眠一段时间;
④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP;
⑤ 当超时时间到了之后,内核也会唤醒APP;
⑥ APP根据poll函数的返回值就可以知道是否有数据,如果有数据就调用read得到数据
函数执行流程如上图①~⑧所示,重点从③开始看。假设一开始无按键数据:
③ APP调用poll之后,进入内核态;
④ 导致驱动程序的drv_poll被调用:(第一次调用_drv_poll)
注意,drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?
drv_poll还会判断一下:有没有数据啊?返回这个状态。
⑤ 假设当前没有数据,则休眠一会;
⑥ 在休眠过程中,按下了按键,发生了中断:
在中断服务程序里记录了按键值,并且从wq中把线程唤醒了。
⑦ 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll:(第二次调用drv_poll)
drv_poll返回数据状态
⑧ 哦,你有数据,那从内核态返回到应用态吧
⑨ APP调用read函数读数据
如果一直没有数据,调用流程也是类似的,重点从③开始看,如下:
③ APP调用poll之后,进入内核态;
④ 导致驱动程序的drv_poll被调用:
注意,drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?
drv_poll还会判断一下:有没有数据啊?返回这个状态。
⑤ 假设当前没有数据,则休眠一会;
⑥ 在休眠过程中,一直没有按下了按键,超时时间到:内核把这个线程唤醒;
⑦ 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll:
drv_poll返回数据状态
⑧ 哦,你还是没有数据,但是超时时间到了,那从内核态返回到应用态吧
⑨ APP不能调用read函数读数据
注意几点:
① drv_poll要把线程挂入等待队列wq,但是并不是在drv_poll中进入休眠,而是在调用drv_poll之后休眠
② drv_poll要返回数据状态
③ APP调用一次poll,有可能会导致drv_poll被调用2次(一次是刚调用drv_poll将线程放在等待队列中,第二次是超时或被唤醒会再次调用drv_poll判断condition)
④ 线程被唤醒的原因有2:
1.中断发生了去队列wq中把它唤醒 2.超时时间到了内核把它唤醒
⑤ APP要判断poll返回的原因:有数据,还是超时。有数据时再去调用read函数。
也就是说poll机制其实就是:休眠一段时间,满足条件或者超时就直接返回。
POLL驱动编程
使用poll机制时,驱动程序的核心就是提供对应的drv_poll函数。
在drv_poll函数中只要做2件事:
① 把当前线程挂入队列wq:poll_wait
APP调用一次poll,可能导致drv_poll被调用2次,但是我们并不需要把当前线程挂入队列2次。
可以使用内核的函数poll_wait把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
② 返回设备状态:
APP调用poll函数时,有可能是查询“有没有数据可以读”:POLLIN,也有可能是查询“你有没有空间给我写数据”:POLLOUT。
所以drv_poll要返回自己的当前状态:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
POLLRDNORM等同于POLLIN,为了兼容某些APP把它们一起返回。
POLLWRNORM等同于POLLOUT ,为了兼容某些APP把它们一起返回。
APP调用poll后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。
驱动程序中poll的代码如下:
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
POLL应用编程
注意:APP可以调用poll或select函数,这2个函数的作用是一样的。
poll的函数原型如下:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数介绍:
pollfd *fds : 指向pollfd结构体数组,用于存放需要检测器状态的Socket 描述符或其它文件描述符。
unsigned int nfds: 指定pollfd 结构体数组的个数,即监控几个pollfd.
timeout:指poll() 函数调用阻塞的时间,单位是ms.如果timeout=0则不阻塞,如timeout=INFTIM 表 示一直阻塞直到感兴趣的事情发生。
返回值:
>0 表示数组fds 中准备好读,写或出错状态的那些socket描述符的总数量
==0 表示数组fds 中都没有准备好读写或出错,当poll 阻塞超时timeout 就会返回。
-1 表示poll() 函数调用失败,同时回自动设置全局变量errno.
pollfd结构体数组:
struct pollfd{
int fd; /*文件描述符,如建立socket后获取的fd, 此处表示想查询的文件描述符*/
short events; /*等待的事件,就是要监测的感兴趣的事情*/
short revents; /*实际发生了的事情*/
};
poll/select函数可以监测多个文件,可以监测多种事件:
事件类型 | 说明 |
POLLIN | 有数据可读 |
POLLRDNORM | 等同于POLLIN |
POLLRDBAND | Priority band data can be read,有优先级较较高的“band data”可读 Linux系统中很少使用这个事件 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 可以写数据 |
POLLWRNORM | 等同于POLLOUT |
POLLWRBAND | Priority data may be written |
POLLERR | 发生了错误 |
POLLHUP | 挂起 |
POLLNVAL | 无效的请求,一般是fd未open |
在调用poll函数时,要指明:
① 你要监测哪一个文件:哪一个fd
② 你想监测这个文件的哪种事件:是POLLIN、还是POLLOUT
最后,在poll函数返回时,要判断状态。
应用程序代码如下:
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
fds[0].fd = fd;
fds[0].events = POLLIN;
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
先判断poll的返回值 知道它是超时返回还是准备好才返回!
异步通知(async)
使用休眠-唤醒、POLL机制时,都需要休眠等待某个事件发生时,它们的差别在于后者可以指定休眠的时长。
如果APP不想休眠怎么办?也有类似的方法:驱动程序有数据时主动通知APP,APP收到信号后执行信息处理函数。
什么叫“异步通知”?
你去买奶茶:
你在旁边等着,眼睛盯着店员,生怕别人插队,他一做好你就知道:你是主动等待他做好,这叫“同步”。
你付钱后就去玩手机了,店员做好后他会打电话告诉你:你是被动获得结果,这叫“异步”。
驱动程序怎么通知APP:发信号,这只有3个字,却可以引发很多问题:
① 谁发:驱动程序发
② 发什么:信号
③ 发什么信号:SIGIO
④ 怎么发:内核里提供有函数
⑤ 发给谁:APP,APP要把自己告诉驱动
⑥ APP收到后做什么:执行信号处理函数
⑦ 信号处理函数和信号,之间怎么挂钩:APP注册信号处理函数
Linux系统中也有很多信号,在Linux内核源文件include\uapi\asm-generic\signal.h中,有很多信号的宏定义:
就APP而言,你想处理SIGIO信息,那么需要提供信号处理函数,并且要跟SIGIO挂钩。这可以通过一个signal函数来“给某个信号注册处理函数”,用法如下:
这和我们之前在学习进程间通信的时候学习的信号是差不多的,但是我们现在是需要驱动发信号到应用程序,需要将应用程序的pid传给驱动程序。
APP还要做什么事?
想想这几个问题:
① 内核里有那么多驱动,你想让哪一个驱动给你发SIGIO信号?
APP要打开驱动程序的设备节点。
② 驱动程序怎么知道要发信号给你而不是别人?
APP要把自己的进程ID(pid)告诉驱动程序。
③ APP有时候想收到信号,有时候又不想收到信号:
应该可以把APP的意愿告诉驱动。
驱动程序要做什么?
发信号。
① APP设置进程ID时,驱动程序要记录下进程ID;
② APP还要使能驱动程序的异步通知功能,驱动中有对应的函数:
APP打开驱动程序时,内核会创建对应的file结构体,file中有f_flags;
f_flags中有一个FASYNC位(file async),它被设置为1时表示使能异步通知功能。
当f_flags中的FASYNC位发生变化时,驱动程序的fasync函数被调用。
③ 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。
这个辅助函数名为kill_fasync。
综上所述,使用异步通知,也就是使用信号的流程如下图所示:
重点从②开始:
② 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函数读取按键。
驱动编程
使用异步通知时,驱动程序的核心有2:
① 提供对应的drv_fasync函数(赋值给file_operations中的fasync成员);
② 并在合适的时机发信号。
drv_fasync函数很简单,调用fasync_helper函数就可以,如下:
static struct fasync_struct *button_async;
static int drv_fasync (int fd, struct file *filp, int on)
{
return fasync_helper (fd, filp, on, &button_async);
}
fasync_helper函数会分配、构造一个fasync_struct结构体button_async:
① 驱动文件的flag被设置为FAYNC时:
button_async->fa_file = filp; // filp表示驱动程序文件,里面含有之前设置的PID
② 驱动文件被设置为非FASYNC时:
button_async->fa_file = NULL;
以后想发送信号时,使用button_async作为参数就可以,它里面“可能”含有PID。
什么时候发信号呢?在本例中,在GPIO中断服务程序中发信号。
怎么发信号呢?代码如下:
kill_fasync (&button_async, SIGIO, POLL_IN);
第1个参数:button_async->fa_file非空时,可以从中得到PID,表示发给哪一个APP;
第2个参数表示发什么信号:SIGIO;
第3个参数表示为什么发信号:POLL_IN,有数据可以读了。(APP用不到这个参数)
应用编程
应用程序要做的事情有这几件:
① 编写信号处理函数:
static void sig_func(int sig)
{
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
② 注册信号处理函数:
signal(SIGIO, sig_func);
③ 打开驱动:
fd = open(argv[1], O_RDWR);
④ 把进程ID告诉驱动:
fcntl(fd, F_SETOWN, getpid());
⑤ 使能驱动的FASYNC功能:
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
程序例子:
/* * test.c * */
static int fd;
static void sig_func(int sig)
{
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv)
{
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
int flags;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
signal(SIGIO, sig_func);
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
fcntl(fd, F_SETOWN, getpid());
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
while (1)
{
printf("www.100ask.net \n");
sleep(2);
}
close(fd);
return 0;
}
/* drv.c */
/*下面是和异步通知有关的部分*/
struct fasync_struct *button_fasync;
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}
/* 定义自己的file_operations结构体 */
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN); //条件满足发送信号
return IRQ_HANDLED;
}
其实核心就是:驱动程序在满足条件时发送信号到应用程序,让应用程序执行信号处理函数。
再论阻塞和非阻塞
1.阻塞(block)概念:
指进程或线程在执行设备操作或管道或网络操作时,不能获取到资源就被挂起,直到满足可操作的条件后在进行操作,被挂起的进程进入休眠状态,从运行队列移走,直到等待的条件满足才继续执行。也就是执行到某些函数时必须等待某个事件发生函数才返回。
2.非阻塞(non_block):进程就算没有获取到资源或没有等到事件发生时不挂起,通常会直接放弃或不断查询,直到可以进行的位置。也就是函数的执行不必等待事件发生,一旦执行肯定返回,用返回值来反应函数执行情况。
我们在学习文件IO时 就学习过阻塞和非阻塞的概念:
open("xxx", O_RDWR|O_NONBLOCK);
我们在打开文件时传入O_NONBLOCK 就可以表明打开方式为非阻塞,对该文件执行的操作不管是否成功一定会返回。
APP调用open函数时,传入O_NONBLOCK,就表示要使用非阻塞方式;默认是阻塞方式。
注意:对于普通文件、块设备文件,O_NONBLOCK不起作用。
注意:对于字符设备文件,O_NONBLOCK起作用的前提是驱动程序针对O_NONBLOCK做了处理。
只能在open时表明O_NONBLOCK吗?在open之后,也可以通过fcntl修改为阻塞或非阻塞。
应用编程
open时设置:
int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式 */
int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式 */
open之后设置:
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式 */
驱动编程
以drv_read为例:
static ssize_t drv_read(struct file *fp, char __user *buf, size_t count, loff_t *ppos)
{
if (queue_empty(&as->queue) && fp->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(apm_waitqueue, !queue_empty(&as->queue));
……
}
从驱动代码也可以看出来,当APP打开某个驱动时,在内核中会有一个struct file结构体对应这个驱动,这个结构体中有f_flags,就是打开文件时的标记位;可以设置f_flasgs的O_NONBLOCK位,表示非阻塞;也可以清除这个位表示阻塞。
驱动程序要根据这个标记位决定事件未就绪时是休眠和还是立刻返回。
驱动开发原则
驱动程序程序“只提供功能,不提供策略”。就是说驱动程序可以提供休眠唤醒、查询等等各种方式,,驱动程序只提供这些能力,怎么用由APP决定。
也就是说 别人可以不用,但是不能没有
定时器
内核函数
所谓定时器,就是闹钟,时间到后你就要做某些事。有2个要素:时间、做事,换成程序员的话就是:超时时间、函数。
在内核中使用定时器很简单,涉及这些函数(参考内核源码include\linux\timer.h):
① setup_timer(timer, fn, data):
设置定时器,主要是初始化timer_list结构体,设置其中的函数、参数。
② 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):
删除定时器。
定时器时间单位
编译内核时,可以在内核源码根目录下用“ls -a”看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:
CONFIG_HZ=100
这表示内核每秒中会发生100次系统滴答中断(tick),这就像人类的心跳一样,这是Linux系统的心跳。每发生一次tick中断,全局变量jiffies就会累加1。
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秒
工作队列
mmap
MMU的两个主要功能是:
1. 将虚地址转换成物理地址。2. 控制存储器存取允许。MMU关掉时,虚地址直接输出到物理地址总线。
应用程序和驱动程序之间传递数据时,可以通过read、write函数进行。这涉及在用户态buffer和内核态buffer之间传数据,如下图所示:
应用程序不能直接读写驱动程序中的buffer,需要在用户态buffer和内核态buffer之间进行一次数据拷贝。这种方式在数据量比较小时没什么问题;但是数据量比较大时效率就太低了。比如更新LCD显示时,如果每次都让APP传递一帧数据给内核,假设LCD采用1024*600*32bpp的格式,一帧数据就有1024*600*32/8=2.3MB左右,这样数据传输就要耗费很长时间 这无法忍受。
改进的方法就是让程序可以直接读写驱动程序中的buffer,这可以通过mmap实现(memory map),把内核的buffer映射到用户态,让APP在用户态直接读写。
复习一下虚拟地址的概念:CPU发出的地址是虚拟地址,它经过MMU(Memory Manage Unit,内存管理单元)映射到物理地址上,对于不同进程的同一个虚拟地址,MMU会把它们映射到不同的物理地址。如下图:
当前运行的是app1时,MMU会把CPU发出的虚拟地址addr映射为物理地址paddr1,用paddr1去访问内存。
当前运行的是app2时,MMU会把CPU发出的虚拟地址addr映射为物理地址paddr2,用paddr2去访问内存。
MMU负责把虚拟地址映射为物理地址,虚拟地址映射到哪个物理地址去?
可以执行ps命令查看进程ID,然后执行“cat /proc/325/maps”得到映射关系。
我们之前在学习进程的时候知道 每一个APP(进程)在内核里都有一个tast_struct,这个结构体中保存有内存信息:mm_struct。而虚拟地址、物理地址的映射关系保存在页目录表中,如下图所示:
解析如下:
① 每个APP在内核中都有一个task_struct结构体,它用来描述一个进程;
② 每个APP都要占据内存,在task_struct中用mm_struct来管理进程占用的内存;
内存有虚拟地址、物理地址,mm_struct中用mmap来描述虚拟地址,用pgd来描述对应的物理地址。
注意:pgd,Page Global Directory,页目录。
③ 每个APP都有一系列的VMA:virtual memory(虚拟地址空间)
比如APP含有代码段、数据段、BSS段、栈等等,还有共享库。这些单元会保存在内存里,它们的地址空间不同,权限不同(代码段是只读的可运行的、数据段可读可写),内核用一系列的vm_area_struct来描述它们。
vm_area_struct中的vm_start、vm_end是虚拟地址。
④ vm_area_struct中虚拟地址如何映射到物理地址去?
每一个APP的虚拟地址可能相同,物理地址不相同,这些对应关系保存在pgd中。
cache和buffer
本小节参考:
ARM的cache和写缓冲器(write buffer)
使用mmap时,需要有cache、buffer的知识。下图是CPU和内存之间的关系,有cache、buffer(写缓冲器)。Cache是一块高速内存;写缓冲器相当于一个FIFO,可以把多个写操作集合起来一次写入内存。
程序运行时有“局部性原理”,这又分为时间局部性、空间局部性。
① 时间局部性:
在某个时间点访问了存储器的特定位置,很可能在一小段时间里,会反复地访问这个位置。
② 空间局部性:
访问了存储器的特定位置,很可能在不久的将来访问它附近的位置。
而CPU的速度非常快,内存的速度相对来说很慢。CPU要读写比较慢的内存时,怎样可以加快速度?
根据“局部性原理”,可以引入cache。
① 读取内存addr处的数据时:
先看看cache中有没有addr的数据,如果有就直接从cache里返回数据:这被称为cache命中。
如果cache中没有addr的数据,则从内存里把数据读入,注意:它不是仅仅读入一个数据,而是读入一行数据(cache line)。
而CPU很可能会再次用到这个addr的数据,或是会用到它附近的数据,这时就可以快速地从cache中获得数据。
② 写数据:
CPU要写数据时,可以直接写内存,这很慢;也可以先把数据写入cache,这很快。
但是cache中的数据终究是要写入内存的啊,这有2种写策略:
a. 写通(write through):
数据要同时写入cache和内存,所以cache和内存中的数据保持一致,但是它的效率很低。能改进吗?可以!使用“写缓冲器”:cache大哥,你把数据给我就可以了,我来慢慢写,保证帮你写完。
有些写缓冲器有“写合并”的功能,比如CPU执行了4条写指令:写第0、1、2、3个字节,每次写1字节;写缓冲器会把这4个写操作合并成一个写操作:写word。对于内存来说,这没什么差别,但是对于硬件寄存器,这就有可能导致问题。
所以对于寄存器操作,不会启动buffer功能;对于内存操作,比如LCD的显存,可以启用buffer功能。
b. 写回(write back):
新数据只是写入cache,不会立刻写入内存,cache和内存中的数据并不一致。
新数据写入cache时,这一行cache被标为“脏”(dirty);当cache不够用时,才需要把脏的数据写入内存。
使用写回功能,可以大幅提高效率。但是要注意cache和内存中的数据很可能不一致。这在很多时间要小心处理:比如CPU产生了新数据,DMA把数据从内存搬到网卡,这时候就要CPU执行命令先把新数据从cache刷到内存。反过来也是一样的,DMA从网卡得过了新数据存在内存里,CPU读数据之前先把cache中的数据丢弃。
使用cache写数据时,数据是不会马上写入到寄存器的,所以对于
当然我们也不是非要使用cache和buffer得看情况
是否使用cache、是否使用buffer,就有4种组合(Linux内核文件arch\arm\include\asm\pgtable-2level.h):
上面4种组合对应下表中的各项,一一对应(下表来自s3c2410芯片手册,高架构的cache、buffer更复杂,但是这些基础知识没变):
是否启用cache | 是否启用buffer | 说明 |
0 | 0 | Non-cached, non-buffered (NCNB) 读、写都直达外设硬件 |
0 | 1 | Non-cached buffered (NCB) 读、写都直达外设硬件; 写操作通过buffer实现,CPU不等待写操作完成,CPU会马上执行下一条指令 |
1 | 0 | Cached, write-through mode (WT),写通 读:cache hit时从cahce读数据;cache miss时已入一行数据到cache; 写:通过buffer实现,CPU不等待写操作完成,CPU会马上执行下一条指令 |
1 | 1 | Cached, write-back mode (WB),写回 读:cache hit时从cahce读数据;cache miss时已入一行数据到cache; 写:通过buffer实现,cache hit时新数据不会到达硬件,而是在cahce中被标为“脏”;cache miss时,通过buffer写入硬件,CPU不等待写操作完成,CPU会马上执行下一条指令 |
cache和buffer的使用场景
/**********************************************************************************************************/
1.第1种是不使用cache也不使用buffer,读写时都直达硬件,这适合寄存器的读写。
2.第2种是不使用cache但是使用buffer,写数据时会用buffer进行优化,可能会有“写合并”,这适合显存的操作。因为对显存很少有读操作,基本都是写操作,而写操作即使被“合并”也没有关系。
3.第3种是使用cache不使用buffer,就是“write through”,适用于只读设备:在读数据时用cache加速,基本不需要写。
4.第4种是既使用cache又使用buffer,适合一般的内存读写。
/**********************************************************************************************************/
mmap编程
驱动程序要做的事
驱动程序要做的事情有3点:
① 确定物理地址
② 确定属性:是否使用cache、buffer
③ 建立映射关系
mmap应用编程
和open read这些的套路一样,我们需要在驱动自己实现drv_mmap让上层使用。
我们先来看看应用程序的mmap函数接口:
mmap函数的函数原型,如下:
参数说明:
1. addr:映射区的开始地址,设置为NULL时表示由系统决定映射区的起始地址。
2.length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理
3.prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
4.flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联,简写为:MAP_ANON。
5.fd:有效的文件描述词。
6.offset:被映射对象内容的偏移量(4K的整数倍)。返回值说明:
成功执行时,mmap()返回被映射区的指针,返回MAP_FAILED(其值为(void *)-1)。
mmap驱动编程
驱动程序要做什么?
① 分配一块8K的内存
使用哪一个函数分配内存?
函数名 | 说明 |
kmalloc | 分配到的内存物理地址是连续的 |
kzalloc | 分配到的内存物理地址是连续的,内容清0 |
vmalloc | 分配到的内存物理地址不保证是连续的 |
vzalloc | 分配到的内存物理地址不保证是连续的,内容清0 |
我们应该使用kmalloc或kzalloc,这样得到的内存物理地址是连续的,在mmap时后APP才可以使用同一个基地址去访问这块内存。(如果物理地址不连续,就要执行多次mmap了)。
② 提供mmap函数
赋值给file_operations结构体的mmap成员让应用程序调用
关键在于mmap函数,代码如下:
常用函数:
virt_to_phys ( ) ——虚拟地址转换成物理地址