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