# XV6 Ch1 OS 組織 - OS 必須具備三項技能:多工、獨立及交流。 ## kernel 組織 - **Monolithic kernel**:整個 OS 都位於 kernel 中,如此一來所有 system calls 都會在 kernel 中執行(xv6)。 - 好處 1. 設計者不須決定 OS 的哪些部份不需要完整的硬體特權。 2. 更方便的讓不同部份的 OS 去合作。 - 壞處 1. 通常在不同部份的 OS 中的介面是複雜的。 2. 這會容易讓開發者出錯。 - **Microkernel**:為了減少 kernel 出錯的風險,設計者可以將 kernel mode 上執行的 OS 程式碼最小化,並大讓OS 在 user mode 中執行。 ![](https://i.imgur.com/OvwFWwq.jpg) --- ## Process 概觀 - 為 UNIX(xv6) 中的一個獨立單元。 - 確保一個 process 不會破壞或是竊取另一程序的記憶體、CPU、檔案描述符等等。 - 亦確保 kernel 不會被破壞。 - Process 為抽象的,這讓一個程式可以假設它佔有一台虛擬機器,即一個接近私有的記憶體或是 address space,其他的 process 不可以 r/w。 - 私有的 adderss space 由不同的 page table 實做,即一個 process 有一個 page table。 ![](https://i.imgur.com/ZryuUVq.jpg) - 每個 process 的 page 都分為 kernel 及 user(如上圖),因此當 process 呼叫一個 system call 時,會直接在自己的 kernel 映射(mapping)中執行。 - Thread:用來執行指令,可以被暫緩,稍後再恢復運作。 - 大部分 thread 的狀態(區域變數等)被保存在 thread 的堆疊上,每個 process 有兩個堆疊:user/kernel 堆疊。 - user 指令執行時,只會用到 user 堆疊,此時 kernel 堆疊為空。 - kernel 指令執行時,user 堆疊的資料不會清空,也不會使用到。 - `p->state` 指 process 的狀態:新建、準備執行、執行中、等待I/O及退出。 - `p->pgdir`:保存 proecess 的 page table。 --- ## Code: 第一個 address space - xv6 為 kernel 建立第一個 address space 的流程: 1. 開機 2. 初始化自己 3. 從硬碟中讀取 boot loader 至記憶體中執行。 4. Boot loader 從硬碟讀取 kernel 並從 *entry.s* 開始執行。 6. Boot loader 會把 xv6 的 kernel 載入實體位址 0x100000。 7. 為了讓剩下的 kernel 能夠執行,設置一個 page table,將虛擬位址 0x80000000(KERNBASE)映射到實體位址 0x0。將兩個虛擬位址映射到同一個實體位址是 page 的常見手法。 ![](https://i.imgur.com/zNMxszZ.png) 8. 跳到 kernel 的 c code,並在高位址上執行: - `%esp` 指向高位址的 stack 記憶體。 - 跳到高位址的 *main*。 ### File: entry.s ```c= _start = V2P_WO(entry) # Entering xv6 on boot processor, with paging off. .globl entry entry: # Turn on page size extension for 4Mbyte pages movl %cr4, %eax orl $(CR4_PSE), %eax movl %eax, %cr4 # Set page directory movl $(V2P_WO(entrypgdir)), %eax movl %eax, %cr3 # Turn on paging. movl %cr0, %eax orl $(CR0_PG|CR0_WP), %eax movl %eax, %cr0 # Set up the stack pointer. movl $(stack + KSTACKSIZE), %esp # Jump to main(), and switch to executing at # high addresses. The indirect call is needed because # the assembler produces a PC-relative instruction # for a direct jump. mov $main, %eax jmp *%eax .comm stack, KSTACKSIZE ``` --- ## Code: 建立第一個 process - 呼叫 `userinit()` 來建立第一個 process(只有在第一個process時會呼叫)。 - 呼叫 `allocproc()`(每個 process 都會呼叫)。 - `Allocproc` 在 process table 中分配一個 slot(`struct proc`),並初始化有關 kernel thread 的 process 片段。 - `Allocproc` 掃描 proc tabel,找到 `p->state` 是 `UNUSED`,接著設定為 `EMBRYO` 來標示被使用,並給予一組唯一的 pid。 :::success **File:** proc.c ::: ### `allocproc()` | 功能 | 回傳值 | | --- | ------ | | 建立一個 process | process 結構 | ```c=29 // Look in the process table for an UNUSED proc. // If found, change state to EMBRYO and initialize // state required to run in the kernel. // Otherwise return 0. static struct proc* allocproc(void) { struct proc *p; char *sp; acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) if(p->state == UNUSED) goto found; release(&ptable.lock); return 0; found: p->state = EMBRYO; p->pid = nextpid++; release(&ptable.lock); ``` - 接著嘗試請求分配一個 kernel stack,如果失敗,把 `p->state` 改回 `UNUSED`。 ```c=+ // Allocate kernel stack. if((p->kstack = kalloc()) == 0){ p->state = UNUSED; return 0; } sp = p->kstack + KSTACKSIZE; // Leave room for trap frame. sp -= sizeof *p->tf; p->tf = (struct trapframe*)sp; ``` ![](https://i.imgur.com/H8sOroj.png) - Allocproc 通過設定返回程式計數器的值來導致新 process 的 kernel thread 會先在 forkret 中執行,再回到 trapret。 - Kernel thread 從 p->context 的拷貝開始執行,因此設定 p->context->eip 指向 forkret 會導致 kernel thread 從 forkret 的開頭開始執行。 ```c=+ // Set up new context to start executing at forkret, // which returns to trapret. sp -= 4; *(uint*)sp = (uint)trapret; sp -= sizeof *p->context; p->context = (struct context*)sp; memset(p->context, 0, sizeof *p->context); p->context->eip = (uint)forkret; return p; } ``` - `Forkret` return 堆疊(`p->context->eip`)底。 - `Allocate` 將 `trapret` 放在 `eip` 的上方,即 `forkret` return 的位置。 - `Trapret` 從 kernel 堆疊頂恢復 user 的暫存器並跳至程序。 --- - 第一個 process 會運行一個小程式 *initcode.s*。 - Process 需要實體記憶體來保存此程式。 - Process 需要被拷貝到記憶體中,也需要 page table 來指向此位址。 - `Userinit` 呼叫 `setupkvm` 來建立 page table 只映射到 kernel 會用到的記憶體。 ### `userinit()` | 功能 | 回傳值 | | --- | ------ | | 建立系統的初始 process | void | ```c=79 userinit(void) { struct proc *p; extern char _binary_initcode_start[], _binary_initcode_size[]; p = allocproc(); initproc = p; if((p->pgdir = setupkvm()) == 0) panic("userinit: out of memory?"); ``` - `inituvm` 請求一個 page 大小的實體記憶體,將虛擬記憶體 0 映射到此記憶體,並將 `_binary_initcode_start_` 及 `_binary_initcode_size_` 拷貝到 page。 ```c=+ inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size); ``` - 把 trap frame 設定為初始使用者模式。 ```c=+ p->sz = PGSIZE; memset(p->tf, 0, sizeof(*p->tf)); p->tf->cs = (SEG_UCODE << 3) | DPL_USER; p->tf->ds = (SEG_UDATA << 3) | DPL_USER; p->tf->es = p->tf->ds; p->tf->ss = p->tf->ds; p->tf->eflags = FL_IF; //allow hardware interrupt p->tf->esp = PGSIZE; p->tf->eip = 0; // beginning of initcode.S ``` - `p->name` 設為 `"initcode"` 是為了 debug,`p->cwd` 設在 process 的現在目錄。 ```c=+ safestrcpy(p->name, "initcode", sizeof(p->name)); p->cwd = namei("/"); ``` - 設定 `p->state` 為 `RUNNABLE`。 ```c=+ p->state = RUNNABLE; } ``` --- ## Code: 執行第一個 process - 當 *main* 呼叫完 *userinit* 後,呼叫 *mpmain*,*mpmain* 接著呼叫 *scheduler* 開始運行 process。 ### `mpmain()` :::success **File:** main.c ::: | 功能 | 回傳值 | | --- | ------ | | 完成多核心開機程序 | void | ```c=55 // Common CPU setup code. static void mpmain(void) { cprintf("cpu%d: starting\n", cpu->id); idtinit(); // load idt register xchg(&cpu->started, 1); // tell startothers() we're up scheduler(); // start running processes } ``` ### `scheduler()` :::success **File:** proc.c ::: | 功能 | 回傳值 | | --- | ------ | | 執行調度,指定執行的 process | void | ```c=249 //PAGEBREAK: 42 // Per-CPU process scheduler. // Each CPU calls scheduler() after setting itself up. // Scheduler never returns. It loops, doing: // - choose a process to run // - swtch to start running that process // - eventually that process transfers control // via swtch back to the scheduler. void scheduler(void) { struct proc *p; ``` - 第一行指令:`sti`,啟動處理器中斷;開機的時候在 *bootasm.S* 中將中斷禁止(`cli`),在 xv6 準備完成後重新開啟。 ```c=+ for(;;){ // Enable interrupts on this processor. sti(); ``` - *Scheduler* 找到一個`p->state`為`RUNNABLE`的 process,此時是唯一的:`initproc`。 ```c=+ // Loop over process table looking for process to run. acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->state != RUNNABLE) continue; ``` - 接著把 pre-cpu 的變量 `proc` 設為此 process。 - 呼叫 `switchuvm` 通知硬體開始使用目標 process 的 page table。 #### `switchuvm()` :::success **File:** vm.c ::: ```c // Switch TSS and h/w page table to correspond to process p. void switchuvm(struct proc *p) { pushcli(); cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0); cpu->gdt[SEG_TSS].s = 0; cpu->ts.ss0 = SEG_KDATA << 3; ltr(SEG_TSS << 3); if(p->pgdir == 0) panic("switchuvm: no pgdir"); lcr3(v2p(p->pgdir)); // switch to new address space popcli(); } ``` - `switchuvm` 同時設置好任務狀態段 `SEG_TSS`,讓硬體在 process 的 kernel stack 中執行 system call 與中斷。 ```c=+ // Switch to chosen process. It is the process's job // to release ptable.lock and then reacquire it // before jumping back to us. proc = p; switchuvm(p); ``` - 接著把 `p->state` 設為 `RUNNING`。 - 呼叫 `swtch`,context switch 到目標程序的 kernel thread。 ```c=+ p->state = RUNNING; swtch(&cpu->scheduler, proc->context); switchkvm(); // Process is done running for now. // It should have changed its p->state before coming back. proc = 0; } release(&ptable.lock); } } ``` ### File: swtch.S ```c= # Context switch # # void swtch(struct context **old, struct context *new); # # Save current register context in old # and then load register context from new. .globl swtch swtch: movl 4(%esp), %eax movl 8(%esp), %edx # Save old callee-save registers pushl %ebp pushl %ebx pushl %esi pushl %edi # Switch stacks movl %esp, (%eax) movl %edx, %esp # Load new callee-save registers popl %edi popl %esi popl %ebx popl %ebp ret ``` - `ret` 指令從 stack pop 目標程序的 `%eip`,結束 context switch。 - 現在處理器在程序 p 的 kernel stack 上執行。 - `allocproc` 把 `initproc` 的 `p->context->eip` 設為 `forkret`,使得 `ret` 開始執行 `forkret`。 - 第一次執行 `forkret` 時會呼叫一些初始化函數(`initlog`),接著返回。 #### `forkret()` :::success **File:** proc.c ::: | 功能 | 回傳值 | | --- | ------ | | - | void | ```c=320 // A fork child's very first scheduling by scheduler() // will swtch here. "Return" to user space. void forkret(void) { static int first = 1; // Still holding ptable.lock from scheduler. release(&ptable.lock); if (first) { // Some initialization functions must be run in the context // of a regular process (e.g., they call sleep), and thus cannot // be run from main(). first = 0; initlog(); } // Return to "caller", actually trapret (see allocproc). } ``` - 接著位於 p->context 的是 `trapret`。 - `%esp` 保存著 `p->tf`。 - `trapret` 恢復暫存器,如同 `swtch` 進行 context switch 一樣。 - `popal` 恢復通用暫存器 - `popl` 恢復`%gs`、`%fs`、`%es`、`%ds` - `addl` 跳過 `trapno` 和 `errcode` 兩個數據 - 最後 `iret` pop `%gs`、`%fs`、`%es`、`%ds` 出堆疊。 #### `trapret` :::success **File:** trapasm.S ::: ```c=26 # Return falls through to trapret... .globl trapret trapret: popal popl %gs popl %fs popl %es popl %ds addl $0x8, %esp # trapno and errcode iret ``` :::info - `iret`:interrupt return,程序返回中斷前的位址。 ::: - 處理器從 `%eip` 的值繼續執行,對於 `initproc` 即為虛擬地址 0,也就是 *initcode.S* 的第一條指令。 --- ## 第一個 system call:exec - *initcode.S* 第一件事是觸發 `exec` system call。 - `exec` 用一個新的程式代替當前 process 的記憶體及暫存器。 - 首先將`$argv`、`$init`、`$0` push 進堆疊,接著把 `%eax` 設為 `$SYS_exec`。 - 最後執行 `int $T_SYSCALL`。 - 這告訴 kernel 來運行 `exec`。 - 正常情況下,`exec` 不會返回;會運行名叫 `$init`(23) 的程式。 - `$init` 會 return `"/init\0"` - 若 `exec` 失敗了且返回,*initcode* 會不斷的呼叫一個 system call:`exit()`(17)。 ### File: initcode.S ```c= # Initial process execs /init. #include "syscall.h" #include "traps.h" # exec(init, argv) .globl start start: pushl $argv pushl $init pushl $0 // where caller pc would be movl $SYS_exec, %eax int $T_SYSCALL # for(;;) exit(); exit: movl $SYS_exit, %eax int $T_SYSCALL jmp exit # char init[] = "/init\0"; init: .string "/init\0" # char *argv[] = { init, 0 }; .p2align 2 argv: .long init .long 0 ``` ###### tags: `xv6` `kernel`