1. 程式人生 > >《一個作業系統的實現》筆記(6)--程序

《一個作業系統的實現》筆記(6)--程序

我們可以把一個單獨的任務所用到的所有東西封裝在一個LDT中,這種思想是多工處理的雛形。
多工所用的段型別如下圖,使用LDT來隔離每個應用程式任務的方法,正是關鍵保護需求之一:
多工所用的段型別

程序示意:
程序示意

我們需要一個數據結構記錄一個程序的狀態,在程序要被掛起的時候,程序資訊就被寫入這個資料結構,等到程序重新啟動的時候,這個資訊重新被讀出來。

最簡單的程序

程序切換的過程:
- 1.程序A執行中
- 2.時鐘中斷髮生,ring1->ring0,時鐘中斷處理器啟動
- 3.程序排程,下一個應執行的程序B被指定
- 4.程序B恢復,ring0->ring1
- 5.程序B執行中
程序切換

程序表

儲存程序資訊的資料結構稱為程序表,或叫程序控制塊,即PCB。

程序棧和核心棧

esp的位置出現在3個不同的區域:
- 程序棧–程序執行時自身的堆疊
- 程序表–儲存程序狀態資訊的資料結構
- 核心棧–程序排程模組執行時使用的堆疊
核心棧

第1步–ring0->ring1

開始第一個程序,我們使用iretd指令來實現由ring0到ring1的轉移,轉移成功後,就可以認為A程序在運行了。

程序表資料結構

typedef struct s_stackframe {   /* proc_ptr points here             ↑ Low           */
u32 gs; /* ┓ │ */ u32 fs; /* ┃ │ */ u32 es; /* ┃ │ */ u32 ds; /* ┃ │ */ u32 edi; /* ┃ │ */ u32 esi; /* ┣ pushed by save() │ */
u32 ebp; /* ┃ │ */ u32 kernel_esp; /* <- 'popad' will ignore it │ */ u32 ebx; /* ┃ ↑棧從高地址往低地址增長*/ u32 edx; /* ┃ │ */ u32 ecx; /* ┃ │ */ u32 eax; /* ┛ │ */ u32 retaddr; /* return address for assembly code save() │ */ u32 eip; /* ┓ │ */ u32 cs; /* ┃ │ */ u32 eflags; /* ┣ these are pushed by CPU during interrupt │ */ u32 esp; /* ┃ │ */ u32 ss; /* ┛ ┷High */ }STACK_FRAME; typedef struct s_proc { STACK_FRAME regs; /* process registers saved in stack frame */ u16 ldt_sel; /* gdt selector giving ldt base and limit */ DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ int ticks; /* remained ticks */ int priority; u32 pid; /* process id passed in from MM */ char p_name[16]; /* name of the process */ }PROCESS;

當要恢復一個程序時,便將esp指向這個結構體的開始處,然後執行一系列的pop命令將暫存器值彈出。
程序表開始位置結構

實現ring0->ring1
而堆疊的資訊也不外乎ss和esp兩個暫存器。
由於要為下一次ring1->ring0做準備,所以用iretd返回之前要保證tss.esp0是正確的。

restart:
    mov esp, [p_proc_ready]
    lldt    [esp + P_LDT_SEL]
    lea eax, [esp + P_STACKTOP]
    mov dword [tss + TSS3_S_SP0], eax
restart_reenter:
    dec dword [k_reenter]
    pop gs
    pop fs
    pop es
    pop ds
    popad
    add esp, 4
    iretd

程序表及相關資料結構對應關係:
程序表及相關資料結構對應關係

第一個程序的啟動過程:
第一個程序的啟動過程

第2步–豐富中斷處理程式

賦值tss.esp0

由ring0到ring1時,推展的切換直接在指令iretd被執行時就完成了,目的碼的cs、eip、ss、esp等都是從堆疊中得到的,這很簡單。但ring1到ring0切換時就免不了用到TSS了。
而堆疊的資訊也不外乎ss和esp兩個暫存器。
由於要為下一次ring1->ring0做準備,所以用iretd返回之前要保證tss.esp0是正確的。

現在的中斷例程:
在中斷髮生的開始,esp的值是剛剛從TSS裡面渠道的程序表A中的regs的最高地址,然後個暫存器的值被壓棧入程序表,然後esp指向regs的最低地址處,然後設定tss.esp0的值,準備下一次程序被中斷時使用。

核心棧

    mov esp, StackTop       ; 切到核心棧
    ;...
    mov esp, [p_proc_ready] ; 離開核心棧

中斷重入

中斷程式是被動的。
為了避免這種巢狀現象的發生,我們必須想一個辦法讓中斷程式知道自己是不是在巢狀執行。
只要設定一個全域性變數就可以了。
目前我們的處理,如果發現當前是巢狀的,則直接跳到最後,結束中斷處理程式的執行。

多程序

從程序A切換到程序B之前,如何保留和恢復現場(即各暫存器的值)?
後面會提到。

一個程序只要有一個程序體和堆疊就可以運行了。

typedef void    (*task_f)   ();
typedef struct s_task {
    task_f  initial_eip;
    int stacksize;
    char    name[32];
}TASK;

TASK    task_table[NR_TASKS] = 
{{TestA, STACK_SIZE_TESTA, "TestA"},
{TestB, STACK_SIZE_TESTB, "TestB"}};

void TestA()
{
    //...
}
void TestB()
{
    //...
}

初始化到proc_table中,從TASK結構中讀取不同任務入口地址、堆疊棧頂和程序名,然後賦值給相應的程序表項。

    for(i=0;i<NR_TASKS;i++){
        strcpy(p_proc->p_name, p_task->name);   // name of the process
        p_proc->pid = i;            // pid
        p_proc->ldt_sel = selector_ldt;

        memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3],
               sizeof(DESCRIPTOR));
        p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5;
        memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3],
               sizeof(DESCRIPTOR));
        p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5;
        p_proc->regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK)
            | SA_TIL | RPL_TASK;
        //...
        p_proc->regs.eip = (u32)p_task->initial_eip;
        p_proc->regs.esp = (u32)p_task_stack;

        p_task_stack -= p_task->stacksize;
        p_proc++;
        p_task++;
        selector_ldt += 1 << 3;
    }

LDT

填充 GDT 中程序的 LDT 的描述符。

    int i;
    PROCESS* p_proc = proc_table;
    u16 selector_ldt = INDEX_LDT_FIRST << 3;
    for(i=0;i<NR_TASKS;i++){
        init_descriptor(&gdt[selector_ldt>>3],
                vir2phys(seg2phys(SELECTOR_KERNEL_DS),
                    proc_table[i].ldts),
                LDT_SIZE * sizeof(DESCRIPTOR) - 1,
                DA_LDT);
        p_proc++;
        selector_ldt += 1 << 3;
    }

每個程序都會在GDT中對應一個LDT描述符。
每個程序都有自己的LDT.所以當程序切換時需要重新載入ldtr

多程序的實現–交替執行A和B程序

一個程序如何由“睡眠”態變成“執行”態?

無非是將esp指向程序表項的開始處,然後在執行lldt之後經歷一系列pop指令恢復各個暫存器的值。一切的資訊都包含在程序表中。所以,要想恢復不同的程序,只需要將esp指向不同的程序表就可以了。
在離開核心棧時給esp賦值。

    ;...
    mov esp, StackTop       ; 切到核心棧
    ;...
    call    clock_handler
    ;...    
    mov esp, [p_proc_ready] ; 離開核心棧
    lldt    [esp + P_LDT_SEL]
    lea eax, [esp + P_STACKTOP]
    mov dword [tss + TSS3_S_SP0], eax
    ;...

一個程序如何由“執行”態變成“睡眠”態?

當CPU不再執行該程序的程式碼指令時,就可以認為這個程序已經睡眠了。
那麼這個暫存器的值是怎麼儲存的呢?肯定是儲存在該程序的程序表裡的,因為由“睡眠”態變成“執行”態就是從這裡獲取的資訊。
保護的時機就在程序排程切換之前。
我們在時鐘中斷切換程序時這樣寫:

hwint00:        ; Interrupt routine for irq 0 (the clock).
    sub esp, 4
    pushad      ; `.
    push    ds  ;  |
    push    es  ;  | 儲存原暫存器值
    push    fs  ;  |
    push    gs  ; /
    mov dx, ss
    mov ds, dx
    mov es, dx

    inc byte [gs:0]     ; 改變螢幕第 0 行, 第 0 列的字元
    ;...
    mov esp, StackTop       ; 切到核心棧
    ;...
    call    clock_handler
    ;...

    mov esp, [p_proc_ready] ; 離開核心棧
    lldt    [esp + P_LDT_SEL]
    lea eax, [esp + P_STACKTOP]
    mov dword [tss + TSS3_S_SP0], eax
    ;...
    pop gs  ; `.
    pop fs  ;  |
    pop es  ;  | 恢復原暫存器值
    pop ds  ;  |
    popad       ; /
    add esp, 4

    iretd

簡單來說,在呼叫clock_handler之前,
我們儲存的是程序A的暫存器到esp所指向的堆疊,也就是程序表A(從ring1跳到ring0,esp的值變成TSS中夜色少的ring0下的esp值)。
之後esp被切換成程序B的堆疊,所以pop出來的就是儲存在程序表B裡的暫存器值了。

系統呼叫

使用者程序因為特權級的關係,無法訪問某些許可權更高的記憶體區域,
只能通過系統呼叫來實現,它是應用程式和作業系統之間的橋樑。
用中斷可以方便地實現系統呼叫。

實現一個簡單的系統呼叫

作業系統給應用程式提供一個get_ticks()的系統呼叫,用來獲得當前總共發生了多少次時鐘中斷。
系統呼叫的過程:
- 1、“問”,告訴作業系統自己要什麼;
- 2、作業系統“找”,即處理;
- 3、“回答”,也就是把結果返回給程序。

;syscall.asm
_NR_get_ticks       equ 0 ; 要跟 global.c 中 sys_call_table 的定義相對應!
INT_VECTOR_SYS_CALL equ 0x90

global  get_ticks ; 匯出符號

bits 32
[section .text]

get_ticks:
    mov eax, _NR_get_ticks
    int INT_VECTOR_SYS_CALL
    ret

sys_call_table是一個函式指標陣列,每一個成員都指向一個函式,用以處理相應的系統呼叫。注意:sys_call是核心呼叫的,如果要把返回值告訴應用程序的話,需要把函式的返回值放在程序表eax的位置,以便程序P被恢復執行時eax中裝的是正確的返回值。

;kernel.asm
sys_call:
        call    save
        sti
        call    [sys_call_table + eax * 4]
        mov     [esi + EAXREG - P_STACKBASE], eax ; 把函式的返回值放在程序表eax的位置,以便程序P被恢復執行時eax中裝的是正確的返回值。
        cli
        ret

程序排程

程序優先順序排程

在中斷髮生時,我們要優先順序選擇下一個要執行的程序時。

PUBLIC void schedule()
{
    PROCESS* p;
    int  greatest_ticks = 0;

    while (!greatest_ticks) {
        for (p = proc_table; p < proc_table+NR_TASKS; p++) {
            if (p->ticks > greatest_ticks) {
                greatest_ticks = p->ticks;
                p_proc_ready = p;
            }
        }

        if (!greatest_ticks) {
            for (p = proc_table; p < proc_table+NR_TASKS; p++) {
                p->ticks = p->priority;
            }
        }
    }
}