深蓝黑背景封面:右侧大标题「三道程序,单道和多道各要跑多久」,副标题「计算与 IO 重叠,省下的都是等待」;左侧是两张迷你甘特图——上图「单道批处理」:P1、P2、P3 三段首尾相接,每段由青色计算块(40ms)和紫色 IO 块(60ms)组成,下方括号标注总计 300ms;下图「多道并发」:CPU 行三道程序的计算块连续排列(0~120ms,之后空闲),IO 行三个 IO 块交错衔接(40~220ms),括号标注总计 220ms、省 80ms。左上标注「笔试复盘 · 操作系统」,右上胶囊标注「Day 07 · 11-14」 图 1:本文封面,单道与多道两种环境的迷你甘特图对比。

先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题目大意:三道程序 a、b、c 共用一台 IO 设备,各自的时间表都是「计算、IO、计算」三段——a 是 30、40、10 毫秒,b 是 60、30、10,c 是 20、40、20;三道程序按 a、b、c 的优先级执行(a 最高、c 最低),系统采用不可抢占调度,调度开销忽略不计。问:单道程序环境和多道程序环境(三道可同时装入内存)下,运行总时间各是多少?

单道环境:纯粹的加法题。

单道批处理里一道跑完才轮到下一道,设备再闲也不能插队。总时间就是三道程序各自三段之和:a 用 80 毫秒,b 用 100,c 用 80,合计 260 毫秒。这一半是送分,但也是理解多道收益的基准线。

多道快在哪:先用对称模型看清原理。

原题数字不对称,直接推容易乱。先看一个对称模型——三道相同程序,各需计算 40 毫秒加 IO 60 毫秒,先算后 IO(本文配图用的就是这个模型)。单道环境下三道首尾相接:3×100=300 毫秒。多道环境下两类资源各画一行:CPU 行连续把三道的计算跑完,占 0~120 毫秒;IO 行从 40 毫秒起排队,P1 占 40~100,P2 占 100~160,P3 占 160~220,全部完成于 220 毫秒。省下的 80 毫秒,正是单道环境里「CPU 干等 IO」的浪费——一道程序去做 IO 时,CPU 转去算另一道,计算与输入输出重叠了起来。这就是多道程序设计的全部核心:让 CPU 与 IO 设备并行,提高资源利用率。

深蓝黑背景概念图。顶部条件卡:P1、P2、P3 三道程序相同,各需 CPU 计算 40ms、IO 60ms,先计算后 IO。中部「单道批处理」甘特图:一道跑完再跑下一道,每道占 100ms(青色计算 40ms + 紫色 IO 60ms),三段首尾相接铺满 0~300ms 时间轴,总计 300ms;时间轴刻度每 60ms 一格。下部「多道并发」甘特图按资源分两行:CPU 行 P1、P2、P3 的计算块连续占用 0~120ms,之后空闲;IO 行 P1 占 40~100ms,P2 等 IO 设备空出后占 100~160ms,P3 占 160~220ms;220ms 处画虚线终点标记「全部完成」,标注 CPU 只在 120ms 后空闲、IO 设备从 40ms 起一直忙,总计 220ms。底部总结条:300 − 220 = 80ms,多道技术把「CPU 等 IO」的浪费变成另一道程序的计算时间 图 2:对称模型下 300 毫秒与 220 毫秒的完整甘特推演。

回到原题:三段式加优先级,逐段画时间轴。

原题多了两个难点:每道程序是「计算—IO—再计算」三段,而且 CPU 不可抢占、资源竞争按优先级裁决。按时间轴推:

0~30:a 优先级最高,先上 CPU 计算。30~70:a 转去做 IO,CPU 交给 b,b 计算 30~90。70 时刻 a 的 IO 完成,想回头算最后 10 毫秒,但 CPU 被 b 占着且不可抢占,a 只能等。90:b 算完转 IO(占设备 90~120),此时等 CPU 的有 a 和 c,a 优先级高,a 算 90~100 后全部结束。100~120:CPU 轮到 c 计算。120:b 的 IO 完成、设备恰好空出,c 无缝开始 IO(120~160);同时 CPU 给 b 算收尾的 10 毫秒(120~130),b 结束。160~180:c 做最后一段计算,180 时刻全部完成。

多道环境总时间 180 毫秒。对照选项,答案就是单道 260、多道 180。

不放心?写个模拟器替你再推一遍。

手工画甘特图容易在「谁先抢到资源」上出错,用 Node.js 22 写个离散事件模拟器,把两种环境、两套参数都跑一遍:

// 每道程序是若干段 ['C'|'I', 毫秒];CPU 不可抢占,资源空出时按优先级取
function simulate(progs, single) {
  if (single) return progs.flatMap(p => p.segs).reduce((s, [, ms]) => s + ms, 0);
  let now = 0; const events = [];
  const cpu = { busy: false }, io = { busy: false }, cpuQ = [], ioQ = [];
  const procs = progs.map(p => ({ ...p, seg: 0 }));
  const later = (t, fn) => { events.push({ t, fn }); events.sort((x, y) => x.t - y.t); };
  const pump = () => {
    for (const [dev, q] of [[cpu, cpuQ], [io, ioQ]]) {
      if (dev.busy || !q.length) continue;
      q.sort((a, b) => a.prio - b.prio);
      const p = q.shift(), [, ms] = p.segs[p.seg];
      dev.busy = true;
      later(now + ms, () => {
        dev.busy = false;
        if (++p.seg < p.segs.length) (p.segs[p.seg][0] === 'C' ? cpuQ : ioQ).push(p);
        pump();
      });
    }
  };
  cpuQ.push(...procs); pump();
  while (events.length) { const e = events.shift(); now = e.t; e.fn(); }
  return now;
}
const sym = ['P1', 'P2', 'P3'].map((name, i) => ({ name, prio: i, segs: [['C', 40], ['I', 60]] }));
const orig = [
  { name: 'a', prio: 1, segs: [['C', 30], ['I', 40], ['C', 10]] },
  { name: 'b', prio: 2, segs: [['C', 60], ['I', 30], ['C', 10]] },
  { name: 'c', prio: 3, segs: [['C', 20], ['I', 40], ['C', 20]] },
];
console.log(simulate(sym, true), simulate(sym, false));   // 300 220
console.log(simulate(orig, true), simulate(orig, false)); // 260 180

输出 300/220 与 260/180,与手工推演完全一致——两组数字互相印证,考试时才有底气。

这道题真正考的是什么。

表面是算术,实际是三个概念:多道程序设计「计算与 IO 重叠」的收益来源、不可抢占调度的含义(开始算了就必须算完这一段)、优先级在资源竞争中的裁决作用。GPU 推理流水线里让计算与数据传输重叠、CPU 与加速器并行,用的是完全同一张甘特图。

答题结构建议:先秒答单道求和;再明确说出你假定的调度规则;然后画两行资源甘特图逐段推演,每个转折点说清「谁完成、谁等待、谁接上」;最后报两个数,并指出省下的时间本质是被填满的设备等待。如果面试官追问「改成可抢占会怎样」,你能现场重画一遍吗?