linux内核双向循环队列,读书笔记之linux内核设计与实现(2)进程调度
調度程序是內核的組成部分,它負責選擇下一個要運行的進程。進程調度程序可看作在可運行態進程之間分配有限的處理器時間資源的內核子系統。
多任務操作系統就是能夠同時并發的交互執行多個進程的操作系統。多任務系統可以劃分為兩類:搶占式和非搶占式。Linux提供了搶占式的多任務模式。在此
模式下,由調度程序來決定什么時候停止一個進程的運行以便其他進程能夠得到執行機會。這個強制的掛起動作叫做搶占。進程在被搶占之前能夠運行的時間是預先
設置好的,而且有一個專門的名字,叫進程的時間片。時間片實際上就是分配給每個可運行進程的處理器時間段。
4.1 策略
4.1.1 進程優先級
進程可以分為I/O消耗型和處理器消耗型。調度算法中最基本的一類就是基于優先級的調度。這是一種根據進程的價值和其對處理器時間的需求來對進程分級的想
法。在Linux系統中,優先級高的進程使用的時間片也較長。調度程序總是選擇時間片未用盡而且優先級高的進程運行。用戶和系統都可以通過設置進程的優先
級來影響系統的調度。
Linux內核提供了兩組獨立的優先級范圍。第一種是nice值,范圍 -20~19,默認是0。值越大,優先級越低,時間片越短。第二個范圍是實時優先級,其值可配置,變化范圍:0~99。
4.2 可執行隊列(運行隊列)
當內核要尋找一個新的進程在CPU上運行時,必須只考慮處于可運行狀態的進程,(即在TASK_RUNNING狀態的進程),因為掃描整個進程鏈表是相當低效的,所以引入了處于可運行狀態的進程的雙向循環鏈表,也叫運行隊列(runqueue)。
運行隊列容納了系統中所有可以運行的進程,它是一個雙向循環隊列,該隊列通過task_struct結構中的兩個指針run_list(Pointers to the next and previous elements in the runqueue list to which the process belongs)鏈表來維持。隊列的標志有兩個:一個是“空進程”idle_task、一個是隊列的長度。
有兩個特殊的進程永遠在運行隊列中待著:當前進程和空進程。前面我們討論過,當前進程就是由cureent指針所指向的進程,也就是當前運行著的進程,但是請注意,current指針在調度過程中(調度程序執行時)是沒有意義的;空進程是個比較特殊的進程,只有系統中沒有進程可運行時它才會被執行,Linux將它看作運行隊列的頭,當調度程序遍歷運行隊列,是從idle_task開始、至idle_task結束的,在調度程序運行過程中,允許隊列中加入新出現的可運行進程,新出現的可運行進程插入到隊尾,這樣的好處是不會影響到調度程序所要遍歷的隊列成員,可見,idle_task是運行隊列很重要的標志。
另一個重要標志是隊列長度,也就是系統中處于可運行狀態(TASK_RUNNING)的進程數目,用全局整型變量nr_running表示,在/kernel/fork.c中定義如下:
int nr_running=1;
若nr_running為0,就表示隊列中只有空進程。在這里要說明一下:若nr_running為0,則系統中的當前進程和空進程就是同一個進程。但是Linux會充分利用CPU而盡量避免出現這種情況。
由于可執行隊列是調度程序的核心數據結構體,所以有一組宏定義用于獲取與“給定處理器或進程”相關的可執行隊列。cpu_rq(processor)宏用于返回給定處理器可執行隊列的指針。this_rq()宏用來返回當前處理器的可執行隊列。宏task_rq(task)返回給定任務所在的隊列指針。
在
對可執行隊列進行操作以前,應該先鎖住它。因為每個可執行隊列唯一的對應一個處理器。在其擁有者讀取或者改寫隊列成員的時候,可執行隊列包含的鎖用來防止
隊列被其它代碼改動。鎖住運行隊列最常見的情況發生在你想鎖住的運行隊列上恰巧有一個特定的任務在運行。此時需要用到task_rq_lock()和
task_rq_unlock()函數:
struct runqueue *rq;
unsigned long flags;
rq = task_rq_lock(task,&flags);
對任務隊列的操作
task_rq_unlock(rq, &flags);
一個疑問,解鎖后,之前的任務還繼續運行嗎?
睡眠和喚醒
休
眠(被阻塞)的進程處于一個特殊的不可執行狀態。
進程休眠有種原因,但肯定都是為了等待一些事件。事件可能是一段時間,從文件I/O讀取更多的數據,或者是某個硬件事件。一個進程還有可能在嘗試獲取一個
已被占用的內核信號量時被迫進入休眠。休眠的一個常見原因是文件I/O——如進程對一個文件執行了read()操作,而這需要從磁盤里讀取。還有,進程在
獲取鍵盤輸入的時候也需要等待。無論哪種情況,內核的操作都相同:進程把它自己標記成休眠狀態,把自己從可執行隊列移出,放入等待隊列,然后調用
schedule()選擇和執行一個其他進程。喚醒的過程剛好相反;進程被設置為可執行狀態,然后再從等待隊列中移到可執行隊列。??? 等待隊列:休眠通過等待隊列來處理,等待隊列是由等待某些事件發生的進程組成的簡單鏈表。
4.3 搶占和上下文切換
上下文切換,也就是從一個可執行進程切換到另一個可執行進程,由定義在kernel/sched.c中的context_switch()函數處理。每當一個新的進程被選出來準備投入運行的時候,schedule()就會調用該函數。它完成兩項基本工作:
(1)調用定義在中的switch_mm(),該函數負責把虛擬內存從上一個進程映射切換到新的進程中(前面已經提及,虛擬內存讓進程在獲取和使用內存時覺得自己擁有整個系統的所有內存資源)。
(2)調用定義在中的switch_to(),該函數負責從上一個進程的處理器狀態切換到新進程的處理器狀態(虛擬處理器)。這包括保存、恢復棧信息和寄存器信息。
4.3.1 用戶搶占
內
核即將返回用戶空間的時候,如果need_resched標志被設置,會導致schedule()被調用,此時就會發生用戶搶占。內核無論是從中斷處理程
序還是在系統調用后返回,都會檢查need_resched標志。如果它被設置了,那么內核會選擇一個其他進程投入運行。從中斷處理程序或系統調用返回的代碼都是和體系結構相關的,在entry.S(此文件不僅包含內核入口部分的程序,內核退出部分的代碼也在其中)文件中通過匯編語言來實現。
4.3.2 內核搶占
只
要當前進程沒有持有鎖,內核就可以進行搶占。鎖是非搶占區的標志。所以,如果沒有持有鎖,那么正在執行的代碼就是可重新導入的,也就是可搶占的。
為了支持內核搶占,每個進程的thread_info引入了preempt_count計數器。初始值為0,每當使用鎖的時候就加1,釋放鎖的時候就減
1。當=0時,內核就可以被搶占。從中斷返回內核空間的時候,內核會檢查need_resched和preempt_count的值。如果前者被設置,且
后者為0,則說明有一個更為重要的任務需要執行并且可以安全的搶占,此時,調度程序就會被調用。
4.4 與調度相關的系統調用
總結
以上是生活随笔為你收集整理的linux内核双向循环队列,读书笔记之linux内核设计与实现(2)进程调度的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 打算开源一个低代码平台,第二天,包含【工
- 下一篇: javascript是一门多线程的语言_