Linux中断系统介绍及底层中断处理源代码分析(一)

1.中断介绍

中断是指CPU在正常执行指令过程中,出现了突发且紧急的事件,CPU必须暂时停止执行当前的程序,跳转到处理突发事件的指令处执行,处理完毕后返回到原来的指令处继续执行。根据中断的来源,中断可分为内部中断和外部中断,内部中断来源于CPU内部,如软件中断指令、除法错误、溢出等,外部中断来自于CPU外部,由外设产生,如串口收到数据产生中断、定时器到期产生中断。根据中断入口跳转方法可分为向量中断和非向量中断,CPU通常为向量中断分配不通的中断号,中断到来后,就自动跳转到该中断号对应的地址处执行,非向量中断共享一个入口地址,进入该地址后,通过判断中断标志确定中断源。

2.Linux中断框架介绍

中断服务程序是在关中断的状态下执行,如果中断服务程序执行的时间过长,会使其他进程得不到调度执行,系统不能响应其他中断,导致系统的实时性和性能降低。为了解决这个问题,Linux将中断服务程序分为两部分,即顶半部分(Top Half)和底半部分(Bottom Half)。顶半部分是在关中断的状态下进行,此阶段不能响应中断,顶半部分用于完成尽量短且比较紧急的工作,如获取中断状态、登记中断、清除中断状态,登记的中断在底半部分处理。底半部分是在开中断的状态下进行,此阶段可以响应中断,底半部分用来处理顶半部分登记的中断,主要是一些复杂且耗时的工作,如拷贝数据、解析数据等工作。当然,中断处理是否一定要分为顶半部分和底半部分,需要根据实际的中断任务确定,对于一些简单的任务,可以直接在顶半部分完成。
Linux内核支持众多的处理器体系结构,中断子系统非常复杂,但根据其功能,可大致分为四部分:
(1)硬件层,例如CPU和中断控制器的连接,硬件中断号,中断触发方式等
(2)处理器的异常处理模式,例如CPU如何处理不同异常(中断也是一种异常),如何进入异常和如何退出异常
(3)中断控制器的管理,例如硬件中断号和软件中断号的映射,中断控制器的操作
(4)Linux内核通用中断处理层,此部分屏蔽了硬件之间的差异,提供了通用的中断处理的框架,向上提供中断处理的统一接口,如注册和注销中断等,向下兼容所有体系结构。

3.ARM处理器异常处理模式

ARMv7-A and ARMv7-R架构支持9种处理器运行模式,其中6种为特权模式,1种为非特权模式,剩下2种和虚拟化、安全相关。特权模式分别为FIQ(快速中断)、IRQ(中断)、Supervisor、Abort、Undefined、System,操作系统通常运行在Supervisor模式下。非特权模式为User mode,即用户模式,应用程序就在此模式下运行。特权模式中除了System,其他模式又称为异常模式,中断也属于异常的一种。ARM架构处理器运行模式可参考《Cortex-A Series Programmer’s Guide》第11节。
ARM处理器对中断的响应过程如下:
(1)CPU自动保存处理器状态,即将中断发生时的CPSR寄存器内容保存到SPSR_irq寄存器中
(2)CPU自动设置当前程序状态寄存器CPSR的低5位,使处理器进入特权模式中的IRQ模式(Linux不使用FIQ,只使用IRQ)
(3)硬件自动关闭IRQ中断
(4)将返回地址(PC)自动保存到LR_irq寄存器中
(5)CPU自动的将程序计数器PC设置成异常中断向量表中的地址,进入相应的异常处理程序中处理中断
(6)处理完中断后,恢复中断发生前的处理器状态,即将SPSR_irq寄存器中的数据复制到CPSR寄存器中
(7)设置程序计数器PC,使其指向中断发生前要执行的指令,即将LR_irq寄存器中的数据复制到PC中
异常处理程序的入口地址保存在中断向量表中,如下图所示。中断向量表正常情况下位于0x0-0x1C地址处,处于内存中的低地址。也有可能位于高地址处,即0xFFFF0000-0xFFFF001C处,Linux中断向量表通常位于内存的高地址处,因为0-3G为用户空间,3-4G为内核空间,中断向量表属于内核空间。我们关心的IRQ中断向量的地址为0x1C或者0xFFFF001C。需要注意的是上述地址都为虚拟地址。

arm_exception_behavior
arm处理器内部的寄存器数量相比x86架构的处理器要多一些,某些cpu模式还拥有专用的寄存器,arm处理器寄存器和处理器模式的对应关系如下图所示。所有模式共用PC和CPSR;除了FIQ模式,所有模式共用R0-R12寄存器,专用的寄存器有SP、LR及SPSR;FIQ模式下有专用的R8-R12寄存器。
arm_system_level_view

4.Linux内核底层中断处理源代码分析

此节侧重分析底层的中断处理机制,如中断向量表的作用、中断是如何跳转到中断处理程序的入口处、中断上下文的保存和恢复等。大部分代码都是汇编代码,学习之前,建议熟悉一下arm架构的汇编代码。

4.1.中断向量表

4.1.1.中断向量表的定义

Linux的中断向量表中存放的是一系列的跳转指令,用于跳转到异常处理程序的入口处。这里重点关注IRQ中断向量。vector_irq是通过宏定义的标签,由vector_stub定义而来。

    [arch/arm/kernel/entry-armv.S]
        .section .vectors, "ax", %progbits  @ 说明中断向量表的段及属性,ax表示可分配、可执行
    .L__vectors_start:  @ 中断向量表开始
        W(b)	vector_rst  @ 复位
        W(b)	vector_und  @ 未定义
        W(ldr)	pc, .L__vectors_start + 0x1000  @ 特权指令(svc、swi)入口
        W(b)	vector_pabt  @ prefetch abort
        W(b)	vector_dabt  @ data abort 
        W(b)	vector_addrexcptn @ 地址异常
        W(b)	vector_irq   @ 中断
        W(b)	vector_fiq   @ 快速中断
4.1.2.中断向量表stubs段

stubs段起到分发中断的作用,使不同的中断跳转到不同的处理程序中去,如中断发生在usr模式,将会跳转到__irq_usr中处理,如中断发生在svc模式,将会跳转到__irq_svc中处理。.macro表示宏,vector_stub是宏的名称,namemodecorrection为参数。correction的默认值为4。将vector_stub irq, IRQ_MODE, 4宏定义展开,则nameirqmode0x00000012correction4

    [arch/arm/kernel/entry-armv.S]
        .macro	vector_stub, name, mode, correction=0  @ 宏定义开始
        .align	5
    vector_\name:  @ \表示使用宏的参数,替换后得到vector_irq,可见中断向量表中的irq跳转指令是跳转到此处
        .if \correction  @ correction为4,条件成立
        @ 将lr寄存器数据减4并保存到lr中,lr中保存的是中断返回的地址,这里减4是由于流水线的原因,发生中断的
        @ 时候pc还未更新,指向了中断发生前执行指令后面的第二条指令,即中断发生前执行指令加8个字节的位置。中
        @ 断返回后要执行中断发生前执行指令后面的第一条指令,则需要将lr减4。
        sub	lr, lr, #\correction
        .endif
        @ 将r0和lr保存到irq模式的栈中,lr是中断返回的地址
        stmia	sp, {r0, lr}		@ save r0, lr
        @ 将spsr保存到lr中
        mrs	lr, spsr
        @ 将spsr_irq保存到栈中,此时的spsr为发生中断时的cpsr
        str	lr, [sp, #8]		@ save spsr

        @ 将cpsr保存到r0中
        mrs	r0, cpsr
        @ 将r0最后一位设置为10x12为irq模式,0x13为svc模式
        eor	r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
        @ 将r0保存到spsr中,此时spsr中的低5位为0x13,表示svc模式,此时处理器还处于irq模式,还未切换到svc模式
        msr	spsr_cxsf, r0
        @ 获取中断发生时cpsr寄存器的低4位,如果低4位为0表示中断发生时cpu处于usr模式(用户空间),
        @ 如低4位为3,则表示中断发生时cpu处于svc模式(内核空间)
        and	lr, lr, #0x0f
        @ 获取标号1的地址,此地址为相对地址,是当前pc的偏移地址
    THUMB(	adr	r0, 1f			)
    @ 将表号1的地址和cpsr低四位相加,得到跳转的相对偏移地址,中断发生在usr模式则跳转到__irq_usr,中断发生在svc模式,
    @ 则跳转到__irq_svc
    THUMB(	ldr	lr, [r0, lr, lsl #2]	) @ lsl为左移指令,左移两位,保证地址4字节对齐
        mov	r0, sp  @ 栈指针保存在r0中
    @ 将偏移地址和pc相加存放到lr寄存器中
    ARM(	ldr	lr, [pc, lr, lsl #2]	)
    @ 将lr中的地址复制到pc中,跳转到__irq_usr或__irq_svc处,同时将spsr的值复制到cpsr,将处理器模式切换到svc模式
        movs	pc, lr			@ branch to handler in SVC mode
    ENDPROC(vector_\name)

        .align	2
        @ handler addresses follow this label
    1:
        .endm    @ 宏定义结束

    	.section .stubs, "ax", %progbits  @ stubs段属性

        vector_stub	irq, IRQ_MODE, 4    @ irq中断服务程序的跳转入口
        @  当中断发生在usr模式时,跳转到__irq_usr处理中断
        .long	__irq_usr			    @  0  (USR_26 / USR_32)
        .long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
        .long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
        @ 当中断发生在svc模式时,跳转到__irq_svc处理中断
        .long	__irq_svc			    @  3  (SVC_26 / SVC_32)
        .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

从上面的分析可以看出,中断向量表存放了一系列跳转指令,将不同的异常分发到对应的异常处理程序中。如当中断irq发生在usr模式时,中断在__irq_usr中被处理,当中断irq发生在svc模式时,中断在__irq_svc中被处理。中断发生后,CPU将很快从irq模式切换到svc模式,处于irq模式的时间很短。cpu在irq模式中将中断发生时的r0寄存器、lr寄存器、cpsr寄存器保存到了irq模式的栈中,后续cpu切换到svc模式,需要读取这三个值,irq模式的栈地址通过r0寄存器传递到svc模式。

4.1.3.中断向量表的初始化

内核编译时,将中断向量表存放在镜像文件的init段中,具体内容可见vmlinux.lds.S链接脚本,内容如下。__vectors_start和__vectors_end是中断向量表开始、结束的地址,中断向量表开始的地址为0xffff0000。__stubs_start和__stubs_end是中断向量stubs段开始地址和结束地址,__stubs_start的地址位于__vectors_start加0x1000处,也即向上偏移一个page处。可以看出,中断向量表和中断向量stubs段各占一个page,这样安排的好处是在中断向量表中可以使用分支跳转指令。

[arch/arm/kernel/vmlinux.lds.S]
// 下面按PAGE_SIZE对齐
. = ALIGN(PAGE_SIZE);
// 表示init段开始
__init_begin = .;
/*
    * The vectors and stubs are relocatable code, and the
    * only thing that matters is their relative offsets
    */
__vectors_start = .;  // 中断向量表开始存放,开始地址为0xffff0000
.vectors 0xffff0000 : AT(__vectors_start) {
    *(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .;  // 中断向量表结束

__stubs_start = .;  // 中断向量stubs段开始存放,开始地址为0xffff1000
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
    *(.stubs)
}
. = __stubs_start + SIZEOF(.stubs);
__stubs_end = .  // 中断向量stubs段结束

中断向量表的内存分配及初始化在devicemaps_init函数中,下面分析该函数。

    [arch/arm/mm/mmu.c]
    paging_init  // 内核启动期间调用,用来初始化页表
      ->devicemaps_init
        ->early_alloc(PAGE_SIZE * 2)    // 分配两个页表
        ->early_trap_init
          vectors_page = vectors_base  //  全局变量vectors_page保存分配的页表地址
          // 第一个页表填充未定义指令0xe7fddef1,作用是当cpu从中断向量表外取指令时,可以捕捉未定义指令异常
          for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)  
		        ((u32 *)vectors_base)[i] = 0xe7fddef1
          // 将中断向量表拷贝到分配的第一个页表中
          ->memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start)
          // 将中断向量表stubs段拷贝到分配的第二个页表中
          ->memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start)
        // 将中断向量表的开始地址转换为页表的编号,virt_to_phys将vectors转换为物理地址
        map.pfn = __phys_to_pfn(virt_to_phys(vectors))
        // 设置中断向量表映射的虚拟地址为0xffff0000
        map.virtual = 0xffff0000
        // 设置中断向量表映射的虚拟地址大小为一页
        map.length = PAGE_SIZE
        // 映射中断向量表
        ->create_mapping
        // 如果需要映射到低地址处
        if (!vectors_high()) {
            map.virtual = 0;
            map.length = PAGE_SIZE * 2;
            map.type = MT_LOW_VECTORS;
            create_mapping(&map);
        }
        // 映射中断向量表stubs段
        map.pfn += 1;
	    map.virtual = 0xffff0000 + PAGE_SIZE;
	    map.length = PAGE_SIZE;
	    map.type = MT_LOW_VECTORS;
        ->create_mapping

4.3.异常栈的初始化

cpu在初始化的时候,需要初始化异常栈,即cpu切换到irq、abt、und、fiq模式下需要使用的栈。上述4中异常模式的栈来自于静态定义的struct stack结构体,每种异常的栈的大小为12字节。异常栈的初始化在cpu_init函数中,将栈的地址保存到不同异常模式下的sp寄存器中。

    [arch/arm/kernel/setup.c]
    static struct stack stacks[NR_CPUS];
    struct stack {
        u32 irq[3];  // irq模式的栈
        u32 abt[3];  // abt模式的栈
        u32 und[3];  // und模式的栈
        u32 fiq[3];  // fiq模式的栈
    } ____cacheline_aligned;
    cpu_init
      ->smp_processor_id    // 获取当前cpu的ID
      ->struct stack *stk = &stacks[cpu]    // 获取当前cpu的irq、abt、und、fiq模式下的栈指针
      // 设置当前cpu的per_cpu变量的偏移值,per_cpu_offset获取当前cpu的per_cpu变量的偏移值
      ->set_my_cpu_offset(per_cpu_offset(cpu))
          // 使用mcr指令将偏移值保存到TPIDRPRW寄存器中,TPIDRPRW的偏移值会一直保持,可加快per_cpu变量的访问速度
          asm volatile("mcr p15, 0, %0, c13, c0, 4" : : "r" (off) : "memory");
        // 下面开始设置栈,非THUMB指令,PLC被定义为"I",表示常数,cpsr_c表示CPSR寄存器的低8位
        __asm__ (
        "msr	cpsr_c, %1\n\t"    @ 进入irq模式,同时屏蔽irq和fiq中断
        "add	r14, %0, %2\n\t"   @ 将irq数组的首地址保存到r14中
        "mov	sp, r14\n\t"       @ 设置irq模式下的栈指针指向struct stack结构体中的irq数组
        "msr	cpsr_c, %3\n\t"    @ 进入abt模式,同时屏蔽irq和fiq中断
        "add	r14, %0, %4\n\t"   @ 将abt数组的首地址保存到r14中
        "mov	sp, r14\n\t"       @ 设置abt模式下的栈指针指向struct stack结构体中的abt数组
        "msr	cpsr_c, %5\n\t"    @ 进入und模式,同时屏蔽irq和fiq中断
        "add	r14, %0, %6\n\t"   @ 将und数组的首地址保存到r14中
        "mov	sp, r14\n\t"       @ 设置und模式下的栈指针指向struct stack结构体中的und数组
        "msr	cpsr_c, %7\n\t"    @ 进入fiq模式,同时屏蔽irq和fiq中断
        "add	r14, %0, %8\n\t"   @ 将fiq数组的首地址保存到r14中
        "mov	sp, r14\n\t"       @ 设置fiq模式下的栈指针指向struct stack结构体中的und数组
        "msr	cpsr_c, %9"        @ 将cpu的模式恢复为svc模式,同时屏蔽irq和fiq中断
            :  @ 输出部
            : "r" (stk), @ 输入部,"r"表示任何寄存器,    %0
            @ (0x00000040 | 0x00000080 | 0x00000012= 0x000000D2,中断模式,屏蔽irq和fiq
            PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE), @ %1
            "I" (offsetof(struct stack, irq[0])),   @ %2 获取struct stack中irq数组的首地址
            PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE), @ %3
            "I" (offsetof(struct stack, abt[0])),   @ %4
            PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE), @ %5
            "I" (offsetof(struct stack, und[0])),   @ %6
            PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE), @ %7
            "I" (offsetof(struct stack, fiq[0])),   @ %8
            PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)  @ %9
            : "r14");  @ 损坏部

4.3.底层的中断的处理过程

Linux对irq中断的处理根据中断发生时cpu所处的模式有所不同,中断发生时cpu处于usr模式时,会跳转到__irq_usr,中断发生时cpu处于svc模式时,会跳转到__irq_svc__irq_usr__irq_svc都是跳转到irq_handler处理中断,只有进入中断和退出中断部分有所不同。
首先看__irq_usrusr_entry将中断上下文保存到当前进程的内核栈中;irq_handler是真正的中断处理函数;处理完中断会检查当前进程是否有挂起的工作要做,如有则会执行;ret_to_user_from_irq恢复中断上下文,退出中断。

    [arch/arm/kernel/entry-armv.S]
    @ 当中断发生在usr模式时,跳转到__irq_usr处理中断
    __irq_usr:
        usr_entry    @ 进入中断,完成中断上下文的保存
        kuser_cmpxchg_check
        irq_handler  @处理中断
        get_thread_info tsk  @ 获取线程的thread_info结构体地址,并将地址保存到r9中
        mov	why, #0          @ why为r8寄存器
        b	ret_to_user_from_irq    @ 退出中断,将中断上下文恢复
    ENDPROC(__irq_usr)  @ __irq_usr标号作用域结束

get_thread_info tsk通过将sp的低13位清0获取被中断线程的thread_info结构体地址,并将地址保存到r9寄存器中。

    [include/linux/sched.h]
    // thread_info和线程的栈定义在一个联合体中,总共8KB,栈在8KB的高地址处,thread_info在8KB的低地址处
    union thread_union {
        struct thread_info thread_info;  // 线程信息,通过此结构体可找到该线程的task_struct和栈地址
        unsigned long stack[THREAD_SIZE/sizeof(long)];  // 栈
    };

    [arch/arm/kernel/entry-header.S]
    why	.req	r8		@ Linux syscall (!= 0),why为r8
    tsk	.req	r9		@ current thread_info,tsk为r9,

    [arch/arm/include/asm/thread_info.h]
    #define THREAD_SIZE_ORDER	1
    [arch/arm/include/asm/page.h]
    #define PAGE_SHIFT		12

    [arch/arm/include/asm/assembler.h]
    .macro	get_thread_info, rd  @ get_thread_info为宏定义,下面为获取thread_info结构体地址的代码
    @ lsr逻辑右移,高位补0,sp右移13位并将结果保存到r9寄存器中
    ARM(	mov	\rd, sp, lsr #THREAD_SIZE_ORDER + PAGE_SHIFT	)
    @ lsl逻辑左移,低位补0,将r9左移13位,低位补0,这样就把sp的低13位清0213次方为8KB,正好获得了
    @ thread_union的基地址,也即thread_info的地址
    mov	\rd, \rd, lsl #THREAD_SIZE_ORDER + PAGE_SHIFT
    .endm

接着看__irq_svcsvc_entry将中断上下文保存到内核栈中;irq_handler是真正的中断处理函数;如果开启了内核抢占,则会检查当前内核是否可以被抢占,如果可以被抢占,则会执行抢占;svc_exit恢复中断上下文,从中断中退出。

    [arch/arm/kernel/entry-armv.S]
    @ 当中断发生在svc模式时,跳转到__irq_svc处理中断
    __irq_svc:
    svc_entry
    irq_handler
    #ifdef CONFIG_PREEMPT
        get_thread_info tsk   @ 获取线程的thread_info结构体地址,并将地址保存到r9中
        @ 将thread_info结构体中preempt_count变量保存到r8中
        ldr	r8, [tsk, #TI_PREEMPT]		@ get preempt count
        @ 将thread_info结构体中flags变量保存到r0中
        ldr	r0, [tsk, #TI_FLAGS]		@ get flags
        @ 测试preempt_count是否为0,preempt_count为0可以抢占,preempt_count不为0,不可以抢占
        teq	r8, #0				@ if preempt count != 0
        @ 如果preempt_count不为0,则将r0清0,不会执行svc_preempt进行抢占调度
        movne	r0, #0				@ force flags to 0
        @ 将r0与_TIF_NEED_RESCHED按位与
        tst	r0, #_TIF_NEED_RESCHED
        @ 按位与的结果不为0,则执行svc_preempt,执行抢占调度
        blne	svc_preempt
    #endif
        svc_exit r5, irq = 1			@ return from exception
    ENDPROC(__irq_svc)  @ __irq_svc标号作用域结束
4.3.1.进入中断

首先分析__irq_usr的中断进入代码,usr_entry是一个宏,定义如下。内核定义了struct pt_regs结构体,用来描述内核保存中断上下文cpu寄存器的排列信息,struct pt_regs内部包含了一个长度为18的数组,其数组元素与cpu寄存器的对应关系由宏定义定义。如r0、r1、r2等寄存器保存到ARM_r0、ARM_r1、ARM_r2中,fp、ip、sp、lr、pc、cpsr寄存器保存到ARM_fp、ARM_ip、ARM_sp、ARM_lr、ARM_pc、ARM_cpsr中。首先将r1-r12寄存器保存到svc模式的栈中,然后将irq模式栈中的数据读出并保存到svc模式的栈中。最后将usr模式下sp、lr寄存器保存到svc栈ARM_lr和ARM_sp位置。

    [arch/arm/include/uapi/asm/ptrace.h]
    struct pt_regs {
        long uregs[18];
    };
    #define ARM_cpsr	uregs[16]
    #define ARM_pc		uregs[15]
    #define ARM_lr		uregs[14]
    #define ARM_sp		uregs[13]
    #define ARM_ip		uregs[12]
    #define ARM_fp		uregs[11]
    #define ARM_r10		uregs[10]
    #define ARM_r9		uregs[9]
    #define ARM_r8		uregs[8]
    #define ARM_r7		uregs[7]
    #define ARM_r6		uregs[6]
    #define ARM_r5		uregs[5]
    #define ARM_r4		uregs[4]
    #define ARM_r3		uregs[3]
    #define ARM_r2		uregs[2]
    #define ARM_r1		uregs[1]
    #define ARM_r0		uregs[0]
    #define ARM_ORIG_r0	uregs[17]

首先将sp减去S_FRAME_SIZES_FRAME_SIZEstruct pt_regs结构体的大小。然后将发生中断时的r1-r12寄存器保存到svc模式的栈中,然后再保存r0、lr和cpsr到svc模式的栈中,lr被保存到了ARM_pc中,用于中断返回时恢复pc寄存器。

    [arch/arm/kernel/entry-armv.S]
    .macro	usr_entry, trace=1, uaccess=1
    sub	sp, sp, #S_FRAME_SIZE      @ 将sp减去S_FRAME_SIZE,留出保存中断上下文的空间
    @ 将中断时刻的寄存器r1-r12保存到svc模式的栈中,对应关系为r0-ARM_r1,r2-ARM_r2...,r12-ARM_ip
    ARM(	stmib	sp, {r1 - r12}	)
    @ 将r0指向的内存地址的三个值加载到r3、r4和r5中,r0中保存的是irq模式下栈的地址,irq模式栈中保存了中断发生时的
    @ r0寄存器、中断返回地址lr及中断发生时的cpsr。对应关系为:r3-r0、r4-lr、r5-cpsr
    ldmia	r0, {r3 - r5}    
    add	r0, sp, #S_PC		@ r0为uregs[15]的地址,S_PC为ARM_pc即uregs[15]相对pt_regs结构体开始地址的偏移值
    mov	r6, #-1			@ r6 = -1
    str	r3, [sp]		@ 将中断发生时的r0寄存器保存到svc模式的栈的ARM_r0位置
    @ 将中断发生时的lr、cpsr保存到svc模式的栈中,对应关系为r4-lr-ARM_pc、r5-cpsr-ARM_cpsr、r6-ARM_ORIG_r0
    @ arm处理器使用寄存器传递参数遵循ATPCS规则,使用r0-r3传递参数,多余的参数通过栈传递,系统调用也使用r0-r3
    @ 传递参数,r0传递的是系统调用号,返回值也通过r0传递,这样可能会把原来的r0覆盖,因此这里会将r0拷贝两份,
    @ ARM_ORIG_r0保存原来的参数,ARM_r0保存返回值,由于中断不需要传递参数,因此这里ARM_ORIG_r0保存为-1,
    @ 表示非系统调用号
    stmia r0, {r4 - r6} 
    @ 将usr模式下sp、lr寄存器保存到svc栈ARM_lr和ARM_sp位置,
    ARM(	stmdb	r0, {sp, lr}^			)
    .endm    @ 宏定义结束

接着分析__irq_svc的中断进入代码,svc_entry中保存的寄存器顺序也是按照struct pt_regs排列。如果定义了CONFIG_AEABI并且__LINUX_ARM_ARCH__ >= 5,则有定义#define SPFIX(code...) code,否则SPFIX定义为空,此定义和ABI接口有关,arm cortex-A9支持EABI,一般都定义了此选项。首先保存r1-r2寄存器,接着将irq栈中的数据保存到svc模式的栈中,最后将中断发生时(svc模式)的sp、lr寄存器保存到栈中。

    .macro	svc_entry, stack_hole=0, trace=1, uaccess=1
    @ sp=sp-S_FRAME_SIZE-4,预留S_FRAME_SIZE+4的空间保存中断上下文
	sub	sp, sp, #(S_FRAME_SIZE + 8 + \stack_hole - 4)
    @ 将sp与4按位与,判断栈是否对齐,tst指令的结果影响cpsr条件标志位Z,tst结果为0,则Z=1,反之Z=0
    SPFIX(	tst	sp, #4		)
    @ 如果将sp与4按位与的结果等于0,则将sp减去4并保存到sp中
    @ sp=sp-4
    SPFIX(	subeq	sp, sp, #4	)
    @ 将r1-r12保存到栈中
    stmia	sp, {r1 - r12}
    @ r0为irq模式中的栈地址,将irq模式栈中的数据分别加载到r3-r5中,对应关系为r3-r0、r4-lr、r5-cpsr
    @ r0、lr、cpsr为中断发生时的寄存器值
    ldmia	r0, {r3 - r5}
    @ r7指向ARM_sp
    add	r7, sp, #S_SP - 4	 
    @ r6为-1
    mov	r6, #-1			
    @ r2保存了未保存中断上下文信息时的svc栈的地址,即刚进入svc模式时栈的地址
    add	r2, sp, #(S_FRAME_SIZE + 8 + \stack_hole - 4)
    @ 如果tst的结果为0,则r2=r2+4
    SPFIX(	addeq	r2, r2, #4	)
    @ 将r0保存到svc模式的栈中
    str	r3, [sp, #-4]!		@ save the "real" r0 copied from the exception stack
    @ r3中保存的是svc的lr寄存器
    mov	r3, lr

    @ We are now ready to fill in the remaining blanks on the stack:
    @  r2 - sp_svc
    @  r3 - lr_svc          中断发生后,cpu会从irq模式切换到svc模式,此lr为svc返回到irq模式的地址
    @  r4 - lr_<exception>, 中断发生的svc模式返回地址
    @  r5 - spsr_<exception>
    @  r6 - orig_r0 (see pt_regs definition in ptrace.h)
    stmia	r7, {r2 - r6}
    .endm
4.3.2.处理中断

__irq_usr__irq_svc都是跳转到irq_handler中处理中断。irq_handler是一个宏,根据是否配置CONFIG_MULTI_IRQ_HANDLER选项有不同的处理方法,CONFIG_MULTI_IRQ_HANDLER配置允许每个机器在运行时指定irq中断服务函数。可适用于多种平台及多种中断控制器。如果没有配置,则默认使用arch_irq_handler_default处理中断。目前arm处理器中断越来越多,一个中断控制器已经无法满足需求,很多处理器采用了中断控制器级联的方式。因此handle_arch_irq方法要比arch_irq_handler_default方法灵活很多。后续会侧重分析handle_arch_irq方法。

    [arch/arm/kernel/entry-armv.S]
        .macro	irq_handler
    #ifdef CONFIG_MULTI_IRQ_HANDLER
        ldr	r1, =handle_arch_irq  @ 获取跳转函数的地址
        mov	r0, sp  @ 将svc模式的栈地址保存到r0中,便于参数传递
        badr	lr, 9997f  @ 设置返回地址
        ldr	pc, [r1]       @ 跳转到handle_arch_irq执行,r0保存了传递的参数
    #else
        arch_irq_handler_default  @ 默认的中断处理函数
    #endif
    9997:
        .endm
        
    [arch/arm/include/asm/irq.h]
    #ifdef CONFIG_MULTI_IRQ_HANDLER
    extern void (*handle_arch_irq)(struct pt_regs *);  @ handle_arch_irq是一个函数指针
    @ 可使用set_handle_irq设置handle_arch_irq
    extern void set_handle_irq(void (*handle_irq)(struct pt_regs *)); 
    #endif

handle_arch_irq在系统启动期间初始化,后续再介绍。arch_irq_handler_default是默认的中断处理函数,也是一个宏,首先使用get_irqnr_and_base获取中断号、中断状态寄存器的值、中断控制器的基地址,

        [arch/arm/include/asm/entry-macro-multi.S]
        .macro	arch_irq_handler_default
        @ 获取中断信息,r0-保存中断号,r2-中断状态寄存器,r6-中断控制器基地址
    1:	get_irqnr_and_base r0, r2, r6, lr
        movne	r1, sp  @ 将r1设置为svc模式的栈地址,便于后续参数传递
        @ routine called with r0 = irq number, r1 = struct pt_regs *
        badrne	lr, 1b  @ 设置函数返回地址
        bne	asm_do_IRQ  @ 跳转到asm_do_IRQ中处理中断,传递的参数为r0和r1

    #ifdef CONFIG_SMP  @ 如果是SMP系统,会检测是否是ppi中断,如果是会处理

        ALT_SMP(test_for_ipi r0, r2, r6, lr)
        ALT_UP_B(9997f)
        movne	r1, sp  @ 将r1设置为svc模式的栈地址,便于后续参数传递
        badrne	lr, 1b  @ 设置函数返回地址
        bne	do_IPI  @ 处理ppi中断
    #endif
    9997:
        .endm
4.3.3.退出中断

首先分析__irq_usr中断退出代码,执行ret_to_user_from_irq标号处的代码,表示中断要退出了,退出中断之前会检查是否还有工作要做,如是否需要重新调度,是否有信号挂起,如有则跳转到slow_work_pending中处理,如没有则继续执行退出中断的流程。arch_ret_to_user和架构相关,如果定义CONFIG_NEED_RET_TO_USER,则在退出中断之前会执行arch_ret_to_userct_user_enter和上下文追踪有关,如果定义CONFIG_CONTEXT_TRACKING,则会被执行。中断退出流程最终会执行restore_user_regs,从名称上可以看出是恢复usr模式的寄存器信息,恢复寄存器信息后,中断就返回了。

    [arch/arm/kernel/asm-offsets.c]
    // 获取flags相对thread_info结构体的偏移地址
    DEFINE(TI_FLAGS, offsetof(struct thread_info, flags));

    [arch/arm/include/asm/thread_info.h]
    struct thread_info {
        unsigned long		flags;		/* low level flags */
        int			preempt_count;	/* 0 => preemptable, <0 => bug */
        mm_segment_t		addr_limit;	/* address limit */
        struct task_struct	*task;		/* main task structure */
        __u32			cpu;		/* cpu */
        __u32			cpu_domain;	/* cpu domain */
        struct cpu_context_save	cpu_context;	/* cpu context */
        __u32			syscall;	/* syscall number */
        __u8			used_cp[16];	/* thread used copro */
        unsigned long		tp_value[2];	/* TLS registers */
    #ifdef CONFIG_CRUNCH
        struct crunch_state	crunchstate;
    #endif
        union fp_state		fpstate __attribute__((aligned(8)));
        union vfp_state		vfpstate;
    #ifdef CONFIG_ARM_THUMBEE
        unsigned long		thumbee_state;	/* ThumbEE Handler Base register */
    #endif
    };
    // 退出中断要检查的标志,_TIF_NEED_RESCHED-是否要重新调度,_TIF_SIGPENDING-是否有信号挂起
    // _TIF_NOTIFY_RESUME-返回用户空间之前是否要调用回调函数,_TIF_UPROBE-断点或单步操作
    #define _TIF_WORK_MASK		(_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
				 _TIF_NOTIFY_RESUME | _TIF_UPROBE)

    [arch/arm/kernel/entry-common.S]
    ENTRY(ret_to_user_from_irq)    @ ret_to_user_from_irq标记开始
        ldr	r1, [tsk, #TI_FLAGS]   @ 获取被中断线程thread_info结构体中的flags成员的值
        @ 将r1与_TIF_WORK_MASK按位与,结果为0则cpsr的条件标志位Z=1,结果不为0,则Z=0
        tst	r1, #_TIF_WORK_MASK    
        @ eq,z=1,ne,z=0
        bne	slow_work_pending      @ 测试按位与的结果是否0,不为0则跳转到slow_work_pending处理
    no_work_pending:
        asm_trace_hardirqs_on save = 0  @ 中断追踪相关,可

        arch_ret_to_user r1, lr   @ 架构相关
        ct_user_enter save = 0    @ 上下文追踪相关

        restore_user_regs fast = 0, offset = 0  @ 恢复中断上下文
    ENDPROC(ret_to_user_from_irq)    @ ret_to_user_from_irq标记结束

下面具体分析一下restore_user_regs是怎么恢复中断上下文的,

        .macro	restore_user_regs, fast = 0, offset = 0
        @ ARM mode restore
        mov	r2, sp  @ 将svc模式的sp栈地址保存到r2中
        @ 将usr模式的cpsr从svc模式的栈中取出并保存到r1中
        ldr	r1, [r2, #\offset + S_PSR]	@ get calling cpsr
        @ 将usr模式的pc从svc模式的栈中取出并保存到lr中
        ldr	lr, [r2, #\offset + S_PC]!	@ get pc
        @ 将usr模式的cpsr保存到svc模式的spsr中
        msr	spsr_cxsf, r1			@ save in spsr_svc
        @ 将svc模式栈中保存的寄存器信息恢复到usr模式的寄存器中,恢复的寄存器有r0、r1、r2...sp、lr
        @ 恢复的顺序为r2+ARM_lr-lr、r2+ARM_sp-sp...r2-r0,r2为svc模式下的栈地址
        ldmdb	r2, {r0 - lr}^			@ get calling r0 - lr
        @ 栈中的中断上下文信息已经恢复了,需要释放栈空间,将sp上移,释放pt_regs占用的空间
        add	sp, sp, #\offset + S_FRAME_SIZE
        @ 将中断返回地址lr恢复到pc中,同时将spsr_svc恢复到cpsr,这里就完成了中断返回
        movs	pc, lr				@ return & move spsr_svc into cpsr
        .endm

接着分析__irq_svc中断退出代码,执行到svc_exit表示中断要退出了。

        [[arch/arm/kernel/entry-header.S]]
        .macro	svc_exit, rpsr, irq = 0
        @ ARM mode SVC restore
        @ 将中断发生时的cpsr保存到spsr_svc中
        msr	spsr_cxsf, \rpsr
        @ 恢复中断上下文,同时将spsr_svc恢复到cpsr中
        ldmia	sp, {r0 - pc}^			@ load r0 - pc, cpsr
        .endm

5.总结

经过以上的分析,可以将Linux内核底层中断处理流程总结如下:

    vector_irq        中断向量表入口
      ->__irq_usr     中断发生在usr模式
          ->usr_entry     保存中断上下文
          ->irq_handler   处理中断
              ->handle_arch_irq  可设置的中断处理函数,需要定义CONFIG_MULTI_IRQ_HANDLER选项
              ->arch_irq_handler_default  默认的中断处理函数
          ->ret_to_user_from_irq  退出中断
              ->slow_work_pending    退出中断前会检查是否需要重新调度、是否有信号挂起等,如有则会执行
      ->__irq_svc    中断发生在svc模式
          ->svc_entry    保存中断上下文
          ->irq_handler  处理中断
              ->handle_arch_irq  可设置的中断处理函数,需要定义CONFIG_MULTI_IRQ_HANDLER选项
              ->arch_irq_handler_default   默认的中断处理函数			
          ->svc_preempt    如果定义CONFIG_PREEMPT选项,则退出中断前会检查内核是否可以抢占,
                           如可以则会执行抢占
          ->svc_exit    退出中断

从相关的书籍中,可以看到ARM处理器进入中断时会自动关闭CPU的中断,退出中断时自动打开中断的结论。但在分析中断进入和退出的时候并没有看到关闭中断和打开中断的代码。ARM CPU的中断关闭和打开通过设置CPSR寄存器的bit[7]和bit[6],bit[7]设置IRQ,bit[6]设置FIQ,bit[6]和bit[7]为1时,关闭对应的中断,为0时打开对应的中断。为了验证上述结果,因此在中断处理的通用函数gic_handle_irq中,读取CPU的CPSR和SPSR寄存器,CPSR可以判断当前CPU的中断状态,SPSR可以判断中断退出后CPU的中断状态。测试代码如下图所示。CPU中断状态测试代码
测试结果如下图所示。在中断处理的通用函数gic_handle_irq中,CPSR的bit[7]始终为1,说明中断是关闭的,在这之前并没有关闭中断的代码,因此可以认为发生中断时CPU自动关闭了中断;SPSR的bit[7]始终为0,说明中断发生前中断是打开的,当中断退出时,将SPSR寄存器的数据恢复到CPSR中,实现了中断返回、CPU模式切换和打开CPU的中断,因此在中断退出时不需要额外的代码去打开中断。
CPU中断状态测试结果

参考资料:

  1. Linux kernel V4.6版本源码
  2. 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
  3. 《Linux内核源代码情景分析》
  4. 《ARM体系结构与编程》
  5. 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》
  6. 《ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition》
  7. 《ARM® Cortex ™ -A Series Programmer’s Guide》
  8. http://www.wowotech.net/irq_subsystem/irq_handler.html
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值