linux調度器源碼分析 - 新進程加入(三)


本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/

 

引言

  之前的文章已經介紹了調度器已經初始化完成,現在只需要加入一個周期定時器tick驅動它進行周期調度即可,而加入定時器tick在下一篇文章進行簡單說明(主要這部分涉及調度器比較少,更多的是時鍾、定時器相關知識)。這篇文章主要說明系統如何把一個進程加入到隊列中。

 

加入時機

  之前的文章也有提到過,只有處於TASK_RUNNING狀態下的進程才能夠加入到調度器,其他狀態都不行,也就說明了,當一個進程處於睡眠、掛起狀態的時候是不存在於調度器中的,而進程加入調度器的時機如下:

  • 當進程創建完成時,進程剛創建完成時,即使它運行起來立即調用sleep()進程睡眠,它也必定先會加入到調度器,因為實際上它加入調度器后自己還需要進行一定的初始化和操作,才會調用到我們的“立即”sleep()。
  • 當進程被喚醒時,也使用sleep的例子說明,我們平常寫程序使用的sleep()函數實現原理就是通過系統調用將進程狀態改為TASK_INTERRUPTIBLE,然后移出運行隊列,並且啟動一個定時器,在定時器到期后喚醒進程,再重新放入運行隊列。

 

sched_fork

  在我的博文關於linux系統如何實現fork的研究(二)中專門描述了copy_process()這個創建函數,而里面有一個函數專門用於進程調度的初始化,就是sched_fork(),其代碼如下

 1 int sched_fork(unsigned long clone_flags, struct task_struct *p)
 2 {
 3     unsigned long flags;
 4     /* 獲取當前CPU,並且禁止搶占 */
 5     int cpu = get_cpu();
 6     
 7     /* 初始化跟調度相關的值,比如調度實體,運行時間等 */
 8     __sched_fork(clone_flags, p);
 9     /*
10      * 標記為運行狀態,表明此進程正在運行或准備好運行,實際上沒有真正在CPU上運行,這里只是導致了外部信號和事件不能夠喚醒此進程,之后將它插入到運行隊列中
11      */
12     p->state = TASK_RUNNING;
13 
14     /*
15      * 根據父進程的運行優先級設置設置進程的優先級
16      */
17     p->prio = current->normal_prio;
18 
19     /*
20      * 更新該進程優先級
21      */
22     /* 如果需要重新設置優先級 */
23     if (unlikely(p->sched_reset_on_fork)) {
24         /* 如果是dl調度或者實時調度 */
25         if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
26             /* 調度策略為SCHED_NORMAL,這個選項將使用CFS調度 */
27             p->policy = SCHED_NORMAL;
28             /* 根據默認nice值設置靜態優先級 */
29             p->static_prio = NICE_TO_PRIO(0);
30             /* 實時優先級為0 */
31             p->rt_priority = 0;
32         } else if (PRIO_TO_NICE(p->static_prio) < 0)
33             /* 根據默認nice值設置靜態優先級 */
34             p->static_prio = NICE_TO_PRIO(0);
35 
36         /* p->prio = p->normal_prio = p->static_prio */
37         p->prio = p->normal_prio = __normal_prio(p);
38         /* 設置進程權重 */
39         set_load_weight(p);
40 
41          /* sched_reset_on_fork成員在之后已經不需要使用了,直接設為0 */
42         p->sched_reset_on_fork = 0;
43     }
44 
45     if (dl_prio(p->prio)) {
46         /* 使能搶占 */
47         put_cpu();
48         /* 返回錯誤 */
49         return -EAGAIN;
50     } else if (rt_prio(p->prio)) {
51         /* 根據優先級判斷,如果是實時進程,設置其調度類為rt_sched_class */
52         p->sched_class = &rt_sched_class;
53     } else {
54         /* 如果是普通進程,設置其調度類為fair_sched_class */
55         p->sched_class = &fair_sched_class;
56     }
57     /* 調用調用類的task_fork函數 */
58     if (p->sched_class->task_fork)
59         p->sched_class->task_fork(p);
60 
61     /*
62      * The child is not yet in the pid-hash so no cgroup attach races,
63      * and the cgroup is pinned to this child due to cgroup_fork()
64      * is ran before sched_fork().
65      *
66      * Silence PROVE_RCU.
67      */
68     raw_spin_lock_irqsave(&p->pi_lock, flags);
69     /* 設置新進程的CPU為當前CPU */
70     set_task_cpu(p, cpu);
71     raw_spin_unlock_irqrestore(&p->pi_lock, flags);
72 
73 #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
74     if (likely(sched_info_on()))
75         memset(&p->sched_info, 0, sizeof(p->sched_info));
76 #endif
77 #if defined(CONFIG_SMP)
78     p->on_cpu = 0;
79 #endif
80     /* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
81     /* 初始化該進程為內核禁止搶占 */
82     init_task_preempt_count(p);
83 #ifdef CONFIG_SMP
84     plist_node_init(&p->pushable_tasks, MAX_PRIO);
85     RB_CLEAR_NODE(&p->pushable_dl_tasks);
86 #endif
87     /* 使能搶占 */
88     put_cpu();
89     return 0;
90 }

   在sched_fork()函數中,主要工作如下:

  • 獲取當前CPU號
  • 禁止內核搶占(這里基本就是關閉了搶占,因為執行到這里已經是內核態,又禁止了被搶占)
  • 初始化進程p的一些變量(實時進程和普通進程通用的那些變量)
  • 設置進程p的狀態為TASK_RUNNING(這一步很關鍵,因為只有處於TASK_RUNNING狀態下的進程才會被調度器放入隊列中)
  • 根據父進程和clone_flags參數設置進程p的優先級和權重。
  • 根據進程p的優先級設置其調度類(實時進程優先級:0~99  普通進程優先級:100~139)
  • 根據調度類進行進程p類型相關的初始化(這里就實現了實時進程和普通進程獨有的變量進行初始化)
  • 設置進程p的當前CPU為此CPU。
  • 初始化進程p禁止內核搶占(因為當CPU執行到進程p時,進程p還需要進行一些初始化)
  • 使能內核搶占

  可以看出sched_fork()進行的初始化也比較簡單,需要注意的是不同類型的進程會使用不同的調度類,並且也會調用調度類中的初始化函數。在實時進程的調度類中是沒有特定的task_fork()函數的,而普通進程使用cfs策略時會調用到task_fork_fair()函數,我們具體看看實現:

 1 static void task_fork_fair(struct task_struct *p)
 2 {
 3     struct cfs_rq *cfs_rq;
 4     
 5     /* 進程p的調度實體se */
 6     struct sched_entity *se = &p->se, *curr;
 7     
 8     /* 獲取當前CPU */
 9     int this_cpu = smp_processor_id();
10     
11     /* 獲取此CPU的運行隊列 */
12     struct rq *rq = this_rq();
13     unsigned long flags;
14     
15     /* 上鎖並保存中斷記錄 */
16     raw_spin_lock_irqsave(&rq->lock, flags);
17     
18     /* 更新rq運行時間 */
19     update_rq_clock(rq);
20     
21     /* cfs_rq = current->se.cfs_rq; */
22     cfs_rq = task_cfs_rq(current);
23     
24     /* 設置當前進程所在隊列為父進程所在隊列 */
25     curr = cfs_rq->curr;
26 
27     /*
28      * Not only the cpu but also the task_group of the parent might have
29      * been changed after parent->se.parent,cfs_rq were copied to
30      * child->se.parent,cfs_rq. So call __set_task_cpu() to make those
31      * of child point to valid ones.
32      */
33     rcu_read_lock();
34     /* 設置此進程所屬CPU */
35     __set_task_cpu(p, this_cpu);
36     rcu_read_unlock();
37 
38     /* 更新當前進程運行時間 */
39     update_curr(cfs_rq);
40 
41     if (curr)
42         /* 將父進程的虛擬運行時間賦給了新進程的虛擬運行時間 */
43         se->vruntime = curr->vruntime;
44     /* 調整了se的虛擬運行時間 */
45     place_entity(cfs_rq, se, 1);
46 
47     if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
48         /*
49          * Upon rescheduling, sched_class::put_prev_task() will place
50          * 'current' within the tree based on its new key value.
51          */
52         swap(curr->vruntime, se->vruntime);
53         resched_curr(rq);
54     }
55 
56     /* 保證了進程p的vruntime是運行隊列中最小的(這里占時不確定是不是這個用法,不過確實是最小的了) */
57     se->vruntime -= cfs_rq->min_vruntime;
58     
59     /* 解鎖,還原中斷記錄 */
60     raw_spin_unlock_irqrestore(&rq->lock, flags);
61 }

  在task_fork_fair()函數中主要就是設置進程p的虛擬運行時間和所處的cfs隊列,值得我們注意的是 cfs_rq = task_cfs_rq(current); 這一行,在注釋中已經表明task_cfs_rq(current)返回的是current的se.cfs_rq,注意se.cfs_rq保存的並不是根cfs隊列,而是所處的cfs_rq,也就是如果父進程處於一個進程組的cfs_rq中,新創建的進程也會處於這個進程組的cfs_rq中。

 

wake_up_new_task()

  到這里新進程關於調度的初始化已經完成,但是還沒有被調度器加入到隊列中,其是在do_fork()中的wake_up_new_task(p);中加入到隊列中的,我們具體看看wake_up_new_task()的實現:

 1 void wake_up_new_task(struct task_struct *p)
 2 {
 3     unsigned long flags;
 4     struct rq *rq;
 5 
 6     raw_spin_lock_irqsave(&p->pi_lock, flags);
 7 #ifdef CONFIG_SMP
 8     /*
 9      * Fork balancing, do it here and not earlier because:
10      *  - cpus_allowed can change in the fork path
11      *  - any previously selected cpu might disappear through hotplug
12      */
13      /* 為進程選擇一個合適的CPU */
14     set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
15 #endif
16 
17     /* Initialize new task's runnable average */
18     /* 這里是跟多核負載均衡有關 */
19     init_task_runnable_average(p);
20     /* 上鎖 */
21     rq = __task_rq_lock(p);
22     /* 將進程加入到CPU的運行隊列 */
23     activate_task(rq, p, 0);
24     /* 標記進程p處於隊列中 */
25     p->on_rq = TASK_ON_RQ_QUEUED;
26     /* 跟調試有關 */
27     trace_sched_wakeup_new(p, true);
28     /* 檢查是否需要切換當前進程 */
29     check_preempt_curr(rq, p, WF_FORK);
30 #ifdef CONFIG_SMP
31     if (p->sched_class->task_woken)
32         p->sched_class->task_woken(rq, p);
33 #endif
34     task_rq_unlock(rq, p, &flags);
35 }

  在wake_up_new_task()函數中,將進程加入到運行隊列的函數為activate_task(),而activate_task()函數最后會調用到新進程調度類中的enqueue_task指針所指函數,這里我們具體看一下cfs調度類的enqueue_task指針所指函數enqueue_task_fair():

 1 static void
 2 enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
 3 {
 4     struct cfs_rq *cfs_rq;
 5     struct sched_entity *se = &p->se;
 6 
 7     /* 這里是一個迭代,我們知道,進程有可能是處於一個進程組中的,所以當這個處於進程組中的進程加入到該進程組的隊列中時,要對此隊列向上迭代 */
 8     for_each_sched_entity(se) {
 9         if (se->on_rq)
10             break;
11         /* 如果不是CONFIG_FAIR_GROUP_SCHED,獲取其所在CPU的rq運行隊列的cfs_rq運行隊列
12          * 如果是CONFIG_FAIR_GROUP_SCHED,獲取其所在的cfs_rq運行隊列
13          */
14         cfs_rq = cfs_rq_of(se);
15         /* 加入到隊列中 */
16         enqueue_entity(cfs_rq, se, flags);
17 
18         /*
19          * end evaluation on encountering a throttled cfs_rq
20          *
21          * note: in the case of encountering a throttled cfs_rq we will
22          * post the final h_nr_running increment below.
23         */
24         if (cfs_rq_throttled(cfs_rq))
25             break;
26         cfs_rq->h_nr_running++;
27 
28         flags = ENQUEUE_WAKEUP;
29     }
30 
31     /* 只有se不處於隊列中或者cfs_rq_throttled(cfs_rq)返回真才會運行這個循環 */
32     for_each_sched_entity(se) {
33         cfs_rq = cfs_rq_of(se);
34         cfs_rq->h_nr_running++;
35 
36         if (cfs_rq_throttled(cfs_rq))
37             break;
38 
39         update_cfs_shares(cfs_rq);
40         update_entity_load_avg(se, 1);
41     }
42 
43     if (!se) {
44         update_rq_runnable_avg(rq, rq->nr_running);
45         /* 當前CPU運行隊列活動進程數 + 1 */
46         add_nr_running(rq, 1);
47     }
48     /* 設置下次調度中斷發生時間 */
49     hrtick_update(rq);
50 }

  在enqueue_task_fair()函數中又使用了enqueue_entity()函數進行操作,如下:

 1 static void
 2 enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
 3 {
 4     /*
 5      * Update the normalized vruntime before updating min_vruntime
 6      * through calling update_curr().
 7      */
 8     if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
 9         se->vruntime += cfs_rq->min_vruntime;
10 
11     /*
12      * Update run-time statistics of the 'current'.
13      */
14     /* 更新當前進程運行時間和虛擬運行時間 */
15     update_curr(cfs_rq);
16     enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
17     /* 更新cfs_rq隊列總權重(就是在原有基礎上加上se的權重) */
18     account_entity_enqueue(cfs_rq, se);
19     update_cfs_shares(cfs_rq);
20 
21     /* 新建的進程flags為0,不會執行這里 */
22     if (flags & ENQUEUE_WAKEUP) {
23         place_entity(cfs_rq, se, 0);
24         enqueue_sleeper(cfs_rq, se);
25     }
26 
27     update_stats_enqueue(cfs_rq, se);
28     check_spread(cfs_rq, se);
29     
30     /* 將se插入到運行隊列cfs_rq的紅黑樹中 */
31     if (se != cfs_rq->curr)
32         __enqueue_entity(cfs_rq, se);
33     /* 將se的on_rq標記為1 */
34     se->on_rq = 1;
35 
36     /* 如果cfs_rq的隊列中只有一個進程,這里做處理 */
37     if (cfs_rq->nr_running == 1) {
38         list_add_leaf_cfs_rq(cfs_rq);
39         check_enqueue_throttle(cfs_rq);
40     }
41 }

 

總結

  需要注意的幾點:

  • 新創建的進程先會進行調度相關的結構體和變量初始化,其中會根據不同的類型進行不同的調度類操作,此時並沒有加入到隊列中。
  • 當新進程創建完畢后,它的父進程會將其運行狀態置為TASK_RUNNING,並加入到運行隊列中。
  • 加入運行隊列時系統會根據CPU的負載情況放入不同的CPU隊列中。


免責聲明!

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



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