一、程序資料結構和組織
二、程序切換
三、程序建立
四、程序排程
程序是一個程式執行的例項,作業系統透過並行和併發的執行多個程序實現多個任務的並行處理;從系統資源的角度看,多個程序同時執行時,作業系統以程序為單位來分配系統資源(比如CPU時間、記憶體等);
程序作為系統資源分配的實體,而排程的基本單位是執行緒;在linux中,對於有多個使用者態執行緒的程序,linux中用輕量級程序來實現這些使用者態執行緒,然後將輕量級程序和核心態執行緒對應起來,這樣輕量級程序之間就可以共享程序的記憶體地址空間、開啟檔案集等資源,並且可以被獨立排程;
一、linux程序資料結構和組織
Linux透過程序描述符task_struct來管理程序,描述符裡面包括了系統給程序分配的資源情況(比如記憶體描述符、開啟的檔案等);每個程序都有一個唯一的程序識別符號PID,存放在程序描述符的pid欄位中,同一個程序的所有執行緒有相同的PID;
對於每個程序,核心都要在核心地址空間給程序分配兩個連續頁框(可以配置成非連續頁框),這個連續頁框用於存放兩個資料結構,一個是執行緒描述符thread_info(其中thread_info結構體的成員包括程序描述符指標),另一個是核心態的程序堆疊,這樣程序切換到核心態後,就可以透過esp暫存器找到程序描述符;
程序的狀態:程序描述符的state欄位描述了程序當前所處的狀態;程序狀態有:可執行狀態、可中斷的等待狀態、不可中斷的等待狀態、暫停狀態、跟蹤狀態、僵死狀態、僵死撤銷狀態;
程序描述符組織:linux把所有程序的程序描述符放在一個程序連結串列裡面,程序連結串列的頭是init_task;對於可執行狀態的程序,linux用單獨的連結串列來組織,為了提高排程程式的執行速度,建立多個可執行程序連結串列,每個程序優先權對應一個程序連結串列,並且每個cpu都有這樣一組可執行程序連結串列;
等待佇列:對於等待特殊事件而暫時休眠的可執行程序,linux引入了等待佇列機制來組織這些程序,等待佇列實現了在事件上的條件等待,希望等待特定事件的程序把自己放進合適的等待佇列,並放棄CPU控制權,然後,事件完成後,由另外一個流程喚醒,大致流程如下:
wait_queue_t wait; --- 定義一個等待佇列成員
init_waitqueue_entry(&wait, current); --- 初始化等待佇列成員
current->state = TASK_UNINTERRUPTIBLE; --- 程序狀態設定為不可中斷喚醒
add_wait_queue(wq, &wait); --- 把程序加入等待佇列wq
schedule(); --- 排程程序,放棄CPU控制權
remove_wait_queue(wq, &wait); --- 程序重新執行,從等待佇列移出
二、程序切換
每個程序都是自己獨立的地址空間,執行時獨佔CPU的資源(比如CPU的暫存器),並且CPU的定址都在這個程序的地址空間中進行;因此,程序切換時主要得完成兩個步驟:第一、將頁全域性目錄切換到新程序的頁全域性目錄,這樣CPU就可以在新程序的地址空間定址;第二、把CPU的硬體上下文儲存起來,這樣下次才可以在切換的點繼續執行下去,這個硬體上下文主要是CPU的暫存器,其中大部分暫存器儲存在程序描述符一個型別為thread_struct的thread欄位,部分諸如eax、ebx等通用暫存器儲存在核心堆疊中;
需要指出的是,多執行緒的應用程序有多個輕量級程序,如果程序切換在同一個程序的輕量級程序之間進行,那麼因為這些輕量級程序共享程序的地址空間,因此切換時不需要切換也全域性目錄,只需要切換硬體上下文即可;
三、 程序建立
Linux程序的建立策略是,基於已有程序建立,透過複製已有程序的資料結構建立新程序,期間透過clone標誌控制如何複製這些資料結構,程序建立完成後,可以被排程執行,不過執行起來的程式碼和父程序是一樣的,需要呼叫excve等函式載入子程序的可執行檔案,如果建立的是輕量級程序,那麼就是執行緒,指定執行緒的回撥函式即可,不用excve載入可執行檔案;
因此程式建立的程序具有父/子關係,子程序之間具有兄弟關係;程序0和程序1是由核心建立,程序1(init)是所有程序的祖先;
建立子程序的函式是clone()、fork()及vfork();這些函式會建立程序的堆疊、程序描述符以及程序需要的其他資料結構,函式返回後,建立的程序已經可以被排程執行,關鍵的幾個步驟如下:
- 查詢pidmap_array點陣圖給子程序分配新的PID;
- 呼叫__unlazy_fpu(),把FPU、MMX和SSE/SSE2暫存器的內容儲存到父程序的thread_info結構中;
- 執行alloc_task_struct()宏,為新程序獲取程序描述符,然後把current程序描述符的內容複製到剛獲取的程序描述符中;
- 執行alloc_thread_info宏獲取一塊空閒記憶體區,用來存放新程序的thread_info結構和核心棧,然後把current程序的thread_info描述符的內容複製到剛分配的thread_info結構中;
- 檢測程序數量是否超過限制;
- 初始化子程序描述符中的list_head資料結構和自旋鎖,併為與掛起訊號、定時器及時間統計表相關的幾個欄位賦值;
- 呼叫copy_semundo()、copy_files()、copy_fs()、copy_sighand()、copy_signal()、copy_mm()和copy_namespace()建立新的資料結構,並把父程序相應資料結構的值複製到新資料結構中;
- 呼叫copy_thread(),用clone()系統呼叫時CPU暫存器的值來初始化子程序的核心棧,子程序描述符的thread.esp欄位初始化為子程序核心棧的基地址;
- 呼叫sched_fork()完成對新程序排程程式資料結構的初始化,該函式把新程序的狀態設定為TASK_RUNNING,並把thread_info結構的preempt_count欄位設定為1,從而禁止核心搶佔,為了保證公平的程序排程,該函式在父子程序之間共享父程序的時間片;
- 執行SET_LINKS宏,把新程序描述符插入程序連結串列;
- 新程序已經被加入程序集合,遞增nr_threads變數的值,遞增total_forks變數以記錄被建立的程序的數量,至此,子程序已經處於可執行狀態,但是限制子程序還在執行佇列中,需要排程程式排程才能執行,當排程程式排程到子程序時,把子程序thread中的值載入到對應的CPU暫存器,特別是把thread.esp裝入esp暫存器,把函式ret_from_fork()的地址裝入eip暫存器,用存放在棧中的值再裝載所有的暫存器,並強迫CPU返回到使用者態,然後在fork()、vfork()或clone()函式返回時,新程序開始執行,函式的返回值放在eax暫存器中,返回給子程序的值是0,返回給父程序的值是子程序的PID,應用程式的編寫可以基於這個事實,在fork()、vfork()或clone()函式返回的地方加個if/else條件判斷子程序和父程序的不同流程;
四、程序排程
Linux程序的排程策略是程序響應時間儘可能快,後臺作業的吞吐量儘可能高,儘可能避免程序的飢餓現象,低優先順序和高優先順序程序儘可能調和;
Linux的排程基於分時技術,給每個程序分配時間片,時間片到期時,程序切換動作就會發生;每個程序都有一個排程優先順序,對於可搶佔的linux(也可以配置成不可搶佔的系統),當高優先順序的程序進入可執行狀態時,就可以搶佔低優先順序的程序,比如一個文字編輯程序,當使用者敲鍵盤後,觸發中斷,核心喚醒文字編輯程序,並且確定文字編輯程序的優先順序比current程序高,那麼就會把文字編輯程序的TIF_NEED_RESCHED標誌設定,這樣核心處理完中斷後就會啟用排程程式;
Linux對不同的程序採用不同的排程策略,目前有5種型別的程序,停機排程類程序、期限排程類程序、實時排程類程序、公平排程類程序、空閒排程類程序,這5類程序優先順序從高到低;
停機排程類:優先順序最高,可以搶佔其他所有程序,但是不能搶佔停機程序,目前只有遷移程序屬於停機排程類,每個處理器有一個遷移程序,遷移程序的任務是把程序從當前處理器遷移到其他處理器,對外偽裝成實時優先順序是99的先進先出實時程序;停機程序沒有時間片,如果不主動讓出處理器,那麼就一直霸佔處理器;
期限排程類:優先順序是-1,比實時排程類優先順序高,但是比停機排程類優先順序低;如下圖所示,每個週期執行一次,在截至期限之前執行完;
實時排程類:用於實時程序的排程,實時程序的優先順序是1~99;有兩種排程方式,一種是先進先出排程策略,這種排程策略,每次程序執行完時間片後,都是放到對應優先順序佇列的隊首,那麼如果沒有高優先順序的程序,處理器會繼續選擇這個程序執行,直到這個程序主動放棄cpu,那麼同優先順序的程序就沒有機會執行;另一種策略是輪流排程,每次時間片執行完後,把程序放到佇列尾部,這樣同優先順序的程序就有機會執行;
公平排程類:用於普通程序的排程,普通程序的優先順序是100~139;使用完全公平排程演算法,這種演算法引入了虛擬執行時間的概念:虛擬執行時間 = 實際執行時間 X nice 0對應的權重 / 程序的權重;優先順序越高,程序的權重就越大,那麼實際執行時間相同的情況下,虛擬執行時間就越小;完全公平排程演算法使用紅黑樹把程序按虛擬執行時間從小到大排序,每次排程時選擇虛擬執行時間最小的程序;
空閒排程類:針對空閒執行緒,每個處理器有一個空閒執行緒,僅當沒有其他程序可以排程的時候,才會排程空閒執行緒;
參考資料:
《深入理解linux核心》
《linux核心深度解析》