CPU调度与上下文切换
章节概述
CPU调度是操作系统的核心功能之一,它决定了多个进程如何共享CPU资源。本章将深入探讨Linux的CPU调度机制,包括调度算法、上下文切换的开销分析,以及如何通过工具观测和优化调度性能。
学习目标:
- 理解CPU调度的基本原理和Linux调度器架构
- 掌握上下文切换的机制和性能影响
- 学会使用vmstat、pidstat等工具观测调度行为
- 能够分析和优化CPU调度相关的性能问题
核心概念
1. CPU调度的基本概念
进程状态转换:
创建 → 就绪 → 运行 → 阻塞 → 就绪
↑ ↓
└────── 终止 ←────────┘
关键概念:
- 时间片(Time Slice):进程在CPU上连续执行的时间
- 优先级(Priority):决定进程被调度的先后顺序
- 调度策略(Scheduling Policy):决定如何选择下一个运行的进程
2. Linux调度器架构
graph TD
A[用户进程] --> B[系统调用]
B --> C[内核调度器]
C --> D[CFS调度器]
C --> E[实时调度器]
D --> F[红黑树]
E --> G[优先级队列]
F --> H[选择进程]
G --> H
H --> I[上下文切换]
I --> J[进程执行]
3. 上下文切换机制
上下文(Context)包含:
- 寄存器状态(PC、SP、通用寄存器)
- 程序计数器(Program Counter)
- 栈指针(Stack Pointer)
- 状态寄存器(Status Register)
- 页表基址寄存器(Page Table Base Register)
切换过程:
- 保存当前进程的上下文
- 更新进程控制块(PCB)
- 恢复目标进程的上下文
- 跳转到目标进程的执行点
源码解析
1. 调度器核心结构
关键文件: kernel/sched/core.c
// 调度器主函数
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
// 核心调度逻辑
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
// 选择下一个要运行的进程
next = pick_next_task(rq, prev, &rf);
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
// 执行上下文切换
rq = context_switch(rq, prev, next, &rf);
}
}
2. 上下文切换实现
关键文件: kernel/sched/core.c
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
// 切换内存空间
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm_irqs_off(oldmm, mm, next);
// 切换寄存器状态
switch_to(prev, next, prev);
return finish_task_switch(prev);
}
️ 实用命令
1. 系统级调度观测
vmstat - 系统整体状态
# 每秒显示一次系统状态
vmstat 1
# 输出解释
# r: 运行队列中的进程数
# b: 阻塞的进程数
# cs: 每秒上下文切换次数
# us: 用户态CPU时间百分比
# sy: 内核态CPU时间百分比
# id: 空闲CPU时间百分比
# wa: IO等待时间百分比
示例输出:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 149360 73660 847480 0 0 1 1 56 103 2 1 96 1 0
2. 进程级调度观测
pidstat - 进程调度统计
# 显示进程调度统计
pidstat -w 1
# 输出解释
# cswch/s: 自愿上下文切换次数/秒
# nvcswch/s: 非自愿上下文切换次数/秒
示例输出:
12:14:43 PM UID PID cswch/s nvcswch/s Command
12:14:44 PM 1000 3492 10.00 2.00 myserver
3. 详细调度信息
查看调度器参数:
# 查看调度延迟
cat /proc/sys/kernel/sched_latency_ns
# 查看最小时间片
cat /proc/sys/kernel/sched_min_granularity_ns
# 查看唤醒粒度
cat /proc/sys/kernel/sched_wakeup_granularity_ns
查看进程优先级:
# 显示进程优先级信息
ps -eo pid,ni,pri,stat,comm | head
# 字段解释
# NI: nice值(-20到+19)
# PRI: 动态优先级
# STAT: 进程状态
代码示例
1. 观测上下文切换的Go程序
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
// 设置GOMAXPROCS
runtime.GOMAXPROCS(2)
var wg sync.WaitGroup
// 启动多个goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 模拟CPU密集型工作
for j := 0; j < 1000000; j++ {
if j%100000 == 0 {
fmt.Printf("Goroutine %d: iteration %d\n", id, j)
}
}
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}
2. 创建高上下文切换的C程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#define NUM_THREADS 100
void* worker(void* arg) {
int id = *(int*)arg;
// 设置CPU亲和性,强制在特定CPU上运行
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(id % 2, &cpuset); // 只在CPU 0和1之间切换
if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) != 0) {
perror("pthread_setaffinity_np");
}
// 模拟工作负载
for (int i = 0; i < 1000000; i++) {
if (i % 100000 == 0) {
printf("Thread %d: iteration %d\n", id, i);
}
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
// 创建多个线程
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
if (pthread_create(&threads[i], NULL, worker, &thread_ids[i]) != 0) {
perror("pthread_create");
exit(1);
}
}
// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("All threads completed\n");
return 0;
}
编译和运行:
gcc -o context_switch_test context_switch_test.c -lpthread
./context_switch_test
动手实验
实验1:观测上下文切换开销
目标: 理解上下文切换对性能的影响
步骤:
- 编译测试程序:
gcc -o context_switch_test context_switch_test.c -lpthread
- 运行程序并观测:
# 终端1:运行测试程序
./context_switch_test
# 终端2:观测上下文切换
vmstat 1
- 分析结果:
- 观察cs(上下文切换次数)的变化
- 记录CPU使用率的变化
- 计算平均每次上下文切换的开销
预期结果:
- cs值会显著增加
- CPU使用率接近100%
- 系统响应可能变慢
实验2:调整调度参数
目标: 理解调度参数对系统性能的影响
步骤:
- 查看当前参数:
cat /proc/sys/kernel/sched_latency_ns
cat /proc/sys/kernel/sched_min_granularity_ns
- 调整参数(需要root权限):
# 增加最小时间片(减少上下文切换)
sudo sysctl -w kernel.sched_min_granularity_ns=3000000 # 3ms
# 增加调度延迟
sudo sysctl -w kernel.sched_latency_ns=12000000 # 12ms
- 运行相同的测试程序:
./context_switch_test
- 对比结果:
- 上下文切换次数是否减少
- 程序执行时间是否变化
- 系统响应性如何
实验3:进程优先级实验
目标: 理解nice值对调度的影响
步骤:
- 创建CPU密集型程序:
// cpu_intensive.c
#include <stdio.h>
#include <unistd.h>
int main() {
int i = 0;
while (1) {
i++;
if (i % 100000000 == 0) {
printf("PID %d: iteration %d\n", getpid(), i);
}
}
return 0;
}
- 编译程序:
gcc -o cpu_intensive cpu_intensive.c
- 以不同优先级运行:
# 终端1:高优先级(-20)
sudo nice -n -20 ./cpu_intensive &
# 终端2:低优先级(+19)
nice -n 19 ./cpu_intensive &
# 终端3:观测进程状态
top -H
- 分析结果:
- 观察两个进程的CPU使用率
- 查看进程的优先级和nice值
- 理解优先级对调度的影响
常见问题
Q1: 为什么上下文切换会影响性能?
A: 上下文切换涉及以下开销:
- 保存和恢复寄存器状态
- 更新页表(如果进程不同)
- 刷新TLB(Translation Lookaside Buffer)
- 缓存失效(Cache Miss)
Q2: 如何减少上下文切换?
A: 可以通过以下方式:
- 减少线程/进程数量
- 使用线程池
- 调整调度参数
- 使用CPU亲和性绑定
Q3: 自愿和非自愿上下文切换有什么区别?
A:
- 自愿切换:进程主动让出CPU(如等待IO、调用sleep)
- 非自愿切换:进程被强制切换(如时间片用完、被高优先级进程抢占)
Q4: 如何判断系统是否存在调度问题?
A: 观察以下指标:
- vmstat中的cs值过高(>10000/秒)
- pidstat中nvcswch/s过高
- 系统响应变慢
- CPU使用率不均衡
复习题
选择题
在Linux中,CFS调度器使用什么数据结构来管理可运行进程?
- A. 链表
- B. 红黑树
- C. 哈希表
- D. 数组
上下文切换的主要开销不包括:
- A. 寄存器保存和恢复
- B. 页表更新
- C. 内存分配
- D. TLB刷新
以下哪个命令可以查看进程的上下文切换统计?
- A. top
- B. ps
- C. pidstat
- D. free
nice值的范围是:
- A. -20到+20
- B. -19到+20
- C. -20到+19
- D. 0到39
在vmstat输出中,cs字段表示:
- A. CPU使用率
- B. 上下文切换次数
- C. 进程数量
- D. 内存使用量
简答题
解释Linux CFS调度器的"完全公平"是如何实现的?
为什么在多核系统中,进程迁移会影响性能?
如何通过调整调度参数来优化CPU密集型应用的性能?
解释自愿上下文切换和非自愿上下文切换的区别,并举例说明。
在什么情况下,高频率的上下文切换会成为性能瓶颈?
实战题
性能分析题: 假设你发现一个Go程序的CPU使用率很高,但QPS很低。请设计一个分析流程来定位问题,并说明可能的原因。
优化设计题: 设计一个高并发的Web服务器,要求能够处理10万并发连接,且CPU使用率不超过80%。请说明你的设计思路和关键优化点。
故障排查题: 在生产环境中,你发现系统响应变慢,vmstat显示cs值异常高。请描述你的排查步骤和可能的解决方案。
扩展阅读
推荐资源
深入方向
- 实时调度器(RT调度器)
- 多核调度和负载均衡
- 调度器的历史演进
- 容器环境下的调度优化
下一章预告: 我们将深入探讨CFS调度器的源码实现,包括红黑树的使用、vruntime的计算,以及负载均衡机制。