本文為原創,轉載請注明: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隊列中。