MIT 6.828 JOS/XV6 LAB4-partA


這一部分要實現的是對多核處理器的支持,然后實現系統調用只喜歡用戶應用創建新的應用,而且還要實現round-robin調度算法

Multiprocessor support

jos中對CPU進行了抽象

clip_image001

要描述一個CPU, 需要知道id,運行狀態,當前正在運行的進程

所有的cpu數目放在cpus數組中

clip_image002

接下來則是對有多個cpu的處理器的抽象,這里使用了三個結構體,總之是比較亂,目前還不能完全看懂

多核處理器的初始化都在mp_init函數中完成,首先是調用mpconfig函數,主要功能是尋找一個MP 配置條目,然后對所有的CPU進行配置,找到啟動的處理器

clip_image003

接下來就是要完成lapic(local advanced programmable interrupt controller)

apic主要是為與其連接的處理器傳送中斷信號

而CPU控制與其相關聯的apic需要通過讀寫其中的寄存器,而讀寫寄存器則是通過映射IO(memory mapped IO)的方法來實現的

有一些物理內存是與apic的寄存器硬件相連的,這樣可以通過讀寫內存完成對寄存器的讀寫

JOS中在ULIM之上,預留了4MB的內存空間來映射APIC的寄存器

所以在使用APIC之前首先要完成映射。首先需要調用mmio_map_region函數來實現

mmio_map_region函數的實現

這部分的實現還是比較簡單的,注意這里的靜態變量base,是記錄當前還未分配空間的其實地址的

clip_image004

從MMIOBASE開始,分配出size大小的空間出來,調用boot_map_region即可

Exercise 2

在操作系統啟動之后,需要啟動其他的應用cpu(application processor, ap)

都在boot_aps中完成

clip_image005

這里是將啟動代碼放到了MPENTRY_PADDR處,而代碼的來源則是在kern/mpentry.S中,這里的代碼功能與boot.S中的非常類似,主要就是開啟分頁,轉到內核棧上去,當然這個時候實際上內核棧還沒建好呢

在執行完mpentry.S中的代碼之后,將會跳轉到mp_main函數中去

而這里需要提前做的,就是將MPENTRY_PADDR處的物理頁表標識為已用,這樣不會講這一頁放在空閑鏈表中分配出去

只需要在page_init中添加一個判斷就可以了

clip_image006

question

clip_image007

這里的mpentry.S的代碼是鏈接到KERNBASE之上的,也就是說,其中的符號的地址都是在KERNBASE之上的,但是實際上現在是將這些代碼移動到了物理地址0X7000處,而且當前的AP處於實模式下,只支持1MB的物理地址尋址,所以這個時候需要計算相對於0X7000的地址,才能跳轉到正確的位置上去

Exercise 3

因為從一個核變成了多個核,所以現在需要注意區分哪些是一個核獨有的,哪些是共享的

每個核獨有的變量應該有:

  1. 內核棧,因為不同的核可能同時進入到內核中執行,因此需要有不同的內核棧
  2. TSS描述符
  3. 每個核的當前執行的任務
  4. 每個核的寄存器

首先需要為每個核都分配一個內核棧,修改mem_init_mp代碼

clip_image008

每個內核棧的大小是KSTKSIZE,而內核棧之間的間距是KSTKGAP,起到保護作用

Exercise 4

需要為每個核的TSS進行初始化任務

clip_image009

Exercise 5

在完成上述的工作之后,4個CPU都啟動,但是除了BSP之外,剩下的三個AP都是在空轉

因為這個時候還沒有處理競爭,所以如果三個AP進入了內核代碼的話,很可能會出現錯誤,所以首先需要解決這個問題

一般在單處理器操作系統中會采用大內核鎖,就是當一個CPU需要進入內核的時候,必須獲取整個內核的鎖

這樣的話,所有的CPU可以並行的跑用戶程序,但是內核程序最多只能有一個在跑

這個當然是很粗糙的設計,但確實簡單而且安全的

更好的設計是給進程表的每個條目以及其他可能出現競爭的變量單獨的加鎖,這樣可以在內核上實現更高的並行度,但是會大福大增加設計的復雜性,比如說XV6就使用了不止一個鎖

在kern/spinlock.*中實現了lock_kernel和unlock_kernel()函數,單反是進入到內核臨界區之前都需要加鎖,離開內核臨界區之后需要盡快釋放鎖

  1. 在啟動的時候,BSP啟動其余的CPU之前,BSP需要取得內核鎖
  2. Mp_main中,也就是CPU被啟動之后執行的第一個函數,這里應該是調用調度函數,選擇一個進程來執行的,但是在執行調度函數之前,必須獲取鎖
  3. trap函數也要修改,因為可以訪問臨界區的CPU只能有一個,所以從用戶態陷入到內核態的話,要加鎖,因為可能多個CPU同時陷入內核態
  4. Env_run函數,也就是啟動進程的函數,之前在試驗3中實現的,在這個函數執行結束之后,就將跳回到用戶態,此時離開內核,也就是需要將內核鎖釋放

啟動其他CPU之前加鎖

clip_image010

在調用調度函數,也就是進入內核臨界區的時候加鎖

clip_image011

如果是從用戶態進入到內核態的話,需要獲得鎖

clip_image012

在env_pop_tf執行結束之后,就回到用戶態了,所以一定要在此之前釋放

clip_image013

lock_kernel的調用關系圖

clip_image014

unlock_kernel的調用關系圖

clip_image015

Question 2

既然有了大內核鎖,為什么還需要為四個CPU分配不同的內核棧

因為不同的內核棧上可能保存有不同的信息,在一個CPU從內核退出來之后,有可能在內核棧中留下了一些將來還有用的數據,所以一定要有單獨的棧

Exercise 6

實現round-robin調度算法

主要是在sched_yield函數內實現,從該核上一次運行的進程開始,在進程描述符表中尋找下一個可以運行的進程,如果沒找到而且上一個進程依然是可以運行的,那么就可以繼續運行上一個進程

同時將這個算法實現為了一個系統調用,進程可以主動放棄CPU

clip_image016

clip_image017

上面這張圖可以清楚的看到什么時候會調用調度器

  1. 初始化的時候,BSP選擇一個進程來運行
  2. AP啟動結束,選擇一個進程運行
  3. 進程運行結束之后,選擇下一個可運行的進程
  4. 進程主動調用系統調用,放棄CPU
  5. 產生時鍾中斷,當前進程CPU時間片結束
  6. 陷入內核之后發現當前進程是僵屍進程,殺掉

接下來是兩個問題

Question 3

clip_image018

這里的問題問的是,在lcr3運行之后,這個CPU對應的頁表就立刻被換掉了,但是這個時候的參數e,也就是現在的curenv,為什么還是能正確的解引用?就是下面這一段

clip_image019

這個問題比較簡單,因為當前是運行在系統內核中的,而每個進程的頁表中都是存在內核映射的,之前也說過,每個進程頁表中虛地址高於UTOP之上的地方,只有UVPT不一樣,其余的都是一樣的,只不過在用戶態下是看不到的

所以雖然這個時候的頁表換成了下一個要運行的進程的頁表,但是curenv的地址沒變,映射也沒變,還是依然有效的

Question 4

clip_image020

當然要保存,這個還用說。。。

是在trap中保存的

clip_image021

所以說,每次進入到內核態的時候,當前的運行狀態都是在一進入的時候就保存了的

如果沒有發生調度,那么之前trapframe中的信息還是會恢復回去,如果發生了調度,恢復的就是被調度運行的進程的上下文了

System calls for environment creation

就是實現最笨的fork

不過給定的函數是將功能細分的,而實現的比較笨的fork放在了user/dumbfork.c文件中

這個函數所做的事情就是將當前進程的寄存器,所有的頁的內容全部都復制過來,唯一不同的地方就是返回值不同,而實現返回值不同就是將存放返回值的eax寄存器的值修改一下就可以了

一個個函數的看

sys_exofork函數

clip_image022

這里調用env_alloc函數,而這個函數所做的,就是做一系列的准備工作,生成存放頁表的頁,初始化頁表內容等

然后將父進程的上下文內容全部拷貝過來,除了返回值eax

但是此時只能將這個進程設置為不可運行,因為還沒有將頁表映射復制過來

sys_env_set_status函數

clip_image023

就是將狀態設置為可執行或者不可執行而已

sys_page_alloc函數

clip_image024

主要功能就是申請一頁物理內存,然后把它映射到虛擬地址va上去

sys_page_map函數

clip_image025

主要功能就是將進程id為srcenvid的進程的srcva處的物理頁的內容,映射到進程id為dstenvid的進程的dstva處

sys_page_unmap函數

clip_image026

就是解出映射

但是看上面幾個函數實際上看不出啥來,還是看看dumbfork怎么實現的吧

clip_image027

所以這里dumbfork做的事情就是首先獲取新生成的子進程的進程id

然后將父進程的進程空間中的每一頁的內容都復制過去,對於處在地址addr處的一頁,具體做法就是首先為子進程申請一個物理頁,然后將這個物理頁映射到子進程虛擬地址addr處

然后同時將這個物理頁頁映射到進程Id為0的進程的虛擬地址PTSIZE處,然后利用memmove,將父進程addr處的內容移動到進程id為0de進程的虛地址PTSIZE處,此時對應的物理頁也就有了相同的內容

之所以這樣做,是因為如果要復制內容的話,要么在內核中復制,但是也需要將兩頁映射到內核空間中來,完成復制之后再解映射

而這里用了一種還算是巧妙的方法,因為當前進程的PTSIZE之上是不用的,所以都映射到那去,復制內容的完成也在那

clip_image028

當然實際系統肯定不這么干,都是用copy on write技術,這個partB中再搞


免責聲明!

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



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