深入Linux內核架構——進程管理和調度(二)

一、調度器的實現

調度器的任務是在程序之間共享CPU時間,創造並行執行的錯覺。該任務可分爲調度策略和上下文切換兩個不同部分。

1、概觀
暫時不考慮實時進程,只考慮CFS調度器。經典的調度器對系統中的進程分別計算時間片,使進程運行直至時間片用盡,所有進程的所有時間片用完時,需要重新計算。相比之下,CFS只考慮進程等待時間,即進程在就緒隊列(run_queue)中已等待的時間,對CPU時間需求最嚴格的進程被調度執行。每次調度器會挑選具有最高等待時間的進程提供CPU,如此進程的不公平等待不會被積累,而會均勻分佈到系統所有進程。

下圖是CFS調度器的工作原理,所有可運行進程都按等待時間在一個紅黑樹中排序,等待CPU時間最長的進程是最左側的項,調度器下一次會考慮該進程。等待時間稍短的進程在該樹上從左至右排序。(調度器時間複雜度爲O(logn))
在這裏插入圖片描述
除了紅黑樹外,就緒隊列還裝備了虛擬時鐘。該時鐘的時間流逝速度慢於實際的時鐘,精確的速度依賴於當前等待調度器挑選的進程數目(如4個進程,那麼虛擬時鐘將以實際時鐘的四分之一的速度運行,如果以完全公平的方式分享計算能力,那麼該時鐘是判斷等待進程將獲得多少CPU時間的基準)。

就緒隊列的虛擬時間由fair_clock給出,進程的等待時間保存在wait_runtime中,爲排序紅黑樹上的進程,內核使用差值fair_clock - wait_runtime的絕對值。fair_clock是完全公平調度的情況下進程將會得到的CPU時間的度量,而wait_runtime直接度量了實際系統的不足造成的不公平。

在進程允許運行時,將從wait_runtime減去它已經運行的時間。這樣,在按時間排序的樹中它會向右移動到某一點,另一個進程將成爲最左邊,下一次會被調度器選擇。

當前該調度策略還受到的影響因素:

  • 進程的不同優先級(即,nice值)必須考慮,更重要的進程必須比次要進程更多的CPU時間份額。
  • 進程不能切換得太頻繁,因爲上下文切換,即從一個進程改變到另一個,是有一定開銷的。另一方面,兩次相鄰的任務切換之間,時間也不能太長,否則會累積比較大的不公平值。

2、數據結構
調度器使用一系列數據結構排序和管理系統中的進程,調度器的工作方式與這些結構的設計密切相關。幾個組件在許多方面彼此交互,如圖2所示。在這裏插入圖片描述
激活調度器方法:

  • 直接的,進程打算睡眠或其他因素放棄cpu。(通用調度器,generic scheduler,本質上是分配器)
  • 週期性的,以固定頻率運行,不時檢查是否有必要進行進程切換。(核心調度器,core scheduler)

調度類:用於判斷接下來運行哪個進程。內核支持不同的調度策略(完全公平調度、實時調度、在無事可做時調度空閒進程),調度類使得能夠以模塊化方法實現這些策略,即一個類的代碼不需要與其他類的代碼交互。

進程切換:在選中將執行的程序之後,要執行底層的任務切換。(需要與CPU緊密交互)

注:每個進程都剛好屬於某一調度類,各個調度類負責管理所屬的進程。通用調度器自身完全不涉及進程管理,其工作都委託給調度器類。

(1)task_struct成員

各進程的task_struct有幾個成員與調度相關:

struct task_struct {
   
   
...
    int prio, static_prio, normal_prio; // prio和normal_prio表示動態優先級,static_prio表示進程的靜態優先級。靜態優先級是進程啓動時分配的優先級。它可以用nice和sched_setscheduler系統調用修改,否則在進程運行期間會一直保持恆定。normal_priority表示基於進程的靜態優先級和調度策略計算出的優先級。調度器考慮的優先級則保存在prio。由於在某些情況下內核需要暫時提高進程的優先級,因此需要第3個成員來表示。
    
    unsigned int rt_priority; //表示實時進程的優先級。該值不會代替先前討論的那些值。最低的實時優先級爲0,最高的是99。值越大,優先級越高。這裏使用的慣例不同於nice值。
    
    struct list_head run_list; //是循環實時調度器所需要的,但不用於完全公平調度器,run_list是一個表頭,用於維護包含各進程的一個運行表
    
    const struct sched_class *sched_class; //表示該進程所屬的調度器類
    
    struct sched_entity se; //由於調度器設計爲處理可調度的實體,在調度器看來各個進程必須也像是這樣的實體。因此se在task_struct中內嵌了一個sched_entity實例,調度器可據此操作各個task struct。
    
    unsigned int policy; //保存了對該進程應用的調度策略。(SCHED_NORMAL用於普通進程,SCHED_BATCH用於非交互、CPU使用密集的批處理進程,SCHED_IDLE是空閒進程,SCHED_RR和SCHED_FIFO用於實現軟實時進程,)
    
    cpumask_t cpus_allowed; //是一個位域,在多處理器系統上用來限制進程可以在哪些CPU上運行
    
    unsigned int time_slice; // 是循環實時調度器所需要的,但不用於完全公平調度器,time_slice則指定進程可使用CPU的剩餘時間段
...
}

(2)調度器類

調度器類由特定數據結構中彙集的幾個函數指針表示。全局調度器請求的各個操作都可以由一個指針表示。這使得無需瞭解不同調度器類的內部工作原理,即可創建通用調度器。

對各個調度類,都必須提供struct sched_class的一個實例。調度類之間的層次結構是平坦的:實時進程最重要,在完全公平進程之前處理;而完全公平進程則優先於空閒進程;空閒進程只有CPU無事可做時才處於活動狀態。next成員將不同調度類的sched_class實例,按上述順序連接起來,要注意這個層次結構在編譯時已經建立。

用戶層應用程序無法直接與調度類交互。它們只知道上文定義的常量SCHED_…。在這些常量和可用的調度類之間提供適當的映射,這是內核的工作。SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE映射到fair_sched_class,而SCHED_RR和SCHED_FIFO與rt_sched_class關聯。fair_sched_class和rt_sched_class都是struct sched_class的實例,分別表示完全公平調度器和實時調度器。

(3)就緒隊列

就緒隊列:核心調度器用於管理活動進程的主要數據結構。各個CPU都有自身的就緒隊列,各個活動進程只出現在一個就緒隊列中。進程不是由就緒隊列的成員直接管理的,而是有調度類管理,就緒隊列中嵌入了特定於調度器類的子就緒隊列。

就緒隊列核心成員及解釋:

struct rq {
   
   
    unsigned long nr_running; //指定了隊列上可運行進程的數目,不考慮其優先級或調度類
    
#define CPU_LOAD_IDX_MAX 5

    unsigned long cpu_load[CPU_LOAD_IDX_MAX]; //用於跟蹤此前的負荷狀態
...
    struct load_weight load; //提供了就緒隊列當前負荷的度量
    
    struct cfs_rq cfs;  //嵌入的子就緒隊列,用於完全公平調度器
    
    struct rt_rq rt;  //嵌入的子就緒隊列,用於和實時調度器
    
    struct task_struct *curr, *idle; //指向idle進程的task_struct實例,該進程亦稱爲idle線程
    
    u64 clock; //用於實現就緒隊列自身的時鐘。每次調用週期性調度器時,都會更新clock的值。
...
};

系統的所有就緒隊列都在runqueues數組中,該數組的每個元素分別對應於系統中的一個CPU。在單處理器系統中,由於只需要一個就緒隊列,數組只有一個元素。

(4)調度實體

由於調度器可以操作比進程更一般的實體,因此需要一個適當的數據結構來描述此類實體。其定義爲:

struct sched_entity {
   
   

    struct load_weight load;  //指定了權重,用於負載均衡
    
    struct rb_node run_node; //是標準的樹結點,使得實體可以在紅黑樹上排序
    
    unsigned int on_rq; //表示該實體當前是否在就緒隊列上接受調度
    
    u64 exec_start; //新進程加入就緒隊列時,或者週期性調度器中。每次調用時,會計算當前時間和exec_start之間的差值,exec_start則更新到當前時間。差值則被加到sum_exec_runtime。
    
    u64 sum_exec_runtime; //用於記錄消耗的CPU時間
    
    u64 vruntime; //統計進程執行期間虛擬時鐘上流逝的時間數量
    
    u64 prev_sum_exec_runtime; //進程被撤銷CPU時,保存當前sum_exec_runtime
...
}

3、處理優先級

(1)優先級的內核表示

在用戶空間可以通過nice命令設置進程的靜態優先級,這在內部會調用nice系統調用。進程的nice值在-20和+19之間(包含)。值越低,表明優先級越高。

內核使用一個簡單些的數值範圍,從0到139(包含),用來表示內部優先級。同樣是值越低,優先級越高。從0到99的範圍專供實時進程使用,nice值[-20, +19]映射到範圍100到139。

(2)計算優先級

進程的優先級計算需要考慮動態優先級(prio),普通優先級(normal_prio)和靜態優先級(static_prio),調用相關函數計算結果。(完成設置到優先級內核表示的轉換)

圖3綜述了針對不同類型進程的計算結果。
在這裏插入圖片描述
在進程分支出子進程時,子進程的靜態優先級繼承自父進程。子進程的動態優先級,即task_struct->prio,則設置爲父進程的普通優先級。這確保了實時互斥量引起的優先級提高不會傳遞到子進程。

(3)計算負荷權重

進程的重要性由優先級和保存在task_struct->se.load的負荷權重同時決定。進程每降低一個nice值,則多獲得10%的CPU時間,每升高一個nice值,則放棄10%的CPU時間。

4、核心調度器

調度器的實現基於兩個函數:週期性調度器函數和主調度器函數。

(1)週期性調度器

在scheduler_tick中實現,如果系統正在活動中,內核會按照頻率HZ自動調用該函數。
沒進程等待時,供電不足情況下,可以關閉週期性調度器以減少電能消耗。
主要任務是管理內核中與系統和每個進程的調度相關的統計量和激活負責當前進程的調度類的週期性調度方法。

void scheduler_tick(void)
{
   
   
    int cpu = smp_processor_id();
    
    struct rq *rq = cpu_rq(cpu);
    
    struct task_struct *curr = rq->curr;
...

    __update_rq_clock(rq);  //更新struct rq當前實例的時鐘時間戳
    
    update_cpu_load(rq);   //更新rq->cup_load[]數組
    
    if (curr != rq->idle)
    
    curr->sched_class->task_tick(rq, curr);
}

如果當前進程應該被重新調度,那麼調度器類方法會在task_struct中設置TIF_NEED_RESCHED標誌,以表示該請求,而內核會在接下來的適當時機完成該請求。

(2)主調度器

將當前cpu分配給另一個進程需要調用主調度器函數(schedule),從該系統調用返回後也要檢查當前進程是否設置了重調度標誌TIF_NEED_RESCHEDULE,如果有,內核會調用schedule。

__sched前綴的用處:有該前綴的函數,都是可能調用schedule的函數,包括schedule自身。該前綴目的在於將相關代碼的函數編譯後,放到目標文件的特定段中,.sched.text中。該信息使內核在現實棧轉儲或類似信息時,忽略所有與調度有關的調用。由於調度器函數調用不是普通代碼流程的部分,因此這種情況下是無意義的。

asmlinkage void __sched schedule( void );該函數的過程:

首先確定當前就緒隊列,並在prev中保存一個指向(當前)活動進程的task_struct的指針。
更新就緒隊列的時鐘,清除當前進程task_struct的重調度標誌TIF_NEED_RESCHED。
判斷當前進程是否在可中斷睡眠狀態,而且現在接收到信號,那麼它將再次提升爲可運行。否則,用deactivate_task講進程停止。
用put_prev_task通知調度類,當前進程要被另一進程代替。pick_next_task,選擇下一個要執行的進程。
只有1個進程,是不要切換的,還讓它留在cpu。如果已經選擇了一個新進程,就用context_switch進行上下文切換。
當前進程,被重新調度回來時,檢測是否要重新調度,如果要,就又重複前面(1)至(5)的步驟了。
(3)與fork的交互





用fork或其變體新建進程時,調度器有機會用sched_fork函數掛鉤到該進程。

單處理器中,sched_fork執行如下:

  • 初始化新進程與調度相關的字段。
  • 建立數據結構(相當簡單直接)。
  • 確定進程的動態優先級。

通過使用父進程的普通優先級作爲子進程的動態優先級,內核確保父進程優先級的臨時提高不會被子進程繼承。在用wake_up_new_task喚醒進程時,內核調用調度類的task_new將新進程加入相應類的就緒隊列中。

(4)上下文切換

內核選擇新進程之後,必須處理與多任務相關的技術細節。這些細節總稱爲上下文切換(context switching)。

  • context_switch是個分配器,它會調用所需的特定於體系結構的方法,主要進行如下操作:
  • prepare_task_switch,執行特定於體系結構的代碼,爲切換做準備。
  • switch_mm更換task_struct->mm描述的內存管理上下文。
  • switch_to切換處理器寄存器和內核棧(虛擬地址空間的用戶部分在第一步已經變更,其中也包括了用戶狀態下的棧,因此用戶棧就不需要顯式變更了)。
  • 切換前,用戶空間進程的寄存器進入和心態時保存在內核棧上,在上下文切換時,內核棧的值自動回覆寄存器數據,再返回用戶空間。

內核線程沒有自身的用戶空間內存上下文,可能在某個隨機進程地址空間的上部執行。其task_struct->mm爲NULL。從當前進程“借來”的地址空間記錄在active_mm中。

此外,由於上下文切換的速度對系統性能的影響舉足輕重,所以內核使用了一種技巧來減少所需的CPU時間。浮點寄存器(及其他內核未使用的擴充寄存器,例如IA-32平臺上的SSE2寄存器)除非有應用程序實際使用,否則不會保存。此外,除非有應用程序需要,否則這些寄存器也不會恢復。這稱之爲惰性FPU技術。由於使用了彙編語言代碼,因此其實現依平臺而有所不同,但基本原理總是同樣的。

小編推薦自己的Linux、C/C++技術交流羣:【960994558】整理了一些個人覺得比較好的學習書籍、視頻資料共享在裏面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等.),有需要的可以自行添加哦!~

在這裏插入圖片描述

二、完全公平調度類

1、數據結構
CFS的就緒隊列cfs_rq:

struct cfs_rq {
   
   
    struct load_weight load;        //維護了所有這些進程的累積負荷值
    unsigned long nr_running;    //計算了隊列上可運行進程的數目
    u64 min_vruntime;        //跟蹤記錄隊列上所有進程的最小虛擬運行時間
    struct rb_root tasks_timeline;        //用於在按時間排序的紅黑樹中管理所有進程
    struct rb_node *rb_leftmost;        //總是設置爲指向樹最左邊的結點,即最需要被調度的進程
    struct sched_entity *curr;    //指向當前執行進程的可調度實體
}

2、CFS操作
(1)虛擬時鐘

完全公平調度算法依賴於虛擬時鐘,用以度量等待進程在完全公平系統中所能得到的CPU時間。數據結構中,可以根據現存的實際時鐘與每個進程相關的負荷權重推算出來。所有與虛擬時鐘有關的計算都在update_curr中執行,該函數在系統中各個不同地方調用,包括週期性調度器之內。
在這裏插入圖片描述
首先,該函數確定就緒隊列的當前執行進程,並獲取主調度器就緒隊列的實際時鐘值(每個調度週期都會更新),如果就緒隊列上當前沒有進程正在執行,則無事可做。否則,內核會計算當前和上一次更新負荷統計量時兩次的時間差,並將其餘的工作委託給__update_curr。

然後,__update_curr需要更新當前進程在CPU上執行花費的物理時間和虛擬時間。物理時間的更新只要將時間差加到先前統計的時間即可;對於虛擬時間,對於運行在nice級別0的進程來說,定義虛擬時間和物理時間是相等的,nice值不爲0時,必須根據進程的負荷權重重新衡定時間。

最後,內核需要設置min_vruntime(min_vruntime是單調遞增的)。

CFS調度器真正關鍵點:紅黑樹根據鍵值進行排序(se->vruntime -cfs_rq->min_vruntime),鍵值較小的結點,排序位置就更靠左(被更快調度)。由此,內核實現了以下兩種對立機制:

  • 在進程運行時,其vruntime穩定地增加,它在紅黑樹中總是向右移動的。
  • 如果進程進入睡眠,則其vruntime保持不變(進程再被喚醒後,在紅黑樹的位置會更靠左)。

(2)延遲跟蹤

良好的調度延遲是 保證每個可運行的進程都應該至少運行一次的某個時間間隔(它在sysctl_sched_latency給出)。一個延遲週期中處理的最大活動數目爲sched_nr_latency,超出該上限,則延遲週期也成比例線性擴展。

通過考慮各個進程的相對權重,將一個延遲週期的時間在活動進程之間進行分配。

3、隊列操作
enqueue_task_fair和dequeue_task_fair分別用來增刪就緒隊列的成員。圖5爲enqueue_task_fair代碼流程圖。
在這裏插入圖片描述
如果通過struct sched_entity的on_rq成員判斷進程已經在就緒隊列上,則無事可做。否則,具體的工作委託給enqueue_entity。


進入enqueue_entity後,首先用updater_curr更新統計量,然後若進程此前在睡眠,那麼在place_entity中首先會調整進程的虛擬運行時間;如果進程最近在運行,其虛擬運行時間仍然有效,那麼(除非它當前在執行中)它可以直接用__enqueue_entity加入紅黑樹中。

4、選擇下一個進程
選擇下一個將要運行的進程由pick_next_task_fair執行。pick_next_task_fair的代碼流程圖如圖6所示。
在這裏插入圖片描述
如果nr_running計數器爲0,即當前隊列上沒有可運行進程,則無事可做,函數可以立即返回。否則將具體工作委託給pick_next_entity。


如果樹中最左邊的進程可用,可以使用輔助函數first_fair立即確定,然後用__pick_next_entity從紅黑樹中提取出sched_entity實例。

完成了選擇工作之後,通過set_next_entity函數將該進程標記爲運行進程。當前執行進程不保存在就緒隊列上,因此使用__dequeue_entity將其從樹中移除。如果當前進程是最左邊的結點,則將leftmost指針設置到下一個最左邊的進程。

5、處理週期性調度器
在處理週期調度時,差值sum_exec_runtime - prev_sum_exec_runtime(表示進程在CPU上執行所花時間)很重要。這個差值形式上由函數task_tick_fair負責,但實際工作由entity_tick完成。
在這裏插入圖片描述
首先,使用update_curr更新統計量。


然後判斷nr_running計數器表明隊列上可運行的進程數,如果少於兩個,則無事可做;否則由由check_preempt_tick作出決策(確保沒有哪個進程能夠比延遲週期中確定的份額運行得更長),如果進程運行時間比期望的時間間隔長,那麼通過resched_task發出重調度請求。這會在task_struct中設置TIF_NEED_RESCHED標誌,核心調度器會在下一個適當時機發起重調度。

6、喚醒搶佔
當在try_to_wake_up和wake_up_new_task中喚醒進程時,內核使用check_preempt_curr看看是否新進程可以搶佔當前運行的進程(該過程不涉及核心調度器)。

新喚醒的進程不必一定由完全公平調度器處理。如果新進程是一個實時進程,則會立即請求重調度,因爲實時進程總是會搶佔CFS進程。

當運行進程被新進程搶佔時,內核確保被搶佔者至少已經運行了某一最小時間限額(sysctl_sched_wakeup_granularity)。如果新進程的虛擬運行時間,加上最小時間限額,仍然小於當前執行進程的虛擬運行時間(由其調度實體se表示),則請求重調度。

7、處理新進程
CFS在創建新進程時調用的掛鉤函數:task_new_fair。該函數的行爲可用sysctl_sched_child_runs_first控制,用於判斷新建子進程是否需要在父進程之前運行。如果父進程的虛擬運行時間(由curr表示)小於子進程的虛擬運行時間,則意味着父進程將在子進程之前調度運行,如果子進程應該在父進程之前運行,則二者的虛擬運算時間需要換過來。然後子進程按常規加入就緒隊列,並請求重調度。

三、實時調度類

1、性質
按照POSIX標準的要求,除了“普通”進程之外,Linux還支持兩種實時調度類。調度器結構使得實時進程可以平滑地集成到內核中,而無需修改核心調度器。

實時進程與普通進程有一個根本的不同之處:如果系統中有一個實時進程且可運行,那麼調度器總是會選中它運行,除非有另一個優先級更高的實時進程。

現有的兩種實時類:

  • 循環進程(SCHED_RR)有時間片,其值在進程運行時會減少,就像是普通進程。在所有的時間段都到期後,則該值重置爲初始值,而進程則置於隊列的末尾。
  • 先進先出進程(SCHED_FIFO)沒有時間片,在被調度器選擇執行後,可以運行任意長時間。

2、數據結構
實時進程的調度類定義:

const struct sched_class rt_sched_class = {
   
   
    .next = &fair_sched_class,
    .enqueue_task = enqueue_task_rt,
    .dequeue_task = dequeue_task_rt,
    .yield_task = yield_task_rt,
    .check_preempt_curr = check_preempt_curr_rt,
    .pick_next_task = pick_next_task_rt,
    .put_prev_task = put_prev_task_rt,
    .set_curr_task = set_curr_task_rt,
    .task_tick = task_tick_rt,
};

實時調度器類的實現比完全公平調度器簡單(核心調度器的就緒隊列也包含了用於實時進程的子就緒隊列,是一個嵌入的struct rt_rq實例)

圖8是時調度類就緒隊列示意圖,一個鏈表中,表頭爲active.queue[prio],而active.bitmap位圖中的每個比特位對應於一個鏈表,凡包含了進程的鏈表,對應的比特位則置位。如果鏈表中沒有進程,則對應的比特位不置位。

在這裏插入圖片描述
sched_find_first_bit是一個標準函數,可以找到active.bitmap中第一個置位的比特位(高優先級)。取出所選鏈表的第一個進程,並將se.exec_start設置爲就緒隊列的當前實際時鐘值。

對於週期調度:SCHED_FIFO進程可以運行任意長的時間,而且必須使用yield系統調用將控制權顯式傳遞給另一個進程。對循環進程(SCHED_RR),則減少其時間片。在尚未超出時間段時,進程可以繼續執行。計數器歸0後,其值重置爲DEF_TIMESLICE,即100 * HZ / 1000( 100毫秒)。如果該進程不是鏈表中唯一的進程,則重新排隊到末尾。通過用set_tsk_need_resched設置TIF_NEED_RESCHED標誌,照常請求重調度。

爲將進程轉換爲實時進程,必須使用sched_setscheduler系統調用,該系統調用完成了以下幾個任務:

  • 使用deactivate_task將進程從當前隊列移除。
  • 在task_struct中設置實時優先級和調度類。
  • 重新激活進程。

只有具有root權限(或等價於CAP_SYS_NICE)的進程執行了sched_setscheduler系統調用,才能修改調度器類或優先級。否則,調度類只能從SCHED_NORMAL改爲SCHED_BATCH。只有目標進程的UID或EUID與調用者進程的EUID相同時,才能修改目標進程的優先級。此外,優先級只能降低,不能提升。

四、調度器增強

1、SMP調度
對於多處理器系統,CPU負荷必須儘可能公平地在所有的處理器上共享;進程與系統中某些處理器的親合性(affinity)必須是可設置的;內核必須能夠將進程從一個CPU遷移到另一個。

進程對特定CPU 的親合性, 定義在task_struct 的cpus_allowed成員中,可以通過sched_setaffinity系統調用修改進程與CPU的現有分配關係。

(1)數據結構的擴展

每當內核認爲有必要重新均衡時,核心調度器就會調用load_balance和move_one_task函數。特定於調度器類的函數建立一個迭代器,使核心調度器能遍歷所有可能遷移到另一個隊列的備選進程。load_balance函數指針採用了一般性的函數load_balance,允許從最忙的就緒隊列分配多個進程到當前CPU,但移動的負荷不能比max_load_move更多;move_one_task使用了iter_move_one_task,從最忙碌的就緒隊列移出一個進程,遷移到當前CPU的就緒隊列。

負載均衡發起過程:在SMP系統上,週期性調度器函數scheduler_tick按上文所述完成所有系統都需要的任務之後,會調用trigger_load_balance函數。這會引發SCHEDULE_SOFTIRQ軟中斷softIRQ(確保會在適當的時機執行run_rebalance_domains)。該函數最終對當前CPU調用rebalance_domains,實現負載均衡。

就緒隊列是特定於CPU的,內核爲每個就緒隊列提供了一個遷移線程,可以接收遷移請求,這些請求保存在鏈表migration_queue中,這樣的請求通常發源於調度器自身,但如果進程被限制在某一特定的CPU集合上,而不能在當前執行的CPU上繼續運行時,也可能出現這樣的請求。內核試圖週期性地均衡就緒隊列,但如果對某個就緒隊列效果不佳,則必須使用主動均衡(active balancing)。

所有的就緒隊列組織爲調度域(scheduling domain)。這可以將物理上鄰近或共享高速緩存的CPU羣集起來,應優先選擇在這些CPU之間遷移進程。

對於load_balance函數,它會檢測在上一次重新均衡操作之後是否已經過去了足夠多的時間,在必要的情況下,它會發起一輪新的均衡操作。首先該函數通過find_busiest_queue標識出哪個隊列工作量最大,如果至少有一個進程在該隊列上執行,則使用move_tasks將該隊列中適當數目的進程遷移到當前隊列。move_tasks函數接下來會調用特定於調度器類的load_balance方法。如果均衡操作失敗,那麼將喚醒負責最忙的就緒隊列的遷移線程。

(2)遷移線程

遷移線程是一個執行migration_thread的內核線程(如圖10所示),用於兩個目的:

  • 完成發自調度器的遷移請求;
  • 實現主動均衡。
    在這裏插入圖片描述
    migration_thread內部是一個無限循環,在無事可做時進入睡眠狀態。

首先,該函數檢測是否需要主動均衡。如果需要,則調用active_load_balance滿足該請求。該函數試圖從當前就緒隊列移出一個進程,且移至發起主動均衡請求CPU的就緒隊列。它使用move_one_task完成該工作,後者又對所有的調度器類,分別調用特定於調度器類的move_one_task函數,直至其中一個成功。

完成主動負載均衡之後,遷移線程會檢測migrate_req鏈表中是否有來自調度器的待決遷移請求。如果沒有,則線程發出重調度請求。否則,用__migrate_task完成相關請求,該函數會直接移出所要求的進程,而不再與調度器類進一步交互。

(3)核心調度器的改變

SMP系統與單處理器系統相比的主要差別:

  • 在用exec系統調用啓動一個新進程時,由於進程尚未執行,這時是調度器跨越CPU移動該進程的一個良好的時機。
  • 完全公平調度器的調度粒度與CPU的數目是成比例的。系統中處理器越多,可以採用的調度粒度就越大。

2、調度域和控制組
對於組調度,進程置於不同的組中,調度器首先在這些組之間保證公平,然後在組中的所有進程之間保證公平(比如系統可以向每個用戶授予相同的CPU時間份額)。

把進程按用戶分組不是唯一可能的做法。內核還提供了控制組(control group),該特性使得通過特殊文件系統cgroups可以創建任意的進程集合,甚至可以分爲多個層次。

3、內核搶佔和低延遲相關工作
(1)內核搶佔

在系統調用時返回用戶狀態之前,或者是內核中某些指定的點上,都會調用調度器,這確保除了一些明確指定的情況之外,內核是無法中斷的,這不同於用戶進程,如果內核處於相對耗時較長的操作中,這種行爲可能會帶來問題。啓用了搶佔特性的內核能夠比普通內核更快速地用緊急進程替代當前進程。

在編譯內核時啓用對內核搶佔的支持。如果高優先級進程有事情需要完成,那麼在啓用內核搶佔(與用戶空間程序被其他進程搶佔不同)的情況下,不僅用戶空間應用程序可以被中斷,內核也可以被中斷。

爲了避免競態條件使系統不一致,內核不能在任意點上被中斷,大多數不能中斷的點已被SMP實現標識,並且實現內核搶佔時可以重用這些信息。內核的某些易於出現問題(臨界區)的部分每次只能由一個處理器訪問,這些部分使用自旋鎖保護。每次內核進入臨界區時,我們必須停用內核搶佔。

系統中的每個進程都有一個特定於體系結構的struct thread_info實例,該結構包含了一個搶佔計數器。

struct thread_info {
   
   
 ...
     int preempt_count; /* 0 => 可搶佔, <0 => BUG */
 ...
 }

preempt_count的值(該值通過輔助函數dec_preempt_count和inc_preempt_count分別進行減1和加1操作)確定了內核當前是否處於一個可被中斷的位置,在內核再次啓用搶佔之前,必須確認已經離開所有的臨界區。

搶佔機制中主要的函數是preempt_schedule。設置了TIF_NEED_RESCHED標誌,它不能保證一定可以搶佔內核(內核有可能正處於臨界區中),可以通過preempt_reschedule檢查是否可搶佔。

激活搶佔的兩種方法(本質區別在於,preempt_schedule_irq調用時停用了中斷,防止中斷造成遞歸調用):

  • 使用preempt_schedule,如果調度是由搶佔機制發起的(查看搶佔計數器中是否設置了PREEMPT_ACTIVE),無需停止當前進程的活動(跳過使用deactivate_task停止不處於可運行狀態進程的活動),儘可能快速選擇下一個進程。
  • 是通過preempt_schedule_irq,處理中斷請求後返回和心態,會檢查搶佔技術企的值和是否設置了重調度標誌,若都滿足,則調用調度器。

(2)低延遲

內核中耗時長的操作(比如繁重的IO操作)不應該完全佔據整個系統。相反,它們應該不時地檢測是否有另一個進程變爲可運行,並在必要的情況下調用調度器選擇相應的進程運行。該機制不依賴於內核搶佔,即使內核聯編時未指定支持搶佔,也能夠降低延遲。發起有條件重調度的函數是cond_resched,內核代碼中,長時間運行的函數都在適當之處插入了對cond_resched的調用,保證較高的相應速度。
在這裏插入圖片描述

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