調度器的任務是在程序之間共享CPU時間,創造並行執行的錯覺。該任務可分爲調度策略和上下文切換兩個不一樣部分。html
暫時不考慮實時進程,只考慮CFS調度器。經典的調度器對系統中的進程分別計算時間片,使進程運行直至時間片用盡,全部進程的全部時間片用完時,須要從新計算。相比之下,CFS只考慮進程等待時間,即進程在就緒隊列(run_queue)中已等待的時間,對CPU時間需求最嚴格的進程被調度執行。每次調度器會挑選具備最高等待時間的進程提供CPU,如此進程的不公平等待不會被積累,而會均勻分佈到系統全部進程。node
圖1是CFS調度器的工做原理,全部可運行進程都按等待時間在一個紅黑樹中排序,等待CPU時間最長的進程是最左側的項,調度器下一次會考慮該進程。等待時間稍短的進程在該樹上從左至右排序。(調度器時間複雜度爲O(logn))算法
圖1 CFS調度器工做原理示意圖數組
除了紅黑樹外,就緒隊列還裝備了虛擬時鐘。該時鐘的時間流逝速度慢於實際的時鐘,精確的速度依賴於當前等待調度器挑選的進程數目(如4個進程,那麼虛擬時鐘將以實際時鐘的四分之一的速度運行,若是以徹底公平的方式分享計算能力,那麼該時鐘是判斷等待進程將得到多少CPU時間的基準)。緩存
就緒隊列的虛擬時間由fair_clock給出,進程的等待時間保存在wait_runtime中,爲排序紅黑樹上的進程,內核使用差值fair_clock - wait_runtime的絕對值。fair_clock是徹底公平調度的狀況下進程將會獲得的CPU時間的度量,而wait_runtime直接度量了實際系統的不足形成的不公平。數據結構
在進程容許運行時,將從wait_runtime減去它已經運行的時間。這樣,在按時間排序的樹中它會向右移動到某一點,另外一個進程將成爲最左邊,下一次會被調度器選擇。負載均衡
當前該調度策略還受到的影響因素:dom
調度器使用一系列數據結構排序和管理系統中的進程,調度器的工做方式與這些結構的設計密切相關。幾個組件在許多方面彼此交互,如圖2所示。模塊化
圖2 調度子系統各組件概觀函數
激活調度器方法:
調度類:用於判斷接下來運行哪一個進程。內核支持不一樣的調度策略(徹底公平調度、實時調度、在無事可作時調度空閒進程),調度類使得可以以模塊化方法實現這些策略,即一個類的代碼不須要與其餘類的代碼交互。
進程切換:在選中將執行的程序以後,要執行底層的任務切換。(須要與CPU緊密交互)
注:每一個進程都恰好屬於某一調度類,各個調度類負責管理所屬的進程。通用調度器自身徹底不涉及進程管理,其工做都委託給調度器類。
(1)task_struct成員
各進程的task_struct有幾個成員與調度相關:
1 struct task_struct { 2 ... 3 int prio, static_prio, normal_prio; // prio和normal_prio表示動態優先級,static_prio表示進程的靜態優先級。靜態優先級是進程啓動時分配的優先級。它能夠用nice和sched_setscheduler系統調用修改,不然在進程運行期間會一直保持恆定。normal_priority表示基於進程的靜態優先級和調度策略計算出的優先級。調度器考慮的優先級則保存在prio。因爲在某些狀況下內核須要暫時提升進程的優先級,所以須要第3個成員來表示。 4 unsigned int rt_priority; //表示實時進程的優先級。該值不會代替先前討論的那些值。最低的實時優先級爲0,最高的是99。值越大,優先級越高。這裏使用的慣例不一樣於nice值。 5 struct list_head run_list; //是循環實時調度器所須要的,但不用於徹底公平調度器,run_list是一個表頭,用於維護包含各進程的一個運行表 6 const struct sched_class *sched_class; //表示該進程所屬的調度器類 7 struct sched_entity se; //因爲調度器設計爲處理可調度的實體,在調度器看來各個進程必須也像是這樣的實體。所以se在task_struct中內嵌了一個sched_entity實例,調度器可據此操做各個task struct。 8 unsigned int policy; //保存了對該進程應用的調度策略。(SCHED_NORMAL用於普通進程,SCHED_BATCH用於非交互、CPU使用密集的批處理進程,SCHED_IDLE是空閒進程,SCHED_RR和SCHED_FIFO用於實現軟實時進程,) 9 cpumask_t cpus_allowed; //是一個位域,在多處理器系統上用來限制進程能夠在哪些CPU上運行 10 unsigned int time_slice; // 是循環實時調度器所須要的,但不用於徹底公平調度器,time_slice則指定進程可以使用CPU的剩餘時間段 11 ... 12 }
(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都有自身的就緒隊列,各個活動進程只出如今一個就緒隊列中。進程不是由就緒隊列的成員直接管理的,而是有調度類管理,就緒隊列中嵌入了特定於調度器類的子就緒隊列。
就緒隊列核心成員及解釋:
1 struct rq { 2 unsigned long nr_running; //指定了隊列上可運行進程的數目,不考慮其優先級或調度類 3 #define CPU_LOAD_IDX_MAX 5 4 unsigned long cpu_load[CPU_LOAD_IDX_MAX]; //用於跟蹤此前的負荷狀態 5 ... 6 struct load_weight load; //提供了就緒隊列當前負荷的度量 7 struct cfs_rq cfs; //嵌入的子就緒隊列,用於徹底公平調度器 8 struct rt_rq rt; //嵌入的子就緒隊列,用於和實時調度器 9 struct task_struct *curr, *idle; //指向idle進程的task_struct實例,該進程亦稱爲idle線程 10 u64 clock; //用於實現就緒隊列自身的時鐘。每次調用週期性調度器時,都會更新clock的值。 11 ... 12 };
系統的全部就緒隊列都在runqueues數組中,該數組的每一個元素分別對應於系統中的一個CPU。在單處理器系統中,因爲只須要一個就緒隊列,數組只有一個元素。
(4)調度實體
因爲調度器能夠操做比進程更通常的實體,所以須要一個適當的數據結構來描述此類實體。其定義爲:
1 struct sched_entity { 2 struct load_weight load; //指定了權重,用於負載均衡 3 struct rb_node run_node; //是標準的樹結點,使得實體能夠在紅黑樹上排序 4 unsigned int on_rq; //表示該實體當前是否在就緒隊列上接受調度 5 u64 exec_start; //新進程加入就緒隊列時,或者週期性調度器中。每次調用時,會計算當前時間和exec_start之間的差值,exec_start則更新到當前時間。差值則被加到sum_exec_runtime。 6 u64 sum_exec_runtime; //用於記錄消耗的CPU時間 7 u64 vruntime; //統計進程執行期間虛擬時鐘上流逝的時間數量 8 u64 prev_sum_exec_runtime; //進程被撤銷CPU時,保存當前sum_exec_runtime 9 ... 10 }
(1)優先級的內核表示
在用戶空間能夠經過nice命令設置進程的靜態優先級,這在內部會調用nice系統調用。進程的nice值在-20和+19之間(包含)。值越低,代表優先級越高。
內核使用一個簡單些的數值範圍,從0到139(包含),用來表示內部優先級。一樣是值越低,優先級越高。從0到99的範圍專供實時進程使用,nice值[-20, +19]映射到範圍100到139。
(2)計算優先級
進程的優先級計算須要考慮動態優先級(prio),普通優先級(normal_prio)和靜態優先級(static_prio),調用相關函數計算結果。(完成設置到優先級內核表示的轉換)
圖3綜述了針對不一樣類型進程的計算結果。
圖3 對各類類型進程計算優先級
在進程分支出子進程時,子進程的靜態優先級繼承自父進程。子進程的動態優先級,即task_struct->prio,則設置爲父進程的普通優先級。這確保了實時互斥量引發的優先級提升不會傳遞到子進程。
(3)計算負荷權重
進程的重要性由優先級和保存在task_struct->se.load的負荷權重同時決定。進程每下降一個nice值,則多得到10%的CPU時間,每升高一個nice值,則放棄10%的CPU時間。詳戳
調度器的實現基於兩個函數:週期性調度器函數和主調度器函數。
(1)週期性調度器
1 void scheduler_tick(void) 2 { 3 int cpu = smp_processor_id(); 4 struct rq *rq = cpu_rq(cpu); 5 struct task_struct *curr = rq->curr; 6 ... 7 __update_rq_clock(rq); //更新struct rq當前實例的時鐘時間戳 8 update_cpu_load(rq); //更新rq->cup_load[]數組 9 if (curr != rq->idle) 10 curr->sched_class->task_tick(rq, curr); 11 }
若是當前進程應該被從新調度,那麼調度器類方法會在task_struct中設置TIF_NEED_RESCHED標誌,以表示該請求,而內核會在接下來的適當時機完成該請求。
(2)主調度器
將當前cpu分配給另外一個進程須要調用主調度器函數(schedule),從該系統調用返回後也要檢查當前進程是否設置了重調度標誌TIF_NEED_RESCHEDULE,若是有,內核會調用schedule。
__sched前綴的用處:有該前綴的函數,都是可能調用schedule的函數,包括schedule自身。該前綴目的在於將相關代碼的函數編譯後,放到目標文件的特定段中,.sched.text中。該信息使內核在現實棧轉儲或相似信息時,忽略全部與調度有關的調用。因爲調度器函數調用不是普通代碼流程的部分,所以這種狀況下是無心義的。
asmlinkage void __sched schedule( void );該函數的過程:
(3)與fork的交互
用fork或其變體新建進程時,調度器有機會用sched_fork函數掛鉤到該進程。
單處理器中,sched_fork執行以下:
經過使用父進程的普通優先級做爲子進程的動態優先級,內核確保父進程優先級的臨時提升不會被子進程繼承。在用wake_up_new_task喚醒進程時,內核調用調度類的task_new將新進程加入相應類的就緒隊列中。
(4)上下文切換
內核選擇新進程以後,必須處理與多任務相關的技術細節。這些細節總稱爲上下文切換(context switching)。
內核線程沒有自身的用戶空間內存上下文,可能在某個隨機進程地址空間的上部執行。其task_struct->mm爲NULL。從當前進程「借來」的地址空間記錄在active_mm中。
此外,因爲上下文切換的速度對系統性能的影響舉足輕重,因此內核使用了一種技巧來減小所需的CPU時間。浮點寄存器(及其餘內核未使用的擴充寄存器,例如IA-32平臺上的SSE2寄存器)除非有應用程序實際使用,不然不會保存。此外,除非有應用程序須要,不然這些寄存器也不會恢復。這稱之爲惰性FPU技術。因爲使用了彙編語言代碼,所以其實現依平臺而有所不一樣,但基本原理老是一樣的。
CFS的就緒隊列cfs_rq:
1 struct cfs_rq { 2 struct load_weight load; //維護了全部這些進程的累積負荷值 3 unsigned long nr_running; //計算了隊列上可運行進程的數目 4 u64 min_vruntime; //跟蹤記錄隊列上全部進程的最小虛擬運行時間 5 struct rb_root tasks_timeline; //用於在按時間排序的紅黑樹中管理全部進程 6 struct rb_node *rb_leftmost; //老是設置爲指向樹最左邊的結點,即最須要被調度的進程 7 struct sched_entity *curr; //指向當前執行進程的可調度實體 8 }
(1)虛擬時鐘
徹底公平調度算法依賴於虛擬時鐘,用以度量等待進程在徹底公平系統中所能獲得的CPU時間。數據結構中,能夠根據現存的實際時鐘與每一個進程相關的負荷權重推算出來。全部與虛擬時鐘有關的計算都在update_curr中執行,該函數在系統中各個不一樣地方調用,包括週期性調度器以內。
圖4 update_curr代碼流程圖
首先,該函數肯定就緒隊列的當前執行進程,並獲取主調度器就緒隊列的實際時鐘值(每一個調度週期都會更新),若是就緒隊列上當前沒有進程正在執行,則無事可作。不然,內核會計算當前和上一次更新負荷統計量時兩次的時間差,並將其他的工做委託給__update_curr。
而後,__update_curr須要更新當前進程在CPU上執行花費的物理時間和虛擬時間。物理時間的更新只要將時間差加到先前統計的時間便可;對於虛擬時間,對於運行在nice級別0的進程來講,定義虛擬時間和物理時間是相等的,nice值不爲0時,必須根據進程的負荷權重從新衡定時間。
最後,內核須要設置min_vruntime(min_vruntime是單調遞增的)。
CFS調度器真正關鍵點:紅黑樹根據鍵值進行排序(se->vruntime -cfs_rq->min_vruntime),鍵值較小的結點,排序位置就更靠左(被更快調度)。由此,內核實現瞭如下兩種對立機制:
(2)延遲跟蹤
良好的調度延遲是 保證每一個可運行的進程都應該至少運行一次的某個時間間隔(它在sysctl_sched_latency給出)。一個延遲週期中處理的最大活動數目爲sched_nr_latency,超出該上限,則延遲週期也成比例線性擴展。
經過考慮各個進程的相對權重,將一個延遲週期的時間在活動進程之間進行分配。
enqueue_task_fair和dequeue_task_fair分別用來增刪就緒隊列的成員。圖5爲enqueue_task_fair代碼流程圖。
圖5 enqueue_task_fair代碼流程圖
若是經過struct sched_entity的on_rq成員判斷進程已經在就緒隊列上,則無事可作。不然,具體的工做委託給enqueue_entity。
進入enqueue_entity後,首先用updater_curr更新統計量,而後若進程此前在睡眠,那麼在place_entity中首先會調整進程的虛擬運行時間;若是進程最近在運行,其虛擬運行時間仍然有效,那麼(除非它當前在執行中)它能夠直接用__enqueue_entity加入紅黑樹中。
選擇下一個將要運行的進程由pick_next_task_fair執行。pick_next_task_fair的代碼流程圖如圖6所示。
圖6 pick_next_task_fair的代碼流程圖
若是nr_running計數器爲0,即當前隊列上沒有可運行進程,則無事可作,函數能夠當即返回。不然將具體工做委託給pick_next_entity。
若是樹中最左邊的進程可用,可使用輔助函數first_fair當即肯定,而後用__pick_next_entity從紅黑樹中提取出sched_entity實例。
完成了選擇工做以後,經過set_next_entity函數將該進程標記爲運行進程。當前執行進程不保存在就緒隊列上,所以使用__dequeue_entity將其從樹中移除。若是當前進程是最左邊的結點,則將leftmost指針設置到下一個最左邊的進程。
在處理週期調度時,差值sum_exec_runtime - prev_sum_exec_runtime(表示進程在CPU上執行所花時間)很重要。這個差值形式上由函數task_tick_fair負責,但實際工做由entity_tick完成。
圖7 entity_tick代碼流程圖
首先,使用update_curr更新統計量。
而後判斷nr_running計數器代表隊列上可運行的進程數,若是少於兩個,則無事可作;不然由由check_preempt_tick做出決策(確保沒有哪一個進程可以比延遲週期中肯定的份額運行得更長),若是進程運行時間比指望的時間間隔長,那麼經過resched_task發出重調度請求。這會在task_struct中設置TIF_NEED_RESCHED標誌,核心調度器會在下一個適當時機發起重調度。
當在try_to_wake_up和wake_up_new_task中喚醒進程時,內核使用check_preempt_curr看看是否新進程能夠搶佔當前運行的進程(該過程不涉及核心調度器)。
新喚醒的進程沒必要必定由徹底公平調度器處理。若是新進程是一個實時進程,則會當即請求重調度,由於實時進程老是會搶佔CFS進程。
當運行進程被新進程搶佔時,內核確保被搶佔者至少已經運行了某一最小時間限額(sysctl_sched_wakeup_granularity)。若是新進程的虛擬運行時間,加上最小時間限額,仍然小於當前執行進程的虛擬運行時間(由其調度實體se表示),則請求重調度。
CFS在建立新進程時調用的掛鉤函數:task_new_fair。該函數的行爲可用sysctl_sched_child_runs_first控制,用於判斷新建子進程是否須要在父進程以前運行。若是父進程的虛擬運行時間(由curr表示)小於子進程的虛擬運行時間,則意味着父進程將在子進程以前調度運行,若是子進程應該在父進程以前運行,則兩者的虛擬運算時間須要換過來。而後子進程按常規加入就緒隊列,並請求重調度。
按照POSIX標準的要求,除了「普通」進程以外,Linux還支持兩種實時調度類。調度器結構使得實時進程能夠平滑地集成到內核中,而無需修改核心調度器。
實時進程與普通進程有一個根本的不一樣之處:若是系統中有一個實時進程且可運行,那麼調度器老是會選中它運行,除非有另外一個優先級更高的實時進程。
現有的兩種實時類:
實時進程的調度類定義:
1 const struct sched_class rt_sched_class = { 2 .next = &fair_sched_class, 3 .enqueue_task = enqueue_task_rt, 4 .dequeue_task = dequeue_task_rt, 5 .yield_task = yield_task_rt, 6 .check_preempt_curr = check_preempt_curr_rt, 7 .pick_next_task = pick_next_task_rt, 8 .put_prev_task = put_prev_task_rt, 9 .set_curr_task = set_curr_task_rt, 10 .task_tick = task_tick_rt, 11 };
實時調度器類的實現比徹底公平調度器簡單(核心調度器的就緒隊列也包含了用於實時進程的子就緒隊列,是一個嵌入的struct rt_rq實例)
圖8是時調度類就緒隊列示意圖,一個鏈表中,表頭爲active.queue[prio],而active.bitmap位圖中的每一個比特位對應於一個鏈表,凡包含了進程的鏈表,對應的比特位則置位。若是鏈表中沒有進程,則對應的比特位不置位。
圖8 實時調度器就緒隊列
實時調度器類中對應於update_cur的是update_curr_rt,該函數將當前進程在CPU上執行花費的時間記錄在sum_exec_runtime中(全部計算的單位都是實際時間,不須要虛擬時間)。
進程的入隊和離隊以p->prio爲索引訪問queue數組queue[p->prio],訪問鏈表,將進程加入鏈表或從鏈表刪除(程老是排列在每一個鏈表的末尾)。
對於選擇下一個要執行的進程,經過函數pick_next_task_rt,該函數書意圖如圖9所示。
圖9 ick_next_task_rt的代碼流程圖
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系統調用,該系統調用完成了如下幾個任務:
只有具備root權限(或等價於CAP_SYS_NICE)的進程執行了sched_setscheduler系統調用,才能修改調度器類或優先級。不然,調度類只能從SCHED_NORMAL改成SCHED_BATCH。只有目標進程的UID或EUID與調用者進程的EUID相同時,才能修改目標進程的優先級。此外,優先級只能下降,不能提高。
對於多處理器系統,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所示),用於兩個目的:
圖10 migration_thread代碼流程圖
migration_thread內部是一個無限循環,在無事可作時進入睡眠狀態。
首先,該函數檢測是否須要主動均衡。若是須要,則調用active_load_balance知足該請求。該函數試圖從當前就緒隊列移出一個進程,且移至發起主動均衡請求CPU的就緒隊列。它使用move_one_task完成該工做,後者又對全部的調度器類,分別調用特定於調度器類的move_one_task函數,直至其中一個成功。
完成主動負載均衡以後,遷移線程會檢測migrate_req鏈表中是否有來自調度器的待決遷移請求。若是沒有,則線程發出重調度請求。不然,用__migrate_task完成相關請求,該函數會直接移出所要求的進程,而再也不與調度器類進一步交互。
(3)核心調度器的改變
SMP系統與單處理器系統相比的主要差異:
對於組調度,進程置於不一樣的組中,調度器首先在這些組之間保證公平,而後在組中的全部進程之間保證公平(好比系統能夠向每一個用戶授予相同的CPU時間份額)。
把進程按用戶分組不是惟一可能的作法。內核還提供了控制組(control group),該特性使得經過特殊文件系統cgroups能夠建立任意的進程集合,甚至能夠分爲多個層次。
(1)內核搶佔
在系統調用時返回用戶狀態以前,或者是內核中某些指定的點上,都會調用調度器,這確保除了一些明確指定的狀況以外,內核是沒法中斷的,這不一樣於用戶進程,若是內核處於相對耗時較長的操做中,這種行爲可能會帶來問題。啓用了搶佔特性的內核可以比普通內核更快速地用緊急進程替代當前進程。
在編譯內核時啓用對內核搶佔的支持。若是高優先級進程有事情須要完成,那麼在啓用內核搶佔(與用戶空間程序被其餘進程搶佔不一樣)的狀況下,不只用戶空間應用程序能夠被中斷,內核也能夠被中斷。
爲了不競態條件使系統不一致,內核不能在任意點上被中斷,大多數不能中斷的點已被SMP實現標識,而且實現內核搶佔時能夠重用這些信息。內核的某些易於出現問題(臨界區)的部分每次只能由一個處理器訪問,這些部分使用自旋鎖保護。每次內核進入臨界區時,咱們必須停用內核搶佔。
系統中的每一個進程都有一個特定於體系結構的struct thread_info實例,該結構包含了一個搶佔計數器。
1 struct thread_info { 2 ... 3 int preempt_count; /* 0 => 可搶佔, <0 => BUG */ 4 ... 5 }
preempt_count的值(該值經過輔助函數dec_preempt_count和inc_preempt_count分別進行減1和加1操做)肯定了內核當前是否處於一個可被中斷的位置,在內核再次啓用搶佔以前,必須確認已經離開全部的臨界區。
搶佔機制中主要的函數是preempt_schedule。設置了TIF_NEED_RESCHED標誌,它不能保證必定能夠搶佔內核(內核有可能正處於臨界區中),能夠經過preempt_reschedule檢查是否可搶佔。
激活搶佔的兩種方法(本質區別在於,preempt_schedule_irq調用時停用了中斷,防止中斷形成遞歸調用):
(2)低延遲
內核中耗時長的操做(好比繁重的IO操做)不該該徹底佔據整個系統。相反,它們應該不時地檢測是否有另外一個進程變爲可運行,並在必要的狀況下調用調度器選擇相應的進程運行。該機制不依賴於內核搶佔,即便內核聯編時未指定支持搶佔,也可以下降延遲。發起有條件重調度的函數是cond_resched,內核代碼中,長時間運行的函數都在適當之處插入了對cond_resched的調用,保證較高的相應速度。