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架构

上下文切换成本分析

上下文切换的组成

上下文切换从一个进程切换到另一个进程时,需要保存当前进程的上下文并恢复目标进程的上下文。主要包括:

  1. 用户空间上下文

    • 通用寄存器
    • 程序计数器
    • 栈指针
  2. 内核空间上下文

    • 内核栈
    • 任务状态结构
    • 调度相关信息
  3. 硬件状态

    • 浮点寄存器
    • SIMD寄存器
    • TLB刷新

性能开销

上下文切换的性能开销主要来自:

  1. 直接开销

    • 保存/恢复寄存器状态:约100-500个CPU周期
    • 内核执行调度算法:约200-1000个CPU周期
  2. 间接开销

    • 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调度器的基本流程:

  1. 进程入队:新进程或可运行进程进入红黑树,按vruntime排序
  2. 进程选择:选择vruntime最小的进程(红黑树最左节点)
  3. 进程执行:选中的进程执行delta_exec时间
  4. 进程出队:进程的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性能优化策略

减少上下文切换

  1. 使用线程替代进程:线程间的上下文切换开销更小
  2. 批处理任务:将小任务合并为大任务减少切换次数
  3. 绑定CPU核心:使用CPU亲和性避免跨核调度
  4. 避免过度调度:合理设置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调度器的工作原理和上下文切换的成本,可以帮助我们:

  1. 合理设置进程优先级:通过nice值和权重优化调度公平性
  2. 减少上下文切换开销:使用线程、CPU亲和性等技术
  3. 监控调度性能:通过系统工具识别调度瓶颈
  4. 优化应用设计:避免过度创建进程,合理使用批处理

下一篇文章我们将深入探讨CPU亲和性和NUMA优化,进一步提升多核系统性能。

参考资源