《一個作業系統的實現》筆記(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;
}
}
}
}