CPU 執行一個 process 時,是不斷的進行:讀取指令、增加程式計數器、執行指令的迴圈;但有時候一個程式需要進入 kernel,而不是執行下一行指令;包括:設備信號的發出、使用者程式做一些非法的事或是呼叫一個 system call。
- 處理上述情況有三大挑戰:
- Kernel 需使處理器能夠從 user mode 轉至 kernel mode(再轉回來)。
- Kernel 及設備須協調好他們平行的活動。
- Kernel 需了解設備的介面。
System call,例外及中斷#
- 有三種情況須從 user 轉至 kernel:
- system call:使用者程式要求 OS 服務。
- 例外 exception:程式執行非法動作(如除零)。
- 中斷 interrupt:設備發出一個信號來引起 OS 注意。
- 所有中斷由 kernel 管理。
- OS 必須在此三種情況保證以下事情:
- 保存暫存器以備將來的狀態回復。
- 系統需準備好在 kernel 中執行。
- 選擇一個 kernel 開始的位置。
- Kernel 能夠取得此事件的資訊。
- 保證安全性(獨立)。
- XV6 使用的方法概述:
- 一個中斷停止了處理器的迴圈,並開始執行 interrupt handler。
- 在開始執行 interrupt handler 之前,處理器儲存他的暫存器。
- Trap:當前 process 引起
- 中斷:由設備引起
XV6 用 trap 來表示中斷,這是因為此術語被 PDP11/40 使用,也是 UNIX 的傳統術語。
X86 的保護機制#
- x86 有 4 個 protection level,0(最高)至 3(最低)。
- 實際上大部分只使用兩個層級:0(kernel mode)及 3(user mode);當前的層級儲存在
%cs
的 CPL 中。 - Interrupt handler 在 IDT 中被定義。
- IDT interrupt descriptor table:有 256 格,每一格都提供了相對應的
%cs
及%eip
。 - 呼叫一個 system call 需要呼叫一個
int n
指令,n 為 IDT 的索引;int n
進行下面步驟:- 從 IDT 獲得第 n 個描述符
- 檢查
%cs
中的 CPL 是否 <= DPL,DPL 為描述符的層級 - 如果目標的段選擇器的 PL < CPL,儲存 CPU 內部的
%esp
及%ss
- 讀取 task segment descriptor 的
%ss
及%esp
- Push
%ss
、%esp
、%eflags
、%cs
及%eip
- 清除
%eflags
的 IF bit
Code: 第一個 system call#
File: initcode.S#
8 | start |
- Process 將
exec
的參數 push 進堆疊,並將 system call number 放進%eax
。 SYS_exec
即為 system call number,對應到 syscalls 的陣列索引(syscall.c 中),一個函數指標陣列。
File: syscall.c
102 | static int (*syscalls[])(void) = { |
Code: Assembly trap handler#
- x86 提供 256 種中斷,0-31 為軟體異常。
- XV6 將 32-63 給硬體中斷,64 作為 system call。
- Main 呼叫
tvinit
。
File: trap.c
功能 | 回傳值 |
---|---|
初始化 trap handler | void |
1 | void |
Tvinit
設置idt
表的 256 項。
6 | for(i = 0; i < 256; i++) |
- 接著執行
T_SYSCALL
,user 會呼叫trap
(將1
傳入SETGATE
的第二變數來指定為 trap gate)。 - Trap gate 不會清除 IF bit。
- 並將 system call 的權限設為
DPL_USER
,允許使用者程式使用int
指令產生trap
;XV6 不允許 process 用int
產生其他中斷,如果這麼做會拋出錯誤並產生 13 號中斷。
8 | SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER); |
File: mmu.h
213 |
Trap 發生時#
- user mode:從 task segment descriptor 讀取
%esp
、%ss
, 接著 push 舊的%ss
、%esp
進新的堆疊。 - kernel mode:不用上述動作。
- 接著 push
%eflags
、%cs
、%eip
。 - 從對應的 IDT 讀取
%eip
、%cs
。
File: vector.pl#
1 | #!/usr/bin/perl -w |
- XV6 用 Perl 腳本來生成 IDT 的進入點(
vector[]
)。 - 如果處理器沒有 push 錯誤碼,則在其項 push。
- Push 中斷號碼,跳至
alltraps
。
File: trapret.S#
1 | #include "mmu.h" |
- 接著繼續 push
%ds
、%es
、%fs
、%gs
及通用暫存器,現在 kernel stack 包含一個struct trapframe
。
12 | # Set up data and per-cpu segments. |
- push
%esp
(trap frame),呼叫 trap。
24 | # Return falls through to trapret... |
- trap return 後跳回 user space。
File: x86.h
150 | struct trapframe { |
Code: C trap handler#
trap#
功能 | 回傳值 | *tf |
---|---|---|
執行 trap | void | trapframe |
36 | void |
- 如果是
TY_SYSCALL
,呼叫 syscall()。
48 | switch(tf->trapno){ |
- 檢查是否為硬體中斷
79 | //PAGEBREAK: 13 |
- 如果非 system call 或硬體中斷,trap 就認定為一個錯誤:
- user:cp->killed (ch5)
- kernel:panic
Code: System calls(機制)#
File: syscall.c
- 從 trap frame 中的
%eax
讀取 system call 號碼,及對應 syscall table 的索引。 - 如果 system call 號碼是非法的,
return -1
。
syscall#
功能 | 回傳值 |
---|---|
執行 system call | void |
126 | void |
102 | static int (*syscalls[])(void) = { |
File: syscall.h
1 | // System call numbers |
- 取得 system call 參數:
argint
:整數argptr
:指標argstr
:字串argfd
:檔案描述符
Code: interrupts#
PIC#
- 早期主機板(單核心)上有一塊 PIC,code: picirq.c
- 多核心主機板的每顆 CPU 都需要一個 PIC,需要一個方法來分發中斷,操作方式分為兩部份:
- IO APIC (ioapic.c):於 I/O 系統上
- Local APIC (lapic.c):與每個 CPU 有關
- IO APIC 包含一張表,處理器可以通過記憶體映射 I/O 來寫其中的一項。
- 在初始化時,XV6 將 0 中斷映射到 CR0,以此類推,但將其關閉。
- 不同的設備自己開啟自己的中斷,同時指定接收中斷的處理器。
%eflags
的 IF bit 是處理器用來控制是否要接收中斷,cli
清除 IF 來關閉中斷,sti
打開。
Code: 硬碟驅動程式#
- 硬碟驅動程式用
struct buf
來表示一個磁碟區
File: buf.h#
1 | struct buf { |
flags
紀錄記憶體與硬碟的關係:B_VALID
表示已被讀入B_DIRTY
表示資料須被寫出B_BUSY
為一個鎖,代表別的 process 正在使用此 buf
- main 呼叫 ideinit 初始化硬碟驅動程式
File: ide.c
功能 | 回傳值 |
---|---|
初始化 IDE | void |
45 | void |
- 呼叫
picenable
打開單處理器的中斷 - 呼叫
ioapicenable
打開多處理器的中斷(只打開最後一個 CPU)
54 | idewait(0); |
idewait
等待硬碟接受命令,直到 busy 位(IDE_BUSY
)被清除,ready 位(IDE_DRDY
)被設置。
55 | // Check if disk 1 is present |
- 設置完成後,只能通過 buffer cache 調用
iderw
,iderw
根據flags
值更新一個鎖著的 buf:- B_DIRTY:將 buf 寫回硬碟
- 若 B_VALID 未設置:從硬碟讀資料進 buf
功能 | 回傳值 | checkerr |
---|---|---|
等待 IDE | void | 錯誤碼 |
50 | // Wait for IDE disk to become ready. |
功能 | 回傳值 | *b |
---|---|---|
讀寫 IDE | void | 欲寫入或讀取的 buffer |
126 | void |
- 把 buf b 放置隊伍的末端
145 | // Start disk if necessary. |
- 如果此 buf 在隊首,呼叫
idestart
將其送到硬碟。 - 其他情況需等上一個處理完畢時才處理。
148 | // Wait for request to finish. |
iderw
將請求加入的隊伍裡,並睡眠,等待 interrupt handler 處理完後更新其 flags。- 最後,硬碟完成其工作並觸發一個中斷,trap 呼叫
ideintr
來處理。
功能 | 回傳值 |
---|---|
IDE trap | void |
91 | void |
- 查詢隊首的 buf,如果正在被寫入,且 IDE 有資料在等待,呼叫
insl
將資料寫入。
108 | // Wake process waiting for this buf. |
- 設置
B_VALID
,清除B_DIRTY
。 - 喚醒
b
112 | // Start disk on next buf in queue. |
- 最後將下一個 buf 傳給硬碟。