Skip to content

IO · 计算机基础面试题 ​

前端考 OS/计基,不是要你去写内核,而是三个很实际的能力:

  1. 理解并发模型——浏览器的多进程、Node 的事件循环、Flutter 的 Isolate,本质上都是进程/线程模型的变体;
  2. 给性能问题归因——"为什么这个循环这么慢"的答案常常在缓存局部性,"为什么内存一直涨"的答案在虚拟内存与 GC;
  3. 跟后端对等对话——"你这个服务能撑多少并发"取决于 I/O 模型,而不是"用了什么框架"。

所以本页的写法是:每个知识点都给"底层原理 + 前端/Node 侧的映射",让你答得出"所以我拿它干什么"。

📌 结构:零、一条主线 · 一、进程/线程/协程 · 二、并发与锁 · 三、内存管理 · 四、I/O 模型 · 五、CPU 缓存与调度 · 六、进程通信 · 七、存储与文件系统 · 八、自检 · 九、误区。


高频 Top 12(不会的话优先补这些) ​

#题目一句答案本页
1进程和线程的区别进程是资源分配的基本单位,线程是 CPU 调度的基本单位1.1
2协程为什么比线程轻量切换不进内核、不换页表、只需保存少量寄存器1.3
3死锁的四个必要条件互斥、占有且等待、不可抢占、循环等待——破坏任一个即可2.3
4悲观锁与乐观锁怎么选冲突频繁用悲观(互斥),冲突少用乐观(CAS + 版本号)2.2
5虚拟内存解决什么问题隔离 + 简化链接 + 突破物理内存 + 共享3.1
6分页和分段的区别分页是物理视角、定长、消外部碎片;分段是逻辑视角、变长、消不了外部碎片3.2
7什么是缺页中断访问的页不在物理内存(或没映射)→ 陷入内核 → 调页/报错3.3
8阻塞/非阻塞 与 同步/异步 的区别前者看线程是否被挂起,后者看结果怎么回来4.1
9select / poll / epoll 的区别epoll 用红黑树 + 就绪链表,不用每次全量拷贝与 O(n) 轮询4.3
10epoll 是异步 I/O 吗不是——它是同步 I/O 的多路复用(Reactor),数据还得你自己 read4.4
11Node 真的是单线程吗JS 执行单线程,I/O 靠 libuv 的多路复用 + 线程池4.6
12为什么顺序遍历数组比跳着快空间局部性——一次取一个 cache line(64B),顺带把后面几个元素也取进来了5.2

零、一条主线:程序眼里的世界长什么样 ​

理解所有计基问题的前提,是这张图:

你的代码(用户态)
   ├── 看到的是【虚拟地址】,不是物理内存 → 进程间天然隔离
   ├── 要读写文件/网络/分配内存,必须【系统调用】陷入内核 → 有切换成本
   ├── CPU 不直接读内存,先过 L1/L2/L3 缓存 → 访问模式决定速度
   └── 一次 I/O 要经历:用户缓冲 → 内核缓冲 → 设备 → (拷贝次数决定吞吐)

衍生出的四条"为什么"(面试时顺着这四条讲,逻辑是通的):

现象根因
为什么要有线程/协程系统调用与上下文切换太贵,要减少阻塞与切换
为什么会有锁和死锁多个执行单元共享资源且调度顺序不可控
为什么内存会"不够"虚拟内存 + 换页,工作集超出物理内存就抖动
为什么高并发要改架构每个连接一个线程扛不住 → 多路复用 → 事件驱动

一、进程 / 线程 / 协程 ​

1.1 本质区别 ​

进程线程协程
是什么资源分配的基本单位CPU 调度的基本单位用户态的轻量级执行单元
地址空间独立与同进程线程共享共享(同一线程内)
调度者内核内核用户程序自己(语言运行时/框架)
切换成本最高(换页表、TLB 失效)中(~微秒级,仍要进内核)最低(不进内核,保存少量寄存器)
通信方式IPC(管道/共享内存/socket)共享变量 + 锁共享变量 + 主动让出(await/yield)

线程之间共享什么、独占什么(高频追问):

共享独占
堆、全局变量、静态区栈、程序计数器(PC)、寄存器
打开的文件描述符、信号处理的部分语义线程局部存储(TLS)、调度优先级的部分状态

Linux 的实现上,线程就是"共享了地址空间的进程"(task_struct),调度的是线程而非进程—— 所以更准确的说法是:进程是资源的容器,线程是调度实体。

1.2 上下文切换到底贵在哪 ​

一次线程切换的成本不止"保存寄存器":

  1. 保存/恢复寄存器、栈指针、PC;
  2. 进入内核态(用户态↔内核态切换);
  3. 内核调度器选下一个任务(CFS 红黑树);
  4. 若是跨进程切换:换页表(写 CR3)→ TLB 大面积失效;
  5. CPU 缓存被冲掉(新任务的数据不在缓存里,前几次访问全是 miss)。

所以"多开几个线程就快了"是错的:线程数超过 CPU 核数后,增加的是切换开销而不是吞吐。 I/O 密集型可以多于核数(反正会阻塞),CPU 密集型应该≈核数(或核数±1)。

1.3 协程为什么轻 ​

协程的切换完全在用户态:

  • 不进内核 → 没有系统调用与特权级切换;
  • 不换页表 → TLB 不失效;
  • 需要保存的上下文极少(少量寄存器 + 栈指针);
  • 调度是协作式的:只有在 await/yield 这类明确的让出点才切换 → 不需要锁保护大部分临界区(因为没有抢占)。
抢占式(线程)协作式(协程)
切换时机任意指令处(时间片到)只在你主动让出时
需要锁是大部分场景不需要(除非多协程跑在多线程上)
风险—一个协程不让出,整条线程就卡死(同步阻塞调用、死循环)

这就是为什么 Go / Kotlin / Dart 的 await 里不能塞 CPU 密集的死循环,以及 为什么 Node 里一段同步长任务会卡掉所有请求——它们是协作式的,你占着不让,别人就全等着。

1.4 一张映射表:把概念落到你写的代码上 ​

场景用的是意味着
浏览器多进程架构:浏览器进程 / 渲染进程 / GPU 进程 / 网络进程(+ 站点隔离后按站点分进程)一个标签页崩了不会带崩整个浏览器;跨标签页不能直接共享内存
渲染进程内主线程(JS + 布局 + 绘制)+ 合成线程 + 光栅线程 + Worker 线程JS 与渲染互斥 → 长任务直接卡 UI;重活要丢给 Web Worker
Web Worker线程,但不共享内存(靠 postMessage 结构化克隆)没有数据竞争;但传大对象有拷贝成本(用 Transferable / SharedArrayBuffer 解决)
Node.jsJS 单线程 + libuv(网络用 epoll/kqueue/IOCP,文件用线程池)见 4.6
Flutter / DartIsolate:独立堆、不共享内存、通过 Port 传消息天生没有锁的问题;但对象要拷贝,大图/大列表传递有成本
Gogoroutine(M:N 协程,跑在多线程上)既轻量又能吃多核;但有数据竞争风险

→ 浏览器事件循环与 Node 差异见 A09。


二、并发与锁 ​

2.1 竞态:单线程也有竞态 ​

竞态条件 = 结果依赖于多个执行单元的调度顺序。

⚠️ 前端常见误解:"JS 是单线程,所以不会有并发问题。" 错。单线程只是没有指令级抢占,但 await 之间有明确的让出点,同一个"逻辑事务"可能被打断:

js
// 竞态示例:check-then-act 被 await 打断
async function pay(orderId) {
  if (await isPaid(orderId)) return;   // ① 检查
  await doPay(orderId);                 // ② 期间另一个 pay() 也通过了 ①
}
前端/Node 里的"并发"来源处理
await 之间的状态被别人改加幂等键 / 把 check-act 做成服务端原子操作
多个请求并发、返回顺序不确定请求序号丢弃过期响应 / AbortController 取消
多个标签页同时操作本地数据Web Locks API(navigator.locks.request)
Worker 间共享内存(SAB)SharedArrayBuffer + Atomics(需 COOP/COEP 响应头)
多进程/多实例的服务端分布式锁(Redis SET NX PX / Redlock)

2.2 锁的取舍:不是越重越好 ​

锁机制适用代价
互斥锁 mutex抢不到就睡眠(进内核等待队列)临界区较长上下文切换开销
自旋锁 spinlock抢不到就循环忙等临界区极短、多核占着 CPU 空转
读写锁读共享、写独占读多写少写饥饿;实现比互斥锁重
乐观锁 / CAS不加锁,更新时校验版本冲突少冲突多时反复重试;ABA 问题(用版本号解决)
无锁(lock-free)CAS 循环极低延迟场景实现复杂、难验证

判断依据:临界区执行时间 vs 一次上下文切换时间。 临界区比切换还短 → 自旋更划算;临界区长 → 睡觉(互斥)更划算。

2.3 死锁:四条件与四种破法 ​

死锁必须同时满足四个条件,破坏任意一个即可:

条件含义怎么破
互斥资源同一时刻只能被一个持有通常破不了(资源本来就是独占的)→ 换思路:资源池化/复制
占有且等待持有一个还去等另一个一次性申请全部资源(要么全拿到,要么都不拿)
不可抢占不能强行收回允许抢占:申请不到就释放已持有的再重试
循环等待A 等 B、B 等 A按固定顺序申请资源(给资源编号,必须从小到大申请)→ 最常用、最实用

答题模板:先说四条件,再给"按序加锁"这个工程上最常用的解法, 最后补一句"数据库里就是加锁顺序不一致 + 长事务导致死锁,所以要统一加锁顺序、缩短事务"。

银行家算法:避免死锁的理论方案(分配前先模拟,看是否处于安全状态)。 实际系统很少用(需要预知最大需求),检测 + 回滚更常见(数据库画等待图,选一个 victim 回滚)。

活锁与饥饿(追问加分):

现象例子
活锁没阻塞,但一直重试、一直失败,什么也没干成两个人都让路,结果来回让
饥饿永远轮不到自己读写锁下的写饥饿;低优先级任务被高优先级持续抢占

三、内存管理 ​

3.1 为什么必须有虚拟内存 ​

四个理由,缺一不可:

作用说明
隔离与安全每个进程看到独立的连续地址空间,踩别人的内存会 Segmentation fault 而不是悄悄改掉别人
简化链接与加载每个程序都可以从同一个固定地址开始链接,不用管物理内存到底放在哪
突破物理内存暂时不用的页换出到磁盘,工作集能大于物理内存(代价见抖动)
共享共享库、mmap 文件、进程间共享内存——同一份物理页映射到多个进程

3.2 分页 vs 分段 ​

分页分段
划分依据物理视角,固定大小(x86-64 通常 4KB)逻辑视角,按语义(代码段/数据段/堆/栈)变长
碎片有内部碎片(最后一页用不满),无外部碎片有外部碎片(段之间留下空洞)
对程序可见不可见(透明)可见(段是程序员/编译器能感知的单位)
现代 OS✅ 主流基本只保留做权限/兼容,实际靠分页

多级页表 + TLB:页表本身很大(64 位下 4 级页表),所以 CPU 里有个 TLB(快表) 缓存最近的地址翻译。 命中就免了多次内存访问;进程切换会换页表 → TLB 失效 → 这是进程切换比线程切换贵的直接原因之一。 还有大页(Huge Page,2MB/1GB)减少 TLB miss。

3.3 地址翻译:页表 → TLB → 缺页 ​

CPU 给出虚拟地址 → MMU 查 TLB
   ├─ 命中 → 直接得到物理地址(快)
   └─ 未命中 → 查多级页表
        ├─ 页在内存且已映射 → 填 TLB,继续(minor fault,成本较低)
        └─ 不在内存 / 未映射 → 【缺页中断】陷入内核
             ├─ 合法(换出到 swap 了 / 首次访问的匿名页 / mmap 的文件)→ 调页进来,重跑指令
             └─ 非法(野指针、越界)→ SIGSEGV

major fault vs minor fault:要不要读磁盘。major fault 慢几个数量级。 (前端能观察的:容器/服务监控里的 page fault 指标异常,往往是内存不够或 swap 开着。)

3.4 页面置换与抖动 ​

算法思路评价
OPT换出最远的将来才用的页理论最优,不可实现(要预知未来),只作基准
FIFO先进先出简单,但有 Belady 异常(给的页框越多,缺页反而越多)
LRU最久未使用接近 OPT,实现成本高(要维护顺序)
Clock / 二次机会环形扫描 + 访问位LRU 的近似实现,工程上用它
LFU用得最少有"历史包袱",旧热点赖着不走

抖动(thrashing):进程工作集 > 可用物理内存 → 刚换出的页马上又要换进来 → CPU 时间全花在换页上,吞吐塌方。 解法:降低并发度(把部分进程挂起)、加内存、关 swap(生产数据库/低延迟服务常见做法)。

3.5 分配与回收:从 OS 到 GC ​

应用 malloc/new  →  glibc/jemalloc/tcmalloc(用户态分配器:空闲链表 / size class / 线程本地缓存)
                 →  不够就 brk(扩堆)或 mmap(大块)
内核            →  伙伴系统(按页,2 的幂,解决外部碎片)+ slab/slub(内核小对象)

GC 算法(与 JS/Flutter 直接相关):

算法问题
引用计数循环引用回收不了(所以还需要标记清除兜底,如 C++ 的 weak_ptr 破环)
标记-清除有内存碎片
复制(半空间)无碎片,但浪费一半空间
标记-整理无碎片、不浪费,但要移动对象(更慢)
分代基于弱分代假说(大多数对象朝生夕死)→ 新生代用复制(便宜),老生代用标记清除/整理

→ V8 的分代回收、内存泄漏排查清单见 A09 · JavaScript 运行时; 线上内存问题的观测方法见 A12。


四、I/O 模型(本章最高频) ​

4.1 最容易答混的一组概念 ​

阻塞/非阻塞 看的是:调用后,当前线程有没有被挂起。 同步/异步 看的是:结果是谁、以什么方式给你的。

阻塞非阻塞
同步read():等到数据来才返回read() + O_NONBLOCK:没数据立刻返回 EAGAIN,你自己去问
异步(概念上不常见)io_submit / io_uring:你提交就走,内核完成后通知你

一句话记忆:同步 = 我自己取;异步 = 送上门。阻塞 = 我一直等;非阻塞 = 先走,回头再问。

4.2 五种 I/O 模型(Stevens 分类) ​

一次 I/O 其实是两步:① 等数据准备好(内核缓冲区)② 把数据从内核拷到用户空间。

模型第①步第②步评价
阻塞 I/O阻塞等待阻塞拷贝最简单,一个线程只能处理一个 fd
非阻塞 I/O立刻返回,轮询阻塞拷贝轮询烧 CPU
I/O 多路复用select/poll/epoll 阻塞等多个 fd阻塞拷贝(对就绪的 fd)高并发网络的事实标准
信号驱动信号通知阻塞拷贝用得少
异步 I/O(AIO)不阻塞内核拷完再通知真正的异步;Linux io_uring、Windows IOCP

4.3 三代多路复用:select → poll → epoll ​

selectpollepoll
数据结构fd_set 位图数组(链表)内核红黑树 + 就绪链表
数量限制FD_SETSIZE 通常 1024无硬限制无硬限制(只受内存/打开句柄数)
每次调用全量拷贝 fd 集合到内核同左一次 epoll_ctl 注册,之后不用重复传
找就绪 fdO(n) 轮询O(n) 轮询O(1) 直接拿就绪链表
触发方式LTLTLT(默认)+ ET

LT vs ET(高频追问):

  • LT 水平触发:只要缓冲区还有数据,就一直通知你 → 你可以分几次读,不容易漏,但可能重复唤醒。
  • ET 边缘触发:只在状态变化时通知一次 → 必须一次把数据读完(循环 read 到 EAGAIN), 而且 fd 必须设为非阻塞,否则最后一次 read 会卡死。epoll + ET + 非阻塞是 Nginx/Redis 的经典组合,性能更好但更易写错。

还有 EPOLLONESHOT(一次事件后不再通知,需要重新 epoll_ctl MOD)用于多线程处理同一 fd、避免竞态。

4.4 两种模式:Reactor vs Proactor ​

Reactor(反应式)Proactor(前摄式)
谁做拷贝应用程序自己 read内核拷完再通知
代表epoll / kqueue、Node、Netty、RedisWindows IOCP、Linux io_uring、.NET
归类同步 I/O(多路复用)异步 I/O

面试点:epoll 不是异步 I/O。它只是"帮你等",数据还是你自己读。 现在 Linux 上真正大规模落地的异步 I/O 是 io_uring(环形队列,还能省掉大量系统调用), 新一代数据库/代理(如部分 MySQL/TiDB、Netty 的 io_uring 分支)在跟进。

4.5 零拷贝:为什么 Kafka/Nginx 这么快 ​

传统 read() + write() 发一个文件:

磁盘 --DMA--> 内核缓冲 --CPU拷贝--> 用户缓冲 --CPU拷贝--> socket 缓冲 --DMA--> 网卡
     4 次上下文切换(2 次 read + 2 次 write),2 次 CPU 拷贝
优化做法省掉什么
mmap把内核缓冲映射到用户空间省 1 次 CPU 拷贝(用户态直接访问)
sendfile内核里直接把文件缓冲推给 socket省 2 次 CPU 拷贝 + 上下文切换减到 2 次(零系统调用拷贝)
splice / tee两个 fd 之间在内核里搬不需要用户态介入,也不要求一端是 socket
io_uring提交/完成用共享环形队列省掉大量系统调用本身的开销

前端的直观对应:静态文件服务器、CDN、大文件下载几乎都走 sendfile/mmap, 所以"用 Nginx 发静态文件"比"用 Node 读文件再写响应"快得多——Node 每发一个字节都要过用户空间。

4.6 Node 的"单线程"到底是什么意思 ​

         ┌─────────────── JS 主线程(单线程执行,事件循环)───────────────┐
         │  timers → pending callbacks → poll → check → close → microtask │
         └──────────────────────────┬─────────────────────────────────────┘
                                    │ 阻塞型工作交给 libuv
        ┌───────────────────────────┴────────────────────────────┐
        │  网络 I/O:epoll / kqueue / IOCP(多路复用,内核态)      │
        │  文件 I/O、DNS(getaddrinfo)、crypto、zlib:线程池        │
        │  线程池大小:UV_THREADPOOL_SIZE,默认 4                   │
        └────────────────────────────────────────────────────────┘
说法对错解释
"Node 是单线程"半对JS 执行单线程;I/O 由 libuv 的多路复用 + 线程池完成
"Node 适合 I/O 密集"✅高并发长连接的等待不占线程
"Node 不适合 CPU 密集"✅同步长任务会卡住整个事件循环(所有请求一起变慢)
"多开线程能解决 CPU 密集"部分worker_threads 可以,但不共享内存(靠消息/ArrayBuffer 转移);cluster 是多进程

CPU 密集的正确解法:worker_threads / cluster 多进程 / 用 Rust、Go 写扩展 / 把计算丢给队列。


五、CPU、缓存与调度 ​

5.1 用户态 / 内核态 ​

用户态内核态
权限受限,不能直接访问硬件/其他进程内存全权
切换方式系统调用(read/write/fork)、中断、异常
代价一次系统调用约 微秒级(保存现场 → 切栈 → 权限检查 → 执行 → 恢复)

实践意义:系统调用不是免费的。所以才有"批量读写""缓冲写""io_uring 减少系统调用"这些优化。

5.2 缓存局部性:前端能真实感知的一条 ​

CPU → L1(~1ns) → L2(~4ns) → L3(~10ns) → 内存(~100ns) → SSD(~100μs) → 网络/磁盘(ms)

cache line 是 64 字节——CPU 不是按"一个变量"取内存,而是一次取一整行。

推论:

写法结果
顺序遍历数组一次 cache line 带进来后面几个元素 → 快
跳着访问链表 / 随机访问大数组每次都 miss → 慢得多
数据紧凑(SoA / 定长结构)缓存命中高
对象里塞一堆指针到处指每次解引用都可能 miss

伪共享(false sharing):两个线程改同一个 cache line 里的不同变量 → 缓存一致性协议(MESI)会让两个核来回作废对方的缓存行 → 比分开在两个 cache line 慢得多。 解法:填充对齐(padding)。

前端能观察到的对应现象: 排序后再遍历更快(分支预测 + 预取友好); TypedArray 比普通 Array 快(连续内存 + 固定类型); 结构化克隆大对象很慢(遍历 + 拷贝,纯内存带宽与 cache miss 问题)。

5.3 分支预测 ​

CPU 会猜测分支走向并提前执行,猜错要回滚流水线。 所以对已排序数据做 if 判断,命中率极高 → 明显快于乱序数据(经典 Stack Overflow 问题 "Why is processing a sorted array faster")。

5.4 调度 ​

概念说明
进程状态就绪 / 运行 / 阻塞(+ 挂起)
调度算法时间片轮转 RR、优先级调度、CFS(Linux 完全公平调度,按 vruntime 用红黑树挑最小的)、多级反馈队列
优先级反转低优先级持锁 → 高优先级等锁 → 被中优先级抢占,导致高优先级反而最慢(火星探路者号的真实故障)→ 解法:优先级继承 / 优先级天花板

前端的映射:浏览器的"调度"也有同样的思路——

机制对应
长任务 > 50ms等价于"独占 CPU 不让出" → INP 变差
时间切片(React Scheduler)手动实现"协作式调度":每 5ms 让出一次,用 MessageChannel 而非 setTimeout(后者有 4ms 嵌套限制)
requestIdleCallback利用空闲时段,等价于"低优先级任务"
scheduler.yield() / isInputPending主动让出、判断是否有更高优先级的输入要处理

→ 完整的前端性能度量见 A25,优化清单见 A10。


六、进程间通信(IPC) ​

方式特点适用
管道 pipe半双工、字节流、有内核缓冲父子进程,shell |
命名管道 FIFO有名字,无关进程也能用本机简单通信
信号 signal只传一个编号,不能带数据通知(终止、重载配置)
消息队列有格式、内核中排队小消息,逐渐被替代
共享内存最快(无拷贝),但要自己同步(信号量/锁)大吞吐,数据库、Chrome 的部分场景
信号量计数器,做同步与互斥配合共享内存
socket / Unix domain socket可跨机;UDS 本机走内核,比 TCP 快通用;Docker/Nginx/php-fpm 常用 UDS
BinderAndroid 的 IPC(一次拷贝 + 能力校验)移动端

性能排序(直觉):共享内存 > UDS/管道 > socket(TCP) > 消息队列 > 信号(只当通知用)。 但共享内存换来的是"你得自己处理同步"——复杂度和性能的经典交换。

前端侧对应的通信机制:

Web/Node类比说明
postMessage消息传递结构化克隆(拷贝),大对象用 Transferable 转移所有权(零拷贝)
SharedArrayBuffer + Atomics共享内存真共享,要自己同步;需 COOP/COEP 响应头(安全隔离要求)
MessageChannel双向管道比 postMessage 更适合持续通信;React Scheduler 就靠它做时间切片
BroadcastChannel同域广播多标签页同步
Web Locks API锁多标签页互斥
Node cluster / child_processIPC主进程与子进程用内置 IPC 通道

七、存储与文件系统(简版) ​

前端不必深挖,但存储选型题会用到这几条。

概念要点
页缓存 page cache内核用空闲内存缓存磁盘数据;读写基本都先落缓存,再异步回写(writeback)
fsync强制把缓存刷到磁盘。不做 fsync 掉电就可能丢数据——代价是慢,所以数据库要调 fsync 策略(每条/每秒/每事务)
顺序 vs 随机顺序读写远快于随机(HDD 差百倍,SSD 也有差距与写放大、GC 压力)→ 所以有 LSM-Tree(把随机写变顺序写,Kafka/RocksDB/ClickHouse 的思路)
inode / 硬链接 / 软链接inode 存元数据;硬链接共享 inode(删除只是减引用计数),软链接是路径(原文件删了就失效)
mmap把文件映射成内存,省一次拷贝;适合大文件只读/随机访问
零拷贝见 4.5

浏览器侧的对应:

存储类比特点
localStorage小、同步、字符串同步 API 会阻塞主线程,别存大东西
IndexedDB事务型 KV(异步)容量大;API 啰嗦,通常包一层
OPFS(File System Access)真实文件 + 可 createSyncAccessHandle 在 Worker 里同步读写做本地大文件/媒体/数据库引擎的底座
Cache Storage请求-响应缓存配合 Service Worker 做离线

八、自检清单 ​

并发模型

  • [ ] 说清进程/线程/协程在"谁调度、切换代价、是否共享内存"上的差异
  • [ ] 说清协作式的风险(一个不让出,全队卡死),并映射到 Node 长任务 / Dart Isolate
  • [ ] 能指出"JS 单线程所以没并发问题"这句话错在哪(至少给两个例子)
  • [ ] 死锁四条件 + 至少两种破法,能说出工程上最常用的是"按序加锁"

内存

  • [ ] 说清虚拟内存的四个作用,以及 TLB 存在的原因
  • [ ] 说清分页 vs 分段、内部碎片 vs 外部碎片
  • [ ] 说清缺页中断的完整流程与 major/minor 的区别
  • [ ] 说清抖动是什么、怎么解;GC 各算法的取舍(并链到 A09)

I/O 与性能

  • [ ] 能区分"阻塞/非阻塞"与"同步/异步"两组概念
  • [ ] 说清 select/poll/epoll 三代差异,以及 LT/ET 的实践要求(ET 必须配非阻塞)
  • [ ] 说清 epoll 为什么属于同步 I/O,真正的异步是什么
  • [ ] 说清零拷贝省了什么(拷贝次数 + 上下文切换次数)
  • [ ] 说清 Node 单线程的真实含义与 CPU 密集的解法
  • [ ] 能用 cache line 解释"为什么顺序遍历更快",并说出伪共享

九、常见误区 ​

❌ 误区✅ 正解
"多开线程就更快"超过核数后增加的是切换开销;CPU 密集应≈核数
"JS 单线程,所以没有并发问题"await 之间有让出点,单线程也有竞态(check-then-act、响应乱序)
"epoll 是异步 I/O"它是同步 I/O 的多路复用(Reactor),拷贝还得自己做
"虚拟内存就是拿磁盘当内存"那只是功能之一;隔离与简化链接同样重要
"分页和分段差不多"分页定长、物理视角、无外部碎片;分段变长、逻辑视角、有外部碎片
"LRU 是真实实现"工程上多用 Clock/二次机会 这类近似算法
"死锁只要加锁小心点就行"系统性解法是统一加锁顺序(破坏循环等待)
"IO 多路复用就是异步"多路复用解决的是"一个线程等很多 fd",异步解决的是"谁来完成拷贝"
"零拷贝是一次都不拷"是不用 CPU 拷(DMA 仍在工作),且省的是上下文切换 + CPU 拷贝
"共享内存最快所以该用"它把复杂度转嫁给你:同步、崩溃一致性、泄漏都得自己扛
"内存不够就加 swap"高负载服务开 swap 会抖动,宁可限流/扩容/关 swap
"操作系统离前端很远"浏览器多进程、事件循环、时间切片、Worker、OPFS——全是这些概念的变体

相关文档 ​