← 返回思维链目录
1. 进程 vs 线程
现象一边放音乐一边打字一边下载,操作系统怎么同时干这么多事?
本质:CPU 早期只有一个,所谓"同时"是快速切换 —— 每个程序跑一小会儿就切到下一个,切换得够快人就看不出。这种切换的最小单位是进程/线程。
直觉:进程重(独立内存空间,切换贵),线程轻(共享内存,切换快)。需要隔离用进程,需要高效通信用线程。
🌊 顺藤摸瓜
① 切换代价
同时干好多事靠时间片轮转切换 → 切换要保存当前状态、切回来要恢复 → 这就是上下文切换(保存/恢复寄存器、内存映射、栈)→ 切换太频繁会浪费 CPU
时间片调度上下文切换并发 vs 并行
② 为什么有线程
进程太重(独立内存,切换贵)→ 一个进程里开多个线程,共享内存但各有栈 → 切换快,通信方便(直接共享内存)→ 但要加锁(共享会冲突)
进程独立内存线程共享内存共享=要加锁
③ 怎么选
要隔离/稳定(一个崩不影响别的)→ 进程(Nginx/Chrome 多进程);要高效通信/共享 → 线程(Java 多线程);更轻量 → 协程(Go goroutine)
隔离性Nginx 多进程协程(更轻)
进程是独立厨房,线程是共用厨房的厨师。切换都有代价,线程切换比进程便宜 —— 这也是为什么 Java 喜欢用线程池复用,别老建新的。
2. 内存管理:虚拟内存
现象电脑 8G 内存,开了十几个程序也没爆?
本质:每个程序以为自己独占一大片内存(虚拟内存),实际用到的才映射到物理内存,用不到的放磁盘。实现了隔离 + 超用。
直觉:程序"以为"自己有 4G,实际用到哪块才给它真内存,用不到的挪磁盘(swap)。挪来挪去慢,这就是"swap 用得多系统就卡"。
🌊 顺藤摸瓜
① 虚拟内存
每个进程有自己的虚拟地址空间 → 实际用到的才映射到物理内存(页表映射)→ 实现隔离(A 改不了 B 的内存)
虚拟地址空间页表/MMU进程隔离
② swap 换页
内存不够 → 把不用的数据挪磁盘 → 用到了再调回来(缺页中断)→ 频繁换页 = 抖动,系统巨慢
swap 交换区缺页中断抖动 thrashing
③ 延伸 Java
JVM 堆 = 进程虚拟内存里的一块 → 所以 JVM OOM 不一定是物理内存不够(可能是 Xmx 设小了)→ 容器里还要看 cgroup 内存限制
JVM 堆/XmxOOM vs 物理内存cgroup 限制
虚拟内存让每个程序"以为自己独占",实现隔离 + 超用。代价是缺页时慢(磁盘 IO)。
3. IO 模型演进:为什么有 epoll
现象一个服务器要同时处理 1 万个客户端连接,怎么办?
本质:IO 模型演进 = 从"主动轮询"到"被动通知"。select/poll 是挨个问"你有数据吗"(累),epoll 是"谁有数据自己来登记"(只处理有事的)。
直觉:一个护士管 1000 个病人,挨个问累死。装个显示屏,谁有事自己亮灯,护士只去亮灯的 —— 这就是 epoll。
🌊 顺藤摸瓜
① BIO 阻塞
最早:一连接一线程(阻塞 IO)→ 1 万连接 = 1 万线程 → 线程太多撑不住 → C10K 问题
阻塞 IO一连接一线程C10K
② select
一个线程管多个连接 → select:每次问所有连接"你有数据吗?" → 问题:① 每次传 1024 个 fd 给内核(慢);② 内核遍历全部找有事的(大部分没事);③ 1024 上限
selectfd 拷贝1024 上限
③ poll
poll 去掉 1024 上限(用链表)→ 但本质没变,还是要遍历全部(O(n))→ 连接多了还是慢
poll仍 O(n) 遍历
④ epoll
epoll:内核维护就绪列表 → 有事的自己挂上来 → 返回时只给有事的(O(1))→ Redis/Nginx/Netty 都用它 → 单线程扛几万连接
epoll 就绪列表O(1) 取就绪Redis/Nginx 用
⑤ 延伸 Java
Java NIO = epoll 的封装 → Netty = Java 网络框架 → 一个 Selector 管很多 Channel → 这就是 Reactor 模式
Java NIONettyReactor 模式
IO 模型演进 = 从"主动轮询"到"被动通知"。Redis/Nginx 单线程扛几万连接,靠的就是 epoll,不靠多线程。
4. 零拷贝:读写文件优化
现象读一个文件再发网络,数据拷贝了好几次?
本质:用户程序不能直接碰硬件,要通过内核。传统读写数据在内核空间和用户空间之间来回拷,零拷贝就是跳过用户空间,数据直接从内核到网卡。
🌊 顺藤摸瓜
① 传统 4 次拷贝
磁盘→内核缓冲→用户缓冲→socket 缓冲→网卡 → 4 次拷贝 + 2 次系统调用 + 2 次 CPU 参与 → 慢
内核 vs 用户空间系统调用开销CPU 拷贝
② 零拷贝
sendfile/mmap → 跳过用户空间 → 数据直接内核→网卡(DMA 搬,不用 CPU)→ Kafka 用 sendfile 高速传输,Nginx 也用
sendfilemmapDMA 拷贝Kafka/Nginx
③ 为什么不直接读
用户不能直接读磁盘 → 权限隔离(用户程序不能碰硬件,要通过内核)→ 这是安全需要(防恶意程序搞坏系统)
用户态/内核态权限隔离ring0/ring3
5. 进程调度
现象100 个进程抢 4 个 CPU,谁先跑?为什么动鼠标比编译快?
本质:调度算法决定谁先用 CPU。现代操作系统用"优先级 + 时间片":交互任务(鼠标键盘)优先级高先响应,后台任务(CPU 密集)按时间片轮转。
🌊 顺藤摸瓜
① 算法演进
FCFS(先来先服务)→ 短作业优先 → 时间片轮转 → 多级反馈队列(优先级 + 时间片 + 动态调整)
FCFS时间片轮转多级反馈队列
② 交互优先
IO 密集型(等键盘鼠标)优先级高 → CPU 密集型(编译)优先级低 → 所以动鼠标立刻有响应,编译在后台慢慢跑
IO 密集 vs CPU 密集响应时间吞吐量
总结
| 现象 | 本质 | 第一反应 |
| 同时干好多事 | CPU 时间片切换 | 进程(重)vs 线程(轻),切换有代价 |
| 内存够用 | 虚拟内存,用到的才映射 | 隔离 + 超用,代价是缺页慢 |
| 1 万连接 | 不能一连接一线程 | select 轮询 → epoll 通知 |
| 读写文件慢 | 数据来回拷 | 跳过用户空间 → 零拷贝 sendfile |
| 谁先用 CPU | 优先级 + 时间片调度 | 交互优先,CPU 密集后台 |
本质:操作系统是个大管家,管理 CPU、内存、磁盘、网络分给各个程序。所有机制都是"在有限资源下,让多个程序高效、公平、隔离地跑"。