这边文章主要是讲解一下,对ARM_Linux中断处理的一个流程介绍,在底层处理部分,不同的架构会有不一样处理,但Linux中断注册过程是一样的。
一、Linux中断的简易模型:
当一个IRQ产生时,会发生什么?
当一个IRQ产生后,CPU会首先自动跳转到IRQ的中断向量(这个中断向量是可设置的),在这个IRQ中断向量里又是一个跳转指令,CPU再次跳转,跳转后的代码主要完成的工作是保存一些寄存器,然后读取中断寄存器经过计算(这个计算并不是单纯的跟INTPND对应)得到中断号,然后跳转到一个中断处理的通用函数,并把中断号传过去(汇编向C传参数)。
在这个通用处理函数里,根据中断号找到我们自己设定的中断处理函数,然后执行。
我们注意到,最后一步linux会去找我们自己设定的中断处理函数,这个函数是我们事先通过注册来挂接在一个结构数组(这个数组里的元素个数即为中断号的个数)上,结构数组的简易图如图一,详细图如图二,linux会去遍历这个数组(如果使用到了共享中断,即一个中断号上注册多个中断服务程序,则每个中断服务程序都会被执行到)。
图二、irq_desc结构数组简易图
图三、irq_desc结构数组详细图
可以进一步归纳为,linux的中断机制可以分为两部分:
- 定义的中断处理函数是如何注册到linux系统的;
- 当中断发生时,linux如何自动跳转并找出中断号,然后根据中断号来找到注册在该中断号上的中断处理函数并执行;
接下来将从两个方面来详细说明这个机制,
第一个方面是,自己定义的中断处理函数是如何注册到系统中断去的(必须要进行注册,linux才能找到并执行);
另一个方面是,中断发生时,linux具体会做些什么进而找到我们注册的函数并执行。
二、Linux中断注册过程:
设备驱动的中断注册一般在probe函数中实现,以TP为例:
TP注册成功后,通过串口可以知道得到的中断号为 irq=298
在Linux内核中用于申请中断的函数是request_irq(),函数原型在Kernel/irq/manage.c中定义,其源码如下:
irq:要申请的硬件中断号;
handler:向系统注册的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它;
flags:中断处理的属性,若设置了IRQF_DISABLED ,则表示中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置了IRQF_SHARED,则表示多个设备共享中断,若设置了IRQF_SAMPLE_RANDOM,表示对系统熵有贡献,对系统获取随机数有好处。(这几个flag是可以通过或的方式同时使用的)
name:设置中断名称,通常是设备驱动程序的名称 在cat /proc/interrupts中可以看到此名称。
dev:在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。
另一种更容易理解的说法:
irq:就是中断号,什么意思,就是说你要放在irq_desc[]这个数组的哪里,换句话就是申请哪个中断号,
irq_handler_t handler:这个就是中断处理函数句柄,flags中断标志位是用来决定中断类型的,比如是快速中断或者共享中断等属性,
name:就是设备驱动的名称
dev:就是设备id,主要用在共享中断里面用来制定中断服务需要参考的数据地址,简单点就是在调用中断服务函数的时候会当作参数床给中断服务函数。
过程是怎样的呢?当用户调用这个函数的时候,内核会创建一个irqaction结构体,然后把它加入到irq_desc[]数组
中就可以了。当CPU收到中断请求后就通过中断好找到中断例程描述符再找到中断函数执行即可。
request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
request_irq()函数做了封装,该函数调用了request_threaded_irq()如下:
而其实最终将自己定义的中断服务函数加入到Linux中还是__setup_irq()这个函数,
从上面的函数可以看到,通过这个while循环,将自己定义的中断服务函数被加入到这个中断号的中断函数链表末尾,在__setup_irq这个函数里,完成一个irqaction的加入,从而我们中断产生时,linux能顺利找到我们自己定义的中断处理函数,这里再展示一下两个重要结构体的示意图,如图四:
图四、中断处理模型中的两个结构体
以上就是中断注册的过程。
三、底层中断处理分析
Linux里有中断号的概念,在中断发生时,linux会执行所有注册在该中断号上的所有函数。
那么linux里的中断向量是怎么设定的呢?中断号是怎么算处理的呢?为什么所有注册在中断号上的函数会在中断产生时被执行呢?这里我们将对这些问题仔细分析。
在中断发生时,处理器的第一个动作是自动(硬件自己完成)跳转到该中断的中断向量,
中断向量表的起始地址可以有两个位置:arm的异常和复位向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端向量,向量地址位于0xffff0000,
Linux选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上。可以通过CP15协处理器c1寄存器中V位(bit[13])控制。V和中断向量表的对应关系如下:
V=0---0x00000000~0x0000001C
V=1---0xffff0000~0xffff001C
当配置好了中断向量起始地址后,只要有中断发生,CPU就会自动跳到这中断向量处,不管这个地址处挂的是SDRAM或ROM。
当设置好了中断向量起始地址后,我们就需要在这些地址上放上我们的中断向量,由于linux启动时,内核代码会有一个从ROM中搬到RAM中的过程,中断向量也需要般过来,实际上在linux中,向量表建立的函数为
中断的分发:
中断是处理器核异常的一种,所以处理器设计中,外设中断引起处理器异常,处理器跳转到异常向量表的相应异常入口取指执行。
我们主要来看产生中断后,处理器跳转到中断异常入口后的执行流程。
对于arm处理器,执行流程如下:
vector_irq --->irq_handler ---> arch_irq_handler_default ---> asm_do_IRQ --->handle_IRQ
在arch_irq_handler_default中调用get_irqnr_and_base获取中断号,传给asm_do_IRQ作为参数。
get_irqnr_and_base由板级支持包实现。
到这里顺便提醒下,中断注意事项:
1. 首次申请中断时,默认情况中断是打开时能的。
2. 中断上半部要使用disable_irq_nosync()而不是disable_irq(),否则会导致死锁。
3.disable_irq()/enable_irq()可以多次嵌套调用,但是一定要在数量上配对。
4. 执行中断上半部时当前中断线是被屏蔽的,包括其他cpu,当前其他irq number不会被屏蔽。
5. 一个软中断不会抢占另一个软中断,唯一可抢占它的只有中断处理程序,因为在处理软中断处理程序的时候,
中断是开启的,但它和中断处理程序一样不能休眠。本地软中断被禁止,但是其他处理器上可执行软中断,
甚至是同类的中断,那么处理函数就会被执行两次,数据就会遭到破坏了。因此如果没必要,我们就用tasklet,
它的同一个处理函数不会在两个处理器上同时运行,这样也就避免了加锁的麻烦。
中断向量表在arch/arm/kernel/entry_armv.S文件的结尾部分,如下:
不过要注意位于__vectors_start和__vectors_end之间的是真正的向量跳转表,位于__stubs_start和__stubs_end之间的是处理跳转的部分。
例如:
vector_stub irq,IRQ_MODE, 4
以上这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。
系统启动阶段,位于arch/arm/kernel/traps.c中的early_trap_init()被调用:
以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x1000处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。
对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两个函数最终都会进入irq_handler这个宏:
接下来这个宏在entry-macro-multi.S,到这里后,就跳转到C调用了,asm_do_IRQ。
此函数在irq.c函数中实现,
在该文件下查看调用关系之后,最后来到关机函数:
接下来就简要的介绍一下三个结构体中重要变量的具体含义:
struct irq_desc {
structirq_data irq_data; //保存中断请求irq和chip相关的数据
irq_flow_handler_t handle_irq; //指向一个跟当前设备中断触发电信号类型相关的函数,比方说如果是边沿触发,那么就指向一个边沿触发类型的函数.在handle_irq指向的函数内部会调用
//设备特定的中断服务例程.特定平台linux系统在初始化阶段会提供handle_irq的具体实现
structirqaction *action; /* IRQ action list */ irqaction链表,是对某一具体设备的中断处理的抽象,设备驱动程序会通过
//request_irq来向这个链表中注册自己的中断处理函数,这个action结构体中有个next成员变量,会把
//注册的ISR串连起来,当然如果此irq line上只有一个设备,那么这个action就对应这个设备的中断处理程序
unsignedint status_use_accessors;//处理中断时的irq状态都保存在这个成员变量中
constchar *name; //会显示在/proc/interrupts中
......
};
struct irq_data {
unsigned int irq; //中断请求号
structirq_chip *chip; //当前中断来自的PIC,chip是对PIC的一个抽象,屏蔽硬件平台上PIC的差异,软件提供统一的对PIC操作的接口.这些函数接口主要用来屏蔽或启用当前
//中断,设定外部设备中断触发电信号的类型,向发出中断请求的设备发送中断响应信号,平台初始化函数负责实现该平台对应的PIC函数,并安装对irq_desc数组中
......
};
struct irqaction {
irq_handler_t handler; //中断服务例程,即我们熟知的ISR,当我们调用request_irq时会把我们实现的ISR注册到这里
void *dev_id; //调用handler时传递的参数,在多个设备共享一个irq号时特别重要,设备驱动程序就是通过此成员变量来标识自己,在free_irq时用到
structirqaction *next; //串连下一个action
......
};
通过以上的介绍,我们可以知道,一个中断对应两个层次,一个是handle_irq,一个是action.前者对应irqline上的处理动作,后者对应设备相关的中断处理,也就是说,一条irq line对应一个handle_irq,而这条irq line上可以挂载多个设备,即多个设备都可以通过同一个irqline产生中断,action成员变量用来串连挂载在此irq line上的各个设备驱动的ISR。如果irq line上只有一个设备驱动注册,那么这个action成员变量即是此设备驱动的ISR。用图五来表示就是:
图五、中断对应的两个层次结构
接着desc->handle_irq(irq, desc)这个调用走,handle_irq是在__irq_set_handler_locked()函数中赋值,得到处理句柄。
到处中断处理的大致流程走完。
图六表示整个中断处理流程: