本贴试图从硬件到软件以全方位角度来剖析基于ARM的Linux内核中如何处理一个完整的外部设备中断流程。
第一部分:硬件的行为
ARM的中断向量表如下:
从上图知道,对于IRQ中断类型(ARM平台下大约99%的外部设备使用IRQ中断,也是驱动程序员打交道最多的中断类型),其低端地址为0x0000_0018,高端地址为0xFFFF_0018(ARM处理器在IRQ中断发生时,到低端地址还是高端地址取中断向量是可配置的,缺省是到低端地址,在OS把MMU以及低端地址处的向量地址搬移到高端地址之后,通过协处理的配置寄存器将ARM的IRQ中断映射到高端地址0xFFFF_0018处),不管是高端地址也好,低端地址也罢,总之,当IRQ发生时,ARM处理器会跳转到0x****_0018处执行,从那里软件逻辑开始介入。但是,在此之前,ARM处理器(其他处理器也同样如此)会在内部执行一段硬件逻辑。这段硬件逻辑用伪码表示如下:
上图是ARM在接收到异常信号时通用的处理逻辑,如果是IRQ中断,更确切的硬件处理逻辑为:
所以在开始中断的软件逻辑前,IRQ是disable的,处理器运行在ARM模式。被中断指令的下条指令被保存在R14中。
如果我们将Linux下中断处理分成传说中的顶半部和底半部,那么所谓的“顶半部”,大约就是指实际响应中断的例程,也就是用request_irq注册的中断处理函数,而所谓的“底半部”是一个被“顶半部”调用,并在稍后更安全的时间内执行的例程。一个流传甚广的经典的叙述是这样的:“顶半部”在执行时,中断是关闭的,而“底半部”在执行时,中断则是打开的。如你在本贴第六部分看到的那样,情况并非如此。 所以,确切的说法应该是:
当一个“快速的顶半部”在运行时,所有的中断是关闭的,当一个“慢速的顶半部”在运行时,中断是打开的。这里区分一个“顶半部”是“快速”的还是“慢速”的,就是看你在request_irq时,是否设置了IRQF_DISABLED标志。而对于“底半部”而言,则总是在中断打开的情况下运行。
最近看2.6.35的内核,貌似hardirq部分的中断已经不能够再利用IRQF_DISABLED去打开中断了。看来大量中断嵌套导致的中断栈溢出的确是个问题。。。
6楼handle_IRQ_event中第一if语句在35下已经没了,而且对于IRQF_DISABLED,在include/linux/interrupt.h中:
* IRQF_DISABLED - keep irqs disabled when calling the action handler.
* DEPRECATED. This flag is a NOOP and scheduled to be removed
这样的话,整个流程中只有softirq还有机会开中断,不过这不成问题,即使这部分被中断的话,有新的中断进来,因为不再会进入softirq部分,所以会很快返回,不再占用栈空间。
第一部分:硬件的行为
ARM的中断向量表如下:
![1.jpg](http://embexperts.com/attachments/forumid_4/1008161422ae130afc3a23deb7.jpg)
从上图知道,对于IRQ中断类型(ARM平台下大约99%的外部设备使用IRQ中断,也是驱动程序员打交道最多的中断类型),其低端地址为0x0000_0018,高端地址为0xFFFF_0018(ARM处理器在IRQ中断发生时,到低端地址还是高端地址取中断向量是可配置的,缺省是到低端地址,在OS把MMU以及低端地址处的向量地址搬移到高端地址之后,通过协处理的配置寄存器将ARM的IRQ中断映射到高端地址0xFFFF_0018处),不管是高端地址也好,低端地址也罢,总之,当IRQ发生时,ARM处理器会跳转到0x****_0018处执行,从那里软件逻辑开始介入。但是,在此之前,ARM处理器(其他处理器也同样如此)会在内部执行一段硬件逻辑。这段硬件逻辑用伪码表示如下:
![1.jpg](http://embexperts.com/attachments/forumid_4/100816142156cfa8f9537e54c0.jpg)
上图是ARM在接收到异常信号时通用的处理逻辑,如果是IRQ中断,更确切的硬件处理逻辑为:
![1.jpg](http://embexperts.com/attachments/forumid_4/10081614275b4c60a5e136d24b.jpg)
所以在开始中断的软件逻辑前,IRQ是disable的,处理器运行在ARM模式。被中断指令的下条指令被保存在R14中。
第二部分 软件行为
在ARM完成硬件逻辑之后,跳转到0018H处开始执行。这部分代码由Linux开始接管,因为不同平台间的差异,Linux试图提供一个general的中断处理框架,对于平台相关的部分留有接口(这部分的代码由BSP的伙计们去完成)。总之,演出开始了。。。
在中断发生时,Linux可能正运行内核态的代码,也可能是用户态的代码。这两种少许有些区别,海豚就用第一种情况(被中断的处理器正处于内核态)来说明。
在arch/arm/kernel/entry-armV.S中,__irq_svc是这种情况的入口点。首先自然是建立中断的上下文环境,尤其是堆栈的建立,然后将那些需要入栈的全给它入栈了,这个帖子不打算讨论太多的细节,否则主线(mainstream)就不清楚了!
接下来很重要的一点是调用一个名为irq_handler的宏,因为在这个宏里会跳转到我们driver安装的中断处理例程里,所以这里仔细看看这段代码: 其中get_irqnr_and_base用来获得本次中断的硬件中断号,显然是个平台相关的函数,所以BSP的伙计们必须针对特定的平台来实现这个函数。这个函数的定义一般是放在include/asm-arm/ arch-imx /entry-macro.S, arch-imx便是一个特定的ARM平台。这段代码会和平台上的中断控制器打交道,用来获得硬件中断号,放在r0中, r1放的是struct pt_regs *,然后跳转到asm_do_IRQ(). Yes, asm_do_IRQ!多么熟悉的身影啊, 经常使C的程序员们就象看到了失散多年的亲人一样,眼泪哗哗的啊。。。。。。。。。。。。
在ARM完成硬件逻辑之后,跳转到0018H处开始执行。这部分代码由Linux开始接管,因为不同平台间的差异,Linux试图提供一个general的中断处理框架,对于平台相关的部分留有接口(这部分的代码由BSP的伙计们去完成)。总之,演出开始了。。。
在中断发生时,Linux可能正运行内核态的代码,也可能是用户态的代码。这两种少许有些区别,海豚就用第一种情况(被中断的处理器正处于内核态)来说明。
在arch/arm/kernel/entry-armV.S中,__irq_svc是这种情况的入口点。首先自然是建立中断的上下文环境,尤其是堆栈的建立,然后将那些需要入栈的全给它入栈了,这个帖子不打算讨论太多的细节,否则主线(mainstream)就不清楚了!
接下来很重要的一点是调用一个名为irq_handler的宏,因为在这个宏里会跳转到我们driver安装的中断处理例程里,所以这里仔细看看这段代码: 其中get_irqnr_and_base用来获得本次中断的硬件中断号,显然是个平台相关的函数,所以BSP的伙计们必须针对特定的平台来实现这个函数。这个函数的定义一般是放在include/asm-arm/ arch-imx /entry-macro.S, arch-imx便是一个特定的ARM平台。这段代码会和平台上的中断控制器打交道,用来获得硬件中断号,放在r0中, r1放的是struct pt_regs *,然后跳转到asm_do_IRQ(). Yes, asm_do_IRQ!多么熟悉的身影啊, 经常使C的程序员们就象看到了失散多年的亲人一样,眼泪哗哗的啊。。。。。。。。。。。。
第三部分:热兵器时代的asm_do_IRQ
这个函数怎么还放在arch/arm/kernel底下呢?我觉得既然它只跟irq_desc打交道的话,应该可以做到处理器无关了吧?应该放到linux/kernel底下,但是好像还不是,个中原因一时半会也搞不明白,还是功力不够啊
asm_do_IRQ定义如下: 初看代码量很少,不过完成的功能可不少。先看函数的两个形参:irq与regs,都刚好是前面汇编代码阶段传过来的r0和r1. 其中的主线调用是call到了desc_handle_irq.
看看desc_handle_irq: 原来是调用irq_desc结构中的handle_irq啊。这个irq_desc一定是被平台相关的代码在系统启动期间给初始化了,否则的话,任何的IRQ发生都啥都不干就返回来了。看看这事是谁干的?
这个函数怎么还放在arch/arm/kernel底下呢?我觉得既然它只跟irq_desc打交道的话,应该可以做到处理器无关了吧?应该放到linux/kernel底下,但是好像还不是,个中原因一时半会也搞不明白,还是功力不够啊
![](http://embexperts.com/images/smilies/default/sad.gif)
asm_do_IRQ定义如下: 初看代码量很少,不过完成的功能可不少。先看函数的两个形参:irq与regs,都刚好是前面汇编代码阶段传过来的r0和r1. 其中的主线调用是call到了desc_handle_irq.
看看desc_handle_irq: 原来是调用irq_desc结构中的handle_irq啊。这个irq_desc一定是被平台相关的代码在系统启动期间给初始化了,否则的话,任何的IRQ发生都啥都不干就返回来了。看看这事是谁干的?
第四部分 每个成功的中断例程后面都有一个默默支持它的BSP伙计
暂时放下asm_do_IRQ那一摊子先不管,来看一看在它的后面默默支持的代码。这个代码就是用来初始化irq_desc的每个数组entry的,又是个平台相关代码,还是以iMX平台为例,这个默默的代码就是mxc_init_irq,因为是平台相关,仔细分析的话似乎意义不大。大体的原理是:在该平台所支持的硬件中断号的范围内,比如0~31(假设通过IRQ支持32个外设的硬件中断号),那么就初始化irq_desc数组的0~31个entry,主要是irq_desc中的chip和handle_irq成员。
这样在asm_do_IRQ中,根据硬件中断号irq索引到相应的irq_desc entry,然后调用handle_irq,便是调用到这里赋予的函数,在iMX平台,这个函数实体是handle_level_irq. 到这里,你依然没看到你的driver中request_irq时安装的中断例程被调用的影子。
暂时放下asm_do_IRQ那一摊子先不管,来看一看在它的后面默默支持的代码。这个代码就是用来初始化irq_desc的每个数组entry的,又是个平台相关代码,还是以iMX平台为例,这个默默的代码就是mxc_init_irq,因为是平台相关,仔细分析的话似乎意义不大。大体的原理是:在该平台所支持的硬件中断号的范围内,比如0~31(假设通过IRQ支持32个外设的硬件中断号),那么就初始化irq_desc数组的0~31个entry,主要是irq_desc中的chip和handle_irq成员。
这样在asm_do_IRQ中,根据硬件中断号irq索引到相应的irq_desc entry,然后调用handle_irq,便是调用到这里赋予的函数,在iMX平台,这个函数实体是handle_level_irq. 到这里,你依然没看到你的driver中request_irq时安装的中断例程被调用的影子。
现在可以回到do_asm_IRQ部分了,有了第四部分的分析,现在应该晓得do_asm_IRQ中的desc_handle_irq实际上是调用到了handle_level_irq。
第五部分 离天堂不过1米的距离
如果不考虑防御性的代码,Linux的源代码的规模至少可以缩减30%左右。然而因为它是OS,所以它必须很健壮,至少表面看起来要很健壮 :)
在天堂1米远的地方是什么?handle_level_irq!那些蓬头垢面的BSP伙计们给我们安排的代码,现在你的设备产生了中断,经过逐级的调用,现在它到达了handle_level_irq,朝圣的路上最后一个桥头堡。
让我来写这个函数吧,效率绝对高,代码绝对精简,请看: 想BS海豚的兄弟应该看到这里的长处:主线是如此分明 :)。分明地,我看到了远处那片圣洁的薰衣草。。。。。。。。。。
第五部分 离天堂不过1米的距离
如果不考虑防御性的代码,Linux的源代码的规模至少可以缩减30%左右。然而因为它是OS,所以它必须很健壮,至少表面看起来要很健壮 :)
在天堂1米远的地方是什么?handle_level_irq!那些蓬头垢面的BSP伙计们给我们安排的代码,现在你的设备产生了中断,经过逐级的调用,现在它到达了handle_level_irq,朝圣的路上最后一个桥头堡。
让我来写这个函数吧,效率绝对高,代码绝对精简,请看: 想BS海豚的兄弟应该看到这里的长处:主线是如此分明 :)。分明地,我看到了远处那片圣洁的薰衣草。。。。。。。。。。
第六部分 谁动了你的奶酪?
在第五部分,你离天堂只有一步之遥,然后借助handle_IRQ_event的调用,现在你来到了西方的大雷音寺。我们有足够的理由兴奋一下,因为我们的request_irq中安装的中断处理例程要被call到了。到handle_IRQ_event里看看吧,是谁动了你的奶酪? 先看看这个函数的参数吧,第一个irq没说的,发生中断设备的硬件中断号,第二个,是irq_desc[irq]->action,你在request_irq里干的坏事,就是生成了一个action,并把它放到了irq索引的irq_desc数组里面,你的中断处理例程函数就附着在这个action上面。上面函数的核心是一个do{}while循环,在这个循环里它调用action->handler,这个调用直接导致你在request_irq里挂载的中断处理例程函数被调用。action作为一个链表而存在,支持了硬件中断号的共享。注意在调用action->handler时传进去的参数(irq, action->dev_id),如果你的中断是个与别的设备共享的中断,你必须小心设定这个dev_id参数。
这个函数还有一个特别有意思的地方,就是: 这一小片代码表明,如果你在request_irq时没有特别指明IRQF_DISABLED这个标志位,那么local_irq_enable_in_hardirq将会被调用来打开本地中断。不是说上半部(top half)是在中断关闭的情况下执行的吗,可是明摆着情况并不总是这样啊。怎么回事???
第七部分 不要迷信顶半部,那只是个传说
在第五部分,你离天堂只有一步之遥,然后借助handle_IRQ_event的调用,现在你来到了西方的大雷音寺。我们有足够的理由兴奋一下,因为我们的request_irq中安装的中断处理例程要被call到了。到handle_IRQ_event里看看吧,是谁动了你的奶酪? 先看看这个函数的参数吧,第一个irq没说的,发生中断设备的硬件中断号,第二个,是irq_desc[irq]->action,你在request_irq里干的坏事,就是生成了一个action,并把它放到了irq索引的irq_desc数组里面,你的中断处理例程函数就附着在这个action上面。上面函数的核心是一个do{}while循环,在这个循环里它调用action->handler,这个调用直接导致你在request_irq里挂载的中断处理例程函数被调用。action作为一个链表而存在,支持了硬件中断号的共享。注意在调用action->handler时传进去的参数(irq, action->dev_id),如果你的中断是个与别的设备共享的中断,你必须小心设定这个dev_id参数。
这个函数还有一个特别有意思的地方,就是: 这一小片代码表明,如果你在request_irq时没有特别指明IRQF_DISABLED这个标志位,那么local_irq_enable_in_hardirq将会被调用来打开本地中断。不是说上半部(top half)是在中断关闭的情况下执行的吗,可是明摆着情况并不总是这样啊。怎么回事???
如果我们将Linux下中断处理分成传说中的顶半部和底半部,那么所谓的“顶半部”,大约就是指实际响应中断的例程,也就是用request_irq注册的中断处理函数,而所谓的“底半部”是一个被“顶半部”调用,并在稍后更安全的时间内执行的例程。一个流传甚广的经典的叙述是这样的:“顶半部”在执行时,中断是关闭的,而“底半部”在执行时,中断则是打开的。如你在本贴第六部分看到的那样,情况并非如此。 所以,确切的说法应该是:
当一个“快速的顶半部”在运行时,所有的中断是关闭的,当一个“慢速的顶半部”在运行时,中断是打开的。这里区分一个“顶半部”是“快速”的还是“慢速”的,就是看你在request_irq时,是否设置了IRQF_DISABLED标志。而对于“底半部”而言,则总是在中断打开的情况下运行。
最近看2.6.35的内核,貌似hardirq部分的中断已经不能够再利用IRQF_DISABLED去打开中断了。看来大量中断嵌套导致的中断栈溢出的确是个问题。。。
6楼handle_IRQ_event中第一if语句在35下已经没了,而且对于IRQF_DISABLED,在include/linux/interrupt.h中:
* IRQF_DISABLED - keep irqs disabled when calling the action handler.
* DEPRECATED. This flag is a NOOP and scheduled to be removed
这样的话,整个流程中只有softirq还有机会开中断,不过这不成问题,即使这部分被中断的话,有新的中断进来,因为不再会进入softirq部分,所以会很快返回,不再占用栈空间。