一個玩具OS, 32bit 最終實現了一個簡易的可交互的Shell 
TO-DO List:
一共分主次兩個硬盤,系統本身安裝於主盤,採用的是MBR的引導模式,MBR->Boot Loader->Kernnel的過程
MBR位於磁盤LBA 0号扇区開始的1個扇區內
Boot Loader位於磁盤LBA 2号扇区開始的4個扇區內
Kernel位於磁盤LBA 9号扇区開始的200個扇區內
文件系統實現在從盤。這裡可能不是很合理,若是按照商業系統的邏輯應該是現實現文件系統,再在相應分區安裝操作系統
內存分頁,一頁為4Kb
內存管理採用位圖管理,分配內存時按大小區分,大於1024字節的直接按頁分配
若是小於1024字節,則在按頁分配arena的基礎上,用arena中的空閒塊鏈進行分配和控制
為了實現方便,雖然開啟了分頁機制,但是並沒有實現內存頁與磁盤上的交換功能

PCB為1頁大小
線程的調度,核心本質是通過時鐘中斷控制ESP指針切換來切換PCB,優先級的體現在於每個線程的運行時間片的長短
進程的實現基於線程,其中TSS的選擇上仿效Linux,採用單TSS備份0級棧以及0級棧指針的做法。和線程最大的不同是進程的PCB中擁有頁表地址,這也正是進程和線程最大的不同,進程真正擁有自己的獨立虛擬內存空間
調度上沒有用什麼高效的算法,直接用隊列循環調度
idle線程的實現
idle線程的實現很簡陋,第一次得到調度時,將自己阻塞讓出CPU,當調度器再次執行調度時,若在ready隊列中沒有發現就緒的線程或進程,就喚醒idle線程,此時idle線程通過hlt掛起CPU,當時間片用完,CPU還沒有發現有ready的進程或線程,則繼續將idle線程換上CPU,此時idle又將繼續把自己阻塞,然後開始重複上面的調度過程
// 空载任务
static void idle ( void * arg ) {
while ( 1 ) {
thread_block ( TASK_BLOCKED );
asm volatile ( "sti; hlt" : : : "memory" );
}
}進程fork
進程的fork,先複製當前進程的PCB,然後再通過當前進程的虛擬池位圖建立一個新頁表,其中虛擬地址的對應和原進程中一模一樣,最後偽造一個中斷現場,將子進程加入到調度隊列中等待調度執行。偽造的中斷現場中,子進程的PCB裡的eax修改成了0,代表新進程中拿到fork的返回值為0,而父進程的PCB中的eax不變,代表著子進程的pid。父進程是通過系統調用結束返回,而子進程是直接通過中斷退出函數返回
進程exec
exec的實現,首先將elf文件從磁盤加載到內存,然後改變當前進程的PCB中的進程名,並把待執行進程所需的參數放入到約定的寄存器中,並將eip修改成elf的entry point,偽造中斷現場,通過直接調用中斷退出函數intr_exit來立即執行新進程。
其中ELF的Entry Point是通過自己實現一個極其簡陋的CRT來實現的,其中給出了一個_start入口,並push約定的參數寄存器到3級棧中,通過call來調用外部命令的main函數來實現參數傳遞
[bits 32]
extern main
extern exit
; 这是一个简易版的CRT
; 如果链接时候ld不指定-e main的话,那ld默认会使用_start来充当入口
; 这里的_start的简陋实现,充当了exec调用的进程从伪造的中断中返回时的入口地址
; 通过这个_start, 压入了在execv中存放用户进程参数的两个寄存器。然后call 用户进程main来实现了向用户进程传递参数
section .text
global _start
_start:
;下面这两个要和 execv 中 load 之后指定的寄存器一致
push ebx ;压入 argv
push ecx ;压入 argc
call main
; 压入main的返回值
push eax
call exit ; 不再返回,直接调度别的进程了,这个进程直接被回收了進程wait
在fork且調用exec執行一個本地命令之後,為了不出現殭屍進程,父進程需要在本地wait子進程結束。
這裡的實現是進入sys_wait系統調用後,遍歷全進程隊列,找到父進程是自己的掛起狀態的進程,然後取得他pcb中的返回值,回收pcb和頁目錄表,並將其從調度隊列中剔除。若遍歷之後沒發現掛起的子進程,則將自己阻塞,等待子進程喚醒。
進程exit
外部命令在執行期間其實是被自己造的簡易CRT包裹的,簡易CRT call外部命令的main,在其結束時取得他的返回值,傳遞給exit,然後call exit
這裡的實現主要做了三件事:
加載外部命令並執行的整個過程
首先外部命令需要提供一個int main(int argc, char **argv)函數,鏈接的時候要帶上自製版的簡易CRT start.o ,最終將編譯完成的外部命令寫入到文件系統中
當要執行一個外部命令時,當前進程fork出一個進程,在新進程中execv,當前進程執行wait,傳入一個地址等待接受子進程返回值,然後阻塞等待新進程返回。新進程在exevc中將外部命令從文件系統加載到內存,並將pcb中的相關內容修改為外部命令的信息,並修改pcb中斷棧中的eip為CRT的_start入口(此時新進程已經完全將自己替換成了待執行的外部命令進程),最後使用中斷退出函數intr_exit來偽造中斷退出從而進入CRT再進入外部命令的main。當外部命令main執行結束返回時,CRT通過main返回值調用exit,在sys_exit中將main返回值放入進程pcb的相應位置,回收除了pcb和頁目錄表之外的所有資源,然後喚醒父進程,阻塞子進程。父進程被喚醒後,系統調用sys_wait從自己掛起的子進程的pcb中拿到返回值,放入到之前傳入的地址,清理子進程的pcb的頁目錄表,從調度隊列和全局隊列去除掉子進程,返回子進程pid,至此,子進程執行完畢並完全被回收。
文件系統的實現模仿類Unix系統的inode
分區限定inode數量4096個,CPU按塊(簇)大小操作硬盤,一塊設置為一扇區,512字節。
inode共支持12個直接塊和1個一級間接表,一個塊為一扇區512字節,所以單文件最大支持140 * 512字節
inode結構

文件系統佈局

文件描述符與inode的對應

管道的實現依賴於文件系統中的file結構體,本質就是將文件結構體原本應該對應的inode替換成內核空間中的一個環形緩衝區空間。
// 因为管道也是当作文件来对待,因此file结构体在针对真实文件和管道是有不同的意义
struct file {
// 文件操作的偏移指针, 当是管道是表示管道打开的次数
uint32_t fd_pos ;
// 文件的操作标志,当是管道是一个固定值0xFFFF
uint32_t fd_flag ;
// 对应的inode指针,当是管道时指向管道的环形缓冲区
struct inode * fd_inode ;
};因為內核空間是共享的,所以可以通過讀寫管道來實現不同進程間的通信。管道的讀寫封裝在sys_write和sys_read中,因此操作管道和操作普通文件無區別
重定向的本質就是改變pcb文件描述符表中對應的全局描述符表地址,之後讀寫對應的文件描述符的操作就被指向了新文件
shell中的管道符|的實現,就是通過重定向標準輸入和標準輸出到管道來實現的
int32_t sys_read ( int32_t fd , void * buf , uint32_t count ) {
if ( fd == stdin_no ) {
if ( is_pipe ( fd )) {
// 从已经重定向好管道中读
ret = pipe_read ( fd , buf , count );
} else {
// 从键盘获取输入
}
} else if ( is_pipe ( fd )) {
// 读管道
ret = pipe_read ( fd , buf , count );
} else {
// 读取普通文件
}
return ret ;
}
int32_t sys_write ( int32_t fd , const void * buf , uint32_t count ) {
if ( fd == stdout_no ) {
if ( is_pipe ( fd )) {
// 向已经重定向好管道中写入
return pipe_write ( fd , buf , count );
} else {
// 向控制台输出内容
}
} else if ( is_pipe ( fd )) {
// 写管道
return pipe_write ( fd , buf , count );
} else {
// 向普通文件写入
}
}