XV6 Ch3 Trap,中斷及驅動程式
CPU 執行一個 porcess 時,是不斷的進行:讀取指令、增加程式計數器、執行指令的迴圈;但有時候一個程式需要進入 kernel,而不是執行下一行指令;包括:設備信號的發出、使用者程式做一些非法的事或是呼叫一個 system call。
- 處理上述情況有三大挑戰:
- Kernel 需使處理器能夠從 user mode 轉至 kernel mode(再轉回來)。
- Kernel 及設備須協調好他們平行的活動。
- Kernel 需了解設備的介面。
System call,例外及中斷
- 有三種情況須從 user 轉至 kernel:
- system call:使用者程式要求 OS 服務。
- 例外 exception:程式執行非法動作(如除零)。
- 中斷 interrupt:設備發出一個信號來引起 OS 注意。
- 所有中斷由 kernel 管理。
- OS 必須在此三種情況保證以下事情:
- 保存暫存器以備將來的狀態回復。p
- 系統需準備好在 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
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
Code: 第一個 system call
File: initcode.S
- Process 將
exec
的參數 push 進堆疊,並將 system call number 放進 %eax
。
SYS_exec
即為 system call number,對應到 syscalls 的陣列索引(syscall.c 中),一個函數指標陣列。
syscalls[]
Code: Assembly trap handler
- x86 提供 256 種中斷,0-31 為軟體異常。
- xv6 將 32-63 給硬體中斷,64 作為 system call。
- Main 呼叫
tvinit
。
tvinit()
- 接著執行
T_SYSCALL
,user 會呼叫 trap
(將 1
傳入 SETGATE
的第二變數來指定為 trap gate)。
- Trap gate 不會清除 IF bit。
- 並將 system call 的權限設為
DPL_USER
,允許使用者程式使用 int
指令產生 trap
;xv6 不允許 process 用 int
產生其他中斷,如果這麼做會拋出錯誤並產生 13 號中斷。
SETGATE()
Trap 發生時
- user mode:從 task segment descriptor 讀取
%esp
、%ss
, 接著 push 舊的 %ss
、%esp
進新的堆疊。
- kernel mode:不用上述動作。
- 接著 push
%eflags
、%cs
、%eip
。
- 從對應的 IDT 讀取
%eip
、%cs
。
- xv6 用 Perl 腳本來生成 IDT 的進入點(
vector[]
)。
- 如果處理器沒有 push 錯誤碼,則在其項 push。
- Push 中斷號碼,跳至
alltraps
。
File: trapret.S
- 接著繼續 push
%ds
、%es
、%fs
、%gs
及通用暫存器,現在 kernel stack 包含一個 struct traprframe
。

struct trapframe
- push
%esp
(trap frame),呼叫 trap。
- trap return 後跳回 user space。
Code: C trap handler
trap()
- 如果是
TY_SYSCALL
,呼叫 syscall()。
- 如果非 system call 或硬體中斷,trap 就認定為一個錯誤:
- user:cp->killed (ch5)
- kernel:panic
Code: System calls(機制)
syscall()
- 從 trap frame 中的
%eax
讀取 system call 號碼,及對應 syscall table 的索引。
- 如果 system call 號碼是非法的,
return -1
。
syscalls[]
File: syscall.h
- 取得 system call 參數:
argint
:整數
argptr
:指標
argstr
:字串
argfd
:檔案描述符
Code: interrupts
Programmable Interrupt Controler 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
flags
紀錄記憶體與硬碟的關係:
B_VALID
表示已被讀入
B_DIRTY
表示資料須被寫出
B_BUSY
為一個鎖,代表別的 process 正在使用此 buf
- main 呼叫 ideinit 初始化硬碟驅動程式
ideinit()
- 呼叫
picenable
打開單處理器的中斷
- 呼叫
ioapicenable
打開多處理器的中斷(只打開最後一個 CPU)
idewait
等待硬碟接受命令,直到 busy 位(IDE_BUSY
)被清除,ready 位(IDE_DRDY
)被設置。
idewait()
- 設置完成後,只能通過 buffer cache 調用
iderw
,iderw
根據 flags
值更新一個鎖著的 buf:
- B_DIRTY:將 buf 寫回硬碟
- 若 B_VALID 未設置:從硬碟讀資料進 buf
iderw()
- 如果此 buf 在隊首,呼叫
idestart
將其送到硬碟。
- 其他情況需等上一個處理完畢時才處理。
iderw
將請求加入的隊伍裡,並睡眠,等待 interrupt handler 處理完後更新其 flags。
- 最後,硬碟完成其工作並觸發一個中斷,trap 呼叫
ideintr
來處理。
ideintr()
- 查詢隊首的 buf,如果正在被寫入,且 IDE 有資料在等待,呼叫
insl
將資料寫入。
- 設置
B_VALID
,清除 B_DIRTY
。
- 喚醒
b