淺析Linux中的進程調度


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。

參考資料:

  1. http://blog.chinaunix.net/uid-27767798-id-3548384.html
  2. linux內核3.11.1源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM