Linux中断处理

简介

1.基于Linux0.11代码进行分析。
2.中断类型分类以及具体的中断。
3.中断向量的注册。
4.中断处理流程。
5.各类型中断的具体执行流程。

中断的类型及具体的种类

Linux0.11内核注释

1.可屏蔽硬件中断。优先级较低,可以被忽略或者延后处理,通常有键盘,打印机。
2.不可屏蔽硬件中断。CPU必须无条件响应,优先级非常的高,通常有电源断电,内存校验错误。
3.软件中断--错误。缺页异常?内存访问时产生缺页异常中断,在中断处理程序中实际分配内存,然后在缺页中断处理完成后,继续访问内存。
4.软件中断--陷阱。常见的例子有系统调用,int 0x80,首先会调用中断处理程序,处理完成后,会继续执行后面的指令。
5.软件中断--放弃。常见的例子有除零,该错误发生后,调用中断处理程序,中断处理程序中会产生SIGFPE信号,程序可通过注册对应的信号处理函数处理该信号。

中断向量的注册

1.源码在head.s这个文件中。
2.0x20-0x2f是硬件中断,在head.s中初始化为ignore_int后,后续的硬件初始化过程中会初始化其中断处理函数。
3.中断向量存储在全局的中断向量数组结构中,该数组长度256,每个元素8个字节,在head.s文件中定义。在system.h文件中,定义了设置该数组的接口。代码如下:

// head.s
.align 2
.word 0
idt_descr:
    .word 256*8-1       # idt contains 256 entries
    .long idt

// system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
    "movw %0,%%dx\n\t" \
    "movl %%eax,%1\n\t" \
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "o" (*((char *) (gate_addr))), \
    "o" (*(4+(char *) (gate_addr))), \
    "d" ((char *) (addr)),"a" (0x00080000))

#define set_intr_gate(n,addr) \
    _set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) \
    _set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)

4.在head.s中,调用startup_32函数过程中调用setup_idt函数将全局的中断描述符初始化为ignore_int。代码如下

setup_idt:
    lea ignore_int,%edx
    movl $0x00080000,%eax
    movw %dx,%ax        /* selector = 0x0008 = cs */
    movw $0x8E00,%dx    /* interrupt gate - dpl=0, present */

    lea idt,%edi  // edi寄存器指向idt数组的起始地址
    mov $256,%ecx // 循环256次
rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi  // 下标递增
    dec %ecx
    jne rp_sidt
    lidt idt_descr
    ret

5.在trap.c文件中,调用trap_init函数注册软件中断。在sched.c中调用sched_init注册2个调度相关的中断,0x20硬件时钟中断,0x80系统调用中断。其余的硬件中断在硬件初始化时注册。以下是部分代码:

void sched_init(void)
{
    set_intr_gate(0x20,&timer_interrupt);
    set_system_gate(0x80,&system_call);
}

void trap_init(void)
{
    int i;

    // 设置除操作出错的中断向量值。
    set_trap_gate(0,&divide_error);
    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);
    set_system_gate(3,&int3);   /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);
    // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    // 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
    set_trap_gate(45,&irq13);
    outb_p(inb_p(0x21)&0xfb,0x21);  // 允许8259A主芯片的IRQ2中断请求。
    outb(inb_p(0xA1)&0xdf,0xA1);    // 允许8259A从芯片的IRQ3中断请求。
    set_trap_gate(39,&parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}

tips:如果想要知道中断处理函数在哪里注册,注册的函数是什么,可搜索system.h文件中的定义的_set_intr_gate, _set_trap_gate, _set_system_gate函数被调用的地方。

中断的处理流程

在分析和阅读源码前,先尝试思考通用的中断处理逻辑。
1.硬件中断。硬件中断通常来自于外部硬件触发。此时进程可能在任意一个状态(用户态执行用户代码,或者在执行中断)。如果是在执行中断,那么就应该判断当前正在执行的中断和触发中断的优先级,然后确定是否要打断正在执行的中断。
2.软件中断。软件中断来自用户代码主动调用产生,所以此时应该是在用户态执行用户代码。
3.用户态下的中断应该有一样的通用流程,大致是,保存当前正在执行代码的上下文,切换到内核态调用中断处理函数,完成后回到用户态恢复上下文,然后继续执行。


中断处理流程

4.中断处理是在内核态下运行,因此使用的是内核的堆栈,如果中断时正在运行用户态的代码,那么在切到内核态后,将当时的上下文寄存器等信息存在内核中的堆栈中。示意图如下:


中断时的堆栈

5.当在中断情况下发生高优先级的中断时,会在中断过程中使用的堆栈的基础上再次保存中断的上下文,然后执行更高优先级的中断。堆栈示意图如下:
高优先级中断打断当前的中断时的堆栈

中断的具体执行

1.源码主要在asm.s和trap.c这2个文件中。
2.在调用具体的中断处理函数前,都会将段寄存器和ip寄存器入中断栈,这是中断打断的正在运行的进程的上下文。然后将实际要执行的用C实现的中断处理函数push入栈,再将普通的寄存器如eax,ebx等寄存器入栈,下一步将一些段寄存器入栈,最后将返回后执行的指令的栈地址入栈。


Linux0.11完全注释中断堆栈图

3.无返回值的中断以int 0x1,除0的中断举例,代码如下:

divide_error:
    pushl $do_divide_error      # 这里push实际要调用的函数,下一条指令又将其和eax寄存器的值交换。
# 其目的是为了代码复用,其它中断可以直接调用no_error_code代码段
# do_divide_error在traps.c中实现
no_error_code:                  # 这里是五出错号处理的入口处。
    xchgl %eax,(%esp)           # _do_divide_error的地址→eax,eax被交换入栈
    pushl %ebx  #保存打断进程的寄存器上下文
    pushl %ecx
    pushl %edx
    pushl %edi
    pushl %esi
    pushl %ebp
    push %ds                    # !!16位的段寄存器入栈后也要占用4个字节。
    push %es
    push %fs
    pushl $0        # "error code"  #将数值0作为出错码入栈
    lea 44(%esp),%edx           # 取对堆栈中原调用返回地址处堆栈指针位置,并压入堆栈。
    pushl %edx
    movl $0x10,%edx             # 初始化段寄存器ds、es和fs,加载内核数据段选择符 0x10是内核栈的段起始地址
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
# 下行上的 * 号表示调用操作数指定地址处的函数,称为间接调用。这句的含义是调用引起本次
# 异常的C处理函数,例如do_divide_error等。
    call *%eax
    addl $8,%esp
    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax                   # 弹出原来eax中的内容
    iret  # 返回时会取出栈中保存的被打断进程的eip寄存器的值 继续执行后续的指令

4.其它的类似的无返回值的中断处理函数如下:

debug:
    pushl $do_int3      # _do_debug C函数指针入栈
    jmp no_error_code  # 复用no_error_code代码段

5.含error_code的中断和有error_code的中断类似,这里就不粘贴重复的代码了,可以参考源码中的asm.s文件。把握住中断处理的核心逻辑,保存上下文,处理中断,恢复上下文,以及参考源码中的system.h, asm.s, traps.c这几个文件。
6.深究int 0x1除0中断的处理。

void do_divide_error(long esp, long error_code)
{
    die("divide error",esp,error_code);
}

static void die(char * str,long esp_ptr,long nr)
{
    long * esp = (long *) esp_ptr;
    int i;

    printk("%s: %04x\n\r",str,nr&0xffff);
    // 下行打印语句显示当前调用进程的CS:EIP、EFLAGS和SS:ESP的值。
    // EIP:\t%04x:%p\n - esp[1]是段选择符(cs),esp[0]是eip.
    // EFLAGS:\t%p\n - esp[2]是eflags
    // ESP:\t%04x:%p\n - esp[4]是源ss,esp[3]是源esp
    printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
        esp[1],esp[0],esp[2],esp[4],esp[3]);
    printk("fs: %04x\n",_fs());
    printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
    if (esp[4] == 0x17) {
        printk("Stack: ");
        for (i=0;i<4;i++)
            printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
        printk("\n");
    }
    str(i);                 // 取当前运行任务的任务号
    printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
    for(i=0;i<10;i++)
        printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
    printk("\n\r");
// 前面都是打印调试信息 这里才是真正的处理逻辑 在这里是直接退出 错误码为11
    do_exit(11);        /* play segment exception */
}

总结

中断处理的分析到这里就告一段落了。首先要对中断进行分类,并且了解每种类型的中断具体有哪些类型。然后了解内核是如何处理中断的,中断处理函数使用的栈都是内核的内存空间,在执行具体的处理函数前,要先保存被中断的进程的上下文,然后再调用具体的处理函数,处理完成后再恢复被中断进程的上下文以继续运行。在这里要说明下,内核中的栈每次起始地址都是一样的,这是因为调用结束后,要么不会返回,要么返回后栈又变空了,所以内核栈是可以重复利用的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342