【内核调度、负载均衡】【entity_tick】

本文详细剖析了CFS调度器中的关键函数task_tick_fair和entity_tick的工作原理,包括如何进行周期性调度、更新运行时间和负载平均值,以及如何在适当时候进行进程抢占决策。

在scheduler_tick函数中会调用task_tick函数,具体到fair class,他的入口函数为

task_tick_fair

/*
 * scheduler tick hitting a task of our scheduling class:
 */
 /*  获取到当前进程curr所在的调度实体  */
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
	struct cfs_rq *cfs_rq;
	/*  获取到当前进程curr所在的调度实体  */
	struct sched_entity *se = &curr->se;
	/* for_each_sched_entity
     * 在不支持组调度条件下, 只循环一次
     * 在组调度的条件下, 调度实体存在层次关系,
     * 更新子调度实体的同时必须更新父调度实体  */
	for_each_sched_entity(se) {
		/*  获取当当前运行的进程所在的CFS就绪队列  */
		cfs_rq = cfs_rq_of(se);
		/*  完成周期性调度  */
		entity_tick(cfs_rq, se, queued);
	}

	if (static_branch_unlikely(&sched_numa_balancing))
		task_tick_numa(rq, curr);

	update_misfit_status(curr, rq);

	update_overutilized_status(rq);
}

我们可以看到, CFS周期性调度的功能实际上是委托给entity_tick函数来完成的

entity_tick

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
	/*
	 * Update run-time statistics of the 'current'.
	 */
	update_curr(cfs_rq);

	/*
	 * Ensure that runnable average is periodically updated.
	 */
	update_load_avg(curr, UPDATE_TG);
	update_cfs_shares(curr);

	//接下来是hrtimer的更新, 这些由内核通过参数CONFIG_SCHED_HRTICK开启
#ifdef CONFIG_SCHED_HRTICK
	/*
	 * queued ticks are scheduled to match the slice, so don't bother
	 * validating it and just reschedule.
	 */
	if (queued) {
		resched_curr(rq_of(cfs_rq));
		return;
	}
	/*
	 * don't let the period tick interfere with the hrtick preemption
	 */
	if (!sched_feat(DOUBLE_TICK) &&
			hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
		return;
#endif
	//如果进程的数目不少于两个, 则由check_preempt_tick作出决策
	if (cfs_rq->nr_running > 1)
		check_preempt_tick(cfs_rq, curr);
}

update_curr主要是更新vruntime和min_vruntime

https://blog.youkuaiyun.com/feifei_csdn/article/details/107200966

update_load_avg主要是更新entity和cpu的负载

https://blog.youkuaiyun.com/feifei_csdn/article/details/107381373

check_preempt_tick

/*
 * Preempt the current task with a newly woken task if needed:
 */
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;
	struct sched_entity *se;
	s64 delta;
	/*  计算curr的理论上应该运行的时间  */
	ideal_runtime = sched_slice(cfs_rq, curr);
	 /*  计算curr的实际运行时间
     *  sum_exec_runtime: 进程执行的总时间
     *  prev_sum_exec_runtime:进程在切换进CPU时的sum_exec_runtime值  */
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	 /*  如果实际运行时间比理论上应该运行的时间长
     *  说明curr进程已经运行了足够长的时间
     *  应该调度新的进程抢占CPU了  */
	if (delta_exec > ideal_runtime) {
		//resched_curr(rq_of(cfs_rq))设置重调度标识, 从而触发延迟调度
		resched_curr(rq_of(cfs_rq));
		/*
		 * The current task ran long enough, ensure it doesn't get
		 * re-elected due to buddy favours.
		 */
		clear_buddies(cfs_rq, curr);
		return;
	}

	/*
	 * Ensure that a task that missed wakeup preemption by a
	 * narrow margin doesn't have to wait for a full slice.
	 * This also mitigates buddy induced latencies under load.
	 */
	if (delta_exec < sysctl_sched_min_granularity)
		return;

	se = __pick_first_entity(cfs_rq);
	delta = curr->vruntime - se->vruntime;

	if (delta < 0)
		return;
//curr进程与红黑树中最左进程left虚拟运行时间的差值大于curr的期望运行时间ideal_runtime
	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq));
}
// 选择最左边的entity
struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
	struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);

	if (!left)
		return NULL;

	return rb_entry(left, struct sched_entity, run_node);
}

 

-exception_panic_reason:Oops - Undefined instruction: Fatal exception -exception_file_infoi:not-bugon -exception_task_id:18051 -exception_task_family:[ptz-bgd-1-IMAGE, 18051][main, 585] -exception_pc_symbol:[<ffffffc0082310f4>] propagate_entity_load_avg+0xf8/0x35c -exception_stack_info:[<ffffffc001806994>] get_exception_stack_info+0x150/0x2d8 [sysdump] [<ffffffc0018067ec>] prepare_exception_info+0x16c/0x1c4 [sysdump] [<ffffffc0018099c4>] sysdump_panic_event+0x76c/0x8ec [sysdump] [<ffffffc0082044fc>] atomic_notifier_call_chain+0x8c/0x138 [<ffffffc0081b4d64>] panic+0x1b8/0x44c [<ffffffc0081137bc>] die+0x5a0/0x67c [<ffffffc008114748>] do_el1_undef+0x68/0x98 [<ffffffc00977cd60>] el1_undef+0x34/0x54 [<ffffffc00977cc04>] el1h_64_sync_handler+0x54/0xb4 [<ffffffc008091310>] el1h_64_sync+0x7c/0x80 [<ffffffc0082310f4>] propagate_entity_load_avg+0xf8/0x35c [<ffffffc008230da4>] update_load_avg+0x1b4/0x40c [<ffffffc008231b74>] propagate_entity_cfs_rq+0x88/0x1f4 [<ffffffc0082341f8>] detach_entity_cfs_rq+0x84/0xb4 [<ffffffc0082331d4>] migrate_task_rq_fair+0x64/0x1c0 [<ffffffc0082182d8>] set_task_cpu+0xd8/0x2ac [<ffffffc0018d6c20>] lb_pull_tasks+0x1b8/0x278 [unisoc_sched] [<ffffffc0018d3028>] android_rvh_sched_newidle_balance+0x298/0x344 [unisoc_sched] [<ffffffc008238b48>] newidle_balance+0x88/0x79c [<ffffffc00823d21c>] pick_next_task_fair+0x5c/0x3dc [<ffffffc0091ef33c>] binder_wait_for_work+0x178/0x48c [<ffffffc0091ec080>] binder_thread_read+0x270/0x2bc4 [<ffffffc0091e7e98>] binder_ioctl_write_read+0x120/0x588 [<ffffffc0091e2bbc>] binder_ioctl+0x1c4/0xfd8 [<ffffffc0085eff20>] __arm64_sys_ioctl+0x184/0x20c [<ffffffc00811fb68>] invoke_syscall+0x60/0x150 [<ffffffc00811fa9c>] el0_svc_common+0x8c/0xf8 [<ffffffc00811f9a0>] do_el0_svc+0x28/0x98 [<ffffffc00977d248>] el0_svc+0x24/0x84 [<ffffffc00977d1c0>] el0t_64_sync_handler+0x88/0xec [<ffffffc00809164c>] el0t_64_sync+0x1b8/0x1bc
06-20
<think>我们正在分析内核函数`propagate_entity_load_avg`引发的Oops(特别是“undefinedinstruction”致命异常)。Oops通常表示内核遇到了无法处理的错误,如无效指令、空指针解引用或内存访问越界。而“undefinedinstruction”错误则明确指向CPU执行了未定义的指令(可能是由于代码损坏或错误的跳转导致PC指向了数据区)。###常见原因分析####1.**内存损坏(MemoryCorruption)**-函数指针或返回地址被破坏,导致CPU跳转到非法地址(如数据区或未映射区域)执行。-可能发生在`propagate_entity_load_avg`的调用栈中:-父调度实体(`structsched_entity*parent`)指针被破坏。-调用`propagate_entity_load_avg`的`cfs_rq`结构体损坏。-**检测方法**:-检查Oops日志中的调用栈(backtrace)和寄存器状态(尤其是PC和LR)。-确认`parent`指针是否有效(非NULL且指向内核地址空间)。####2.**未对齐内存访问(UnalignedAccess)**-ARM等架构严格要求内存对齐访问。若`propagate_entity_load_avg`访问了未对齐的结构体成员(如`se->avg.load_avg`),可能触发对齐异常(但通常不会直接导致“undefinedinstruction”)。-**注意**:未对齐访问通常会触发“alignmentfault”而非“undefinedinstruction”。####3.**内核代码损坏(CodeCorruption)**-内核文本段(存储代码)被意外修改:-硬件错误(如内存故障)。-内核模块或驱动向代码段错误写入。-**检测方法**:-检查Oops日志中PC值是否指向合法内核代码区(如`__kernel_text_address`)。-若PC指向的数据看起来像调度器结构体(如`structsched_entity`),则可能是函数指针被覆盖。####4.**并发竞争(RaceCondition)**-`propagate_entity_load_avg`在更新父子实体负载时未正确加锁,导致:-父实体在传播过程中被释放(如cgroup被删除)。-实体指针变为悬垂指针(danglingpointer)。-**关键锁机制**:-CFS使用`rq->lock`保护运行队列操作。-组调度可能涉及额外的cgroup互斥锁(如`cgroup_mutex`)。-**检测点**:-确认调用`propagate_entity_load_avg`前是否持有正确的锁。###基于Oops日志的诊断步骤假设一个典型的Oops日志片段如下:```plaintextUnabletohandlekernelpagingrequestatvirtualaddressdeadbeef...PCisatpropagate_entity_load_avg+0x30/0x1c0LRisatupdate_load_avg+0x88/0x1f0...```####步骤1:定位触发指令-通过PC值(如`propagate_entity_load_avg+0x30`)反汇编函数:```bash$aarch64-linux-gnu-objdump-dvmlinux|grep-A20"propagate_entity_load_avg:"```-检查触发异常的指令:-若指令本身合法(如`ldrx0,[x1]`),则问题可能由操作数(如`x1`寄存器)指向无效地址引起。-若指令非法(如非ARMv8指令),则可能是代码损坏。####步骤2:分析寄存器状态-**关键寄存器**:-`PC`:触发异常的指令地址。-`LR`:函数返回地址(可能被破坏)。-`SP`:栈指针(检查是否越界)。-`X1/X2/...`:函数参数(如`cfs_rq`、`se`指针)。-示例:若`se`寄存器(`structsched_entity*`)值为`0xdeadbeef`,表明传递了无效指针。####步骤3:检查调度实体状态-在`propagate_entity_load_avg`中,关键操作包括:```cstructsched_entity*parent=se->parent;//可能访问无效se指针longdelta=se->avg.load_avg-se->last_load_avg;//访问se->avg```-若`se`已被释放或损坏,上述访问将触发异常。####步骤4:确认调用路径-常见调用链:```update_load_avg()→propagate_entity_load_avg()```-检查`update_load_avg`的调用点(如任务唤醒`enqueue_task_fair`或负载更新`task_tick_fair`)是否在锁保护下。###修复建议1.**增强指针检查**:-在访问`se->parent`前增加`DEBUG`检查:```cBUG_ON(!se||!se->parent);```-使用`ACCESS_ONCE()`避免编译器优化导致多次读取损坏指针。2.**锁范围扩展**:-确保在操作调度实体时持有`rq->lock`(整个负载更新过程应处于锁内)。3.**内存屏障**:-在更新父子实体指针时插入`smp_wmb()`/`smp_rmb()`,避免并发访问看到部分更新的状态。###相关问题1.如何在内核中捕获并分析调度器相关的内存损坏?2.CFS组调度中实体指针的生命周期是如何管理的?3.内核有哪些工具可以检测并发问题(如死锁、数据竞争)?[^1]:函数指针损坏可能由越界写入引起,需结合KASAN或KFENCE等工具检测。</think>### `propagate_entity_load_avg` 函数引发 Oops 异常的原因分析 在内核中,`propagate_entity_load_avg` 函数引发的 **"undefined instruction"** 致命异常通常由以下原因导致: --- ### 1. **内存访问越界或空指针解引用** - **根本原因**: 当函数访问无效的调度实体(`struct sched_entity`)或运行队列(`struct cfs_rq`)时: ```c struct sched_entity *parent = se->parent; // 若 se 已释放或无效 long delta = se->avg.load_avg - se->last_load_avg; // 访问损坏的内存 ``` - **触发机制**: - 若 `se` 或 `parent` 指针指向已释放内存或非法地址,CPU 会尝试执行该地址的"数据"(而非指令),引发未定义指令异常。 - Oops 日志中通常包含 `Unable to handle kernel paging request` 或 `PC is at NULL`。 - **调试方法**: 检查 Oops 日志中的寄存器值(如 `x0`/`x1` 存储 `se` 指针),确认指针有效性[^1]。 --- ### 2. **并发修改导致数据结构损坏** - **竞争场景**: - 负载传播时,若父调度实体被并发删除(如 cgroup 被销毁) - 未正确持有 `rq->lock` 或 `cgroup_mutex` 锁 - **代码脆弱点**: ```c void propagate_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se) { struct sched_entity *parent = se->parent; // 此处 parent 可能被并发置NULL if (!parent) return; // 检查可能来不及 parent->avg.load_avg += delta; // 写操作触发异常 } ``` - **异常表现**: Oops 日志显示异常发生在父实体访问指令位置(如 `ldr x0, [x1]`),且 `x1` 寄存器值为 `0` 或非法地址。 --- ### 3. **内核栈溢出** - **递归深度问题**: 函数通过递归向上传播负载: ```c if (parent->parent) propagate_entity_load_avg(cfs_rq_of(parent), parent); // 深度 cgroup 导致栈溢出 ``` - **触发条件**: - cgroup 层级过深(如超过 20 层) - 内核栈大小不足(通常 8-16KB) - **Oops 特征**: `kernel stack overflow` + `PC at propagate_entity_load_avg+0x...`,伴随 `undefined instruction`(因返回地址被覆盖)。 --- ### 4. **硬件或编译器问题** - **可能性**: - CPU 缓存一致性错误(如 ARM 的 L1/L2 缓存故障) - 编译器优化错误(如错误内联或指令重排) - **排查方法**: 对比 Oops 中的指令地址与反汇编代码: ```bash aarch64-linux-gnu-objdump -d vmlinux | grep -A 10 "propagate_entity_load_avg" ``` 若实际指令与源码不符,可能是编译器/缓存问题。 --- ### 诊断步骤 1. **分析 Oops 日志关键字段**: - `PC (Program Counter)`:触发异常的指令地址 - `LR (Link Register)`:函数返回地址 - `Stack Trace`:调用链是否显示递归过深 2. **检查调度实体状态**: - 使用 `kmemleak` 或 `KASAN` 检测内存泄漏 - 在函数入口添加 `BUG_ON(!se || !se->parent)` 3. **复现与锁定**: - 启用 `CONFIG_DEBUG_ATOMIC_SLEEP` 检测锁问题 - 通过 `ftrace` 记录函数调用深度 --- ### 修复方案 1. **防御性编程**: ```c void propagate_entity_load_avg(...) { if (unlikely(!se || !se->parent)) // 强化检查 return; ... } ``` 2. **限制递归深度**: ```c static void __propagate_entity_load_avg(..., int depth) { if (depth > MAX_CGROUP_DEPTH) // 设置阈值(如 16) return; ... } ``` 3. **锁范围扩展**: ```c // 在调用 propagate_entity_load_avg 前确保持有 cgroup_mutex mutex_lock(&cgroup_mutex); update_load_avg(cfs_rq, se); // 内部调用 propagate mutex_unlock(&cgroup_mutex); ``` --- ### 相关问题 1. 如何在内核中捕获并分析调度器相关的内存损坏? 2. CFS 组调度中实体指针的生命周期管理机制是怎样的? 3. 内核递归函数的安全深度如何确定? 4. ARM 架构下未定义指令异常有哪些常见诱因? [^1]: 指针有效性可通过 `virt_addr_valid()` 或 `__kernel_text_address()` 验证。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值