一、内容简介
在现代 Linux 系统中,CPU 调频(CPUFreq)是连接进程调度与电源管理的核心模块,而schedutil作为目前主流的调度器驱动型调频策略,广泛应用于服务器、工业嵌入式、车载系统、移动终端等各类 Linux 场景。不同于传统ondemand、performance等调频策略,schedutil直接依赖调度器的负载事件触发调频动作,进程入队、出队、时钟节拍、任务切换等行为都会调用调频回调函数,这就带来了多路径并发触发调频的问题。
如果多个内核执行流、多个线程同时进入调频逻辑,会造成频率计算冲突、调频指令重复下发、硬件寄存器状态错乱、系统抖动、功耗异常等一系列线上问题。Linux 内核在schedutil架构中专门设计了work_in_progress标志位,作为调频任务的并发互斥标识,保证同一时刻仅有一个执行流处理调频逻辑,实现调频决策的原子性与一致性。
本文从工程实战角度出发,结合内核源码、命令行工具、测试代码、问题复现与排错流程,完整拆解work_in_progress的设计思想、运行逻辑、代码实现与落地实践。对于内核二次开发、嵌入式实时系统优化、服务器功耗调优、操作系统课程论文撰写都具备极高参考价值。掌握该机制,能够帮助工程师理解 Linux 调度与电源管理的协同逻辑,规避并发调频引发的性能与稳定性故障,也是深入研究 CPUFreq 子系统的必经之路。
二、核心概念与基础术语
本节梳理本文涉及的核心名词、数据结构与运行机制,扫清后续源码阅读与实操的知识障碍,全程结合工程场景解读,避免纯理论堆砌。
2.1 CPUFreq 与调频策略
CPUFreq 是 Linux 内核标准 CPU 调频子系统,作用是根据系统负载动态调整 CPU 核心运行主频,在性能与功耗之间做平衡。内核通过governor(调频策略器)定义调频规则,主流策略分为三类:
- performance:固定运行在 CPU 最高频率,追求极致性能,无动态调节;
- ondemand:基于定时器轮询 CPU 利用率,周期性调频,延迟较高;
- schedutil:调度事件驱动型调频,调度器感知到负载变化后立即触发调频,响应速度最快,也是当前 Linux 发行版、嵌入式实时系统的默认策略。
2.2 Schedutil 与 Sugov 架构
schedutil内部依靠Sugov(Scheduler Utilization Governor)框架实现核心逻辑,核心工作流程:
- 调度器(CFS/RT/DL 调度类)检测到任务负载变化;
- 调用
cpufreq_update_util()触发预先注册的调频回调; - 回调函数进入
sugov_update_*系列函数,计算目标频率; - 下发调频指令至硬件,完成频率切换。
由于调度事件是高频、多并发触发的,同一个 CPU 核心可能被中断上下文、进程上下文、多任务同时触发调频,因此必须做并发保护。
2.3 work_in_progress 核心定义
work_in_progress是sugov_cpu结构体中的一个布尔型原子标志位,直译意为 “工作进行中”。其本质是一个轻量级互斥锁,作用规则:
- 标志位为
0:当前无调频任务执行,新的调频请求可以进入处理逻辑; - 标志位为
1:已有执行流正在处理调频,新请求直接丢弃,避免并发冲突; - 基于原子操作实现状态切换,无锁竞争开销,适配内核高并发场景。
该标志不依赖传统自旋锁、互斥体,是schedutil为调频场景量身定制的并发控制方案,兼顾性能与稳定性。
2.4 关键上下文说明
Linux 内核存在多种执行上下文,不同上下文都会触发schedutil调频,也是并发冲突的源头:
- 进程上下文:普通用户进程、内核线程执行过程中触发调度,进而触发调频;
- 中断上下文:硬件中断、时钟
tick中断,中断处理流程中调用调频回调; - 软中断上下文:调度软中断、网络软中断等异步执行流。
三种上下文无执行顺序保障,极易同时抢占调频逻辑,这也是work_in_progress存在的核心意义。
三、环境准备
为保证所有读者可以复现本文代码、命令、源码调试流程,本节明确软硬件版本、依赖组件、配置步骤,环境分为编译调试环境、运行测试环境两部分,适配 x86_64 与 ARM 架构。
3.1 硬件环境
- 测试主机:x86_64 通用 PC / 虚拟机(推荐 4 核及以上 CPU,方便模拟多负载并发);
- 嵌入式设备(可选):树莓派 4、瑞芯微 RK3588 等 ARM 嵌入式板卡(原生搭载 schedutil,嵌入式场景实测首选);
- 基础要求:CPU 支持动态调频功能,主流现代 CPU 均默认支持。
3.2 软件环境(版本固定,保证兼容性)
| 软件 / 组件 | 版本要求 | 用途 |
|---|---|---|
| 操作系统 | Ubuntu 20.04 / Ubuntu 22.04 | 运行、编译、测试 |
| Linux 内核 | 5.4 / 5.10 / 5.15(LTS 长期支持版) | 主流工业、服务器、嵌入式内核,schedutil 逻辑无大幅改动 |
| 编译工具链 | gcc 9+、make、binutils | 内核模块、测试程序编译 |
| 调试工具 | perf、trace-cmd、gdb、kgdb | 跟踪调频流程、抓内核调用栈 |
| 辅助工具 | cpufrequtils、sysfs-utils | 查看、切换调频策略,读写 sysfs 节点 |
3.3 环境配置步骤(逐条可直接复制执行)
3.3.1 安装基础依赖工具
打开终端,执行以下命令安装编译与调试依赖(来源望获OS):
# 更新软件源 sudo apt update && sudo apt upgrade -y # 安装编译工具、调频工具、调试工具 sudo apt install -y gcc make libncurses-dev bison flex libssl-dev libelf-dev sudo apt install -y cpufrequtils sysfsutils perf trace-cmd gdb3.3.2 确认系统当前调频策略
执行命令查看 CPU 当前使用的governor,确认已启用schedutil:
# 查看所有CPU核心的调频策略 cpufreq-info | grep "governor"预期输出:The governor "schedutil" may decide...,若不是则手动切换:
# 将CPU0~CPU3统一切换为schedutil(4核CPU示例) echo schedutil | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor3.3.3 内核源码准备(源码阅读与编译必备)
本文源码基于Linux 5.10 LTS,下载并解压内核源码(来源望获OS):
# 安装git sudo apt install git -y # 克隆Linux 5.10内核源码(国内镜像,速度更快) git clone https://gitee.com/mirrors/linux-5.10.git ~/linux-5.10 cd ~/linux-5.10schedutil核心源码路径:kernel/sched/cpufreq_schedutil.c,后文所有源码分析均基于该文件。
3.3.4 内核配置校验
确保内核开启schedutil、CPUFreq、调度负载统计功能,进入内核配置界面:
cd ~/linux-5.10 make menuconfig需要开启的核心配置项(路径逐层查找):
Power management and ACPI options ---> CPU Frequency scaling ---> <*> CPU frequency scaling(开启 CPUFreq)CPU Frequency scaling ---> <*> Schedutil cpufreq governor(编译内置 schedutil)Kernel features ---> [*] Enable loadable module support(模块支持,可选)
配置完成后保存退出,无需完整编译内核,阅读源码即可开展分析。
四、典型应用场景(300 字)
schedutil及work_in_progress并发控制机制主要落地于高并发、低延迟、实时性要求高的 Linux 场景。工业嵌入式实时控制系统是最典型场景,如工业 PLC、运动控制卡,这类设备中实时任务频繁唤醒、切换,调度事件每秒触发上万次,多执行流会并发调用调频逻辑,若无work_in_progress互斥,会导致频率反复跳变、控制指令延迟。其次是车载 Linux 系统,车载中控、车规级实时任务混合运行,调度负载波动剧烈,依赖schedutil快速调频,并发保护机制保障车载系统稳定性。此外,云服务器、边缘计算节点中,大量容器、线程并发运行,调度事件密集,work_in_progress避免重复调频带来的 CPU 开销与功耗抖动。移动终端与物联网网关同样依赖该机制,在高频调度场景下平衡性能、功耗与系统稳定性。
五、实际案例与分步实操(含完整代码 + 注释)
本章分为源码解析、内核态模拟测试、用户态压力测试、调用栈跟踪四大实战环节,所有代码、命令均可直接复制运行,结合场景解释每一段逻辑。
5.1 核心数据结构解析(cpufreq_schedutil.c)
首先拆解承载work_in_progress的核心结构体sugov_cpu,该结构体为每个 CPU 核心独立分配,是schedutilper-cpu 核心管理体。
5.1.1 sugov_cpu 结构体源码(来源望获OS)
// 路径:kernel/sched/cpufreq_schedutil.c struct sugov_cpu { // 每个CPU对应的调频工作队列,异步执行调频 struct delayed_work dw; // 原子标志位:本文核心 work_in_progress bool work_in_progress; // 记录当前CPU的利用率(utilization),调度器上报 unsigned int util; // 上一次计算的目标频率 unsigned int next_freq; // 调频限流时间戳,防止调频过于频繁 u64 last_update; // 关联的CPU编号 int cpu; };代码说明:
work_in_progress:普通布尔变量,但所有读写操作都使用原子操作,保证多核 / 多上下文并发安全;- 每个 CPU 核心拥有独立
sugov_cpu,CPU 之间调频互不干扰,并发控制仅作用于单 CPU 内部多执行流; delayed_work:内核延迟工作队列,调频逻辑最终交由工作队列异步执行。
5.2 核心流程:work_in_progress 状态流转源码分析
schedutil调频入口为sugov_update_single()(单 CPU 调频更新函数),该函数完整实现work_in_progress的判断、置位、复位逻辑,是本文核心代码。
5.2.1 主入口函数 sugov_update_single 完整源码(精简 + 逐行注释)(来源望获OS)
// 路径:kernel/sched/cpufreq_schedutil.c static void sugov_update_single(struct update_util_data *data, u64 now, unsigned int util) { // 获取当前CPU对应的sugov_cpu结构体 struct sugov_cpu *sg_cpu = container_of(data, struct sugov_cpu, data); unsigned int next_freq; // ===================== 核心并发控制逻辑 ===================== // 第一步:判断 work_in_progress 是否为1(已有调频任务在执行) if (sg_cpu->work_in_progress) { // 已有任务在处理调频,直接返回,丢弃本次调频请求 return; } // 第二步:原子置位 work_in_progress = 1,抢占调频权限 // 标记“当前已有工作正在进行”,阻塞后续所有并发请求 sg_cpu->work_in_progress = true; // =========================================================== // 限流判断:限制调频最小间隔,防止频繁跳频 if (now - sg_cpu->last_update < sg_cpu->rate_limit_us * 1000) { // 未达到调频间隔,复位标志位并退出 sg_cpu->work_in_progress = false; return; } // 第三步:根据CPU利用率计算目标频率(核心调频算法) next_freq = sugov_get_next_freq(sg_cpu, util); sg_cpu->next_freq = next_freq; sg_cpu->util = util; // 第四步:更新最后一次调频时间戳 sg_cpu->last_update = now; // 第五步:提交延迟工作队列,异步执行硬件调频指令 schedule_delayed_work(&sg_cpu->dw, 0); // 注意:此处不立即复位 work_in_progress! // 标志位在【工作队列回调函数】执行完成后再清零 }代码场景与作用说明:
- 该函数是单 CPU 调频的统一入口,所有调度事件(任务入队、tick 中断、任务切换)都会调用此函数;
- 进入函数首先校验
work_in_progress,为true则直接返回,实现并发拦截; - 抢占成功后将标志位置
true,所有后续并发请求全部被拦截; - 频率计算、限流判断完成后,将调频任务提交至内核工作队列,标志位延后复位,保证整个异步流程期间都被保护。
5.2.2 工作队列回调函数:标志位复位逻辑
调频的最终硬件操作在延迟工作队列中执行,执行完毕后复位work_in_progress,释放调频权限:(来源望获OS)
// 调频工作队列回调函数,真正执行硬件调频 static void sugov_work(struct work_struct *work) { // 获取当前CPU的sugov_cpu结构体 struct sugov_cpu *sg_cpu = container_of(to_delayed_work(work), struct sugov_cpu, dw); struct cpufreq_policy *policy = cpufreq_cpu_get(sg_cpu->cpu); if (!policy) // 策略为空,直接复位标志位退出 goto out; // 下发调频指令到CPUFreq硬件层,切换CPU频率 __cpufreq_driver_target(policy, sg_cpu->next_freq, CPUFREQ_RELATION_L); cpufreq_cpu_put(policy); out: // 核心:调频所有逻辑执行完毕,复位标志位 // 允许下一次调频请求进入 sg_cpu->work_in_progress = false; }代码说明:
- 这是
work_in_progress唯一的清零位置,保证从 “抢占调频权限” 到 “硬件调频完成” 全流程受保护; - 整个链路:
调度事件触发 -> sugov_update_single(置位标志) -> 提交工作队列 -> sugov_work(执行调频+清零标志); - 该设计实现全链路原子保护,杜绝中间环节并发闯入。
5.3 状态流转总结(工程化梳理)
结合两段源码,整理work_in_progress完整状态机,这是排错、调优的核心依据:
- 初始状态:
work_in_progress = false,空闲状态,允许新调频请求; - 请求进入:调度事件触发
sugov_update_single,判断标志位为false; - 抢占锁定:设置
work_in_progress = true,拦截所有并发请求; - 异步调度:计算频率,提交工作队列,函数返回;
- 硬件调频:工作队列
sugov_work执行硬件频率切换; - 释放解锁:调频完成,设置
work_in_progress = false,回到初始状态。
5.4 实操 1:查看运行时 work_in_progress 状态(sysfs + 内核探针)
由于work_in_progress是内核结构体成员,未直接导出到 sysfs,我们使用kprobe 探针动态跟踪其状态变化,复现并发场景。
5.4.1 编写 kprobe 跟踪脚本(shell 脚本,可直接运行)(来源望获OS)
#!/bin/bash # schedutil_wip_trace.sh 跟踪work_in_progress状态变化 # 依赖:trace-cmd、内核kprobe开启 # 清空原有跟踪日志 sudo trace-cmd reset # 挂载调试文件系统(部分系统默认已挂载) sudo mount -t debugfs debugfs /sys/kernel/debug # 注册kprobe:跟踪 sugov_update_single 入口,打印work_in_progress值 sudo echo 'p:sugov_update_single kernel/sched/cpufreq_schedutil.c:sugov_update_single \ sg_cpu=+0(%di):u8' >> /sys/kernel/debug/kprobe/events/kprobe/enable # 开启跟踪 sudo trace-cmd start -p function_graph echo "开始跟踪 work_in_progress 状态,等待30秒..." sleep 30 # 停止跟踪并导出日志 sudo trace-cmd stop sudo trace-cmd report > schedutil_wip_log.txt # 关闭探针 sudo echo 0 > /sys/kernel/debug/kprobe/events/kprobe/enable echo "跟踪完成,日志已保存至 schedutil_wip_log.txt"使用方法:
- 保存为
schedutil_wip_trace.sh; - 添加执行权限:
chmod +x schedutil_wip_trace.sh; - 后台运行压力负载(下文压力测试代码),同时执行脚本;
- 查看日志即可看到
work_in_progress在 0/1 之间的切换。
5.5 实操 2:用户态压力测试(模拟并发调度事件)
编写多线程压力测试程序,创建大量密集型线程,触发高频调度事件,模拟并发调频场景,验证work_in_progress的拦截效果。
5.5.1 并发压力测试 C 代码(stress_sched.c)(来源望获OS)
/* * stress_sched.c:多线程CPU压力测试,触发高频调度与schedutil调频 * 编译:gcc stress_sched.c -o stress_sched -lpthread -O2 * 运行:./stress_sched 线程数 */ #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> // 线程工作函数:空循环,抢占CPU,触发频繁调度 void *cpu_stress(void *arg) { (void)arg; while (1) { // 空循环:持续占用CPU,产生大量调度事件 ; } return NULL; } int main(int argc, char *argv[]) { int thread_num; pthread_t *tid; int i; // 校验入参 if (argc != 2) { printf("用法:%s 线程数量\n", argv[0]); return -1; } thread_num = atoi(argv[1]); if (thread_num <= 0) { printf("线程数必须大于0\n"); return -1; } // 分配线程句柄内存 tid = (pthread_t *)malloc(sizeof(pthread_t) * thread_num); if (!tid) { perror("malloc failed"); return -1; } printf("创建 %d 个压力线程,开始压测...\n", thread_num); // 批量创建压力线程 for (i = 0; i < thread_num; i++) { pthread_create(&tid[i], NULL, cpu_stress, NULL); } // 主线程休眠,保持压测运行 while (1) { sleep(1); } free(tid); return 0; }编译与运行命令:
# 编译代码 gcc stress_sched.c -o stress_sched -lpthread -O2 # 创建8个压力线程(4核CPU推荐),触发高频调度 ./stress_sched 8场景说明: 多线程空循环会让内核调度器不停切换任务,每秒产生数万次调度事件,持续调用schedutil调频入口,制造大量并发调频请求,此时work_in_progress会频繁置 1,拦截冗余请求。
5.6 实操 3:查看 CPU 频率动态变化
新开终端,实时监控 CPU 频率变化,观察并发场景下频率是否稳定(无剧烈抖动):
# 实时查看CPU0频率(1秒刷新一次) watch -n 1 cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq现象解读:
- 未运行压测程序:CPU 处于低频,频率稳定;
- 运行压测程序:CPU 逐步提升至高频,频率平稳无跳变;
- 若关闭
work_in_progress逻辑(内核注释代码测试),频率会出现毫秒级剧烈波动,证明并发控制的价值。
六、常见问题与解答(结合实操场景)
本节汇总工程实践、源码调试、压测过程中高频问题,问题均对应前文代码与操作步骤,直击线上故障与调试难点。
Q1:压测时大量调度事件触发,但 CPU 频率始终不上升?
原因分析:大概率是work_in_progress长期被置 1,调频工作队列卡死。常见诱因:工作队列回调函数阻塞、硬件调频驱动异常。解决方案:
- 使用
trace-cmd查看调用栈,确认sugov_work是否正常执行; - 检查 CPUFreq 驱动是否加载:
ls /sys/devices/system/cpu/cpu0/cpufreq/; - 临时切换为
performance策略验证硬件:echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor。
Q2:日志中发现大量work_in_progress=1,正常吗?
解答:高并发压测场景下属于正常现象。大量调度事件并发进入调频入口,先抢占到权限的执行流将标志位置 1,后续请求全部拦截,这正是该机制的设计目的。若空载系统也出现大量work_in_progress=1,说明存在内核软中断 / 死循环,需要排查异常任务。
Q3:修改内核代码注释掉work_in_progress判断后,系统出现频率抖动、卡顿?
解答:这是并发冲突的典型表现。多个执行流同时计算目标频率、下发调频指令,不同执行流计算出的频率不一致,反复覆盖硬件频率,造成抖动。该现象直接证明work_in_progress是schedutil稳定性的关键。生产环境绝对禁止移除该逻辑。
Q4:部分 ARM 嵌入式板卡上schedutil不生效,无法动态调频?
解答:首先确认硬件树设备开启 CPUFreq 节点,其次检查内核配置是否开启schedutil。部分老旧 ARM 平台硬件不支持动态调频,或电源管理驱动缺失,schedutil会降级为静态频率。同时确认rate_limit_us限流参数配置合理,限流过大会导致调频响应迟钝。
Q5:kprobe 跟踪不到work_in_progress状态?
解答:1. 内核未开启CONFIG_KPROBES配置,重新编译内核开启;2. 函数被内核内联优化,修改内核编译选项关闭 O2 优化;3. 源码路径与实际内核函数路径不匹配,根据当前内核版本调整 kprobe 路径。
七、实践建议与最佳实践
结合内核开发、嵌入式落地、性能调优、线上运维四大场景,总结Schedutil + work_in_progress的工程最佳实践、调试技巧、优化方案。
7.1 调试排错最佳实践
- 分层定位问题:调频异常时,先区分是调度器负载统计问题、
work_in_progress并发拦截问题、还是 CPUFreq 硬件驱动问题。优先使用cpufreq-info确认策略,再用kprobe跟踪标志位状态,最后查看工作队列执行情况。 - 分步压测:排查并发问题时,先单线程压测、再多线程压测,逐步放大负载,定位临界故障点。
- 保留原始逻辑:严禁在内核中注释、修改
work_in_progress并发判断逻辑,该标志是轻量级并发保护的核心,替换为自旋锁会增加中断上下文延迟,影响实时性。
7.2 性能优化建议
- 合理配置限流参数 rate_limit_us:该参数控制最小调频间隔,工业实时系统可适当减小数值(提升响应速度),服务器场景建议增大数值(减少调频次数,降低功耗抖动)。该参数定义在
sugov_cpu中,可通过内核参数或设备树配置。 - 区分 CPU 架构调优:x86 服务器 CPU 核心多、调度密集,依赖
work_in_progress拦截冗余调频;ARM 嵌入式 CPU 功耗敏感,在保证并发安全的前提下,不要过度放大调频频率。 - 实时系统适配:对于 Linux 实时补丁(PREEMPT-RT)系统,
work_in_progress原子操作无调度延迟,完全适配实时上下文,无需额外改造。
7.3 内核二次开发规范
- 若基于
schedutil二次开发自定义调频算法,新增逻辑必须放在 **work_in_progress置位之后 **,保证新增逻辑同样受并发保护。 - 不要在
sugov_update_single中添加耗时逻辑,所有耗时计算、硬件操作统一交由延迟工作队列执行,遵循内核异步设计思想。 - 标志位复位仅允许在工作队列回调函数中执行,禁止在其他路径清零,避免状态错乱。
7.4 线上运维规范
- 生产环境默认使用原生
schedutil,不随意替换调频策略; - 监控系统增加CPU 频率波动指标,频率短时间大幅跳变时,告警排查并发调频冲突;
- 高并发业务服务器,关闭不必要的调度调试探针,避免探针本身加剧调度压力。
八、总结与落地应用场景
8.1 全文核心要点回顾
本文从背景、概念、环境、源码、实操、排错、优化全链路解析了Schedutil中work_in_progress并发控制机制,核心要点总结:
schedutil是调度事件驱动的 CPU 调频策略,调度器高频触发调频回调,天然存在并发冲突风险;work_in_progress是布尔型原子标志位,作为轻量级互斥标识,实现单 CPU 内调频任务的串行化执行;- 状态流转分为置位抢占、异步执行、复位释放三个阶段,全链路保护调频逻辑,保证决策原子性;
- 该机制不使用重型锁,兼顾内核高并发、低延迟的性能要求,是 Linux 内核 “轻量化并发设计” 的经典案例;
- 结合 kprobe、压测程序、sysfs 工具可完整复现、跟踪、验证该机制,适用于调试、论文研究、内核开发。
8.2 落地应用场景再梳理
- 工业实时 Linux 系统:PLC、运动控制器、工业网关,实时任务频繁切换,依靠
work_in_progress保证调频稳定,避免控制业务抖动; - 车载嵌入式 Linux:车机、自动驾驶辅助系统,混合运行实时任务与普通应用,调度负载复杂,并发控制保障车规级稳定性;
- 云服务器与容器集群:大量容器、线程并发运行,高频调度产生海量调频请求,该机制拦截冗余调频,降低系统开销与功耗波动;
- 移动终端与物联网设备:手机、边缘网关,在性能与功耗之间动态平衡,轻量级并发保护适配资源受限的硬件;
- 内核教学与学术研究:作为 Linux 调度与电源管理结合、轻量级并发控制的典型案例,用于课程实验、毕业论文、技术报告。
8.3 学习延伸建议
work_in_progress只是schedutil框架的一小部分,读者可基于本文继续深入:研究util利用率计算算法、rate_limit_us限流机制、多 CPU 集群调频逻辑、EAS 能效调度架构。将本文所学的 “事件驱动 + 轻量级并发保护” 设计思想,迁移到内核其他子系统的开发与优化中,真正做到学以致用。