linux内核分析——中断与异常

      学习linux的中断异常是前公司所在部门组织的学习任务,参照《深入理解linux内核》,每人选择一个章节进行系统性的深入学习,然后组织大家进行知识分享。这样每个人花费时间认真学习一个章节,就可以获取所有章节的知识,尽量用最少的时间达到最好的效果。当然如果不是自己尽心尽力去系统的学习,听别人讲解一般也就算入门级水平,知道某些概念和框架而已,但也可以节省大量时间了。实际执行过程中,毕竟大家不一定有充裕的时间学习,而且linux基础因人而异,所以在我离职之前也只组织过几次培训,想起来还是蛮怀念那段时间的。当时选择中断和异常这一章,是因为我是从小的嵌入式实时系统转到linux的,之前用的是uc/os,那时候就研究过uc/os的移植包括内核代码,还自学并移植freertos。因为这两个实时系统的移植工作主要是跟中断异常相关的,个人对这方面就会更感兴趣,想知道linux系统中复杂的中断和异常是如何实现的。

        一开始去看《深入理解linux内核》感觉真的是晦涩难懂,而且书本内容属于平铺直叙型,就是单纯的介绍,并不侧重于前后的逻辑性和思路引导。对于一个linux小白并且对逻辑需求又很高的人来说,认真的看完一段话其实只是在脑海中读了一遍而已,大脑完全没有去想这段文字的意思。但是毕竟时间可以改变一切,经历了万事开头难的阶段,以及其后一年多的时间断断续续的巩固和深化理解,终于可以对linux的中断框架总结一些东西。

        本文主要从四个方面来讲,中断和异常向量表的初始化、进入中断、中断描述符、退出中断。因为我是对细节需求很高的人,有一个小的环节不明白都会影响我对整个框架的理解,所以中后期的学习基本都是直接查阅代码的,这样可以看到每一个点的细节。所以文中会粘贴不少的源码,我现在使用的内核源码是linux4.20.5版本。学习过程中也拜读了许多大牛的博客。

        一、中断和异常向量表的初始化

        linux的链接文件是/arch/x86/kernel/目录下的vmlinux.lds文件,从该文件可以看一下内存分配。系统的入口地址,中断向量表的定义等。从vmlinux.lds文件中看到,初始位置存放的是HEAD_TEXT段,也就是*(.head.text)段。从命名方式上看,这个段应该是跟makefile中指定的head-y中添加的内容相关,所以去head-y中定义的head_$(BITS).s中查找.head.text段的定义。

       果然,在head_32/64.s文件开头分别看到了下面的定义,其中__HEAD在/inlude/linux/init.h中定义为*(.head.text)。这里定义的就是入口函数  startup_32和startup_64.

head_32.s:
__HEAD
ENTRY(startup_32)
	movl pa(initial_stack),%ecx
	
	/* test KEEP_SEGMENTS flag to see if the bootloader is asking
		us to not reload segments */
	testb $KEEP_SEGMENTS, BP_loadflags(%esi)
	jnz 2f

head_64.s: 
	.text
	__HEAD
	.code64
	.globl startup_64
startup_64:
	UNWIND_HINT_EMPTY

    跑题了,实际上中断和异常向量表的初始化并不是在head-32/64.s中定义的,而是在entry_32/64.s中定义的。entry.s这个文件定义的都是跟中断(中断和异常的进入退出、中断和异常处理入口)、fork退出(ret_from_fork)、任务调度(switch_to_asm)等相关的汇编代码。

下面就看中断处理函数在entry.s中的定义:

/*
 * Build the entry stubs with some assembler magic.
 * We pack 1 stub into every 8-byte block.
 */
    .align 8
ENTRY(irq_entries_start)
    vector=FIRST_EXTERNAL_VECTOR
    .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
    pushl    $(~vector+0x80)            /* Note: always in signed byte range */
    vector=vector+1
    jmp    common_interrupt
    .align    8
    .endr
END(irq_entries_start)

分析上面的代码,真正定义的中断处理函数有两句话,内容如下:

pushl $(~vector + 0x80)
jmp   common_interrupt

使用伪代码循环定义了所有外部中断的中断处理函数,循环次数即为外部中断的个数(FIRST_SYSTEM_VECTOR -- FIRST_EXTERNAL_VECTOR)。因为所有的中断处理函数的保存现场、跳转到内核中断处理函数的动作都是相同的,所以这部分工作是统一由common_interrupt实现的,只需要将当前的中断向量号推入堆栈即可,类似于向后面的commin_interrupt传入了一个参数,通知它当前处理的是哪一个中断。

这段汇编代码相当于定义了(FIRST_SYSTEM_VECTOR -- FIRST_EXTERNAL_VECTOR)个外部中断处理函数。所有的外部中断处理函数在内存中的存储形式如下图,这个图在后续中断向量表初始化过程中需要用到。

       除了外部中断处理函数,异常处理函数也在entry.s中定义,在x86中前32个中断/异常向量编号服务于异常处理。截取部分异常处理代码如下图。异常处理的入口代码会有不同的处理方式,所以都是分开定义的,其通用的保存现场、调用异常处理子函数、异常返回等操作定义在了common_exception函数中。common_exception函数中需要直接调用不同的异常处理子函数。所以在这部分每个异常单独拥有的代码中,需要将对应的异常处理子函数的地址传递给common_exception,当然还会根据具体的异常处理子函数的设计,看是否需要向其输入参数。

ENTRY(coprocessor_error)
    ASM_CLAC
    pushl    $0
    pushl    $do_coprocessor_error
    jmp    common_exception
END(coprocessor_error)

ENTRY(device_not_available)
    ASM_CLAC
    pushl    $-1                # mark this as an int
    pushl    $do_device_not_available
    jmp    common_exception
END(device_not_available)

        综上所述,entry.s中定义了所有的异常向量处理函数和外部中断处理函数。

        谈到中断/异常向量表的初始化,还需要涉及到x86的中断体系架构。这部分内容对于每个CPU都不同,ARM架构和X86都有自己独特的体系结构,所以是在arch目录下定义的。X86的中断向量表称为IDT(interrupt description table),每一个表项称为中断描述符。这连续存放的每个表项就对应相应编号的异常/中断。像某些嵌入式系统的CPU,中断向量表可能就是直接存放中断处理程序的地址。对于复杂的X86架构,中断描述符不仅需要指明中断处理程序的入口地址,还包括一些权限说明。

        中断描述符中,描述符段选择子+偏移量共同实现了中断处理程序的寻址,这个与X86的段寻址结构设计相关。中断处理程序的地址=全局描述符表[段选择子] + 偏移量。但是在linux中并没有采用段式管理机制,所以全局描述符表中的段基址都被设置为0,其实偏移量就是最终的虚拟地址。其他还有一些bit,是用于设置CPU安全等级等相关操作的,与CPU架构设计息息相关,此处不再详细介绍。

        

         

          两个描述符的格式摘自  https://www.jianshu.com/p/54c1bf1b4aef

二、中断处理

common_interrupt的代码实现如下。

/*
 * the CPU automatically disables interrupts when executing an IRQ vector,
 * so IRQ-flags tracing has to follow that:
 */
    .p2align CONFIG_X86_L1_CACHE_SHIFT
common_interrupt:
    ASM_CLAC
    addl    $-0x80, (%esp)            /* Adjust vector into the [-256, -1] range */  //修正堆栈中中断向量号的格式

    SAVE_ALL switch_stacks=1              //SAVE_ALL宏,保存现场
    ENCODE_FRAME_POINTER
    TRACE_IRQS_OFF                        
    movl    %esp, %eax                    //将堆栈中的数据(中断向量号)放到EAX寄存器,因为X86架构通过EAX向子函数传递参数
    call do_IRQ //调用do_IRQ,相当于C代码中执行 do_IRQ(中断向量号)。 jmp ret_from_intr //跳转到中断退出函数(包括linux中断退出前的操作、恢复现场等操作) ENDPROC(common_interrupt)

看一下SAVE_ALL的宏实现

.macro SAVE_ALL pt_regs_ax=%eax switch_stacks=0
    cld                                       //清楚方向标志
    PUSH_GS
    pushl    %fs
    pushl    %es
    pushl    %ds
    pushl    \pt_regs_ax
    pushl    %ebp
    pushl    %edi
    pushl    %esi
    pushl    %edx
    pushl    %ecx
    pushl    %ebx                             //至此,将段寄存器、通用寄存器的数值推入堆栈,防止原来的状态被中断处理过程污染
    movl    $(__USER_DS), %edx
    movl    %edx, %ds
    movl    %edx, %es
    movl    $(__KERNEL_PERCPU), %edx
    movl    %edx, %fs
    SET_KERNEL_GS %edx

    /* Switch to kernel stack if necessary */
.if \switch_stacks > 0
    SWITCH_TO_KERNEL_STACK                   //是否切换到内核堆栈
.endif

.endm

异常处理函数也是在这个文件中定义:

        

猜你喜欢

转载自www.cnblogs.com/wood-fire/p/11662556.html