Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2927633
  • 博文数量: 401
  • 博客积分: 12926
  • 博客等级: 上将
  • 技术积分: 4588
  • 用 户 组: 普通用户
  • 注册时间: 2009-02-22 14:51
文章分类

全部博文(401)

文章存档

2015年(16)

2014年(4)

2013年(12)

2012年(82)

2011年(98)

2010年(112)

2009年(77)

分类: LINUX

2012-07-13 13:53:16

关键词:ARM中断向量表  中断处理例程  顶半部和底半部  快速和慢速中断处理例程 硬中断与软中断

本贴试图从硬件到软件以全方位角度来剖析基于ARM的Linux内核中如何处理一个完整的外部设备中断流程。

第一部分:硬件的行为
ARM的中断向量表如下:1.jpg

从上图知道,对于IRQ中断类型(ARM平台下大约99%的外部设备使用IRQ中断,也是驱动程序员打交道最多的中断类型),其低端地址为0x0000_0018,高端地址为0xFFFF_0018(ARM处理器在IRQ中断发生时,到低端地址还是高端地址取中断向量是可配置的,缺省是到低端地址,在OS把MMU以及低端地址处的向量地址搬移到高端地址之后,通过协处理的配置寄存器将ARM的IRQ中断映射到高端地址0xFFFF_0018处),不管是高端地址也好,低端地址也罢,总之,当IRQ发生时,ARM处理器会跳转到0x****_0018处执行,从那里软件逻辑开始介入。但是,在此之前,ARM处理器(其他处理器也同样如此)会在内部执行一段硬件逻辑。这段硬件逻辑用伪码表示如下:

1.jpg

上图是ARM在接收到异常信号时通用的处理逻辑,如果是IRQ中断,更确切的硬件处理逻辑为:
1.jpg

所以在开始中断的软件逻辑前,IRQ是disable的,处理器运行在ARM模式。被中断指令的下条指令被保存在R14中。
第二部分 软件行为

在ARM完成硬件逻辑之后,跳转到0018H处开始执行。这部分代码由Linux开始接管,因为不同平台间的差异,Linux试图提供一个general的中断处理框架,对于平台相关的部分留有接口(这部分的代码由BSP的伙计们去完成)。总之,演出开始了。。。

在中断发生时,Linux可能正运行内核态的代码,也可能是用户态的代码。这两种少许有些区别,海豚就用第一种情况(被中断的处理器正处于内核态)来说明。

在arch/arm/kernel/entry-armV.S中,__irq_svc是这种情况的入口点。首先自然是建立中断的上下文环境,尤其是堆栈的建立,然后将那些需要入栈的全给它入栈了,这个帖子不打算讨论太多的细节,否则主线(mainstream)就不清楚了!
接下来很重要的一点是调用一个名为irq_handler的宏,因为在这个宏里会跳转到我们driver安装的中断处理例程里,所以这里仔细看看这段代码:
  1. /*
  2. * Interrupt handling.  Preserves r7, r8, r9
  3. */
  4.         .macro        irq_handler
  5.         get_irqnr_preamble r5, lr
  6. 1:        get_irqnr_and_base r0, r6, r5, lr
  7.         movne        r1, sp
  8.         @
  9.         @ routine called with r0 = irq number, r1 = struct pt_regs *
  10.         @
  11.         adrne        lr, 1b
  12.         bne        asm_do_IRQ

  13. #ifdef CONFIG_SMP
  14.         /*
  15.          * XXX
  16.          *
  17.          * this macro assumes that irqstat (r6) and base (r5) are
  18.          * preserved from get_irqnr_and_base above
  19.          */
  20.         test_for_ipi r0, r6, r5, lr
  21.         movne        r0, sp
  22.         adrne        lr, 1b
  23.         bne        do_IPI

  24. #ifdef CONFIG_LOCAL_TIMERS
  25.         test_for_ltirq r0, r6, r5, lr
  26.         movne        r0, sp
  27.         adrne        lr, 1b
  28.         bne        do_local_timer
  29. #endif
  30. #endif

  31.         .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定义如下:
  1. /*
  2. * do_IRQ handles all hardware IRQ's.  Decoded IRQs should not
  3. * come via this function.  Instead, they should provide their
  4. * own 'handler'
  5. */
  6. asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
  7. {
  8.         struct pt_regs *old_regs = set_irq_regs(regs);
  9.         struct irq_desc *desc = irq_desc + irq;

  10.         /*
  11.          * Some hardware gives randomly wrong interrupts.  Rather
  12.          * than crashing, do something sensible.
  13.          */
  14.         if (irq >= NR_IRQS)
  15.                 desc = &bad_irq_desc;

  16.         irq_enter();

  17.         desc_handle_irq(irq, desc);

  18.         /* AT91 specific workaround */
  19.         irq_finish(irq);

  20.         irq_exit();
  21.         set_irq_regs(old_regs);
  22. }
复制代码
初看代码量很少,不过完成的功能可不少。先看函数的两个形参:irq与regs,都刚好是前面汇编代码阶段传过来的r0和r1. 其中的主线调用是call到了desc_handle_irq.
看看desc_handle_irq:
  1. /*
  2. * Obsolete inline function for calling irq descriptor handlers.
  3. */
  4. static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
  5. {
  6.         desc->handle_irq(irq, desc);
  7. }
复制代码
原来是调用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,朝圣的路上最后一个桥头堡。

让我来写这个函数吧,效率绝对高,代码绝对精简,请看:
  1. void
  2. handle_level_irq(unsigned int irq, struct irq_desc *desc)
  3. {
  4.        action = desc->action;
  5.        action_ret = handle_IRQ_event(irq, action);
  6. }
复制代码
想BS海豚的兄弟应该看到这里的长处:主线是如此分明 :)。分明地,我看到了远处那片圣洁的薰衣草。。。。。。。。。。
第六部分 谁动了你的奶酪?

在第五部分,你离天堂只有一步之遥,然后借助handle_IRQ_event的调用,现在你来到了西方的大雷音寺。我们有足够的理由兴奋一下,因为我们的request_irq中安装的中断处理例程要被call到了。到handle_IRQ_event里看看吧,是谁动了你的奶酪?
  1. /**
  2. * handle_IRQ_event - irq action chain handler
  3. * @irq:        the interrupt number
  4. * @action:        the interrupt action chain for this irq
  5. *
  6. * Handles the action chain of an irq event
  7. */
  8. irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
  9. {
  10.         irqreturn_t ret, retval = IRQ_NONE;
  11.         unsigned int status = 0;

  12.         handle_dynamic_tick(action);

  13.         if (!(action->flags & IRQF_DISABLED))
  14.                 local_irq_enable_in_hardirq();

  15.         do {
  16.                 ret = action->handler(irq, action->dev_id);
  17.                 if (ret == IRQ_HANDLED)
  18.                         status |= action->flags;
  19.                 retval |= ret;
  20.                 action = action->next;
  21.         } while (action);

  22.         if (status & IRQF_SAMPLE_RANDOM)
  23.                 add_interrupt_randomness(irq);
  24.         local_irq_disable();

  25.         return retval;
  26. }
复制代码
先看看这个函数的参数吧,第一个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参数。
这个函数还有一个特别有意思的地方,就是:
  1. if (!(action->flags & IRQF_DISABLED))
  2.                 local_irq_enable_in_hardirq();
复制代码
这一小片代码表明,如果你在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部分,所以会很快返回,不再占用栈空间。
阅读(1511) | 评论(0) | 转发(3) |
给主人留下些什么吧!~~