arm linux按键中断
2012-08-30 10:55:25 zhaohc_nj 阅读数 765
ARM Linux 中断分析
2008年03月24日 星期一 12:53
ARM体系结构中,把复位、中断、快速中断等都看作‘异常’,当这些‘异常’发生时,CPU会到固定地址处去找指令,他们对应的地址如下:
地址 异常类型 进入时的工作模式
0x00000000 Reset Supervisor
0x00000004 Und Undefined
0x00000008 Soft interupt Supervisor
0x0000000c Abort(prefetch) Abort
0x00000010 Abort(data) Abort
0x00000014 Reserved Reserved
0x00000018 IRQ IRQ
0x0000001c FIQ FIQ

    首先要明确的一点就是,无论内存地址空间是如何映射的,以上这些地址都不会变,比如当有快速中断发生时,ARM将铁定到0X0000001C这个地址处取指令。这也是BOOTLOADER把操作系统引导以后,内存必须重映射的原因!否则操作系统不能真正接管整套系统!
    LINUX启动以后要初始化这些区域,初始化代码在main.c中的start_kernel()中,具体是调用函数trap_ini()来实现的。如下面所示(具体可参照entry-armv.S):

.LCvectors:    swi SYS_ERROR0
              b      __real_stubs_start + (vector_undefinstr - __stubs_start)
           ldr     pc, __real_stubs_start + (.LCvswi - __stubs_start)
             b      __real_stubs_start + (vector_prefetch - __stubs_start)
             b      __real_stubs_start + (vector_data - __stubs_start)
            b      __real_stubs_start + (vector_addrexcptn - __stubs_start)
             b      __real_stubs_start + (vector_IRQ - __stubs_start)
             b      __real_stubs_start + (vector_FIQ - __stubs_start)

ENTRY(__trap_init)
           stmfd         sp!, {r4 - r6, lr}
           adr    r1, .LCvectors                        @ set up the vectors
           ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr}
           stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr}
           add    r2, r0, #0x200
           adr    r0, __stubs_start          @ copy stubs to 0x200
           adr    r1, __stubs_end
1:                 ldr     r3, [r0], #4
           str     r3, [r2], #4
           cmp r0, r1
           blt     1b
           LOADREGS(fd, sp!, {r4 - r6, pc})

以上可以看出这个函数初始化了中断向量,实际上把相应的跳转指令拷贝到了对应的地址。
当发生中断时,不管是从用户模式还是管理模式调用的,最终都要调用do_IRQ():

__irq_usr:    sub    sp, sp, #S_FRAME_SIZE
           stmia sp, {r0 - r12}                         @ save r0 - r12
           ldr     r4, .LCirq
           add    r8, sp, #S_PC
           ldmia r4, {r5 - r7}                           @ get saved PC, SPSR
           stmia r8, {r5 - r7}                           @ save pc, psr, old_r0
           stmdb         r8, {sp, lr}^
           alignment_trap r4, r7, __temp_irq
           zero_fp
1:                 get_irqnr_and_base r0, r6, r5, lr
           movne        r1, sp
           adrsvc        ne, lr, 1b
           @
           @ routine called with r0 = irq number, r1 = struct pt_regs *
           @
           bne    do_IRQ    @ 调用do_IRQ来实现具体的中断处理
           mov why, #0
           get_current_task tsk
           b       ret_to_user

对于以上代码,在很多文章中都有过分析,这里不再赘述。

    Linux每个中断通过一个结构irqdesc来描述,各中断的信息都在这个结构中得以体现:

struct irqdesc {
unsigned int         nomask   : 1;            /* IRQ does not mask in IRQ   */
unsigned int         enabled : 1;              /* IRQ is currently enabled   */
unsigned int         triggered: 1;                 /* IRQ has occurred           */
unsigned int         probing : 1;              /* IRQ in use for a probe     */
unsigned int         probe_ok : 1;              /* IRQ can be used for probe */
unsigned int         valid    : 1;               /* IRQ claimable       */
unsigned int         noautoenable : 1;        /* don't automatically enable IRQ */
unsigned int         unused   :25;
void (*mask_ack)(unsigned int irq);         /* Mask and acknowledge IRQ   */
void (*mask)(unsigned int irq);                /* Mask IRQ                      */
void (*unmask)(unsigned int irq);   /* Unmask IRQ                  */
struct irqaction *action;
/*
* IRQ lock detection
*/
unsigned int         lck_cnt;
unsigned int         lck_pc;
unsigned int         lck_jif;
};

在具体的ARM芯片中会有很多的中断类型,每一种类型的中断用以上结构来表示:
struct irqdesc irq_desc[NR_IRQS];   /* NR_IRQS根据不同的MCU会有所区别*/

在通过request_irq()函数注册中断服务程序的时候,将会把中断向量和中断服务程序对应起来。
我们来看一下request_irq的源码:

int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *),
            unsigned long irq_flags, const char * devname, void *dev_id)
{
unsigned long retval;
struct irqaction *action;
if (irq >= NR_IRQS || !irq_desc[irq].valid || !handler ||
    (irq_flags & SA_SHIRQ && !dev_id))
           return -EINVAL;
action = (struct irqaction *)kmalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)              /* 生成action结构*/
           return -ENOMEM;
action->handler = handler;
action->flags = irq_flags;
action->mask = 0;
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
retval = setup_arm_irq(irq, action);   /*把中断号irq和action 对应起来*/
if (retval)
           kfree(action);
return retval;
}
    其中第一个参数irq就是中断向量,第二个参数即是要注册的中断服务程序。很多同仁可能疑惑的是,我们要注册的中断向量号是怎么确定的呢?这要根据具体芯片的中断控制器,比如三星的S3C2410,需要通过读取其中的中断状态寄存器,来获得是哪个设备发生了中断:

if defined(CONFIG_ARCH_S3C2410)
#include <asm/hardware.h>
           .macro disable_fiq
           .endm
           .macro get_irqnr_and_base, irqnr, irqstat, base, tmp
           mov r4, #INTBASE             @ virtual address of IRQ registers
           ldr     irqnr, [r4, #0x8] @ read INTMSK   中断掩码寄存器
           ldr     irqstat, [r4, #0x10]   @ read INTPND 中断寄存器
           bics    irqstat, irqstat, irqnr
           bics    irqstat, irqstat, irqnr
           beq    1002f                  
           mov irqnr, #0
1001:           tst     irqstat, #1
           bne    1002f                            @ found IRQ
           add    irqnr, irqnr, #1
           mov irqstat, irqstat, lsr #1
           cmp irqnr, #32
           bcc    1001b
1002:
           .endm
           .macro irq_prio_table
           .endm                

以上代码也告诉了我们,中断号的确定,其实是和S3C2410手册中SRCPND寄存器是一致的,即:

/* Interrupt Controller */
#define IRQ_EINT0               0       /* External interrupt 0 */
#define IRQ_EINT1               1       /* External interrupt 1 */
#define IRQ_EINT2               2       /* External interrupt 2 */
#define IRQ_EINT3               3       /* External interrupt 3 */
#define IRQ_EINT4_7           4       /* External interrupt 4 ~ 7 */
#define IRQ_EINT8_23                  5       /* External interrupt 8 ~ 23 */
#define IRQ_RESERVED6              6       /* Reserved for future use */
#define IRQ_BAT_FLT                   7
#define IRQ_TICK                 8       /* RTC time tick interrupt */
#define IRQ_WDT                          9       /* Watch-Dog timer interrupt */
#define IRQ_TIMER0           10     /* Timer 0 interrupt */
#define IRQ_TIMER1           11     /* Timer 1 interrupt */
#define IRQ_TIMER2           12     /* Timer 2 interrupt */
#define IRQ_TIMER3           13     /* Timer 3 interrupt */
#define IRQ_TIMER4           14     /* Timer 4 interrupt */
#define IRQ_UART2             15     /* UART 2 interrupt */
#define IRQ_LCD                            16     /* reserved for future use */
#define IRQ_DMA0              17     /* DMA channel 0 interrupt */
#define IRQ_DMA1              18     /* DMA channel 1 interrupt */
#define IRQ_DMA2              19     /* DMA channel 2 interrupt */
#define IRQ_DMA3              20     /* DMA channel 3 interrupt */
#define IRQ_SDI                    21     /* SD Interface interrupt */
#define IRQ_SPI0                   22     /* SPI interrupt */
#define IRQ_UART1             23     /* UART1 receive interrupt */
#define IRQ_RESERVED24            24
#define IRQ_USBD                25     /* USB device interrupt */
#define IRQ_USBH                26     /* USB host interrupt */
#define IRQ_IIC                     27     /* IIC interrupt */
#define IRQ_UART0             28     /* UART0 transmit interrupt */
#define IRQ_SPI1                   29     /* UART1 transmit interrupt */
#define IRQ_RTC                            30     /* RTC alarm interrupt */
#define IRQ_ADCTC            31     /* ADC EOC interrupt */
#define NORMAL_IRQ_OFFSET 32

这些宏定义在文件irqs.h中,大家可以看到它的定义取自S3C2410的文档。

总结: linux在初始化的时候已经把每个中断向量的地址准备好了!就是说添加中断服务程序的框架已经给出,当某个中断发生时,将会到确定的地址处去找指令,所以我们做驱动程序时,只需要经过request_irq()来挂接自己编写的中断服务程序即可。

另:对于快速中断,linux在初始化时是空的,所以要对它挂接中断处理程序,就需要单独的函数set_fiq_handler()来实现,此函数在源文件fiq.c中,有兴趣的读者可进一步研究

2011-10-17 19:56:00 iteye_15968 阅读数 14

中断机制的核心数据结构是 irq_desc,它完整地描述了一条中断线(或称为“中断通道” ) 。

irq_desc 结构在 include/linux/irq.h 中定义在 kernel/irq/handle.c 中

有个全局 irq_desc 数组,描述了系统中所有的中断线:
handle_irq,这是个函数指针,指向的是一个高层次的中断事件处理函数,定义了处理中断事件的一种策略。
在 kernel/irq/chip.c 中实现了 5 个函数:handle_simple_irq(),handle_level_irq(),
handle_irq 指针可handle_edge_irq(),handle_fasteoi_irq()以及 handle_percpu_irq()。
以指向这 5 个函数中的一个,选择一种中断事件处理策略,这是通过函数 set_irq_handler()完成的。



接下来是 chip, 这是个 irq_chip 结构指针。irq_chip 结构定义了对中断线的底层硬件操作,
在 include/linux/irq.h 中:


最后的数据成员是 action,这是个 irq_action 结构指针。 irq_action 结构定义了安装在中
断线上的一个中断处理程序,在 include/linux/interrupt.h 中


init/main.c
asmlinkage void __init start_kernel(void)
{
......
584 setup_arch(&command_line);//{750 init_arch_irq = mdesc->init_irq;} 对627行调用的init_arch_irq函数指针赋值。
......
/* 重定位中断向量表,修改中断向量表占用页面的访问权限 */
606 trap_init();
....
/* 这个函数首先把irq_desc数组中的所有元素的status设为IRQ_NOREQUEST|IRQ_NOPROBE(没有请求,没有检测),然后调用init_arch_irq()*/
627 init_IRQ();//{init_arch_irq();...}

}
init_arch_irq()在setup_arch()中被赋值,指向machine_desc中定义的init_irq函数指针。AT91SAM9260EK 平台上,就指向了 arch/arm/mach-at91/board-sam9260ek.c 中定义的函数ek_init_irq()。ek_init_irq() 又 调 用 了 arch/arm/mach-at91/at91sam9260.c中的at91sam9260_init_interrupts()。(这个machine_desc的赋值在arch/arm/mach-at91/board-sam9260ek.c最后几行中MACHINE_START(AT91SAM9260EK, "Atmel AT91SAM9260-EK").....其中就有.init_irq = ek_init_irq。)
接下来 arch/arm/mach-at91/irq.c 中 的 at91_aic_init()进行AIC初始化,调用arch/arm/mach-at91/gpio.c 中的 at91_gpio_irq_setup()进行GPIO的中断初始化。
arch/arm/mach-at91/irq.c
static struct irq_chip at91_aic_chip = {
.name = "AIC",
.ack = at91_aic_mask_irq,
.mask = at91_aic_mask_irq,
.unmask = at91_aic_unmask_irq,
.set_type = at91_aic_set_type,
.> .set_wake = at91_aic_set_wake,
};
132 void __init at91_aic_init(unsigned int priority[NR_AIC_IRQS])
这个函数主要是写了 AIC 的一些寄存器,还为每个中断线设置了 chip(指向at91_aic_chip) 和 handle_irq ( 指 向 handle_level_irq ) 然 后 把 中 断 线 的 状 态 修 改 为IRQF_VALID | IRQF_PROBE(已有效&以检测),at91_aic_chip 定义的底层操作函数也在arch/arm/mach-at91/irq.c 中,不再详述。
至此,中断机制的初始化工作已完成。

2015-01-30 17:19:28 michaelcao1980 阅读数 1566

一、前言

本文主要以ARM体系结构下的中断处理为例,讲述整个中断处理过程中的硬件行为和软件动作。具体整个处理过程分成三个步骤来描述:

1、第二章描述了中断处理的准备过程

2、第三章描述了当发生中的时候,ARM硬件的行为

3、第四章描述了ARM的中断进入过程

4、第五章描述了ARM的中断退出过程

本文涉及的代码来自3.14内核。另外,本文注意描述ARM指令集的内容,有些source code为了简短一些,删除了THUMB相关的代码,除此之外,有些debug相关的内容也会删除。

 

二、中断处理的准备过程

1、中断模式的stack准备

ARM处理器有多种process mode,例如user mode(用户空间的AP所处于的模式)、supervisor mode(即SVC mode,大部分的内核态代码都处于这种mode)、IRQ mode(发生中断后,处理器会切入到该mode)等。对于linux kernel,其中断处理处理过程中,ARM 处理器大部分都是处于SVC mode。但是,实际上产生中断的时候,ARM处理器实际上是进入IRQ mode,因此在进入真正的IRQ异常处理之前会有一小段IRQ mode的操作,之后会进入SVC mode进行真正的IRQ异常处理。由于IRQ mode只是一个过度,因此IRQ mode的栈很小,只有12个字节,具体如下:

struct stack { 
    u32 irq[3]; 
    u32 abt[3]; 
    u32 und[3]; 
} ____cacheline_aligned;

static struct stack stacks[NR_CPUS];

除了irq mode,linux kernel在处理abt mode(当发生data abort exception或者prefetch abort exception的时候进入的模式)和und mode(处理器遇到一个未定义的指令的时候进入的异常模式)的时候也是采用了相同的策略。也就是经过一个简短的abt或者und mode之后,stack切换到svc mode的栈上,这个栈就是发生异常那个时间点current thread的内核栈。anyway,在irq mode和svc mode之间总是需要一个stack保存数据,这就是中断模式的stack,系统初始化的时候,cpu_init函数中会进行中断模式stack的设定:

void notrace cpu_init(void) 
{

    unsigned int cpu = smp_processor_id();------获取CPU ID 
    struct stack *stk = &stacks[cpu];---------获取该CPU对于的irq abt和und的stack指针

……

#ifdef CONFIG_THUMB2_KERNEL 
#define PLC    "r"------Thumb-2下,msr指令不允许使用立即数,只能使用寄存器。 
#else 
#define PLC    "I" 
#endif


    __asm__ ( 
    "msr    cpsr_c, %1\n\t"------让CPU进入IRQ mode 
    "add    r14, %0, %2\n\t"------r14寄存器保存stk->irq 
    "mov    sp, r14\n\t"--------设定IRQ mode的stack为stk->irq 
    "msr    cpsr_c, %3\n\t" 
    "add    r14, %0, %4\n\t" 
    "mov    sp, r14\n\t"--------设定abt mode的stack为stk->abt 
    "msr    cpsr_c, %5\n\t" 
    "add    r14, %0, %6\n\t" 
    "mov    sp, r14\n\t"--------设定und mode的stack为stk->und 
    "msr    cpsr_c, %7"--------回到SVC mode 
        :--------------------上面是code,下面的output部分是空的 
        : "r" (stk),----------------------对应上面代码中的%0 
          PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------对应上面代码中的%1 
          "I" (offsetof(struct stack, irq[0])),------------对应上面代码中的%2 
          PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此类推,下面不赘述
          "I" (offsetof(struct stack, abt[0])), 
          PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE), 
          "I" (offsetof(struct stack, und[0])), 
          PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE) 
        : "r14");--------上面是input操作数列表,r14是要clobbered register列表 
}

嵌入式汇编的语法格式是:asm(code : output operand list : input operand list : clobber list);大家对着上面的code就可以分开各段内容了。在input operand list中,有两种限制符(constraint),"r"或者"I","I"表示立即数(Immediate operands),"r"表示用通用寄存器传递参数。clobber list中有一个r14,表示在汇编代码中修改了r14的值,这些信息是编译器需要的内容。

2、SVC模式的stack准备

我们经常说进程的用户空间和内核空间,对于一个应用程序而言,可以运行在用户空间,也可以通过系统调用进入内核空间。在用户空间,使用的是用户栈,也就是我们软件工程师编写用户空间程序的时候,保存局部变量的stack。陷入内核后,当然不能用用户栈了,这时候就需要使用到内核栈。所谓内核栈其实就是处于SVC mode时候使用的栈。

Linux kernel在创建进程(包括用户进程和内核线程)的时候都会分配一个(或者两个,和配置相关)page frame,底部是struct thread_info数据结构,顶部(高地址)就是该进程的内核栈。当进程切换的时候,整个硬件和软件的上下文都会进行切换,这里就包括了svc mode的sp寄存器的值被切换到调度算法选定的新的进程的内核栈上来。

 

3、异常向量表的准备

对于ARM处理器而言,当发生异常的时候,处理器会暂停当前指令的执行,保存现场,转而去执行对应的异常向量处的指令,当处理完该异常的时候,恢复现场,回到原来的那点去继续执行程序。系统所有的异常向量(共计8个)组成了异常向量表。向量表(vector table)的代码如下:

.section .vectors, "ax", %progbits 
__vectors_start: 
    W(b)    vector_rst 
    W(b)    vector_und 
    W(ldr)    pc, __vectors_start + 0x1000 
    W(b)    vector_pabt 
    W(b)    vector_dabt 
    W(b)    vector_addrexcptn 
    W(b)    vector_irq ---------------------------IRQ Vector 
    W(b)    vector_fiq

对于本文而言,我们重点关注vector_irq这个exception vector。异常向量表可能被安放在两个位置上:

(1)异常向量表位于0x0的地址。这种设置叫做Normal vectors或者Low vectors。

(2)异常向量表位于0xffff0000的地址。这种设置叫做high vectors

具体是low vectors还是high vectors是由ARM的一个叫做的SCTLR寄存器的第13个bit (vector bit)控制的。对于启用MMU的ARM Linux而言,系统使用了high vectors。为什么不用low vector呢?对于linux而言,0~3G的空间是用户空间,如果使用low vector,那么异常向量表在0地址,那么则是用户空间的位置,因此linux选用high vector。当然,使用Low vector也可以,这样Low vector所在的空间则属于kernel space了(也就是说,3G~4G的空间加上Low vector所占的空间属于kernel space),不过这时候要注意一点,因为所有的进程共享kernel space,而用户空间的程序经常会发生空指针访问,这时候,内存保护机制应该可以捕获这种错误(大部分的MMU都可以做到,例如:禁止userspace访问kernel space的地址空间),防止vector table被访问到。对于内核中由于程序错误导致的空指针访问,内存保护机制也需要控制vector table被修改,因此vector table所在的空间被设置成read only的。在使用了MMU之后,具体异常向量表放在那个物理地址已经不重要了,重要的是把它映射到0xffff0000的虚拟地址就OK了,具体代码如下:

static void __init devicemaps_init(const struct machine_desc *mdesc) 

    …… 
    vectors = early_alloc(PAGE_SIZE * 2); -----分配两个page的物理页帧

    early_trap_init(vectors); -------copy向量表以及相关help function到该区域

    …… 
    map.pfn = __phys_to_pfn(virt_to_phys(vectors)); 
    map.virtual = 0xffff0000; 
    map.length = PAGE_SIZE; 
#ifdef CONFIG_KUSER_HELPERS 
    map.type = MT_HIGH_VECTORS; 
#else 
    map.type = MT_LOW_VECTORS; 
#endif 
    create_mapping(&map); ----------映射0xffff0000的那个page frame

    if (!vectors_high()) {---如果SCTLR.V的值设定为low vectors,那么还要映射0地址开始的memory 
        map.virtual = 0; 
        map.length = PAGE_SIZE * 2; 
        map.type = MT_LOW_VECTORS; 
        create_mapping(&map); 
    }


    map.pfn += 1; 
    map.virtual = 0xffff0000 + PAGE_SIZE; 
    map.length = PAGE_SIZE; 
    map.type = MT_LOW_VECTORS; 
    create_mapping(&map); ----------映射high vecotr开始的第二个page frame

…… 
}

为什么要分配两个page frame呢?这里vectors table和kuser helper函数(内核空间提供的函数,但是用户空间使用)占用了一个page frame,另外异常处理的stub函数占用了另外一个page frame。为什么会有stub函数呢?稍后会讲到。

在early_trap_init函数中会初始化异常向量表,具体代码如下:

void __init early_trap_init(void *vectors_base) 

    unsigned long vectors = (unsigned long)vectors_base; 
    extern char __stubs_start[], __stubs_end[]; 
    extern char __vectors_start[], __vectors_end[]; 
    unsigned i;

    vectors_page = vectors_base;

    将整个vector table那个page frame填充成未定义的指令。起始vector table加上kuser helper函数并不能完全的充满这个page,有些缝隙。如果不这么处理,当极端情况下(程序错误或者HW的issue),CPU可能从这些缝隙中取指执行,从而导致不可知的后果。如果将这些缝隙填充未定义指令,那么CPU可以捕获这种异常。 
    for (i = 0; i < PAGE_SIZE / sizeof(u32); i++) 
        ((u32 *)vectors_base)[i] = 0xe7fddef1;

  拷贝vector table,拷贝stub function 
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); 
    memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

    kuser_init(vectors_base); ----copy kuser helper function

    flush_icache_range(vectors, vectors + PAGE_SIZE * 2); 
    modify_domain(DOMAIN_USER, DOMAIN_CLIENT); 
}

一旦涉及代码的拷贝,我们就需要关心其编译连接时地址(link-time address)和运行时地址(run-time address)。在kernel完成链接后,__vectors_start有了其link-time address,如果link-time address和run-time address一致,那么这段代码运行时毫无压力。但是,目前对于vector table而言,其被copy到其他的地址上(对于High vector,这是地址就是0xffff00000),也就是说,link-time address和run-time address不一样了,如果仍然想要这些代码可以正确运行,那么需要这些代码是位置无关的代码。对于vector table而言,必须要位置无关。B这个branch instruction本身就是位置无关的,它可以跳转到一个当前位置的offset。不过并非所有的vector都是使用了branch instruction,对于软中断,其vector地址上指令是“W(ldr)    pc, __vectors_start + 0x1000 ”,这条指令被编译器编译成ldr     pc, [pc, #4080],这种情况下,该指令也是位置无关的,但是有个限制,offset必须在4K的范围内,这也是为何存在stub section的原因了。

4、中断控制器的初始化

具体可以参考GIC代码分析

 

三、ARM HW对中断事件的处理

当一切准备好之后,一旦打开处理器的全局中断就可以处理来自外设的各种中断事件了。

当外设(SOC内部或者外部都可以)检测到了中断事件,就会通过interrupt requestion line上的电平或者边沿(上升沿或者下降沿或者both)通知到该外设连接到的那个中断控制器,而中断控制器就会在多个处理器中选择一个,并把该中断通过IRQ(或者FIQ,本文不讨论FIQ的情况)分发给process。ARM处理器感知到了中断事件后,会进行下面一系列的动作:

1、修改CPSR(Current Program Status Register)寄存器中的M[4:0]。M[4:0]表示了ARM处理器当前处于的模式( processor modes)。ARM定义的mode包括:

处理器模式 缩写 对应的M[4:0]编码 Privilege level
User usr 10000 PL0
FIQ fiq 10001 PL1
IRQ irq 10010 PL1
Supervisor svc 10011 PL1
Monitor mon 10110 PL1
Abort abt 10111 PL1
Hyp hyp 11010 PL2
Undefined und 11011 PL1
System sys 11111 PL1

一旦设定了CPSR.M,ARM处理器就会将processor mode切换到IRQ mode。

2、保存发生中断那一点的CPSR值(step 1之前的状态)和PC值

ARM处理器支持9种processor mode,每种mode看到的ARM core register(R0~R15,共计16个)都是不同的。每种mode都是从一个包括所有的Banked ARM core register中选取。全部Banked ARM core register包括:

Usr System Hyp Supervisor abort undefined Monitor IRQ FIQ
R0_usr                
R1_usr                
R2_usr                
R3_usr                
R4_usr                
R5_usr                
R6_usr                
R7_usr                
R8_usr               R8_fiq
R9_usr               R9_fiq
R10_usr               R10_fiq
R11_usr               R11_fiq
R12_usr               R12_fiq
SP_usr   SP_hyp SP_svc SP_abt SP_und SP_mon SP_irq SP_fiq
LR_usr     LR_svc LR_abt LR_und LR_mon LR_irq LR_fiq
PC                
CPSR                
    SPSR_hyp SPSR_svc SPSR_abt SPSR_und SPSR_mon SPSR_irq SPSR_fiq
    ELR_hyp            

在IRQ mode下,CPU看到的R0~R12寄存器、PC以及CPSR是和usr mode(userspace)或者svc mode(kernel space)是一样的。不同的是IRQ mode下,有自己的R13(SP,stack pointer)、R14(LR,link register)和SPSR(Saved Program Status Register)。

CPSR是共用的,虽然中断可能发生在usr mode(用户空间),也可能是svc mode(内核空间),不过这些信息都是体现在CPSR寄存器中。硬件会将发生中断那一刻的CPSR保存在SPSR寄存器中(由于不同的mode下有不同的SPSR寄存器,因此更准确的说应该是SPSR-irq,也就是IRQ mode中的SPSR寄存器)。

PC也是共用的,由于后续PC会被修改为irq exception vector,因此有必要保存PC值。当然,与其说保存PC值,不如说是保存返回执行的地址。对于IRQ而言,我们期望返回地址是发生中断那一点执行指令的下一条指令。具体的返回地址保存在lr寄存器中(注意:这个lr寄存器是IRQ mode的lr寄存器,可以表示为lr_irq):

(1)对于thumb state,lr_irq = PC

(2)对于ARM state,lr_irq = PC - 4

为何要减去4?我的理解是这样的(不一定对)。由于ARM采用流水线结构,当CPU正在执行某一条指令的时候,其实取指的动作早就执行了,这时候PC值=正在执行的指令地址 + 8,如下所示:

----> 发生中断的指令

               发生中断的指令+4

-PC-->发生中断的指令+8

               发生中断的指令+12

一旦发生了中断,当前正在执行的指令当然要执行完毕,但是已经完成取指、译码的指令则终止执行。当发生中断的指令执行完毕之后,原来指向(发生中断的指令+8)的PC会继续增加4,因此发生中断后,ARM core的硬件着手处理该中断的时候,硬件现场如下图所示:

----> 发生中断的指令

               发生中断的指令+4 <-------中断返回的指令是这条指令

              发生中断的指令+8

-PC-->发生中断的指令+12

这时候的PC值其实是比发生中断时候的指令超前12。减去4之后,lr_irq中保存了(发生中断的指令+8)的地址。为什么HW不帮忙直接减去8呢?这样,后续软件不就不用再减去4了。这里我们不能孤立的看待问题,实际上ARM的异常处理的硬件逻辑不仅仅处理IRQ的exception,还要处理各种exception,很遗憾,不同的exception期望的返回地址不统一,因此,硬件只是帮忙减去4,剩下的交给软件去调整。

3、mask IRQ exception。也就是设定CPSR.I = 1

4、设定PC值为IRQ exception vector。基本上,ARM处理器的硬件就只能帮你帮到这里了,一旦设定PC值,ARM处理器就会跳转到IRQ的exception vector地址了,后续的动作都是软件行为了。

 

四、如何进入ARM中断处理

1、IRQ mode中的处理

IRQ mode的处理都在vector_irq中,vector_stub是一个宏,定义如下:

.macro    vector_stub, name, mode, correction=0 
    .align    5

vector_\name: 
    .if \correction 
    sub    lr, lr, #\correction-------------(1) 
    .endif

    @ 
    @ Save r0, lr_ (parent PC) and spsr_ 
    @ (parent CPSR) 
    @ 
    stmia    sp, {r0, lr}        @ save r0, lr--------(2) 
    mrs    lr, spsr 
    str    lr, [sp, #8]        @ save spsr

    @ 
    @ Prepare for SVC32 mode.  IRQs remain disabled. 
    @ 
    mrs    r0, cpsr-----------------------(3) 
    eor    r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) 
    msr    spsr_cxsf, r0

    @ 
    @ the branch table must immediately follow this code 
    @ 
    and    lr, lr, #0x0f---lr保存了发生IRQ时候的CPSR,通过and操作,可以获取CPSR.M[3:0]的值

                            这时候,如果中断发生在用户空间,lr=0,如果是内核空间,lr=3 
THUMB( adr    r0, 1f            )----根据当前PC值,获取lable 1的地址 
THUMB( ldr    lr, [r0, lr, lsl #2]  )-lr根据当前mode,要么是__irq_usr的地址 ,要么是__irq_svc的地址 
    mov    r0, sp------将irq mode的stack point通过r0传递给即将跳转的函数 
ARM(    ldr    lr, [pc, lr, lsl #2]    )---根据mode,给lr赋值,__irq_usr或者__irq_svc 
    movs    pc, lr            @ branch to handler in SVC mode-----(4) 
ENDPROC(vector_\name)

    .align    2 
    @ handler addresses follow this label 
1: 
    .endm

(1)我们期望在栈上保存发生中断时候的硬件现场(HW context),这里就包括ARM的core register。上一章我们已经了解到,当发生IRQ中断的时候,lr中保存了发生中断的PC+4,如果减去4的话,得到的就是发生中断那一点的PC值。

(2)当前是IRQ mode,SP_irq在初始化的时候已经设定(12个字节)。在irq mode的stack上,依次保存了发生中断那一点的r0值、PC值以及CPSR值(具体操作是通过spsr进行的,其实硬件已经帮我们保存了CPSR到SPSR中了)。为何要保存r0值?因为随后的代码要使用r0寄存器,因此我们要把r0放到栈上,只有这样才能完完全全恢复硬件现场。

(3)可怜的IRQ mode稍纵即逝,这段代码就是准备将ARM推送到SVC mode。如何准备?其实就是修改SPSR的值,SPSR不是CPSR,不会引起processor mode的切换(毕竟这一步只是准备而已)。

(4)很多异常处理的代码返回的时候都是使用了stack相关的操作,这里没有。“movs    pc, lr ”指令除了字面上意思(把lr的值付给pc),还有一个隐含的操作(movs中‘s’的含义):把SPSR copy到CPSR,从而实现了模式的切换。

2、当发生中断的时候,代码运行在用户空间

Interrupt dispatcher的代码如下:

vector_stub    irq, IRQ_MODE, 4 -----减去4,确保返回发生中断之后的那条指令

.long    __irq_usr            @  0  (USR_26 / USR_32)   <---------------------> base address + 0 
.long    __irq_invalid            @  1  (FIQ_26 / FIQ_32) 
.long    __irq_invalid            @  2  (IRQ_26 / IRQ_32) 
.long    __irq_svc            @  3  (SVC_26 / SVC_32)<---------------------> base address + 12 
.long    __irq_invalid            @  4 
.long    __irq_invalid            @  5 
.long    __irq_invalid            @  6 
.long    __irq_invalid            @  7 
.long    __irq_invalid            @  8 
.long    __irq_invalid            @  9 
.long    __irq_invalid            @  a 
.long    __irq_invalid            @  b 
.long    __irq_invalid            @  c 
.long    __irq_invalid            @  d 
.long    __irq_invalid            @  e 
.long    __irq_invalid            @  f

这其实就是一个lookup table,根据CPSR.M[3:0]的值进行跳转(参考上一节的代码:and    lr, lr, #0x0f)。因此,该lookup table共设定了16个入口,当然只有两项有效,分别对应user mode和svc mode的跳转地址。其他入口的__irq_invalid也是非常关键的,这保证了在其模式下发生了中断,系统可以捕获到这样的错误,为debug提供有用的信息。

    .align    5 
__irq_usr: 
    usr_entry---------请参考本章第一节(1)保存用户现场的描述 
    kuser_cmpxchg_check---和本文描述的内容无关,这些不就介绍了 
    irq_handler----------核心处理内容,请参考本章第二节的描述 
    get_thread_info tsk------tsk是r9,指向当前的thread info数据结构 
    mov    why, #0--------why是r8 
    b    ret_to_user_from_irq----中断返回,下一章会详细描述

(1)保存发生中断时候的现场。所谓保存现场其实就是把发生中断那一刻的硬件上下文(各个寄存器)保存在了SVC mode的stack上。

    .macro    usr_entry 
    sub    sp, sp, #S_FRAME_SIZE--------------A 
    stmib    sp, {r1 - r12} -------------------B

    ldmia    r0, {r3 - r5}--------------------C 
    add    r0, sp, #S_PC-------------------D 
    mov    r6, #-1----orig_r0的值

    str    r3, [sp] ----保存中断那一刻的r0


    stmia    r0, {r4 - r6}--------------------E 
    stmdb    r0, {sp, lr}^-------------------F 
    .endm

A:代码执行到这里的时候,ARM处理已经切换到了SVC mode。一旦进入SVC mode,ARM处理器看到的寄存器已经发生变化,这里的sp已经变成了sp_svc了。因此,后续的压栈操作都是压入了发生中断那一刻的进程的(或者内核线程)内核栈(svc mode栈)。具体保存多少个寄存器值?S_FRAME_SIZE已经给出了答案,这个值是18个寄存器。r0~r15再加上CPSR也只有17个而已。先保留这个疑问,我们稍后回答。

B:压栈首先压入了r1~r12,这里为何不处理r0?因为r0在irq mode切到svc mode的时候被污染了,不过,原始的r0被保存的irq mode的stack上了。r13(sp)和r14(lr)需要保存吗,当然需要,稍后再保存。执行到这里,内核栈的布局如下图所示:

ir1

stmib中的ib表示increment before,因此,在压入R1的时候,stack pointer会先增加4,重要是预留r0的位置。stmib    sp, {r1 - r12}指令中的sp没有“!”的修饰符,表示压栈完成后并不会真正更新stack pointer,因此sp保持原来的值。

C:注意,这里r0指向了irq stack,因此,r3是中断时候的r0值,r4是中断现场的PC值,r5是中断现场的CPSR值。

D:把r0赋值为S_PC的值。根据struct pt_regs的定义(这个数据结构反应了内核栈上的保存的寄存器的排列信息),从低地址到高地址依次为:

ARM_r0 
ARM_r1 
ARM_r2 
ARM_r3 
ARM_r4 
ARM_r5 
ARM_r6 
ARM_r7 
ARM_r8 
ARM_r9 
ARM_r10 
ARM_fp 
ARM_ip 
ARM_sp  
ARM_lr 
ARM_pc<---------add    r0, sp, #S_PC指令使得r0指向了这个位置 
ARM_cpsr 
ARM_ORIG_r0

为什么要给r0赋值?因此kernel不想修改sp的值,保持sp指向栈顶。

E:在内核栈上保存剩余的寄存器的值,根据代码,依次是r0,PC,CPSR和orig r0。执行到这里,内核栈的布局如下图所示:

ir2

R0,PC和CPSR来自IRQ mode的stack。实际上这段操作就是从irq stack就中断现场搬移到内核栈上。

F:内核栈上还有两个寄存器没有保持,分别是发生中断时候sp和lr这两个寄存器。这时候,r0指向了保存PC寄存器那个地址(add    r0, sp, #S_PC),stmdb    r0, {sp, lr}^中的“db”是decrement before,因此,将sp和lr压入stack中的剩余的两个位置。需要注意的是,我们保存的是发生中断那一刻(对于本节,这是当时user mode的sp和lr),指令中的“^”符号表示访问user mode的寄存器。

(2)核心处理

irq_handler的处理有两种配置。一种是配置了CONFIG_MULTI_IRQ_HANDLER。这种情况下,linux kernel允许run time设定irq handler。如果我们需要一个linux kernel image支持多个平台,这是就需要配置这个选项。另外一种是传统的linux的做法,irq_handler实际上就是arch_irq_handler_default,具体代码如下:

    .macro    irq_handler 
#ifdef CONFIG_MULTI_IRQ_HANDLER 
    ldr    r1, =handle_arch_irq 
    mov    r0, sp--------设定传递给machine定义的handle_arch_irq的参数 
    adr    lr, BSYM(9997f)----设定返回地址 
    ldr    pc, [r1] 
#else 
    arch_irq_handler_default 
#endif 
9997: 
    .endm

对于情况一,machine相关代码需要设定handle_arch_irq函数指针,这里的汇编指令只需要调用这个machine代码提供的irq handler即可(当然,要准备好参数传递和返回地址设定)。

情况二要稍微复杂一些(而且,看起来kernel中使用的越来越少),代码如下:

    .macro    arch_irq_handler_default 
    get_irqnr_preamble r6, lr 
1:    get_irqnr_and_base r0, r2, r6, lr 
    movne    r1, sp 
    @ 
    @ asm_do_IRQ 需要两个参数,一个是 irq number(保存在r0) 
    @                                          另一个是 struct pt_regs *(保存在r1中) 
    adrne    lr, BSYM(1b)-------返回地址设定为符号1,也就是说要不断的解析irq状态寄存器

                                       的内容,得到IRQ number,直到所有的irq number处理完毕 
    bne    asm_do_IRQ  
    .endm

这里的代码已经是和machine相关的代码了,我们这里只是简短描述一下。所谓machine相关也就是说和系统中的中断控制器相关了。get_irqnr_preamble是为中断处理做准备,有些平台根本不需要这个步骤,直接定义为空即可。get_irqnr_and_base 有四个参数,分别是:r0保存了本次解析的irq number,r2是irq状态寄存器的值,r6是irq controller的base address,lr是scratch register。

3、当发生中断的时候,代码运行在内核空间

如果中断发生在内核空间,代码会跳转到__irq_svc处执行:

    .align    5 
__irq_svc: 
    svc_entry----保存发生中断那一刻的现场保存在内核栈上 
    irq_handler ----具体的中断处理,同user mode的处理。

#ifdef CONFIG_PREEMPT--------和preempt相关的处理,本文不进行描述 
    get_thread_info tsk 
    ldr    r8, [tsk, #TI_PREEMPT]        @ get preempt count 
    ldr    r0, [tsk, #TI_FLAGS]        @ get flags 
    teq    r8, #0                @ if preempt count != 0 
    movne    r0, #0                @ force flags to 0 
    tst    r0, #_TIF_NEED_RESCHED 
    blne    svc_preempt 
#endif

    svc_exit r5, irq = 1            @ return from exception

保存现场的代码和user mode下的现场保存是类似的,因此这里不再详细描述,只是在下面的代码中内嵌一些注释。

    .macro    svc_entry, stack_hole=0 
    sub    sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)----sp指向struct pt_regs中r1的位置 
    stmia    sp, {r1 - r12} ------寄存器入栈。

    ldmia    r0, {r3 - r5} 
    add    r7, sp, #S_SP - 4 ------r7指向struct pt_regs中r12的位置 
    mov    r6, #-1 ----------orig r0设为-1 
    add    r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)----r2是发现中断那一刻stack的现场 
    str    r3, [sp, #-4]! ----保存r0,注意有一个!,sp会加上4,这时候sp就指向栈顶的r0位置了

    mov    r3, lr ----保存svc mode的lr到r3 
    stmia    r7, {r2 - r6} ---------压栈,在栈上形成形成struct pt_regs 
    .endm

五、中断退出过程

1、中断发生在user mode下的退出过程,代码如下:

ENTRY(ret_to_user_from_irq) 
    ldr    r1, [tsk, #TI_FLAGS] 
    tst    r1, #_TIF_WORK_MASK---------------A 
    bne    work_pending 
no_work_pending: 
    asm_trace_hardirqs_on ------和irq flag trace相关,暂且略过

    /* perform architecture specific actions before user return */ 
    arch_ret_to_user r1, lr----有些硬件平台需要在中断返回用户空间做一些特别处理 
    ct_user_enter save = 0 ----和trace context相关,暂且略过

    restore_user_regs fast = 0, offset = 0------------B 
ENDPROC(ret_to_user_from_irq) 
ENDPROC(ret_to_user)

A:thread_info中的flags成员中有一些low level的标识,如果这些标识设定了就需要进行一些特别的处理,这里检测的flag主要包括:

#define _TIF_WORK_MASK   (_TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME)

这三个flag分别表示是否需要调度、是否有信号处理、返回用户空间之前是否需要调用callback函数。只要有一个flag被设定了,程序就进入work_pending这个分支。

B:从字面的意思也可以看成,这部分的代码就是将进入中断的时候保存的现场(寄存器值)恢复到实际的ARM的各个寄存器中,从而完全返回到了中断发生的那一点。具体的代码如下:

    .macro    restore_user_regs, fast = 0, offset = 0 
    ldr    r1, [sp, #\offset + S_PSR] ----r1保存了pt_regs中的spsr,也就是发生中断时的CPSR 
    ldr    lr, [sp, #\offset + S_PC]!    ----lr保存了PC值,同时sp移动到了pt_regs中PC的位置 
    msr    spsr_cxsf, r1 ---------赋值给spsr,进行返回用户空间的准备 
    clrex                    @ clear the exclusive monitor 

    .if    \fast 
    ldmdb    sp, {r1 - lr}^            @ get calling r1 - lr 
    .else 
    ldmdb    sp, {r0 - lr}^ ------将保存在内核栈上的数据保存到用户态的r0~r14寄存器 
    .endif 
    mov    r0, r0   ---------NOP操作,ARMv5T之前的需要这个操作 
    add    sp, sp, #S_FRAME_SIZE - S_PC----现场已经恢复,移动svc mode的sp到原来的位置 
    movs    pc, lr               --------返回用户空间 
    .endm

2、中断发生在svc mode下的退出过程,代码如下:

    .macro    svc_exit, rpsr, irq = 0 
    .if    \irq != 0 
    @ IRQs already off 
    .else 
    @ IRQs off again before pulling preserved data off the stack 
    disable_irq_notrace 
    .endif 
    msr    spsr_cxsf, \rpsr-----将中断现场的cpsr值保存到spsr中,准备返回中断发生的现场 

    ldmia    sp, {r0 - pc}^ -----这条指令是ldm异常返回指令,这条指令除了字面上的操作,

                                                             还包括了将spsr copy到cpsr中。 
    .endm

2013-09-28 23:48:42 dingjian8086 阅读数 409

中断和异常

同步中断(异常):软件产生,只有执行了某条指令之后才会发生

异步中断(中断):硬件产生,随时可能发生

一、 中断信号的作用

中断是一个内核控制路径,它代表中断发生时的进程在内核中执行。

Ø 中断随时可能发生,因此内核处理中断要尽量的快,把更多的处理向后推迟:关键而紧急的部分内核立即执行;其余部分内核推迟到后续执行

Ø 中断程序必须可以嵌套执行

Ø 内核的临界区必须关中断执行

二、 中断和异常

Ø 中断

² 可屏蔽中断:一般的I/O设备中断

² 不可屏蔽中断:只有危急时间才是不可屏蔽中,如硬件故障

Ø 异常

² 故障:可以纠正,异常处理结束之后,引发故障的那条指令会重新执行

² 陷阱:

² 异常中止:

² 编程异常:

三、 中断和异常处理程序的嵌套执行

每个中断或者异常都会引发一个内核控制路径,代表当前进程在内核态执行单独的指令序列。

内核允许控制路劲嵌套执行,其代价是:中断处理程序永远不能被阻塞,即中断处理程序运行期间不能发生进程切换。因为中断处理程序是代表当前进程在内核执行特定的指令序列,因此,中断的一些数据是保存在当前进程的内核态栈中的,如果发生进程切换,后续要恢复中断继续执行就要等到被挂起的进程重新获得cpu,但是被挂起进程何时会获得CPU是不可而知的,这样中断可能会等待很久才会得到执行,这对中断处理来说是不可容忍的。

如果内核没有bug,则大多数的异常是在用户态发生的,缺页异常发生在内核态,在处理这个异常的时候,进程运行被挂起,用另外一个进程代替它,直到请求的页可用为止,只要被挂起的进程重新获得cpu,处理缺页异常的控制路径就恢复执行。

缺页异常不会进一步引发异常。

中断可以抢占另一个中断,也可以抢占异常;但是异常从不抢占中断,中断从不执行导致缺页的操作(缺页异常这就意味这进程切换)。

Linux交错执行内核控制路径:

1.提高中断控制器和设备控制器的吞吐量。PIC接受到一个设备控制器的中断信号之后,使二者保持阻塞,直到从CPU接受到一个应答,由于LINUX的中断是交错执行的,即使在执行其他中断的时候,CPU收到一个中断信号也会立即向PIC发生应答信号。

2.实现没有优先级的中断模型。因为每个中断都可以被另外一个中断抢占,因此就没有必要给每个设备预定义中断优先级了。

四、 异常处理

异常处理的标注过程:

Ø 在内核栈上保存大多数寄存器的值

Ø 用高级C程序处理异常

Ø 使用ret_from_exception()退出异常

五、 中断处理

Ø I/O中断处理

² IRQ共享

² IRQ动态分配

Linux把中断要执行的操作分三类:

² 紧急:如对中断控制器做出应答,这样的操作需要中断程序在第一时间完成,且是在关中断的前提下执行。

² 非紧急:如按下一个键后读扫描码,这个操作也需要中断处理程序立即完成,但是在开中断的情况下执行

² 非紧急可延迟:如把把缓存区的数据读入进程的地址空间,这些操作可能耗时很久,这个操作由独立的函数完成。

中断处理程序的四个步骤:

² 在内核栈中保存IRQ的值以及寄存器的内容

² 给中断控制器发送应答

² 执行共享该IRQ的所有设备的中断服务程序ISR

² 跳转到ret_from_intr()

Ø IRQ数据结构

² IRQ描述符irq_desc

struct irq_desc {

irq_flow_handler_t  handle_irq;

/*服务于中断控制器某个引脚的函数指针*/

......

void *handler_data;

/*中断控制器方法的数据指针*/

......

struct irqaction *action; /*IRQn的服务例程,组成一个单项链表*/

unsigned int status; /*IRQn的一组状态*/

unsigned int depth; /*IRQn被激活时值为0,禁止时为一个正数*/

unsigned int irq_count;/*IRQn发生了多少次中断*/

unsigned int irqs_unhandled;

spinlock_t lock;

const char *name;

}中断IRQ描述符

这些描述符组成一个数组struct irq_desc irq_desc[NR_IRQS];irq_desc[]数组。

² Irqaction描述符

struct irqaction {

irq_handler_t handler;/*指向一个I/O设备的中断服务例程*/

unsigned long flags;

/*描述IRQI/O设备的关系,SA_SHIRQ /SA_INTERRUPT /SA_SAMPLE_RANDOM,分别表示允许其他设备共享IRQ线、必须关中断执行、随机事件发生源*/

cpumask_t mask;

const char *name;/* I/O设备名*/

void *dev_id;

struct irqaction *next; /*链表下一个元素地址*/

int irq;  /*irq线*/

struct proc_dir_entry *dir;

};

Ø 中断处理

在汇编代码的irq_handler宏中会调用asm_do_IRQ()asm_do_IRQ()函数是中断处理程序C的代码的总入口。

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;//按照中断号得到中断描述符

if (irq >= NR_IRQS)

desc = &bad_irq_desc;

irq_enter();//增加中断处理程序嵌套计数器

desc_handle_irq(irq, desc);//最终调用__do_IRQ()函数,后边分析

irq_exit();//中断嵌套计数器递减,检查是否有可延迟函数在等待执行

set_irq_regs(old_regs);

}

fastcall unsigned int __do_IRQ(unsigned int irq)

{

struct irq_desc *desc = irq_desc + irq;

struct irqaction *action;

unsigned int status;

spin_lock(&desc->lock);

if (desc->chip->ack)

desc->chip->ack(irq);//给中断控制器发送应答

action = NULL;

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

action = desc->action;

status &= ~IRQ_PENDING; /*发完应答了*/

status |= IRQ_INPROGRESS; /* 我正打算来处理这个中断*/

}

desc->status = status;

if (unlikely(!action))

goto out;

for (;;) {

irqreturn_t action_ret;

spin_unlock(&desc->lock);

action_ret = handle_IRQ_event(irq, 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;//指向链表的下一个action元素

} while (action);

local_irq_disable();//关本地中断

|-----------------------------------------------------------------------------------------------------|

spin_lock(&desc->lock);

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

break;

desc->status &= ~IRQ_PENDING;

}

desc->status &= ~IRQ_INPROGRESS;/*中断处理结束*/

out:

desc->chip->end(irq);/*和中断控制器相关*/

spin_unlock(&desc->lock);

return 1;

}

挽救丢失的中断:enable_irq()函数强迫硬件将丢失的中断再产生一次。

Ø IRQ线动态分配

² request_irq

² setup_irq

² free_irq

六、 软中断、tasklet

内核将可以推迟执行的中断从中断处理程序中抽取抽出来,保持较短的中断响应时间。内核用两种方式实现延迟执行的中断处理:可延迟函数(软中断和tasklet)、通过工作队列执行的函数。

Softirq上实现tasklet,中断上下文:当前程序正在执行一个中断处理程序或可延迟函数。

软中断分配是静态的,tasklet的分配是动态的;即使类型相同的软中断可以在多个CPU上并发执行,不同类型的tasklet可以在多个CPU上并发执行,但是相同类型的tasklet不能并发执行,只能串行执行;所以softirq的函数必须是可重入的,tasklet不必可重入。

初始化、激活(下一轮调度中执行)、屏蔽(即使激活了也不执行)、执行,这四种可延迟函数操作。

Ø 软中断

软中断

下标(优先级)

说明

HI_SOFTIRQ

0

处理高优先级的tasklet

TIMER_SOFTIRQ

1

时钟中断相关的tasklet

NET_TX_SOFTIRQ

2

数据包传送到网卡

NET_RX_SOFTIRQ

3

从网卡读取数据包

SCSI_SOFTIRQ

4

SCSI命令的后台中断处理

TASKLET_SOFTIRQ

5

处理常规tasklet

² 软中断的数据结构

static  struct softirq_action  softirq_vec[32];

struct softirq_action

{

void (*action)(struct softirq_action *);//指向软中断的函数指针

void *data;//软中断需要的数据

};

一个包含32softirq_action元素的softirq_vec[32]数组,数组只有前6个元素被使用,数组的下标对应软中断的优先级。

进程描述符的thread_info字段中的preempt_count字段编码表示三个不同计数器。

preempt_count字段含义

说明

Bit0-7

抢占计数器(禁用内核抢占的次数,0表示运行抢占)

Bit8-15

软中断计算器(可延迟函数被禁用的程度,0表示激活)

Bit16-27

硬中断计数器(本地CPU中断嵌套层数,irq_enter增加,irq_exit减少)

Bit28

PREEMPT_ACTIVE标志

² 处理软中断

open_softirq()处理软中断的初始化,raise_softirq()激活软中断,激活软中断的实际操作就是唤醒ksoftirqd内核进程,内核进程来执行所要延迟的操作。

void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)

{

softirq_vec[nr].data = data;

softirq_vec[nr].action = action;

}

open_softirq()主要就是用所带的软中断号以及软中断函数指针以及函数所需的数据来初始化对应的softirq_vecp[]数组元素。

void fastcall raise_softirq(unsigned int nr)

{

unsigned long flags;

local_irq_save(flags);

raise_softirq_irqoff(nr);

/*最终调用wake_up_process(ksoftirqd) 来唤醒ksoftirqd内核进程*/

local_irq_restore(flags);

}

内核需要周期性的检查挂起/活动的软中断,比如do_IRQ()完成中断处理之后或者调用irq_exit()

² do_softirq

asmlinkage void do_softirq(void)

{

__u32 pending;

unsigned long flags;

if (in_interrupt())

//检查preempt_count字段的0-15bit,为正数说明已经禁用软中断

return;

local_irq_save(flags);//保存IF标志,并禁中断

pending = local_softirq_pending();

if (pending)

__do_softirq();

local_irq_restore(flags);//恢复IF标志

}

² __do_softirq

执行一个软中断的时候可能出现新挂起的软中断,为保证软中断的低延迟性,__do_softirq一直循环运行直到执行完所有的软中断,但是这样可能使得__do_softirq执行较长的时间,用户态程序得不到运行,因此__do_softirq只执行一定次数的循环后就返回,其余挂起的软中断会在不久之后的ksoftirqd内核线程中得到执行。

asmlinkage void __do_softirq(void)

{

struct softirq_action *h;

__u32 pending;

int max_restart = MAX_SOFTIRQ_RESTART;/*循环次数设置为10*/

int cpu;

pending = local_softirq_pending();/*获得本地软中断位掩码*/

__local_bh_disable((unsigned long)__builtin_return_address(0));

/*增加preempt_count字段中软中断计数器的值,即禁用软中断,因为软中断大多在开中的情况下执行,在执行期间可能会产生新的中断,在do_IRQ执行irq_exit的时候可能另一个__do_softirq函数的实例开始执行,可延迟函数应该以串行的方式执行,所以要避免并发的发生,在这里禁用软中断,可使得新的函数实例在运行的第一步就退出*/

cpu = smp_processor_id();

restart:

/* Reset the pending bitmask before enabling irqs */

set_softirq_pending(0);

/*清除本地CPU软中断的位图,以便可以接受新的软中断*/

local_irq_enable();/*开中断*/

h = softirq_vec;/*把软中断数组地址保存在局部变量h*/

do {

if (pending & 1) {

/*pending的最低位开始循环检查位图,即由高优先级到低优先级*/

h->action(h);/*如果该为存在被挂起的软中断则执行它的函数*/

rcu_bh_qsctr_inc(cpu);

}

h++;/*h指向数组的下一个元素*/

pending >>= 1;

/*把位图的bit1移动到bit0,以便在下次循环的开始处if (pending & 1) 做判断*/

} while (pending);

local_irq_disable();

/*关本地中断,因为后边要访问CPU软中断掩码,如果中断开启的情况下,新来的中断可能会对位掩码作写操作,这样就会产生访问冲突*/

pending = local_softirq_pending();

/*读取CPU软中断的位掩码,因为在前面开中断执行的期间可能会产生新的中断,中断处理程序又会挂起新的软中断,在结束一次扫描之后再次检查是否有挂起的软中断需要执行,这样软中断可以得到快速的执行*/

if (pending && --max_restart)

goto restart;

/*如果位掩码为0,或者达到执行循环的次数就不再继续执行软中断,因为这样会耗时较多,阻碍了用户态的程序的执行*/

if (pending)

wakeup_softirqd();

/*如果达到了执行循环的最大次数,而不得不放弃继续执行软中断的话,还有最后一招,那就是唤醒ksoftirqd内核进程,让内核进程替代我们来执行未及时处理的软中断,在不就之后,ksoftirqd得到了CPU就会帮助我们来完成未做完的事情*/

_local_bh_enable();/*软中断计数器减一,重新几乎可延迟函数*/

}

² ksoftirqd内核线程

for(;;)

{

set_current_state(TASK_INTERRUPTIBLE);

schedule();

while(local_softirq_pending())

{

preempt_disable();

do_softirq();

preempt_enable();

cond_resched();

}

}

获取软中断掩码,调用do_softirq执行软中断处理,如果没有需要处理的软中断则把进程状态改为TASK_INTERRUPTIBLE,随后调用cond_resched()实现进程切换。

Ksoftirqd内核线程提供了一种平衡的解决方法: do_softirq确定哪些软中断是挂起的并执行它们,执行期间中断是开着的,因此执行期间会由中断挂起新的软中断,但是do_softirq执行最多10次的检查新的挂起软中断,超过10次还有软中断的话就唤醒内核进程ksoftirqd。内核进程有着较低的优先级,这样用户程序就有机会得到调度,如果机器空闲,软中断就会得到很快的执行。

² tasklet

驱动程序中实现可延迟函数的首选方法。它是建立在HI_SOFTIRQ、 TASKLET_SOFTIRQ的基础之上的。几个tasklet可以与一个软中断相联。

struct tasklet_struct

{

struct tasklet_struct  *next;/*链表下一个元素*/

unsigned long  state;

/*tasklet的状态,TASKLET_STATE_SCHEDTASKLET_STATE_RUN,前者表示tasklet被挂起在某个链表中;后者表示正在执行*/

atomic_t  count;/*锁计数器*/

void  (*func)(unsigned long);/*tasklet的函数指针*/

unsigned long  data;/*tasklet函数所使用,可以当做指针等*/

};tasklet描述符

struct tasklet_head

{

struct tasklet_struct *list;

};

struct tasklet_head tasklet_vec[NR_CPUS]

struct tasklet_head tasklet_hi_vec[NR_CPUS]

每个cpu都在tasklet_vec[]数组和tasklet_hi_vec[]数组中各有自己的一个一个元素,这两个数组中的元素就是某个CPUtasklet的链表头。

void tasklet_init(struct tasklet_struct *t,

  void (*func)(unsigned long), unsigned long data)

{

t->next = NULL;

t->state = 0;

atomic_set(&t->count, 0);

t->func = func;

t->data = data;

}

tasklet_init()用于初始化一个tasklet_struct结构。tasklet_disable_nosync()tasklet_disable()用于禁止tasklet(增加count字段),前者立即返回,后者要等到tasklet的实例运行结束才返回。tasklet_enable()激活tasklet

static inline void tasklet_schedule(struct tasklet_struct *t)

{

unsigned long flags;

if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))

{

local_irq_save(flags);/*保存IF标志*/

t->next = __get_cpu_var(tasklet_vec).list;

__get_cpu_var(tasklet_vec).list = t;

/*t插入到链表首位置*/

raise_softirq_irqoff(TASKLET_SOFTIRQ);

/*唤醒TASKLET_SOFTIRQ 软中断*/

local_irq_restore(flags);/*恢复IF标志*/

}

}

tasklet_schedule(),tasklet_hi_schedule()就是将tasklet_struct结构体插入到相应的链表中,并唤醒对应的软中断TASKLET_SOFTIRQ或者HI_SOFTIRQ

void __init softirq_init(void)

{

open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);

open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);

}

该函数把tasklet_action,tasklet_hi_action分别赋值给TASKLET_SOFTIRQHI_SOFTIRQ软中断的action字段,因此这两个软中断在执行的时候就会执行它们所挂载的tasklet,下面具体分析一下如何指向tasklet

static void tasklet_action(struct softirq_action *a)

{

struct tasklet_struct *list;

local_irq_disable();

list = __get_cpu_var(tasklet_vec).list;/*获得链表头存放在局部变量list*/

__get_cpu_var(tasklet_vec).list = NULL;

/*链表头清零,这样在tasklet执行期间新来的中断可以重新构建链表*/

local_irq_enable();/*开中断*/

while (list) {/*tasklet链表做遍历*/

struct tasklet_struct *t = list;

list = list->next;/*指向链表下个元素*/ 

if (tasklet_trylock(t)) {

if (!atomic_read(&t->count)) {

if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))

BUG();

t->func(t->data);

tasklet_unlock(t);

continue;/*跳出本次循环,执行下一次循环*/

}

tasklet_unlock(t);

}

local_irq_disable();

t->next = __get_cpu_var(tasklet_vec).list;

__get_cpu_var(tasklet_vec).list = t;

__raise_softirq_irqoff(TASKLET_SOFTIRQ);

local_irq_enable();

}

}

七、 工作队列

可延迟函数执行在中断上下文,工作队列工作在进程上下文(工作者线程worker thread);中断上下文不能发生进程切换,所以可延迟函数不能执行阻塞操作,工作队列的函数可以执行导致阻塞的操作;二者皆不可以访问用户空间。

Ø 工作队列的数据结构

workqueue_struct, cpu_ workqueue_struct, work_struct三个数据结构的关系图。

Ø 工作队列函数

create_workqueue(“foo”)创建工作队列,返回新创建的工作队列的描述符workqueue_struct的地址。并创建N个工作者内核线程,NCPU的个数。destroy_workqueue()销毁工作队列。

int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)

{

int ret = 0;

if (!test_and_set_bit(WORK_STRUCT_PENDING, work_data_bits(work))) {

BUG_ON(!list_empty(&work->entry));

__queue_work(wq_per_cpu(wq, get_cpu()), work);

put_cpu();

ret = 1;

}

return ret;

}

/*work_struct加入到链表,首先检查该work是不是已经在链表中了,如果不在就插入到链表,如果工作者进程睡眠则唤醒它*/

工作者进程的在worker_thread()函数内执行循环操作,进程一旦醒来就调用run_workqueue(),删除工作链表中删除所有的work_struct描述符并执行相应的挂起函数(work_struct内的func函数指针)。

Ø 预定义工作队列

内核预定义了一个叫做events的预定义工作队列,内核开发者可以随意使用,它的workqueue_struct描述符存放在keventd_wq数组中。

2011-10-17 19:56:06 zwj0403 阅读数 822

中断机制的核心数据结构是 irq_desc,它完整地描述了一条中断线(或称为“中断通道” ) 。

irq_desc 结构在 include/linux/irq.h 中定义在 kernel/irq/handle.c 中

有个全局 irq_desc 数组,描述了系统中所有的中断线:
handle_irq,这是个函数指针,指向的是一个高层次的中断事件处理函数,定义了处理中断事件的一种策略。
在 kernel/irq/chip.c 中实现了 5 个函数:handle_simple_irq(),handle_level_irq(),
handle_irq 指针可handle_edge_irq(),handle_fasteoi_irq()以及 handle_percpu_irq()。
以指向这 5 个函数中的一个,选择一种中断事件处理策略,这是通过函数 set_irq_handler()完成的。



接下来是 chip,   这是个 irq_chip 结构指针。irq_chip 结构定义了对中断线的底层硬件操作,
在 include/linux/irq.h 中:


最后的数据成员是 action,这是个 irq_action 结构指针。 irq_action 结构定义了安装在中
断线上的一个中断处理程序,在 include/linux/interrupt.h 中


init/main.c
asmlinkage void __init start_kernel(void)
{
......
584   setup_arch(&command_line);//{750   init_arch_irq = mdesc->init_irq;} 对627行调用的init_arch_irq函数指针赋值。
......
/* 重定位中断向量表,修改中断向量表占用页面的访问权限 */
606   trap_init();
....
/* 这个函数首先把irq_desc数组中的所有元素的status设为IRQ_NOREQUEST|IRQ_NOPROBE(没有请求,没有检测),然后调用init_arch_irq()*/
627   init_IRQ();//{init_arch_irq();...}

}
init_arch_irq()在setup_arch()中被赋值,指向machine_desc中定义的init_irq函数指针。AT91SAM9260EK 平台上,就指向了 arch/arm/mach-at91/board-sam9260ek.c 中定义的函数ek_init_irq()。ek_init_irq() 又 调 用 了 arch/arm/mach-at91/at91sam9260.c中的at91sam9260_init_interrupts()。(这个machine_desc的赋值在arch/arm/mach-at91/board-sam9260ek.c最后几行中MACHINE_START(AT91SAM9260EK, "Atmel AT91SAM9260-EK").....其中就有.init_irq = ek_init_irq。)
接下来 arch/arm/mach-at91/irq.c 中 的 at91_aic_init()进行AIC初始化,调用arch/arm/mach-at91/gpio.c 中的 at91_gpio_irq_setup()进行GPIO的中断初始化。
arch/arm/mach-at91/irq.c
static struct irq_chip at91_aic_chip = {
    .name   = "AIC",
    .ack    = at91_aic_mask_irq,
    .mask   = at91_aic_mask_irq,
    .unmask   = at91_aic_unmask_irq,
    .set_type = at91_aic_set_type,                                                                      
.>  .set_wake = at91_aic_set_wake,
  };
132 void __init at91_aic_init(unsigned int priority[NR_AIC_IRQS])
这个函数主要是写了 AIC 的一些寄存器,还为每个中断线设置了 chip(指向at91_aic_chip) 和 handle_irq ( 指 向 handle_level_irq ) 然 后 把 中 断 线 的 状 态 修 改 为IRQF_VALID | IRQF_PROBE(已有效&以检测),at91_aic_chip 定义的底层操作函数也在arch/arm/mach-at91/irq.c 中,不再详述。
至此,中断机制的初始化工作已完成。

没有更多推荐了,返回首页