2016-11-22
前面在看軟中斷的時候,牽扯到不少進程調度的知識,這方面自己確實一直不怎么了解,就趁這個機會好好學習下。
現代的操作系統都是多任務的操作系統,盡管隨着科技的發展,硬件的處理器核心越來越多,但是仍然不能保證一個進程對應一個核心,這就勢必需要一個管理單元,負責調度進程,由管理單元來決定下一刻應該由誰使用CPU,這里充當管理單元的就是進程調度器。
進程調度器的任務就是合理分配CPU時間給運行的進程,創造一種所有進程並行運行的錯覺。這就對調度器提出了要求:
1、調度器分配的CPU時間不能太長,否則會導致其他的程序響應延遲,難以保證公平性。
2、調度器分配的時間也不能太短,每次調度會導致上下文切換,這種切換開銷很大。
而調度器的任務就是:1、分配時間給進程 2、上下文切換
所以具體而言,調度器的任務就明確了:用一句話表述就是在恰當的實際,按照合理的調度算法,切換兩個進程的上下文。
調度器的結構
在Linux內核中,調度器可以分成兩個層級,在進程中被直接調用的成為通用調度器或者核心調度器,他們作為一個組件和進程其他部分分開,而通用調度器和進程並沒有直接關系,其通過第二層的具體的調度器類來直接管理進程。具體架構如下圖:
如上圖所示,每個進程必然屬於一個特定的調度器類,Linux會根據不同的需求實現不同的調度器類。各個調度器類之間具備一定的層次關系,即在通用調度器選擇進程的時候,會從最高優先級的調度器類開始選擇,如果通用調度器類沒有可運行的進程,就選擇下一個調度器類的可用進程,這樣逐層遞減。
每個CPU會維護一個調度隊列稱之為就緒隊列,每個進程只會出現在一個就緒隊列中,因為同一進程不能同時被兩個CPU選中執行。就緒隊列的數據結構為struct rq,和上面的層次結構一樣,通用調度器直接和rq打交道,而具體和進程交互的是特定於調度器類的子就緒隊列。
調度器類
在linux內核中實現了一個調度器類的框架,其中定義了調度器應該實現的函數,每一個具體的調度器類都要實現這些函數 。
在當前linux版本中(3.11.1),使用了四個調度器類:stop_sched_class、rt_sched_class、fair_sched_class、idle_sched_class。在最新的內核中又添加了一個調度類dl_sched_class,但是由於筆者能力所限,且大部分進程都是屬於實時調度器和完全公平調度器,所以我們主要分析實時調度器和完全公平調度器。
看下調度器類的定義:
1 struct sched_class { 2 const struct sched_class *next; 3 4 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); 5 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); 6 void (*yield_task) (struct rq *rq); 7 bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt); 8 9 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); 10 11 struct task_struct * (*pick_next_task) (struct rq *rq); 12 void (*put_prev_task) (struct rq *rq, struct task_struct *p); 13 14 #ifdef CONFIG_SMP 15 int (*select_task_rq)(struct task_struct *p, int sd_flag, int flags); 16 void (*migrate_task_rq)(struct task_struct *p, int next_cpu); 17 18 void (*pre_schedule) (struct rq *this_rq, struct task_struct *task); 19 void (*post_schedule) (struct rq *this_rq); 20 void (*task_waking) (struct task_struct *task); 21 void (*task_woken) (struct rq *this_rq, struct task_struct *task); 22 23 void (*set_cpus_allowed)(struct task_struct *p, 24 const struct cpumask *newmask); 25 26 void (*rq_online)(struct rq *rq); 27 void (*rq_offline)(struct rq *rq); 28 #endif 29 30 void (*set_curr_task) (struct rq *rq); 31 void (*task_tick) (struct rq *rq, struct task_struct *p, int queued); 32 void (*task_fork) (struct task_struct *p); 33 34 void (*switched_from) (struct rq *this_rq, struct task_struct *task); 35 void (*switched_to) (struct rq *this_rq, struct task_struct *task); 36 void (*prio_changed) (struct rq *this_rq, struct task_struct *task, 37 int oldprio); 38 39 unsigned int (*get_rr_interval) (struct rq *rq, 40 struct task_struct *task); 41 42 #ifdef CONFIG_FAIR_GROUP_SCHED 43 void (*task_move_group) (struct task_struct *p, int on_rq); 44 #endif 45 };
enqueue_task向就緒隊列添加一個進程,該操作發生在一個進程變成就緒態(可運行態)的時候。
dequeue_task就是執行enqueue_task的逆操作,在一個進程由運行態轉為阻塞的時候就會發生該操作。
yield_task用於進程自願放棄控制權的時候。
pick_next_task用於挑選下一個可運行的進程,發生在進程調度的時候,又調度器調用。
set_curr_task當進程的調度策略發生變化時,需要執行此函數
task_tick,在每次激活周期調度器時,由周期調度器調用。
task_fork用於建立fork系統調用和調度器之間的關聯,每次新進程建立后,就調用該函數通知調度器。
就緒隊列
如前所述,每個CPU維護一個就緒隊列,由結構struct rq表示,通用調度器直接和rq交互,在rq中又維護了子就緒隊列,這些子就緒隊列和具體的調度器類相關,進程入隊出隊都需要根據調度器類的具體算法。
rq為維護針對當前CPU而言全局的信息,其結構比較龐大,這里就不詳細列舉。其大致內容包括當前CPU就緒隊列包含的所有進程數、記載就緒隊列當前負荷的度量,並嵌入子就緒隊列cfs_rq和rt_rq等,系統所有的就緒隊列位於一個runqueues數組中,每個CPU對應一個元素。
內核也定義了一些宏,操作這些全局的隊列:
1 #define cpu_rq(cpu) (&per_cpu(runqueues, (cpu))) 2 #define this_rq() (&__get_cpu_var(runqueues)) 3 #define task_rq(p) cpu_rq(task_cpu(p)) 4 #define cpu_curr(cpu) (cpu_rq(cpu)->curr) 5 #define raw_rq() (&__raw_get_cpu_var(runqueues))
調度實體
linux中可調度的不僅僅是進程,也可能是一個進程組,所以LInux就把調度對象抽象化成一個調度實體。就像是很多結構中嵌入list_node用於連接鏈表一樣,這里需要執行調度的也就需要加入這樣一個調度實體。實際上,調度器直接操作的也是調度實體,只是會根據調度實體獲取到其對應的結構。
1 struct sched_entity { 2 struct load_weight load; /* for load-balancing */ 3 struct rb_node run_node; 4 struct list_head group_node; 5 unsigned int on_rq; 6 7 u64 exec_start; 8 u64 sum_exec_runtime; 9 u64 vruntime; 10 u64 prev_sum_exec_runtime; 11 12 u64 nr_migrations; 13 14 #ifdef CONFIG_SCHEDSTATS 15 struct sched_statistics statistics; 16 #endif 17 18 #ifdef CONFIG_FAIR_GROUP_SCHED 19 struct sched_entity *parent; 20 /* rq on which this entity is (to be) queued: */ 21 struct cfs_rq *cfs_rq; 22 /* rq "owned" by this entity/group: */ 23 struct cfs_rq *my_q; 24 #endif 25 26 #ifdef CONFIG_SMP 27 /* Per-entity load-tracking */ 28 struct sched_avg avg; 29 #endif 30 };
load用於負載均衡,決定了各個實體占隊列中負荷的比例,計算負荷權重是調度器的主要責任,因為選擇下一個進程就是要根據這些信息。run_node是一個紅黑樹節點,用於把實體加入到紅黑樹,on_rq表明該實體是否位於就緒隊列,當為1的時候就說明在就緒隊列中,一個進程在得到調度的時候會從就緒隊列中摘除,在讓出CPU的時候會重新添加到就緒隊列(正常調度的情況,不包含睡眠、等待)。在后面有一個時間相關的字段,exec_start記錄進程開始在CPU上運行的時間;sum_exec_time記錄進程一共在CPU上運行的時間,pre_sum_exec_time記錄本地調度之前,進程已經運行的時間。在進程被調離CPU的時候,會把sum_exec_time的值保存到pre_sum_exec_time,而sum_exec_time並不重置,而是一直隨着在CPU上的運行遞增。而vruntime 記錄在進程執行期間,在虛擬時鍾上流逝的時間,用於CFS調度器,后面會具體講述。后面的parent、cfs_rq、my_rq是和組調度相關的,這里我們暫且不涉及。
看下load字段
struct load_weight { unsigned long weight, inv_weight; };
這里有兩個字段,weight和inv_weight。前者是當前實體優先級對應的權重,這個可以根據prio_to_weight數組轉化得到。而后者是是用於快速計算vruntime用的,可以通過prio_to_wmult數組得到,后者是一個和prio_to_weight同樣大小的數組,每一項的值為2^32/weight,內核中的除法運算沒那么簡單,為了加速操作,選取的折中辦法。vruntime的計算可以參考calc_delta_mine函數。
到這里,調度器的基本架構就比較清楚了,調度過程中需要計算進程的優先級,這點是比較復雜的過程,我們單獨分一節描述,下面根據CFS調度類探索下進程調度的過程。
- 什么時候調度
- 如何進行調度
進程調度並不是什么時候都可以,前面也說過,系統會有一個周期調度器,根據頻率自動調用schedule_tick函數。其主要作用就是根據進程運行時間觸發調度;在進程遇到資源等待被阻塞也可以顯示的調用調度器函數進行調度;另外在有內核空間返回到用戶空間時,會判斷當前是否需要調度,在進程對應的thread_info結構中,有一個flag,該flag字段的第二位(從0開始)作為一個重調度標識TIF_NEED_RESCHED,當被設置的時候表明此時有更高優先級的進程,需要執行調度。另外目前的內核支持內核搶占功能,在適當的時機可以搶占內核的運行。關於內核搶占,我們最后論述。
而至於如何進行調度呢?就要看具體調度器類了。一旦確定了要進行調度,那么schedule函數被調用。注意,周期性調度器並不直接調度,至多設置進程的重調度位TIF_NEED_RESCHED,這樣在返回用戶空間的時候仍然由主調度器執行調度。跟蹤下schedule函數,其實具體實現由__schedule函數完成,直接看該函數:
1 static void __sched __schedule(void) 2 { 3 struct task_struct *prev, *next; 4 unsigned long *switch_count; 5 struct rq *rq; 6 int cpu; 7 8 need_resched: 9 /*禁止內核搶占*/ 10 preempt_disable(); 11 cpu = smp_processor_id(); 12 /*獲取CPU 的調度隊列*/ 13 rq = cpu_rq(cpu); 14 rcu_note_context_switch(cpu); 15 /*保存當前任務*/ 16 prev = rq->curr; 17 18 schedule_debug(prev); 19 20 if (sched_feat(HRTICK)) 21 hrtick_clear(rq); 22 23 /* 24 * Make sure that signal_pending_state()->signal_pending() below 25 * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE) 26 * done by the caller to avoid the race with signal_wake_up(). 27 */ 28 smp_mb__before_spinlock(); 29 raw_spin_lock_irq(&rq->lock); 30 31 switch_count = &prev->nivcsw; 32 /* 如果內核態沒有被搶占, 並且內核搶占有效 33 即是否同時滿足以下條件: 34 1 該進程處於停止狀態 35 2 該進程沒有在內核態被搶占 */ 36 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { 37 if (unlikely(signal_pending_state(prev->state, prev))) { 38 prev->state = TASK_RUNNING; 39 } else { 40 deactivate_task(rq, prev, DEQUEUE_SLEEP); 41 prev->on_rq = 0; 42 43 /* 44 * If a worker went to sleep, notify and ask workqueue 45 * whether it wants to wake up a task to maintain 46 * concurrency. 47 */ 48 if (prev->flags & PF_WQ_WORKER) { 49 struct task_struct *to_wakeup; 50 51 to_wakeup = wq_worker_sleeping(prev, cpu); 52 if (to_wakeup) 53 try_to_wake_up_local(to_wakeup); 54 } 55 } 56 switch_count = &prev->nvcsw; 57 } 58 59 pre_schedule(rq, prev); 60 61 if (unlikely(!rq->nr_running)) 62 idle_balance(cpu, rq); 63 /*告訴調度器prev進程即將被調度出去*/ 64 put_prev_task(rq, prev); 65 /*挑選下一個可運行的進程*/ 66 next = pick_next_task(rq); 67 /*清除pre的TIF_NEED_RESCHED標志*/ 68 clear_tsk_need_resched(prev); 69 rq->skip_clock_update = 0; 70 /*如果next和當前進程不一致,就可以調度*/ 71 if (likely(prev != next)) { 72 rq->nr_switches++; 73 /*設置當前調度進程為next*/ 74 rq->curr = next; 75 ++*switch_count; 76 /*切換進程上下文*/ 77 context_switch(rq, prev, next); /* unlocks the rq */ 78 /* 79 * The context switch have flipped the stack from under us 80 * and restored the local variables which were saved when 81 * this task called schedule() in the past. prev == current 82 * is still correct, but it can be moved to another cpu/rq. 83 */ 84 cpu = smp_processor_id(); 85 rq = cpu_rq(cpu); 86 } else 87 raw_spin_unlock_irq(&rq->lock); 88 89 post_schedule(rq); 90 91 sched_preempt_enable_no_resched(); 92 if (need_resched()) 93 goto need_resched; 94 }
調度器運行期間是要禁止內核搶占的,從級別上來講,LInux中的調度器不見得比其他進程的級別高,但是肯定不會低於普通進程,即調度器運行期間會禁止內核搶占。相比之下,windows中使用中斷請求級別的概念,普通進程運行在passive level,而調度器運行在DPC level,調度器運行期間只有硬件中斷可以打斷。從函數代碼來看,這里首先調用了preempt_disable函數設置了preempt_count禁止內核搶占,然后獲取當前CPU的就緒隊列結構rq,prev保存當前任務,下面的prev->state && !(preempt_count() & PREEMPT_ACTIVE)是對有些進行移除運行隊列。具體就是如果當前進程是阻塞並且PREEMPT_ACTIVE沒有被設置,就有了移除就緒隊列的條件,然后判斷是否又掛起的信號,如果有,那么暫時不移除隊列,否則就執行deactivate_task函數移除隊列,並設置prev->on_rq=0,表明該進程不在就緒隊列中。下面的if是判斷如果當前進程是一個工作線程,那么就通知工作隊列,看是否需要喚醒另一個worker。
出了if就調用了pre_schedule,該函數在CFS中沒有實現,而在實時調度器中實現了,具體什么作用不太清楚。
下面的一個if判斷當前CPU就緒隊列是否存在可運行的進程,如果不存在即沒有進程可以運行就調用idle_balance從其他的CPU平衡一下任務。當然這種情況極少見。
接下來就要進行正式工作了,調用put_prev_task預處理下,具體是調用對應調度器類的實現函數:prev->sched_class->put_prev_task(rq, prev);主要任務是把當前任務重新加入就緒隊列。當然在此之前如果當前任務還在就緒隊列(或者說是當前任務是否是可運行狀態),就調用update_curr更新下其進程時間,包括vruntime等。
重要的是下面的pick_next_task,它使用對應的調度器類選擇一個具體的任務作為下一個占用CPU的任務,選定好之后就調用clear_tsk_need_resched清楚prev的重調度標識。
之后進行if判斷,如果prev不是我們選擇的下一個進程,就執行進程的切換。具體 先設置就緒隊列的切換計數nr_switches,然后設置rq->curr=next,這里就從就緒隊列而言,已經標識next為當前進程了。然后就調用context_switch函數切換上下文,主要包含兩部分:切換地址空間、切換寄存器域。之后就開啟內核搶占,這樣一個進程切換就完成了。最后會判斷是否又有新設置的高優先級進程,有的話再次執行調度。
前面大致過程如前所述,但是具體而言,下一個進程的執行是從替換了進程的EIP開始執行的,即調度器函數不到結束就開始運行另一個進程了,而待下次這個進程重新獲得控制權時,就從之前保存的狀態開始運行。在__schedule函數的最后仍然會判斷是否被設置了重調度位,如果被設置了,那么很不幸,又要被調度出去了,但是這種幾率很小,只是以防萬一而已。這樣出了調度函數,正常運行了。
核心函數實現分析:
pick_next_task
1 static inline struct task_struct * 2 pick_next_task(struct rq *rq) 3 { 4 const struct sched_class *class; 5 struct task_struct *p; 6 7 /* 8 * Optimization: we know that if all tasks are in 9 * the fair class we can call that function directly: 10 */ 11 /*如果所有任務都處於完全公平調度類,則可以直接選擇下一個任務*/ 12 if (likely(rq->nr_running == rq->cfs.h_nr_running)) { 13 p = fair_sched_class.pick_next_task(rq); 14 if (likely(p)) 15 return p; 16 } 17 /*從優先級最高的調度器類開始遍歷,順序為stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class*/ 18 /* 19 #define for_each_class(class) \ 20 for (class = sched_class_highest; class; class = class->next) 21 */ 22 for_each_class(class) { 23 p = class->pick_next_task(rq); 24 if (p) 25 return p; 26 } 27 28 BUG(); /* the idle class will always have a runnable task */ 29 }
該函數還是處於主調度器的層面,沒有涉及到核心邏輯,所以還比較好理解。首先判斷當前CPu就緒隊列上的可運行進程數和CFS就緒隊列上的可運行進程數是否一致,如果一致就說明當前主就緒隊列上沒有只有CFS調度類的進程,那么這樣直接調用CFS調度類的方法挑選下一個進程即可。否則還需要從最高級的調度類,層層選擇。下面的for_each_class便是實現這個功能。它按照stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class這個順序,依次調用其pick函數,只有前一個調度類沒有找到可運行的進程,才會查找后一個調度類。我們這里值看CFS的實現:
在fair.c中,對應的函數是pick_next_task_fair
1 static struct task_struct *pick_next_task_fair(struct rq *rq) 2 { 3 struct task_struct *p; 4 /*從CPU 的就緒隊列找到公平調度隊列*/ 5 struct cfs_rq *cfs_rq = &rq->cfs; 6 struct sched_entity *se; 7 /*如果公平調度類沒有可運行的進程,直接返回*/ 8 if (!cfs_rq->nr_running) 9 return NULL; 10 /*如果調度的是一組進程,則需要進行循環設置,否則執行一次就退出了*/ 11 do { 12 /*從公平調度類中找到一個可運行的實體*/ 13 se = pick_next_entity(cfs_rq); 14 /*設置紅黑樹中下一個實體,並標記cfs_rq->curr為se*/ 15 set_next_entity(cfs_rq, se); 16 cfs_rq = group_cfs_rq(se); 17 } while (cfs_rq); 18 /*獲取到具體的task_struct*/ 19 p = task_of(se); 20 if (hrtick_enabled(rq)) 21 hrtick_start_fair(rq, p); 22 23 return p; 24 }
代碼也是比較簡單的,核心在下面的那個do循環中,從這里看該循環做了兩個事情,調用pick_next_entity從CFS就緒隊列中選擇一個調度實體,然后調用set_next_entity設置下一個可以調度的任務,由於CFS的調度實體通過紅黑樹維護,所以這里實際上是調整紅黑樹的過程。而使用循環時應用與組調度的場合,這里我們暫且忽略。
看下pick_next_entity
1 static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq) 2 { 3 /*從紅黑樹中找到最左邊即等待時間最長的那個實體*/ 4 struct sched_entity *se = __pick_first_entity(cfs_rq); 5 struct sched_entity *left = se; 6 7 /* 8 * Avoid running the skip buddy, if running something else can 9 * be done without getting too unfair. 10 */ 11 if (cfs_rq->skip == se) { 12 struct sched_entity *second = __pick_next_entity(se); 13 if (second && wakeup_preempt_entity(second, left) < 1) 14 se = second; 15 } 16 17 /* 18 * Prefer last buddy, try to return the CPU to a preempted task. 19 */ 20 if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) 21 se = cfs_rq->last; 22 23 /* 24 * Someone really wants this to run. If it's not unfair, run it. 25 */ 26 if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) 27 se = cfs_rq->next; 28 29 clear_buddies(cfs_rq, se); 30 31 return se; 32 }
該函數核心在__pick_first_entity,其本身也是很簡答的,不妨看下代碼:
1 struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) 2 { 3 struct rb_node *left = cfs_rq->rb_leftmost; 4 5 if (!left) 6 return NULL; 7 8 return rb_entry(left, struct sched_entity, run_node); 9 }
以為每次選擇后都會設置好下一個應該選擇的,所以這里僅僅獲取下cfs_rq->rb_leftmost就可以了,然后就進行了三個if判斷,但是都使用了同一個函數wakeup_preempt_entity
然后返回相應的調用實體。last表示最后一個調用喚醒操作的進程,next表示最后一個被喚醒的進程。
1 static int 2 wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) 3 { 4 s64 gran, vdiff = curr->vruntime - se->vruntime; 5 6 if (vdiff <= 0) 7 return -1; 8 9 gran = wakeup_gran(curr, se); 10 if (vdiff > gran) 11 return 1; 12 13 return 0; 14 }
這里是對比兩個實體的vruntime,如果curr->vruntime - se->vruntime大於一個固定值,那么就返回1.這個值一般是sysctl_sched_wakeup_granularity。
所以這里邏輯當選定一個實體后,判斷該實體是否是cfs_rq指定跳過的實體,如果是就選擇下一個實體,判斷該實體和上一個實體的vruntime的差距,只要不大於閾值,就可以接收從而選擇后者。
在設定好初始實體后,判斷cfs_rq->last和left的vruntime,如果在可接受的范圍內,則選擇cfs_rq->last,然后接着判斷cfs_rq->next,如果仍然在可接收的范圍內,就選擇cfs_rq->next作為最終選定的調度實體。NEXT_BUDDY表示在cfs選擇next sched_entity的時候會優先選擇最后一個喚醒的sched_entity,而 LAST_BUDDY表示在cfs選擇next sched_entity的時候會優先選擇最后一個執行喚醒操作的那個sched_entity,這兩種調度策略都有助於提高cpu cache的命中率。從代碼來看,next比last優先級更高!
1 static void 2 set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) 3 { 4 /* 'current' is not kept within the tree. */ 5 /*如果該實體處於就緒態,就可以被調度*/ 6 if (se->on_rq) { 7 /* 8 * Any task has to be enqueued before it get to execute on 9 * a CPU. So account for the time it spent waiting on the 10 * runqueue. 11 */ 12 update_stats_wait_end(cfs_rq, se); 13 /*把se出隊列,然后選取一個實體設置到紅黑樹的最左邊*/ 14 __dequeue_entity(cfs_rq, se); 15 } 16 17 update_stats_curr_start(cfs_rq, se); 18 cfs_rq->curr = se; 19 #ifdef CONFIG_SCHEDSTATS 20 /* 21 * Track our maximum slice length, if the CPU's load is at 22 * least twice that of our own weight (i.e. dont track it 23 * when there are only lesser-weight tasks around): 24 */ 25 if (rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { 26 se->statistics.slice_max = max(se->statistics.slice_max, 27 se->sum_exec_runtime - se->prev_sum_exec_runtime); 28 } 29 #endif 30 se->prev_sum_exec_runtime = se->sum_exec_runtime; 31 }
該函數首先判斷選定的實體是否可運行,如果可以就調用調度器的出隊函數,把進程出隊,然后調整紅黑樹,這里為何用個if筆者不是很懂,進程被調用前都要入隊,所以這里選定的se,其on_rq肯定為1呀。難道不是么?之后設置se->exec_start記錄開始運行的時間,然后設置cfs—>curr=se。之后就是設置時間等操作。然后就執行返回了。回到pick_next_task_fair函數中,這里接下來就返回了實體對應的task_struct。
context_switch函數
1 static inline void 2 context_switch(struct rq *rq, struct task_struct *prev, 3 struct task_struct *next) 4 { 5 struct mm_struct *mm, *oldmm; 6 /*進程切換准備工作,需要枷鎖和關中斷,最后需要調用finish_task_switch*/ 7 prepare_task_switch(rq, prev, next); 8 9 mm = next->mm; 10 oldmm = prev->active_mm; 11 /* 12 * For paravirt, this is coupled with an exit in switch_to to 13 * combine the page table reload and the switch backend into 14 * one hypercall. 15 */ 16 arch_start_context_switch(prev); 17 /*如果將要執行的是內核線程*/ 18 if (!mm) { 19 next->active_mm = oldmm; 20 atomic_inc(&oldmm->mm_count); 21 enter_lazy_tlb(oldmm, next); 22 } else 23 switch_mm(oldmm, mm, next); 24 /*如果被調度的是內核線程*/ 25 if (!prev->mm) { 26 prev->active_mm = NULL; 27 rq->prev_mm = oldmm; 28 } 29 /* 30 * Since the runqueue lock will be released by the next 31 * task (which is an invalid locking op but in the case 32 * of the scheduler it's an obvious special-case), so we 33 * do an early lockdep release here: 34 */ 35 #ifndef __ARCH_WANT_UNLOCKED_CTXSW 36 spin_release(&rq->lock.dep_map, 1, _THIS_IP_); 37 #endif 38 39 context_tracking_task_switch(prev, next); 40 /* Here we just switch the register state and the stack. */ 41 /*切換寄存器域和棧*/ 42 switch_to(prev, next, prev); 43 44 barrier(); 45 /* 46 * this_rq must be evaluated again because prev may have moved 47 * CPUs since it called schedule(), thus the 'rq' on its stack 48 * frame will be invalid. 49 */ 50 finish_task_switch(this_rq(), prev); 51 }
這部分內容主要做了兩件事情:切換地址空間、切換寄存器域和棧空間。整個切換過程需要加鎖和關中斷,首先切換的是地址空間,mm 和active_mm分別代表調度和被調度的進程的 mm_struct,如果mm為空,則表明next是內核線程,內核線程沒有自己獨立的地址空間,所以其mm為null,運行的時候使用prev的active_mm即可。如果非空,則是用戶進程,那么可以直接切換,這里調用
switch_mm函數進行切換;如果prev為內核線程,由於其沒有獨立地址空間,所以需要設置其active_mm為null。
接下來就要調用switch_to來切換寄存器域和棧了。這也是進程切換的最后部分。
1 #define switch_to(prev, next, last) \ 2 do { \ 3 /* \ 4 * Context-switching clobbers all registers, so we clobber \ 5 * them explicitly, via unused output variables. \ 6 * (EAX and EBP is not listed because EBP is saved/restored \ 7 * explicitly for wchan access and EAX is the return value of \ 8 * __switch_to()) \ 9 */ \ 10 unsigned long ebx, ecx, edx, esi, edi; \ 11 \ 12 asm volatile("pushfl\n\t" /* save flags */ \ 13 "pushl %%ebp\n\t" /* save EBP */ \ 14 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ 15 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ 16 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ 17 "pushl %[next_ip]\n\t" /* restore EIP */ \ 18 __switch_canary \ 19 "jmp __switch_to\n" /* regparm call */ \ 20 "1:\t" \ 21 "popl %%ebp\n\t" /* restore EBP */ \ 22 "popfl\n" /* restore flags */ \ 23 \ 24 /* output parameters */ \ 25 : [prev_sp] "=m" (prev->thread.sp), \ 26 [prev_ip] "=m" (prev->thread.ip), \ 27 "=a" (last), \ 28 \ 29 /* clobbered output registers: */ \ 30 "=b" (ebx), "=c" (ecx), "=d" (edx), \ 31 "=S" (esi), "=D" (edi) \ 32 \ 33 __switch_canary_oparam \ 34 \ 35 /* input parameters: */ \ 36 : [next_sp] "m" (next->thread.sp), \ 37 [next_ip] "m" (next->thread.ip), \ 38 \ 39 /* regparm parameters for __switch_to(): */ \ 40 [prev] "a" (prev), \ 41 [next] "d" (next) \ 42 \ 43 __switch_canary_iparam \ 44 \ 45 : /* reloaded segment registers */ \ 46 "memory"); \ 47 } while (0)
實際上switch_to是一個宏,由一大串的匯編代碼實現,這段代碼稍微有點復雜,首先把標識寄存器壓棧,然后ebp入棧,接着保存當前esp指針到prev->thread.sp變量中,第15行就把next->thread.sp設置到當前esp寄存器了,也就是說從現在開始使用的是next進程的棧,但是EBP 還沒有切換,所以還可以使用prev進程的變量。接下來movl $1f,%[prev_ip]是把標號1的地址保存到prev->thread.ip,這個就是下次prev進程被調度的時候,開始執行的IP。到目前為止,prev進程的狀態域就保存好了,接下來pushl %[next_ip]是把next進程的起始EIP壓棧,因為后面直接調用一個函數,所以在函數返回后執行執行ret指令,就直接從棧中取出地址放到EIP開始執行,next進程正是從此處開始執行即代碼中標號1的位置。第19行直接使用jmp跳轉到目標函數__switch_to,主要是使用call會自動push eip,這樣函數返回后又從原位置開始,要執行next還需要手動切換EIP,比較麻煩。
切換到next后,就從next進程的標號1位置開始,即popl %%ebp,popfl.需要注意的是,__switch_to的參數在最下方的部分
[prev] "a" (prev), \
[next] "d" (next)
被放到eax和edx中,不明白的人可能怎么也發現不了,涉及到AT&T匯編語法,這里就不在多說,具體可以參考相關文檔。
到這里進程便切換過來了,但是細心的人可能會注意到,這里switch_to本來僅僅是切換兩個進程,卻傳遞進去三個參數,這是為何?
具體來說,這是為了讓被調度到的進程知道在他之前運行的實際進程,為何這么說呢?
下面三個調度,在switch_to執行的時候,狀態如下:
1、A——>B prev=A next=B
2、B——>C prev=B next=C
3、C——>A prev=C next=A
看第三次調度的時候,這個時候A重新獲得控制權,恢復了A的棧狀態,即在A的進程空間,prev=A next=B,而A並不知道在他之前實際運行的是C,所以需要一種方式告知A,在他之前實際運行的進程是C。
參考資料:
- http://blog.chinaunix.net/uid-27767798-id-3548384.html
- linux內核3.11.1源碼