HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 系统底层修炼

    • 操作系统核心知识学习指南
    • CPU调度与上下文切换
    • CFS调度器原理与源码
    • 内存管理与虚拟内存
    • PageCache与内存回收
    • 文件系统与IO优化
    • 零拷贝与Direct I/O
    • 网络子系统架构
    • TCP协议深度解析
    • TCP问题排查实战
    • 网络性能优化
    • epoll与IO多路复用
    • 进程与线程管理
    • Go Runtime调度器GMP模型
    • 系统性能分析方法论
    • DPDK与用户态网络栈
    • eBPF与内核可观测性
    • 综合实战案例

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)

切换过程:

  1. 保存当前进程的上下文
  2. 更新进程控制块(PCB)
  3. 恢复目标进程的上下文
  4. 跳转到目标进程的执行点

源码解析

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:观测上下文切换开销

目标: 理解上下文切换对性能的影响

步骤:

  1. 编译测试程序:
gcc -o context_switch_test context_switch_test.c -lpthread
  1. 运行程序并观测:
# 终端1:运行测试程序
./context_switch_test

# 终端2:观测上下文切换
vmstat 1
  1. 分析结果:
  • 观察cs(上下文切换次数)的变化
  • 记录CPU使用率的变化
  • 计算平均每次上下文切换的开销

预期结果:

  • cs值会显著增加
  • CPU使用率接近100%
  • 系统响应可能变慢

实验2:调整调度参数

目标: 理解调度参数对系统性能的影响

步骤:

  1. 查看当前参数:
cat /proc/sys/kernel/sched_latency_ns
cat /proc/sys/kernel/sched_min_granularity_ns
  1. 调整参数(需要root权限):
# 增加最小时间片(减少上下文切换)
sudo sysctl -w kernel.sched_min_granularity_ns=3000000  # 3ms

# 增加调度延迟
sudo sysctl -w kernel.sched_latency_ns=12000000  # 12ms
  1. 运行相同的测试程序:
./context_switch_test
  1. 对比结果:
  • 上下文切换次数是否减少
  • 程序执行时间是否变化
  • 系统响应性如何

实验3:进程优先级实验

目标: 理解nice值对调度的影响

步骤:

  1. 创建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;
}
  1. 编译程序:
gcc -o cpu_intensive cpu_intensive.c
  1. 以不同优先级运行:
# 终端1:高优先级(-20)
sudo nice -n -20 ./cpu_intensive &

# 终端2:低优先级(+19)
nice -n 19 ./cpu_intensive &

# 终端3:观测进程状态
top -H
  1. 分析结果:
  • 观察两个进程的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使用率不均衡

复习题

选择题

  1. 在Linux中,CFS调度器使用什么数据结构来管理可运行进程?

    • A. 链表
    • B. 红黑树
    • C. 哈希表
    • D. 数组
  2. 上下文切换的主要开销不包括:

    • A. 寄存器保存和恢复
    • B. 页表更新
    • C. 内存分配
    • D. TLB刷新
  3. 以下哪个命令可以查看进程的上下文切换统计?

    • A. top
    • B. ps
    • C. pidstat
    • D. free
  4. nice值的范围是:

    • A. -20到+20
    • B. -19到+20
    • C. -20到+19
    • D. 0到39
  5. 在vmstat输出中,cs字段表示:

    • A. CPU使用率
    • B. 上下文切换次数
    • C. 进程数量
    • D. 内存使用量

简答题

  1. 解释Linux CFS调度器的"完全公平"是如何实现的?

  2. 为什么在多核系统中,进程迁移会影响性能?

  3. 如何通过调整调度参数来优化CPU密集型应用的性能?

  4. 解释自愿上下文切换和非自愿上下文切换的区别,并举例说明。

  5. 在什么情况下,高频率的上下文切换会成为性能瓶颈?

实战题

  1. 性能分析题: 假设你发现一个Go程序的CPU使用率很高,但QPS很低。请设计一个分析流程来定位问题,并说明可能的原因。

  2. 优化设计题: 设计一个高并发的Web服务器,要求能够处理10万并发连接,且CPU使用率不超过80%。请说明你的设计思路和关键优化点。

  3. 故障排查题: 在生产环境中,你发现系统响应变慢,vmstat显示cs值异常高。请描述你的排查步骤和可能的解决方案。

扩展阅读

推荐资源

  • Linux内核调度器源码
  • CFS调度器论文
  • Brendan Gregg的调度器分析

深入方向

  • 实时调度器(RT调度器)
  • 多核调度和负载均衡
  • 调度器的历史演进
  • 容器环境下的调度优化

下一章预告: 我们将深入探讨CFS调度器的源码实现,包括红黑树的使用、vruntime的计算,以及负载均衡机制。

Prev
操作系统核心知识学习指南
Next
CFS调度器原理与源码