概述
操作系統(英語:operating system,縮寫:OS)是管理計算機硬件與軟件資源的計算機程序,同時也是計算機系統的內核與基石。操作系統需要處理如管理與配置內存、決定系統資源供需的優先次序、控制輸入與輸出設備、操作網絡與管理文件系統等基本事務。操作系統也提供一個讓用戶與系統交互的操作界面。
計算機操作系統原理課程是計算機科學與技術及相關專業的核心課程之一,對理論與實踐要求都很高,歷來爲計算機及信息學科所重視。操作系統課程設計正是該課程實踐環節的集中表現,不僅可使學生鞏固理論學習的概念、原理、設計、算法及數據結構,同時培養開發大型軟件所應擁有的系統結構設計和軟件工程素養。對該課程考覈體系的構建可以促進學 生設計能力、創新能力和科學素養的全面提升。
實驗環境
平臺 | 軟件版本 |
---|---|
實驗代碼 | GeekOS-0.3.0 |
硬件模擬器 | BOCHS x86 Emulator 2.3.7 |
Linux操作系統 | Linux發行版Ubuntu9.04 |
虛擬機 | Vmware12.5.7 build-5813279 |
主機系統 | Windows10,64-bit |
計算機硬件 | X86 PC |
GeekOS-0.3.0
基於X86的GeekOS教學型類Linux操作系統.GeekOS主要用於操作系統課程設計,目的是使學生能夠實際動手參與到一個操作系統的開發工作中學生可以在Linux或Unix環境或/windows下使用BochsPC模擬器進行開發,且其針對進程、文件系統、存儲管理等操作系統核心內容分別設計了7個難度逐漸增加的項目供教師選擇.出於教學目的,這個系統內核設計簡單,讓學生易於閱讀、設計和添加代碼,但它又涵蓋了操作系統課程的核心內容,能夠滿足操作系統課程教學的需求,卻又兼備實用性,它可以運行在真正的X86PC硬件平臺.GeekOS由一個基本的操作系統內核作爲基礎,已經實現如下功能:
(1) 操作系統與硬件之間的所有必備接口。
(2) 系統引導、實模式到保護模式的轉換、中斷調用及異常處理。
(3) 基於段式的內存管理。
(4) 內核進程以及FIFO進程調度算法。
(5) 基本的輸入輸出:鍵盤作爲輸入設備,顯示器作爲輸出設備。
(6) 只讀文件系統PFAT:用於存放用戶程序。
目前,除上述所列的之外,還缺少虛擬內存、存儲設備驅動和文件系統。在GeekOS中,使用分段機制實現了用戶模式任務的內存保護。爲了克服在存儲設備和文件系統方面的欠缺,GeekOS提供了一個種機制以實現將用戶程序編譯成直接鏈接內核的數據對象。這種技術也可以用來實現基於RAM的文件系統。
Bochs和Vmware介紹
Bochs是一個x86硬件平臺的開源模擬器。它可以模擬各種硬件的配置。Bochs模擬的是整個PC平臺,包括I/O設備、內存和BIOS。更爲有趣的是,甚至可以不使用PC硬件來運行Bochs。事實上,它可以在任何編譯運行Bochs的平臺上模擬x86硬件。通過改變配置,可以指定使用的CPU(386、486或者586),以及內存大小等。一句話,Bochs是電腦裏的“PC”。根據需要,Bochs還可以模擬多臺PC,此外,它甚至還有自己的電源按鈕。
VMWare虛擬機軟件是一個“虛擬PC”軟件,它使你可以在一臺機器上同時運行二個或更多Windows、DOS、LINUX系統。與“多啓動”系統相比,VMWare採用了完全不同的概念。多系統在一個時刻只能運行一個系統,在系統切換時需要重新啓動機器。
開發過程
爲順利的進行課程設計開發,避免出現軟件版本不兼容導致一系列問題,使用了指導老師提供的虛擬機鏡像以及虛擬機軟件Vmware,虛擬機操作系統爲Ubuntu9,其中包含了一份geekOS源碼,以及安裝好的Bochs硬件模擬器。
編譯運行
編譯
終端中進入Project下的build目錄,先輸入make depend,生成depend.mak文件,目的是鏈接頭文件,爲了快速的進行編譯。然後輸入make,使用gcc編譯器讀取文件夾下的Makefile對源碼進行編譯。編譯完成後在對應的文件夾下生成後綴爲.o文件,根據.o文件生成fd.img系統鏡像文件。同時在project1-4也生成了運行鏡像的文件系統diskc.img。
運行
生成系統鏡像後使用Bochs進行模擬硬件平臺,引導運行系統鏡像。需要事先編輯.bochsrc配置文件,教程在下面。
方法:在終端中進入Project下的build目錄,輸入bochs就可以直接運行。
配置文件
在Bochs引導系統鏡像運行過程中需要配置描述模擬器硬件配置的配置文件。文件內容如下。
Project0所需的.bochs文件
megs: 8
boot: a
floppya: 1_44=fd.img, status=inserted
log: ./bochs.out
#Project0 1-4還需要ata串口驅動器,需要加上:
ata0-master: type=disk, path=diskc.img, mode=flat, cylinders=40, heads=8, spt=64
前導知識
一、全局描述符表GDT(Global Descriptor Table)
在整個系統中,全局描述符表GDT只有一張(一個處理器對應一個GDT),GDT可以被放在內存的任何位置,但CPU必須知道GDT的入口,也就是基地址放在哪裏,Intel的設計者門提供了一個寄存器GDTR用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之後,可以通過LGDT指令將GDT的入口地址裝入此積存器,從此以後,CPU就根據此寄存器中的內容作爲GDT的入口來訪問GDT了。GDTR中存放的是GDT在內存中的基地址和其表長界限。
二、段選擇子(Selector)
訪問全局描述符表是通過“段選擇子” 來完成的。段選擇子共計16位,如圖:
段選擇子包括三部分:描述符索引(index)、TI、請求特權級(RPL)。index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由這個位置再根據在GDTR中存儲的描述符表基址就可以找到相應的LDT描述符。段選擇子中的TI值只有一位0或1,0代表選擇子是在GDT,1代表選擇子是在LDT。請求特權級(RPL)則代表選擇子的特權級,共有4個特權級(0級、1級、2級、3級)。
三、局部描述符表LDT(Local Descriptor Table)
四、CPU訪問控制
Intel的x86處理器是通過Ring級別來進行訪問控制的,級別共分4層,從Ring0到Ring3(後面簡稱R0、R1、R2、R3)。R0層擁有最高的權限,R3層擁有最低的權限。按照Intel原有的構想,應用程序工作在R3層,只能訪問R3層的數據;操作系統工作在R0層,可以訪問所有層的數據;而其他驅動程序位於R1、R2層,每一層只能訪問本層以及權限更低層的數據。
這樣操作系統工作在最核心層,沒有其他代碼可以修改它;其他驅動程序工作在R1、R2層,有要求則向R0層調用,這樣可以有效保障操作系統的安全性。但現在的OS,包括Windows和Linux都沒有采用4層權限,而只是使用2層——R0層和R3層,分別來存放操作系統數據和應用程序數據,
項目設計
Project0
項目設計目的
熟悉GeekOS的項目編譯、調試和運行環境,掌握GeekOS運行的工作過程。
項目設計要求
(1) 搭建GeekOS的編譯和調試平臺,掌握GeekOS的內核進程工作原理。
(2) 熟悉鍵盤操作函數,編程實現一個內核進程。此進程的功能是:接收鍵盤輸入的字符並顯示到屏幕上,當輸入“Ctrl+D”時,結束進程的運行。
3.1.3 項目設計原理
鍵盤設備驅動程序提供了一系列的高級接口來使用鍵盤。鍵盤事件的邏輯關係爲:用戶按鍵引發鍵盤中斷,根據是否按下鍵,分別在鍵值表中尋找掃描碼對應的按鍵值,經過處理後將鍵值放入鍵盤緩衝區s_queue中,最後通知系統重新調度進程。
若用戶進程需要從鍵盤輸入信息,可調用Wait_For_Key()函數,該函數首先檢查鍵盤緩衝區是否有按鍵。如果有,就讀取一個鍵碼,如果此時鍵盤緩衝區中沒有按鍵,就將進程放入鍵盤事件等待隊列s_waitQueue,由於按鍵觸發了鍵盤中斷,鍵盤中斷處理函數Keyboard_Interrupt_Handler就會讀取用戶按鍵,將低級鍵掃描碼轉換爲含ASCII字符的高級代碼,並刷新鍵盤緩衝區,最後喚醒等待按鍵的進程繼續運行。
項目設計代碼
(1) 編寫一個函數,此函數的功能是:接收鍵盤輸入的字符並顯示到屏幕上,當輸入“Ctrl+D”時就退出。函數代碼如下:
void EchoCount()
{
Keycode keycode;
while (1)
{
if ( Read_Key( &keycode ) )
{
if((keycode & 0x4000) == 0x4000)
{
if((Wait_For_Key() & 0x00ff) == 'd')
{
Set_Current_Attr(ATTRIB(BLACK, RED));
Print("Ctrl+d Is Entered! Program Ended!");
Exit(1);
}
}
else if ( !(keycode & KEY_SPECIAL_FLAG) && !(keycode & KEY_RELEASE_FLAG) )
{
keycode &= 0xff;
Set_Current_Attr(ATTRIB(BLACK, CYAN));
Print( "%c", (keycode == '\r') ? '\n' : keycode );
if(keycode=='\r')
{
Set_Current_Attr(ATTRIB(AMBER, BLUE));
}
}
}
}
}
(2) 在Main函數體內調用Start_Kernel_Thread函數,將以上函數地址傳遞給參數startFunc,建立一個內核級進程。相關代碼如下:
struct Kernel_Thread *kerThd;
kerThd = Start_Kernel_Thread(&EchoCount, 0 , PRIORITY_NORMAL, false);
運行結果
對項目進行編譯生成系統鏡像,然後使用bochs運行系統鏡像,測試結果如圖3-1所示,運行。
圖 3 1
Project1
項目設計目的
熟悉ELF文件格式,瞭解GeekOS系統如何將ELF格式的可執行程序裝入到內存,建立內核進程並運行的實現技術。
項目設計要求
修改/geekos/elf.c文件,在函數Parse_ELF_Executable()中添加代碼,分析ELF格式的可執行文件(包括分析得出ELF文件頭、程序頭,獲取可執行文件長度,代碼段、數據段等信息),並填充Exe_Format數據結構中的域值。
項目設計原理
(1) ELF文件格式
Executable and linking format(ELF)文件是x86 Linux系統下的一種常用目標文件(object file)格式,有三種主要類型:
① 適於連接的可重定位文件(relocatable file),可與其它目標文件一起創建可執行文件和共享目標文件。
② 適於執行的可執行文件(executable file),用於提供程序的進程映像,加載的內存執行。
③ 共享目標文件(shared object file),連接器可將它與其它可重定位文件和共享目標文件連接成其它的目標文件,動態連接器又可將它與可執行文件和其它共享目標文件結合起來創建一個進程映像。
爲了方便和高效,ELF文件內容有兩個平行的視圖:一個是程序連接角度,另一個是程序運行角度。GeekOS中的用戶程序全部在系統的編譯階段完成編譯和連接,形成可執行文件,用戶可執行文件保存在PFAT文件系統中,如圖所示。
在Parse_ELF_Executable函數中,此函數的作用爲根據ELF文件格式,從exeFileData指向的內容中得到ELF文件頭,繼續分析可得到程序頭和程序代碼段等信息。
連接程序視圖 | 執行程序視圖 |
---|---|
ELF頭部 | ELF 頭部 |
程序頭部表(可選) | 程序頭部表 |
節區1 | 段1 |
… | 段1 |
節區n | 段2 |
… | 段2 |
… | … |
節區頭部表 節區頭部表(可選)
(2) 建立線程過程如圖3 2所示
圖 3 2
具體流程爲在geek/main.c中的main函數中Spawn_Init_Process()然後跳轉到函數Start_Kernel_Thread(Spawner, 0, PRIORITY_NORMAL, true),該函數中的第一個參數爲啓動lprog.c中的函數Spwaner(),然後使用Read_Fully()讀取提前編譯好c.exe文件,並返回可執行文件elf數據和長度,然後使用自己編寫的Parse_ELF_Executable()函數,對讀取到的elf文件進行分析,得出ELF文件頭、程序頭,獲取可執行文件長度,代碼段、數據段等信息。然後在函數Spawn_Progrm()中,分配內存,通過Trampoline函數模擬執行用戶態進程。
項目設計代碼
int Parse_ELF_Executable(char *exeFileData, ulong_t exeFileLength,
struct Exe_Format *exeFormat)
{
int i;
elfHeader *head = (elfHeader*)exeFileData;
programHeader *proHeader = (programHeader *)(exeFileData+head->phoff);
KASSERT(exeFileData != NULL);
KASSERT(exeFileLength > head->ehsize+head->phentsize*head->phnum);
KASSERT(head->entry%4 == 0);
exeFormat->numSegments = head->phnum;
exeFormat->entryAddr = head->entry;
for(i=0; i<head->phnum; i++)
{
exeFormat->segmentList[i].offsetInFile = proHeader->offset;
exeFormat->segmentList[i].lengthInFile = proHeader->fileSize;
exeFormat->segmentList[i].startAddress = proHeader->vaddr;
exeFormat->segmentList[i].sizeInMemory = proHeader->memSize;
exeFormat->segmentList[i].protFlags = proHeader->flags;
proHeader++;
}
return 0;
}
運行結果
結果如圖3-4所示,成功讀取a.exe文件並運行其中的代碼。
圖 3 4
Project2
項目設計目的
擴充GeekOS操作系統內核,使得系統能夠支持用戶級進程的動態創建和執行。
項目設計要求
- 本項目要求用戶對以下/src/geekos/中的文件進行修改:
- (1) user.c:完成函數Spawn()和Switch_To_User_Context()。//創建進程,切換用戶上下文
- (2) elf.c:完成函數Parse_ELF_Executable(),要求與項目1相同。//分析exe文件,用於上下文(context)
- (3) userseg.c:完成函數Destroy_User_Context()、Load_User_Program()、Copy_From_User()、Copy_To_User()和: Switch_To_Address_Space()。//銷燬用戶進程上下文,加載用戶進行,切換用戶地址空間,用來進出內核操作
- (4) kthread.c:完成函數Setup_User_Thread()和Start_User_Thread()。//設置,啓動進程,進入等待隊列
- (5) syscall.c:完成函數Sys_Exit()、Sys_PrintString()、Sys_GetKey()、Sys_SetAttr()、Sys_GetCursor()、: Sys_PutCursor()、Sys_Spawn()、Sys_Wait()和Sys_GetPID()。//系統調用函數,方便用戶進程執行內核操作,以及只有內核纔有的權限操作,如創建進程,進行系統調用需要進入內核空間使用(3)中的函數
- (6) main.c:改寫Spawn_Init_Process(void),改寫時將“/c/shell.exe”作爲可執行文件傳遞給Spawn函數的program參數,創建第一個用戶態進程,然後由它來創建其它進程。
開始本項目前需要閱讀/src/geekos目錄中的entry.c、lowlevel.asm、kthread.c、userseg.c,其中在userseg.c中主要關注Destroy_User_Context()和Load_User_Program()兩個函數。
項目設計原理
進程是可併發執行的程序在某個數據集合上的一次計算活動,也是操作系統資源分配和保護的基本單位。進程和程序有着本質的區別,程序是一些能保存在磁盤上的指令的有序集合,沒有任何執行的概念;而進程是程序執行的過程,包括了創建、調度和消亡的整個過程。因此,對系統而言,當用戶在系統中輸入命令執行一個程序時,它將啓動一個進程。
在GeekOS中,進程的執行過程分爲運行態、就緒態和等待態。GeekOS爲不同狀態的進程準備了不同的進程隊列(Thread_Queue)。如果一個進程正處於就緒態,就會在隊列s_runQueue中出現;如果一個進程處於等待態,就會在s_reaperWaitQueue隊列中出現;如果一個進程準備被銷燬,就會在s_graveyardQueue隊列中出現。由於處於運行態的進程最多隻能有一個,所以沒有隊列,由指針g_currentThread指向此進程。
系統中每個進程有且僅有一個進程控制塊(PCB),它記錄了有關進程的所有信息,GeekOS的PCB用數據結構Kernel_Thread來表示。GeekOS最早創建的內核級進程是Idle、Reaper和Main。GeekOS在幾種情況下會進行進程切換:一是時間片用完時;二是執行進程Idle時;三是進程退出調用Exit函數時;四是進程進入等待態調用Wait函數時。如圖3-5所示。用戶進程切換通過Switch_To_User_Context函數實現,此函數負責檢測當前進程是否爲用戶級進程,若是就切換至用戶進程空間,它由我們自己實現。
圖 3 5
在GeekOS中爲了區分用戶級進程與內核級進程,在Kernel_Thread結構體中設置了一個字段userContext,它指向用戶態進程上下文。對於內核級進程來說,此指針爲空。因此,要判斷一個進程是用戶級的還是內核級的,只要判斷userContext字段是否爲空就行了。新建和註銷User_Context結構的函數分別是Create_User_Context函數和Destroy_User_Context函數,它們都由我們自己實現。
每個用戶態進程都要佔用一段物理上連續的內存空間,存儲用戶級進程的數據和代碼。所以,爲了實現存取訪問控制,每個用戶級進程都有屬於自己的內存段空間,每一個段有一個段描述符,並且每一個進程都有一個段描述符表(LDT),它用於保存此進程的所有段描述符。
- 爲用戶級進程創建LDT的步驟是:
- (1) 調用Allocate_Segment_Descriptor()新建一個LDT描述符;
- (2) 調用Selector()新建一個LDT選擇子;
- (3) 調用Init_Code_Segment_Descriptor()新建一個文本段描述符;
- (4) 調用Init_Data_Segment_Descriptor()新建一個數據段描述符;
- (5) 調用Selector()新建一個數據段選擇子;
- (6) 調用Selector()新建一個文本段選擇子。
在用戶態進程首次被調度前,系統必須初始化用戶態進程的堆棧,使之看上去像進程剛被中斷運行一樣,因此需要使用Push函數將以下數據壓入堆棧:數據選擇子、堆棧指針、Eflags、文本選擇子、程序計數器、錯誤代碼、中斷號、通用寄存器、DS寄存器、ES寄存器、FS寄存器和GS寄存器。 - GeekOS的用戶級進程創建過程可以描述如下:
- (1) Spawn函數導入用戶程序並初始化:調用Load_User_Program進行User_Context的初始化及用戶級進程空間的分配及: 用戶程序各段的裝入;
- (2) Spawn函數調用Start_User_Thread(),初始化一個用戶態進程,包括初始化進程Kernel_Thread結構以及調用Setup_User_Thread初始化用戶級進程內核堆棧;
- (3) 最後Spawn函數退出,這時用戶級進程已被添加至系統運行進程隊列,可以被調度了。
具體運行過程爲在main.c調用函數Spawn(),使用shell.c建立第一個用戶態進程,該進程的作用爲一直等待讀取用戶輸入指令,然後根據指令讀取文件系統中的對應.exe文件,通過系統調用方法來建立用戶進程,因爲shell進程爲用戶進程,沒有權限分配內存以及其他資源建立進程,只有內核纔有權限,所以需要通過系統調用提供建立進程方法,以及讀取進程pid方法。同時在切換到內核時候,需要把在用戶級進程數據複製進內核棧進行繼續操作。
項目設計代碼
部分代碼,其餘詳見附錄。
int Spawn(const char *program, const char *command, struct Kernel_Thread **pThread)
{
/*
* Hints:
* - Call Read_Fully() to load the entire executable into a memory buffer
* - Call Parse_ELF_Executable() to verify that the executable is
* valid, and to populate an Exe_Format data structure describing
* how the executable should be loaded
* - Call Load_User_Program() to create a User_Context with the loaded
* program
* - Call Start_User_Thread() with the new User_Context
*
* If all goes well, store the pointer to the new thread in
* pThread and return 0. Otherwise, return an error code.
*/
/* Por Victor Rosales */
char *exeFileData = 0;
ulong_t exeFileLength = 0;
struct Exe_Format exeFormat;
struct User_Context *userContext = NULL;
struct Kernel_Thread *process = NULL;
int ret = 0;
//ret 函數運行返回的結果,判斷該函數是否正常運行
//將整個可執行文件加載到內存緩衝區中
ret = Read_Fully(program, (void**) &exeFileData, &exeFileLength);
if (ret != 0) {
ret = ENOTFOUND;
goto error;
}
//驗證可執行的有效性,讀出運行文件結構
ret = Parse_ELF_Executable(exeFileData, exeFileLength, &exeFormat);
if (ret != 0) {
ret = ENOEXEC;
goto error;
}
//通過加載可執行文件鏡像創建新進程的User_Context結構
//調用Load_User_Program將可執行程序的程序段和數據段裝入內存
ret = Load_User_Program(exeFileData, exeFileLength, &exeFormat,
command, &userContext);
if (ret != 0) {
ret = -1;
goto error;
}
//調用Start_User_Thread函數創建一個進程並使其進入準備運行隊列
process = Start_User_Thread(userContext, false);
if (process == NULL) {
ret = -1;
goto error;
}
*pThread = process;
ret =(*pThread)->pid;
error:
if (exeFileData)
Free(exeFileData);
exeFileData = 0;
return ret;
}
void Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state)
{
/*
* Hint: Before executing in user mode, you will need to call
* the Set_Kernel_Stack_Pointer() and Switch_To_Address_Space()
* functions.
*/
if (kthread->userContext != NULL) {
//切換用戶地址空間
Switch_To_Address_Space(kthread->userContext);
Set_Kernel_Stack_Pointer(((ulong_t) kthread->stackPage) + PAGE_SIZE);
}
}
運行結果
如圖3 7所示,啓動項目後,首先建立shell進程進程pid爲6,然後創建b,c進程。再創建一個shell進程,再shell進程輸入pid輸出的是目前shell進程的進程號,因爲之前以及建立了b,c進程,所以第二個shell進程pid爲9。
圖 3 7
Project3
項目設計目的
研究進程調度算法,掌握用信號量實現進程間同步的方法。爲GeekOS擴充進程調度算法——基於時間片輪轉的進程多級反饋調度算法,並能用信號量實現進程協作。
項目設計要求
實現src/geekos/syscall.c文件中的Sys_SetSchedulingPolicy系統調用,它的功能是設置系統採用的何種進程調度策略;
實現src/geekos/syscall.c文件中的Sys_GetTimeOfDay系統調用,它的功能是獲取全局變量g_numTicks的值;
實現函數Change_Scheduling_Policy(),具體實現不同調度算法的轉換。
實現syscall.c中信號量有關的四個系統調用:sys_createsemaphore()、sys_P()、sys_V()和sys_destroysemaphore()。
項目設計原理
(1) 多級反饋隊列調度隊列模型
如圖3 10和3 11所示。
圖 3 10
圖 3 11
(2) 多級反饋隊列與分時調度進程隊列的轉換
通過把多級反饋隊列中的所有隊列合併成一個隊列,實現切換到分時調度隊列。Get_Next_Runable()函數會自動選擇優先級最高的隊列,如圖3-12所示。
圖 3 12
(3) 函數設計提示
① 添加函數Chang_Scheduling_Policy(int policy, int quantum),policy是設置的調度策略,quantum是設置的時間片。例如policy爲1說明設置的是多級反饋隊列調度算法,此時若g_SchedPolicy(爲系統添加的標識算法的變量,初始化爲0)爲0,說明當前的調度算法爲輪轉調度,要變成MLF就必須把空閒線程放入3隊列,若g_SchedPolicy爲1,說明當前是多級反饋隊列調度算法,則返回。如果policy爲0,則說明設置的是輪轉調度,此時若g_SchedPolicy爲1,則必須把4個隊列變成一個隊列,即所有的線程都在隊列0上了。若g_SchedPolicy爲0,則返回。
② 在系統調用Sys_GetTimeOfDay()中,只需要返回g_numTicks就可以了。在Sys_SetSchedulingPolicy()中,如果state->ebx是1,則設置的是MLF算法,調用Change_Scheduling_Policy(SCHED_RR,quantum),爲0則是RR算法,調用Change_Scheduling_Policy(SCHED_MLF,quantum)。如果state->ebx爲其他值,則返回-1。
③ 在Init_Thread()中都是把隊列放在0隊列上的,並且blocked變量爲false。
④ 在Get_Next_Runnable()中,從最高級的隊列開始,調用Find_Best()來找線程優先級最大的線程,直到在某級隊列中找到符合條件可以運行的線程。
⑤ 在Wait()函數中,線程被阻塞,所以blocked變量被設置爲true,並且如果是MLF算法,則該進程的currentReadyQueue加一,下次運行的時候進入高一級的線程隊列。
(4) 信號量定義
GeekOS定義了信號量的結構體:
struct Semaphore{
int semaphoreID; /*信號量的ID*/
char *semaphoreName; /*信號量的名字*/
int value; /*信號量的值*/
int registeredThreadCount; /*註冊該信號量的線程數量*/
struct Kernel_Thread *registeredThreads[MAX_REGISTERED_THREADS];
/*註冊的線程*/
struct Thread_Queue waitingThreads; /*等待該信號的線程隊列*/
DEFINE_LINK(Semaphore_List,Semaphore); /*連接信號鏈表的域*/
}
(5) 信號量PV操作
信號量操作:
Semaphore_Create( )(創建信號量)
Semaphore_Acquire(P操作)
Semaphore_Release(V操作)
Semaphore_Destroy( )(銷燬信號量)
Create_Semaphore()函數首先檢查請求創建的這個信號量的名字是否存在,如果存在,
那麼就把這個線程加入到這個信號量所註冊的線程鏈表上;
如果不存在,則分配內存給新的信號量,清空它的線程隊列,
把當前的這個線程加入到它的線程隊列中,設置註冊線程數量爲1,
初始化信號量的名字,值和信號量的ID,並把這個信號量添加到信號量鏈表上,最後返回信號量的ID。
項目設計代碼
static int Sys_SetSchedulingPolicy(struct Interrupt_State* state)
{
if (state->ebx != ROUND_ROBIN && state->ebx != MULTILEVEL_FEEDBACK)
return -1;
g_schedulingPolicy = state->ebx;
g_Quantum = state->ecx;
return 0;
}
static int Sys_GetTimeOfDay(struct Interrupt_State* state)
{
return g_numTicks;
}
struct Kernel_Thread* Get_Next_Runnable(void)
{
struct Kernel_Thread* best = 0;
int i, best_index_queue = -1;
if (g_schedulingPolicy == ROUND_ROBIN) {
struct Kernel_Thread* best_in_queue = NULL;
for (i = 0; i < MAX_QUEUE_LEVEL; i++){
best_in_queue = Find_Best(&s_runQueue[i]);
if (best == NULL) {
best = best_in_queue;
best_index_queue = i;
} else if (best_in_queue != NULL){
if (best_in_queue->priority > best->priority) {
best = best_in_queue;
best_index_queue = i;
}
}
}
} else if (g_schedulingPolicy == MULTILEVEL_FEEDBACK) {
if ( g_currentThread->priority != PRIORITY_IDLE ){
if ( g_currentThread->blocked && g_currentThread->currentReadyQueue > 0 )
g_currentThread->currentReadyQueue--;
}
for (i = 0; i < MAX_QUEUE_LEVEL; i++){
best = Find_Best(&s_runQueue[i]);
best_index_queue = i;
if (best != NULL)
break;
}
if ( best->currentReadyQueue < MAX_QUEUE_LEVEL-1 )
best->currentReadyQueue++;
}
KASSERT(best != NULL);
Remove_Thread(&s_runQueue[best_index_queue], best);
return best;
}
static int Sys_CreateSemaphore(struct Interrupt_State* state)
{
int rc;
char *name = 0;
//int exit, id_sem;
struct Semaphore *s=s_sphlist.head;
if ((rc = Copy_User_String(state->ebx, state->ecx, VFS_MAX_PATH_LEN, &name)) != 0 )
goto fail;
//Print("Copy_User_String_Name =%s\n",name);
while(s!=0)
{
//Print("whiles->semaphoreName=%s\n",s->semaphoreName);
if(strcmp(s->semaphoreName,name)==0)
{
s->registeredThreads[s->registeredThreadCount]=g_currentThread;
s->registeredThreadCount+=1;
return s->semaphoreID;
}
s=Get_Next_In_Semaphore_List(s);
}
s=(struct Semaphore *)Malloc(sizeof(struct Semaphore));
s->registeredThreads[0]=g_currentThread;
s->registeredThreadCount=1;
//strcpy(s->semaphoreName,name);
s->semaphoreName=name;
//Print("s->semaphoreName=name===%s\n",s->semaphoreName);
Clear_Thread_Queue(&s->waitingThreads);
s->value=state->edx;
s->semaphoreID=semnub;
semnub++;
Add_To_Back_Of_Semaphore_List(&s_sphlist,s);
return s->semaphoreID;
fail:
Print("CreateSemaphore failed!");
return -1;
}
static int Sys_P(struct Interrupt_State* state)
{
struct Semaphore *s=s_sphlist.head;
while(s!=0)
{
if(s->semaphoreID == state->ebx)
break;
s=Get_Next_In_Semaphore_List(s);
}
if(s==0)
return -1;
s->value-=1;
if(s->value<0)
Wait(&s->waitingThreads);
return 0;
}
static int Sys_V(struct Interrupt_State* state)
{
struct Kernel_Thread *kthread;
struct Semaphore *s=s_sphlist.head;
while(s!=0)
{
if(s->semaphoreID==state->ebx)
break;
s=Get_Next_In_Semaphore_List(s);
}
if(s==0)
return -1;
s->value+=1;
if(s->value>=0){
kthread = s->waitingThreads.head;
if( kthread !=0){
//kthread = Get_Front_Of_Thread_Queue(&s->waitingThreads);
//Remove_Thread(&s->waitingThreads, kthread);
Wake_Up_One(&s->waitingThreads);
}
}
return 0;
}
運行結果
多級調度隊列實現,如圖3-13和3-14所示,從結果來看這兩種調度算法在運行過程中,每個相同的時間間輸出的2或者1基本相等。
圖 3 13
圖 3 14
信號量測試,如圖3-15和3-16所示,結果顯示已經成功的實現了信號量的創建以及銷燬,以及使用信號量實現單個共享資源的生產消費問題。
圖 3 15
圖 3 16
課程設計過程中問題
問題1:在project1中運行a.exe文件的時候沒有正確的把代碼中的兩個字符串打印出來,只打印出了一個全局變量的字符串。
解決方法:因爲是局部變量和全局變量的地址段不一樣,所以把不能正常輸出的字符串改成全局變量或者設爲靜態變量即可解決。
問題2:在make depend 和make生成文件過程中出現Permission denied,顯示權限不夠,不允許生成文件。
解決方法:可能是由於使用root權限修改文件夾,導致權限級別變高,不允許普通用戶權限生成文件,在指令前加sudo使用root權限運行即可。
問題3:在project3中對於多級調度算法以及時間片輪轉算法一直不能準確的理解。
解決方法:使用source inside等代碼閱讀軟件對代碼函數以及頭文件進行追蹤閱讀理解,慢慢的體驗到進程創建,銷燬,運行,調度的美妙。
總結
在本次課程設計中,實現了project0,1,2,3。發現了幾個project的項目設計都是有很大的關聯性的,在項目0中實現了從鍵盤讀取字符串,理解了操作系統是怎麼實現輸入的,還有格式化的輸出,還有理解了操作系統中最重要的中斷操作。在項目二中,通過讀取exe文件並分析其中的elf文件,並使用其中的代碼段創建進程,從中理解到操作系統如何通過一個可執行文件,從中讀取信息並默認生成一個內核進程。而項目三則在項目二基礎上進一步把進程創建這個操作分離爲用戶進程和內核進程。並提供了系統調用來實現在用戶進程進行中斷進入內核創建用戶進程。在項目三則在項目二上的原始先進先出進程調度算法擴展爲可以在多級隊列調度算法和時間片輪轉算法間切換。還有同時實現了信號量,有效解決了生產消費問題。
通過這次課程設計深刻的理解到了操作系統如何管理與配置內存、決定系統資源供需的優先次序、控制輸入與輸出設備。鞏固了操作系統理論學習的概念、原理、設計、算法及數據結構。閱讀代碼過程中感受到了操作系統從無到有的發展不易,還有操作系統深層次的對硬件資源的管理,而不僅僅限於停留於現在成熟的操作系統的炫酷的界面展示。
更詳細的解析參考csdn另一個博主 本然233
附錄
Project2全部代碼
intSpawn(const.char*program,const.char*command,struct.Kernel_Thread**pThread)
{
Int rc;
//標記各函數的返回值,爲0則表示成功,否則失敗
char*exeFileData=0;
//保存在內存緩衝中的用戶程序可執行文件ulong_t exeFileLength;
//可執行文件的長度
Struct User_Context*userContext=0;
//指向User_Conetxt的指針
Struct Kernel_Thread*process=0;
//指向Kernel_Thread*pThread的指針
Struct Exe_Format exeFormat;
//調用Parse_ELF_Executable函數得到的可執行文件信息
if((rc=Read_Fully(program,(void**)&exeFileData,&exeFileLength)) !=0){//調用Read_Fully函數將名爲program的可執行文件全部讀入內存緩衝區Print("Failed to Read File%s!\n",program);goto fail;}
if((rc=Parse_ELF_Executable(exeFileData,exeFileLength,&exeFormat))!=0){//調用Parse_ELF_Executable函數分析ELF格式文件Print("Failed to Parse ELF File!\n");goto fail;}
if((rc=Load_User_Program(exeFileData,exeFileLength,&exeFormat,command,&userContext))!=0)
{//調用Load_User_Program將可執行程序的程序段和數據段裝入內存Print("Failed to Load User Program!\n");goto fail;}
//在堆分配方式下釋放內存並再次初始化exeFileData Free(exeFileData);exeFileData=0;
/* 開始用戶進程,調用Start_User_Thread函數創建一個進程並使其進入準備運行隊列*/
process=Start_User_Thread(userContext,false);
if(process!=0){//不是核心級進程(即爲用戶級進程)KASSERT(process->refCount==2);/*返回核心進程的指針*/*pThread=process;
rc=process->pid;//記錄當前進程的ID} else//超出內存 project2\include\geekos\errno.h rc = ENOMEM; return rc;
fail: //如果新進程創建失敗則註銷User_Context對象 if (exeFileData != 0)
Free(exeFileData);//釋放內存 if (userContext != 0)
Destroy_User_Context(userContext);//銷燬進程對象 return rc; }
------------------------------------- //切換至用戶上下文
void Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state) {
static struct User_Context* s_currentUserContext; /* last user context used */ //extern int userDebug;
struct User_Context* userContext = kthread->userContext;//指向User_Conetxt的指針,並初始化爲準備切換的進程 KASSERT(!Interrupts_Enabled());
if (userContext == 0) { //userContext爲0表示此進程爲核心態進程就不用切換地址空間 return; }
if (userContext != s_currentUserContext) { ulong_t esp0;
//if (userDebug) Print("A[%p]\n", kthread);
Switch_To_Address_Space(userContext);
esp0 = ((ulong_t) kthread->stackPage) + PAGE_SIZE; //if (userDebug)
// Print("S[%lx]\n", esp0); /* 新進程的核心棧. */
Set_Kernel_Stack_Pointer(esp0);//設置內核堆棧指針 /* New user context is active */
s_currentUserContext = userContext; } }
static struct User_Context* Create_User_Context(ulong_t size) {struct User_Context * UserContext; size = Round_Up_To_Page(size);
UserContext = (struct User_Context *)Malloc(sizeof(struct User_Context));//爲用戶態進程 if (UserContext != 0)
UserContext->memory = Malloc(size);
//爲核心態進程
else
goto fail;//內存爲空
if (0 == UserContext->memory)
goto fail;
memset(UserContext->memory, '\0', size);UserContext->size = size;
UserContext->ldtDescriptor = Allocate_Segment_Descriptor();if (0 == UserContext->ldtDescriptor)goto fail;
Init_LDT_Descriptor(UserContext->ldtDescriptor, UserContext->ldt, NUM_USER_LDT_ENTRIES);
UserContext->ldtSelector = Selector(KERNEL_PRIVILEGE, true, Get_Descriptor_Index(UserContext->ldtDescriptor));
Init_Code_Segment_Descriptor(&UserContext->ldt[0],
(ulong_t) UserContext->memory,size / PAGE_SIZE,USER_PRIVILEGE);
//新建一個數據段
Init_Data_Segment_Descriptor(&UserContext->ldt[1],
(ulong_t) UserContext->memory,size / PAGE_SIZE,USER_PRIVILEGE);
//新建數據段和文本段選擇子
UserContext->csSelector = Selector(USER_PRIVILEGE, false, 0); UserContext->dsSelector = Selector(USER_PRIVILEGE, false, 1);//將引用數清0
UserContext->refCount = 0;return UserContext; fail:
if (UserContext != 0){
if (UserContext->memory != 0){Free(UserContext->memory); }
Free(UserContext);}
return 0;}
//摧毀用戶上下文
void Destroy_User_Context(struct User_Context* userContext)
{Free_Segment_Descriptor(userContext->ldtDescriptor); userContext->ldtDescriptor=0;
Free(userContext->memory);
userContext->memory=0;
Free(userContext); userContext=0; }
int Load_User_Program(char *exeFileData, ulong_t exeFileLength,struct Exe_Format *exeFormat, const char *command,
struct User_Context **pUserContext)
{int i;
ulong_t maxva = 0;//要分配的最大內存空間
unsigned numArgs;//進程數目
ulong_t argBlockSize;//參數塊的大小
ulong_t size,
argBlockAddr; s
truct User_Context *userContext = 0;
for (i = 0; i < exeFormat->numSegments; ++i)
{
struct Exe_Segment *segment = &exeFormat->segmentList[i];
ulong_t topva = segment->startAddress + segment->sizeInMemory; /* FIXME: range check */
if (topva > maxva) maxva = topva; }
Get_Argument_Block_Size(command, &numArgs, &argBlockSize);//獲取參數塊信息
size = Round_Up_To_Page(maxva) + DEFAULT_USER_STACK_SIZE;//用戶進程大小=參數塊總大小 + 進程堆棧大小(8192) argBlockAddr = size; size += argBlockSize;
userContext = Create_User_Context(size);//按相應大小創建一個進程 if (userContext == 0)//如果爲核心態進程 return -1;
for (i = 0; i < exeFormat->numSegments; ++i) {
struct Exe_Segment *segment = &exeFormat->segmentList[i];
//根據段信息將用戶程序中的各段內容複製到分配的用戶內存空間
memcpy(userContext->memory + segment->startAddress, exeFileData + segment->offsetInFile,segment->lengthInFile); }
//格式化參數塊
Format_Argument_Block(userContext->memory + argBlockAddr, numArgs, argBlockAddr, command);
//初始化數據段,堆棧段及代碼段信息
userContext->entryAddr = exeFormat->entryAddr; userContext->argBlockAddr = argBlockAddr; userContext->stackPointerAddr = argBlockAddr;
//將初始化完畢的User_Context賦給*pUserContext *pUserContext = userContext; return 0;//成功 }
bool Copy_From_User(void* destInKernel, ulong_t srcInUser, ulong_t bufSize) { struct User_Context * UserContext = g_currentThread->userContext; //--: check if memory if validated
if (!Validate_User_Memory(UserContext,srcInUser, bufSize)) return false; //--:user->kernel
memcpy(destInKernel, UserContext->memory + srcInUser, bufSize); return true; }
----------------------------------------- //將內核態的進程複製到用戶態
bool Copy_To_User(ulong_t destInUser, void* srcInKernel, ulong_t bufSize) {struct User_Context * UserContext = g_currentThread->userContext; //--: check if memory if validated
if (!Validate_User_Memory(UserContext, destInUser, bufSize)) return false;
//--:kernel->user
memcpy(UserContext->memory + destInUser, srcInKernel, bufSize); return true; }
---------------------------------------- //切換到用戶地址空間
void Switch_To_Address_Space(struct User_Context *userContext)
{
ushort_t ldtSelector= userContext->ldtSelector;/* Switch to the LDT of the new user context */
__asm__ __volatile__ ("lldt %0"::"a"(ldtSelector)); }
#include <geekos/user.h> //創建一個用戶進程
/*static*/ void Setup_User_Thread(struct Kernel_Thread* kthread, struct User_Context* userContext)
{ulong_t eflags = EFLAGS_IF;
unsigned csSelector=userContext->csSelector;
unsigned dsSelector=userContext->dsSelector;
Attach_User_Context(kthread, userContext);
Push(kthread, dsSelector);
Push(kthread, userContext->stackPointerAddr);
Push(kthread, eflags); //Eflags
Push(kthread, csSelector);
Push(kthread, userContext->entryAddr);
Push(kthread, 0);
Push(kthread, 0); //中斷號(0)
//初始化通用寄存單元,將ESI用戶傳遞參數塊地址Push(kthread, 0); /* eax */ Push(kthread, 0); /* ebx */ Push(kthread, 0); /* edx */ Push(kthread, 0); /* edx */
Push(kthread, userContext->argBlockAddr); /* esi */ Push(kthread, 0); /* edi */ Push(kthread, 0); /* ebp */
//初始化數據段寄存單元
Push(kthread, dsSelector); /* ds */ Push(kthread, dsSelector); /* es */ Push(kthread, dsSelector); /* fs */ Push(kthread, dsSelector); /* gs */ }
//開始用戶進程
struct Kernel_Thread* Start_User_Thread(struct User_Context* userContext, bool detached)
{ struct Kernel_Thread* kthread = Create_Thread(PRIORITY_USER, detached); //爲用戶態進程 if (kthread != 0){
Setup_User_Thread(kthread,userContext); Make_Runnable_Atomic(kthread); }
return kthread; }
//需在此文件別的函數前增加一個函數,函數名爲Copy_User_String,它被函數Sys_PrintString調用,具體實現如下:
static int Copy_User_String(ulong_t uaddr, ulong_t len, ulong_t maxLen, char **pStr) { int rc = 0; char *str;
if (len > maxLen){ //超過最大長度 return EINVALID; }
str = (char*) Malloc(len+1); //爲字符串分配空間 if (0 == str){
rc = ENOMEM; goto fail; }
if (!Copy_From_User(str, uaddr, len)){ //從用戶空間中複製數據 rc = EINVALID; Free(str); goto fail; }
str[len] = '\0'; //成功 *pStr = str; fail:
return rc; }
-----------------------------------------
static int Sys_Exit(struct Interrupt_State* state) { Exit(state->ebx); }
-----------------------------------------
static int Sys_PrintString(struct Interrupt_State* state)
{int rc = 0;//返回值
uint_t length = state->ecx;//字符串長度 uchar_t* buf = 0; if (length > 0) {
/* Copy string into kernel. 將字符串複製到內核*/
if ((rc = Copy_User_String(state->ebx, length, 1023, (char**) &buf)) != 0) goto done;
/* Write to console. 將字符串打印到屏幕 */ Put_Buf(buf, length); } done:
if (buf != 0) Free(buf); return rc; }
----------------------------------------------
static int Sys_GetKey(struct Interrupt_State* state) {
return Wait_For_Key(); //返回按鍵碼keyboard.c/Wait_For_Key() }
---------------------------------------------
static int Sys_SetAttr(struct Interrupt_State* state) { Set_Current_Attr((uchar_t) state->ebx); return 0; }
---------------------------------------------
static int Sys_GetCursor(struct Interrupt_State* state) {int row, col;
Get_Cursor(&row, &col);
if (!Copy_To_User(state->ebx, &row, sizeof(int)) ||!Copy_To_User(state->ecx, &col, sizeof(int))) return -1; return 0; }
-----------------------------------------------
static int Sys_PutCursor(struct Interrupt_State* state)
{ return Put_Cursor(state->ebx, state->ecx) ? 0 : -1; }
-----------------------------------------------
static int Sys_Spawn(struct Interrupt_State* state)
{ int rc;
char *command = 0;
//用戶命令
struct Kernel_Thread *process;
/* Copy program name and command from user space. */
if ((rc = Copy_User_String(state->ebx, state->ecx, VFS_MAX_PATH_LEN, &program)) != 0)
{//從用戶空間複製進程名稱
goto fail; }
if(rc = Copy_User_String(state->edx, state->esi, 1023, &command)) != 0) {//從用戶空間複製用戶命令 goto fail; }
Enable_Interrupts(); //開中斷
rc = Spawn(program, command, &process);//得到進程名稱和用戶命令後便可生成一個新進程
if (rc == 0) {//若成功則返回新進程ID號 KASSERT(process != 0);rc = process->pid; }
Disable_Interrupts();//關中斷 fail://返回小於0的錯誤代碼if (program != 0)Free(program);if (command != 0)Free(command); return rc; }
-----------------------------------------
static int Sys_Wait(struct Interrupt_State* state) {int exitCode;
struct Kernel_Thread *kthread = Lookup_Thread(state->ebx);
if (kthread == 0) return -1;
Enable_Interrupts(); exitCode = Join(kthread); Disable_Interrupts(); return exitCode; }
---------------------------------------
static int Sys_GetPID(struct Interrupt_State* state)
{return g_currentThread->pid;}
=================main.c================== Spawn_Init_Process(void){
struct.Kernel_Thread*pThread;
Spawn("/c/shell.exe","/c/shell.exe",&pThread);}