LAB4 多進程調度

我們思考這幾個問題:

  • 操作系統kernel要如何實現多處理器支持?
  • 用戶進程能不能由用戶進程創建?怎麼實現?提到fork()、exec()你想到什麼?
  • 操作系統kernel要如何支持多進程?你設計的多進程調度可能在哪些地方會有性能弊端?如何去改進?
  • 你的kernel支持了多進程,那兩個(多個)用戶進程之間可不可以進行通信呢?要怎麼設計呢?

多處理器支持

  • JOS將多CPU分成一個BSP、多個AP。每個CPU對資源有等效訪問權。BSP負責初始化系統、引導啓動操作系統並激活應用程序處理器APs。開機時和開機後的準備工作由BSP處理,在準備工作中BSP根據MP表逐個激活AP,每個AP在內存上都有一段自己的棧。
  • 假如多個CPU同時訪問同一個I/O設備怎麼辦?假如多個CPU同時寫同一段內存怎麼辦?假如多個CPU同時發生了中斷要轉入內核態怎麼辦?
    • 前兩個問號:多個CPU對資源同等訪問權,所以我們採用加鎖的方式來應對這個情況。
    • 第三個問號:內核態下是全能的,也是要慎重的。假若多個CPU同時工作在內核態下,那管理起來很噁心的。最優雅的設計是設置一個內核鎖,任意時刻只允許一個CPU處於內核態。

用戶進程能不能由用戶進程創建?

你設計的OS應該支持這個!

  • 我們可以去想,就兩種情況:一個是User Environment複製出和自己一樣的User Environment,一個是User Environment從外存load一個User Environment到操作系統進程列表裏。這就是大家熟知的fork()和exec()。
  • fork()的邏輯是這樣的:在操作系統進程鏈表中添加新進程;複製原進程的內存段給fork出的新進程;將新進程加入進程調度隊列。如果你去看linux,windows這些成品的設計和運行邏輯的話,你會發現fork()的使用頻率很高。而fork操作複製內存段時的時間開銷是很拖沓讓人不舒服的,那我們就想想怎麼優化它?我們發現,很多情況下,A去fork出B後,B並不會急於修改自己的內存段。那可不可以fork時不copy內存空間,直接讓新進程的內存空間指針指向原進程的?可以!只要當原進程或新進程希望修改自己內存段時把它們分開就好了。也就是變相延後或者省掉了fork的內存複製操作。這項設計叫“copy-on-write fork”。

多進程支持

從字面意義上看“多進程”,很好理解,把多個User Environment分配到多個CPU上就行了。但是動起手來會發現,這個“調度”的設計很硬核。如果設計不好,在用戶視角上看到的是等待拖延宕機,這必然是整個操作系統最要命的點。。。

調度算法focus on 這兩點:

  • 調度進程上CPU的先後順序
  • 被調度進程每次上CPU後要執行多久

我們所面臨的問題是這樣的:

  • 每時每刻都有新進程加入,老進程退出。
  • 用戶希望多個User Environment可以同時跑,但主板上的CPU核心數量有限,絕大多數情況下,要運行的User Environment數遠大於處理器數。
  • 不同進程的時效性不同,對用戶的重要性不同。

JOS中只實現了一個簡單的輪轉調度算法。而目前Linux的多進程調度算法急於完全公平調度算法實現,完全公平調度(CFS)到最後一節看。

img

多用戶進程通信

爲什麼需要“多進程通信”?

數據傳輸,資源共享,通知事件,進程同步、互斥。

怎麼去實現“多用戶通信”?

在內存上開一段空間,讓多個進程都可以訪問。想一下,如果這段內存區間是在某一個用戶進程的地址空間內,那會很難管理吧?所以這段內存要開在內核區上。

至此,我們可以明確:“多用戶通信”就是在內核區內存開一段緩衝區,多個用戶進程按照一定的規則分別對這段緩衝區有一定的操作權限。

“規則”怎麼講?我們看看Linux都有什麼“規則”:

“pipe” 無名管道

只用於具有親緣關係的進程間通信(父子進程、兄弟進程)。

嚴格A進程寫,B進程讀的規則。數據只能單向流動。半雙工。

多通過read(),write()進行操作。

其原型爲

#include <unistd.h>
int pipe(int fd[2]); //成功返回1,失敗返回-1

其中fd[1]是用來寫的文件描述符,fd[0]是用來讀的文件描述符。

FIFO 命名管道

以特殊設備文件的形式存在於外存中。類似於在進程間使用文件來傳輸數據。

數據讀出時,FIFO管道中同時清除數據。

用read(),write()進行操作。

其原型爲:

#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 成功返回0,失敗返回-1

共享內存

需要使用信號量對多個要訪問它的進程進行同步。
其原型爲:

#include <sys/shm.h>

// 創建或獲取一塊共享內存:成功返回其ID,失敗返回-1
int shmget(key_t key, size_t size, int flag);

// 連接共享內存到當前進程的地址空間:成功返回指向共享內存的指針,失敗返回-1
void *shmat(int shm_id, const void *addr, int flag);

// 斷開與共享內存的連接:成功返回0,失敗返回-1
int shmdt(void *addr);

// 控制共享內存的相關信息:成功返回0,失敗返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

socket網絡通信

只用於通過網絡通信的在兩臺機器上的兩個用戶進程。

完全公平調度算法(CFS)

是在linux 2.6.23(2007年10月)中出現的調度算法。由Ingo Molnár所提出。

對於每個CPU核心,歸屬於它的所有RUNNING的進程用一個隊列進行維護,即cfs_rq。

struct cfs_rq {
    struct load_weight load;  //運行隊列總的進程權重
    unsigned int nr_running, h_nr_running; //進程的個數

    u64 exec_clock;  //運行的時鐘
    u64 min_vruntime; //該cpu運行隊列的vruntime推進值, 一般是紅黑樹中最小的vruntime值

    struct rb_root tasks_timeline; //紅黑樹的根結點
    struct rb_node *rb_leftmost;  //指向vruntime值最小的結點
    //當前運行進程, 下一個將要調度的進程, 馬上要搶佔的進程, 
    struct sched_entity *curr, *next, *last, *skip;

    struct rq *rq; //系統中有普通進程的運行隊列, 實時進程的運行隊列, 這些隊列都包含在rq運行隊列中  
    ...
};

數據結構中的每個進程,都有一個屬性:虛擬運行時間(vruntime)。它在進程的sched_entity結構體裏。

vruntime = 實際運行時間 * 1024 / 進程權重
struct sched_entity {
    struct load_weight  load; //進程的權重
    struct rb_node      run_node; //運行隊列中的紅黑樹結點
    struct list_head    group_node; //與組調度有關
    unsigned int        on_rq; //進程現在是否處於TASK_RUNNING狀態

    u64         exec_start; //一個調度tick的開始時間
    u64         sum_exec_runtime; //進程從出生開始, 已經運行的實際時間
    u64         vruntime; //虛擬運行時間
    u64         prev_sum_exec_runtime; //本次調度之前, 進程已經運行的實際時間
    struct sched_entity *parent; //組調度中的父進程
    struct cfs_rq       *cfs_rq; //進程此時在哪個運行隊列中
};

每個cfs_rq內所有進程的sched_entity使用紅黑樹維護。vruntime作爲鍵值。

每次時鐘中斷髮生時;更新當前進程的vruntime;在紅黑樹中找到vruntime最小的進程搶佔當前進程,當然如果還是它自己就無搶佔一說。時間複雜度O(logN)。

新進程加入時:初始化其vruntime,並將其插入到紅黑樹中。時間複雜度O(logN)。

舊進程銷燬時:從紅黑樹中刪除該節點。時間複雜度O(logN)。

新進程的vruntime初始值怎麼安排?

每個CPU的運行隊列cfs_rq都維護一個min_vruntime變量。新進程的初始vruntime值以它所在運行隊列的min_vruntime爲基礎來設置,這樣就能與老進程保持在合理的差距範圍內。

休眠進程vruntime值一直不變嗎?

休眠進程被喚醒時重設vruntime,以min_vruntime爲基礎給予一定補償。這樣能把進程間的vruntime保持在合理的差距範圍內。

進程從CPU A切換到CPU B上,vruntime怎麼變化?

新的vruntime = 舊的vruntime - min_vruntime_A + min_vruntime_B

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章