【Linux内核二十】进程管理模块:CFS调度器enqueue(七):enqueue_entity函数的收尾

接上篇:【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。

相关推荐
安科士andxe5 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
小白同学_C8 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖8 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
儒雅的晴天9 小时前
大模型幻觉问题
运维·服务器
通信大师10 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
不做无法实现的梦~10 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
默|笙12 小时前
【Linux】fd_重定向本质
linux·运维·服务器
叫我龙翔12 小时前
【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构
服务器·网络·c++·json
陈苏同学12 小时前
[已解决] Solving environment: failed with repodata from current_repodata.json (python其实已经被AutoDL装好了!)
linux·python·conda
“αβ”12 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping