本篇文章給大家帶來了關于linux中內核源碼分析進程調度邏輯的相關知識,希望對大家有幫助。
0.1?本文目錄結構
1?操作系統理論層面
2?調度主函數
3?__schedule()?方法核心邏輯概覽
4?pick_next_task():從就緒隊列中選中一個進程
-
4.1?負載均衡邏輯
-
4.2?選中高優進程
-
4.3?pick_next_task()?小結
5?context_switch():執行上下文切換
-
5.1?切換虛擬內存
-
5.2?切換通用寄存器
-
5.3?context_switch()?小結
6?本文總結
0.2?pick_next_task():從就緒隊列中選中一個進程
內核在選擇進程進行調度的時候,會首先判斷當前?CPU?上是否有進程可以調度,如果沒有,執行進程遷移邏輯,從其他?CPU?遷移進程,如果有,則選擇虛擬時間較小的進程進行調度。
內核在選擇邏輯?CPU?進行遷移進程的時候,為了提升被遷移進程的性能,即避免遷移之后?L1?L2?L3?高速緩存失效,盡可能遷移那些和當前邏輯?CPU?共享高速緩存的目標邏輯?CPU,離當前邏輯?CPU?越近越好。
內核將進程抽象為調度實體,為的是可以將一批進程進行統一調度,在每一個調度層次上,都保證公平。
所謂選中高優進程,實際上選中的是虛擬時間較小的進程,進程的虛擬時間是根據進程的實際優先級和進程的運行時間等信息動態計算出來的。
0.3?5?context_switch():執行上下文切換
進程上下文切換,核心要切換的是虛擬內存及一些通用寄存器。
進程切換虛擬內存,需要切換對應的?TLB?中的?ASID?及頁表,頁表也即不同進程的虛擬內存翻譯需要的?“map”。
進程的數據結構中,有一個間接字段?cpu_context?保存了通用寄存器的值,寄存器切換的本質就是將上一個進程的寄存器保存到?cpu_context?字段,然后再將下一個進程的?cpu_context?數據結構中的字段加載到寄存器中,至此完成進程的切換。
1?操作系統理論層面
學過操作系統的同學應該都知道,進程調度分為如下兩個步驟:
根據某種算法從就緒隊列中選中一個進程。
執行進程上下文切換。
其中第二個步驟又可以分為:
切換虛擬內存。
切換寄存器,即保存上一個進程的寄存器到進程的數據結構中,加載下一個進程的數據結構到寄存器中。
關于虛擬內存相關的邏輯,后續文章會詳細剖析,這篇文章會簡要概括。
這篇文章,我們從內核源碼的角度來剖析?linux?是如何來實現進程調度的核心邏輯,基本上遵從操作系統理論。
2?調度主函數
Linux?進程調度的主函數是?schedule()?和?__schedule(),從源碼中可以看出兩者的關系:
//?kernel/sched/core.c:3522 void?schedule(void)?{ ????... ????//?調度過程中禁止搶占 ????preempt_disable();? ????__schedule(false);?//:3529 ????//?調度完了,可以搶占了 ????sched_preempt_enable_no_resched(); ????... } //?kernel/sched/core.c:3395 __schedule(bool?preempt)?{? ????...? }
當一個進程主動讓出?CPU,比如?yield?系統調用,會執行?schedule()?方法,在執行進程調度的過程中,禁止其他進程搶占當前進程,說白了就是讓當前進程好好完成這一次進程切換,不要剝奪它的?CPU;
3529?行代碼表示?schedule()?調用了?__schedule(false)?方法,傳遞?fasle?參數,表示這是進程的一次主動調度,不可搶占。
等當前的進程執行完調度邏輯之后,開啟搶占,也就是說,其他進程可以剝奪當前進程的?CPU?了。
而如果某個進程像個強盜一樣一直占著?CPU?不讓,內核會通過搶占機制(比如上一篇文章提到的周期調度機制)進行一次進程調度,從而把當前進程從?CPU?上踢出去。
__schedule()?方法的框架便是這篇文章分析的主要內容,由于代碼較多,我會挑選核心部分來描述。
3?__schedule()?方法核心邏輯概覽
我們先來看看?Linux?內核中,進程調度核心函數?__schedule()?的框架:
//?kernel/sched/core.c:3395 void?__schedule(bool?preempt)?{ ????struct?task_struct?*prev,?*next; ????unsigned?long?*switch_count; ????struct?rq?*rq; ????int?cpu; ????//?獲取當前?CPU ????cpu?=?smp_processor_id(); ????//?獲取當前?CPU?上的進程隊列 ????rq?=?cpu_rq(cpu); ????//?獲取當前隊列上正在運行的進程 ????prev?=?rq->curr; ????... ????//?nivcsw:進程非主動切換次數 ????switch_count?=?&prev->nivcsw;?//?:3430 ????if?(!preempt?...)?{ ????????... ????????//?nvcsw:進程主動切換次數? ????????switch_count?=?&prev->nvcsw;?//?:3456 ????} ????... ????//?1?根據某種算法從就緒隊列中選中一個進程 ????next?=?pick_next_task(rq,?prev,?&rf); ????... ????if?(prev?!=?next)?{ ????????rq->nr_switches++; ????????rq->curr?=?next; ????????++*switch_count; ????????//?2?執行進程上下文切換 ????????rq?=?context_switch(rq,?prev,?next,?&rf); ????}? ????... }
可以看到,__schedule()?方法中,進程切換的核心步驟和操作系統理論中是一致的(1?和?2?兩個核心步驟)。
此外,進程切換的過程中,內核會根據是主動發起調度(preempt?為?fasle)還是被動發起調度,來統計進程上下文切換的次數,分別保存在進程的數據結構?task_struct?中:
//?include/linux/sched.h:592 struct?task_struct?{ ????... ????//?主動切換:Number?of?Voluntary(自愿)?Context?Switches ????unsigned?long?nvcsw;?//?:811 ????//?非主動切換:Number?of?InVoluntary(非自愿)?Context?Switches ????unsigned?long?nivcsw;?//?:812 ????... };
在?Linux?中,我們可以通過?pidstat?命令來查看一個進程的主動和被動上下文切換的次數,我們寫一個簡單的?c?程序來做個測試:
//?test.c #include?<stdlib.h> #include?<stdio.h> int?main()?{ ????while?(1)?{ ????????//?每隔一秒主動休眠一次 ????????//?即主動讓出?CPU ????????//?理論上每秒都會主動切換一次 ????????sleep(1) ????} }</stdio.h></stdlib.h>
然后編譯運行
gcc?test.c?-o?test ./test
通過?pidstat?來查看
可以看到,test?應用程序每秒主動切換一次進程上下文,和我們的預期相符,對應的上下文切換數據就是從?task_struct?中獲取的。
接下來,詳細分析進程調度的兩個核心步驟:
通過?pick_next_task()?從就緒隊列中選中一個進程。
通過?context_switch?執行上下文切換。
4?pick_next_task():從就緒隊列中選中一個進程
我們回顧一下?pick_next_task()?在?__schedule()?方法中的位置
//?kernel/sched/core.c:3395 void?__schedule(bool?preempt)?{ ????... ????//?rq?是當前?cpu?上的進程隊列 ????next?=?pick_next_task(rq,?pre?...);?//?:3459 ????... }
跟著調用鏈往下探索:
//?kernel/sched/core.c:3316 /*? ?*?找出優先級最高的進程 ?*?Pick?up?the?highest-prio?task: ?*/ struct?task_struct?*pick_next_task(rq,?pre?...)?{ ????struct?task_struct?*p; ????... ????p?=?fair_sched_class.pick_next_task(rq,?prev?...);?//?:3331 ????... ????if?(!p) ????????p?=?idle_sched_class.pick_next_task(rq,?prev?...);?//?:3337 ????return?p; }
從?pick_next_task()?方法的注釋上也能看到,這個方法的目的就是找出優先級最高的進程,由于系統中大多數進程的調度類型都是公平調度,我們拿公平調度相關的邏輯來分析。
從上述的核心框架中可以看到,3331?行先嘗試從公平調度類型的隊列中獲取進程,3337?行,如果沒有找到,就把每個?CPU?上的?IDLE?進程取出來運行:
//?kernel/sched/idle.c:442 const?struct?sched_class?idle_sched_class?=?{ ????...???? ????.pick_next_task????=?pick_next_task_idle,?//?:451 ????... }; //?kernel/sched/idle.c:385 struct?task_struct?*?pick_next_task_idle(struct?rq?*rq?...)?{ ????... ????//?每一個?CPU?運行隊列中都有一個?IDLE?進程? ????return?rq->idle;?//?:383 }
接下來,我們聚焦公平調度類的進程選中算法?fair_sched_class.pick_next_task()
//?kernel/sched/fair.c:10506 const?struct?sched_class?fair_sched_class?=?{ ???... ???.pick_next_task?=?pick_next_task_fair,?//?:?10515? ???... } //?kernel/sched/fair.c:6898 static?struct?task_struct?*?pick_next_task_fair(struct?rq?*rq?...)?{ ????//?cfs_rq?是當前?CPU?上公平調度類隊列 ????struct?cfs_rq?*cfs_rq?=?&rq->cfs; ????struct?sched_entity?*se; ????struct?task_struct?*p; ????int?new_tasks; again: ????//?1?當前?CPU?進程隊列沒有進程可調度,則執行負載均衡邏輯 ????if?(!cfs_rq->nr_running)? ????????goto?idle; ????//?2.?當前?CPU?進程隊列有進程可調度,選中一個高優進程?p ????do?{ ????????struct?sched_entity?*curr?=?cfs_rq->curr; ????????... ????????se?=?pick_next_entity(cfs_rq,?curr);? ????????cfs_rq?=?group_cfs_rq(se); ????}?while?(cfs_rq);? ????p?=?task_of(se); ????... idle: ????//?通過負載均衡遷移進程 ????new_tasks?=?idle_balance(rq,?rf);?//?:7017 ????... ???? ????if?(new_tasks?>?0) ????????goto?again; ????return?NULL; }
pick_next_task_fair()?的邏輯相對還是比較復雜的,但是,其中的核心思想分為兩步:
如果當前?CPU?上已無進程可調度,則執行負載邏輯,從其他?CPU?上遷移進程過來;
如果當前?CPU?上有進程可調度,從隊列中選擇一個高優進程,所謂高優進程,即虛擬時間最小的進程;
下面,我們分兩步拆解上述步驟。
4.1?負載均衡邏輯
內核為了讓各?CPU?負載能夠均衡,在某些?CPU?較為空閑的時候,會從繁忙的?CPU?上遷移進程到空閑?CPU?上運行,當然,如果進程設置了?CPU?的親和性,即進程只能在某些?CPU?上運行,則此進程無法遷移。
負載均衡的核心邏輯是?idle_balance?方法:
//?kernel/sched/fair.c:9851 static?int?idle_balance(struct?rq?*this_rq?...)?{ ????int?this_cpu?=?this_rq->cpu; ????struct?sched_domain?*sd; ????int?pulled_task?=?0; ???? ????... ????for_each_domain(this_cpu,?sd)?{?//?:9897 ????????... ????????pulled_task?=?load_balance(this_cpu...); ????????... ????????if?(pulled_task?...)?//?:9912 ????????????break; ????} ???? ????... ????return?pulled_task; }
idle_balance()?方法的邏輯也相對比較復雜:但是大體上概括就是,遍歷當前?CPU?的所有調度域,直到遷移出進程位置。
這里涉及到一個核心概念:sched_domain,即調度域,下面用一張圖介紹一下什么是調度域。
內核根據處理器與主存的距離將處理器分為兩個?NUMA?節點,每個節點有兩個處理器。NUMA?指的是非一致性訪問,每個?NUMA?節點中的處理器訪問內存節點的速度不一致,不同?NUMA?節點之間不共享?L1?L2?L3?Cache。
每個?NUMA?節點下有兩個處理器,同一個?NUMA?下的不同處理器共享?L3?Cache。
每個處理器下有兩個?CPU?核,同個處理器下的不同核共享?L2?L3?Cache。
每個核下面有兩個超線程,同一個核的不同超線程共享?L1?L2?L3?Cache。
我們在應用程序里面,通過系統?API?拿到的都是超線程,也可以叫做邏輯?CPU,下文統稱邏輯?CPU。
進程在訪問一個某個地址的數據的時候,會先在?L1?Cache?中找,若未找到,則在?L2?Cache?中找,再未找到,則在?L3?Cache?上找,最后都沒找到,就訪問主存,而訪問速度方面?L1?>?L2?>?L3?>?主存,內核遷移進程的目標是盡可能讓遷移出來的進程能夠命中緩存。
內核按照上圖中被虛線框起來的部分,抽象出調度域的概念,越靠近上層,調度域的范圍越大,緩存失效的概率越大,因此,遷移進程的一個目標是,盡可能在低級別的調度域中獲取可遷移的進程。
上述代碼?idle_balance()?方法的?9897?行:for_each_domain(this_cpu,?sd),this_cpu?就是邏輯?CPU(也即是最底層的超線程概念),sd?是調度域,這行代碼的邏輯就是逐層往上擴大調度域;
//?kernel/sched/sched.h:1268 #define?for_each_domain(cpu,?__sd)? ????for?(__sd?=?cpu_rq(cpu)->sd);? ????????????__sd;?__sd?=?__sd->parent)
而?idle_balance()?方法的?9812?行,如果在某個調度域中,成功遷移出了進程到當前邏輯?CPU,就終止循環,可見,內核為了提升應用性能真是煞費苦心。
經過負載均衡之后,當前空閑的邏輯?CPU?進程隊列很有可能已經存在就緒進程了,于是,接下來從這個隊列中獲取最合適的進程。
4.2?選中高優進程
接下來,我們把重點放到如何選擇高優進程,而在公平調度類中,會通過進程的實際優先級和運行時間,來計算一個虛擬時間,虛擬時間越少,被選中的概率越高,所以叫做公平調度。
以下是選擇高優進程的核心邏輯:
//?kernel/sched/fair.c:6898 static?struct?task_struct?*?pick_next_task_fair(struct?rq?*rq?...)?{ ????//?cfs_rq?是當前?CPU?上公平調度類隊列 ????struct?cfs_rq?*cfs_rq?=?&rq->cfs; ????struct?sched_entity?*se; ????struct?task_struct?*p; ????//?2.?當前?CPU?進程隊列有進程可調度,選中一個高優進程?p ????do?{ ????????struct?sched_entity?*curr?=?cfs_rq->curr; ????????... ????????se?=?pick_next_entity(cfs_rq,?curr);? ????????cfs_rq?=?group_cfs_rq(se); ????}?while?(cfs_rq);? ????//?拿到被調度實體包裹的進程 ????p?=?task_of(se);?//?:6956 ????... ????return?p; }
內核提供一個調度實體的的概念,對應數據結構叫?sched_entity,內核實際上是根據調度實體為單位進行調度的:
//?include/linux/sched.h:447 struct?sched_entity?{ ????... ????//?當前調度實體的父親 ????struct?sched_entity????*parent; ????//?當前調度實體所在隊列 ????struct?cfs_rq?*cfs_rq;??//?:468 ????//?當前調度實體擁有的隊列,及子調度實體隊列 ????//?進程是底層實體,不擁有隊列 ????struct?cfs_rq?*my_q; ????... };
每一個進程對應一個調度實體,若干調度實體綁定到一起可以形成一個更高層次的調度實體,因此有個遞歸的效應,上述?do?while?循環的邏輯就是從當前邏輯?CPU?頂層的公平調度實體(cfs_rq->curr)開始,逐層選擇虛擬時間較少的調度實體進行調度,直到最后一個調度實體是進程。
內核這樣做的原因是希望盡可能在每個層次上,都能夠保證調度是公平的。
拿?Docker?容器的例子來說,一個?Docker?容器中運行了若干個進程,這些進程屬于同一個調度實體,和宿主機上的進程的調度實體屬于同一層級,所以,如果?Docker?容器中瘋狂?fork?進程,內核會計算這些進程的虛擬時間總和來和宿主機其他進程進行公平抉擇,這些進程休想一直霸占著?CPU!
選擇虛擬時間最少的進程的邏輯是?se?=?pick_next_entity(cfs_rq,?curr);??,相應邏輯如下:
//?kernel/sched/fair.c:4102 struct?sched_entity?* pick_next_entity(cfs_rq?*cfs_rq,?sched_entity?*curr)?{ ????//?公平運行隊列中虛擬時間最小的調度實體 ????struct?sched_entity?*left?=?__pick_first_entity(cfs_rq); ????struct?sched_entity?*se; ????//?如果沒找到或者樹中的最小虛擬時間的進程 ????//?還沒當前調度實體小,那就選擇當前實體 ????if?(!left?||?(curr?&&?entity_before(curr,?left)))? ????????left?=?curr; ????se?=?left;? ????return?se; } //?kernel/sched/fair.c:489 int?entity_before(struct?sched_entity?*a,?struct?sched_entity?*b)?{ ????//?比較兩者虛擬時間 ????return?(s64)(a->vruntime?-?b->vruntime)?<p>上述代碼,我們可以分析出,pick_next_entity()?方法會在當前公平調度隊列?cfs_rq?中選擇最靠左的調度實體,最靠左的調度實體的虛擬時間越小,即最優。</p><p>而下面通過?__pick_first_entity()?方法,我們了解到,公平調度隊列?cfs_rq?中的調度實體被組織為一棵紅黑樹,這棵樹的最左側節點即為最小節點:</p><pre class="brush:sql;toolbar:false">//?kernel/sched/fair.c:565 struct?sched_entity?*__pick_first_entity(struct?cfs_rq?*cfs_rq)?{ ????struct?rb_node?*left?=?rb_first_cached(&cfs_rq->tasks_timeline); ????if?(!left) ????????return?NULL; ????return?rb_entry(left,?struct?sched_entity,?run_node); } //?include/linux/rbtree.h:91 //?緩存了紅黑樹最左側節點 #define?rb_first_cached(root)?(root)->rb_leftmost
通過以上分析,我們依然通過一個?Docker?的例子來分析:?一個宿主機中有兩個普通進程分別為?A,B,一個?Docker?容器,容器中有?c1、c2、c3?進程。
這種情況下,系統中有兩個層次的調度實體,最高層為?A、B、c1+c2+c3,再往下為?c1、c2、c3,下面我們分情況來討論進程選中的邏輯:
1)若虛擬時間分布為:A:100s,B:200s,c1:50s,c2:100s,c3:80s
選中邏輯:先比較?A、B、c1+c2+c3?的虛擬時間,發現?A?最小,由于?A?已經是進程,選中?A,如果?A?比當前運行進程虛擬時間還小,下一個運行的進程就是?A,否則保持當前進程不變。
2)若虛擬時間分布為:A:100s,B:200s,c1:50s,c2:30s,c3:10s
選中邏輯:先比較?A、B、c1+c2+c3?的虛擬時間,發現?c1+c2+c3?最小,由于選中的調度實體非進程,而是一組進程,繼續往下一層調度實體進行選擇,比較?c1、c2、c3?的虛擬時間,發現?c3?的虛擬時間最小,如果?c3?的虛擬時間小于當前進程的虛擬時間,下一個運行的進程就是?c3,否則保持當前進程不變。
到這里,選中高優進程進行調度的邏輯就結束了,我們來做下小結。
4.3?pick_next_task()?小結
內核在選擇進程進行調度的時候,會先判斷當前?CPU?上是否有進程可以調度,如果沒有,執行進程遷移邏輯,從其他?CPU?遷移進程,如果有,則選擇虛擬時間較小的進程進行調度。
內核在選擇邏輯?CPU?進行遷移進程的時候,為了提升被遷移進程的性能,即避免遷移之后?L1?L2?L3?高速緩存失效,盡可能遷移那些和當前邏輯?CPU?共享高速緩存的目標邏輯?CPU,離當前邏輯?CPU?越近越好。
內核將進程抽象為調度實體,為的是可以將一批進程進行統一調度,在每一個調度層次上,都保證公平。
所謂選中高優進程,實際上選中的是虛擬時間較小的進程,進程的虛擬時間是根據進程的實際優先級和進程的運行時間等信息動態計算出來的。
5?context_switch():執行上下文切換
選中一個合適的進程之后,接下來就要執行實際的進程切換了,我們把目光重新聚焦到?__schedule()?方法
//?kernel/sched/core.c:3395 void?__schedule(bool?preempt)?{ ????struct?task_struct?*prev,?*next; ????... ????//?1?根據某種算法從就緒隊列中選中一個進程 ????next?=?pick_next_task(rq,?prev,...);?//?:3459 ????... ????if?(prev?!=?next)?{ ????????rq->curr?=?next; ????????//?2?執行進程上下文切換 ????????rq?=?context_switch(rq,?prev,?next?...);?//?::3485 ????}? ????... }
其中,進程上下文切換的核心邏輯就是?context_switch,對應邏輯如下:
//?kernel/sched/core.c:2804 struct?rq?*context_switch(...?task_struct?*prev,?task_struct?*next?...)?{ ????struct?mm_struct?*mm,?*oldmm; ????... ????mm?=?next->mm; ????oldmm?=?prev->active_mm; ????... ????//?1?切換虛擬內存 ????switch_mm_irqs_off(oldmm,?mm,?next); ????... ????//?2?切換寄存器狀態 ????switch_to(prev,?next,?prev); ????... }
上述代碼,我略去了一些細節,保留我們關心的核心邏輯。context_switch()?核心邏輯分為兩個步驟,切換虛擬內存和寄存器狀態,下面,我們展開這兩段邏輯。
5.1?切換虛擬內存
首先,簡要介紹一下虛擬內存的幾個知識點:
進程無法直接訪問到物理內存,而是通過虛擬內存到物理內存的映射機制間接訪問到物理內存的。
每個進程都有自己獨立的虛擬內存地址空間。如,進程?A?可以有一個虛擬地址?0x1234?映射到物理地址?0x4567,進程?B?也可以有一個虛擬地址?0x1234?映射到?0x3456,即不同進程可以有相同的虛擬地址。如果他們指向的物理內存相同,則兩個進程即可通過內存共享進程通信。
進程通過多級頁表機制來執行虛擬內存到物理內存的映射,如果我們簡單地把這個機制當做一個?map?數據結構的話,那么可以理解為不同的進程有維護著不同的?map;
map?的翻譯是通過多級頁表來實現的,訪問多級頁表需要多次訪問內存,效率太差,因此,內核使用?TLB?緩存頻繁被訪問的??的項目,感謝局部性原理。
由于不同進程可以有相同的虛擬地址,這些虛擬地址往往指向了不同的物理地址,因此,TLB?實際上是通過?
進程的虛擬地址空間用數據結構?mm_struct?來描述,進程數據結構?task_struct?中的?mm?字段就指向此數據結構,而上述所說的進程的?“map”?的信息就藏在?mm_struct?中。
關于虛擬內存的介紹,后續的文章會繼續分析,這里,我們只需要了解上述幾個知識點即可,我們進入到切換虛擬內存核心邏輯:
//?include/linux/mmu_context.h:14 #?define?switch_mm_irqs_off?switch_mm //?arch/arm64/include/asm/mmu_context.h:241 void?switch_mm(mm_struct?*prev,?mm_struct?*next)?{ ????//?如果兩個進程不是同一個進程 ????if?(prev?!=?next) ????????__switch_mm(next); ????... } //?arch/arm64/include/asm/mmu_context.h:224 void?__switch_mm(struct?mm_struct?*next)?{ ????unsigned?int?cpu?=?smp_processor_id(); ????check_and_switch_context(next,?cpu); }
接下來,調用?check_and_switch_context?做實際的虛擬內存切換操作:
//?arch/arm64/mm/context.c:194 void?check_and_switch_context(struct?mm_struct?*mm,?unsigned?int?cpu)?{ ????... ????u64?asid; ????//?拿到要將下一個進程的?ASID ????asid?=?atomic64_read(&mm->context.id);?//?:218 ????... ????//?將下一個進程的?ASID?綁定到當前?CPU ????atomic64_set(&per_cpu(active_asids,?cpu),?asid);??//?:236 ????//?切換頁表,及切換我們上述中的?"map", ????//?將?ASID?和?"map"?設置到對應的寄存器 ????cpu_switch_mm(mm->pgd,?mm);?//?:248 }
check_and_switch_context?總體上分為兩塊邏輯:
-
將下一個進程的?ASID?綁定到當前的?CPU,這樣?TLB?通過虛擬地址翻譯出來的物理地址,就屬于下個進程的。
-
拿到下一個進程的?“map”,也就是頁表,對應的字段是?“mm->pgd”,然后執行頁表切換邏輯,這樣后續如果?TLB?沒命中,當前?CPU?就能夠知道通過哪個?“map”?來翻譯虛擬地址。
cpu_switch_mm?涉及的匯編代碼較多,這里就不貼了,本質上就是將?ASID?和頁表(”map”)的信息綁定到對應的寄存器。
5.2?切換通用寄存器
虛擬內存切換完畢之后,接下來切換進程執行相關的通用寄存器,對應邏輯為?switch_to(prev,?next?…);?方法,這個方法也是切換進程的分水嶺,調用完之后的那一刻,當前?CPU?上執行就是?next?的代碼了。
拿?arm64?為例:
//?arch/arm64/kernel/process.c:422 struct?task_struct?*__switch_to(task_struct?*prev,?task_struct?*next)?{ ????... ????//?實際切換方法 ????cpu_switch_to(prev,?next);?//?:444 ????... }
cpu_switch_to?對應的是一段經典的匯編邏輯,看著很多,其實并不難理解。
//?arch/arm64/kernel/entry.S:1040 //?x0?->?pre //?x1?->?next ENTRY(cpu_switch_to) ????//?x10?存放?task_struct->thread.cpu_context?字段偏移量 ????mov????x10,?#THREAD_CPU_CONTEXT?//?:1041 ???? ????//?===保存?pre?上下文=== ????//?x8?存放?prev->thread.cpu_context ????add????x8,?x0,?x10? ????//?保存?prev?內核棧指針到?x9 ????mov????x9,?sp ????//?將?x19?~?x28?保存在?cpu_context?字段中 ????//?stp?是?store?pair?的意思 ????stp????x19,?x20,?[x8],?#16 ????stp????x21,?x22,?[x8],?#16 ????stp????x23,?x24,?[x8],?#16 ????stp????x25,?x26,?[x8],?#16 ????stp????x27,?x28,?[x8],?#16 ????//?將?x29?存在?fp?字段,x9?存在?sp?字段 ????stp????x29,?x9,?[x8],?#16? ????//?將?pc?寄存器?lr?保存到?cpu_context?的?pc?字段 ????str????lr,?[x8]? ???? ???? ????//?===加載?next?上下文=== ????//?x8?存放?next->thread.cpu_context ????add????x8,?x1,?x10 ????//?將?cpu_context?中字段載入到?x19?~?x28 ????//?ldp?是?load?pair?的意思 ????ldp????x19,?x20,?[x8],?#16 ????ldp????x21,?x22,?[x8],?#16 ????ldp????x23,?x24,?[x8],?#16 ????ldp????x25,?x26,?[x8],?#16 ????ldp????x27,?x28,?[x8],?#16 ????ldp????x29,?x9,?[x8],?#16 ????//?設置?pc?寄存器 ????ldr????lr,?[x8] ????//?切換到?next?的內核棧 ????mov????sp,?x9 ???? ????//?將?next?指針保存到?sp_el0?寄存器 ????msr????sp_el0,?x1? ????ret ENDPROC(cpu_switch_to)
上述匯編的邏輯可以和操作系統理論課里的內容一一對應,即先將通用寄存器的內容保存到進程的數據結構中對應的字段,然后再從下一個進程的數據結構中對應的字段加載到通用寄存器中。
1041?行代碼是拿到?task_struct?結構中的?thread_struct?thread?字段的?cpu_contxt?字段:
//?arch/arm64/kernel/asm-offsets.c:53 DEFINE(THREAD_CPU_CONTEXT,????offsetof(struct?task_struct,?thread.cpu_context));
我們來分析一下對應的數據結構:
//?include/linux/sched.h:592 struct?task_struct?{ ????... ????struct?thread_struct?thread;?//?:1212 ????... }; //?arch/arm64/include/asm/processor.h:129 struct?thread_struct?{ ????struct?cpu_context??cpu_context; ????... }
而?cpu_context?數據結構的設計就是為了保存與進程有關的一些通用寄存器的值:
//?arch/arm64/include/asm/processor.h:113 struct?cpu_context?{ ????unsigned?long?x19; ????unsigned?long?x20; ????unsigned?long?x21; ????unsigned?long?x22; ????unsigned?long?x23; ????unsigned?long?x24; ????unsigned?long?x25; ????unsigned?long?x26; ????unsigned?long?x27; ????unsigned?long?x28; ????//?對應?x29?寄存器 ????unsigned?long?fp; ????unsigned?long?sp; ????//?對應?lr?寄存器 ????unsigned?long?pc; };
這些值剛好與上述匯編片段的代碼一一對應上,讀者應該不需要太多匯編基礎就可以分析出來。
上述匯編中,最后一行?msr?sp_el0,?x1,x1?寄存器中保存了?next?的指針,這樣后續再調用?current?宏的時候,就指向了下一個指針:
//?arch/arm64/include/asm/current.h:15 static?struct?task_struct?*get_current(void)?{ ????unsigned?long?sp_el0; ????asm?("mrs?%0,?sp_el0"?:?"=r"?(sp_el0)); ????return?(struct?task_struct?*)sp_el0; } //?current?宏,很多地方會使用到 #define?current?get_current()
進程上下文切換的核心邏輯到這里就結束了,最后我們做下小結。
5.3?context_switch()?小結
進程上下文切換,核心要切換的是虛擬內存及一些通用寄存器。
進程切換虛擬內存,需要切換對應的?TLB?中的?ASID?及頁表,頁表也即不同進程的虛擬內存翻譯需要的?“map”。
進程的數據結構中,有一個間接字段?cpu_context?保存了通用寄存器的值,寄存器切換的本質就是將上一個進程的寄存器保存到?cpu_context?字段,然后再將下一個進程的?cpu_context?數據結構中的字段加載到寄存器中,至此完成進程的切換。
6?本文總結
最后,我們全文做下總結:
進程調度分為主動(非搶占)和被動調度(搶占),調度的核心邏輯在?__schedule()?方法中。
進程調度的核心邏輯分為兩個大步驟,其一是選擇一個合適的進程,其二是進行進程切換。
在選擇合適的進程中,如果當前邏輯?CPU?沒有可調度的進程,就從其他?CPU?來調度,遷移的進程盡可能靠近當前?CPU。
進程調度的單位其實是調度實體,一個調度實體對應一個或多個進程,這樣就能夠在各個層次上完成公平調度,通過調度實體的虛擬時間選擇最優調度實體對應的進程。
進程切換,本質上就是切換虛擬內存及通用寄存器。
進程調度的邏輯幾乎都完全遵循操作系統理論來設計的,學完操作系統之后,希望大家能夠理論聯系實際,對照著內核去翻一翻源碼。
相關推薦:《Linux視頻教程》