大名鼎鼎的程序排程就是從這裡開始的

語言: CN / TW / HK

書接上回,上回書咱們說到,time_init 方法通過與 CMOS 埠進行讀寫互動,獲取到了年月日時分秒等資料,並通過這些計算出了開機時間 startup_time 變數,是從 1970 年 1 月 1 日 0 時起到開機當時經過的秒數。

我們繼續往下看,大名鼎鼎的程序排程初始化,shed_init。

void main(void) { 
    ... 
    mem_init(main_memory_start,memory_end); 
    trap_init(); 
    blk_dev_init(); 
    chr_dev_init(); 
    tty_init(); 
    time_init(); 
    sched_init(); 
    buffer_init(buffer_memory_end); 
    hd_init(); 
    floppy_init(); 
     
    sti(); 
    move_to_user_mode(); 
    if (!fork()) {init();} 
    for(;;) pause(); 
} 

這方法可了不起,因為它就是多程序的基石!

終於來到了興奮的時刻,是不是很激動?不過先別激動,這裡只是程序排程的初始化,也就是為程序排程所需要用到的資料結構做個準備,真正的程序排程還需要排程演算法、時鐘中斷等機制的配合。

當然,對於理解作業系統,流程和資料結構最為重要了,而這一段作為整個流程的起點,以及建立資料結構的地方,就顯得格外重要了。

我們進入這個方法,一點點往後看。

void sched_init(void) { 
    set_tss_desc(gdt+4, &(init_task.task.tss)); 
    set_ldt_desc(gdt+5, &(init_task.task.ldt)); 
    ... 
} 

兩行程式碼初始化了下 TSS 和 LDT。

先別急問這倆結構是啥。還記得之前講的全域性描述符表 gdt 麼?它在記憶體的這個位置,並且被設定成了這個樣子。

忘了的看一下第八回 | 煩死了又要重新設定一遍 idt 和 gdt,這就說明之前看似沒用的細節有多重要了,大家一定要有耐心。

說回這兩行程式碼,其實就是往後又加了兩項,分別是 TSS 和 LDT。

好,那再說說這倆結構是幹嘛的,不過本篇先簡單理解,後面會詳細講到。

TSS 叫任務狀態段,就是儲存和恢復程序的上下文的,所謂上下文,其實就是各個暫存器的資訊而已,這樣程序切換的時候,才能做到儲存和恢復上下文,繼續執行。

由它的資料結構你應該可以看出點意思。

struct tss_struct{ 
    long back_link; 
    long esp0; 
    long ss0; 
    long esp1; 
    long ss1; 
    long esp2; 
    long ss2; 
    long cr3; 
    long eip; 
    long eflags; 
    long eax, ecx, edx, ebx; 
    long esp; 
    long ebp; 
    long esi; 
    long edi; 
    long es; 
    long cs; 
    long ss; 
    long ds; 
    long fs; 
    long gs; 
    long ldt; 
    long trace_bitmap; 
    struct i387_struct i387; 
}; 

而 LDT 叫區域性描述符表,是與 GDT 全域性描述符表相對應的,核心態的程式碼用 GDT 裡的資料段和程式碼段,而使用者程序的程式碼用每個使用者程序自己的 LDT 裡得資料段和程式碼段。

先不管它,我這裡放一張超綱的圖,你先找找感覺。

我們接著往下看。

struct desc_struct { 
    unsigned long a,b; 
} 
 
struct task_struct * task[64] = {&(init_task.task), }; 
 
void sched_init(void) { 
    ... 
    int i; 
    struct desc_struct * p; 
        p = gdt+6; 
    for(i=1;i<64;i++) { 
        task[i] = NULL; 
        p->a=p->b=0; 
        p++; 
        p->a=p->b=0; 
        p++; 
    } 
    ... 
} 

這段程式碼有個迴圈,幹了兩件事。

一個是給一個長度為 64,結構為 task_struct 的陣列 task 附上初始值。

這個 task_struct 結構就是代表每一個程序的資訊,這可是個相當相當重要的結構了,把它放在心裡。

struct task_struct { 
/* these are hardcoded - don't touch */ 
    long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 
    long counter; 
    long priority; 
    long signal; 
    struct sigaction sigaction[32]; 
    long blocked; /* bitmap of masked signals */ 
  /* various fields */ 
    int exit_code; 
    unsigned long start_code,end_code,end_data,brk,start_stack; 
    long pid,father,pgrp,session,leader; 
    unsigned short uid,euid,suid; 
    unsigned short gid,egid,sgid; 
    long alarm; 
    long utime,stime,cutime,cstime,start_time; 
    unsigned short used_math; 
  /* file system info */ 
    int tty;  /* -1 if no tty, so it must be signed */ 
    unsigned short umask; 
    struct m_inode * pwd; 
    struct m_inode * root; 
    struct m_inode * executable; 
    unsigned long close_on_exec; 
    struct file * filp[NR_OPEN]; 
  /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */ 
    struct desc_struct ldt[3]; 
  /* tss for this task */ 
    struct tss_struct tss; 
}; 

這個迴圈做的另一件事,是給 gdt 剩下的位置填充上 0,也就是把剩下留給 TSS 和 LDT 的描述符都先附上空值。

往後展望一下的話,就是以後每建立一個新程序,就會在後面新增一組 TSS 和 LDT 表示這個程序的任務狀態段以及區域性描述符表資訊。

還記得剛剛的超綱圖吧,未來整個記憶體的規劃就是這樣的,不過你先不用理解得很細。

那為什麼一開始就先有了一組 TSS 和 LDT 呢?現在也沒建立程序呀。錯了,現在雖然我們還沒有建立起程序排程的機制,但我們正在執行的程式碼就是會作為未來的一個程序的指令流。

也就是當未來程序排程機制一建立起來,正在執行的程式碼就會化身成為程序 0 的程式碼。所以我們需要提前把這些未來會作為程序 0 的資訊寫好。

如果你覺得很疑惑,別急,等後面整個程序排程機制建立起來,並且讓你親眼看到程序 0 以及程序 1 的建立,以及它們後面因為程序排程機制而切換,你就明白這一切的意義了。

好,收回來,初始化了一組 TSS 和 LDT 後,再往下看兩行。

#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n))) 
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n))) 
 
void sched_init(void) { 
    ... 
    ltr(0); 
    lldt(0); 
    ... 
} 

這又涉及到之前的知識咯。

還記得 lidt 和 lgdt 指令麼?一個是給 idtr 暫存器賦值,以告訴 CPU 中斷描述符表 idt 在記憶體的位置;一個是給 gdtr 暫存器賦值,以告訴 CPU 全域性描述符表 gdt 在記憶體的位置。

那這兩行和剛剛的類似,ltr 是給 tr 暫存器賦值,以告訴 CPU 任務狀態段 TSS 在記憶體的位置;lldt 一個是給 ldt 暫存器賦值,以告訴 CPU 區域性描述符 LDT 在記憶體的位置。

這樣,CPU 之後就能通過 tr 暫存器找到當前程序的任務狀態段資訊,也就是上下文資訊,以及通過 ldt 暫存器找到當前程序在用的區域性描述符表資訊。

我們繼續看。

void sched_init(void) { 
    ... 
    outb_p(0x36,0x43);      /* binary, mode 3, LSB/MSB, ch 0 */ 
    outb_p(LATCH & 0xff , 0x40);    /* LSB */ 
    outb(LATCH >> 8 , 0x40);    /* MSB */ 
    set_intr_gate(0x20,&timer_interrupt); 
    outb(inb_p(0x21)&~0x01,0x21); 
    set_system_gate(0x80,&system_call); 
    ... 
} 

四行埠讀寫程式碼,兩行設定中斷程式碼。

埠讀寫我們已經很熟悉了,就是 CPU 與外設互動的一種方式,之前講硬碟讀寫以及 CMOS 讀寫時,已經接觸過了。

而這次互動的外設是一個可程式設計定時器的晶片,這四行程式碼就開啟了這個定時器,之後這個定時器變會持續的、以一定頻率的向 CPU 發出中斷訊號。

而這段程式碼中設定的兩個中斷,第一個就是時鐘中斷,中斷號為 0x20,中斷處理程式為 timer_interrupt。那麼每次定時器向 CPU 發出中斷後,便會執行這個函式。

這個定時器的觸發,以及時鐘中斷函式的設定,是作業系統主導程序排程的一個關鍵!沒有他們這樣的外部訊號不斷觸發中斷,作業系統就沒有辦法作為程序管理的主人,通過強制的手段收回程序的 CPU 執行許可權。

第二個設定的中斷叫系統呼叫 system_call,中斷號是 0x80,這個中斷又是個非常非常非常非常非常非常非常重要的中斷,所有使用者態程式想要呼叫核心提供的方法,都需要基於這個系統呼叫來進行。

比如 Java 程式設計師寫一個 read,底層會執行彙編指令 int 0x80,這就會觸發系統呼叫這個中斷,最終呼叫到 Linux 裡的 sys_read 方法。

這個過程之後會重點講述,現在只需要知道,在這個地方,偷偷把這個極為重要的中斷,設定好了。

所以你看這一章的內容,偷偷設定了影響程序和影響使用者程式呼叫系統方法的兩個重量級中斷處理函式,不簡單呀~

到目前為止,中斷已經設定了不少了,我們現在看看所設定好的中斷有哪些。

中斷號 中斷處理函式
0 ~ 0x10 trap_init 裡設定的一堆
0x20 timer_interrupt
0x21 keyboard_interrupt
0x80 system_call

其中 0-0x10 這 17 箇中斷是 trap_init 裡初始化設定的,是一些基本的中斷,比如除零異常等。這個在 第14回 中斷初始化 trap_init 有講到。

之後,在控制檯初始化 con_init 裡,我們又設定了 0x21 鍵盤中斷,這樣按下鍵盤就有反應了。這個在 第16回 控制檯初始化 tty_init 有講到。

現在,我們又設定了 0x20 時鐘中斷,並且開啟定時器。最後又偷偷設定了一個極為重要的 0x80 系統呼叫中斷。

找到些感覺沒,有沒有越來越發現,作業系統有點靠中斷驅動的意思,各個模組不斷初始化各種中斷處理函式,並且開啟指定的外設開關,讓作業系統自己慢慢“活”了起來,逐漸通過中斷忙碌於各種事情中,無法自拔。

恭喜你,我們已經逐漸在接近作業系統的本質了。

回顧一下我們今天干了什麼,就三件事。

第一,我們往全域性描述符表寫了兩個結構,TSS 和 LDT,作為未來程序 0 的任務狀態段和區域性描述符表資訊。

第二,我們初始化了一個結構為 task_struct 的陣列,未來這裡會存放所有程序的資訊,並且我們給陣列的第一個位置附上了 init_task.init 這個具體值,也是作為未來程序 0 的資訊。

第三,設定了時鐘中斷 0x20 和系統呼叫 0x80,一個作為程序排程的起點,一個作為使用者程式呼叫作業系統功能的橋樑,非常之重要。

後面,我們將會逐漸看到,這些重要的事情,是如何緊密且精妙地結合在一起,發揮出奇妙的作用。

欲知後事如何,且聽下回分解。