操作系統lab3實驗報告


實驗文檔-lab3

一、思考題匯總

思考1:

為什么我們在構造空閑進程鏈表時必須使用特定的插入的順序?(順序或者逆序)

答:插入空閑進程鏈表時采用的是逆序插入。

由於我們的操作系統在插入空閑進程鏈表時采用的方式為LIST_INSERT_HEAD,所以在插入時只有通過逆序插入,才能使我們在從env_free_list中取出進程塊時能夠從前往后地進行取出。


思考2:

思考env.c/mkenvid函數和envid2env函數:

  • 請你談談對mkenvid函數中生成id的運算的理解,為什么這么做?

  • 為什么envid2env中需要判斷e->env_id != envid的情況?如果沒有這步判斷會發生什么情況?

答:(1)生成id時,先用一個靜態變量表示第幾次調用這個函數,再將其左移11位,再加上這個env塊在env數組中的偏移,得到這個進程塊所對應的env_id

(2)在該判斷語句的之前有一句e = &envs[ENVX(envid)];來通過envid來求出env。可以發現,在這個語句中判斷對應關系只用了低位位數,也就是進程塊的偏移。但實際上,envid還有高位部分,高位不同代表這個進程塊被調用過不止一次,仍然不是一個進程。但由於一個進程塊同時只能對標一個正在執行的進程,所以若高位不同,代表所查詢的envid所對應的進程一定不存在,因此返回-E_BAD_ENV。若沒有這步判斷,則會在查詢一個不存在的進程id時卻能夠得到對應的進程,導致程序錯誤。


思考3:

結合include/mmu.h 中的地址空間布局,思考env_setup_vm 函數:

  • 我們在初始化新進程的地址空間時為什么不把整個地址空間的pgdir 都清零,而是復制內核的boot_pgdir作為一部分模板?(提示:mips 虛擬空間布局)

  • UTOP 和ULIM 的含義分別是什么,在UTOP 到ULIM 的區域與其他用戶區相比有什么最大的區別?

  • 在env_setup_vm 函數的最后,我們為什么要讓pgdir[PDX(UVPT)]=env_cr3?(提示: 結合系統自映射機制)

  • 談談自己對進程中物理地址和虛擬地址的理解

答:(1)MIPS操作系統的虛擬地址采用的是2G/2G的結構,其中高2G是內核區,低2G是用戶區。在內核區中。每一個進程的這一段區域虛擬地址到物理地址的映射都是完全一樣的,這就意味着所以的進程都可以直接通過在boot_pgdir中復制來得到這一部分的內容。

(2)UTOP = 0x7f400000,其含義為用戶所能操縱的地址空間的最大值;ULIM = 0x80000000,其含義為操作系統分配給用戶地址空間的最大值。這一段空間被定義為一個只讀片段,屬於“內核態”,主要功能在於讓用戶進程去查看其他進程的信息,用戶在此處進行讀取不會陷入異常。

(3)UVPT的含義為User Virtual Page Table,因此這一段需要映射到他的進程在pgdir中的頁目錄地址。所以我們在將這一段空間的虛擬地址轉化為物理地址時可以很快找到對應的頁目錄。

(4)進程只能操作虛擬地址,而實現虛擬地址和物理地址之間的映射由操作系統完成。


思考4:

思考user_data 這個參數的作用。沒有這個參數可不可以?為什么?(如果你能說明哪些應用場景中可能會應用這種設計就更好了。可以舉一個實際的庫中的例子)

答:在函數load_icode_mapper中,被傳入的user_data被用於這樣一個語句中:

struct Env *env = (struct Env *)user_data;

因此這個所謂的user_data實際上在函數中的真正含義就是這個被操作的進程指針。那么我們來一步步追溯這個變量最開始是以什么形式被傳入的。

回到load_elf函數中,我們可以看到user_data從函數本身被傳入到調用load_icode_mapper中沒有改變,那再回到調用load_elfload_icode中,我們發現在調用load_elf時的語句為:

r = load_elf(binary, size, &entry_point, e, load_icode_mapper);

而其中的e則為傳入load_icode中的struct Env *e,因此我們的推測得到證實。

沒有進程指針,我們的加載鏡像的步驟顯然不能正常完成。


思考5:

結合load_icode_mapper 的參數以及二進制鏡像的大小,考慮該函數可能會面臨哪幾種復制的情況?你是否都考慮到了? (提示:1、頁面大小是多少;2、回顧lab1中的ELF文件解析,什么時候需要自動填充.bss段)

答:在load_icode_mapper中,需要復制的頁面特征可以由以下的一張圖表示

image

因此我們可以分析出復制的情況有這么幾種:

.text & .data

  • 第一段,需要切除前半部分的offset的一段。
  • 中間的普通段。
  • 最后一段,即前半部分屬於.test & .data,后半部分屬於.bss
  • 需要考慮的特殊情況有:
    • 第一段的前半段已經裝載過內容,因此不能在這一段進行allocinsert操作,從而保留前半段內容。
    • offset = 0,此時從最開始的所有端可以當做正常頁處理。
    • .test & .data.bss被某一個頁分割恰好切開,不存在共同占用一個page的情況。
    • .test & .data這一段的長度極小,即第一個page就為最后一個page,因此需要同時對兩側的頁面分割進行判定與相應操作。

.bss

  • 第一段,需要同前半段的.test & .data段協同考慮。
  • 中間的普通段。
  • 最后一段,即前半部分屬於.bss,后半段在需要復制的內容之外。
  • 需要考慮的特殊情況有:
    • 第一段的前半段已經在.text & .data段被裝載過相關內容,為保證那一段內容不被破壞,在處理.bss的這一段時,不能使用alloc以及insert來進行新的頁面插入。注:在操作正確的情況下,只要兩段不是恰好的頁面分割,那么一定會出現這種情況!!
    • .test & .data.bss被某一個頁分割恰好切開,不存在共同占用一個page的情況。
    • .bss這一段的長度極小,即第一個page就為最后一個page
    • 最后一頁被恰好在page的交界分開。

最終代碼如下:

/*** exercise 3.6 ***/
static int load_icode_mapper(u_long va, u_int32_t sgsize,
                             u_char *bin, u_int32_t bin_size, void *user_data)
{
    struct Env *env = (struct Env *)user_data;
    struct Page *p = NULL;
    u_long i;
    int r;
    u_long offset = va - ROUNDDOWN(va, BY2PG);
    Pte *temp;

    /*Step 1: load all content of bin into memory. */

    i = 0;
    if (offset != 0) {
            p = page_lookup(env -> env_pgdir, va, &temp);
            if (p == 0) {
                    xxxxx;
            }
            if (BY2PG - offset <= bin_size) {
                    xxxxx;
            } else {
                    xxxxx;
            }
    }
    for (; i < bin_size; i += BY2PG) {
        /* Hint: You should alloc a new page. */
            xxxxx;
                if (BY2PG + i - offset <= bin_size) {
                        xxxxx;
                } else {
                        xxxxx;
                }
    }
    /*Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`.
    * hint: variable `i` has the value of `bin_size` now! */
    offset = i - ROUNDDOWN(i, BY2PG);
    if (offset != 0) {
            p = page_lookup(env -> env_pgdir, va + i, &temp);
            if (offset + sgsize - i < BY2PG) {
                    xxxxx;
            } else {
                    xxxxx;
            }
    }
    while (i < sgsize) {
        xxxxx;
        if (sgsize - i < BY2PG) {
                xxxxx;
        } else {
                xxxxx;
        }
        i += BY2PG;
    }
    return 0;
}

思考6:

思考上面這一段話,並根據自己在lab2 中的理解,回答:

  • 我們這里出現的” 指令位置” 的概念,你認為該概念是針對虛擬空間,還是物理內存所定義的呢?

  • 你覺得entry_point其值對於每個進程是否一樣?該如何理解這種統一或不同

答:(1)此概念針對的是虛擬地址空間

(2)該值是從load_elf中的*entry_point = ehdr->e_entry;語句中進行賦值,所以該值對於每個進程是一樣的。這種值來源於他們都是從ELF文件中的同一個部分進行取值的,elf文件結構的統一決定了該值的統一。


思考7:

思考一下,要保存的進程上下文中的env_tf.pc的值應該設置為多少?為什么要這樣設置?

答:應當設置為env_tf.cp0_epc。因為在《計算機組成》中介紹過EPC寄存器就是用來存放異常中斷發生時進程正在執行的指令的地址的。因此在進入異常處理時,進程上下文中的env_tf.pc應該設置為epc


思考8:

思考TIMESTACK 的含義,並找出相關語句與證明來回答以下關於TIMESTACK 的問題:

  • 請給出一個你認為合適的TIMESTACK 的定義

  • 請為你的定義在實驗中找出合適的代碼段作為證據(請對代碼段進行分析)

  • 思考TIMESTACK 和第18 行的KERNEL_SP 的含義有何不同

答:(1)TIMESTACK是內存中的一塊棧空間,用於存儲進程的狀態。

(2)TIMESTACK在env.c中再兩處地方被調用:

env_destroy

bcopy((void *)KERNEL_SP - sizeof(struct Trapframe),
              (void *)TIMESTACK - sizeof(struct Trapframe),
              sizeof(struct Trapframe));

env_run

old = (struct Trapframe *)(TIMESTACK - sizeof(struct Trapframe));
                bcopy((void *)old, (void *)&(curenv->env_tf), sizeof(struct Trapframe));

我們不難發現,在我們的操作系統代碼中對這個變量的調用均在bcopy中進行,並且利用的都是在TIMESTACK以下的一段長度為struct Trapframe的空間。在env_destroy中,將存於KERNEL_SP的進程狀態復制到TIMESTACK處,而在env_run中,則是從這一段空間中取出進程狀態並轉移給當前進程。

在mmu.h中,我們得到TIMESTACK的具體值為0x82000000,對這段地址的調用進行查找,我們在匯編代碼stackframe.S中又找到了一處調用該段地址的地方,即:

.macro get_sp
	mfc0	k1, CP0_CAUSE
	andi	k1, 0x107C
	xori	k1, 0x1000
	bnez	k1, 1f
	nop
	li	sp, 0x82000000
	j	2f
	nop
1:
	bltz	sp, 2f
	nop
	lw	sp, KERNEL_SP
	nop

2:	nop


.endm

這段代碼的功能為獲取棧指針的值,若檢測到是中斷異常則將棧指針置於TIMESTACK處,這樣在發生中斷時我們就能將當前進程的狀態存入TIMESTACK處,從而進行保存。

(3)從上面那段匯編代碼中我們可以看出,將棧指針設在TIMESTACK還是KERNEL_SPCP0_CAUSE有關,經查閱,在發生中斷時將進程的狀態保存到TIMESTACK中,在發生系統調用時,將進程的狀態保存到KERNEL_SP中。


思考9:

閱讀 kclock_asm.S 文件並說出每行匯編代碼的作用

答:kclock_asm.S內容如下:

.macro	setup_c0_status set clr
	.set	push
	mfc0	t0, CP0_STATUS
	or	t0, \set|\clr
	xor	t0, \clr
	mtc0	t0, CP0_STATUS			
	.set	pop
.endm

	.text
LEAF(set_timer)

	li t0, 0x01
	sb t0, 0xb5000100
	sw	sp, KERNEL_SP
setup_c0_status STATUS_CU0|0x1001 0
	jr ra

	nop
END(set_timer)

.text段開始:

首先先將0x01寫入地址0xb5000100中,其中基地址0xb5000000gxemul用於映射實時鍾的地址,偏移量0x100代表時鍾的頻率。將棧指針設置為KERNEL_SP中能夠正確產生時鍾中斷,二、再調用宏函數setup_c0_status 來設置CP0_STATUS的值,最后通過jr ra來返回。


思考10:

閱讀相關代碼,思考操作系統是怎么根據時鍾周期切換進程的。

答:在我們的操作系統中,設置了一個進程就緒隊列,並且給每一個進程添加了一個時間片,這個時間片起到計時的作用,一旦時間片的時間走完,則代表該進程需要執行時鍾中斷操作,則再將這個進程移動到就緒隊列的尾端,並復原其時間片,再讓就緒隊列最首端的進程執行相應的時間片段,按照這種規律實現循環往復,從而做到根據時鍾周期切換進程。


二、實驗難點圖示

難點1:初始化新進程地址空間,即env_setup_vm函數的填寫

在這個函數中,內存空間被分成了如下的兩個部分,即UTOP以上和UTOP以下,在UTOP以下的部分,我們需要將頁目錄的這一塊區域清零,而在UTOP以上的部分,用戶不能操作,屬於內核態,因此我們可以將boot_pgdir的內容直接復制到進程的頁目錄中。

UTOP之上有一塊被稱為UVPT的地址,這一塊區域作為用戶進程頁目錄,需要用自映射機制進行單獨處理。

地址空間的結構圖如下:

image


難點2:加載二進制鏡像

這一部分的內容較多且難度較大,由三個函數共同完成,即:

  • env.c中的load_icode
  • kernal_elfloader.c中的load_elf
  • env.c中的load_icode_mapper

其中load_icode為實現這個功能的代碼,它的功能在於:

  1. 分配內存
  2. 將二進制代碼裝入分配好的內存中

其中,第二步,即裝入內存的操作交給了函數load_elf來完成,而load_elf的工作又被分為:

  1. 解析ELF結構
  2. ELF的內容復制到內存中

其中,第二步,即將內容復制到內存中的操作又交給了load_icode_mapper函數去進行,所以三段代碼的協作方式如下圖:

image

其中,函數load_icode_mapper函數的難點及圖示已在思考5中給出

在函數load_elf中,我們不難發現,我們在load_icode_mapper中用到的許多參量在這里都有了很明確的實例對應,具體映射如下:

image

因此我們只要將給定的ELF文件進行正確解析,就能利用load_icode_mapper對其進行內容復制

函數load_icode重點在於設置PC值,即從load_elf中返回的entry_point


難點3:中斷與進程調度

中斷機制的設置主要包括三步,都較為容易:

  1. start.S中補充異常分法代碼
  2. 修改鏈接腳本,使得異常中斷發生時能夠正確的跳轉到異常處理代碼段
  3. 調用set_timer()開啟時鍾中斷

進程的調度主要在sched_yield中進行。

在我們的env結構體中,有這么一個變量:

u_int env_pri;

這個變量的字面含義為進程的“優先級”,但實際上在完成實驗的過程中,我們可以發現這個所謂“優先級”與進程的優先執行並沒有關系,它的真正含義是一個進程所能連續運行時間片的大小,即產生多少次時鍾中斷之后必須要切換到下一個進程。在創建一個進程時,這個量的賦值在函數env_create_priority中進行:

void
env_create_priority(u_char *binary, int size, int priority)
{
        struct Env *e;
    /*Step 1: Use env_alloc to alloc a new env. */
        if (env_alloc(&e, 0) != 0) {
                return;
        }

    /*Step 2: assign priority to the new env. */
        e->env_pri = priority;

    /*Step 3: Use load_icode() to load the named elf binary,
      and insert it into env_sched_list using LIST_INSERT_HEAD. */
        load_icode(e, binary, size);
        LIST_INSERT_HEAD(&env_sched_list[0], e, env_sched_link);

}

我們在init.c中創建進程時,所用到的代碼為:

ENV_CREATE_PRIORITY(user_A, 2);
ENV_CREATE_PRIORITY(user_B, 1);

我們可以看到這兩個進程的“優先級”分別被設置為了2和1,這也就解釋了為什么最后的輸出結果中兩個量的輸出比例為2:1

經過查找,發現代碼中的user_Auser_Bcode_A.ccode_B.c中被定義,且兩個文件中都只有一個unsigned char型數組,兩個數組中,只有第六行的第三個值不同,user_A0x1user_B0x2,於是猜測這個值決定了這兩個進程的輸出,經過修改這個值並重新運行,此猜測得到驗證,與最后輸出中呈現的1為2的兩倍恰好對應。

進程的調度也是基於這個時間片來進行,主要的步驟為如下幾步:

  1. 設置兩個隊列,其中一個為目前的進程調度隊列q0,另一個為一個空隊列q1
  2. 首先判斷當前隊列指針指向的隊首進程的env_status
    • 如果為ENV_FREE,則要將該進程從隊列中移除
    • 如果為ENV_NOT_RUNNABLE,則直接將其插入另一個隊列的尾部
    • 如果為ENV_RUNNABLE,則判斷這個進程的時間片是否用完,若用完則復原其時間片並將其插入到另一個隊列尾部
    • 當一個隊列為空時,將指針轉移到另一個隊列隊首

image


三、體會與感想

本次實驗中首次讓我們脫離“程序”的概念,而是用“進程”來思考問題。

就代碼填寫難度來說,個人認為這一lab的難度是要低於lab2的,大多數的部分只需要調用相關函數即可完成,但本次實驗仍然會在許多的地方運用到lab2的內容,因此可以看出關於內存管理的內容貫穿着整個操作系統課程,一定要掌握充分。

本單元仍然需要多閱讀代碼,且此次需要閱讀的代碼量更大,分布更廣,我認為對代碼的理解應該為:

  • 需要填寫的代碼:一定要理解每一行的意思,清楚這一個函數的功能以及其中每一個語句的含義
  • 在填寫中反復調用的函數和宏函數:同樣需要理解代碼的含義與功能,以便在實驗中能夠靈活運用
  • 功能極其特化,只在一個或有限幾個地方會用到,且十分難以理解的C代碼或匯編代碼:最起碼要理解這個函數的功能是什么,在精力允許情況下可以嘗試去掌握其中的細節

還有一個十分重要的內容便是程序Bug的修復,操作系統的Bug有時候會十分隱蔽,可能在很多請況下都發現不了,並且能通過課程組提供的自測和公測點,但是一旦在后期突然出現就會十分致命,並且很難找到(比如我在lab2時曾因為某個尋址語句沒有進行強制類型轉化導致lab2-2的課上測試一直0分且完全沒有意識到是課下的內容出了問題)。同樣的,在這次實驗中許多身邊的同學都在最后一個進程調度中無法得到正常的結果,最終發現是lab2-1的宏函數填寫出了問題。因此我們在之后填寫代碼中一定要十分細心,多去關注可能出bug的語句,必要時與同學和老師,助教交流。

在Debug過程中,由於在虛擬式上沒有ide的調試工具,可以采用在可疑點的上下文增加一些有標記性意義的輸出:

  • 判斷是否在這一步被卡住:輸出chk1chk2等具有索引的尋蹤字段
  • 判斷這一步是否發生內容錯誤:輸出env_id等與程序執行中的重要變量有關的信息(在lab2-1-exam的part2給了我們一種全方位的判斷信息是否正確的思路,可以在之后的實驗中采用)

同時,在填寫一些復雜度很高的函數,如load_icode_mapper時,不能拿到代碼,看到hint就往上一股腦的填,而應該去嘗試復盤整個過程,並考慮到各種情況,無重復、無遺漏地對每個情況進行處理


四、指導書反饋

個人認為在load_icode_mapper中的函數內容可以略加改善

在其中,部分已提供的代碼是一個全局循環,其中iBY2PG為一個單位遞增

/*Step 1: load all content of bin into memory. */
    for (i = 0; i < bin_size; i += BY2PG) {
        /* Hint: You should alloc a new page. */
    }

但實際上,經過分析我們得出i要想變化到正確位置,每次遞增的量並不一定是BY2PG(詳見思考5),且並不一定每一個段都需要alloc操作,因此我認為這段代碼具有一定的誤導性,且如果要完全保留的話可能會在后面寫出一些意義不明的代碼(如強制調整i的位置),可在之后的課程中改善。


五、殘留難點

本次實驗的主要殘留難點在於進程的行為,即這個進程在執行時在做什么。

個人目前的猜測是進程的行為和之前發現的輸出數據一樣,存儲在code_a.c的大數組中,用機器碼表示,但目前仍然沒有具體找出能夠代表行為的代碼,可能在之后的lab中能夠解析進程的行為。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM