【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】深入剖析Linux中断机制之三--Linux对异常和中断的处理

来源:互联网 发布:电脑抽奖软件免费 编辑:程序博客网 时间:2024/05/21 06:54

 

 

深入剖析Linux中断机制之三

--Linux对异常和中断的处理

 

Sailor_forever  sailing_9806@163.com 转载请注明

http://blog.csdn.net/sailor_8318/archive/2008/07/09/2627136.aspx

 

【摘要】本文详解了Linux内核的中断实现机制。首先介绍了中断的一些基本概念,然后分析了面向对象的Linux中断的组织形式、三种主要数据结构及其之间的关系。随后介绍了Linux处理异常和中断的基本流程,在此基础上分析了中断处理的详细流程,包括保存现场、中断处理、中断退出时的软中断执行及中断返回时的进程切换等问题。最后介绍了中断相关的API,包括中断注册和释放、中断关闭和使能、如何编写中断ISR、共享中断、中断上下文中断状态等。

【关键字】中断,异常,hw_interrupt_typeirq_desc_tirqactionasm_do_IRQ,软中断,进程切换,中断注册释放request_irqfree_irq,共享中断,可重入,中断上下文

 

 

1       Linux对异常和中断的处理

1.1    异常处理

Linux利用异常来达到两个截然不同的目的:

²      进程发送一个信号以通报一个反常情况

²      管理硬件资源

 

对于第一种情况,例如,如果进程执行了一个被0除的操作,CPU则会产生一个“除法错误”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号。当前进程接收到这个信号后,就要采取若干必要的步骤,或者从错误中恢复,或者终止执行(如果这个信号没有相应的信号处理程序)。

 

内核对异常处理程序的调用有一个标准的结构,它由以下三部分组成:

²      在内核栈中保存大多数寄存器的内容(由汇编语言实现)

²      调用C编写的异常处理函数

²      通过ret_from_exception()函数从异常退出。

 

1.2    中断处理

当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。另外中断处理程序不能执行任何阻塞过程,如I/O设备操作。因此,Linux把一个中断要执行的操作分为下面的三类:

²      紧急的(Critical

这样的操作诸如:中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程,或者对设备和处理器同时访问的数据结构进行修改。这些操作都是紧急的,应该被很快地执行,也就是说,紧急操作应该在一个中断处理程序内立即执行,而且是在禁用中断的状态下

²      非紧急的(Noncritical

这样的操作如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。

²      非紧急可延迟的(Noncritical deferrable

这样的操作如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序的进程)。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程会等待需要的数据。

 

所有的中断处理程序都执行四个基本的操作:

²      在内核栈中保存IRQ的值和寄存器的内容。

²      给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求。

²      执行共享这个IRQ的所有设备的中断服务例程(ISR)。

²      跳到ret_to_usr( )的地址后终止。

 

1.3    中断处理程序的执行流程

1.3.1      流程概述

现在,我们可以从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,沿着这一思路走一遍,以体会Linux内核对中断的响应及处理。

 

假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求,CPU就在执行完当前指令后来响应该中断。

 

中断处理系统在Linux中的实现是非常依赖于体系结构的,实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。

 

设备产生中断,通过总线把电信号发送给中断控制器。如果中断线是激活的,那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,否则,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。

 

对于ARM系统来说,有个专用的IRQ运行模式,有一个统一的入口地址。假定中断发生时CPU运行在用户空间,而中断处理程序属于内核空间,因此,要进行堆栈的切换。也就是说,CPUTSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。

 

 

若当前处于内核空间时,对于ARM系统来说是处于SVC模式,此时产生中断,中断处理完毕后,若是可剥夺内核,则检查是否需要进行进程调度,否则直接返回到被中断的内核空间;若需要进行进程调度,则svc_preempt,进程切换。

 190        .align  5

 191__irq_svc:

 192        svc_entry

 197#ifdef CONFIG_PREEMPT

 198        get_thread_info tsk

 199        ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count

 200        add     r7, r8, #1                      @ increment it

 201        str     r7, [tsk, #TI_PREEMPT]

 202#endif

 203

 204        irq_handler

 205#ifdef CONFIG_PREEMPT

 206        ldr     r0, [tsk, #TI_FLAGS]            @ get flags

 207        tst     r0, #_TIF_NEED_RESCHED

 208        blne    svc_preempt

 209preempt_return:

 210        ldr     r0, [tsk, #TI_PREEMPT]          @ read preempt value

 211        str     r8, [tsk, #TI_PREEMPT]          @ restore preempt count

 212        teq     r0, r7

 213        strne   r0, [r0, -r0]                   @ bug()

 214#endif

 215        ldr     r0, [sp, #S_PSR]                @ irqs are already disabled

 216        msr     spsr_cxsf, r0

 221        ldmia   sp, {r0 - pc}^                  @ load r0 - pc, cpsr

 222

 223        .ltorg

 

 

当前处于用户空间时,对于ARM系统来说是处于USR模式,此时产生中断,中断处理完毕后,无论是否是可剥夺内核,都调转到统一的用户模式出口ret_to_user,其检查是否需要进行进程调度,若需要进行进程调度,则进程切换,否则直接返回到被中断的用户空间

 

 404        .align  5

 405__irq_usr:

 406        usr_entry

 407

 411        get_thread_info tsk

 412#ifdef CONFIG_PREEMPT

 413        ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count

 414        add     r7, r8, #1                      @ increment it

 415        str     r7, [tsk, #TI_PREEMPT]

 416#endif

 417

 418        irq_handler

 419#ifdef CONFIG_PREEMPT

 420        ldr     r0, [tsk, #TI_PREEMPT]

 421        str     r8, [tsk, #TI_PREEMPT]

 422        teq     r0, r7

 423        strne   r0, [r0, -r0]       @ bug()

 424#endif

 428

 429        mov     why, #0

 430        b       ret_to_user

 432        .ltorg

 

 

1.3.2      保存现场

 105/*

 106 * SVC mode handlers

 107 */

 108

 115        .macro  svc_entry

 116        sub     sp, sp, #S_FRAME_SIZE

 117 SPFIX( tst     sp, #4          )

 118 SPFIX( bicne   sp, sp, #4      )

 119        stmib   sp, {r1 - r12}

 120

 121        ldmia   r0, {r1 - r3}

 122        add     r5, sp, #S_SP           @ here for interlock avoidance

 123        mov     r4, #-1                 @  ""  ""      ""       ""

 124        add     r0, sp, #S_FRAME_SIZE   @  ""  ""      ""       ""

 125 SPFIX( addne   r0, r0, #4      )

 126        str     r1, [sp]                @ save the "real" r0 copied

 127                                        @ from the exception stack

 128

 129        mov     r1, lr

 130

 131        @

 132        @ We are now ready to fill in the remaining blanks on the stack:

 133        @

 134        @  r0 - sp_svc

 135        @  r1 - lr_svc

 136        @  r2 - lr_<exception>, already fixed up for correct return/restart

 137        @  r3 - spsr_<exception>

 138        @  r4 - orig_r0 (see pt_regs definition in ptrace.h)

 139        @

 140        stmia   r5, {r0 - r4}

 141        .endm

 

1.3.3      中断处理

因为C的调用惯例是要把函数参数放在栈的顶部,因此pt- regs结构包含原始寄存器的值,这些值是以前在汇编入口例程svc_entry中保存在栈中的。

linux+v2.6.19/include/asm-arm/arch-at91rm9200/entry-macro.S

  18        .macro  get_irqnr_and_base, irqnr, irqstat, base, tmp

  19        ldr     /base, =(AT91_VA_BASE_SYS)              @ base virtual address of SYS peripherals

  20        ldr     /irqnr, [/base, #AT91_AIC_IVR]          @ read IRQ vector register: de-asserts nIRQ to processor (and clears interrupt)

  21        ldr     /irqstat, [/base, #AT91_AIC_ISR]        @ read interrupt source number

  22        teq     /irqstat, #0                            @ ISR is 0 when no current interrupt, or spurious interrupt

  23        streq   /tmp, [/base, #AT91_AIC_EOICR]          @ not going to be handled further, then ACK it now.

  24        .endm

 

  26/*

  27 * Interrupt handling.  Preserves r7, r8, r9

  28 */

  29        .macro  irq_handler

  301:      get_irqnr_and_base r0, r6, r5, lr

  31        movne   r1, sp

  32        @

  33        @ routine called with r0 = irq number, r1 = struct pt_regs *

  34        @

  35        adrne   lr, 1b

  36        bne     asm_do_IRQ

  58        .endm

 

 

中断号的值也在irq_handler初期得以保存,所以,asm_do_IRQ可以将它提取出来。这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数,最后这个函数才真正地执行中断服务例程(ISR)。下图给出它们的调用关系:

 

asm_do_IRQ

 

    do_IRQ()

 

handle_IRQ_event()

 

中断服务

例程1

 

 

 

 

 

 

例程

 

中断服务

例程2

 

 

 

 

 

 

例程

 

 

 


 

               

 

 

 

 

 

 

 

 

 

                               中断处理函数的调用关系

 

1.3.3.1          asm_do_IRQ

 112asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

 113{

 114        struct pt_regs *old_regs = set_irq_regs(regs);

 115        struct irqdesc *desc = irq_desc + irq;

 116

 121        if (irq >= NR_IRQS)

 122                desc = &bad_irq_desc;

 123

 124        irq_enter(); //记录硬件中断状态,便于跟踪中断情况确定是否是中断上下文

 125

 126        desc_handle_irq(irq, desc);

///////////////////desc_handle_irq

  33static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)

  34{

  35        desc->handle_irq(irq, desc); //通常handle_irq指向__do_IRQ

  36}

///////////////////desc_handle_irq

 130

 131        irq_exit(); //中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套

 132        set_irq_regs(old_regs);

 133}

 

1.3.3.2          __do_IRQ

 157 * __do_IRQ - original all in one highlevel IRQ handler

 167fastcall unsigned int __do_IRQ(unsigned int irq)

 168{

 169        struct irq_desc *desc = irq_desc + irq;

 170        struct irqaction *action;

 171        unsigned int status;

 172

 173        kstat_this_cpu.irqs[irq]++;

 186

 187        spin_lock(&desc->lock);

 188        if (desc->chip->ack) //首先响应中断,通常实现为关闭本中断线

 189                desc->chip->ack(irq);

 190       

 194        status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);

 195        status |= IRQ_PENDING; /* we _want_ to handle it */

 196

 201        action = NULL;

 202        if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {

 203                action = desc->action;

 204                status &= ~IRQ_PENDING; /* we commit to handling */

 205                status |= IRQ_INPROGRESS; /* we are handling it */

 206        }

 207        desc->status = status;

 208

 215        if (unlikely(!action))

 216                goto out;

 217

 218        /*

 219         * Edge triggered interrupts need to remember

 220         * pending events.

 227         */

 228        for (;;) {

 229                irqreturn_t action_ret;

 230

 231                spin_unlock(&desc->lock);//解锁,中断处理期间可以响应其他中断,否则再次进入__do_IRQ时会死锁

 233                action_ret = handle_IRQ_event(irq, action);

 237                spin_lock(&desc->lock);

 238                if (likely(!(desc->status & IRQ_PENDING)))

 239                        break;

 240                desc->status &= ~IRQ_PENDING;

 241        }

 242        desc->status &= ~IRQ_INPROGRESS;

 243

 244out:

 249        desc->chip->end(irq);

 250        spin_unlock(&desc->lock);

 251

 252        return 1;

 253}

 

 

该函数的实现用到中断线的状态,下面给予具体说明:

#define IRQ_INPROGRESS  1   /* 正在执行这个IRQ的一个处理程序*/

#define IRQ_DISABLED    2    /* 由设备驱动程序已经禁用了这条IRQ中断线 */

#define IRQ_PENDING     4    /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */

#define IRQ_REPLAY      8    /* Linux重新发送一个已被删除的IRQ */

#define IRQ_WAITING     32   /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */

#define IRQ_LEVEL       64    /* IRQ level triggered */

#define IRQ_MASKED      128    /* IRQ masked - shouldn't be seen again */

#define IRQ_PER_CPU     256     /* IRQ is per CPU */

8个状态的前5个状态比较常用,因此我们给出了具体解释。

 

经验表明,应该避免在同一条中断线上的中断嵌套,内核通过IRQ_PENDING标志位的应用保证了这一点。当do_IRQ()执行到for (;;)循环时,desc->status 中的IRQ_PENDING的标志位肯定为0。当CPU执行完handle_IRQ_event()函数返回时,如果这个标志位仍然为0,那么循环就此结束。如果这个标志位变为1,那就说明这条中断线上又有中断产生(对单CPU而言),所以循环又执行一次。通过这种循环方式,就把可能发生在同一中断线上的嵌套循环化解为“串行”。

 

在循环结束后调用desc->handler->end()函数,具体来说,如果没有设置IRQ_DISABLED标志位,就启用这条中断线。

 

1.3.3.3          handle_IRQ_event

当执行到for (;;)这个无限循环时,就准备对中断请求队列进行处理,这是由handle_IRQ_event()函数完成的。因为中断请求队列为一临界资源,因此在进入这个函数前要加锁。

handle_IRQ_event执行所有的irqaction链表:

 130irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)

 131{

 132        irqreturn_t ret, retval = IRQ_NONE;

 133        unsigned int status = 0;

 134

 135        handle_dynamic_tick(action);

 136       // 如果没有设置IRQF_DISABLED,则中断处理过程中,打开中断

 137        if (!(action->flags & IRQF_DISABLED))

 138                local_irq_enable_in_hardirq();

 139

 140        do {

 141                ret = action->handler(irq, action->dev_id);

 142                if (ret == IRQ_HANDLED)

 143                        status |= action->flags;

 144                retval |= ret;

 145                action = action->next;

 146        } while (action);

 147

 150        local_irq_disable();

 151

 152        return retval;

 153}

 

       

这个循环依次调用请求队列中的每个中断服务例程。这里要说明的是,如果设置了IRQF_DISABLED,则中断服务例程在关中断的条件下进行(不包括非屏蔽中断),但通常CPU在穿过中断门时自动关闭中断。但是,关中断时间绝不能太长,否则就可能丢失其它重要的中断。也就是说,中断服务例程应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理。即后半部分(bottom half)来处理,这一部分内容将在下一节进行讨论。

 

不同的CPU不允许并发地进入同一中断服务例程,否则,那就要求所有的中断服务例程必须是“可重入”的纯代码。可重入代码的设计和实现就复杂多了,因此,Linux在设计内核时巧妙地“避难就易”,以解决问题为主要目标。

 

1.3.3.4          irq_exit()

中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套

////////////////////////////////////////////////////////////

linux+v2.6.19/kernel/softirq.c

 285void irq_exit(void)

 286{

 287        account_system_vtime(current);

 288        trace_hardirq_exit();

 289        sub_preempt_count(IRQ_EXIT_OFFSET);

 290        if (!in_interrupt() && local_softirq_pending())

 291                invoke_softirq();

////////////

 276#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED

 277# define invoke_softirq()       __do_softirq()

 278#else

 279# define invoke_softirq()       do_softirq()

 280#endif

////////////

 292        preempt_enable_no_resched();

 293}

////////////////////////////////////////////////////////////

 

1.3.4      从中断返回

asm_do_IRQ()这个函数处理所有外设的中断请求后就要返回。返回情况取决于中断前程序是内核态还是用户态以及是否是可剥夺内核。

²      内核态可剥夺内核,只有在preempt_count0时,schedule()才会被调用,其检查是否需要进行进程切换,需要的话就切换。在schedule()返回之后,或者如果没有挂起的工作,那么原来的寄存器被恢复,内核恢复到被中断的内核代码。

²      内核态不可剥夺内核,则直接返回至被中断的内核代码。

²      中断前处于用户态时,无论是否是可剥夺内核,统一跳转到ret_to_user

 

虽然我们这里讨论的是中断的返回,但实际上中断、异常及系统调用的返回是放在一起实现的,因此,我们常常以函数的形式提到下面这三个入口点:

ret_to_user()

终止中断处理程序

ret_slow_syscall ( ) 或者ret_fast_syscall

终止系统调用,即由0x80引起的异常

ret_from_exception(  )

终止除了0x80的所有异常

 

 565/*

 566 * This is the return code to user mode for abort handlers

 567 */

 568ENTRY(ret_from_exception)

 569        get_thread_info tsk

 570        mov     why, #0

 571        b       ret_to_user

 

  57ENTRY(ret_to_user)

  58ret_slow_syscall:

 

由上可知,中断和异常需要返回用户空间时以及系统调用完毕后都需要经过统一的出口ret_slow_syscall,以此决定是否进行进程调度切换等。

 

linux+v2.6.19/arch/arm/kernel/entry-common.S

  16        .align  5

  17/*

  18 * This is the fast syscall return path.  We do as little as

  19 * possible here, and this includes saving r0 back into the SVC

  20 * stack.

  21 */

  22ret_fast_syscall:

  23        disable_irq                             @ disable interrupts

  24        ldr     r1, [tsk, #TI_FLAGS]

  25        tst     r1, #_TIF_WORK_MASK

  26        bne     fast_work_pending

  27

  28        @ fast_restore_user_regs

  29        ldr     r1, [sp, #S_OFF + S_PSR]        @ get calling cpsr

  30        ldr     lr, [sp, #S_OFF + S_PC]!        @ get pc

  31        msr     spsr_cxsf, r1                   @ save in spsr_svc

  32        ldmdb   sp, {r1 - lr}^                  @ get calling r1 - lr

  33        mov     r0, r0

  34        add     sp, sp, #S_FRAME_SIZE - S_PC

  35        movs    pc, lr                @ return & move spsr_svc into cpsr

  36

  37/*

  38 * Ok, we need to do extra processing, enter the slow path.

  39 */

  40fast_work_pending:

  41        str     r0, [sp, #S_R0+S_OFF]!          @ returned r0

  42work_pending:

  43        tst     r1, #_TIF_NEED_RESCHED

  44        bne     work_resched

  45        tst     r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING

  46        beq     no_work_pending

  47        mov     r0, sp                          @ 'regs'

  48        mov     r2, why                         @ 'syscall'

  49        bl      do_notify_resume

  50        b       ret_slow_syscall                @ Check work again

  51

  52work_resched:

  53        bl      schedule

  54/*

  55 * "slow" syscall return path.  "why" tells us if this was a real syscall.

  56 */

  57ENTRY(ret_to_user)

  58ret_slow_syscall:

  59        disable_irq                             @ disable interrupts

  60        ldr     r1, [tsk, #TI_FLAGS]

  61        tst     r1, #_TIF_WORK_MASK

  62        bne     work_pending

  63no_work_pending:

  64        @ slow_restore_user_regs

  65        ldr     r1, [sp, #S_PSR]                @ get calling cpsr

  66        ldr     lr, [sp, #S_PC]!                @ get pc

  67        msr     spsr_cxsf, r1                   @ save in spsr_svc

  68        ldmdb   sp, {r0 - lr}^                  @ get calling r1 - lr

  69        mov     r0, r0

  70        add     sp, sp, #S_FRAME_SIZE - S_PC

  71        movs    pc, lr                @ return & move spsr_svc into cpsr

 

进入ret_slow_syscall后,首先关中断,也就是说,执行这段代码时CPU不接受任何中断请求。然后,看调度标志是否为非0tst     r1, #_TIF_NEED_RESCHED),如果调度标志为非0,说明需要进行调度,则去调用schedule()函数进行进程调度。

 

 

 

原创粉丝点击