Linux CFS 的 entity_eligible:任务调度资格的 lag 值判断

张开发
2026/4/19 1:57:58 15 分钟阅读

分享文章

Linux CFS 的 entity_eligible:任务调度资格的 lag 值判断
一、简介1.1 背景与重要性Linux 内核的调度子系统是操作系统最核心的组件之一直接决定了系统资源分配的公平性与效率。自 2007 年 Completely Fair SchedulerCFS引入以来Linux 的公平调度策略经历了多次重大演进。2023 年Linux 6.6 内核正式将 Earliest Eligible Virtual Deadline FirstEEVDF调度策略作为 CFS 的默认实现标志着 Linux 调度器从传统的基于 vruntime 的启发式调度向基于数学严格性的延迟感知调度转变。在这一演进过程中entity_eligible函数成为判断任务是否具备调度资格的核心机制。该函数通过计算任务的lag 值应得虚拟时间减去实际虚拟时间来决定任务是否有资格eligible获得 CPU 时间。掌握这一机制对于理解现代 Linux 调度行为、优化延迟敏感型应用、以及进行调度器相关的学术研究具有重要价值。1.2 应用场景实时系统开发理解 lag 机制有助于开发低延迟音频/视频处理系统云原生优化在容器化环境中精确控制 CPU 分配策略性能调优诊断调度延迟问题优化交互式应用响应学术研究操作系统调度算法研究、实时系统论文撰写二、核心概念2.1 从 CFS 到 EEVDF 的演进传统的 CFS 调度器仅依赖vruntime虚拟运行时间进行调度决策总是选择 vruntime 最小的任务运行。然而这种策略存在** sleeper fairness **问题长时间睡眠的任务醒来后可能获得过大的 CPU 份额导致其他任务饥饿。EEVDF 调度器引入了两个关键概念来解决这些问题概念含义作用Virtual Lagvlag应得虚拟时间 - 实际虚拟时间衡量任务是否获得公平份额Virtual Deadlinevruntime (slice / weight)控制任务的最大延迟2.2 Lag 值的数学定义Lag 值的核心公式为lagi​S−si​wi​×(V−vi​)其中S 任务应得的服务时间加权后si​ 任务实际获得的服务时间wi​ 任务权重weightV 系统虚拟时间加权平均 vruntimevi​ 任务的虚拟运行时间vruntime关键判定条件lag ≥ 0任务未获得足够 CPU 时间具备调度资格eligiblelag 0任务已超额使用 CPU不具备调度资格2.3 entity_eligible 函数的作用entity_eligible是 EEVDF 调度器的核心判定函数用于检查调度实体sched_entity是否满足lag 0的条件。该函数直接影响pick_eevdf()的调度决策。三、环境准备3.1 硬件环境要求配置项最低要求推荐配置CPUx86_64 架构支持虚拟化多核处理器4核以上内存4GB RAM8GB RAM存储20GB 可用空间SSD 50GB3.2 软件环境配置3.2.1 操作系统要求本文基于Linux Kernel 6.6进行讲解EEVDF 在该版本成为默认调度器。# 检查当前内核版本 uname -r # 输出示例6.8.0-40-generic # 确保版本 6.63.2.2 开发工具安装# Ubuntu/Debian 系统 sudo apt update sudo apt install -y build-essential libncurses-dev bison flex \ libssl-dev libelf-dev git bc dwarves # CentOS/RHEL 系统 sudo yum groupinstall -y Development Tools sudo yum install -y ncurses-devel bison flex openssl-devel \ elfutils-libelf-devel git bc dwarves # 安装内核调试工具 sudo apt install -y linux-tools-common linux-tools-generic \ trace-cmd kernelshark3.2.3 内核源码获取# 创建内核源码目录 mkdir -p ~/kernel-src cd ~/kernel-src # 下载与当前运行内核匹配的源码 git clone --depth 1 --branch v6.8 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git # 或者下载特定版本 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.8.tar.xz tar -xf linux-6.8.tar.xz cd linux-6.8 # 验证关键文件存在 ls -la kernel/sched/fair.c # 核心调度文件3.3 调试环境配置# 启用调度器调试选项需要重新编译内核 cd ~/kernel-src/linux-6.8 # 复制当前配置 cp /boot/config-$(uname -r) .config # 配置内核选项 make menuconfig # 需要启用的关键选项 # Kernel hacking - Scheduler Debugging - [*] Scheduler debugging # Kernel hacking - Tracers - [*] Kernel Function Tracer # General setup - [*] Kernel .config support # 编译内核可选仅当需要修改调度器时 make -j$(nproc) sudo make modules_install sudo make install四、应用场景在现代云计算环境中延迟敏感型微服务的调度优化是一个典型应用场景。假设一个电商平台部署了订单处理服务高吞吐和支付回调服务低延迟两者运行在同一台物理机的容器内。传统的 CFS 调度可能导致支付回调服务因订单处理服务的突发 CPU 使用而经历不可预测的延迟。通过理解entity_eligible和 lag 机制开发者可以利用 EEVDF 的sched_setattr系统调用为支付服务设置更短的 time slice使其获得更早的 virtual deadline。当支付服务唤醒时其正的 lag 值确保它立即成为 eligible 任务而entity_eligible的严格数学判定避免了启发式调度的不确定性。结合sched_attr的sched_runtime和sched_deadline字段可以构建一个理论可证的延迟边界系统。这在金融交易系统、实时音视频处理、工业控制系统中尤为重要开发者可以通过分析/proc/sched_debug中的 lag 值分布验证调度策略的实际效果并据此撰写性能分析报告或学术论文。五、实际案例与步骤5.1 源码定位与结构分析首先定位entity_eligible函数在内核源码中的位置# 在源码目录中搜索 entity_eligible cd ~/kernel-src/linux-6.8 grep -n entity_eligible kernel/sched/fair.c # 输出示例 # 738:int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se) # 944: if (se entity_eligible(cfs_rq, se)) { # 1001: if (entity_eligible(cfs_rq, se)) {5.2 entity_eligible 函数实现详解打开kernel/sched/fair.c查看核心实现/* * kernel/sched/fair.c - Linux 6.8 版本 * * entity_eligible - 判断调度实体是否具备调度资格 * * 核心逻辑检查 lag 0 * lag_i S - s_i w_i * (V - v_i) * lag_i 0 V v_i */ int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se) { return vruntime_eligible(cfs_rq, se-vruntime); } /* * vruntime_eligible - 基于 vruntime 判定 eligibility * * 数学推导 * V (Σ(v_i - v) * w_i) / Σw_i v * lag_i 0 * V v_i * (Σ(v_i - v) * w_i) / Σw_i v_i - v * Σ(v_i - v) * w_i (v_i - v) * Σw_i * * 实际计算中使用 avg_vruntime 和 avg_load 避免遍历所有任务 */ static int vruntime_eligible(struct cfs_rq *cfs_rq, u64 vruntime) { struct sched_entity *curr cfs_rq-curr; s64 avg cfs_rq-avg_vruntime; // Σ(v_i - min_vruntime) * w_i long load cfs_rq-avg_load; // Σw_i /* 考虑当前运行任务的影响 * 当前任务不在红黑树中但需要计入系统负载计算 */ if (curr curr-on_rq) { unsigned long weight scale_load_down(curr-load.weight); /* entity_key vruntime - min_vruntime */ avg entity_key(cfs_rq, curr) * weight; load weight; } /* 核心判定avg (vruntime - min_vruntime) * load * 这等价于检查加权平均 vruntime 当前任务的 vruntime */ return avg (s64)(vruntime - cfs_rq-min_vruntime) * load; }5.3 Lag 值计算实战编写内核模块观测创建可加载内核模块LKM来实时观测 lag 值变化// lag_monitor.c - 用于观测 entity_eligible 相关指标的内核模块 #include linux/module.h #include linux/kernel.h #include linux/kthread.h #include linux/sched.h #include linux/sched/signal.h #include linux/proc_fs.h #include linux/seq_file.h #include linux/cpumask.h MODULE_LICENSE(GPL); MODULE_AUTHOR(Researcher); MODULE_DESCRIPTION(CFS Entity Eligible and Lag Monitor); static struct proc_dir_entry *proc_entry; static int target_pid -1; module_param(target_pid, int, 0644); MODULE_PARM_DESC(target_pid, PID to monitor); /* * 计算并显示指定任务的调度实体信息 * 模拟 entity_eligible 的判定逻辑 */ static void show_entity_info(struct seq_file *m, struct task_struct *p) { struct sched_entity *se p-se; struct cfs_rq *cfs_rq get_task_cfs_rq(p); if (!cfs_rq || !se) { seq_printf(m, Error: Cannot access sched entity\n); return; } /* 读取关键调度指标 */ u64 vruntime se-vruntime; u64 min_vruntime cfs_rq-min_vruntime; u64 avg_vruntime cfs_rq-avg_vruntime; long avg_load cfs_rq-avg_load; unsigned long weight se-load.weight; /* 计算 entity_key vruntime - min_vruntime */ u64 entity_key vruntime - min_vruntime; /* 计算 lag 的分子部分未加权 * 实际 lag weight * (V - vruntime) * 其中 V avg_vruntime / avg_load min_vruntime */ s64 avg; long load; struct sched_entity *curr cfs_rq-curr; avg avg_vruntime; load avg_load; if (curr curr-on_rq) { unsigned long curr_weight scale_load_down(curr-load.weight); avg (curr-vruntime - min_vruntime) * curr_weight; load curr_weight; } /* 判定 eligibleavg entity_key * load */ int is_eligible (avg (s64)entity_key * load); /* 计算虚拟 lag 值归一化 */ s64 lag_numerator avg - (s64)entity_key * load; seq_printf(m, Task: %s (PID: %d) \n, p-comm, p-pid); seq_printf(m, Weight: %lu\n, weight); seq_printf(m, vruntime: %llu ns\n, vruntime); seq_printf(m, min_vruntime: %llu ns\n, min_vruntime); seq_printf(m, entity_key (vruntime - min): %llu ns\n, entity_key); seq_printf(m, cfs_rq-avg_vruntime: %lld\n, avg_vruntime); seq_printf(m, cfs_rq-avg_load: %ld\n, avg_load); seq_printf(m, Effective avg: %lld\n, avg); seq_printf(m, Effective load: %ld\n, load); seq_printf(m, Lag numerator (avg - key*load): %lld\n, lag_numerator); seq_printf(m, Is Eligible (lag 0): %s\n, is_eligible ? YES : NO); seq_printf(m, On RQ: %d\n, se-on_rq); seq_printf(m, \n); } static int lag_show(struct seq_file *m, void *v) { struct task_struct *p; if (target_pid 0) { /* 监控指定 PID */ rcu_read_lock(); p pid_task(find_vpid(target_pid), PIDTYPE_PID); if (p p-sched_class fair_sched_class) { show_entity_info(m, p); } else { seq_printf(m, Task %d not found or not CFS task\n, target_pid); } rcu_read_unlock(); } else { /* 显示所有 CFS 任务的摘要 */ seq_printf(m, All CFS Tasks Summary \n\n); rcu_read_lock(); for_each_process(p) { if (p-sched_class fair_sched_class) { show_entity_info(m, p); } } rcu_read_unlock(); } return 0; } static int lag_open(struct inode *inode, struct file *file) { return single_open(file, lag_show, NULL); } static const struct proc_ops lag_ops { .proc_open lag_open, .proc_read seq_read, .proc_lseek seq_lseek, .proc_release single_release, }; static int __init lag_init(void) { proc_entry proc_create(lag_monitor, 0444, NULL, lag_ops); printk(KERN_INFO Lag Monitor: /proc/lag_monitor created\n); printk(KERN_INFO Usage: echo PID /sys/module/lag_monitor/parameters/target_pid\n); return 0; } static void __exit lag_exit(void) { proc_remove(proc_entry); printk(KERN_INFO Lag Monitor: removed\n); } module_init(lag_init); module_exit(lag_exit);编译与加载模块# 创建 Makefile cat Makefile EOF obj-m lag_monitor.o KDIR ? /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean EOF # 编译 make # 加载模块 sudo insmod lag_monitor.ko target_pid1234 # 替换为实际 PID # 查看输出 cat /proc/lag_monitor # 动态修改监控目标 echo 5678 | sudo tee /sys/module/lag_monitor/parameters/target_pid cat /proc/lag_monitor # 卸载模块 sudo rmmod lag_monitor5.4 使用 eBPF 追踪 entity_eligible 调用使用 eBPF/BCC 工具实时追踪调度决策#!/usr/bin/env python3 # entity_eligible_trace.py - 追踪 entity_eligible 调用和 lag 值 from bcc import BPF import ctypes as ct # eBPF 程序 bpf_text #include uapi/linux/ptrace.h #include linux/sched.h #include linux/cfs_sched.h struct data_t { u64 ts; u32 pid; char comm[TASK_COMM_LEN]; u64 vruntime; u64 min_vruntime; s64 avg_vruntime; long avg_load; int eligible; u64 lag; }; BPF_PERF_OUTPUT(events); int trace_entity_eligible(struct pt_regs *ctx, struct cfs_rq *cfs_rq, struct sched_entity *se) { struct data_t data {}; struct task_struct *p; data.ts bpf_ktime_get_ns(); data.pid bpf_get_current_pid_tgid() 32; bpf_get_current_comm(data.comm, sizeof(data.comm)); // 安全地读取调度实体信息 bpf_probe_read(data.vruntime, sizeof(data.vruntime), se-vruntime); bpf_probe_read(data.min_vruntime, sizeof(data.min_vruntime), cfs_rq-min_vruntime); bpf_probe_read(data.avg_vruntime, sizeof(data.avg_vruntime), cfs_rq-avg_vruntime); bpf_probe_read(data.avg_load, sizeof(data.avg_load), cfs_rq-avg_load); // 模拟 entity_eligible 计算 u64 entity_key data.vruntime - data.min_vruntime; s64 avg data.avg_vruntime; long load data.avg_load; // 获取当前任务简化处理 struct sched_entity *curr; bpf_probe_read(curr, sizeof(curr), cfs_rq-curr); if (curr) { u64 curr_vruntime; unsigned long curr_weight; bpf_probe_read(curr_vruntime, sizeof(curr_vruntime), curr-vruntime); bpf_probe_read(curr_weight, sizeof(curr_weight), curr-load.weight); curr_weight curr_weight 10; // scale_load_down avg (curr_vruntime - data.min_vruntime) * curr_weight; load curr_weight; } data.eligible (avg (s64)entity_key * load) ? 1 : 0; data.lag avg - (s64)entity_key * load; events.perf_submit(ctx, data, sizeof(data)); return 0; } # 加载 BPF b BPF(textbpf_text) b.attach_kprobe(evententity_eligible, fn_nametrace_entity_eligible) print(Tracing entity_eligible() calls... Ctrl-C to exit) print(%-18s %-6s %-16s %-8s %-12s %-12s %-8s % ( TIME, PID, COMM, ELIGIBLE, LAG, VRUNTIME, AVG_LOAD)) class Data(ct.Structure): _fields_ [ (ts, ct.c_ulonglong), (pid, ct.c_uint), (comm, ct.c_char * 16), (vruntime, ct.c_ulonglong), (min_vruntime, ct.c_ulonglong), (avg_vruntime, ct.c_longlong), (avg_load, ct.c_long), (eligible, ct.c_int), (lag, ct.c_ulonglong), ] def print_event(cpu, data, size): event ct.cast(data, ct.POINTER(Data)).contents print(%-18.9f %-6d %-16s %-8s %-12lld %-12llu %-8ld % ( event.ts / 1000000000.0, event.pid, event.comm.decode(utf-8, replace), YES if event.eligible else NO, event.lag, event.vruntime, event.avg_load)) b[events].open_perf_buffer(print_event) while True: try: b.perf_buffer_poll() except KeyboardInterrupt: break运行追踪# 需要 root 权限 sudo python3 entity_eligible_trace.py # 在另一个终端运行 CPU 密集型任务以观察调度行为 stress-ng --cpu 4 --timeout 30s5.5 模拟 lag 值变化的测试程序编写用户态程序测试不同负载下的 lag 行为// lag_test.c - 测试 lag 值和 eligibility 变化 #define _GNU_SOURCE #include stdio.h #include stdlib.h #include unistd.h #include sched.h #include sys/syscall.h #include linux/sched.h #include string.h #include time.h // 使用 sched_setattr 设置自定义 time slice struct sched_attr { __u32 size; __u32 sched_policy; __u64 sched_flags; __s32 sched_nice; __u32 sched_priority; __u64 sched_runtime; __u64 sched_deadline; __u64 sched_period; }; #ifndef __NR_sched_setattr #ifdef __x86_64__ #define __NR_sched_setattr 314 #define __NR_sched_getattr 315 #endif #endif static int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags) { return syscall(__NR_sched_setattr, pid, attr, flags); } // 绑定到指定 CPU void set_cpu_affinity(int cpu) { cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(cpu, cpuset); if (sched_setaffinity(0, sizeof(cpuset), cpuset) 0) { perror(sched_setaffinity); exit(1); } } // 消耗指定时间的 CPU void burn_cpu_ms(int ms) { struct timespec start, now; clock_gettime(CLOCK_MONOTONIC, start); while (1) { clock_gettime(CLOCK_MONOTONIC, now); long elapsed (now.tv_sec - start.tv_sec) * 1000 (now.tv_nsec - start.tv_nsec) / 1000000; if (elapsed ms) break; // 忙等待消耗 CPU volatile int x 0; for (int i 0; i 100000; i) x; } } int main(int argc, char *argv[]) { int mode argc 1 ? atoi(argv[1]) : 0; int cpu argc 2 ? atoi(argv[2]) : 0; set_cpu_affinity(cpu); printf(PID: %d, CPU: %d\n, getpid(), cpu); printf(Mode: %s\n, mode 0 ? Normal CFS : EEVDF with custom slice); if (mode 1) { // 设置 EEVDF 参数10ms runtime, 20ms deadline struct sched_attr attr { .size sizeof(attr), .sched_policy SCHED_OTHER, // 使用 CFS/EEVDF .sched_flags 0, .sched_nice 0, .sched_runtime 10 * 1000 * 1000, // 10ms in ns .sched_deadline 20 * 1000 * 1000, // 20ms in ns .sched_period 0 }; if (sched_setattr(0, attr, 0) 0) { perror(sched_setattr); printf(Falling back to normal CFS\n); } else { printf(Set sched_runtime10ms, sched_deadline20ms\n); } } printf(Starting CPU burn cycles...\n); printf(Observe /proc/lag_monitor (with PID %d) in another terminal\n\n, getpid()); // 交替运行和睡眠观察 lag 值变化 for (int i 0; i 10; i) { printf(Cycle %d: Running 100ms...\n, i); burn_cpu_ms(100); printf(Cycle %d: Sleeping 50ms...\n, i); usleep(50000); } printf(\nTest completed. Check lag values during sleep/wake transitions.\n); return 0; }编译与运行# 编译 gcc -o lag_test lag_test.c -O2 # 终端 1运行测试程序 sudo ./lag_test 1 0 # 模式1自定义sliceCPU0 # 终端 2监控 lag 值 watch -n 0.1 sudo cat /proc/lag_monitor | grep -A 10 PID: pid # 终端 3运行竞争任务 stress-ng --cpu 1 --taskset 0 --timeout 30s六、常见问题与解答Q1: entity_eligible 返回 false 是否意味着任务永远不会被调度解答不是。entity_eligible只表示任务当前不具备调度资格lag 0。随着系统虚拟时间V的推进其他任务运行该任务的 lag 值会逐渐增加。当 lag 变为非负时任务立即成为 eligible。// 在 pick_eevdf() 中的处理逻辑 if (se entity_eligible(cfs_rq, se)) { best se; // 立即选中 goto found; } // 如果不 eligible继续搜索红黑树 while (node) { // ... 寻找其他 eligible 任务 }Q2: 为什么使用 avg_vruntime 而不是直接计算加权平均解答直接计算V Σ(v_i × w_i) / Σw_i需要遍历所有任务时间复杂度 O(N)。内核使用增量维护的avg_vruntime和avg_load实现 O(1) 判定// 增量更新当任务入队/出队时 cfs_rq-avg_vruntime (vruntime - min_vruntime) * weight; cfs_rq-avg_load weight;Q3: 如何解释 lag numerator 为负但任务仍在运行解答当前运行任务cfs_rq-curr可能处于保护期protect slice即使 lag 为负也会继续运行直到时间片用完。这是为了避免过度频繁的上下文切换。Q4: 在多核系统中entity_eligible 的判断是否考虑其他 CPU解答entity_eligible仅基于本地cfs_rq的数据进行判定。跨 CPU 的负载均衡由load_balance()处理会在迁移任务时调用place_entity()重新计算 vruntime 和 lag。七、实践建议与最佳实践7.1 调试技巧使用 ftrace 追踪调度决策# 启用调度器事件追踪 sudo mount -t tracefs nodev /sys/kernel/tracing cd /sys/kernel/tracing # 启用相关事件 echo 1 events/sched/sched_switch/enable echo 1 events/sched/sched_wakeup/enable echo 1 events/sched/sched_stat_runtime/enable # 设置过滤器可选 echo comm your_app events/sched/sched_switch/filter # 查看实时追踪 cat trace_pipe # 或者记录到文件 echo 1 tracing_on # ... 运行测试 ... echo 0 tracing_on cat trace /tmp/sched_trace.txt分析 vruntime 分布# 查看所有任务的 vruntime grep -r vruntime /proc/sched_debug | head -20 # 计算 vruntime 范围判断公平性 awk /vruntime/ {print $2} /proc/sched_debug | sort -n | \ awk NR1{min$1} END{print Range:, $1-min, ns}7.2 性能优化建议避免过度睡眠-唤醒频繁睡眠的任务可能积累大量 lag唤醒后产生脉冲式 CPU 使用// 优化前频繁睡眠 for (i 0; i 1000; i) { do_small_work(); usleep(1000); // 1ms 睡眠 } // 优化后批量处理 for (i 0; i 1000; i) { do_small_work(); } usleep(1000000); // 集中睡眠合理使用 sched_setattr为延迟敏感任务设置合适的 runtime 和 deadlinestruct sched_attr attr { .sched_runtime 5 * 1000 * 1000, // 5ms .sched_deadline 10 * 1000 * 1000, // 10ms }; sched_setattr(pid, attr, 0);监控 lag 值趋势编写脚本监控任务的 lag 值变化#!/bin/bash # lag_monitor.sh PID$1 while true; do awk -v pid$PID /^Task.*PID:/{p0} $0 ~ PID: pid {p1} p /Lag numerator/{print strftime(%H:%M:%S), $0} \ /proc/lag_monitor sleep 1 done7.3 常见错误解决方案问题现象可能原因解决方案任务饥饿长期不调度vruntime 漂移过大检查min_vruntime更新逻辑重启任务高优先级任务延迟大权重计算错误检查 nice 值到 weight 的转换多核负载不均未考虑跨核迁移 lag检查place_entity的PLACE_REL_DEADLINE标志八、总结与应用场景8.1 核心要点回顾本文深入剖析了 Linux CFS/EEVDF 调度器中的entity_eligible函数及其核心的 lag 值判定机制数学基础entity_eligible实现了严格的公平性判定通过检查lag w × (V - v) 0确保任务只在其应得 CPU 时间未满足时获得调度资格。实现优化通过avg_vruntime和avg_load的增量维护将 O(N) 的遍历计算优化为 O(1) 的快速判定。实战价值理解这一机制有助于开发延迟敏感型应用、诊断调度异常、以及进行操作系统相关的学术研究。8.2 应用场景展望云原生调度在 Kubernetes 等容器编排平台中基于 lag 的调度策略可实现更精确的 CPU 配额控制实时系统结合SCHED_DEADLINEEEVDF 的 lag 机制可为软实时任务提供理论可证的延迟边界学术研究entity_eligible的严格数学定义为调度算法研究提供了可验证的基准适合作为论文中的案例分析掌握entity_eligible和 lag 机制不仅是理解现代 Linux 内核的关键更是构建高性能、低延迟系统的必备技能。建议读者结合本文提供的内核模块和 eBPF 工具在实际系统上进行观测和实验以深化对调度器行为的理解。

更多文章