本贴试图从硬件到软件以全方位角度来剖析基于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中。
第二部分 软件行为
在ARM完成硬件逻辑之后,跳转到0018H处开始执行。这部分代码由Linux开始接管,因为不同平台间的差异,Linux试图提供一个general的中断处理框架,对于平台相关的部分留有接口(这部分的代码由BSP的伙计们去完成)。总之,演出开始了。。。
在中断发生时,Linux可能正运行内核态的代码,也可能是用户态的代码。这两种少许有些区别,海豚就用第一种情况(被中断的处理器正处于内核态)来说明。
在arch/arm/kernel/entry-armV.S中,__irq_svc是这种情况的入口点。首先自然是建立中断的上下文环境,尤其是堆栈的建立,然后将那些需要入栈的全给它入栈了,这个帖子不打算讨论太多的细节,否则主线(mainstream)就不清楚了!
接下来很重要的一点是调用一个名为irq_handler的宏,因为在这个宏里会跳转到我们driver安装的中断处理例程里,所以这里仔细看看这段代码:
- /*
- * Interrupt handling. Preserves r7, r8, r9
- */
- .macro irq_handler
- get_irqnr_preamble r5, lr
- 1: get_irqnr_and_base r0, r6, r5, lr
- movne r1, sp
- @
- @ routine called with r0 = irq number, r1 = struct pt_regs *
- @
- adrne lr, 1b
- bne asm_do_IRQ
- #ifdef CONFIG_SMP
- /*
- * XXX
- *
- * this macro assumes that irqstat (r6) and base (r5) are
- * preserved from get_irqnr_and_base above
- */
- test_for_ipi r0, r6, r5, lr
- movne r0, sp
- adrne lr, 1b
- bne do_IPI
- #ifdef CONFIG_LOCAL_TIMERS
- test_for_ltirq r0, r6, r5, lr
- movne r0, sp
- adrne lr, 1b
- bne do_local_timer
- #endif
- #endif
- .endm
其中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定义如下:
- /*
- * do_IRQ handles all hardware IRQ's. Decoded IRQs should not
- * come via this function. Instead, they should provide their
- * own 'handler'
- */
- asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
- {
- struct pt_regs *old_regs = set_irq_regs(regs);
- struct irq_desc *desc = irq_desc + irq;
- /*
- * Some hardware gives randomly wrong interrupts. Rather
- * than crashing, do something sensible.
- */
- if (irq >= NR_IRQS)
- desc = &bad_irq_desc;
- irq_enter();
- desc_handle_irq(irq, desc);
- /* AT91 specific workaround */
- irq_finish(irq);
- irq_exit();
- set_irq_regs(old_regs);
- }
初看代码量很少,不过完成的功能可不少。先看函数的两个形参:irq与regs,都刚好是前面汇编代码阶段传过来的r0和r1. 其中的主线调用是call到了desc_handle_irq.
看看desc_handle_irq:
- /*
- * Obsolete inline function for calling irq descriptor handlers.
- */
- static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
- {
- desc->handle_irq(irq, desc);
- }
原来是调用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时安装的中断例程被调用的影子。
现在可以回到do_asm_IRQ部分了,有了第四部分的分析,现在应该晓得do_asm_IRQ中的desc_handle_irq实际上是调用到了handle_level_irq。
第五部分 离天堂不过1米的距离
如果不考虑防御性的代码,Linux的源代码的规模至少可以缩减30%左右。然而因为它是OS,所以它必须很健壮,至少表面看起来要很健壮 :)
在天堂1米远的地方是什么?handle_level_irq!那些蓬头垢面的BSP伙计们给我们安排的代码,现在你的设备产生了中断,经过逐级的调用,现在它到达了handle_level_irq,朝圣的路上最后一个桥头堡。
让我来写这个函数吧,效率绝对高,代码绝对精简,请看:
- void
- handle_level_irq(unsigned int irq, struct irq_desc *desc)
- {
- action = desc->action;
- action_ret = handle_IRQ_event(irq, action);
- }
想BS海豚的兄弟应该看到这里的长处:主线是如此分明 :)。分明地,我看到了远处那片圣洁的薰衣草。。。。。。。。。。
第六部分 谁动了你的奶酪?
在第五部分,你离天堂只有一步之遥,然后借助handle_IRQ_event的调用,现在你来到了西方的大雷音寺。我们有足够的理由兴奋一下,因为我们的request_irq中安装的中断处理例程要被call到了。到handle_IRQ_event里看看吧,是谁动了你的奶酪?
- /**
- * handle_IRQ_event - irq action chain handler
- * @irq: the interrupt number
- * @action: the interrupt action chain for this irq
- *
- * Handles the action chain of an irq event
- */
- irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
- {
- irqreturn_t ret, retval = IRQ_NONE;
- unsigned int status = 0;
- handle_dynamic_tick(action);
- if (!(action->flags & IRQF_DISABLED))
- local_irq_enable_in_hardirq();
- do {
- ret = action->handler(irq, action->dev_id);
- if (ret == IRQ_HANDLED)
- status |= action->flags;
- retval |= ret;
- action = action->next;
- } while (action);
- if (status & IRQF_SAMPLE_RANDOM)
- add_interrupt_randomness(irq);
- local_irq_disable();
- return retval;
- }
先看看这个函数的参数吧,第一个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参数。
这个函数还有一个特别有意思的地方,就是:
- if (!(action->flags & IRQF_DISABLED))
- local_irq_enable_in_hardirq();
这一小片代码表明,如果你在request_irq时没有特别指明IRQF_DISABLED这个标志位,那么local_irq_enable_in_hardirq将会被调用来打开本地中断。不是说上半部(top half)是在中断关闭的情况下执行的吗,可是明摆着情况并不总是这样啊。怎么回事???
第七部分 不要迷信顶半部,那只是个传说
如果我们将Linux下中断处理分成传说中的顶半部和底半部,那么所谓的“顶半部”,大约就是指实际响应中断的例程,也就是用request_irq注册的中断处理函数,而所谓的“底半部”是一个被“顶半部”调用,并在稍后更安全的时间内执行的例程。一个流传甚广的经典的叙述是这样的:“顶半部”在执行时,中断是关闭的,而“底半部”在执行时,中断则是打开的。如你在本贴第六部分看到的那样,情况并非如此。所以,确切的说法应该是:
当一个“快速的顶半部”在运行时,所有的中断是关闭的,当一个“慢速的顶半部”在运行时,中断是打开的。这里区分一个“顶半部”是“快速”的还是“慢速”的,就是看你在request_irq时,是否设置了IRQF_DISABLED标志。而对于“底半部”而言,则总是在中断打开的情况下运行。
================================================================================
对上述内容做出部分补充,例如Linux内核启动的时候是怎么初始化中断的。
在这里我有一个问题:上述都是介绍IRQ中断的,为什么没有FIQ中断的介绍?
阅读(5437) | 评论(1) | 转发(5) |