CFSスケジューリングの原理と主要なデータ構造を前に学びましたが、今日はコード分析に入ります。もちろん、コード分析はメイントランクのみを対象とし、キャピラリーは対象外とします。同時に、プロセスのスケジュール方法のアイデアに従って、いくつかの重要なコードも分析します。
コードを分析する前に、最初に分析する必要があるいくつかの小さな関数がありますが、言われるように、高層ビルが地面に上がるので、これらの小さな関数は依然として非常に重要です。
calc_delta_fair
calc_delta_fair関数は、プロセスのvruntimeを計算するために使用される関数です。以前のCFS原則では、プロセスのvruntimeの計算式は次のとおりであることを学びました。
プロセスのvruntimeは、プロセスの実際の実行時間に、NICE0プロセスの対応する重みを掛け、現在のプロセスの重みで割った値に等しくなります。除算の計算に浮動小数点演算が含まれないようにするために、Linuxカーネルは、32ビット左にシフトしてから32ビット右にシフトすることにより、浮動小数点演算を回避します。変更された式は次のとおりです。
そして、次の式に変換されます
その中で、inv_weightの値はカーネルコードで計算されており、それを使用する場合は、テーブルを参照するだけでInv_weigthの値を取得できます。
/*
* Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated.
*
* In cases where the weight does not change often, we can use the
* precalculated inverse to speed up arithmetics by turning divisions
* into multiplications:
*/
const u32 sched_prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};
このように、上記の計算方法により、プロセスの実行時間を簡単に取得できます。計算プロセスがわかったら、コードを見てみましょう
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
現在のスケジューリングエンティティの重み値がNICE_0_LOADと等しい場合、プロセスの実際の実行時間が直接返されます。nice0プロセスの仮想時間は物理時間と等しいためです。それ以外の場合は、__ calc_delta関数を呼び出して、プロセスのvruntimeを計算します
static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
u64 fact = scale_load_down(weight);
int shift = WMULT_SHIFT;
__update_inv_weight(lw);
if (unlikely(fact >> 32)) {
while (fact >> 32) {
fact >>= 1;
shift--;
}
}
/* hint to use a 32x32->64 mul */
fact = (u64)(u32)fact * lw->inv_weight;
while (fact >> 32) {
fact >>= 1;
shift--;
}
return mul_u64_u32_shr(delta_exec, fact, shift);
}
最後に、プロセスの仮想実行時間は、上記の計算式で計算できます。詳細なコードはプッシュされず、必要ありません。興味のある方はご覧になれます。
sched_slice
この関数は、スケジューリングエンティティがスケジューリング期間内に割り当てることができる実行時間を計算するために使用されます
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);
for_each_sched_entity(se) {
struct load_weight *load;
struct load_weight lw;
cfs_rq = cfs_rq_of(se);
load = &cfs_rq->load;
if (unlikely(!se->on_rq)) {
lw = cfs_rq->load;
update_load_add(&lw, se->load.weight);
load = &lw;
}
slice = __calc_delta(slice, se->load.weight, load);
}
return slice;
}
__sched_period関数は、スケジューリング周期を計算する関数で、プロセス数が8未満の場合、スケジューリング周期は6msに等しいスケジューリング遅延と等しくなります。それ以外の場合、スケジューリング期間はプロセス数に0.75msを掛けた値に等しく、プロセスが速すぎる場合のコンテキスト切り替えを防ぐために、プロセスが少なくとも0.75ms実行できることを示します。
次のステップは、現在のスケジューリングエンティティをトラバースすることです。スケジューリングエンティティにスケジューリンググループの関係がない場合、そのエンティティは1回だけ実行されます。現在のCFS実行キューcfs_rqを取得し、実行キューの重みを取得しますcfs_rq-> rqはこの実行キューの重みを表します。最後に、このプロセスの実際の実行時間は__calc_deltaによって計算されます。
__calc_deltaこの関数は、以前に仮想関数を計算するときに導入されました。これは、プロセスの仮想時間を計算できるだけでなく、総スケジューリングサイクルにおけるプロセスの実行時間を計算するためのものです。式は次のとおりです。
进程的运行时间 = (调度周期时间 * 进程的weight) / CFS运行队列的总weigth
place_entity
この関数は、スケジューリングエンティティを罰するために使用されます。本質は、vruntime値を変更することです。
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);
}
- 現在のCFS実行キューのmin_vruntime値を取得します
- パラメータinitialがtrueに等しい場合、それは新しく作成されたプロセスを表し、新しく作成されたプロセスはそのvruntimeに値を追加します。つまり、プロセスを罰します。新しく作成されたプロセスのvruntimeが小さすぎてCPUを占有できないため、これは新しく作成されたプロセスに対する罰です
- initalが真でない場合、それは覚醒のプロセスを表しています。覚醒プロセスに注意する必要があります。最大の注意は、スケジューリング遅延の半分です。
- スケジューリングエンティティのvruntimeが後方に移動できないことを確認し、max_vruntimeを通じて最大vruntimeを取得します。
update_curr
update_curr関数は、現在のプロセスの実行時間情報を更新するために使用されます
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = now;
schedstat_set(curr->statistics.exec_max,
max(delta_exec, curr->statistics.exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq->exec_clock, delta_exec);
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
- delta_exec = now-curr-> exec_start;現在のCFS実行キュープロセスと最後に更新された仮想時間との差を計算します
- curr-> exec_start = now; exec_startの値を更新
- curr-> sum_exec_runtime + = delta_exec;現在のプロセスの合計実行時間を更新します
- 現在のプロセスの仮想時間をcalc_delta_fairで計算します
- update_min_vruntime関数を使用して、CFS実行キューの最小vruntime値を更新します。
新しいプロセスの作成
新しいプロセス作成プロセスを通じて、CFSスケジューラーが新しく作成されたプロセスをセットアップする方法を分析します。forkで新しいプロセスを作成したとき、それをschedモジュールに持ってきて、ここでは分析に焦点を当てます。
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
unsigned long flags;
__sched_fork(clone_flags, p);
/*
* We mark the process as NEW here. This guarantees that
* nobody will actually run it, and a signal or other external
* event cannot wake it up and insert it on the runqueue either.
*/
p->state = TASK_NEW;
/*
* Make sure we do not leak PI boosting priority to the child.
*/
p->prio = current->normal_prio;
if (dl_prio(p->prio))
return -EAGAIN;
else if (rt_prio(p->prio))
p->sched_class = &rt_sched_class;
else
p->sched_class = &fair_sched_class;
init_entity_runnable_average(&p->se);
raw_spin_lock_irqsave(&p->pi_lock, flags);
/*
* We're setting the CPU for the first time, we don't migrate,
* so use __set_task_cpu().
*/
__set_task_cpu(p, smp_processor_id());
if (p->sched_class->task_fork)
p->sched_class->task_fork(p);
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
init_task_preempt_count(p);
return 0;
}
- __sched_forkは主にスケジューリングエンティティを初期化するためのもので、ここでは親プロセスを継承する必要はありません。子プロセスが再実行されるため、これらの値は再コピーのプロセスになります。
- プロセスのステータスをTASK_NEWに設定します。これは、これが新しいプロセスであることを意味します
- 現在の現在のプロセスの優先度を新しく作成されたプロセスに設定します。新しく作成されたプロセスの動的優先度p-> prio = current-> normal_prio
- プロセスの優先度に従ってプロセスのスケジューリングクラスを設定します。RTプロセスの場合は、スケジューリングクラスをrt_sched_classに設定します。通常のプロセスの場合は、スケジューリングクラスをfair_sched_classに設定します。
- 現在のプロセスが実行されているCPUを設定します。ここでは、簡単な設定を示します。また、スケジューラーの実行キューに参加するときに一度設定されます
- 最後に、スケジューリングクラスでtask_fork関数ポインターを呼び出し、最後に、fair_sched_classでtask_fork関数ポインターを呼び出します。
static void task_fork_fair(struct task_struct *p)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se, *curr;
struct rq *rq = this_rq();
struct rq_flags rf;
rq_lock(rq, &rf);
update_rq_clock(rq);
cfs_rq = task_cfs_rq(current);
curr = cfs_rq->curr;
if (curr) {
update_curr(cfs_rq);
se->vruntime = curr->vruntime;
}
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);
}
se->vruntime -= cfs_rq->min_vruntime;
rq_unlock(rq, &rf);
}
- currentを介して現在のCFS実行キューを取得し、実行キューのcurrポインターを介して現在のスケジューリングエンティティを取得してから、update_currを介して現在のスケジューリングエンティティの実行時間を更新し、同時に、現在のスケジューリングエンティティの仮想vruntimeの値を、新しく作成されたプロセスのvruntimeに割り当てます。
- place_entity関数を使用して新しく作成されたプロセスにペナルティを課し、3番目のパラメーターが1であると見なして、新しく作成されたプロセスを罰する
- se-> vruntime- = cfs_rq-> min_vruntime;現在のスケジューリングエンティティの仮想実行時間からmin_vruntimeを減算します。この文は、スケジューリングエンティティが実行キューに追加される前にまだ期間があるためと理解できます。この期間中、min_vruntime値が変わります。実行キューに追加すると、公平に見えます。
上記は新しく作成されたプロセスフローであり、次のフローチャートで要約されています
新しいプロセスをレディキューに追加する
forkプロセスが完了すると、wake_up_new_task関数を介してプロセスを起動し、新しく作成されたプロセスをレディキューに追加します
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
p->state = TASK_RUNNING;
#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
*
* Use __set_task_cpu() to avoid calling sched_class::migrate_task_rq,
* as we're not fully set-up yet.
*/
p->recent_used_cpu = task_cpu(p);
__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif
rq = __task_rq_lock(p, &rf);
update_rq_clock(rq);
post_init_entity_util_avg(&p->se);
activate_task(rq, p, ENQUEUE_NOCLOCK);
p->on_rq = TASK_ON_RQ_QUEUED;
trace_sched_wakeup_new(p);
check_preempt_curr(rq, p, WF_FORK);
task_rq_unlock(rq, p, &rf);
}
- プロセスの状態をTASK_RUNNINGに設定します。これは、プロセスがすでに準備完了状態であることを意味します
- SMPを開くと、__ set_task_cpuを介して最適なCPUがリセットされ、新しいプロセスが実行されます
- 最後に、新しく作成されたプロセスをレディキューに追加するactivate_task(rq、p、ENQUEUE_NOCLOCK);関数
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 inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
if (!(flags & ENQUEUE_NOCLOCK))
update_rq_clock(rq);
if (!(flags & ENQUEUE_RESTORE)) {
sched_info_queued(rq, p);
psi_enqueue(p, flags & ENQUEUE_WAKEUP);
}
p->sched_class->enqueue_task(rq, p, flags);
}
最終的には、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;
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;
}
}
- スケジューリングエンティティのon_rqが設定されている場合、スタッフはレディキューにあり、直接ジャンプします。
- enqueue_entity関数は、スケジューリングエンティティをエンキューします。
- 実行できるCFS実行キューの数を増やしますh_nr_running
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
bool curr = cfs_rq->curr == se;
/*
* If we're the current task, we must renormalise before calling
* update_curr().
*/
if (renorm && curr)
se->vruntime += cfs_rq->min_vruntime;
update_curr(cfs_rq);
/*
* Otherwise, renormalise after, such that we're placed at the current
* moment in time, instead of some random moment in the past. Being
* placed in the past could significantly boost this task to the
* fairness detriment of existing tasks.
*/
if (renorm && !curr)
se->vruntime += cfs_rq->min_vruntime;
/*
* When enqueuing a sched_entity, we must:
* - Update loads to have both entity and cfs_rq synced with now.
* - Add its load to cfs_rq->runnable_avg
* - For group_entity, update its weight to reflect the new share of
* its group cfs_rq
* - Add its new weight to cfs_rq->load.weight
*/
update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
update_cfs_group(se);
enqueue_runnable_load_avg(cfs_rq, se);
account_entity_enqueue(cfs_rq, se);
if (flags & ENQUEUE_WAKEUP)
place_entity(cfs_rq, se, 0);
check_schedstat_required();
update_stats_enqueue(cfs_rq, se, flags);
check_spread(cfs_rq, se);
if (!curr)
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;
if (cfs_rq->nr_running == 1) {
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle(cfs_rq);
}
}
- se-> vruntime + = cfs_rq-> min_vruntime;スケジューリングエンティティの仮想時間を追加します。min_vruntimeは以前にフォークで差し引かれました。今追加する必要があります。min_vruntimeがより正確になりました。
- update_curr(cfs_rq);現在のスケジューリングエンティティの実行時間とCFS実行キューのmin_vruntimeを更新します
- コメントを介して、スケジューリングエンティティがレディキューに追加されると、実行中のキューの負荷とスケジューリングエンティティの負荷を更新する必要があります。
- ENQUEUE_WAKEUPが設定されている場合、それは現在のプロセスがウェイクアッププロセスであり、特定の補正が必要であることを意味します
- __enqueue_entityは、CFSレッドブラックツリーにスケジューリングエンティティを追加します
- se-> on_rq = 1; on_rqを1に設定します。これは、実行キューに追加されたことを意味します
次の実行中のプロセスを選択します
プロセスがforkによって作成され、CFS実行キューの赤と黒のツリーに追加されたら、その実行を選択する必要があります。スケジュール関数を直接調べます。バックボーンを読みやすくするために、コードは単純化されています
static void __sched notrace __schedule(bool preempt)
{
cpu = smp_processor_id(); //获取当前CPU
rq = cpu_rq(cpu); //获取当前的struct rq, PER_CPU变量
prev = rq->curr; //通过curr指针获取当前运行进程
next = pick_next_task(rq, prev, &rf); //通过pick_next回调选择进程
if (likely(prev != next)) {
rq = context_switch(rq, prev, next, &rf); //如果当前进程和下一个进程不同,则发生切换
}
- pick_nextを介して次の実行中のプロセスを取得する
- context_switchによるコンテキスト切り替え
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;
if (likely((prev->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = fair_sched_class.pick_next_task(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto again;
/* Assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev, rf);
return p;
}
again:
for_each_class(class) {
p = class->pick_next_task(rq, prev, rf);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
}
pick_nextの2つの主なステップ
- システムには非常に多くの共通プロセスがあるため、ここでは、現在のプロセスのスケジューリングクラスと実行キュー内の実行可能プロセスの数がCFS実行キュー内の実行可能プロセスの数と等しいかどうかを判断して最適化を行います。同じ場合、残りのプロセスは通常のプロセスであり、fair_sched_classでpick_nextコールバックを直接呼び出します。
- それ以外の場合は、もう一度ジャンプして、pick_next_task関数ポインターへの呼び出しを正直に、スケジューリングクラスの優先度の高いものから低いものへとトラバースします。
static 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;
struct sched_entity *se;
struct task_struct *p;
int new_tasks;
again:
if (!cfs_rq->nr_running)
goto idle;
put_prev_task(rq, prev);
do {
se = pick_next_entity(cfs_rq, NULL);
set_next_entity(cfs_rq, se);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
- CFS実行キューにプロセスがなくなると、アイドルプロセスが返されます。
- pick_next_entryは、CFS赤黒ツリーの左端のノードからスケジューリングエンティティを取得します
- set_next_entryは、次のスケジューリングエンティティをCFS実行キューのcurrポインタに設定します
- その後、context_switchが切り替わり、スイッチの内容が次の章で紹介されます