# 操作系统
30 秒速记
- 操作系统 = 管硬件的大管家:管
CPU、内存、文件、设备,对上给程序提供系统调用 - 四个基本特征:并发、共享、虚拟、异步;并发是最核心的,其他三个都围绕它
- 进程是资源分配单位,线程是调度单位;同进程的线程共享内存,切换轻但隔离差
- 调度看目标:批处理看吞吐,交互系统看响应,实时系统看截止时间,没法同时最优
- 并发就要处理互斥、同步和死锁;内核结构上有大内核(快)和微内核(隔离好)之分
操作系统就是一层管家软件,把 CPU、内存、磁盘、各种设备管起来,再用系统调用这套受控接口租给应用程序用。 它有四个基本特征:并发是让多个程序在一段时间里一起推进;共享是这些程序共用同一批资源;虚拟是把一个物理资源变成多个逻辑资源,比如一颗 CPU 分时给很多进程;异步是进程走走停停,什么时候跑完没法预知。围绕这几个特征,才有了进程线程、调度算法、锁和死锁、虚拟内存这些后面要讲的内容。
# 一、概述
# 基本特征
面试官追问
追问 1操作系统四个基本特征里,哪个是最根本的?
并发。正因为要让多个程序同时推进,才需要共享资源;资源不够分,才需要虚拟化把一个变多个;多个程序抢资源,执行节奏就变得走走停停,也就是异步。
追问 2浏览器一边下载文件一边播动画,体现了哪个特征?
首先是并发:下载在等网络时,CPU 去渲染动画,两件事在一段时间里一起推进。多核机器上浏览器进程和 GPU 进程还可能真的并行。
追问 3进程和线程的区别,用浏览器举个例子?
Chrome 每个标签页一般是独立的渲染进程,一个页面崩了不影响别的,这是进程隔离的好处。同一个渲染进程里有主线程、合成线程、Worker 线程,它们共享这个进程的内存,通信快但一个线程写坏内存整个进程都会挂。
追问 4为什么说调度算法没有最好的?
目标互相打架:想吞吐高就少切换,让长任务一口气跑完;想响应快就得频繁切换给交互任务让路,切换本身又有开销。服务器批处理和桌面系统要的东西不一样,算法自然不同。
追问 5前端为什么要学操作系统?
很多前端问题的根在这:JS 单线程为什么能处理并发请求、Web Worker 为什么不能直接操作 DOM、Node.js 的 libuv 线程池为什么默认 4 个线程,都要用进程、线程、I/O 模型去解释。
# 1. 并发
30 秒速记
- 并发 = 一段时间内多个任务交替推进;并行 = 同一时刻真的有多个任务在跑
- 并行要硬件:多核、多流水线、多台机器;单核只能并发,不能并行
- 操作系统靠进程、线程加时间片切换实现并发
- 并发的价值是「等待时不闲着」:
I/O密集型收益大,纯计算任务在单核上拆线程反而更慢 - 前端例子:
JS单线程 + 事件循环是并发不是并行;Web Worker跑在另一个线程上,才可能并行
并发是一段时间里多件事交替推进,并行是同一瞬间多件事真的在同时跑。 打个比方,一个厨师在炖汤的时候去切菜,两道菜都在进行,这是并发;两个厨师各做一道,才是并行。单核 CPU 靠时间片切换也能做到并发,但并行必须多核。并发真正的价值是等待时不闲着,比如一个请求在等磁盘,CPU 先去处理别的。JS 就是典型:单线程,靠事件循环在等网络时去干别的,所以能并发处理很多请求,但纯计算还是一个一个算。
并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。
并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。
操作系统通过引入进程和线程,使得程序能够并发运行。
原理拆解: 操作系统把可运行的线程放入调度队列,按时间片、优先级和阻塞状态选择执行者。单核上,线程在计算、等待 I/O、被抢占之间快速切换,使多个任务在观察尺度上共同前进;切换时要保存和恢复寄存器、栈指针等执行现场。多核环境可以让不同线程分别占用核心,此时并发任务才可能在物理时间上重叠执行。
具体例子: 一个服务同时处理下载和日志写入时,请求线程在等待磁盘或网络期间会被挂起,处理器转去运行其他线程,因此吞吐量和响应性可能提升。若把一个纯计算循环拆成多个线程,却仍运行在单核上,工作总量没有减少,还增加了调度、上下文切换和同步成本,完成时间反而可能变长。共享计数器等数据还会产生竞争,需要锁、原子操作或消息传递来约束访问顺序。
边界与反例: “同时处理很多请求”不能证明它们并行执行;异步事件循环也能在单线程中组织大量并发 I/O。相反,多核并行也不保证线性加速:串行部分、锁竞争、缓存失效、内存带宽和任务分配不均都会限制收益。线程数量远大于核心数时,过度切换还可能降低有效计算时间。
工程验证: 可分别构造 I/O 等待型和持续计算型任务,逐步增加线程数,并记录总耗时、吞吐量、处理器利用率、上下文切换次数及运行队列长度。若 I/O 型任务在线程增加后吞吐提升而处理器未满,说明等待时间被其他任务利用;若计算型任务超过核心数后吞吐不再增长且切换次数上升,则瓶颈已从任务数量转为计算资源或同步开销。判断并发效果应结合时间线和系统指标,不能只看线程是否都处于存活状态。
面试官追问
追问 1JS 是单线程的,为什么能同时发十个请求?
发请求只是把任务交给浏览器的网络线程,JS 主线程马上就空了,等结果回来再通过事件循环执行回调。十个请求在一段时间里一起推进,是并发;真正的计算还是主线程一个一个跑。
追问 2那页面里一个大循环算了 3 秒,点按钮没反应,怎么解决?
这是 CPU 密集,并发救不了,主线程被占着事件循环转不动。把计算挪到 Web Worker 里,多核机器上它和主线程是真正并行的;或者把任务切片,用 requestIdleCallback 分批跑。
追问 3单核机器上把计算任务拆成 8 个线程,会变快吗?
不会,还可能更慢。总计算量没变,单核同一时刻只能跑一个,拆完还多了线程切换和同步的开销。多线程只有在多核或者有 I/O 等待时才有收益。
追问 4Node.js 也是单线程,为什么能扛高并发?
JS 执行是单线程,但 I/O 交给了 libuv:网络用操作系统的 epoll/kqueue 异步通知,文件操作丢到线程池里。主线程只负责调度回调,大量请求都在等 I/O 时它几乎不闲着。
追问 5线程数一直加,吞吐量会一直涨吗?
不会。超过核心数后,CPU 密集任务就开始排队切换,上下文切换、锁竞争、缓存失效都会吃掉收益,到某个点吞吐反而下降。线程数要按任务类型压测出来,不是越多越好。
# 2. 共享
30 秒速记
- 共享 = 系统资源被多个并发进程共同使用
- 两种方式:同时共享(多个进程可以同时用,比如只读文件)/ 互斥共享(一次只能一个用,比如打印机)
- 互斥共享的资源叫临界资源,访问它的那段代码叫临界区,同一时刻只能有一个进程进去
- 实现互斥靠锁、信号量这类同步机制;锁保护的是「读-改-写」这个过程,不是资源名字
- 易错:锁粒度太大会把能并行的也串行化;多把锁顺序不一致会死锁
共享就是多个进程共用同一批系统资源,关键看这些访问能不能安全地叠在一起。 像只读的配置文件,大家同时读没问题,这是同时共享;但打印机就不行,两个任务同时往里写,纸上内容就交叉了,这种一次只能一个人用的叫临界资源,要互斥共享。实现上就是给访问它的代码段加锁,保证同一时刻只有一个执行者在里面。前端也会遇到:单线程的 JS 照样有竞态,因为 await 中间会让出执行权,「读、等、写」三步被别的任务插队,结果就被覆盖了。
共享是指系统中的资源可以被多个并发进程共同使用。
有两种共享方式:互斥共享和同时共享。
互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。
原理拆解: 共享的判断链路是:先确认多个进程是否会访问同一资源,再判断这些访问能否安全重叠。只读数据通常可以同时共享;打印机、可写文件偏移量或未提供原子操作的共享状态,若并发访问会产生输出交叉、覆盖更新等错误,就应视为临界资源。互斥锁把“检查、使用、更新”组成不可分割的临界区,其他访问者只能等待。锁保护的是访问协议,而不是资源名称本身:同一个文件在只读时可同时共享,执行覆盖写入时却可能需要互斥。
最小验证: 输入三个并发任务,每个任务进入临界区后等待不同时间并修改计数器;要验证的结论是,无锁时临界区可能重叠,加锁后任一时刻最多只有一个任务进入。以下代码可直接由 Node.js 执行:
class Mutex {
constructor() {
this.tail = Promise.resolve();
}
async run(fn) {
const previous = this.tail;
let release;
this.tail = new Promise(resolve => (release = resolve));
await previous;
try {
return await fn();
} finally {
release();
}
}
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function test(useMutex) {
const mutex = new Mutex();
let active = 0;
let maxActive = 0;
let counter = 0;
const task = async ms => {
const critical = async () => {
active++;
maxActive = Math.max(maxActive, active);
const old = counter;
await sleep(ms);
counter = old + 1;
active--;
};
