Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

13|中断初始化trap_init:你的键盘是何时生效的?

讲述:闪客 大小:5.19MB 时长:00:05:25
00:00
1.0×

你好,我是闪客。

上一讲我们说到管理内存前划分出的三个边界值,以及主内存初始化函数 mem_init,也算是迈出了main 函数的第一步。

这一讲我们要来探索“中断”隐藏的奥秘,建立一个中断描述符表 idt,来管理各种中断

trap_init 方法

话不多说,我们先看一个方法。

// init/main.c
void main(void) {
    ...
    trap_init();
    ...
}

这个方法是干嘛的?不着急,先来听我抛出一个问题。

当你的计算机刚刚启动时,你按下键盘是没有任何效果的,但是过了一段时间后,再按下键盘就有效果了,也就是计算机会给出反应,最简单的反应就是直接将键盘字符显示在屏幕上。

那我们今天就来刨根问底一下,到底过了多久之后,按下键盘才有效果呢?我们带着这个问题,打开 trap_init 方法看一看。

// kernel/traps.c
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);
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);
    set_trap_gate(39,&parallel_interrupt);
}

看到这个方法的全部代码后,你可能会会心一笑,也可能一头雾水,这是什么呀?这么多 set_xxx_gate!有密集恐惧症的话,绝对看不下去这段代码。所以我就给它简化一下,把相同功能的去掉。

// kernel/traps.c
void trap_init(void) {
    int i;
    // set 了一堆 trap_gate
    set_trap_gate(0, &divide_error);
    ... 
    // 又 set 了一堆 system_gate
    set_system_gate(45, &bounds);
    ...
    // 又又批量 set 了一堆 trap_gate
    for (i=17;i<48;i++)
        set_trap_gate(i, &reserved);
    ...
}

这就简单多了,下面我们一块一块地看。

set_trap_gateset_system_gate

首先,我们看 set_trap_gateset_system_gate 这两个东西,发现了这么几个宏定义。

// include/asm/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_trap_gate(n,addr) \
    _set_gate(&idt[n],15,0,addr)

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

别怕,我也看不懂。不过它们最终都指向了同一个宏定义 _set_gate,说明是有共性的。什么共性呢?我直接说吧,那段你完全看不懂的代码,是将汇编语言嵌入到 C 语言了,这种内联汇编的格式非常恶心,所以我也不想搞懂它,知道最终的效果就行。而最终的效果就是在中断描述符表中插入了一个中断描述符

中断描述符表

中断描述符表还记得吧,英文叫 idt。

图片

这段代码就是往这个 idt 表里一项一项地写东西,其对应的中断号就是第一个参数,中断处理程序就是第二个参数。产生的效果就是,之后如果来了一个中断,CPU 根据其中断号,就可以到这个中断描述符表 idt 中找到对应的中断处理程序了。

比如这个。

set_trap_gate(0,&divide_error);

就是设置 0 号中断,对应的中断处理程序是 divide_error

等 CPU 执行一条除零指令的时候,会从硬件层面发起一个 0 号异常中断,然后执行由我们操作系统定义的 divide_error,也就是除法异常处理程序,执行完之后再返回。

再比如这个。

set_system_gate(5,&overflow);

就是设置 5 号中断,对应的中断处理程序是 overflow,是边界出错中断。

TIPS:这个 system 与 trap 的区别仅仅在于,设置的中断描述符的特权级不同,前者是 0(内核态),后者是 3(用户态)。这块展开之后是非常严谨、绕口、复杂的特权级相关的知识,现在不明白的话可以先不用管,都理解为设置一个中断号和中断处理程序的对应关系就好了。

批量设置默认中断

我们再往后看,批量操作这里。

void trap_init(void) {
    ...
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    ...
}

17 到 48 号中断都批量设置为了 reserved 函数,这是暂时的,后面各个硬件初始化时要重新设置好这些中断,把之前暂时设置的这个函数给覆盖掉,这里你先留个印象就好。

所以整段代码执行下来,内存中那个 idt 的位置会变成这个样子。

好了,我们看到了设置中断号与中断处理程序对应的地方。所以说,这个 trap_init 里的代码全部执行完毕后,按下键盘就有反应了吗?

NO!键盘产生中断的中断号是 0x21,此时这个中断号还仅仅对应着一个临时的中断处理程序 &reserved,我们接着往后看。

tty_init 方法

这行代码往后几行,还有一个叫 tty_init 的方法。

// init/main.c
void main(void) {
    ...
    trap_init();
    ...
    tty_init();
    ...
}

// kernel/chr_dev/tty_io.c
void tty_init(void) {
    rs_init();
    con_init();
}

// kernel/chr_dev/console.c
void con_init(void) {
    ...
    set_trap_gate(0x21,&keyboard_interrupt);
    ...
}

我省略了大量的代码,只保留了我们关心的。

注意,这个 tty_init 根据调用链,会调用到一行添加 0x21 号中断处理程序的代码,用的就是刚刚学习的 set_trap_gate 方法。而后面的 keyboard_interrupt 根据名字也可以猜出,就是键盘的中断处理程序嘛!

好了,我们终于找到答案了,就是从这一行代码开始,我们的键盘生效了!没错,不过还有点小小的不严谨,就是我们现在的中断处于禁用状态,不管是键盘中断还是其他中断,通通都不好使。而 main 方法继续往下读,还有一行这个东西——sti。

// init/main.c
void main(void) {
    ...
    trap_init();
    ...
    tty_init();
    ...
    sti();
    ...
}

sti 最终会对应一个同名的汇编指令 sti,表示允许中断。所以这行代码之后,键盘才真正开始生效!

至于按下键盘后究竟会作何反应,那就是 keyboard_interrupt 方法里的故事了。别着急,后面一些内容会彻底解答你心中的这个疑惑。

总结时刻

这一讲我们由键盘中断生效时间这个问题,引出了我们这一讲的重点——中断描述符表,用到的方法就是中断初始化函数trap_init()。

我们可以通过 trap_init 函数,设置好中断描述符表的一些默认中断,之后再由各个模块设置它们自己需要的个性化的中断,比如硬盘中断、时钟中断、键盘中断等。

好,我是闪客。欲知后事如何,且听下回分解。