Linux 內(nèi)核調(diào)度器源碼解析:從調(diào)度入口到挑選下一個進程
在Linux內(nèi)核中,調(diào)度器(scheduler)扮演著至關重要的角色,決定了哪個進程將獲得CPU的執(zhí)行時間。本文將深入剖析內(nèi)核中調(diào)度器的代碼實現(xiàn),從入口函數(shù)開始,一步步分析如何選擇下一個要執(zhí)行的進程。讓我們一同揭開這個內(nèi)核之謎。
調(diào)度器入口
Linux調(diào)度器入口函數(shù)定義在kernel/sched/core.c中:
asmlinkage __visible void __sched schedule(void)
{
// 獲取當前任務結(jié)構體的指針
struct task_struct *tsk = current;
// 將任務提交到調(diào)度工作隊列中
sched_submit_work(tsk);
// 進入調(diào)度循環(huán),直到?jīng)]有需要被調(diào)度的任務
do {
// 禁用搶占
preempt_disable();
// 調(diào)用實際的調(diào)度函數(shù) __schedule,并傳入調(diào)度策略參數(shù) SM_NONE
__schedule(SM_NONE);
// 啟用搶占,但不進行重新調(diào)度
sched_preempt_enable_no_resched();
} while (need_resched()); // 循環(huán)直到?jīng)]有需要重新調(diào)度的任務
// 更新工作隊列中的任務狀態(tài)
sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);
調(diào)度器的入口函數(shù)是schedule,首先獲取當前任務結(jié)構體的指針,然后將任務提交到調(diào)度工作隊列中,接著進入一個循環(huán),該循環(huán)會禁用搶占,調(diào)用實際的調(diào)度函數(shù)__schedule,并在循環(huán)結(jié)束后啟用搶占。循環(huán)會一直執(zhí)行,直到?jīng)]有需要重新調(diào)度的任務為止。最后,函數(shù)會更新工作隊列中任務的狀態(tài)。函數(shù)最后export導出schedule函數(shù)以供其他部分使用。
static void __sched __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
prev = current;
rq = this_rq();
switch_count = &prev->nivcsw;
// 獲取下一個要運行的進程
next = pick_next_task(rq);
// 切換到下一個進程
context_switch(rq, prev, next, switch_count);
// 如果需要搶占,啟用搶占
if (preempt)
need_resched();
}
} 這里,__schedule函數(shù)負責實際的調(diào)度操作。首先,它獲取了當前任務結(jié)構體的指針(prev)、運行隊列(rq)以及切換計數(shù)器(switch_count)。然后,通過調(diào)用pick_next_task函數(shù),它選擇下一個要運行的進程(next)。最后,通過context_switch函數(shù),它進行進程切換,將CPU控制權移交給下一個進程。
具體如何挑選下一個需要運行的進程,就要扒開pick_next_task函數(shù)。
pick_next_task
/*
* 選擇下一個要運行的任務。
*/
static inline struct task_struct *
__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class; // 定義調(diào)度類指針
struct task_struct *p; // 定義任務結(jié)構體指針
// 優(yōu)化:如果前一個任務是公平調(diào)度類中的任務,且運行隊列中的任務數(shù)與CFS隊列中的任務數(shù)相等,
// 則可以直接選擇下一個公平類任務,因為其他調(diào)度類的任務無法搶占CPU。
if (likely(!sched_class_above(prev->sched_class, &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = pick_next_task_fair(rq, prev, rf); // 選擇下一個公平調(diào)度類任務
if (unlikely(p == RETRY_TASK)) // 如果選擇任務失敗,需要重新嘗試
goto restart;
if (!p) {
put_prev_task(rq, prev);
p = pick_next_task_idle(rq); // 如果沒有可運行任務,則選擇下一個空轉(zhuǎn)調(diào)度類任務
}
return p;
}
restart:
put_prev_task_balance(rq, prev, rf); // 將前一個任務放回隊列,進行重新平衡
// 遍歷所有調(diào)度類
for_each_class(class) {
p = class->pick_next_task(rq); // 選擇下一個任務
if (p)
return p;
}
BUG(); // 如果沒有可運行任務,引發(fā)BUG??辙D(zhuǎn)類應該始終有可運行的任務。
}
這段代碼是用于選擇下一個要運行的任務的函數(shù)。首先,它檢查是否可以優(yōu)化選擇下一個任務,如果前一個任務是公平調(diào)度類中的任務,并且運行隊列中的任務數(shù)與CFS隊列中的任務數(shù)相等,就可以直接選擇下一個公平調(diào)度類任務。如果選擇任務失敗,會重新嘗試,然后如果沒有可運行任務,將選擇下一個空轉(zhuǎn)調(diào)度類任務。如果不滿足優(yōu)化條件,將會重新平衡隊列,然后遍歷所有的調(diào)度類,選擇下一個任務。如果沒有可運行任務,將引發(fā)BUG,因為空轉(zhuǎn)類應該始終有可運行的任務。
struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
struct cfs_rq *cfs_rq = &rq->cfs; // 獲取CFS隊列
struct sched_entity *se; // 定義調(diào)度實體指針
struct task_struct *p; // 定義任務結(jié)構體指針
int new_tasks;
again:
// 如果沒有可運行的公平調(diào)度任務,跳轉(zhuǎn)到idle標簽
if (!sched_fair_runnable(rq))
goto idle;
#ifdef CONFIG_FAIR_GROUP_SCHED
// 如果沒有前一個任務,或者前一個任務不屬于公平調(diào)度類,跳轉(zhuǎn)到simple標簽
if (!prev || prev->sched_class != &fair_sched_class)
goto simple;
do {
struct sched_entity *curr = cfs_rq->curr;
// 如果當前任務存在
if (curr) {
// 如果當前任務在隊列上,則更新其運行時間
if (curr->on_rq)
update_curr(cfs_rq);
else
curr = NULL;
// 如果CFS隊列的運行時間不正常,跳轉(zhuǎn)到idle標簽
if (unlikely(check_cfs_rq_runtime(cfs_rq))) {
cfs_rq = &rq->cfs;
// 如果沒有可運行任務,跳轉(zhuǎn)到idle標簽
if (!cfs_rq->nr_running)
goto idle;
goto simple;
}
}
// 選擇下一個調(diào)度實體,并切換到相應的CFS隊列
se = pick_next_entity(cfs_rq, curr);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
// 獲取與選定實體關聯(lián)的任務結(jié)構體
p = task_of(se);
// 如果前一個任務不等于選定任務,進行任務切換
if (prev != p) {
struct sched_entity *pse = &prev->se;
while (!(cfs_rq = is_same_group(se, pse))) {
int se_depth = se->depth;
int pse_depth = pse->depth;
if (se_depth <= pse_depth) {
put_prev_entity(cfs_rq_of(pse), pse);
pse = parent_entity(pse);
}
if (se_depth >= pse_depth) {
set_next_entity(cfs_rq_of(se), se);
se = parent_entity(se);
}
}
put_prev_entity(cfs_rq, pse);
set_next_entity(cfs_rq, se);
}
goto done;
simple:
#endif
// 如果有前一個任務,將其放回隊列
if (prev)
put_prev_task(rq, prev);
do {
// 選擇下一個調(diào)度實體,并切換到相應的CFS隊列
se = pick_next_entity(cfs_rq, NULL);
set_next_entity(cfs_rq, se);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
// 獲取與選定實體關聯(lián)的任務結(jié)構體
p = task_of(se);
done: __maybe_unused;
#ifdef CONFIG_SMP
// 將下一個正在運行的任務移動到隊列的前面
list_move(&p->se.group_node, &rq->cfs_tasks);
#endif
// 如果啟用高精度定時器,開始高精度定時
if (hrtick_enabled_fair(rq))
hrtick_start_fair(rq, p);
// 更新不適合運行的任務狀態(tài)
update_misfit_status(p, rq);
return p;
idle:
// 如果沒有rf標志,返回NULL
if (!rf)
return NULL;
// 嘗試進行新的空閑平衡操作
new_tasks = newidle_balance(rq, rf);
// 如果新的平衡操作失敗,返回RETRY_TASK標志
if (new_tasks < 0)
return RETRY_TASK;
// 如果有新的可運行任務,回到again標簽重新選擇
if (new_tasks > 0)
goto again;
// 如果隊列即將變?yōu)榭臻e狀態(tài),檢查是否需要更新時鐘pelt的lost_idle_time
update_idle_rq_clock_pelt(rq);
return NULL;
}
這個函數(shù)用于選擇下一個要在公平調(diào)度類中運行的任務。函數(shù)中包含了條件判斷和循環(huán),以確保選擇最適合的任務。
/*
* 選擇下一個調(diào)度實體,考慮以下因素,按照順序:
* 1) 在進程/任務組之間保持公平性
* 2) 選擇“下一個”進程,因為某個進程確實希望運行
* 3) 選擇“上一個”進程,以提高緩存局部性
* 4) 如果其他任務可用,則不運行“跳過”的進程
*/
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq); // 獲取最左邊的實體
struct sched_entity *se;
/*
* 如果 curr 被設置,我們必須查看它是否位于樹中最左邊的實體的左側(cè),
* 前提是樹中確實有實體存在。
*/
if (!left || (curr && entity_before(curr, left)))
left = curr;
se = left; /* 理想情況下,我們運行最左邊的實體 */
/*
* 避免運行跳過的實體,如果可以不運行其他實體而不會太不公平。
*/
if (cfs_rq->skip && cfs_rq->skip == se) {
struct sched_entity *second;
if (se == curr) {
second = __pick_first_entity(cfs_rq); // 獲取最左邊的實體
} else {
second = __pick_next_entity(se); // 獲取下一個實體
if (!second || (curr && entity_before(curr, second)))
second = curr;
}
if (second && wakeup_preempt_entity(second, left) < 1)
se = second;
}
if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) {
/*
* 有人確實希望運行這個實體。如果不不公平,就運行它。
*/
se = cfs_rq->next;
} else if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) {
/*
* 更傾向于運行最后一個實體,嘗試將 CPU 返回到一個被搶占的任務。
*/
se = cfs_rq->last;
}
return se;
}
if (se == curr) {
second = __pick_first_entity(cfs_rq); // 獲取最左邊的實體
} else {
second = __pick_next_entity(se); // 獲取下一個實體
if (!second || (curr && entity_before(curr, second)))
second = curr;
}
if (second && wakeup_preempt_entity(second, left) < 1)
se = second;
return se; } 函數(shù)pick_next_entity的作用是選擇下一個要運行的調(diào)度實體,它根據(jù)一系列因素來決定選擇哪個實體,以確保公平性、滿足任務需求,并盡量提高緩存局部性。
總結(jié)
通過深入分析Linux內(nèi)核調(diào)度器的代碼實現(xiàn),我們了解了調(diào)度器的入口函數(shù)和選擇下一個執(zhí)行進程的過程。這個過程是內(nèi)核多任務處理的核心,確保了系統(tǒng)資源的合理分配。深入理解調(diào)度器的工作原理將有助于我們更好地優(yōu)化系統(tǒng)性能,提高響應速度。