Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2851458
  • 博文数量: 523
  • 博客积分: 11908
  • 博客等级: 上将
  • 技术积分: 5475
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-03 15:50
文章分类

全部博文(523)

文章存档

2019年(3)

2013年(4)

2012年(71)

2011年(78)

2010年(57)

2009年(310)

分类: LINUX

2009-07-12 01:49:05

【摘要】本文详解了Linux内核的中断实现机制。首先介绍了中断的一些基本概念,然后分析了面向对象的Linux中断的组织形式、三种主要数据结构及其之间的关系。随后介绍了Linux处理异常和中断的基本流程,在此基础上分析了中断处理的详细流程,包括保存现场、中断处理、中断退出时的软中断执行及中断返回时的进程切换等问题。最后介绍了中断相关的API,包括中断注册和释放、中断关闭和使能、如何编写中断ISR、共享中断、中断上下文中断状态等。
【关键字】中断,异常,hw_interrupt_type,irq_desc_t,irqaction,asm_do_IRQ,软中断,进程切换,中断注册释放request_irq,free_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运行在用户空间,而中断处理程序属于内核空间,因此,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。
 

 
若当前处于内核空间时,对于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_, already fixed up for correct return/restart
 137        @  r3 - spsr_
 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_count为0时,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不接受任何中断请求。然后,看调度标志是否为非0(tst     r1, #_TIF_NEED_RESCHED),如果调度标志为非0,说明需要进行调度,则去调用schedule()函数进行进程调度。
 
 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/sailor_8318/archive/2008/08/28/2841002.aspx
阅读(1510) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~