日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

基于 Bochs 的操作系统内核实现

發(fā)布時間:2025/6/15 windows 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于 Bochs 的操作系统内核实现 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

簡介

Bochs 簡介

Bochs(讀音Box)是一個開源的模擬器(Emulator),它可以完全模擬x86/x64的硬件以及一些外圍設(shè)備。與VirtualBox / VMware等虛擬機(Virtual Machine)產(chǎn)品不同,它的設(shè)計目標在于模擬一臺真正的硬件,并不追求執(zhí)行速度的高效,而追求模擬環(huán)境的真實,同時帶有強大的調(diào)試功能,比如觀察寄存器、對實地址/虛擬地址下斷點、裝載符號表等等。對于操作系統(tǒng)內(nèi)核的開發(fā)者而言,是一只不可多得的強力工具,通過簡單的設(shè)置,即可大大地降低內(nèi)核開發(fā)與調(diào)試的困難。

作為開源軟件,我們可以很方便地獲取它:

  • 主頁:http://bochs.sourceforge.net/getcurrent.html
  • 參考文檔:?http://wiki.osdev.org/Bochs

安裝

在Ubuntu操作系統(tǒng)下,可以通過apt-get來安裝:

sudo apt-get install bochs

若要利用Bochs的調(diào)試功能,則需要自己編譯安裝:

wget http://sourceforge.net/projects/bochs/files/bochs/2.5.1/bochs-2.5.1.tar.gz/download -O bochs.tar.gz tar -xvfz bochs.tar.gz cd bochs-2.5.1 ./configure --enable-debugger --enable-debugger-gui --enable-disasm --with-x --with-term make sudo cp ./bochs /usr/bin/bochs-dbg

配置

Bochs 提供了許多配置選項,在項目中,我們可以靈活的選擇/設(shè)置自己所需的功能,比如模擬器的內(nèi)存大小、軟/硬盤鏡像以及引導方式等等。而這些配置選項都統(tǒng)一在一個.bochsrc文件中,樣例如下:

.bochsrc:

# BIOS與VGA鏡像 romimage: file=/usr/share/bochs/BIOS-bochs-latest vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest # 內(nèi)存大小 megs: 128 # 軟盤鏡像 floppya: 1_44=bin/kernel.img, status=inserted # 硬盤鏡像 ata0-master: type=disk, path="bin/rootfs.img", mode=flat, cylinders=2, heads=16, spt=63 # 引導方式(軟盤) boot: a # 日志輸出 log: .bochsout panic: action=ask error: action=report info: action=report debug: action=ignore # 雜項 vga_update_interval: 300000 keyboard_serial_delay: 250 keyboard_paste_delay: 100000 mouse: enabled=0 private_colormap: enabled=0 fullscreen: enabled=0 screenmode: name="sample" keyboard_mapping: enabled=0, map= keyboard_type: at # 符號表(調(diào)試用) debug_symbols: file=main.sym # 鍵盤類型 keyboard_type: at

在啟動bochs時,使用命令:

bochs -q -f .bochsrc

內(nèi)置調(diào)試器

bochs內(nèi)置了強大且方便的調(diào)試功能。主要命令如下:

  • b,vb,lb?分別為物理地址、虛擬地址、邏輯地址設(shè)置斷點
  • c?持續(xù)執(zhí)行,直到遇到斷點或者錯誤
  • n?下一步執(zhí)行
  • step?單步執(zhí)行
  • r?顯示當前寄存器的值
  • sreg?顯示當前的段寄存器的值
  • info gdt,?info idt,?info tss,?info tab?分別顯示當前的GDT、IDT、TSS、頁表信息
  • print-stack?打印當前棧頂?shù)闹?/li>
  • help?顯示幫助

fleurix 簡介

fleurix 是一個簡單的單內(nèi)核(Monolithic Kernel)操作系統(tǒng)實現(xiàn),它的功能精簡但不失完整,代碼簡短(七千行C,二百多行匯編)且易于閱讀,可作為操作系統(tǒng)課程教學中的樣例系統(tǒng)。在設(shè)計時選擇采用了類UNIX的系統(tǒng)調(diào)用接口,因此在開發(fā)過程中可以獲取豐富的文檔供參考,也可以作為學習UNIX操作系統(tǒng)實現(xiàn)的一個參考材料。

fleurix 在編寫時盡量使用最簡單的方案。它假定CPU為單核心、內(nèi)存固定為128mb,前者可以簡化內(nèi)核同步機制的實現(xiàn),后者可以簡化內(nèi)存管理的實現(xiàn)。從技術(shù)角度來看,這些假定并不合理,但可以有效地降低剛開始開發(fā)時的復雜度。待開發(fā)進入軌道,也不難回頭解決。 此外,你也可以在源碼中發(fā)現(xiàn)許多窮舉算法——在數(shù)據(jù)量較小的前提下,窮舉并不是太糟的解決方案。

  • 主頁:?http://github.com/fleurer/fleurix
  • 開發(fā)環(huán)境: Ubuntu
  • 平臺:x86
  • 依賴:?bochs,?rake,?binutils,?nasm,?mkfs.minix

特性

  • minix v1的文件系統(tǒng)。原理簡單,而且可以利用linux下的mkfs.minix,fsck.minix等工具。
  • fork()/exec()/exit()等系統(tǒng)。可執(zhí)行文件格式為a.out,實現(xiàn)了寫時復制與請求調(diào)頁。
  • 信號。
  • 一個純分頁的內(nèi)存管理系統(tǒng),每個進程4gb的地址空間,共享128mb的內(nèi)核地址空間。至少比Linux0.11中的段頁式內(nèi)存管理方式更加靈活。
  • 一個簡單的kmalloc()。
  • 一個簡單的終端。

編譯運行

git clone git@github.com:Fleurer/fleurix.git cd fleurix rake

調(diào)試

# 需要自行編譯安裝帶調(diào)試功能的bochs-dbg,安裝步驟參見前文。 cd fleurix rake debug

設(shè)計與實現(xiàn)

編譯與鏈接

fleurix的內(nèi)核鏡像為裸的二進制文件,結(jié)構(gòu)大體如下:

(補圖)

Rakefile

對于項目中的一些日常性質(zhì)操作,比如:

  • 編譯bootloader,生成引導鏡像
  • 編譯并鏈接內(nèi)核,生成內(nèi)核鏡像
  • 生成符號表
  • 初始化根文件系統(tǒng),生成硬盤鏡像
  • 編譯整個項目,并運行bochs進行調(diào)試

它們需要的命令比較多,而且存在依賴關(guān)系,此任務(wù)必須在確保彼任務(wù)執(zhí)行完畢并成功之后才可以執(zhí)行。對此,比較通用的解決方案便是make,它可以自動分析任務(wù)之間的依賴關(guān)系再依次執(zhí)行,從而簡化日常操作的腳本編寫。但是make的語法比較晦澀,對于沒有任何基礎(chǔ)的初學者來講,上手起來并不容易。為此fleurix選擇了rake,它相當于make的ruby實現(xiàn),可以使用ruby語言的語法來編寫make腳本,好處是易于上手,而代價是不如make的語法簡潔。

fleurix中常用的rake命令有:

  • rake或者rake bochs,構(gòu)建整個項目并運行bochs
  • rake build,構(gòu)建整個項目到/bin目錄
  • rake debug,構(gòu)建整個項目并運行bochs的調(diào)試器
  • rake clean,將/bin目錄清空
  • rake nm,生成符號表
  • rake todo,列出代碼中遺留的待解決事項
  • rake werr,打開gcc的-Werror選項進行編譯,方便排除代碼中的warning
  • rake rootfs,構(gòu)建根文件系統(tǒng)
  • rake fsck,對根文件系統(tǒng)執(zhí)行fsck,檢查結(jié)構(gòu)是否正確

ldscript

內(nèi)核開發(fā)與應(yīng)用程序開發(fā)的不同之一便在于開發(fā)者需要對二進制鏡像的結(jié)構(gòu)有所了解,在必要時必須進行一些重定位。比如內(nèi)核的入口為0x100000,為此需要將入口的代碼(bin/entry.o)安排到內(nèi)核鏡像的最前方。而這便可以通過ldscript來完成,如下:

tool/main.ld:

ENTRY(kmain) SECTIONS {__bios__ = 0xa0000; # 綁定BIOS保留內(nèi)存的地址到__bios__ vgamem = 0xb8000; # 綁定vga緩沖區(qū)的地址到符號vgamem.text 0x100000 : { # 內(nèi)核二進制鏡像中的.text段(Section),從0x100000開始__kbegin__ = .; # 內(nèi)核鏡像的開始地址__code__ = .;bin/entry.o(.text) bin/main.o(.text) *(.text); # 將bin/entry.o中的.text段安排到內(nèi)核鏡像的最前方. = ALIGN(4096); # .text段按4kb對齊}.data : { __data__ = .;*(.rodata);*(.data);. = ALIGN(4096);}.bss : {__bss__ = .;*(.bss);. = ALIGN(4096);}__kend__ = .; # 內(nèi)核鏡像的結(jié)束地址 }

Rakefile中的相關(guān)命令如下,在鏈接時選擇tool/main.ld作為鏈接腳本:

sh "ld #{ofiles * ' '} -o bin/main.elf -e c -T tool/main.ld"

bootloader

bootloader是一段小程序,負責執(zhí)行一些初始化操作,并將內(nèi)核裝載到內(nèi)存,是內(nèi)核執(zhí)行的入口,也是內(nèi)核開發(fā)的第一步。

x86體系結(jié)構(gòu)的CPU在設(shè)計中為了保持向前兼容,在PC機電源打開之后,x86平臺的CPU會先進入實模式(Real Mode),并從0xFFF0開始執(zhí)行BIOS的一些初始化操作。隨后,BIOS將依次檢測啟動設(shè)備(軟盤或者硬盤)的第一個扇區(qū)(512字節(jié)),如果它的第510字節(jié)處的值為0xAA55,則認為它是一個引導扇區(qū),將它裝載到物理地址0x7C00,并跳轉(zhuǎn)到0x7C00處開始執(zhí)行。這便是bootloader的入口地址。

實模式中默認可用的地址總線為20位,可以尋址1mb的內(nèi)存,但寄存器只有16位。為此英特爾公司做出的設(shè)計是,在實模式的尋址模式中,令物理地址為16位段寄存器左移4位加16位邏輯地址的偏移所得的20位地址。若要訪問1mb之后的內(nèi)存,則必須開啟A20 Line開關(guān),將32位地址總線打開,并進入保護模式(Protect Mode)才可以。

在實模式中,0~4kb為中斷向量表保留,640kb~1mb為顯存與BIOS保留,實際可用的內(nèi)存只有636kb。考慮到日后內(nèi)核鏡像的體積有超過1mb的可能,所以將其裝載到物理地址1mb(0x100000)之后連續(xù)的一塊內(nèi)存中可能會更好。但實模式中并不可以訪問1mb以后的內(nèi)存,若要裝載內(nèi)核到物理地址1mb,一個解決方案便是在實模式中暫時將其裝載到一個臨時位置,待進入保護模式之后再移動它。

由上總結(jié)可知,bootloader所需要做的工作便依次為:

  • 裝載內(nèi)核鏡像到一個臨時的地址;
  • 進入保護模式;
  • 移動內(nèi)核鏡像;
  • 跳轉(zhuǎn)到內(nèi)核的入口。

相關(guān)代碼可見于?src/boot/boot.S?。

保護模式與GDT

x86的保護模式是對段尋址的增強,除去可以訪問32位的地址空間(4Gb)之外,更有了對保護級別(即ring0/ring1/ring2/ring3)的劃分、對內(nèi)存區(qū)域的限制、以及訪問控制。為實現(xiàn)這些功能,x86的做法是引入了GDT(Global Descriptor Table)。將每個段(Segments)的屬性對應(yīng)為GDT中的一項段描述符(Segment Descriptor),并通過段寄存器(如cs、ds、ss)中指明的選擇符進行選擇。GDT是駐留于內(nèi)存中的一個表,通過lgdt指令裝載到CPU。

在bootloader中進入保護模式的目的僅僅是為了訪問1mb以后的內(nèi)存,而且bootloader在完成引導系統(tǒng)之后即被視為廢棄,因此這里的GDT只能做臨時使用。其中含有兩個段描述符,它們的選擇符分別為0x08與0x10,分別用于內(nèi)核態(tài)代碼與數(shù)據(jù)的訪問。

進入內(nèi)核之后,fleurix會在gdt_init()中重新設(shè)置GDT(見scr/kern/seg.c)。

fleurix是一個純分頁的系統(tǒng),雖然并不需要段式的內(nèi)存管理,但依然需要一個GDT,只采用它的內(nèi)存保護功能,而繞過它的分段功能。在fleurix最終的GDT中,將只保留四個段描述符,它們的內(nèi)存區(qū)域皆為0~4Gb,選擇符分別為KERN_CS、KERN_DS、USER_CS與USER_DS——前兩者的權(quán)限為ring0,用于內(nèi)核態(tài)代碼與數(shù)據(jù)的訪問;后兩者的權(quán)限為ring3,分別用于用戶態(tài)代碼與數(shù)據(jù)的訪問——從而實現(xiàn)內(nèi)核態(tài)與用戶態(tài)的分離,使后者受到更多限制,將系統(tǒng)“保護”起來。

需要留意的是,除四個段描述符之外,fleurix的GDT中也帶有一個TSS描述符,其選擇符為(_TSS)。英特爾公司引入TSS機制的動機為實現(xiàn)硬件的任務(wù)切換,每個任務(wù)擁有一個TSS,在進程切換時,將當前進程的所有上下文保存在TSS中。比起軟件的任務(wù)切換,硬件任務(wù)切換的開銷相對比較大,而且沒有調(diào)試與優(yōu)化的余地。fleurix采用了軟件的任務(wù)切換機制,并無用到TSS的任務(wù)切換功能,但依然保留一個TSS是為了保存中斷處理時ss0與esp0兩個寄存器的值,在CPU通過中斷門或者自陷門轉(zhuǎn)移控制權(quán)時,據(jù)此獲取內(nèi)核棧的位置。

裝載內(nèi)核

在早期開發(fā)中為方便裝載,fleurix內(nèi)核的二進制鏡像被放置在軟盤鏡像中,自第二個扇區(qū)開始,大約為50kb。

在實模式中,可以通過調(diào)用13h號中斷來讀取軟盤扇區(qū),將內(nèi)核鏡像臨時讀取到物理地址0x10000處。在設(shè)置臨時的GDT之后,通過jmp指令進入保護模式,并將內(nèi)核拷貝至物理地址0x100000(1mb)處。

內(nèi)核初始化

待bootloader執(zhí)行完畢之后,內(nèi)核會首先進入kmain()(見src/kern/main.c),執(zhí)行一些初始化操作。這些操作依次為:

  • 清理屏幕(cls(),見src/chr/vga.c),初始化puts()與printk()等函數(shù)供調(diào)試與輸出使用。
  • 重新設(shè)置GDT(gdt_init(),見src/kern/seg.c)。
  • 初始化IDT(idt_init(),見src/kern/trap.c)。
  • 初始化內(nèi)存管理(mm_init(),見src/kern/pm.c)。
  • 初始化進程0(proc0_init(),見src/kern/proc.c)。
  • 初始化高速緩沖(buf_init(),見src/blk/buf.c)。
  • 初始化tty(tty_init(),見src/chr/tty.c)。
  • 初始化硬盤驅(qū)動(hd_init(),見src/blk/hd.c)。
  • 初始化內(nèi)核定時器(timer_init(),見src/kern/timer.c)
  • 初始化鍵盤驅(qū)動(keybd_init(),見src/chr/keybd.c)。
  • 開啟中斷(sti(),見src/inc/asm.h)。
  • 初始化進程1(kspawn(&init)),通過do_exec()(見src/kern/exec.c)即進入用戶態(tài)。

中斷處理

中斷是CPU中打斷當前程序的控制流以處理外部事件、報告錯誤或者處理異常的一種機制。若詳細分類,仍可將中斷分為三種:

  • 中斷(Interrupt):由CPU外部產(chǎn)生,CPU處于被動的位置,多用于CPU與外部設(shè)備的交互。
  • 自陷(Trap):在CPU本身的執(zhí)行過程中產(chǎn)生。一般由專門的指令有意產(chǎn)生,比如int $0x80,因此又被稱作"軟件中斷"。
  • 異常(Exception):因CPU執(zhí)行某指令失敗而產(chǎn)生,如除0、缺頁等等。與自陷的不同在于,CPU會在處理例程結(jié)束之后重新執(zhí)行產(chǎn)生異常的指令。

(注:即,自陷發(fā)生時,入棧的返回地址為下一條指令的地址;而異常發(fā)生時,入棧的返回地址為當前指令的地址)

在保護模式的x86平臺中,中斷通過中斷門(Interrupt Gate)轉(zhuǎn)移控制權(quán),自陷與異常通過自陷門(Trap Gate)轉(zhuǎn)移控制權(quán)。

每個中斷對應(yīng)一個中斷號,系統(tǒng)開發(fā)者可以將自己的中斷處理例程綁定到相應(yīng)的中斷號,表示中斷號與中斷處理例程之間映射關(guān)系的結(jié)構(gòu)被稱作中斷向量表(Interupt Vector Table)。在保護模式中的x86平臺,這一結(jié)構(gòu)的實現(xiàn)為IDT(Interrupt Descriptor Table)。與GDT類似,IDT也是一個駐留于內(nèi)存中的結(jié)構(gòu),通過lidt指令裝載到CPU。每個中斷處理例程對應(yīng)一個門描述符(Gate Descriptor)。在fleurix中初始化IDT的代碼位于idt_init()(見src/trap.c)。

在中斷發(fā)生時,CPU會先執(zhí)行一些權(quán)限檢查,若正常,則依據(jù)特權(quán)級別從TSS中取出相應(yīng)的ss與esp切換棧到內(nèi)核棧,并將當前的eflags、cs、eip寄存器壓棧(某些中斷還會額外壓一個error code入棧),隨后依據(jù)門描述符中指定的段選擇符(Segment Selector)與目標地址跳轉(zhuǎn)到中斷處理例程。 保存當前程序的上下文則屬于中斷處理例程的工作。在fleurix中,保存中斷上下文的操作由_hwint_common_stub(見src/kern/entry.S.rb)負責執(zhí)行,它會將中斷上下文保存到棧上的struct trap結(jié)構(gòu)(見src/inc/idt.h)。

在這里有三個地方值得留意:

  • 只有部分中斷會壓入error code,這會導致棧結(jié)構(gòu)的不一致。為了簡化中斷處理例程的接口,fleurix采用的方法是通過代碼生成,在中斷處理例程之初為不帶有error code的中斷統(tǒng)一壓一個雙字入棧,值為0,占據(jù)error code在struct trap中的位置。并將中斷調(diào)用號壓棧,以方便程序的編寫與調(diào)試。
  • fleurix中的中斷處理例程都經(jīng)過匯編例程_hwint_common_stub,它在保存中斷上下文之后,會調(diào)用hwint_common()(見src/kern/trap.c)函數(shù)。hwint_common()函數(shù)將依據(jù)中斷號,再查詢hwint_routines數(shù)組找到并調(diào)用相應(yīng)的處理例程。
  • 中斷的發(fā)生往往就意味著CPU特權(quán)級別的轉(zhuǎn)換,因此,可以將陷入(或稱"軟件中斷")作為用戶態(tài)進入內(nèi)核態(tài)的入口,從而實現(xiàn)系統(tǒng)調(diào)用。在fleurix中系統(tǒng)調(diào)用對應(yīng)的中斷號為0x80,與linux相同。

I/O

外部設(shè)備一般分為機械部分與電路部分。電路部分又被稱作控制器(Controller)或者適配器(Adapter),負責設(shè)備的邏輯與接口。

CPU一般都是通過寄存器的形式來訪問外部設(shè)備。外設(shè)的寄存器通常包括控制寄存器、狀態(tài)寄存器與數(shù)據(jù)寄存器三類,分別用于發(fā)送命令、讀取狀態(tài)、讀寫數(shù)據(jù)。按照訪問外設(shè)的寄存器的方式,CPU又主要分為兩類:

  • 將外設(shè)寄存器與內(nèi)存統(tǒng)一編址(Memory-Mapped):訪問寄存器即一般的內(nèi)存讀寫,沒有專門用于I/O的指令。
  • 將外設(shè)寄存器獨立編址(I/O-Mapped):每個寄存器對應(yīng)一個端口號(port),通過專門讀/寫的指令訪問外設(shè)的寄存器,如in與out指令。

x86是后者:采用獨立編址的方式,外設(shè)寄存器即I/O端口,并通過in、out等匯編指令進行讀寫。

在fleurix中,提供了如下的幾個函數(shù)來讀寫端口:

  • inb()與outb():按字節(jié)讀寫端口
  • inw()與outw():按字讀寫端口
  • insb()與outsb():對某端口讀/寫一個字節(jié)序列
  • insl()與outsl():對某端口讀/寫一個雙字的序列

以上函數(shù)都是對匯編指令的簡單包裝,定義于src/inc/asm.h。

留意它們的源代碼,可以注意到它們都會在最后調(diào)用一個io_delay()函數(shù)。這是因為對于一些老式總線的外部設(shè)備,讀寫I/O端口的速度若過快就容易出現(xiàn)丟失數(shù)據(jù)的現(xiàn)象,為此在每次I/O操作之間插入幾條指令作為延時,等待慢速外設(shè)。

PIT

fleurix通過Intel 8253 PIT(Programmable Interval Timer)定時器定時產(chǎn)生中斷,用于計時與進程調(diào)度。

Intel 8253 PIT芯片擁有三個定時器:作為系統(tǒng)時鐘,定時器1為歷史遺留中用于定期刷新DRAM,定時器2用于揚聲器。三個定時器分別對應(yīng)三個數(shù)據(jù)寄存器0x40、0x41、0x42,以及一個命令寄存器0x43。這里只需要關(guān)心計時器0的功能,用到的寄存器只有0x40與0x43。

定時器0的默認頻率為1193180HZ,可以通過如下的代碼調(diào)整它的頻率:

uint di = 1193180/HZ; outb(0x43, 0x36); outb(0x40, (uchar)(di&0xff)); outb(0x40, (uchar)(di>>8));

以上代碼摘自src/kern/timer中的timer_init()。其中HZ常量的值為100,如此設(shè)置,可使PIT在每100毫秒產(chǎn)生一次中斷,觸發(fā)中斷處理例程do_timer()。每次時鐘中斷被稱作一個節(jié)拍(tick)。

VGA

VGA(Video Graphics Array,視頻圖形陣列)是使用模擬信號的一種視頻傳輸標準,內(nèi)核可以通過它來控制屏幕上字符或者圖形的顯示。

在默認的文本模式(Text-Mode)下,VGA控制器保留了一塊內(nèi)存(0x8b000~0x8bfa0)作為屏幕上字符顯示的緩沖區(qū),若要改變屏幕上字符的顯示,只需要修改這塊內(nèi)存就好了。它可以被視作如下的一個二維數(shù)組,以表示屏幕上顯示的25x80個字符:

/* VGA is a memory mapping interface, you may view it as an 80x25 array* which located at 0x8b000 (defined in main.ld).* */ extern struct vchar vgamem[25][80];

其中每項的結(jié)構(gòu)如下:

struct vchar {char vc_char:8;char vc_color:4;char vc_bgcolor:4; };

其中,vc_char表示要顯示的字符內(nèi)容,vc_color與vc_bgcolor分別表示字符的顏色與字符的背景色。

除了字符的顯示,我們也希望能夠控制光標的位置,這里需要用到的是0x3D4與0x3D5兩個端口,相關(guān)代碼如下:

/* adjust the position of cursor */ void flush_csr(){uint pos = py * 80 + px;outb(0x3D4, 14);outb(0x3D5, pos >> 8);outb(0x3D4, 15);outb(0x3D5, pos); }

VGA內(nèi)部的寄存器多達300多個,顯然無法一一映射到I/O端口的地址空間。對此VGA控制器的解決方案是,將一個端口作為內(nèi)部寄存器的索引:0x3D4,再通過0x3D5端口來設(shè)置相應(yīng)寄存器的值。在這里用到的兩個內(nèi)部寄存器的編號為14與15,分別表示光標位置的高8位與低8位。

以上代碼皆可見于src/chr/vga.c。

系統(tǒng)調(diào)用

系統(tǒng)調(diào)用(System Call)即應(yīng)用程序訪問內(nèi)核的接口。

在fleurix中,每個系統(tǒng)調(diào)用對應(yīng)一個系統(tǒng)調(diào)用號,在調(diào)用時,將系統(tǒng)調(diào)用號放置于eax,將可能的參數(shù)放置于ebx、ecx、edx,最后通過int 0x80從而進入內(nèi)核并觸發(fā)中斷處理例程do_syscall(),繼而依據(jù)系統(tǒng)調(diào)用號查詢數(shù)組sys_routines[]找到對應(yīng)的處理例程并執(zhí)行。待執(zhí)行結(jié)束之后,將返回值放置于中斷上下文的eax寄存器中,并在出錯時設(shè)置errno。

為方便在應(yīng)用程序中調(diào)用系統(tǒng)調(diào)用,fleurix提供了四個宏_SYS0、_SYS1、_SYS2、_SYS4來生成系統(tǒng)調(diào)用的C接口。以_SYS3為例:

#define _SYS3(T0, FN, T1, T2, T3) \T0 FN(T1 p1, T2 p2, T3 p3){ \register int r; \asm volatile( \"int $0x80" \:"=a"(r) \:"a"(NR_##FN), \"b"((int)p1), \"c"((int)p2), \"d"((int)p3) \); \if (r<0){ \errno = -r; \return -1; \} \return r; \}... static inline _SYS3(int, write, int, char*, int); static inline _SYS3(int, read, int, char*, int); static inline _SYS3(int, lseek, int, int, int); ...

更具體的實例,可見于usr/目錄下的幾個應(yīng)用程序。

分頁

fleurix應(yīng)用x86平臺的分頁機制,實現(xiàn)了純頁式的內(nèi)存管理。與段式內(nèi)存管理(如DOS)或者段頁式混合的內(nèi)存管理(如linux0.11)相比,純頁式內(nèi)存管理(以下簡稱"頁式內(nèi)存管理")中可用的地址空間更大,也更加靈活。比如寫時復制與請求調(diào)頁這樣的機制,在段式內(nèi)存管理中則屬于不可能實現(xiàn)的。

按照x86平臺的分頁機制,內(nèi)存被劃分為4kb或者4mb大小的物理頁(又稱"頁框"),由頁表來表示虛擬頁到物理頁的映射關(guān)系。為節(jié)約頁表本身所占用的內(nèi)存,x86采用了二級頁表。每個頁表占4kb,含有1024條頁表項,可以映射4mb的地址空間;頁目錄也同樣4kb,含有1024項,可以映射4gb的地址空間。在進行地址翻譯時,將先查詢頁目錄,找到虛擬地址對應(yīng)的頁表,再在頁表中查詢得出相應(yīng)的物理頁,外加頁內(nèi)的偏移,最終得到物理地址。其中有個例外,便是4mb的大頁,頁目錄中的表項可以不指向一個頁表,而是僅僅表示一個4mb大頁的地址映射,它的便利之處在于映射大塊連續(xù)的地址空間,可以做到既方便又高效。

對于CPU來說,每次地址翻譯都到內(nèi)存中查詢頁表是不可容忍的高開銷,為此,支持分頁的CPU往往都提供了TLB(Translation Lookaside Buffer,俗稱"快表")作為頁表的緩存。在這里開發(fā)者需要留意的是,只要更新了頁表,便需要留意保持TLB的同步,不然就會有一些難于調(diào)試的問題出現(xiàn)。在bochs的內(nèi)嵌調(diào)試器中,可以通過info tab命令來檢查當前的頁面映射。

(注: 因為內(nèi)存局部性原理,TLB一般只需要很小(比如64項)即可達到不錯的效果。)

頁面可以被標記為只讀(Readonly)或者不存在(Non-Present),也可以設(shè)置頁面的保護級別。這一來在讀寫內(nèi)存時,如果發(fā)生不合法的內(nèi)存讀寫,就會產(chǎn)生一個頁面錯誤(Page Fault),觸發(fā)中斷處理例程do_pgfault()(見src/mm/pgfault.c)。這時產(chǎn)生頁面錯誤的地址,將被保存在cr2寄存器中,同時產(chǎn)生一個error code,表示頁面錯誤的類型。待頁面錯誤處理完成,被打斷的程序可以恢復執(zhí)行,也有可能因為嚴重的錯誤而中止(收到信號SIGSEGV)。

在fleurix中,每個進程擁有一個獨立的頁目錄,從而實現(xiàn)進程地址空間的隔離;通過4mb的大頁,實現(xiàn)虛擬地址與物理地址的一對一映射直到128mb為止,作為內(nèi)核地址空間;并過頁表項的保護級別,限制用戶態(tài)應(yīng)用程序?qū)?nèi)核地址空間的讀寫;通過將頁面標記為只讀或者不存在,實現(xiàn)寫時復制(Copy On Write)與請求調(diào)頁(Demand Paging)。 。

對于x86平臺,值得留意的地方有:

  • cr0寄存器中的Paging位表示分頁機制的開關(guān)(mmu_enable(), 見src/inc/asm.h);
  • cr4寄存器中的PSE位表示4mb大頁的開關(guān)(在一些較舊的CPU上并沒有PSE的支持);
  • 頁目錄的地址裝載于cr3寄存器(lpgd(),見src/inc/asm.h);
  • 頁面錯誤中的error code可能會有三種flag,即PFE_P、PFE_W與PFE_U(定義于src/inc/mmu.h),分別表示頁面不存在、頁面只讀及權(quán)限不足。
  • 只要重新裝載頁目錄,即為刷新TLB(flmmu(),見src/mm/pte.c)。

內(nèi)存分配

fleurix假定用戶的物理內(nèi)存為128mb,并將內(nèi)核永遠地映射于每個地址空間的低端(0~128mb),使得內(nèi)核地址空間中的虛擬地址與物理地址做到一對一的映射。這一來只要分配了物理頁面,內(nèi)核就可以直接讀寫它的內(nèi)容或?qū)⑺成涞接脩暨M程。需要留意的是,從技術(shù)角度這一假設(shè)并不合理:若用戶的物理內(nèi)存若小于128mb,內(nèi)核就會崩潰;若用戶的物理內(nèi)存大于128mb,則無法利用128mb以上的內(nèi)存。但它可以有效地降低項目開發(fā)之初的復雜度,待項目進入軌道,則應(yīng)優(yōu)先解決這一問題。

pgalloc()與pgfree()為內(nèi)核內(nèi)存分配的基礎(chǔ)例程,分別用于申請/釋放一個物理頁。一個物理頁面可能會被多個進程映射到,因此一個引用計數(shù)是必須的;物理頁面可能會比較多,使用窮舉式的分配效率不高。對此,fleurix實現(xiàn)了一個struct page結(jié)構(gòu)(定義于src/inc/page.h),并在內(nèi)核初始化時,初始化一個數(shù)組struct page coremap[NPAGE]與一個隊列struct page pgfreelist(見于src/mm/pm.c中的pm_init()),前者作為物理頁是否可用的標記,數(shù)組的每一項對應(yīng)一個物理頁,物理頁面的地址就等于數(shù)組下標 * 4kb,若對應(yīng)的struct page結(jié)構(gòu)中的引用計數(shù)為0,則表示物理頁是可用的;后者則將所有可用的物理頁組織到一個鏈表之中,這一來即可將分配/釋放物理頁的操作的時間復雜度降到O(1)。

kmalloc()

fleurix使用了一個簡單且高效的內(nèi)存分配算法,它將pgalloc()作為后端,能夠以O(shè)(1)的時間分配2次冪對齊的虛擬內(nèi)存塊,單次內(nèi)存分配的上限為4kb。

kmalloc()將固定大小的內(nèi)存塊(32b、64b、128b...4kb)分別組織為不同的鏈表。假如待分配的內(nèi)存塊大小為n,它會依據(jù)n來找到合適的鏈表(通過bkslot(),見于src/mm/malloc.c),其中內(nèi)存塊的大小為m(m為2次冪且n <= m <= 4096),然后檢查鏈表中是否有可用的內(nèi)存塊。如果有,就將它取出鏈表,直接返回;如果沒有,則通過pgalloc()分配一個物理頁,將它劃分為4096 / m個內(nèi)存塊并鏈到對應(yīng)的鏈表中,重復嘗試分配。

與C標準庫函數(shù)free()的不同在于,kfree()需要調(diào)用者記住內(nèi)存塊的大小,用以找到對應(yīng)的鏈表。這是個不好的設(shè)計,使用者若將大小寫錯就會有bug產(chǎn)生。另外值得留意的地方是,除了4kb內(nèi)存塊的特殊情況,kfree()不能將其它物理頁返還給操作系統(tǒng),而是留做以后內(nèi)存分配的保留內(nèi)存。這是一個不足之處,如果一次性分配比較多的臨時對象,將會造成較大的內(nèi)存浪費。

kmalloc()與kfree()都不會進入睡眠,如果物理內(nèi)存用盡則會產(chǎn)生一個panic(),這是編寫代碼時為方便調(diào)試而遺留的問題,也是亟需待改進的一個地方。

靜態(tài)內(nèi)存分配

對于內(nèi)核中常用數(shù)據(jù)結(jié)構(gòu)(比如struct inode、struct super等)的內(nèi)存分配,fleurix采用的還是早期UNIX的解決方案:某類對象單獨一個固定長度的數(shù)組,通過對象的一個標志判斷是否可用,如果可用,則為修改標志并返回;如果不可用,則根據(jù)情況進入睡眠或者返回錯誤。

這一方案的優(yōu)點在于幾乎沒有任何依賴,在內(nèi)核開發(fā)之初即可在一定程度上滿足內(nèi)存分配的需求。不足在于每類對象的分配都是代碼的重復,代碼的復用率很低。

改進方案就是采用slab算法,將不同的對象組織在不同的緩存之中,而將內(nèi)存分配的接口統(tǒng)一起來。

進程

進程即運行中的程序?qū)嶓w。每個進程擁有獨立的地址空間以及一些資源,相互并發(fā)執(zhí)行。在fleurix中,進程為代碼執(zhí)行與資源管理的基本單位。

終其一生,進程可能有五種狀態(tài):

  • SSLEEP: 睡眠且不可被信號喚醒,等待一個高優(yōu)先級的事件;
  • SWAIT: 睡眠,可被信號喚醒,等待一個低優(yōu)先級的事件;
  • SRUN: 正常執(zhí)行;
  • SZOMB: 僵尸進程,是在進程因為某種原因退出執(zhí)行(主動調(diào)用_exit()或者被信號殺死)、在被父進程回收之前的進程狀態(tài)。
  • SSTOP: 停止中,在進程創(chuàng)建之初以及進程回收時的進程狀態(tài)。

在fleurix中,表示進程的結(jié)構(gòu)為struct proc,它含有進程的pid(p_pid)、狀態(tài)(p_stat)、父進程id(p_ppid)、進程組(p_pgid)、用戶id(p_uid)、組id(p_gid)、地址空間(p_vm)、上下文(p_contxt)、打開的文件(p_ofile)、信號處理例程(p_sigact)、可執(zhí)行文件的inode(p_inode)等諸多信息,正是fleurix中最為復雜的結(jié)構(gòu)。

struct proc與這一進程的內(nèi)核棧同處一個物理頁,前者位于低端固定,后者位于高端向下增長。在這里不難發(fā)現(xiàn),內(nèi)核棧的可用空間非常小(小于4kb),因此在內(nèi)核開發(fā)中,應(yīng)尤其注意不要在棧上放置較大的對象,抑或進行較深的遞歸,不然內(nèi)核棧若溢出,絕不會像用戶態(tài)中那樣出現(xiàn)Segmentation Fault的提示,而會默默地搞亂內(nèi)核中的數(shù)據(jù)結(jié)構(gòu),出現(xiàn)一些難于調(diào)試的問題。

為方便對進程結(jié)構(gòu)的引用,fleurix設(shè)置了一個數(shù)組即struct proc *proc[NPROC],數(shù)組的下標即進程的pid,NPROC則為系統(tǒng)中進程數(shù)量的上限;以及一個指針struct proc *cu,永遠指向當前的進程結(jié)構(gòu)。

進程創(chuàng)建

進程只能通過fork()系統(tǒng)調(diào)用創(chuàng)建,它會復制當前進程的地址空間與資源,生成一個一模一樣的子進程。不過,fork()會在父進程中返回0,在子進程中則返回子進程的pid作為區(qū)別,如下:

int pid; if ((pid = fork()) == 0) {printf("I'm the parent process\n"); } else {printf("I'm the child process\n"); }

在fork()時,直接復制整個進程地址空間的操作是昂貴的,而且大多數(shù)子進程都會在執(zhí)行之初調(diào)用exec()覆蓋掉當前地址空間,之前的復制也就沒有意義了。對此,類UNIX系統(tǒng)大多基于CPU的分頁機制,提供了寫時復制的實現(xiàn):在復制進程地址空間時,并不直接拷貝地址空間中頁面的內(nèi)容,而是僅僅復制父進程的頁表,使得父子進程共享相同的物理頁,并將二者的虛擬頁面皆設(shè)置為只讀。隨后若二者任一方試圖修改內(nèi)存,則申請一個新的物理頁并復制舊頁的內(nèi)容。這里需要留意的是,為控制物理頁的共享,每個物理頁都需要維護一個引用計數(shù),當fork()時引用計數(shù)增1,當進程殺死或者發(fā)生寫時復制時減1,并在引用計數(shù)為0時釋放這個物理頁。

fork()的主要行為大致如下:

  • 申請pid與進程結(jié)構(gòu)
  • 設(shè)置ppid為父進程的pid
  • 復制用戶相關(guān)的字段,如p_pgrp、p_gid、p_ruid、p_euid、p_rgid、p_egid
  • 復制調(diào)度相關(guān)的字段,如p_cpu、p_nice、p_pri
  • 復制父進程的文件描述符(p_ofile),并增加引用計數(shù)
  • 復制父進程的信號處理例程(p_sigact)
  • 通過vm_clone()(見于src/mm/vm.c),復制父進程的地址空間(p_vm)
  • 復制父進程的寄存器狀態(tài)(p_contxt)
  • 復制父進程的中斷上下文,并設(shè)置tf->eax為0,使fork()在子進程中返回0。

fleurix在開始運行之初會初始化一個0號進程(proc0_init(),見于src/kern/fork.c),其后的所有進程皆由它fork而來。

程序執(zhí)行

exec()是fleurix中行為最為復雜的系統(tǒng)調(diào)用之一。就表面的行為而言,它會取一個可執(zhí)行文件的地址與相關(guān)參數(shù)(argv),并執(zhí)行它。然而在內(nèi)部,它所做的工作卻遠比表面上復雜:

  • 讀取文件的第一個塊,檢查Magic Number(NMAGIC)是否正確
  • 保存參數(shù)(argv)到臨時分配的幾個物理頁,其中的每個字符串單獨一頁
  • 清空舊的進程地址空間(vm_clear(),見于src/mm/vm.c),并結(jié)合可執(zhí)行文件的header,初始化新的進程地址空間(vm_renew(),見于src/mm/vm.c)
  • 將argv與argc壓入新地址空間中的棧
  • 釋放臨時存放參數(shù)的幾個物理頁
  • 關(guān)閉帶有FD_CLOEXEC標識的文件描述符
  • 清理信號處理例程
  • 通過_retu()返回用戶態(tài)

這里值得留意的是,之所以將argv保存到臨時分配的幾個頁面,是因為argv中的字符串與這個數(shù)組本身都是來自舊的地址空間,而舊的地址空間會被銷毀,argv所指向的內(nèi)存區(qū)域,自然也就無法訪問了。

與寫時復制的實現(xiàn)相似,exec()在執(zhí)行時,并不會立即將可執(zhí)行文件完全讀入內(nèi)存。而是通過vm_renew(),將當前進程的虛擬頁面統(tǒng)統(tǒng)設(shè)置為不存在,待進入用戶態(tài)開始執(zhí)行時,每發(fā)生一次頁面不存在的錯誤,便讀取一頁可執(zhí)行文件的內(nèi)容并映射。這樣的機制被稱作請求調(diào)頁(Demand Paging),好處是可以加速程序的啟動,不必等待可執(zhí)行文件完全讀入內(nèi)存即可開始程序的執(zhí)行,在某種意義上,也可以節(jié)約內(nèi)存的使用。缺點是如果程序的體積較小,就不如一次性將可執(zhí)行文件全部讀入內(nèi)存的方式高效。

為簡單起見,fleurix只支持a.out格式作為可執(zhí)行文件格式,對應(yīng)可執(zhí)行文件中不同的區(qū)段(section),進程的地址空間也分為不同的內(nèi)存區(qū)(VMA,Virutal Memory Area),如正文區(qū)(.text)、數(shù)據(jù)區(qū)(.data)、bss區(qū)(.bss)、堆區(qū)(.heap)與棧區(qū)(.stack)。它們的性質(zhì)各不相同:正文區(qū)與數(shù)據(jù)區(qū)內(nèi)容都來自可執(zhí)行文件,然而正文區(qū)是只讀的,數(shù)據(jù)區(qū)可讀可寫;bss區(qū)、堆區(qū)與棧區(qū)的內(nèi)存皆來自動態(tài)分配,都可讀可寫,不過bss區(qū)的內(nèi)存都默認為0,堆區(qū)可以通過brk()系統(tǒng)調(diào)用來調(diào)整它的長度,而棧區(qū)可以自動向下增長。對于這些不同需求,fleurix提供了一個結(jié)構(gòu)struct vma,它可以綁定一個inode,并在必要時依據(jù)相關(guān)的幾個標志(即VMA_RDONLY、VMA_STACK、VMA_ZERO、VMA_MMAP、VMA_PRIVATE)執(zhí)行不同的操作。具體可見于src/mm/pgfault.c文件中do_no_page()的相關(guān)代碼。

進程切換

負責進程切換的函數(shù)為swtch_to(),可見于src/kern/sched.c。內(nèi)容如下:

void swtch_to(struct proc *to){struct proc *from;tss.esp0 = (uint)to + PAGE; from = cu;cu = to;lpgd(to->p_vm.vm_pgd);_do_swtch(&(from->p_contxt), &(to->p_contxt)); }

_do_swtch()是一段匯編例程,它負責將當前的上下文保存到from->p_contxt,同時將to->p_contxt中保存的上下文恢復出來,也就是真正發(fā)生進程切換的地方。

fleurix采用軟件的進程切換,一切進程切換都發(fā)生在內(nèi)核態(tài)。結(jié)合swtch_to的源碼,已知進程的上下文有:

  • 內(nèi)核棧的頂,供中斷處理例程使用;
  • 頁目錄,也就是地址空間;
  • eip;
  • esp與所有其它通用寄存器(eax、ebx、ecx、edx、edi、esi、ebp)。

需要留意的是,依據(jù)gcc的調(diào)用約定,eax、ecx與edx為caller-saved registers,會在調(diào)用_swtch_to()時由調(diào)用者自動保存,不需要額外保存,因此內(nèi)核只需要保存ebx、ebp、edi、esi、esp五個通用寄存器。 另外,因為進程切換都發(fā)生在內(nèi)核態(tài),cs等段寄存器的內(nèi)容皆等同于常量,也無需保存。

fleurix將上下文相關(guān)的寄存器保存在一個struct jmp_buf結(jié)構(gòu)中,它與C標準庫中的jmp_buf基本相同,甚至可以這樣想:進程切換等價于在切換地址空間之后,為當前進程的上下文執(zhí)行setjmp()記錄下來,同時通過longjmp()跳轉(zhuǎn)到目標進程的上下文。

進程調(diào)度

fleurix采用傳統(tǒng)UNIX的優(yōu)先級調(diào)度算法。

在src/kern/proc.h中可以見到幾個默認的優(yōu)先級:PSWP、PINOD、PRIBIO、PPIPE、PRITTY、PWAIT、PSLEP、PUSER。其中除了優(yōu)先級最小的PUSER專用于CPU調(diào)度的基數(shù)之外,皆表示某事件的特定優(yōu)先級。

在進程結(jié)構(gòu)中,調(diào)度相關(guān)的字段只有三個(取值范圍皆為-126到127):

  • p_cpu:已執(zhí)行的時間片計數(shù);
  • p_nice:用戶通過nice()系統(tǒng)調(diào)用設(shè)置的微調(diào);
  • p_pri:進程的優(yōu)先值,優(yōu)先值越小優(yōu)先級越高。

內(nèi)核會隨著節(jié)拍(tick)增加當前進程的p_cpu,同時每隔一定時間便依據(jù)p_cpu與p_nice重新計算p_pri(可見于src/kern/timer.c中的sched_cpu())。公式大致為:

p->p_pri = p->p_cpu/16 + PUSER + p->p_nice;

也會調(diào)整所有進程的p_cpu,使得進程不至餓死:

p->p_cpu /= 2;

隨著時間的增加,當前進程的優(yōu)先級會慢慢地低于其它的任何進程。在這時調(diào)用swtch(),便可以找出當前優(yōu)先級最高的進程并切換。

值得留意的是,調(diào)用swtch()的時機有兩種:

  • 從內(nèi)核態(tài)返回用戶態(tài)的那一刻,發(fā)生進程搶占;
  • 進程主動調(diào)用,自愿放棄控制權(quán),一般是為了等待資源。
  • fleurix是非搶占的內(nèi)核,一切進程搶占都發(fā)生在內(nèi)核態(tài)返回用戶態(tài)的那一刻。這樣的考慮主要出于:

    • 來自PIT的時鐘中斷會定時觸發(fā)。
    • 一些中斷處理例程就在執(zhí)行結(jié)束之后,一般都會喚醒一些等待資源的進程。這些進程的優(yōu)先級都比較高(PRIBIO、PINO),在這時切換進程,可以使得相應(yīng)的資源在第一時間得到處理。

    相關(guān)代碼可見于src/kern/trap.c中hwint_common()的結(jié)尾處:

    setpri(cu); if ((tf->cs & 3)==RING3) {swtch(); }

    它首先嘗試調(diào)整當前進程的優(yōu)先級,再通過中斷上下文中保存的cs寄存器判斷當前的中斷上下文是否是來自用戶態(tài)。只有確定是來自用戶態(tài),才嘗試執(zhí)行任務(wù)切換,這樣可以保證內(nèi)核態(tài)中不會發(fā)生搶占。

    進程同步

    fleurix的內(nèi)核是非搶占的,但中斷處理例程依然有可能打斷內(nèi)核代碼的執(zhí)行。要保證代碼的一致性,可以通過cli()與sti()來關(guān)/開中斷形成一個臨界區(qū)。需要留意的是,開/關(guān)中斷的方式只在單處理器環(huán)境中適用,若支持多處理器,則需要提供自選鎖(spin lock)的實現(xiàn)。

    此外一個常見的情景是,在申請資源時若這一資源不可用,就讓這個進程進入睡眠(sleep())以等待資源的釋放,待資源恢復可用時,再喚醒(wakeup())所有等待該資源的進程恢復執(zhí)行。sleep()與wakeup()即為fleurix的基本同步原語。

    sleep()的代碼如下,取自src/kern/sched.c:

    /* mark a proccess SWAIT, commonly used on waiting a resource. **/ void sleep(uint chan, int pri){if (pri < 0) {cli();cu->p_chan = chan;cu->p_pri = pri;cu->p_stat = SSLEEP; // uninterruptiblesti();swtch();}else {if (issig())psig();cli();cu->p_chan = chan;cu->p_pri = pri;cu->p_stat = SWAIT; // interruptiblesti();if (issig()) psig();swtch();} }

    sleep()的第一個參數(shù)chan為"channel"的縮寫,表示等待的事件的標志符;第二個參數(shù)pri表示進程在喚醒那一刻的優(yōu)先級。依據(jù)優(yōu)先級的分類,睡眠又分為可中斷(interruptible)與不可中斷(uniterruptible)兩種,意指在睡眠中的進程若收到信號,是否中斷睡眠恢復執(zhí)行。

    wakeup()的代碼如下,取自src/kern/sched.c:

    void wakeup(uint chan){struct proc *p;int i;for(i=0; i<NPROC; i++){if ((p = proc[i]) == NULL) continue;if (p->p_chan == chan) {setrun(p);}} }

    它的內(nèi)容就是依據(jù)參數(shù)chan,找到所有因等待此事件而進入睡眠的進程并喚醒。

    設(shè)備

    設(shè)備(Device)即外部設(shè)備在內(nèi)核中的基本抽象,主要分為兩種:

    • 塊設(shè)備:將數(shù)據(jù)儲存在固定大小的塊中,每個塊都有自己的地址,可供驅(qū)動程序隨機訪問,如硬盤、光驅(qū)等;
    • 字符設(shè)備:輸入輸出都是不可以隨機訪問數(shù)據(jù)流,如鍵盤、打字機等。

    在fleurix中,塊設(shè)備與字符設(shè)備分別對應(yīng)struct bdevsw與struct cdevsw兩個結(jié)構(gòu)(定義于src/inc/conf.h),如下:

    struct bdevsw {int (*d_open)(); int (*d_close)();int (*d_request)(struct buf *bp);struct devtab *d_tab; };extern struct bdevsw bdevsw[NBLKDEV]; struct cdevsw {int (*d_open) (ushort dev);int (*d_close) (ushort dev);int (*d_read) (ushort dev, char *buf, uint cnt);int (*d_write) (ushort dev, char *buf, uint cnt);int (*d_sgtty)(); };extern struct cdevsw cdevsw[NCHRDEV];

    可以看出,兩個結(jié)構(gòu)的主要部分都是函數(shù)指針,它們就是設(shè)備驅(qū)動程序的統(tǒng)一接口了。

    類UNIX系統(tǒng)將設(shè)備文件(Device File)作為應(yīng)用程序訪問設(shè)備的接口,使得訪問外部設(shè)備與訪問一個普通的文件并無二致。設(shè)備文件本身并沒有任何內(nèi)容,真正發(fā)揮作用的只是設(shè)備類型與設(shè)備號——在讀寫設(shè)備文件時,內(nèi)核會依據(jù)它們來調(diào)用對應(yīng)的設(shè)備驅(qū)動程序(Device Driver),執(zhí)行真正的讀寫。

    在linux下可以通過mknod命令來創(chuàng)建一個設(shè)備文件,比如:

    mknod /dev/tty0 c 1 0

    Buffer Cache

    比起訪問內(nèi)存,訪問外部設(shè)備的速度往往要慢許多。為此,fleurix為塊設(shè)備實現(xiàn)了Buffer Cache,將最近讀過的塊緩存到內(nèi)存中,從而加快對塊設(shè)備的訪問。另外,Buffer Cache也扮演著I/O請求隊列的角色,從中斷中讀取的數(shù)據(jù)將直接寫入Buffer Cache中。

    Buffer Cache相關(guān)的結(jié)構(gòu)主要為struct buf、struct devtab,以及char buffers[NBUF][BUF]與bfreelist,其中每個struct buf都對應(yīng)著buffers[]中的一塊內(nèi)存,大小與文件系統(tǒng)的虛擬塊相同(1024字節(jié),可見于param.h中BLK的定義)。另外,它們主要構(gòu)成了三個buf對象的鏈表:

    • 空閑列表:表示當前系統(tǒng)中所有可用的buf,用于buf的分配。使用LRU(Last Recently Used)策略,分配一定是在鏈表的頭部取出,釋放則一般都是放回鏈表的尾部。鏈表的頭部為bfreelist,buf之間由av_prev和av_next連接;
    • 緩存列表:每個設(shè)備擁有獨立的緩存列表,盛放著設(shè)備所有的buf,用于buf緩存的查找。除非被標記為B_BUSY,buf可以同時存在于空閑列表和緩存列表。鏈表的頭部為struct devtab,由b_prev與b_next連接;
    • 請求隊列:同為每個設(shè)備獨立,表示該設(shè)備的I/O請求隊列。位于請求隊列中的buf會存在于設(shè)備的緩存列表,但不會存在于空閑列表。鏈表的頭部為struct devtab,由av_prev與av_next連接。

    可以認為,Buffer Cache為塊設(shè)備的讀寫提供了統(tǒng)一的接口,也為文件系統(tǒng)實現(xiàn)了所需的基礎(chǔ)例程。這些例程主要有:

    • getblk(dev, blknum):分配buf對象,并標記為B_BUSY;
    • brelse(buf):釋放buf對象,將其放回空閑列表;
    • bread(dev, blknum):讀取設(shè)備的塊,將buf對象插入設(shè)備的請求隊列,隨后進入睡眠等待讀取完畢;
    • bwrite(dev, buf):將buf對象中的內(nèi)容寫回設(shè)備。

    以上例程皆可見于src/blk/buf.c。

    需要留意的是,getblk()這個名字很容易給人一個錯誤的印象,實際上getblk()函數(shù)并不會讀取設(shè)備的塊(讀取設(shè)備塊的函數(shù)為bread()),它用于分配buf結(jié)構(gòu),并將其標記為B_BUSY:依據(jù)設(shè)備號和塊號,查找相應(yīng)的buf是否存在于緩存中,若存在,就直接返回它;若不存在,則從空閑列表中取出一個可用的buf對象返回。 具體起來,有如下五種情景:

  • 在設(shè)備的緩存列表中找到了對應(yīng)的buf對象,且正好可用,則返回這一buf并標記為B_BUSY(通過notavail()函數(shù));
  • 在設(shè)備的緩存列表中找到了對應(yīng)的buf對象,不過這個buf正忙(B_BUSY),則將這個buf標記為B_WANTED,令進程睡眠等待它被釋放;
  • 沒有在設(shè)備的緩存列表中找到對應(yīng)的buf對象,且bfreelist為空,則將整個bfreelist標記為B_WANTED,令進程睡眠等待它獲取可用的buf;
  • 沒有在設(shè)備的緩存列表中找到對應(yīng)的buf對象,且bfreelist不為空,則將頭部的buf取出bfreelist;
  • 若得到的buf對象被標記為B_DIRTY,則表示它有內(nèi)容發(fā)生變化,需要寫回到設(shè)備中。
  • 請求隊列

    設(shè)備在同一時刻一般只能處理一個請求,且時間較長。因此,合理的做法是將待處理的I/O操作排隊,在發(fā)出一個請求之后使進程進入睡眠等待中斷,待中斷發(fā)生時,讀取輸入并再次嘗試發(fā)送隊列中的請求,如是循環(huán)。

    在fleurix中,請求隊列并無專門的數(shù)據(jù)結(jié)構(gòu),而是直接利用struct devtab為隊列的頭部,struct buf作為隊列的成員,通過av_prev與av_next連接起來。

    以硬盤為例,發(fā)送I/O請求的例程為hd_request(),它取一個buf對象作為參數(shù),只負責將其插入hdtab的請求隊列。而真正依據(jù)請求隊列發(fā)送I/O請求的例程則為hd_start(),它取出隊列的頭部,依據(jù)buf對象的標志(B_READ或者B_WRITE)來發(fā)送讀請求或者寫請求,待設(shè)備在讀取/寫入完畢之后,就會觸發(fā)中斷處理例程do_hd_intr(),在這里將buf對象取出請求隊列,在讀取數(shù)據(jù)之后,喚醒等待在這一buf對象上的所有進程,并再次調(diào)用hd_start(),嘗試處理排隊中的I/O請求。

    以上例程皆定義于src/blk/hd.c。

    文件系統(tǒng)

    文件是對I/O的抽象,而文件系統(tǒng)提供了文件的組織方式。fleurix實現(xiàn)了minix v1文件系統(tǒng),它結(jié)構(gòu)簡單、易于實現(xiàn),而且在開發(fā)環(huán)境中也有豐富的工具可供使用,如mkfs.minix、fsck.minix等。

    超級塊

    超級塊表示了文件系統(tǒng)的基本信息,也描述了文件系統(tǒng)的存儲結(jié)構(gòu)。在內(nèi)核中,有時可以將超級塊視作文件系統(tǒng)的同義詞。

    minix v1文件系統(tǒng)主要分為六個部分,如下圖:

  • 引導塊,總是位于設(shè)備的第一個虛擬塊,為bootloader所保留;
  • 超級塊,位于第二個虛擬塊。它保存了一個文件系統(tǒng)的詳細信息,比如inode的數(shù)量、zone的最大數(shù)量等;
  • inode位圖,每個位對應(yīng)一個磁盤上的inode,表示它是否空閑,大小與超級塊中的s_nimap_blk字段相關(guān);
  • zone位圖,每個位對應(yīng)一個zone,表示它是否空閑,大小與超級塊中的s_nzmap_blk字段相關(guān)。zone是文件系統(tǒng)中虛擬塊的別名,一個zone可能等于1個物理塊,也可能等于2、4、8個物理塊的大小,具體由超級塊中s_log_bz字段指定。在fleurix中,zone等于兩個物理塊的大小(1024字節(jié))。
  • inode區(qū)域,儲存著文件系統(tǒng)中所有的inode,大小與超級塊中的s_max_inode字段相關(guān)。
  • 數(shù)據(jù)區(qū)域,也就是文件系統(tǒng)中所有的虛擬塊,供inode引用。
  • 在fleurix中有兩個數(shù)據(jù)結(jié)構(gòu)與超級塊相關(guān):struct d_super與struct super,皆定義于src/inc/super.h,分別對應(yīng)磁盤上與內(nèi)存中超級塊的表示。后者多了幾個字段來表示掛載信息。

    與struct buf結(jié)構(gòu)類似,內(nèi)存中也有一個固定的struct super mnt[NMNT]數(shù)組(定義于src/fs/mount.c),用作super對象的分配與緩存。然而更重要的用途,則為內(nèi)核中所有文件系統(tǒng)的掛載表。

    在類UNIX系統(tǒng)中若要訪問一個文件系統(tǒng),必先將它掛載(Mount)。在fleurix中,對應(yīng)的例程為do_mount(uint dev, struct inode *ip)。它取兩個參數(shù),第一個參數(shù)為文件系統(tǒng)的設(shè)備號,第二個參數(shù)指向掛載目標的inode。它會首先遍歷mnt[]數(shù)組,查找設(shè)備號對應(yīng)的super對象是否已存在,若存在,則直接跳轉(zhuǎn)至_found;若不存在,則選出一個空閑的super對象,讀取磁盤上的超級塊并跳轉(zhuǎn)至_found。隨后,它會依據(jù)設(shè)備號判斷是否為根文件系統(tǒng),并增加掛載目標的引用計數(shù)。

    與之相對,卸載一個文件系統(tǒng)的例程為do_umount(ushort dev)。它會將設(shè)備號對應(yīng)的super對象寫回到磁盤,隨后釋放它,并減少目標inode的引用計數(shù)。

    除do_mount()與do_umount()之外,有關(guān)超級塊的例程還有:

    • getsp(),依據(jù)設(shè)備號,獲取一個已掛載的super對象并上鎖;
    • unlk_sp(),釋放一個super對象的鎖;
    • spload(),讀取磁盤中的超級塊到super對象;
    • spupdate(),將super對象的改動寫回磁盤。

    以上例程皆定義于src/fs/super.c。

    塊分配

    minix文件系統(tǒng)采用位圖來表示文件系統(tǒng)中空閑的塊,一個位對應(yīng)著數(shù)據(jù)區(qū)域的一個邏輯塊,1表示已占用,0表示可用。每個塊對應(yīng)著一個塊號,最小為0,最大為數(shù)據(jù)區(qū)中塊的數(shù),在文件系統(tǒng)格式化時確定。表示塊號的類型為unsigned short(16位)。

    在fleurix中,通過balloc()(定義于src/fs/alloc.c)來分配塊。它取一個設(shè)備號做參數(shù),通過查詢位圖找到并返回文件系統(tǒng)中第一個可用的塊號。若沒有可用的塊,則報告一個錯誤。

    與之相對,釋放塊的例程為bfree()。

    inode

    在類UNIX操作系統(tǒng)中,普通文件、目錄或者文件系統(tǒng)中的其它對象皆由一個統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)inode來表示。它記錄了文件的類型、用戶信息、訪問權(quán)限、修改日期等信息,也記錄了文件中邏輯塊的布局,但并不包含本文件的名字信息。

    每個inode擁有一個唯一的編號,作為內(nèi)核訪問inode的憑據(jù)。編號從1開始,最大為文件系統(tǒng)中inode的數(shù)量,在文件系統(tǒng)格式化時確定。表示inode編號的類型為unsigned short(16位)。

    同超級塊類似,fleurix中有兩個數(shù)據(jù)結(jié)構(gòu)與inode相關(guān):struct d_inode與struct inode,皆定義于src/inc/inode.h,分別對應(yīng)磁盤上與內(nèi)存中inode的表示。后者增加了引用計數(shù)、設(shè)備號、inode編號與標志等信息。

    在fleurix中,要訪問一個inode對象,可以通過iget()例程(定義于src/fs/inode.c),它取一個設(shè)備號與inode編號做參數(shù),返回一個上鎖的inode對象。大體行為如下:

  • 依據(jù)設(shè)備號與inode編號,遍歷inode[]數(shù)組判斷inode對象是否位于緩存;
  • 若位于緩存且無鎖,則增加引用計數(shù)(使i_count增1)并上鎖(通過lock_ino(),定義于src/fs/inode.c)后返回;
  • 若位于緩存但上鎖,則進入睡眠等待inode對象釋放,重復步驟1;
  • 若沒有位于緩存,則分配一個空閑的inode對象,讀取磁盤中的inode對象,重復步驟1;
  • 若沒有位于緩存,且沒有空閑的inode對象,則報告一個錯誤。
  • 與之相對,釋放inode對象的例程為iput(),它會將i_count減一,當i_count為0時釋放inode對象。

    除了i_count,inode結(jié)構(gòu)還有一個字段i_nlink,用于 表示磁盤上的引用數(shù),也就是硬連接的數(shù)量。當新建一個文件時,i_nlink的值為1,隨后每增加一個硬連接時增1,刪除時減1,當i_nlink為0時才真正刪除磁盤上的inode。

    此外值得注意的是,iput()與unlk_ino()雖同為"釋放一個inode對象",但含義有所不同。準確來講,unlk_ino()的行為是釋放一個inode對象的鎖,iput()則是根據(jù)引用計數(shù)來釋放inode對象本身。鎖的目的是限制對象的控制權(quán),保護對象的數(shù)據(jù)不被破壞,內(nèi)核必須在系統(tǒng)調(diào)用的結(jié)束之前及時地釋放鎖,不然將導致死鎖;而引用計數(shù)的目的是跟蹤對象的所有權(quán)的變化,來管理對象的生存周期。

    bmap()

    文件是組織虛擬I/O的一種方式,每個文件都可以視作是獨立的一段地址空間。fleurix通過bmap()(定義于src/fs/bmap.c)將文件中的偏移地址翻譯為設(shè)備的物理塊號,而inode在這里就扮演了翻譯表的角色。

    如上圖,minix v1文件系統(tǒng)采用了傳統(tǒng)UNIX文件系統(tǒng)的分組多級中間表,默認只提供7個邏輯塊的映射,若文件增長超過7個塊的大小,則分配一個塊作為中間表,額外提供512個塊(即NINDBLK,定義于src/inc/param.h,等于BLK / sizeof(unsigned short))的映射。如果文件更長,就采用二級中間表,這樣最大可以支持262663個塊(7+512+512*512)的映射,也就是說,單個文件最大限制約為256mb(MAX_FILESIZ,定義于src/inc/param.h)。

    bmap(struct inode *ip, ushort nr, uchar creat)取三個參數(shù),第一個參數(shù)ip指向一個上鎖的inode對象,第二個參數(shù)nr表示文件中虛擬塊的偏移,第三個參數(shù)creat表示查找過程中是否申請新的塊。當查找失敗時若creat為0,會返回0表示映射不存在;若creat不為0,則申請一個塊并繼續(xù)查找。值得一提的是,文件系統(tǒng)中的一切塊分配皆發(fā)生于設(shè)置creat標志時的bmap()。

    bmap()主要用于read()、write()與lseek()等系統(tǒng)調(diào)用的實現(xiàn),也在內(nèi)核中讀取文件時有所使用。

    namei()

    前面曾提到,inode結(jié)構(gòu)并沒有保存本文件的名字信息。所有文件的文件名,以及文件目錄之間的層級關(guān)系,都保存在目錄類型(S_IFDIR,定義于src/inc/stat.h)的inode中。每個文件系統(tǒng)的第1個inode都是目錄類型。

    目錄的數(shù)據(jù)布局與普通文件一致,不同在于數(shù)據(jù)的內(nèi)容。目錄文件的格式可以視作是struct dirent結(jié)構(gòu)的一個數(shù)組,表示了一個目錄中文件名到inode編號的映射關(guān)系。struect dirent的聲明為:

    #define NAMELEN 12struct dirent {ushort d_ino;char d_name[NAMELEN];char __p[18]; /* a padding. each dirent is aligned with a 32 bytes boundary. */ };

    每條dirent(目錄項)占據(jù)32字節(jié),其中inode編號為2字節(jié),文件名為12字節(jié),保留16字節(jié)。可知在minix v1文件系統(tǒng)中,文件名的大小限制為12個字符。

    而namei(char *path, uchar creat)所做的工作就是,根據(jù)路徑依次查找目錄文件,并在必要時新建inode,最后返回一個上鎖的inode對象或在出錯時返回NULL。此外,內(nèi)核有時會對父目錄的內(nèi)容更感興趣(比如通過unlink()來刪除一個文件),對此fleurix也提供了一個namei_parent(char *path, char **name)例程,它可以返回父目錄的inode對象,同時找到指向目標文件名的指針。

    在查找過程中需要小心地處理inode對象的鎖和一些異常情況,namei()與namei_parent()中有關(guān)遍歷目錄的代碼會很復雜,所以將它們分開實現(xiàn)是沒有意義的。為此,fleurix實現(xiàn)了_namei(char *path, uchar creat, uchar parent, char **name)作為namei()與namei_parent()所共有的基礎(chǔ)例程。它的4個參數(shù)的意義分別為:

    • path:目標文件的路徑,若path為絕對路徑(以'/'開頭),_namei()將從根目錄開始查找,否則從當前的活動(cu->p_wdir)目錄開始查找;
    • creat:若最后沒有找到對應(yīng)的文件,則新建一個文件;
    • parent:若不為0,則返回父目錄的inode對象,同時將目標文件名的地址存入第四個參數(shù)name;
    • name:當parent不為0時,保存目標文件名的地址。

    _namei()主要用于link()、unlink()、open()、exec()等系統(tǒng)調(diào)用的實現(xiàn),凡是需要訪問文件路徑的地方,就都會調(diào)用到它。

    遇到的問題

    總結(jié)

    以上是生活随笔為你收集整理的基于 Bochs 的操作系统内核实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。