引言

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(随机种子)

> 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读取指令时,情况会变得复杂。在这种情况下,进程的运行顺序对运行效率有很大影响:

> 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等待期间一直空等。

> 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选项进行模拟:

> 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%利用率。

> 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完成策略的差距可以忽略,不必为此费心。