Linux CFS 抢占策略:WAKEUP_PREEMPTION 与 RUN_TO_PARITY 的权衡

张开发
2026/4/5 10:33:17 15 分钟阅读

分享文章

Linux CFS 抢占策略:WAKEUP_PREEMPTION 与 RUN_TO_PARITY 的权衡
一、简介在现代多任务操作系统中调度器Scheduler是决定系统性能与响应性的核心组件。Linux内核采用的完全公平调度器Completely Fair Scheduler, CFS自2.6.23版本引入以来一直是普通任务SCHED_NORMAL/SCHED_OTHER的默认调度策略。CFS摒弃了传统的时间片轮转机制转而基于虚拟运行时间vruntime的红黑树数据结构实现了理论上完全公平的CPU时间分配。然而纯粹的公平性并非唯一目标。在实际生产环境中系统往往需要在响应性Responsiveness与吞吐量Throughput之间做出权衡。当任务被唤醒时是否应该立即抢占当前运行的任务如果抢占过于激进可能导致频繁的上下文切换降低CPU缓存命中率损害整体吞吐量如果抢占过于保守则可能导致交互式任务响应延迟影响用户体验。本文深入剖析Linux CFS调度器的抢占触发机制重点分析WAKEUP_PREEMPTION、RUN_TO_PARITY等调度特性开关的作用并通过实际案例演示如何在不同场景下调整这些参数以平衡系统的响应性与吞吐量。掌握这些知识对于从事内核开发、系统性能调优、实时系统设计的开发者具有重要的实践价值。二、核心概念2.1 CFS调度器基础CFS调度器的核心思想是完全公平——每个可运行任务按照其权重比例获得CPU时间。调度器维护一个以vruntime为键值的红黑树每次选择vruntime最小的任务执行。vruntime的计算公式为vruntime delta_exec * (NICE_0_LOAD / weight)其中delta_exec是实际执行时间weight由nice值决定nice 0对应权重1024nice -20对应权重88761nice 19对应权重15。权重越大的任务vruntime增长越慢从而获得更多CPU时间。2.2 抢占触发条件Linux CFS调度器在以下三种主要场景下触发抢占检查1. 任务唤醒Task Wakeup当任务从睡眠状态被唤醒时调度器会调用check_preempt_wakeup_fair()函数判断新唤醒的任务是否应该抢占当前运行的任务。这是本文重点分析的场景。2. 时钟中断Timer Tick周期性时钟中断由sched_tick()处理会更新当前任务的vruntime并检查是否超出其应得的时间配额从而触发抢占。3. 优先级变化Priority Change当任务通过nice()系统调用或其他机制改变优先级时调度器会重新评估其调度资格可能触发抢占。2.3 关键调度特性sched_featuresLinux内核通过sched_features机制控制调度器的各种行为特性。这些特性定义在kernel/sched/features.h中可以通过debugfs接口在运行时动态开关。与抢占策略相关的关键特性包括WAKEUP_PREEMPTION允许在任务唤醒时抢占当前运行任务RUN_TO_PARITY控制当前任务是否可以运行到其lag归零即达到公平状态PLACE_LAG控制新加入任务是否获得lag补偿RESPECT_SLICEEEVDF中抑制抢占直到当前任务耗尽其时间片2.4 唤醒抢占粒度Wakeup Granularitysched_wakeup_granularity_ns是控制唤醒抢占的关键参数。它定义了新唤醒任务与当前任务的vruntime差距阈值只有当新任务的vruntime加上这个粒度值仍小于当前任务的vruntime时才会触发抢占。公式表示为if (vnew G vcurr) then preempt其中G就是sched_wakeup_granularity_ns。增大该值会减少唤醒抢占有利于计算密集型任务减小该值会增加唤醒抢占有利于交互式任务。2.5 EEVDF调度器演进从Linux 6.6开始CFS逐步演进为EEVDFEarliest Eligible Virtual Deadline First调度器。EEVDF引入了虚拟截止时间Virtual Deadline, VD和资格性Eligibility概念取代了传统的vruntime比较逻辑。在EEVDF中任务只有在vruntime deadline时才被认为是有资格eligible运行的。三、环境准备3.1 硬件环境CPU建议多核处理器4核及以上以便观察任务迁移和负载均衡行为内存至少4GB RAM用于运行多个测试进程存储普通SSD即可用于保存内核源码和编译产物3.2 软件环境操作系统要求Linux内核版本 6.6EEVDF调度器已合并或 Linux内核 5.x-6.5传统CFS调度器检查当前内核版本uname -r # 输出示例6.8.0-40-generic必要工具安装# Ubuntu/Debian sudo apt-get update sudo apt-get install -y linux-headers-$(uname -r) build-essential git bc \ bison flex libncurses-dev libssl-dev debugfs-utils trace-cmd # RHEL/CentOS/Fedora sudo yum install -y kernel-headers-$(uname -r) gcc git make bc bison \ flex ncurses-devel openssl-devel trace-cmd3.3 内核编译配置可选如需修改调度器参数并重新编译内核# 获取内核源码 git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git cd linux # 配置内核选项 make menuconfig # 确保以下选项已启用 # - CONFIG_SCHED_DEBUGy 启用调度器调试接口 # - CONFIG_SCHEDSTATSy 启用调度统计信息 # - CONFIG_PREEMPTy 抢占模式选择 # 编译内核 make -j$(nproc) sudo make modules_install sudo make install3.4 调试接口准备挂载debugfs通常已自动挂载sudo mount -t debugfs none /sys/kernel/debug # 验证调度器特性接口 ls /sys/kernel/debug/sched/features查看当前调度器特性状态cat /sys/kernel/debug/sched/features # 输出示例 # PLACE_LAG PLACE_DEADLINE_INITIAL RUN_TO_PARITY WAKEUP_PREEMPTION ...四、应用场景4.1 数据库服务器场景在MySQL、PostgreSQL等数据库服务器中通常存在两类典型负载前台查询线程处理客户端SQL请求对响应延迟敏感要求快速唤醒执行后台刷盘线程执行checkpoint、WAL写入等操作属于批处理性质对延迟不敏感但需要稳定吞吐量在此场景下WAKEUP_PREEMPTION的开启对前台查询线程至关重要。当客户端请求到达网络中断唤醒监听线程后该线程需要立即抢占当前运行的后台线程以最小化查询响应时间。然而如果抢占过于激进后台刷盘线程频繁被中断可能导致I/O吞吐量下降甚至影响事务持久性。通过调整sched_wakeup_granularity_ns参数可以在两类线程间取得平衡。例如将粒度设置为较小值如1ms优先保证查询响应或设置为较大值如10ms优先保证刷盘吞吐量。此外对于MySQL 8.0可以结合SCHED_BATCH策略标记后台线程使其自动放弃唤醒抢占减少对前台线程的干扰。4.2 实时音视频处理场景在视频会议、直播推流等场景中存在严格的时延约束。音视频编解码任务通常以固定帧率如30fps每帧33ms产生如果调度延迟超过帧间隔将导致画面卡顿。此类场景下RUN_TO_PARITY特性的行为直接影响用户体验。当编解码任务被周期性唤醒时如果当前运行的任务如日志清理、数据统计可以运行到公平run to parity编解码任务可能需要等待当前任务主动让出CPU这可能导致数十毫秒的延迟超过帧间隔要求。通过禁用RUN_TO_PARITY即启用NO_RUN_TO_PARITY可以允许编解码任务在唤醒时立即抢占当前任务只要其具有更早的虚拟截止时间从而保证帧处理的及时性。当然这需要配合WAKEUP_PREEMPTION的开启才能生效。五、实际案例与步骤5.1 查看与修改调度器特性步骤1查看当前调度特性# 查看所有可用的调度特性 cat /sys/kernel/debug/sched/features # 典型输出Linux 6.8 # PLACE_LAG PLACE_DEADLINE_INITIAL RUN_TO_PARITY WAKEUP_PREEMPTION # HRTICK HRTICK_DL NONTASK_CAPACITY TTWU_QUEUE SIS_UTIL WARN_DOUBLE_CLOCK步骤2动态修改调度特性# 切换到root用户 sudo -i # 禁用WAKEUP_PREEMPTION需要root权限 echo NO_WAKEUP_PREEMPTION /sys/kernel/debug/sched/features # 验证修改 cat /sys/kernel/debug/sched/features # 确认WAKEUP_PREEMPTION不再出现在列表中 # 重新启用WAKEUP_PREEMPTION echo WAKEUP_PREEMPTION /sys/kernel/debug/sched/features # 同时禁用多个特性以空格分隔 echo NO_WAKEUP_PREEMPTION NO_RUN_TO_PARITY /sys/kernel/debug/sched/features步骤3设置sysctl参数# 查看当前调度参数 sysctl -a | grep sched # 修改唤醒抢占粒度单位纳秒 # 减小此值增加抢占频率提高响应性 sudo sysctl kernel.sched_wakeup_granularity_ns1000000 # 1ms # 修改最小调度粒度 sudo sysctl kernel.sched_min_granularity_ns4000000 # 4ms # 修改调度延迟目标 sudo sysctl kernel.sched_latency_ns24000000 # 24ms # 使配置持久化重启后生效 echo kernel.sched_wakeup_granularity_ns1000000 | sudo tee -a /etc/sysctl.conf5.2 编写测试程序验证抢占行为测试程序测量唤醒延迟// wakeup_latency.c // 编译gcc -o wakeup_latency wakeup_latency.c -lpthread -O2 #define _GNU_SOURCE #include stdio.h #include stdlib.h #include pthread.h #include time.h #include unistd.h #include sched.h #include string.h #include sys/syscall.h #define ITERATIONS 1000 #define NS_PER_SEC 1000000000L static inline long long get_ns() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return ts.tv_sec * NS_PER_SEC ts.tv_nsec; } // CPU密集型任务干扰任务 void *cpu_hog(void *arg) { cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(0, cpuset); pthread_setaffinity_np(pthread_self(), sizeof(cpuset), cpuset); // 设置低优先级nice 10 nice(10); volatile long long counter 0; while (1) { counter; } return NULL; } // 测量唤醒延迟的任务 void *latency_tester(void *arg) { cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(0, cpuset); pthread_setaffinity_np(pthread_self(), sizeof(cpuset), cpuset); // 设置高优先级nice -10 nice(-10); long long *latencies (long long*)arg; for (int i 0; i ITERATIONS; i) { struct timespec sleep_time {0, 1000000}; // 睡眠1ms long long sleep_start get_ns(); nanosleep(sleep_time, NULL); long long wake_time get_ns(); // 计算唤醒延迟从应该醒来到实际执行的时间 latencies[i] wake_time - sleep_start - 1000000; } return NULL; } int main() { pthread_t hog_thread, test_thread; long long latencies[ITERATIONS]; printf( 唤醒延迟测试 \n); printf(测试环境CPU 0上运行CPU密集型任务 周期性睡眠任务\n); printf(迭代次数%d\n\n, ITERATIONS); // 创建CPU密集型干扰线程 pthread_create(hog_thread, NULL, cpu_hog, NULL); // 给干扰线程时间启动 usleep(100000); // 创建测试线程 pthread_create(test_thread, NULL, latency_tester, latencies); pthread_join(test_thread, NULL); // 计算统计数据 long long sum 0, min latencies[0], max latencies[0]; for (int i 0; i ITERATIONS; i) { sum latencies[i]; if (latencies[i] min) min latencies[i]; if (latencies[i] max) max latencies[i]; } double avg (double)sum / ITERATIONS; // 计算百分位数 qsort(latencies, ITERATIONS, sizeof(long long), (int(*)(const void*, const void*))strcmp); long long p50 latencies[ITERATIONS / 2]; long long p99 latencies[ITERATIONS * 99 / 100]; printf(唤醒延迟统计微秒\n); printf( 最小值%lld\n, min / 1000); printf( 平均值%.2f\n, avg / 1000); printf( P50 %lld\n, p50 / 1000); printf( P99 %lld\n, p99 / 1000); printf( 最大值%lld\n, max / 1000); // 终止干扰线程实际应用中需要更优雅的退出机制 pthread_cancel(hog_thread); return 0; }编译与运行# 编译测试程序 gcc -o wakeup_latency wakeup_latency.c -lpthread -O2 # 以root权限运行需要设置nice值 sudo ./wakeup_latency # 在不同调度特性配置下重复测试 sudo bash -c echo NO_WAKEUP_PREEMPTION /sys/kernel/debug/sched/features sudo ./wakeup_latency result_no_preempt.txt sudo bash -c echo WAKEUP_PREEMPTION /sys/kernel/debug/sched/features sudo ./wakeup_latency result_with_preempt.txt # 对比结果 echo 无抢占 ; cat result_no_preempt.txt echo 有抢占 ; cat result_with_preempt.txt5.3 使用trace-cmd分析调度事件步骤1安装与基本使用# 安装trace-cmd sudo apt-get install trace-cmd # 查看可用的调度跟踪事件 sudo trace-cmd list | grep sched # 关键事件 # sched_wakeup - 任务被唤醒 # sched_switch - 任务切换 # sched_migrate_task - 任务迁移步骤2记录调度事件# 开始记录调度事件持续10秒 sudo trace-cmd record -e sched_wakeup -e sched_switch -e sched_migrate_task \ -e sched_wakeup_new sleep 10 # 这将生成trace.dat文件 ls -lh trace.dat步骤3分析唤醒延迟# 查看跟踪报告 sudo trace-cmd report | head -100 # 输出示例 # idle-0 [000] d.h. 1000.123456: sched_wakeup: commwakeup_latency pid12345 ... # wakeup_latency-12345 [000] d... 1000.124567: sched_switch: prevswapper/0 nextwakeup_latency ... # 计算延迟sched_switch的时间戳 - sched_wakeup的时间戳步骤4编写分析脚本#!/usr/bin/env python3 # analyze_sched.py - 分析sched跟踪数据 import re import sys from collections import defaultdict if len(sys.argv) 2: print(fUsage: {sys.argv[0]} trace.dat.report) sys.exit(1) wakeup_times {} latencies [] with open(sys.argv[1], r) as f: for line in f: # 解析sched_wakeup事件 wakeup_match re.search( r(\d\.\d): sched_wakeup: comm(\S) pid(\d), line) if wakeup_match: ts float(wakeup_match.group(1)) pid int(wakeup_match.group(3)) wakeup_times[pid] ts continue # 解析sched_switch事件任务开始运行 switch_match re.search( r(\d\.\d): sched_switch:.*?next_pid(\d), line) if switch_match: ts float(switch_match.group(1)) pid int(switch_match.group(2)) if pid in wakeup_times: latency (ts - wakeup_times[pid]) * 1000000 # 转换为微秒 latencies.append(latency) del wakeup_times[pid] if latencies: latencies.sort() avg_latency sum(latencies) / len(latencies) p50 latencies[len(latencies) // 2] p99 latencies[int(len(latencies) * 0.99)] print(f唤醒延迟统计微秒) print(f 样本数{len(latencies)}) print(f 平均值{avg_latency:.2f}) print(f P50 {p50:.2f}) print(f P99 {p99:.2f}) print(f 最大值{latencies[-1]:.2f}) else: print(未找到有效的唤醒事件)5.4 调整RUN_TO_PARITY特性实验观察RUN_TO_PARITY对吞吐量的影响# 使用hackbench测试调度器性能 sudo apt-get install rt-tests # 或从源码编译hackbench # 测试1启用RUN_TO_PARITY默认 echo RUN_TO_PARITY | sudo tee /sys/kernel/debug/sched/features echo 启用RUN_TO_PARITY hackbench -l 1000 -g 20 # 20组每组1000次消息传递 # 测试2禁用RUN_TO_PARITY echo NO_RUN_TO_PARITY | sudo tee /sys/kernel/debug/sched/features echo 禁用RUN_TO_PARITY hackbench -l 1000 -g 20结果分析启用RUN_TO_PARITY当前任务会运行直到其lag归零达到公平状态减少了上下文切换提高吞吐量但可能增加唤醒延迟禁用RUN_TO_PARITY允许有资格的唤醒任务立即抢占当前任务降低延迟但可能增加上下文切换开销5.5 综合调优脚本#!/bin/bash # cfs_tune.sh - CFS调度器综合调优脚本 MODE${1:-interactive} if [ $MODE throughput ]; then # 吞吐量优先配置适合批处理、编译服务器 echo 配置为吞吐量优先模式... # 增大唤醒抢占粒度减少抢占 sudo sysctl -w kernel.sched_wakeup_granularity_ns10000000 # 10ms # 增大最小调度粒度 sudo sysctl -w kernel.sched_min_granularity_ns8000000 # 8ms # 启用RUN_TO_PARITY让任务运行到公平状态 echo RUN_TO_PARITY | sudo tee /sys/kernel/debug/sched/features /dev/null # 禁用WAKEUP_PREEMPTION可选进一步减少抢占 # echo NO_WAKEUP_PREEMPTION | sudo tee /sys/kernel/debug/sched/features /dev/null elif [ $MODE latency ]; then # 延迟优先配置适合交互式、实时应用 echo 配置为延迟优先模式... # 减小唤醒抢占粒度增加抢占 sudo sysctl -w kernel.sched_wakeup_granularity_ns1000000 # 1ms # 减小最小调度粒度 sudo sysctl -w kernel.sched_min_granularity_ns2000000 # 2ms # 禁用RUN_TO_PARITY允许立即抢占 echo NO_RUN_TO_PARITY | sudo tee /sys/kernel/debug/sched/features /dev/null # 启用WAKEUP_PREEMPTION echo WAKEUP_PREEMPTION | sudo tee /sys/kernel/debug/sched/features /dev/null elif [ $MODE interactive ]; then # 平衡模式默认桌面配置 echo 配置为平衡模式... sudo sysctl -w kernel.sched_wakeup_granularity_ns2500000 # 2.5ms sudo sysctl -w kernel.sched_min_granularity_ns4000000 # 4ms echo RUN_TO_PARITY | sudo tee /sys/kernel/debug/sched/features /dev/null echo WAKEUP_PREEMPTION | sudo tee /sys/kernel/debug/sched/features /dev/null fi echo 当前配置 sysctl kernel.sched_wakeup_granularity_ns kernel.sched_min_granularity_ns cat /sys/kernel/debug/sched/features | grep -E (RUN_TO_PARITY|WAKEUP_PREEMPTION)六、常见问题与解答Q1为什么修改了sched_wakeup_granularity_ns但没有观察到明显效果A可能原因包括特性未启用确保WAKEUP_PREEMPTION特性已启用否则唤醒抢占逻辑根本不会执行cat /sys/kernel/debug/sched/features | grep WAKEUP_PREEMPTIONnice值差异如果唤醒任务与当前任务的nice值差异很大权重差异可能已足够触发抢占此时粒度参数影响较小EEVDF调度器在Linux 6.6的EEVDF调度器中抢占决策基于虚拟截止时间而非单纯的vruntime比较行为有所不同Q2EEVDF中的RESPECT_SLICE和RUN_TO_PARITY有什么区别ARESPECT_SLICE抑制抢占直到当前任务耗尽其完整的时间片slice提供最严格的保护RUN_TO_PARITY放宽RESPECT_SLICE的限制只保护当前任务直到其lag归零达到公平状态之后允许被抢占在EEVDF中RUN_TO_PARITY作为RESPECT_SLICE的弱化版存在提供了响应性与吞吐量之间的中间地带。Q3如何永久保存sched_features的修改Adebugfs接口不支持持久化需要在系统启动时通过脚本设置。创建systemd服务# /etc/systemd/system/sched-tune.service [Unit] DescriptionScheduler Features Tuning Afterlocal-fs.target [Service] Typeoneshot ExecStart/bin/bash -c echo NO_RUN_TO_PARITY /sys/kernel/debug/sched/features ExecStart/bin/bash -c echo WAKEUP_PREEMPTION /sys/kernel/debug/sched/features [Install] WantedBymulti-user.target启用服务sudo systemctl enable sched-tune.service sudo systemctl start sched-tune.serviceQ4禁用WAKEUP_PREEMPTION后任务如何获得CPUA禁用WAKEUP_PREEMPTION后唤醒的任务不会立即抢占当前运行的任务而是被加入运行队列红黑树等待当前任务主动让出CPU如阻塞、时间片耗尽在下一个调度点schedule()调用被选中执行对于交互式任务这可能导致明显的响应延迟因此通常建议保持WAKEUP_PREEMPTION开启。Q5如何在cgroup环境中应用这些调优A在启用CONFIG_FAIR_GROUP_SCHED的系统中调度发生在cgroup层次结构的每个节点上。sched_features是全局设置影响所有cgroup但抢占决策在每个cfs_rq上独立进行。可以通过cpu.shares或cpu.weight调整cgroup间的权重分配结合全局抢占参数实现细粒度控制。七、实践建议与最佳实践7.1 调试技巧1. 使用/proc/sched_debug查看调度器状态sudo cat /proc/sched_debug | grep -A 20 cfs_rq\[0\] # 查看min_vruntime、当前运行任务、队列长度等信息2. 使用schedstat监控调度统计cat /proc/$(pidof mysqld)/schedstat # 输出运行时间 等待时间 切换次数3. 使用perf分析上下文切换sudo perf stat -e context-switches -a sleep 10 # 监控10秒内的上下文切换次数7.2 性能优化建议1. 数据库服务器优化保持WAKEUP_PREEMPTION开启确保查询线程快速响应将sched_wakeup_granularity_ns设置为2-4ms平衡响应与吞吐对后台维护任务使用SCHED_BATCH策略自动放弃抢占2. 实时音视频处理禁用RUN_TO_PARITY允许立即抢占减小sched_min_granularity_ns至1-2ms减少单次占用时间考虑使用SCHED_FIFO或SCHED_RR策略需root权限3. 编译服务器/批处理系统启用RUN_TO_PARITY最大化吞吐量增大sched_wakeup_granularity_ns至10ms以上减少抢占考虑禁用WAKEUP_PREEMPTION如果延迟不敏感7.3 常见错误避免1. 粒度过小导致抖动不要将sched_wakeup_granularity_ns设置过小500us否则可能导致频繁的上下文切换反而降低整体性能。2. 忽略nice值的影响nice值差异会显著影响抢占行为。即使禁用了某些抢占特性高优先级任务nice -20仍可能因vruntime增长慢而频繁抢占低优先级任务。3. 混淆CFS与RT调度器WAKEUP_PREEMPTION等特性仅影响CFSSCHED_NORMAL任务。实时任务SCHED_FIFO/SCHED_RR的抢占行为由不同机制控制不受这些参数影响。八、总结与应用场景本文深入剖析了Linux CFS调度器的抢占策略重点分析了WAKEUP_PREEMPTION和RUN_TO_PARITY两个关键特性及其权衡关系。核心要点回顾WAKEUP_PREEMPTION控制任务唤醒时是否允许抢占当前运行任务。开启时提高响应性关闭时提高吞吐量。RUN_TO_PARITY控制当前任务是否可以运行到公平lag归零。启用时减少上下文切换禁用时降低唤醒延迟。唤醒粒度sched_wakeup_granularity_ns细粒度控制抢占阈值与WAKEUP_PREEMPTION配合使用。EEVDF演进Linux 6.6引入的EEVDF调度器用虚拟截止时间VD和资格性Eligibility概念取代了传统CFS的抢占逻辑但sched_features机制仍然适用。应用场景建议场景WAKEUP_PREEMPTIONRUN_TO_PARITY唤醒粒度建议桌面/工作站开启开启2-4ms数据库服务器开启开启2-4msWeb服务器开启开启1-2ms编译服务器开启开启4-8ms实时音视频开启禁用0.5-1ms科学计算可选禁用开启8-10ms掌握这些调度器调优技术可以帮助系统管理员和开发者在不同工作负载下优化系统性能在响应性与吞吐量之间找到最佳平衡点。对于从事Linux内核开发、系统性能调优、实时系统设计的读者这些知识是深入理解操作系统调度机制的重要基础。参考资源Linux内核源码kernel/sched/fair.c,kernel/sched/features.hLWN文章An EEVDF CPU scheduler for Linux内核文档Documentation/scheduler/sched-design-CFS.rst

更多文章