---
title: "进程切换策略"
date: 2026-09-08
category: "操作系统"
tags: ["进程"]
summary: "对OSTEP进程运行模拟器中SWITCH与IO_RUN两类切换策略的模拟结果分析，以及最大化cpu利用率的选择建议"
series:
  name: "操作系统虚拟化"
  order: 0
---

## 引言

OSTEP（Operating Systems: Three Easy Pieces）第4章的课后练习提供了一个进程运行模拟器，可以根据不同的参数模拟出不同策略下进程运行结果的具体区别。

本文针对该课后练习的几组对比实验，对进程切换策略的选择结果做出推测和总结。

## 正文

模拟器用一个单cpu的模型模拟多个进程的运行：每个进程由若干条指令构成，每条指令以一定概率是cpu指令或io指令，io指令发起后进程会阻塞固定时长（默认5个时间片）等待设备完成。输出里每个时刻各进程处于RUN（占用cpu，其中RUN:io是发起io，RUN:io_done是io完成后的收尾）、READY（就绪等待调度）、BLOCKED（等待io完成）、DONE（结束）四种状态之一，带\*的时刻表示恰有一次io完成。现实里发起io对应一次系统调用，进程在设备完成工作前无事可做，这正是调度器接管cpu的时机。

### 纯cpu指令进程运行

在没有io请求发出时，操作系统按顺序执行每个进程，只在上一个进程结束时发生进程切换：

>命令介绍：-c（计算并显示运行结果）-p（统计运行数据）-S（进程发起io时是否切换）-I（io完成后切换给谁）-l（进程列表，每个进程写成x:y，x为指令数，y为该条指令是cpu指令的概率）-L（单次io持续时长，默认5）-s（随机种子）

```bash
> python ./process-run.py -c -p -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -l 5:100,5:100
Time        PID: 0        PID: 1           CPU           IOs
  1        RUN:cpu         READY             1
  2        RUN:cpu         READY             1
  3        RUN:cpu         READY             1
  4        RUN:cpu         READY             1
  5        RUN:cpu         READY             1
  6           DONE       RUN:cpu             1
  7           DONE       RUN:cpu             1
  8           DONE       RUN:cpu             1
  9           DONE       RUN:cpu             1
 10           DONE       RUN:cpu             1

Stats: Total Time 10
Stats: CPU Busy 10 (100.00%)
Stats: IO Busy  0 (0.00%)
```

不出意料，此时cpu的运行效率是100%。

### cpu和io指令

当加入io读取指令时，情况会变得复杂。在这种情况下，进程的运行顺序对运行效率有很大影响：

```bash
> python ./process-run.py -c -p -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -l 5:100,1:0
Time        PID: 0        PID: 1           CPU           IOs
  1        RUN:cpu         READY             1
  2        RUN:cpu         READY             1
  3        RUN:cpu         READY             1
  4        RUN:cpu         READY             1
  5        RUN:cpu         READY             1
  6           DONE        RUN:io             1
  7           DONE       BLOCKED                           1
  8           DONE       BLOCKED                           1
  9           DONE       BLOCKED                           1
 10           DONE       BLOCKED                           1
 11           DONE       BLOCKED                           1
 12*          DONE   RUN:io_done             1

Stats: Total Time 12
Stats: CPU Busy 7 (58.33%)
Stats: IO Busy  5 (41.67%)
> python ./process-run.py -c -p -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -l 1:0,5:100
Time        PID: 0        PID: 1           CPU           IOs
  1         RUN:io         READY             1
  2         BLOCKED       RUN:cpu             1             1
  3         BLOCKED       RUN:cpu             1             1
  4         BLOCKED       RUN:cpu             1             1
  5         BLOCKED       RUN:cpu             1             1
  6         BLOCKED       RUN:cpu             1             1
  7*   RUN:io_done          DONE             1

Stats: Total Time 7
Stats: CPU Busy 7 (100.00%)
Stats: IO Busy  5 (71.43%)
```

显而易见，想要最大化cpu利用率，应该先发起io操作，在io进程阻塞的时候运行其他进程的cpu指令。

### SWITCH策略

如果将-S选项改为SWITCH_ON_END：进程发起io时不切换到其他就绪进程，cpu在io等待期间一直空等。

```bash
> python ./process-run.py -c -p -S SWITCH_ON_END -I IO_RUN_IMMEDIATE -l 1:0,5:100
Time        PID: 0        PID: 1           CPU           IOs
  1         RUN:io         READY             1
  2         BLOCKED         READY                           1
  3         BLOCKED         READY                           1
  4         BLOCKED         READY                           1
  5         BLOCKED         READY                           1
  6         BLOCKED         READY                           1
  7*   RUN:io_done         READY             1
  8           DONE       RUN:cpu             1
  9           DONE       RUN:cpu             1
 10           DONE       RUN:cpu             1
 11           DONE       RUN:cpu             1
 12           DONE       RUN:cpu             1

Stats: Total Time 12
Stats: CPU Busy 7 (58.33%)
Stats: IO Busy  5 (41.67%)
```

可以推断出，当操作系统选择在进程阻塞时让cpu空等，进程的运行顺序对整体运行效率就不再起作用，一次io会退化成固定损失5个时间片（-L的默认时长）的串行开销。

**进程发起io阻塞时立刻切换到其他就绪进程，是重叠io等待与cpu执行的前提。**

### IO_RUN策略

另外一个重要策略是io操作完成时，是否要立刻切换回刚完成io的进程。用-I选项进行模拟：

```bash
> python ./process-run.py -c -p -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -l 5:60,7:80
Time        PID: 0        PID: 1           CPU           IOs
  1         RUN:io         READY             1
  2         BLOCKED       RUN:cpu             1             1
  3         BLOCKED       RUN:cpu             1             1
  4         BLOCKED       RUN:cpu             1             1
  5         BLOCKED       RUN:cpu             1             1
  6         BLOCKED       RUN:cpu             1             1
  7*   RUN:io_done         READY             1
  8         RUN:io         READY             1
  9         BLOCKED        RUN:io             1             1
 10        BLOCKED       BLOCKED                           2
 11        BLOCKED       BLOCKED                           2
 12        BLOCKED       BLOCKED                           2
 13        BLOCKED       BLOCKED                           2
 14*   RUN:io_done       BLOCKED             1             1
 15*         READY   RUN:io_done             1
 16          READY       RUN:cpu             1
 17        RUN:cpu          DONE             1
 18        RUN:cpu          DONE             1
 19        RUN:cpu          DONE             1

Stats: Total Time 19
Stats: CPU Busy 15 (78.95%)
Stats: IO Busy  11 (57.89%)
> python ./process-run.py -c -p -S SWITCH_ON_IO -I IO_RUN_LATER -l 5:60,7:80
Time        PID: 0        PID: 1           CPU           IOs
  1         RUN:io         READY             1
  2         BLOCKED       RUN:cpu             1             1
  3         BLOCKED       RUN:cpu             1             1
  4         BLOCKED       RUN:cpu             1             1
  5         BLOCKED       RUN:cpu             1             1
  6         BLOCKED       RUN:cpu             1             1
  7*         READY        RUN:io             1
  8    RUN:io_done       BLOCKED             1             1
  9         RUN:io       BLOCKED             1             1
 10        BLOCKED       BLOCKED                           2
 11        BLOCKED       BLOCKED                           2
 12        BLOCKED       BLOCKED                           2
 13*       BLOCKED   RUN:io_done             1             1
 14        BLOCKED       RUN:cpu             1             1
 15*   RUN:io_done          DONE             1
 16        RUN:cpu          DONE             1
 17        RUN:cpu          DONE             1
 18        RUN:cpu          DONE             1

Stats: Total Time 18
Stats: CPU Busy 15 (83.33%)
Stats: IO Busy  12 (66.67%)
```

结果有点出人意料，居然是io完成后暂不切回（IO_RUN_LATER）的策略运行效率更高。按道理来说，在io操作完成后立刻切换回该进程可以最大化io的利用，从而减少cpu的等待时间。

对比两组输出可知，IO_RUN_LATER那一组里，进程二（PID:1）在进程一（PID:0）阻塞期间多运行了一条cpu指令，总时间因此从19压到18。

这么看来哪种策略更优，非常依赖进程的先后和进程中指令的顺序。

为了探究IO_RUN_LATER的反超是否只是特例，又进行了多组测试。测试结果发现，在少量进程、少量指令的情况下，两种策略的模拟结果非常相近，都有胜出的情况。但是，在进程变多、指令也变多的情况下，io完成后立刻切换回该进程的策略平均表现更优，甚至有几次可以做到100%利用率。

```bash
> python ./process-run.py -c -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -l 5:40,6:60,2:0,20:100 -p
Time        PID: 0        PID: 1        PID: 2        PID: 3           CPU           IOs
  1         RUN:io         READY         READY         READY             1
  2        BLOCKED       RUN:cpu         READY         READY             1             1
  3        BLOCKED        RUN:io         READY         READY             1             1
  4        BLOCKED       BLOCKED        RUN:io         READY             1             2
  5        BLOCKED       BLOCKED       BLOCKED       RUN:cpu             1             3
  6        BLOCKED       BLOCKED       BLOCKED       RUN:cpu             1             3
  7*   RUN:io_done       BLOCKED       BLOCKED         READY             1             2
  8         RUN:io       BLOCKED       BLOCKED         READY             1             2
  9*       BLOCKED   RUN:io_done       BLOCKED         READY             1             2
 10*       BLOCKED         READY   RUN:io_done         READY             1             1
 11        BLOCKED         READY        RUN:io         READY             1             1
 12        BLOCKED         READY       BLOCKED       RUN:cpu             1             2
 13        BLOCKED         READY       BLOCKED       RUN:cpu             1             2
 14*   RUN:io_done         READY       BLOCKED         READY             1             1
 15         RUN:io         READY       BLOCKED         READY             1             1
 16        BLOCKED       RUN:cpu       BLOCKED         READY             1             2
 17*       BLOCKED         READY   RUN:io_done         READY             1             1
 18        BLOCKED         READY          DONE       RUN:cpu             1             1
 19        BLOCKED         READY          DONE       RUN:cpu             1             1
 20        BLOCKED         READY          DONE       RUN:cpu             1             1
 21*   RUN:io_done         READY          DONE       READY             1             1
 22        RUN:cpu         READY          DONE       READY             1
 23         RUN:io         READY          DONE       READY             1
 24        BLOCKED       RUN:cpu          DONE       RUN:cpu             1             1
 25        BLOCKED       RUN:cpu          DONE       RUN:cpu             1             1
 26        BLOCKED       RUN:io          DONE       RUN:cpu             1             1
 27        BLOCKED       BLOCKED          DONE       RUN:cpu             1             2
 28        BLOCKED       BLOCKED          DONE       RUN:cpu             1             2
 29*   RUN:io_done       BLOCKED          DONE       RUN:cpu             1             1
 30           DONE       BLOCKED          DONE       RUN:cpu             1             1
 31           DONE       BLOCKED          DONE       RUN:cpu             1             1
 32*          DONE   RUN:io_done          DONE       READY             1
 33           DONE          DONE          DONE       RUN:cpu             1
 34           DONE          DONE          DONE       RUN:cpu             1
 35           DONE          DONE          DONE       RUN:cpu             1
 36           DONE          DONE          DONE       RUN:cpu             1
 37           DONE          DONE          DONE       RUN:cpu             1
 38           DONE          DONE          DONE       RUN:cpu             1
 39           DONE          DONE          DONE       RUN:cpu             1
 40           DONE          DONE          DONE       RUN:cpu             1
 41           DONE          DONE          DONE       RUN:cpu             1

Stats: Total Time 41
Stats: CPU Busy 41 (100.00%)
Stats: IO Busy  27 (65.85%)
```

分析结果可以发现，当cpu指令数占多数时，io结束后立刻切回的策略可以最大化填充io阻塞的间隙，提高cpu的利用效率。

对于这种结果，可以这样理解：刚结束io的进程有更大的可能再次发起io，并且现代计算机运行时，cpu指令数通常远多于io指令数，此时采取`IO_RUN_IMMEDIATE`是更优的选择。

**io完成后立刻切换回刚完成io的进程，是平均意义下更优的选择。**

## 小结

进程切换策略的本质是重叠：让一个进程的io等待与其他进程的cpu执行同时进行。SWITCH_ON_IO是重叠的前提，没有它，io就退化成固定损失5个时间片的串行开销，进程顺序也随之失去意义；进程的先后次序决定重叠的机会有多大，先发起io再运行cpu指令能填满阻塞的间隙；io完成后的切换策略影响最小，少量进程时互有胜负，进程和指令变多后IO_RUN_IMMEDIATE平均更优，因为刚结束io的进程大概率还会再次发起io，尽早切回它，下一次重叠就能尽早开始。

一个最优的实践是，SWITCH_ON_IO搭配IO_RUN_IMMEDIATE，并让会发起io的进程先于纯cpu进程投入运行，除非负载只有一两个短进程，此时两种io完成策略的差距可以忽略，不必为此费心。
