接上篇:【Linux内核十九】进程管理模块:CFS调度器enqueue(六):计算新入队的进程的时间片和vruntime
来来回回的,花了好几篇,把CFS调度器中的enqueue_task_fair回调函数中的其中一个关键函数:enqueue_entity学习了大半,这个函数主要完成两个工作:
- 根据需要入队的进程信息,完成cfs队列相关信息的更新,包括vruntime等。
- 计算该进程的vruntime等信息,把进程添加到cfs队列中。
前面好几篇都是和这个相关的内容,除了这些动作,最后还有几个函数,学习完这个enqueue_entity方法就结束了。
enqueue_entity
check_schedstat_required
检查几个追踪点是否开启,如果CONFIG_SCHEDSTATS宏没有打开的,就是个空函数。
c
static inline void check_schedstat_required(void)
{
#ifdef CONFIG_SCHEDSTATS
if (schedstat_enabled())
return;
/* Force schedstat enabled if a dependent tracepoint is active */
if (trace_sched_stat_wait_enabled() ||
trace_sched_stat_sleep_enabled() ||
trace_sched_stat_iowait_enabled() ||
trace_sched_stat_blocked_enabled() ||
trace_sched_stat_runtime_enabled()) {
printk_deferred_once("Scheduler tracepoints stat_sleep, stat_iowait, "
"stat_blocked and stat_runtime require the "
"kernel parameter schedstats=enable or "
"kernel.sched_schedstats=1\n");
}
#endif
}
- sched_stat_wait:统计进程在就绪队列的等待时长(调度延迟);
- sched_stat_sleep:统计主动睡眠时长(非 IO / 阻塞);
- sched_stat_iowait:统计 IO 等待时长(定位 IO 瓶颈);
- sched_stat_blocked:统计同步阻塞时长(定位锁竞争);
- sched_stat_runtime:统计进程实际运行时长(分析 CPU 占用)
关于追踪点的学习,可以看一下【Linux内核十八】进程管理模块:CFS调度器enqueue(五),update_curr中的其他记账 & 内核追踪点
update_stats_enqueue
c
static inline void
update_stats_enqueue(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
if (!schedstat_enabled())
return;
/*
* Are we enqueueing a waiting task? (for current tasks
* a dequeue/enqueue event is a NOP)
*/
if (se != cfs_rq->curr)
update_stats_wait_start(cfs_rq, se);
if (flags & ENQUEUE_WAKEUP)
update_stats_enqueue_sleeper(cfs_rq, se);
}
同样,是要求CONFIG_SCHEDSTATS宏是打开的状态。
update_stats_wait_start
用于更新要入队的进程的等待时间,在se->stats->wait_start字段。
c
update_stats_wait_start(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
u64 wait_start, prev_wait_start;
struct sched_statistics *stats;
// 1. 若调度统计功能未开启,直接返回(避免无意义计算)
if (!schedstat_enabled())
return;
// 2. 获取该调度实体的统计结构体(存储wait_start等统计数据)
stats = __schedstats_from_se(se);
// 3. 记录"开始等待"的起始时钟:当前rq的调度时钟(纳秒级)
wait_start = rq_clock(rq_of(cfs_rq));
// 4. 读取上一次记录的wait_start(处理迁移场景用)
prev_wait_start = schedstat_val(stats->wait_start);
// 5. 处理"进程跨CPU迁移"的特殊场景:
// - entity_is_task(se):se是普通进程(非组调度实体)
// - task_on_rq_migrating:进程正在跨CPU迁移
// - wait_start > prev_wait_start:时钟单调递增(避免时间回退)
if (entity_is_task(se) && task_on_rq_migrating(task_of(se)) &&
likely(wait_start > prev_wait_start))
// 修正:减去上一次的wait_start,避免迁移导致等待时长重复统计
wait_start -= prev_wait_start;
// 6. 将修正后的起始时间存入统计结构体(供后续计算等待时长)
__schedstat_set(stats->wait_start, wait_start);
}
- 代码
stats = __schedstats_from_se(se);是通过contain_of方法,获得se结构体中对应的sched_statistics成员的指针。 - 代码
rq_clock(rq_of(cfs_rq))函是获取当前 CPU 运行队列(struct rq)的 "调度时钟"。- 代码
wait_start = rq_clock(rq_of(cfs_rq));获得当前的时钟。 - 代码
prev_wait_start = schedstat_val(stats->wait_start);获得前一次的时钟。schedstat_val宏定义,在CONFIG_SCHEDSTATS宏打开的情况下,是返回参数本身的值,如果没打开,则返回0。 - __schedstat_set在CONFIG_SCHEDSTATS宏打开的情况下,是将wait_start的值赋值给stats->wait_start,否则什么都不做。
- 代码
- 也就是说,如果是迁移过来的进程,需要用当前的队列时钟减去之前的队列时钟,应该是要保持一致。
- 如果不是迁移过来的进程,则直接获取当前的队列时钟,作为进程开始等待的时间。
update_stats_enqueue_sleeper
上面的函数是用于处理正常和迁移的进程,而通过下面的代码来计算需要入队进程的sleep时钟和block时钟的统计信息:
c
if (flags & ENQUEUE_WAKEUP)
update_stats_enqueue_sleeper(cfs_rq, se);
来调用update_stats_enqueue_sleeper函数进行sleep和block的时间更新。
c
static inline void
update_stats_enqueue_sleeper(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct sched_statistics *stats; // 调度统计结构体(存储睡眠/阻塞的起始时间、累计时长等)
struct task_struct *tsk = NULL; // 对应的进程结构体(仅当se是普通进程时非空)
u64 sleep_start, block_start; // 睡眠/阻塞的起始时钟(基于rq_clock)
// 1. 若调度统计功能未开启,直接返回(避免无意义计算)
if (!schedstat_enabled())
return;
// 2. 获取当前调度实体(se)的统计结构体(每个se独立存储统计数据)
stats = __schedstats_from_se(se);
// 3. 读取之前记录的"睡眠起始时间"和"阻塞起始时间"(进程进入睡眠/阻塞时记录)
sleep_start = schedstat_val(stats->sleep_start);
block_start = schedstat_val(stats->block_start);
// 4. 若se是普通进程(非组调度实体),获取对应的task_struct指针
if (entity_is_task(se))
tsk = task_of(se);
// ===================== 第一部分:统计"主动睡眠"时长 =====================
if (sleep_start) { // 若sleep_start非0(说明进程有主动睡眠行为)
// 4.1 计算睡眠总时长:当前rq时钟 - 睡眠起始时间(纳秒级)
u64 delta = rq_clock(rq_of(cfs_rq)) - sleep_start;
// 4.2 防御性处理:避免时钟回退导致delta为负(rq_clock单调递增,理论上不会发生)
if ((s64)delta < 0)
delta = 0;
// 4.3 更新"最大睡眠时长":若本次睡眠时长超过历史最大值,更新
if (unlikely(delta > schedstat_val(stats->sleep_max)))
__schedstat_set(stats->sleep_max, delta);
// 4.4 重置睡眠起始时间(避免重复统计)
__schedstat_set(stats->sleep_start, 0);
// 4.5 累加睡眠总时长到统计结构体(sum_sleep_runtime包含睡眠+阻塞时长)
__schedstat_add(stats->sum_sleep_runtime, delta);
// 4.6 若se是普通进程,输出跟踪点+统计调度延迟
if (tsk) {
// 统计调度延迟(delta>>10:纳秒转微秒,最后一个参数1表示"睡眠类延迟")
account_scheduler_latency(tsk, delta >> 10, 1);
// 输出sched_stat_sleep跟踪点(供perf/ftrace采集)
trace_sched_stat_sleep(tsk, delta);
}
}
// ===================== 第二部分:统计"阻塞/IO等待"时长 =====================
if (block_start) { // 若block_start非0(说明进程有阻塞行为)
// 5.1 计算阻塞总时长:当前rq时钟 - 阻塞起始时间(纳秒级)
u64 delta = rq_clock(rq_of(cfs_rq)) - block_start;
// 5.2 防御性处理:避免时钟回退导致delta为负
if ((s64)delta < 0)
delta = 0;
// 5.3 更新"最大阻塞时长":若本次阻塞时长超过历史最大值,更新
if (unlikely(delta > schedstat_val(stats->block_max)))
__schedstat_set(stats->block_max, delta);
// 5.4 重置阻塞起始时间(避免重复统计)
__schedstat_set(stats->block_start, 0);
// 5.5 累加阻塞时长到总睡眠时长(sum_sleep_runtime)
__schedstat_add(stats->sum_sleep_runtime, delta);
// 5.6 若se是普通进程,细分处理(IO等待/普通阻塞)
if (tsk) {
// 5.6.1 若进程是"IO等待"(tsk->in_iowait标记)
if (tsk->in_iowait) {
// 累加IO等待总时长
__schedstat_add(stats->iowait_sum, delta);
// 增加IO等待次数统计
__schedstat_inc(stats->iowait_count);
// 输出sched_stat_iowait跟踪点(定位IO瓶颈)
trace_sched_stat_iowait(tsk, delta);
}
// 5.6.2 输出sched_stat_blocked跟踪点(统计所有阻塞行为)
trace_sched_stat_blocked(tsk, delta);
/*
* Blocking time is in units of nanosecs, so shift by
* 20 to get a milliseconds-range estimation of the
* amount of time that the task spent sleeping:
*/
// 5.6.3 睡眠分析(SLEEP_PROFILING):
// delta>>20:纳秒转毫秒(2^20 ≈ 100万,1ns<<20=1ms左右)
// profile_hits:记录进程阻塞时的内核调用栈(定位阻塞原因)
if (unlikely(prof_on == SLEEP_PROFILING)) {
profile_hits(SLEEP_PROFILING,
(void *)get_wchan(tsk), // 获取进程阻塞时的内核函数
delta >> 20);
}
// 5.6.4 统计调度延迟(最后一个参数0表示"阻塞类延迟")
account_scheduler_latency(tsk, delta >> 10, 0);
}
}
}
check_spread
这个函数也是一个通过宏来控制的函数:
c
static void check_spread(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
#ifdef CONFIG_SCHED_DEBUG
s64 d = se->vruntime - cfs_rq->min_vruntime;
if (d < 0)
d = -d;
if (d > 3*sysctl_sched_latency)
schedstat_inc(cfs_rq->nr_spread_over);
#endif
}
- CONFIG_SCHED_DEBUG 是 Linux 内核编译时的配置宏,开启后会编译调度器的调试代码、暴露调试接口、输出调试信息,核心服务于调度器的开发、调优和问题排查。
- check_spread 是调度器负载均衡子系统中的辅助检查函数,没怎么花时间去了解这个里面的逻辑。
__enqueue_entity
又有一个enqueue_entity函数,只不过前面加了前缀。其实我理解这个才是真正意义上的入队,之前的那么多内容,基本上都是在做入队前的准备工作,包括各种信息的计算和统计。
c
static __always_inline struct rb_node *
rb_add_cached(struct rb_node *node, struct rb_root_cached *tree,
bool (*less)(struct rb_node *, const struct rb_node *))
{
struct rb_node **link = &tree->rb_root.rb_node;
struct rb_node *parent = NULL;
bool leftmost = true;
while (*link) {
parent = *link;
if (less(node, parent)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(node, parent, link);
rb_insert_color_cached(node, tree, leftmost);
return leftmost ? node : NULL;
}
/*
* Enqueue an entity into the rb-tree:
*/
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
rb_add_cached(&se->run_node, &cfs_rq->tasks_timeline, __entity_less);
}
- 从代码上来看,就是真正的把入队的进程中的se挂在到cfs的红黑树上去。就不一条代码一条代码的分析如何往红黑树上挂节点了。
- struct rb_root_cached tasks_timeline;是cfs_rq结构体中的成员,就是一颗带缓存的红黑树。我看到这里的疑问是,为什么这个成员变量叫做tasks_timeline。我咨询了一下豆包,给出的答复是:红黑树的排序键值是vruntime(虚拟运行时间),而非进程 PID、优先级(nice 值)等 ------ 这是 "timeline" 的核心体现:红黑树的左→右方向,对应vruntime的 "过去→未来" 方向(时间线的流向);好像很有道理的样子。
list_add_leaf_cfs_rq
和宏CONFIG_FAIR_GROUP_SCHED有关,否则就是一个空函数:
c
static inline bool list_add_leaf_cfs_rq(struct cfs_rq *cfs_rq)
{
return true;
}
if (cfs_rq->nr_running == 1 || cfs_bandwidth_used())
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle
和宏CONFIG_CFS_BANDWIDTH有关,否则就是一个空函数:
c
static void check_enqueue_throttle(struct cfs_rq *cfs_rq) {}
if (cfs_rq->nr_running == 1)
check_enqueue_throttle(cfs_rq);
这样,enqueue_entity的内容就结束了,下一篇继续了解enqueue回调函数的最外层:enqueue_task_fair。