這篇文章主要說明系統如何把一個進程加入到隊列中。
1 加入時機
只有處於TASK_RUNNING狀態下的進程才能夠加入到調度器,其他狀態都不行,也就說明了,當一個進程處於睡眠、掛起狀態的時候是不存在於調度器中的,而進程加入調度器的時機如下:
- 當進程創建完成時,進程剛創建完成時,即使它運行起來立即調用sleep()進程睡眠,它也必定先會加入到調度器,因爲實際上它加入調度器後自己還需要進行一定的初始化和操作,纔會調用到我們的“立即”sleep()。
- 當進程被喚醒時,也使用sleep的例子說明,我們平常寫程序使用的sleep()函數實現原理就是通過系統調用將進程狀態改爲TASK_INTERRUPTIBLE,然後移出運行隊列,並且啓動一個定時器,在定時器到期後喚醒進程,再重新放入運行隊列。
2 調度初始化sched_fork
linux創建進程或線程的時候會調用copy_process這個函數,而裏面有一個函數專門用於進程調度的初始化,就是sched_fork(),其代碼如下
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)
/* 根據默認nice值設置靜態優先級 */
p->static_prio = NICE_TO_PRIO(0);
/* p->prio = p->normal_prio = p->static_prio,初始化時設置的默認優先級爲120 */
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;
#endif
/* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
/* 初始化該進程爲內核禁止搶佔 */
init_task_preempt_count(p); //preempt_count不爲0則禁止搶佔
#ifdef CONFIG_SMP
plist_node_init(&p->pushable_tasks, MAX_PRIO);
RB_CLEAR_NODE(&p->pushable_dl_tasks);
#endif
/* 使能搶佔 */
put_cpu(); //每次使能搶佔時都會檢查,當前任務是否能被搶佔,如果可以直接調用schedule
return 0;
}
看一下set_load_weight函數,可以看到weight是根據nice值來設定的,每個nice值對應一個weight,而inv_weight爲weight的倒數,用於內核中方便計算使用:
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
static void set_load_weight(struct task_struct *p)
{
int prio = p->static_prio - MAX_RT_PRIO;
struct load_weight *load = &p->se.load;
/*
* SCHED_IDLE tasks get minimal weight:
*/
if (p->policy == SCHED_IDLE) {
load->weight = scale_load(WEIGHT_IDLEPRIO);
load->inv_weight = WMULT_IDLEPRIO;
return;
}
load->weight = scale_load(prio_to_weight[prio]);
load->inv_weight = prio_to_wmult[prio];
}
在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()函數,我們具體看看實現:
2.1 普通非實時進程task_fork
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();
unsigned long flags;
/* 上鎖並保存中斷記錄 */
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中。
上面函數只是初始化了當前新建task調度相關的結構,但是還沒有真正把該task放入調度隊列。其中還有幾個比較重要的跟調度時間相關的函數。
update_rq_clock
void update_rq_clock(struct rq *rq)
{
s64 delta;
if (rq->skip_clock_update > 0)
return;
delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
rq->clock += delta;
update_rq_clock_task(rq, delta);
}
一個tick對應一個clock,sched_clock_cpu獲取系統運行至今的tick計數。該函數更新rq->clock和rq->clock_task,其中rq->clock記錄總的tick計數,而clock_task會減去中斷佔用的tick計數,是真正進程執行所使用的tick計數。update_rq_clock_task去更新clock_task。
update_curr
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_of(cfs_rq)->clock_task;
unsigned long delta_exec;
if (unlikely(!curr))
return;
/*
* Get the amount of time the current task was running
* since the last time we changed load (this cannot
* overflow on 32 bits):
*/
delta_exec = (unsigned long)(now - curr->exec_start);
if (!delta_exec)
return;
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
curr->exec_start記錄當前進程最近一次執行的時間點。
curr->sum_exec_runtime記錄當前進程運行的總的時間。
__update_curr()函數主要完成了三個任務:1.更新當前進程的實際運行時間(抽象模型中的runtime)。2.更新當前進程的虛擬時間vruntime。3.更新cfs_rq->min_vruntime。
place_entity
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime = cfs_rq->min_vruntime;
/*
* The 'current' period is already promised to the current tasks,
* however the extra weight of the new task will slow them down a
* little, place the new task so that it fits in the slot that
* stays open at the end.
*/
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice(cfs_rq, se);
/* sleeps up to a single latency don't count. */
if (!initial) {
unsigned long thresh = sysctl_sched_latency;
/*
* Halve their sleep time's effect, to allow
* for a gentler effect of sleepers:
*/
if (sched_feat(GENTLE_FAIR_SLEEPERS))
thresh >>= 1;
vruntime -= thresh;
}
/* ensure we never gain time by being placed backwards. */
se->vruntime = max_vruntime(se->vruntime, vruntime);
}
place_entity()函數的功能是調整進程的虛擬時間。當新進程被創建或者進程被喚醒時都需要調整它的vruntime值
3 wake_up_new_task
到這裏新進程關於調度的初始化已經完成,但是還沒有被調度器加入到隊列中,其是在do_fork()中的wake_up_new_task(p);中加入到隊列中的,我們具體看看wake_up_new_task()的實現:
void wake_up_new_task(struct task_struct *p)
{
unsigned long flags;
struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, flags);
#ifdef CONFIG_SMP
/*
* 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));
#endif
/* 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);
/* 檢查是否需要切換當前進程,如果需要則設置當前線程的調度flag */
check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
if (p->sched_class->task_woken)
p->sched_class->task_woken(rq, p);
#endif
task_rq_unlock(rq, p, &flags);
}
在wake_up_new_task()函數中,將進程加入到運行隊列的函數爲activate_task()。
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
if (task_contributes_to_load(p))
rq->nr_uninterruptible--;
enqueue_task(rq, p, flags);
}
static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
update_rq_clock(rq);
sched_info_queued(p);
p->sched_class->enqueue_task(rq, p, flags);
}
可以看到,最後根據調度類的不同,會使用具體調度類的enqueue_task函數加入到調度隊列中。
3.1 cfs調度類的enqueue_task
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
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))
break;
cfs_rq->h_nr_running++;
flags = ENQUEUE_WAKEUP;
}
/* 只有se不處於隊列中或者cfs_rq_throttled(cfs_rq)返回真纔會運行這個循環 */
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
cfs_rq->h_nr_running++;
if (cfs_rq_throttled(cfs_rq))
break;
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()函數進行操作,如下:
static void
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'.
*/
/* 更新當前進程運行時間和虛擬運行時間 */
update_curr(cfs_rq);
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);
}
}
看一下__enqueue_entity
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
int leftmost = 1;
/*
* Find the right place in the rbtree:
*/
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* We dont care about collisions. Nodes with
* the same key stay together.
*/
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = 0;
}
}
/*
* Maintain a cache of leftmost tree entries (it is frequently
* used):
*/
if (leftmost) //更新rb_leftmost節點,後面選擇下一個需要調度的fair task的時候會使用
cfs_rq->rb_leftmost = &se->run_node;
rb_link_node(&se->run_node, parent, link);
rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}
3.2 rt調度類的enqueue_task
static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
struct sched_rt_entity *rt_se = &p->rt;
if (flags & ENQUEUE_WAKEUP)
rt_se->timeout = 0;
enqueue_rt_entity(rt_se, flags & ENQUEUE_HEAD);
if (!task_current(rq, p) && p->nr_cpus_allowed > 1)
enqueue_pushable_task(rq, p);
inc_nr_running(rq);
}
static void enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
dequeue_rt_stack(rt_se); //先dequeue,因爲這邊本來就不再隊列裏,所以什麼都沒做
for_each_sched_rt_entity(rt_se)
__enqueue_rt_entity(rt_se, head);
}
__enqueue_rt_entity函數是真正加入rt隊列的函數:
static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
struct rt_prio_array *array = &rt_rq->active;
struct rt_rq *group_rq = group_rt_rq(rt_se);
struct list_head *queue = array->queue + rt_se_prio(rt_se);//根據優先級值算出要放入的hash表的queue
/*
* Don't enqueue the group if its throttled, or when empty.
* The latter is a consequence of the former when a child group
* get throttled and the current group doesn't have any other
* active members.
*/
if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running))
return;
if (!rt_rq->rt_nr_running)
list_add_leaf_rt_rq(rt_rq);
if (head)
list_add(&rt_se->run_list, queue);
else
list_add_tail(&rt_se->run_list, queue);//插入queue中
__set_bit(rt_se_prio(rt_se), array->bitmap); //並把該hash表對應的位圖置1
inc_rt_tasks(rt_se, rt_rq);
}