XV6操做系統是MIT 6.828課程中使用的教學操做系統,是在現代硬件上對Unix V6系統的重寫。XV6總共只有一萬多行,很是適合初學者用於學習和實踐操做系統相關知識。git
MIT 6.828的課程網站是https://pdos.csail.mit.edu/6.828/。XV6操做系統有官方文檔,英文版在前面的網站能夠下載,中文版翻譯參見https://th0ar.gitbooks.io/xv6-chinese/content/。github
在閱讀XV6操做系統代碼前,須要熟練掌握C語言,瞭解有關X86體系結構的基本知識,操做系統相關的基本概念,以及關於編譯、連接相關的基本知識。關於相關理論知識,我的推薦的教材是文末的參考文獻[1]、[2]。此外,閱讀過程當中可能遇到不少新概念,熟練掌握Google和Stack Overflow也是必須的。其中,尤爲有用的資料是OS Dev Wiki和x86指令手冊。最後,推薦能熟練使用某種代碼編輯器,提高本身閱讀代碼的效率。數據結構
在操做系統中,內核態指的是操做系統內核在運行時系統的狀態,在這個狀態下,內核程序具備訪問任何已有硬件和執行任何已有指令的權限;用戶態指的是用戶進程在執行時系統的狀態,在這個狀態下,用戶進程只能執行一部分指令,按照操做系統提供的系統調用來訪問硬件和與其餘進程交互。將內核態與用戶態隔離是爲了提高系統總體的安全性和健壯性,避免惡意進程和出錯進程破壞系統。編輯器
中斷是一種能讓操做系統響應外部硬件的機制,好比說,在一個用戶進程執行時,另外一個用戶進程請求的磁盤文件加載完畢,那麼須要設計一箇中斷信號來通知操做系統,暫停當前用戶進程,讓操做系統處理這個中斷事件;而系統調用則是使得用戶進程可以陷入內核態,請求某種系統服務的機制,好比利用系統提供的syscall指令陷入內核,爲進程完成須要內核權限的輸入輸出任務,而後返回用戶態,進程繼續執行。函數
計算機在運行時,經過CPU內某些寄存器的權限位來得知當前是處於內核態仍是用戶態。好比,在x86系統中,CPU經過檢查%cs寄存器內的CPL位,來檢查當前指令的執行權限級別。在XV6系統中,CPL0表明內核態,CPL3表明用戶態。若是指令的執行權限不符合CPL位的值,那麼就會產生一個通用保護異常(General Protection Fault)。學習
ELF是Unix系統中主要被使用的可執行文件格式,詳細信息能夠參考https://en.wikipedia.org/wiki/Executable_and_Linkable_Format。在bootmain()函數中,涉及到了ELF中兩個重要的概念,ELF Header和Program Header。ELF Header記錄了ELF文件相關的基本信息,其中包含一組Program Header,每一個Program Header記錄ELF文件中的一段代碼或者數據的具體位置和大小等基本信息。Program Header所指向的ELF段包括.text .data等。bootmain()函數就是先從加載到內存0x10000地址處的ELF Header中得到全部Program Header的信息,而後將這些Program段依次從磁盤加載到內存中。經過readelf命令,能夠查看內核究竟有哪些Program Header,獲得結果以下:網站
Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 80100000 001000 008111 00 AX 0 0 4 [ 2] .rodata PROGBITS 80108114 009114 000672 00 A 0 0 4 [ 3] .stab PROGBITS 80108786 009786 000001 0c WA 4 0 1 [ 4] .stabstr STRTAB 80108787 009787 000001 00 WA 0 0 1 [ 5] .data PROGBITS 80109000 00a000 002596 00 WA 0 0 4096 [ 6] .bss NOBITS 8010b5a0 00c596 00715c 00 WA 0 0 32 [ 7] .debug_line PROGBITS 00000000 00c596 001f8c 00 0 0 1 [ 8] .debug_info PROGBITS 00000000 00e522 00a965 00 0 0 1 [ 9] .debug_abbrev PROGBITS 00000000 018e87 0026ed 00 0 0 1 [10] .debug_aranges PROGBITS 00000000 01b578 0003a0 00 0 0 8 [11] .debug_loc PROGBITS 00000000 01b918 002f30 00 0 0 1 [12] .debug_str PROGBITS 00000000 01e848 000cdc 01 MS 0 0 1 [13] .comment PROGBITS 00000000 01f524 00001c 01 MS 0 0 1 [14] .debug_ranges PROGBITS 00000000 01f540 000018 00 0 0 1 [15] .shstrtab STRTAB 00000000 01f558 0000a5 00 0 0 1 [16] .symtab SYMTAB 00000000 01f8d0 0023d0 10 17 138 4 [17] .strtab STRTAB 00000000 021ca0 0012d0 00 0 0 1
在源代碼中,XV6系統的啓動運行軌跡如圖。系統的啓動分爲如下幾個步驟:ui
首先,在bootasm.S
中,系統必須初始化CPU的運行狀態。具體地說,須要將x86 CPU從啓動時默認的Intel 8088 16位實模式切換到80386以後的32位保護模式;而後設置初始的GDT(詳細解釋參見https://wiki.osdev.org/Global_Descriptor_Table),將虛擬地址直接按值映射到物理地址;最後,調用bootmain.c
中的bootmain()
函數。this
bootmain()
函數的主要任務是將內核的ELF文件從硬盤中加載進內存,並將控制權轉交給內核程序。具體地說,此函數首先將ELF文件的前4096個字節(也就是第一個內存頁)從磁盤裏加載進來,而後根據ELF文件頭裏記錄的文件大小和不一樣的程序頭信息,將完整的ELF文件加載到內存中。而後根據ELF文件裏記錄的入口點,將控制權轉交給XV6系統。
entry.S
的主要任務是設置頁表,讓分頁硬件可以正常運行,而後跳轉到main.c
的main()
函數處,開始整個操做系統的運行。
main()
函數首先初始化了與內存管理、進程管理、中斷控制、文件管理相關的各類模塊,而後啓動第一個叫作initcode
的用戶進程。至此,整個XV6系統啓動完畢。
XV6的操做系統的加載與真實狀況有一些區別。首先,XV6操做系統做爲教學操做系統,它的啓動過程是相對比較簡單的。XV6並不會在啓動時對主板上的硬件作全面的檢查,而真實的Bootloader會對全部鏈接到計算機的全部硬件的狀態進行檢查。此外,XV6的Boot loader足夠精簡,以致於可以被壓縮到小於512字節,從而可以直接將Bootloader加載進0x7c00的內存位置。真實的操做系統中,一般會有一個兩步加載的過程。首先將一個加載Bootloader的程序加載在0x7c00處,而後加載進完整的功能複雜的Bootloader,再使用Bootloader加載內核。
bootmain()
函數詳解void bootmain(void) { struct elfhdr *elf; struct proghdr *ph, *eph; void (*entry)(void); uchar* pa; elf = (struct elfhdr*)0x10000; // scratch space // Read 1st page off disk readseg((uchar*)elf, 4096, 0); // Is this an ELF executable? if(elf->magic != ELF_MAGIC) return; // let bootasm.S handle error // Load each program segment (ignores ph flags). ph = (struct proghdr*)((uchar*)elf + elf->phoff); eph = ph + elf->phnum; for(; ph < eph; ph++){ pa = (uchar*)ph->paddr; readseg(pa, ph->filesz, ph->off); if(ph->memsz > ph->filesz) stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz); } // Call the entry point from the ELF header. // Does not return! entry = (void(*)(void))(elf->entry); entry(); }
bootmain.c
中的bootmain()
函數是XV6系統啓動的核心代碼。bootmain()
函數首先從磁盤中讀取第一個內存頁(11行);而後判斷讀取到的內存頁是不是ELF文件的開頭(14-15行);若是是的話,根據ELF文件頭內保存的每一個程序頭和其長度信息,依次將程序讀入內存(18-25行);最後,從ELF文件頭內找到程序的入口點,跳轉到那裏執行(29-30行)。經過readelf
命令能夠獲得ELF文件中程序頭的詳細信息。總而言之,boot loader在XV6系統的啓動中主要用來將內核的ELF文件從硬盤中加載進內存,並將控制權轉交給內核程序。
經過獲取struct elfhdr
中struct proghdr
的位置和大小信息(18-19行,elf->phoff
elf->phnum
),就能得知XV6內核程序段(Program Header)的位置和數量,在加載硬盤扇區的過程當中,逐步向前移動ph
指針,一個個加載對應的程序段。對於一個程序段,經過ph->filesz
和ph->off
得到程序段的大小和位置,使用readseg()
函數來加載程序段,逐步向前移動pa
指針,直到加載進的磁盤扇區使得加載進的扇區大小超過程序文件的結尾epa
,從而完成單個程序段的加載。對於單個內核程序段,代碼確保它會填滿最後一個內存頁。
中斷描述符表是X86體系結構中保護模式下用來存放中斷服務程序信息的數據結構,其中的條目被稱爲中斷描述符。在XV6數據結構中,涉及的數據結構以下
// Gate descriptors for interrupts and traps struct gatedesc { uint off_15_0 : 16; // low 16 bits of offset in segment uint cs : 16; // code segment selector uint args : 5; // # args, 0 for interrupt/trap gates uint rsv1 : 3; // reserved(should be zero I guess) uint type : 4; // type(STS_{IG32,TG32}) uint s : 1; // must be 0 (system) uint dpl : 2; // descriptor(meaning new) privilege level uint p : 1; // Present uint off_31_16 : 16; // high bits of offset in segment }; struct gatedesc idt[256]; extern uint vectors[];
其中,struct gatedesc的格式與X86體系結構所要求的徹底相同https://wiki.osdev.org/Interrupt_Descriptor_Table。對於第\(i\)條中斷描述符,CS寄存器存儲的是內核代碼段的段編號SEG_KCODE,offset部分存儲的是vector[i]的地址。在XV6系統中,全部的vector[i]地址均指向trapasm.S中的alltraps函數。
因爲中斷機制是由CPU硬件支持的,因此計算機在運行階段一開始時,BIOS就開啓並支持中斷。可是,在XV6系統的啓動過程當中,第一條指令就使用cli指令來屏蔽中斷,直到第一個進程調度時纔會在scheduler()裏使用STI指令容許硬件中斷。在容許硬件中斷以前,必須先配置好中斷描述符表,具體的實如今tvinit()和idtinit()函數中
void tvinit(void) { int i; for(i = 0; i < 256; i++) SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0); SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER); initlock(&tickslock, "time"); } void idtinit(void) { lidt(idt, sizeof(idt)); }
在XV6系統中,只有中斷和系統調用機制能夠實現用戶態到內核態的轉變。所以,即便是第一個用戶進程啓動時,XV6系統也會在內核態手動構建Trap Frame,設置Trap Frame中的CS寄存器上的相關權限位,而後調用中斷返回函數進入用戶態。XV6中的硬件中斷都是使用CTI和STI指令來進行開關。在實際的計算機中,中斷分爲外部中斷和內部中斷。外部中斷包括來自外部IO設備的中斷、來自時鐘的中斷、斷電信號等,外部中斷又分爲可屏蔽中斷和不可屏蔽中斷。對於內部中斷,包括由軟件調用INT指令觸發的中斷和由CPU內部錯誤(指令除零等)觸發的中斷。
以除零錯誤爲例。當XV6的指令執行中遇到除零錯誤時,首先CPU硬件會發現這個錯誤,觸發中斷處理機制。在中斷處理機制中,硬件會執行以下步驟:
此時,因爲CS已經被設置爲描述符中的值(SEG_KCODE),因此此時已經進入了內核態,而且EIP指向了trapasm.S中alltraps函數的開頭。在alltrap函數中,系統將用戶寄存器壓棧,構建Trap Frame,而且設置數據寄存器段爲內核數據段,而後跳轉到trap.c中的trap函數。在trap函數中,首先經過檢查中斷調用號,發現這不是一個系統調用,也不是一個外部硬件中斷,所以進入以下代碼段:
if(myproc() == 0 || (tf->cs&3) == 0){ // In kernel, it must be our mistake. cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n", tf->trapno, cpuid(), tf->eip, rcr2()); panic("trap"); } // In user space, assume process misbehaved. cprintf("pid %d %s: trap %d err %d on cpu %d " "eip 0x%x addr 0x%x--kill proc\n", myproc()->pid, myproc()->name, tf->trapno, tf->err, cpuid(), tf->eip, rcr2()); myproc()->killed = 1;
根據觸發中斷的是內核態仍是用戶進程,執行不一樣的處理。若是是用戶進程出錯了,那麼系統會殺死這個用戶進程;若是是內核進程出錯了,那麼在輸出一段錯誤信息後,整個系統進入死循環。
若是是一個能夠修復的錯誤,好比頁錯誤,那麼系統會在處理完後返回trap()函數進入trapret()函數,在這個函數中恢復進程的執行上下文,讓整個系統返回到觸發中斷的位置和狀態。
在Linux系統中,setrlimit系統調用的做用是設置資源使用限制。咱們以setrlimit爲例,要在XV6系統中添加一個新的系統調用,首先在syscall.h中添加一個新的系統調用的定義
#define SYS_setrlimit 22
而後,在syscall.c中增長新的系統調用的函數指針
static int (*syscalls[])(void) = { ... [SYS_setrlimit] sys_setrlimit, };
固然如今sys_setrlimit這個符號還不存在,所以在sysproc.c中聲明並實現這個函數
int sys_setrlimit(int resource, const struct rlimit *rlim) { // set max memory for this process, etc }
最後,在user.h中聲明setrlimit()這個函數系統調用函數的接口,並在usys.S中添加有關的用戶系統調用接口。
SYSCALL(setrlimit) int setrlimit(int resource, const struct rlimit *rlim);
這個問題事實上涉及到了不少關於x86的底層實現的細節。在80386中,硬件對內存訪問支持保護模式,在32位保護模式中,CPU使用Global Descriptor Table來存儲有關內存段的信息,使用CS寄存器來存儲GDT的索引,經過這個方式來索引內存段的過程當中,能夠經過GDT中的相應位來設置這塊內存的權限。注意,這與操做系統的虛擬內存是相互獨立的兩個機制。對於XV6系統而言,GDT中只有5個描述符,分別是內核代碼段、內核數據段、用戶代碼段、用戶數據段和TSS,對應的定義以下
// various segment selectors. #define SEG_KCODE 1 // kernel code #define SEG_KDATA 2 // kernel data+stack #define SEG_UCODE 3 // user code #define SEG_UDATA 4 // user data+stack #define SEG_TSS 5 // this process's task state
在中斷切換的時候,須要從用戶代碼段切換到內核代碼段,所以須要保存CS的值,在中斷返回的時候再彈出。此外,中斷描述符表中的CS寄存器的值指明瞭中斷處理程序應該使用的CS值,也就是對應的內存段。
代碼的執行權限由CS寄存器中的權限位標記。在中斷調用時,INT指令會保存原來的CS寄存器,讀入新的CS寄存器,從而維持中斷先後的代碼執行權限不變。對於第一個用戶進程的而言,須要在啓動前手動設置CS寄存器的相關權限位才行,具體的代碼片斷以下
p->tf->cs = (SEG_UCODE << 3) | DPL_USER; p->tf->ds = (SEG_UDATA << 3) | DPL_USER;