CPU调度与上下文切换
深入理解Linux CPU调度机制,掌握上下文切换的成本与优化策略,为高性能系统编程奠定基础
概述
CPU是计算系统的核心资源,Linux的CPU调度机制决定了系统的吞吐量和响应时间。从简单的单核调度到复杂的多核调度,从优先级调度到CFS完全公平调度器,CPU调度策略直接影响系统性能。
本文将深入分析Linux CPU调度机制,理解上下文切换的成本,掌握CPU性能优化的基本策略。这是CPU性能优化模块的基础,后续我们将学习CPU亲和性、NUMA优化和高性能计算框架。
学习目标:
- 理解Linux CPU调度器的演进和设计理念
- 掌握上下文切换的成本和影响
- 了解CFS调度器的工作原理
- 学习CPU优先级和时间片配置
- 掌握CPU性能优化的基本策略
CPU调度器演进
早期调度器
Linux最早的调度器是O(1)调度器,它为每个CPU维护两个优先级队列:活动队列和过期队列。每个进程被分配一个静态优先级,时间片用完后从活动队列移到过期队列,当活动队列为空时两个队列互换。
O(1)调度器的问题在于:
- 静态优先级缺乏公平性
- 交互式进程和批处理进程混合时性能差
- 多核扩展性有限
CFS调度器
从Linux 2.6.23开始,完全公平调度器(CFS)成为默认调度器。CFS的核心思想是:在一个理想的多任务系统中,每个进程应该获得相同的CPU时间份额。
CFS的设计理念:
- 使用红黑树管理可运行进程,按虚拟运行时间排序
- 完全公平的调度策略,优先级通过权重实现
- 动态时间片分配,系统负载高时时间片减小
- 原生支持多核SMP架构
上下文切换成本分析
上下文切换的组成
上下文切换从一个进程切换到另一个进程时,需要保存当前进程的上下文并恢复目标进程的上下文。主要包括:
-
用户空间上下文:
- 通用寄存器
- 程序计数器
- 栈指针
-
内核空间上下文:
- 内核栈
- 任务状态结构
- 调度相关信息
-
硬件状态:
- 浮点寄存器
- SIMD寄存器
- TLB刷新
性能开销
上下文切换的性能开销主要来自:
-
直接开销:
- 保存/恢复寄存器状态:约100-500个CPU周期
- 内核执行调度算法:约200-1000个CPU周期
-
间接开销:
- CPU缓存失效:约100-1000个CPU周期
- TLB刷新:约200-500个CPU周期
- 分支预测失效:约50-100个CPU周期
总体来看,一次上下文切换可能消耗500-2000个CPU周期,在高性能场景下这个开销不可忽视。
测量上下文切换成本
#include <stdio.h>
#include <unistd.h>
#include <sys/times.h>
#include <time.h>
#define ITERATIONS 1000000
void measure_context_switch() {
struct timespec start,end;
pid_t pid;
int pipes[2];
char buf;
pipe(pipes);
pid = fork();
if (pid == 0) {
// 子进程
for (int i = 0; i < ITERATIONS; i++) {
read(pipes[0],&buf,1);
write(pipes[1],&buf,1);
}
} else {
// 父进程
clock_gettime(CLOCK_MONOTONIC,&start);
for (int i = 0; i < ITERATIONS; i++) {
write(pipes[1],&buf,1);
read(pipes[0],&buf,1);
}
clock_gettime(CLOCK_MONOTONIC,&end);
double elapsed = (end.tv_sec - start.tv_sec) * 1e9 +
(end.tv_nsec - start.tv_nsec);
double per_switch = elapsed / (ITERATIONS * 2);
printf("平均上下文切换时间: %.2f 纳秒 (%.2f CPU周期)\n",
per_switch,per_switch * 3.0); // 假设3GHz CPU
}
}
int main() {
measure_context_switch();
return 0;
}
CFS调度器工作原理
虚拟运行时间(vruntime)
CFS的核心概念是虚拟运行时间。每个进程都有一个vruntime值,表示它在公平调度下应该获得的CPU时间。
vruntime += delta_exec * (NICE_0_LOAD / load.weight)
其中:
delta_exec:实际执行的CPU时间NICE_0_LOAD:nice值为0时的基准权重load.weight:当前进程的权重
调度流程
CFS调度器的基本流程:
- 进程入队:新进程或可运行进程进入红黑树,按vruntime排序
- 进程选择:选择vruntime最小的进程(红黑树最左节点)
- 进程执行:选中的进程执行delta_exec时间
- 进程出队:进程的vruntime更新,重新入队
权重和优先级
CFS通过权重实现进程优先级,权重映射到nice值:
static const int prio_to_weight[40] = {
/* -20 */ 88761,71755,56483,46273,36291,
/* -15 */ 29154,23254,18705,14949,11916,
/* -10 */ 9548,7620,6100,4904,3906,
/* -5 */ 3121,2501,1991,1586,1277,
/* 0 */ 1024,820,655,526,416,
/* 5 */ 335,272,215,172,137,
/* 10 */ 110,87,70,56,45,
/* 15 */ 36,29,23,18,15,
};
nice值-20的进程权重是nice值19进程的88761/15≈5917倍,这意味着高优先级进程将获得更多的CPU时间。
CPU性能优化策略
减少上下文切换
- 使用线程替代进程:线程间的上下文切换开销更小
- 批处理任务:将小任务合并为大任务减少切换次数
- 绑定CPU核心:使用CPU亲和性避免跨核调度
- 避免过度调度:合理设置nice值,优先级差异不要太大
优化进程优先级
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
int main() {
pid_t pid = getpid();
int current_nice = getpriority(PRIO_PROCESS,pid);
printf("当前nice值: %d\n",current_nice);
// 设置更高的优先级(需要root权限)
if (setpriority(PRIO_PROCESS,pid,-5) == -1) {
perror("设置优先级失败");
} else {
printf("新nice值: %d\n",getpriority(PRIO_PROCESS,pid));
}
return 0;
}
利用CPU亲和性
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
int main() {
cpu_set_t cpuset;
pid_t pid = getpid();
// 设置进程只在CPU 0和CPU 1上运行
CPU_ZERO(&cpuset);
CPU_SET(0,&cpuset);
CPU_SET(1,&cpuset);
if (sched_setaffinity(pid,sizeof(cpu_set_t),&cpuset) == -1) {
perror("设置CPU亲和性失败");
return 1;
}
// 获取当前CPU亲和性
CPU_ZERO(&cpuset);
sched_getaffinity(pid,sizeof(cpu_set_t),&cpuset);
printf("进程可以运行在CPU: ");
for (int i = 0; i < CPU_SETSIZE; i++) {
if (CPU_ISSET(i,&cpuset)) {
printf("%d ",i);
}
}
printf("\n");
return 0;
}
监控调度性能
# 查看上下文切换统计
vmstat 1
# 查看进程调度信息
pidstat -w 1
# 查看CPU调度延迟
latencytop
# 查看进程优先级和调度信息
ps -eo pid,comm,pri,nice,rtprio
小结
CPU调度和上下文切换是操作系统性能的核心组件。理解CFS调度器的工作原理和上下文切换的成本,可以帮助我们:
- 合理设置进程优先级:通过nice值和权重优化调度公平性
- 减少上下文切换开销:使用线程、CPU亲和性等技术
- 监控调度性能:通过系统工具识别调度瓶颈
- 优化应用设计:避免过度创建进程,合理使用批处理
下一篇文章我们将深入探讨CPU亲和性和NUMA优化,进一步提升多核系统性能。