之前的文章已經介紹了排程器已經初始化完成,現在隻需要加入一個周期定時器tick驅動它進行周期排程即可,而加入定時器tick在下一篇文章進行簡單說明(主要這部分涉及排程器比較少,更多的是時鐘、定時器相關知識)。這篇文章主要說明系統如何把一個程序加入到隊列中。
之前的文章也有提到過,隻有處于TASK_RUNNING狀态下的程序才能夠加入到排程器,其他狀态都不行,也就說明了,當一個程序處于睡眠、挂起狀态的時候是不存在于排程器中的,而程序加入排程器的時機如下:
當程序建立完成時,程序剛建立完成時,即使它運作起來立即調用sleep()程序睡眠,它也必定先會加入到排程器,因為實際上它加入排程器後自己還需要進行一定的初始化和操作,才會調用到我們的“立即”sleep()。
當程序被喚醒時,也使用sleep的例子說明,我們平常寫程式使用的sleep()函數實作原理就是通過系統調用将程序狀态改為TASK_INTERRUPTIBLE,然後移出運作隊列,并且啟動一個定時器,在定時器到期後喚醒程序,再重新放入運作隊列。
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
unsigned long flags;
/* 擷取目前CPU,并且禁止搶占 */
int cpu = get_cpu();
/* 初始化跟排程相關的值,比如排程實體,運作時間等 */
__sched_fork(clone_flags, p);
/*
* 标記為運作狀态,表明此程序正在運作或準備好運作,實際上沒有真正在CPU上運作,這裡隻是導緻了外部信号和事件不能夠喚醒此程序,之後将它插入到運作隊列中
*/
p->state = TASK_RUNNING;
* 根據父程序的運作優先級設定設定程序的優先級
p->prio = current->normal_prio;
* 更新該程序優先級
/* 如果需要重新設定優先級 */
if (unlikely(p->sched_reset_on_fork)) {
/* 如果是dl排程或者實時排程 */
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
/* 排程政策為SCHED_NORMAL,這個選項将使用CFS排程 */
p->policy = SCHED_NORMAL;
/* 根據預設nice值設定靜态優先級 */
p->static_prio = NICE_TO_PRIO(0);
/* 實時優先級為0 */
p->rt_priority = 0;
} else if (PRIO_TO_NICE(p->static_prio) 0)
/* p->prio = p->normal_prio = p->static_prio */
p->prio = p->normal_prio = __normal_prio(p);
/* 設定程序權重 */
set_load_weight(p);
/* sched_reset_on_fork成員在之後已經不需要使用了,直接設為0 */
p->sched_reset_on_fork = 0;
}
if (dl_prio(p->prio)) {
/* 使能搶占 */
put_cpu();
/* 傳回錯誤 */
return -EAGAIN;
} else if (rt_prio(p->prio)) {
/* 根據優先級判斷,如果是實時程序,設定其排程類為rt_sched_class */
p->sched_class = &rt_sched_class;
} else {
/* 如果是普通程序,設定其排程類為fair_sched_class */
p->sched_class = &fair_sched_class;
/* 調用調用類的task_fork函數 */
if (p->sched_class->task_fork)
p->sched_class->task_fork(p);
* The child is not yet in the pid-hash so no cgroup attach races,
* and the cgroup is pinned to this child due to cgroup_fork()
* is ran before sched_fork().
*
* Silence PROVE_RCU.
raw_spin_lock_irqsave(&p->pi_lock, flags);
/* 設定新程序的CPU為目前CPU */
set_task_cpu(p, cpu);
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
if (likely(sched_info_on()))
memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#if defined(CONFIG_SMP)
p->on_cpu = 0;
/* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
/* 初始化該程序為核心禁止搶占 */
init_task_preempt_count(p);
#ifdef CONFIG_SMP
plist_node_init(&p->pushable_tasks, MAX_PRIO);
RB_CLEAR_NODE(&p->pushable_dl_tasks);
/* 使能搶占 */
put_cpu();
return 0;
}
在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()函數,我們具體看看實作:
static void task_fork_fair(struct task_struct *p)
struct cfs_rq *cfs_rq;
/* 程序p的排程實體se */
struct sched_entity *se = &p->se, *curr;
/* 擷取目前CPU */
int this_cpu = smp_processor_id();
/* 擷取此CPU的運作隊列 */
struct rq *rq = this_rq();
/* 上鎖并儲存中斷記錄 */
raw_spin_lock_irqsave(&rq->lock, flags);
/* 更新rq運作時間 */
update_rq_clock(rq);
/* cfs_rq = current->se.cfs_rq; */
cfs_rq = task_cfs_rq(current);
/* 設定目前程序所在隊列為父程序所在隊列 */
curr = cfs_rq->curr;
* Not only the cpu but also the task_group of the parent might have
* been changed after parent->se.parent,cfs_rq were copied to
* child->se.parent,cfs_rq. So call __set_task_cpu() to make those
* of child point to valid ones.
rcu_read_lock();
/* 設定此程序所屬CPU */
__set_task_cpu(p, this_cpu);
rcu_read_unlock();
/* 更新目前程序運作時間 */
update_curr(cfs_rq);
if (curr)
/* 将父程序的虛拟運作時間賦給了新程序的虛拟運作時間 */
se->vruntime = curr->vruntime;
/* 調整了se的虛拟運作時間 */
place_entity(cfs_rq, se, 1);
if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
/*
* Upon rescheduling, sched_class::put_prev_task() will place
* 'current' within the tree based on its new key value.
*/
swap(curr->vruntime, se->vruntime);
resched_curr(rq);
/* 保證了程序p的vruntime是運作隊列中最小的(這裡占時不确定是不是這個用法,不過确實是最小的了) */
se->vruntime -= cfs_rq->min_vruntime;
/* 解鎖,還原中斷記錄 */
raw_spin_unlock_irqrestore(&rq->lock, flags);
在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中。
到這裡新程序關于排程的初始化已經完成,但是還沒有被排程器加入到隊列中,其是在do_fork()中的wake_up_new_task(p);中加入到隊列中的,我們具體看看wake_up_new_task()的實作:
void wake_up_new_task(struct task_struct *p)
struct rq *rq;
* Fork balancing, do it here and not earlier because:
* - cpus_allowed can change in the fork path
* - any previously selected cpu might disappear through hotplug
/* 為程序選擇一個合适的CPU */
set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
/* Initialize new task's runnable average */
/* 這裡是跟多核負載均衡有關 */
init_task_runnable_average(p);
/* 上鎖 */
rq = __task_rq_lock(p);
/* 将程序加入到CPU的運作隊列 */
activate_task(rq, p, 0);
/* 标記程序p處于隊列中 */
p->on_rq = TASK_ON_RQ_QUEUED;
/* 跟調試有關 */
trace_sched_wakeup_new(p, true);
/* 檢查是否需要切換目前程序 */
check_preempt_curr(rq, p, WF_FORK);
if (p->sched_class->task_woken)
p->sched_class->task_woken(rq, p);
task_rq_unlock(rq, p, &flags);
在wake_up_new_task()函數中,将程序加入到運作隊列的函數為activate_task(),而activate_task()函數最後會調用到新程序排程類中的enqueue_task指針所指函數,這裡我們具體看一下cfs排程類的enqueue_task指針所指函數enqueue_task_fair():
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
struct sched_entity *se = &p->se;
/* 這裡是一個疊代,我們知道,程序有可能是處于一個程序組中的,是以當這個處于程序組中的程序加入到該程序組的隊列中時,要對此隊列向上疊代 */
for_each_sched_entity(se) {
if (se->on_rq)
break;
/* 如果不是CONFIG_FAIR_GROUP_SCHED,擷取其所在CPU的rq運作隊列的cfs_rq運作隊列
* 如果是CONFIG_FAIR_GROUP_SCHED,擷取其所在的cfs_rq運作隊列
cfs_rq = cfs_rq_of(se);
/* 加入到隊列中 */
enqueue_entity(cfs_rq, se, flags);
* end evaluation on encountering a throttled cfs_rq
*
* note: in the case of encountering a throttled cfs_rq we will
* post the final h_nr_running increment below.
*/
if (cfs_rq_throttled(cfs_rq))
cfs_rq->h_nr_running++;
flags = ENQUEUE_WAKEUP;
/* 隻有se不處于隊列中或者cfs_rq_throttled(cfs_rq)傳回真才會運作這個循環 */
update_cfs_shares(cfs_rq);
update_entity_load_avg(se, 1);
if (!se) {
update_rq_runnable_avg(rq, rq->nr_running);
/* 目前CPU運作隊列活動程序數 + 1 */
add_nr_running(rq, 1);
/* 設定下次排程中斷發生時間 */
hrtick_update(rq);
在enqueue_task_fair()函數中又使用了enqueue_entity()函數進行操作,如下:
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
* Update the normalized vruntime before updating min_vruntime
* through calling update_curr().
if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
se->vruntime += cfs_rq->min_vruntime;
* Update run-time statistics of the 'current'.
/* 更新目前程序運作時間和虛拟運作時間 */
enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
/* 更新cfs_rq隊列總權重(就是在原有基礎上加上se的權重) */
account_entity_enqueue(cfs_rq, se);
update_cfs_shares(cfs_rq);
/* 建立的程序flags為0,不會執行這裡 */
if (flags & ENQUEUE_WAKEUP) {
place_entity(cfs_rq, se, 0);
enqueue_sleeper(cfs_rq, se);
update_stats_enqueue(cfs_rq, se);
check_spread(cfs_rq, se);
/* 将se插入到運作隊列cfs_rq的紅黑樹中 */
if (se != cfs_rq->curr)
__enqueue_entity(cfs_rq, se);
/* 将se的on_rq标記為1 */
se->on_rq = 1;
/* 如果cfs_rq的隊列中隻有一個程序,這裡做處理 */
if (cfs_rq->nr_running == 1) {
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle(cfs_rq);
需要注意的幾點:
新建立的程序先會進行排程相關的結構體和變量初始化,其中會根據不同的類型進行不同的排程類操作,此時并沒有加入到隊列中。
當新程序建立完畢後,它的父程序會将其運作狀态置為TASK_RUNNING,并加入到運作隊列中。
加入運作隊列時系統會根據CPU的負載情況放入不同的CPU隊列中。