操作系统基础
你写的高并发、JVM、锁,全都建立在操作系统的这几个概念上。 这篇不只讲「为什么」,还给了每个概念对应的观测命令 —— 知道概念却不会查,等于不会。
一、进程 vs 线程 vs 协程
| 进程 | 线程 | 协程 | |
|---|---|---|---|
| 资源 | 资源分配的基本单位(独立地址空间) | 共享进程内存 | 共享线程栈 |
| 调度 | 进程级调度 | CPU 调度的基本单位 | 用户态调度(无需内核) |
| 开销 | 创建销毁重 | 较轻(约 1~10 微秒级切换) | 极轻(纳秒级切换) |
| 隔离 | 强(一个进程崩不影响别人) | 弱(共享内存,一个崩可能全崩) | 弱 |
一句话:进程是「有自己房子的人」,线程是「房子里的人」,协程是「人手里同时做的几件事」。
为什么线程切换比进程快:进程切换要换页表、清 TLB;线程切换只换栈和寄存器(共享地址空间)。
Java 的
Thread在 JDK 21 之前是1:1 映射到内核线程;JDK 21 的虚拟线程是 M:N(大量虚拟线程跑在少量载体线程上),见 JUC 并发编程。
二、进程状态与调度
新建 → 就绪 ⇄ 运行 → 终止
↑ ↓
└─ 阻塞 ←─(等待 IO / 锁)新建 → 就绪 ⇄ 运行 → 终止
↑ ↓
└─ 阻塞 ←─(等待 IO / 锁)| 算法 | 特点 | 问题 |
|---|---|---|
| FCFS(先来先服务) | 简单 | 长任务拖死后面(护航效应) |
| SJF(短作业优先) | 平均等待最短 | 长任务可能饿死 |
| 时间片轮转(RR) | 公平 | 时间片太大=退化成 FCFS,太小=切换开销大 |
| 多级反馈队列 | 现代系统主流(Linux 的 CFS 近似) | 实现复杂 |
上下文切换的代价:保存寄存器/栈,恢复下一个,通常 2~10 微秒。看起来很小,但每秒切几万次就是几十毫秒的开销 —— 这是「多线程不一定更快」的根源,也是 top 里 sy(内核态)特别高的常见原因。
怎么观测:vmstat 1 的 cs 列(每秒上下文切换次数)、pidstat -w 1(每个进程的 cswch/nvcswch)。
三、线程同步
| 原语 | 作用 |
|---|---|
| 互斥锁(Mutex) | 互斥访问临界区 |
| 信号量(Semaphore) | 控制同时访问的数量(可以 >1) |
| 条件变量(CondVar) | 等待某个条件成立(配合锁用) |
| 读写锁 | 读读并发,写写/读写互斥 |
死锁四条件(缺一不可):互斥、持有并等待、不可剥夺、循环等待。破坏任意一个即可避免:
- 一次性申请所有资源(破坏持有并等待)
- 按固定顺序加锁(破坏循环等待,最实用)
tryLock带超时(破坏不可剥夺)
怎么观测:jstack 能直接报 Found one Java-level deadlock;系统级用 cat /proc/<pid>/stack、gdb 抓线程栈。
四、虚拟内存与分页
为什么需要虚拟内存
每个进程都以为「我独占整个内存」,其实靠操作系统把虚拟地址映射到物理地址。好处:
- 隔离:一个进程读不到别的进程的内存;
- 省内存:不同进程共享同一份只读代码(如 libc);
- 简化链接:程序可以从固定地址开始加载。
分页与页表
虚拟地址 = 页号 + 页内偏移
页表:页号 → 物理页框号虚拟地址 = 页号 + 页内偏移
页表:页号 → 物理页框号- 页:内存管理的最小单位(通常 4KB)。
- 页表:进程级,太大 → 多级页表(只给用到的地址建表)。
- TLB:页表的缓存(在 CPU 里),命中就不查页表 —— 这就是「进程切换慢」的原因(TLB 要刷)。
三个内存指标必须分清
| 指标 | 含义 | 排查时怎么用 |
|---|---|---|
| VSS(Virtual Set Size) | 进程申请的虚拟内存总量 | 通常很大(含未使用、共享库、预留),几乎没有参考价值 |
| RSS(Resident Set Size) | 实际占用的物理内存 | 最常用,但共享库被重复计算(10 个进程用同一个 lib,每个都算一份) |
| PSS(Proportional Set Size) | 共享内存按比例分摊后的实际占用 | 最准,smem 才能看到 |
典型误区:
ps看到的 RSS 加起来远超物理内存 —— 不是内存泄漏,是共享页被重复计数了。
缺页中断
访问的页不在物理内存 → 触发缺页中断 → 从磁盘换入。频繁缺页 = 抖动(thrashing),说明物理内存不够或程序局部性差。
怎么观测:vmstat 1 的 si/so(换入换出,持续 >0 就危险)、pidstat -r 1 的 majflt(主缺页,要从磁盘读)。
五、文件系统
| 概念 | 说明 |
|---|---|
| inode | 文件的元数据(权限、大小、数据块位置),不含文件名 |
| 文件名 | 目录项里「名字 → inode」的映射 |
| 软链接 | 指向路径(可跨文件系统,删原文件就断) |
| 硬链接 | 指向同一个 inode(删一个名字,数据还在) |
| 页缓存(page cache) | 文件 IO 走内核缓存,写有延迟 —— fsync 才落盘 |
两个高频现象:
rm了文件空间不释放:进程还开着这个 inode,要等它关闭。lsof +L1能看到。free显示内存快满了但系统不卡:那是 page cache,可回收,看available而不是free。
六、五种 IO 模型与零拷贝
| 模型 | 特点 | 例子 |
|---|---|---|
| 阻塞 IO | 一直等数据就绪 | 传统 read() |
| 非阻塞 IO | 立即返回,自己轮询 | O_NONBLOCK |
| IO 多路复用 | 一个线程监听多个 fd | select / poll / epoll |
| 信号驱动 IO | 内核发信号通知 | SIGIO |
| 异步 IO | 内核拷完再回调 | io_uring、AIO |
epoll 为什么快:注册一次 + 只返回就绪的(回调通知),不像 select 每次全量拷贝并遍历。完整对比见 Java NIO。
零拷贝(后端必问)
普通 read + write 传文件要 4 次拷贝 + 4 次上下文切换:
磁盘 → 内核缓冲区 → 用户缓冲区 → socket 缓冲区 → 网卡磁盘 → 内核缓冲区 → 用户缓冲区 → socket 缓冲区 → 网卡| 方案 | 省了什么 | 适用 |
|---|---|---|
sendfile | 省掉用户态(2 次拷贝) | 静态文件服务(Nginx) |
mmap + write | 省掉一次拷贝 | 大文件、需要改内容 |
splice | 内核管道传递 | 代理转发 |
Kafka 为什么吞吐高:大量用到了 sendfile + 页缓存,消费时数据不落用户态。
七、概念 ↔ 观测命令对照表
这张表是这篇最有用的部分:每个 OS 概念,怎么用命令看到它。
| 概念 | 命令 | 看什么 |
|---|---|---|
| 平均负载 | uptime / top | load average(1/5/15 分钟),> 核数说明排队 |
| CPU 使用率 | top、mpstat -P ALL 1 | %us(用户态)/%sy(内核态)/%wa(IO 等待) |
| 哪个线程在吃 CPU | top -Hp <pid> | 线程级 CPU,配合 jstack 的 nid |
| 上下文切换 | vmstat 1(cs)、pidstat -w 1 | 每秒切换次数 |
| 内存总量/可用 | free -h | 看 available,别只看 free |
| 进程内存 | ps aux --sort=-rss、smem -tk | RSS vs PSS |
| 交换 | vmstat 1(si/so)、swapon -s | 持续换入换出=内存不足 |
| 缺页 | pidstat -r 1 | majflt(主缺页,要读磁盘) |
| 磁盘吞吐/延迟 | iostat -xz 1 | %util(饱和)、await(延迟) |
| 谁在读写磁盘 | iotop -o | 进程级 IO |
| 磁盘空间 | df -h、du -sh * | 分区用量 / 目录占用 |
| 被删除仍占用的文件 | lsof +L1 | 日志没轮转的典型症状 |
| 网络连接 | ss -tanp、ss -s | 各状态连接数(TIME_WAIT / ESTAB) |
| 网络重传/丢包 | netstat -s、nstat | retrans、drop |
| 抓包 | tcpdump -i eth0 port 8080 -w x.pcap | 握手、重传、RST |
| 系统调用 | strace -p <pid> -T -f | 卡在哪个 syscall |
| 文件句柄 | lsof -p <pid>、ulimit -n | 句柄泄漏、上限 |
| 内核日志 | dmesg -T | OOM Killer、驱动报错 |
八、从现象到命令(排查路径)
| 现象 | 先跑什么 | 常见结论 |
|---|---|---|
| CPU 100% | top → top -Hp → jstack 对 nid | 死循环 / 频繁 GC / 锁竞争 |
| load 高但 CPU 闲 | iostat -xz 1 | 磁盘 IO 等待(D 状态进程) |
| 内存持续涨 | free -h + smem + jmap -histo:live | 泄漏 / page cache / 堆外内存 |
| 进程被 kill | dmesg -T | grep -i oom | OOM Killer 选中了它 |
| 接口变慢 | iostat → ss -s → strace -T | 磁盘、连接堆积、某个 syscall 阻塞 |
| 连接数打满 | ss -s、ulimit -n、sysctl net.core.somaxconn | 短连接太多 / backlog 太小 |
| 磁盘写满 | df -h → du -sh → lsof +L1 | 日志、core dump、未释放的删除文件 |
九、和 Java 世界的对应关系
| OS 概念 | Java 里对应什么 |
|---|---|
| 进程 | 一个 JVM 实例(java -jar) |
| 内核线程 | java.lang.Thread(JDK 21 前 1:1 映射) |
| 用户态调度 | 虚拟线程(JDK 21,M:N) |
| 互斥锁 / 条件变量 | synchronized、ReentrantLock + Condition(AQS 实现) |
| 信号量 | Semaphore |
| 读写锁 | ReentrantReadWriteLock、StampedLock |
| 虚拟内存 | JVM 堆、Metaspace、直接内存(-Xmx、-XX:MaxDirectMemorySize) |
| 页 | JVM 的**卡表(card table)**用于 GC 跨代引用、堆按 Region 划分 |
| 缺页 / 换页 | 堆被 swap 会严重拖慢 GC —— 生产环境应 vm.swappiness=0 或禁用 swap |
| epoll | NIO 的 Selector(Linux 下就是 epoll) |
| 零拷贝 | FileChannel.transferTo(sendfile)、Netty 的 FileRegion |
| page cache | 读写文件走内核缓存;mmap 映射大文件 |
| 上下文切换 | 线程池开太大反而慢;jstack 里大量 BLOCKED 是锁竞争 |
十、速查
| 问题 | 一句话 |
|---|---|
| 进程线程区别 | 资源分配单位 vs CPU 调度单位 |
| 线程切换为什么快 | 共享地址空间,不换页表 |
| 上下文切换代价 | 约 2~10 微秒,看 vmstat 的 cs |
| 死锁四条件 | 互斥、持有并等待、不可剥夺、循环等待 |
| 虚拟内存好处 | 隔离、共享、简化加载 |
| RSS / PSS 区别 | RSS 重复计算共享页,PSS 按比例分摊更准 |
| TLB 是什么 | 页表缓存,miss 要查内存页表 |
| 缺页抖动 | 内存不够,vmstat 的 si/so 持续为正 |
| page cache 占满内存 | 正常,看 available 不看 free |
| 硬链接 vs 软链接 | 同 inode vs 指路径 |
| 零拷贝省了什么 | 省掉用户态那次拷贝(sendfile) |
| epoll 为什么快 | 注册一次 + 就绪回调 |
Cao Note