OS 必須具備三項技能:多工、獨立及交流。
kernel 組織#
- Monolithic kernel:整個 OS 都位於 kernel 中,如此一來所有 system calls 都會在 kernel 中執行(XV6)。
- 好處
- 設計者不須決定 OS 的哪些部份不需要完整的硬體特權。
- 更方便的讓不同部份的 OS 去合作。
- 壞處
- 通常在不同部份的 OS 中的介面是複雜的。
- 這會容易讓開發者出錯。
- 好處
- Microkernel:為了減少 kernel 出錯的風險,設計者可以將 kernel mode 上執行的 OS 程式碼最小化,並大讓OS 在 user mode 中執行。
Process 概觀#
- 為 UNIX(XV6) 中的一個獨立單元。
- 確保一個 process 不會破壞或是竊取另一程序的記憶體、CPU、檔案描述符等等。
- 亦確保 kernel 不會被破壞。
- Process 為抽象的,這讓一個程式可以假設它佔有一台虛擬機器,即一個接近私有的記憶體或是 address space,其他的 process 不可以 r/w。
- 私有的 adderss space 由不同的 page table 實做,即一個 process 有一個 page table
- 每個 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
:保存 process 的 page table。
Code: 第一個 address space#
- XV6 為 kernel 建立第一個 address space 的流程:
- 開機
- 初始化自己
- 從硬碟中讀取 boot loader 至記憶體中執行。
- Boot loader 從硬碟讀取 kernel 並從 entry.s 開始執行。
- Boot loader 會把 XV6 的 kernel 載入實體位址 0x100000。
- 為了讓剩下的 kernel 能夠執行,設置一個 page table,將虛擬位址 0x80000000(KERNBASE)映射到實體位址 0x0。將兩個虛擬位址映 到同一個實體位址是 page 的常見手法。
- 跳到 kernel 的 c code,並在高位址上執行:
%esp
指向高位址的 stack 記憶體。- 跳到高位址的 main。
File: entry.s
1 | _start = V2P_WO(entry) |
Code: 建立第一個 process#
File: proc.c
- 呼叫
userinit()
來建立第一個 process(只有在第一個process時會呼叫)。 - 呼叫
allocproc()
(每個 process 都會呼叫)。 Allocproc
在 process table 中分配一個 slot(struct proc
),並初始化有關 kernel thread 的 process 片段。Allocproc
掃描 proc tabel,找到p->state
是UNUSED
,接著設定為EMBRYO
來標示被使用,並給予一組唯一的 pid。
allocproc#
功能 | 回傳值 |
---|---|
建立一個 process | process 結構 |
29 | // Look in the process table for an UNUSED proc. |
- 接著嘗試請求分配一個 kernel stack,如果失敗,把
p->state
改回UNUSED
。
50 | // Allocate kernel stack. |
- Allocproc 通過設定返回程式計數器的值來導致新 process 的 kernel thread 會先在 forkret 中執行,再回到 trapret。
- Kernel thread 從 p->context 的拷貝開始執行,因此設定 p->context->eip 指向 forkret 會導致 kernel thread 從 forkret 的開頭開始執行。
60 | // Set up new context to start executing at forkret, |
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 會用到的記憶體。
功能 | 回傳值 |
---|---|
建立系統的初始 process | void |
userinit#
79 | void |
inituvm
請求一個 page 大小的實體記憶體,將虛擬記憶體 0 映射到此記憶體,並將_binary_initcode_start_
及_binary_initcode_size_
拷貝到 page。
88 | inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size); |
- 把 trap frame 設定為初始使用者模式。
89 | p->sz = PGSIZE; |
p->name
設為"initcode"
是為了 debug,p->cwd
設在 process 的現在目錄。
98 | safestrcpy(p->name, "initcode", sizeof(p->name)); |
- 設定
p->state
為RUNNABLE
。
100 | p->state = RUNNABLE; |
Code: 執行第一個 process#
- 當 main 呼叫完 userinit 後,呼叫 mpmain,mpmain 接著呼叫 scheduler 開始運行 process。
File: main.c
功能 | 回傳值 |
---|---|
完成多核心開機程序 | void |
55 | // Common CPU setup code. |
File: proc.c
功能 | 回傳值 |
---|---|
執行調度,指定執行的 process | void |
249 | //PAGEBREAK: 42 |
- 第一行指令:
sti
,啟動處理器中斷;開機的時候在 bootasm.S 中將中斷禁止(cli
),在 XV6 準備完成後重新開啟。
261 | for(;;){ |
- Scheduler 找到一個
p->state
為RUNNABLE
的 process,此時是唯一的:initproc
。
264 | // Loop over process table looking for process to run. |
- 接著把 pre-cpu 的變量
proc
設為此 process。 - 呼叫
switchuvm
通知硬體開始使用目標 process 的 page table。
369 | // Switch to chosen process. It is the process's job |
- 接著把
p->state
設為RUNNING
。 - 呼叫
swtch
,context switch 到目標程序的 kernel thread。
274 | p->state = RUNNING; |
File: vm.c
163 | // Switch TSS and h/w page table to correspond to process p. |
switchuvm
同時設置好任務狀態段SEG_TSS
,讓硬體在 process 的 kernel stack 中執行 system call 與中斷。
177 | // Switch to chosen process. It is the process's job |
- 接著把
p->state
設為RUNNING
。 - 呼叫
swtch
,context switch 到目標程序的 kernel thread。
182 | p->state = RUNNING; |
File: swtch.S#
1 | # Context switch |
ret
指令從 stack pop 目標程序的%eip
,結束 context switch。- 現在處理器在程序 p 的 kernel stack 上執行。
allocproc
把initproc
的p->context->eip
設為forkret
,使得ret
開始執行forkret
。- 第一次執行
forkret
時會呼叫一些初始化函數(initlog
),接著返回。
File: proc.c
功能 | 回傳值 |
---|---|
- | void |
320 | // A fork child's very first scheduling by scheduler() |
- 接著位於 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
出堆疊。
File: trapasm.S
# Return falls through to trapret... |
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#
1 | # Initial process execs /init. |