你的鍵盤是什麼時候生效的?

當你的計算機剛剛啓動時,你按下鍵盤是不生效的,但是過了一段時間後,再按下鍵盤就有效果了。

那我們今天就來刨根問底一下,到底過了多久之後,按下鍵盤纔有效果呢?

當然首先你得知道,按下鍵盤後會觸發中斷,CPU 收到你的鍵盤中斷後,根據中斷號,尋找由操作系統寫好的鍵盤中斷處理程序。

中斷的原理和過程不瞭解的,可以看我的文章,認認真真的聊聊中斷

這個中斷處理程序會把你的鍵盤碼放入一個隊列中,由相應的用戶程序或內核程序讀取,並顯示在控制檯,或者其他用途,這就代表你的鍵盤生效了。

不過放寬心,我們不展開講這個中斷處理程序以及用戶程序讀取鍵盤碼後的處理細節,我們把關注點放在,究竟是 “什麼時候”,按下鍵盤纔會有這個效果。

我們以 Linux 0.11 源碼爲例,發現進入內核的 main 函數後不久,有這樣一行代碼。

void main(void) {
    ...
    trap_init();
    ...
}

看到這個方法的全部代碼後,你可能會會心一笑,也可能一臉懵逼。

void trap_init(void) {
    int i;
    set_trap_gate(0,÷_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,¶llel_interrupt);
}

這啥玩意?這麼多 set_xxx_gate

有密集恐懼症的話,絕對看不下去這個代碼,所以我就給他簡化一下。

把相同功能的去掉。

void trap_init(void) {
    int i;
    // set 了一堆 trap_gate
    set_trap_gate(0, ÷_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 這倆貨,發現了這麼幾個宏定義。

#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,÷_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 的位置會變成如下的樣子。

好了,我們看到了設置中斷號與中斷處理程序對應的地方,那這行代碼過去後,鍵盤好使了麼?

NO

鍵盤產生的中斷的中斷號是 0x21,此時這個中斷號還僅僅對應着一個臨時的中斷處理程序 &reserved,我們接着往後看。

在這行代碼往後幾行,還有這麼一行代碼。

void main(void) {
    ...
    trap_init();
    ...
    tty_init();
    ...
}

void tty_init(void) {
    rs_init();
    con_init();
}

void con_init(void) {
    ...
    set_trap_gate(0x21,&keyboard_interrupt);
    ...
}

我省略了大量的代碼,只保留了我們關心的。

注意到 trap_init 後有個 tty_init,最後根據調用鏈,會調用到一行添加 0x21 號中斷處理程序的代碼,就是剛剛熟悉的 set_trap_gate

而後面的 **keyboard_interrupt **根據名字也可以猜出,就是鍵盤的中斷處理程序嘛!

好了,那我們終於找到大案了,就是從這一行代碼開始,我們的鍵盤生效了!

沒錯,不過還有點小問題,不過不重要,就是我們現在的中斷處於禁用狀態,不論是鍵盤中斷還是其他中斷,通通都不好使。

而 main 方法繼續往下讀,還有一行這個東西。

void main(void) {
    ...
    trap_init();
    ...
    tty_init();
    ...
    sti();
    ...
}

sti 最終會對應一個同名的彙編指令 sti,表示允許中斷。所以這行代碼之後,鍵盤才真正開始生效!

動畫酷不酷?好啦,今天的文章就到這裏了,中斷的原理和細節,就看我之前的文章,認認真真的聊聊中斷

鍵盤處理的具體流程,可以跟着我今天的代碼深入進去看看喲,Linux 0.11 裏還是很簡單的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sFp_388qRncB-jpJeRzCGQ