Skip to content

操作系统基础 ​

你写的高并发、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 抓线程栈。

四、虚拟内存与分页 ​

为什么需要虚拟内存 ​

每个进程都以为「我独占整个内存」,其实靠操作系统把虚拟地址映射到物理地址。好处:

  1. 隔离:一个进程读不到别的进程的内存;
  2. 省内存:不同进程共享同一份只读代码(如 libc);
  3. 简化链接:程序可以从固定地址开始加载。

分页与页表 ​

虚拟地址 = 页号 + 页内偏移
页表:页号 → 物理页框号
虚拟地址 = 页号 + 页内偏移
页表:页号 → 物理页框号
  • 页:内存管理的最小单位(通常 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 才落盘

两个高频现象:

  1. rm 了文件空间不释放:进程还开着这个 inode,要等它关闭。lsof +L1 能看到。
  2. free 显示内存快满了但系统不卡:那是 page cache,可回收,看 available 而不是 free。

六、五种 IO 模型与零拷贝 ​

模型特点例子
阻塞 IO一直等数据就绪传统 read()
非阻塞 IO立即返回,自己轮询O_NONBLOCK
IO 多路复用一个线程监听多个 fdselect / 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 / topload average(1/5/15 分钟),> 核数说明排队
CPU 使用率top、mpstat -P ALL 1%us(用户态)/%sy(内核态)/%wa(IO 等待)
哪个线程在吃 CPUtop -Hp <pid>线程级 CPU,配合 jstack 的 nid
上下文切换vmstat 1(cs)、pidstat -w 1每秒切换次数
内存总量/可用free -h看 available,别只看 free
进程内存ps aux --sort=-rss、smem -tkRSS vs PSS
交换vmstat 1(si/so)、swapon -s持续换入换出=内存不足
缺页pidstat -r 1majflt(主缺页,要读磁盘)
磁盘吞吐/延迟iostat -xz 1%util(饱和)、await(延迟)
谁在读写磁盘iotop -o进程级 IO
磁盘空间df -h、du -sh *分区用量 / 目录占用
被删除仍占用的文件lsof +L1日志没轮转的典型症状
网络连接ss -tanp、ss -s各状态连接数(TIME_WAIT / ESTAB)
网络重传/丢包netstat -s、nstatretrans、drop
抓包tcpdump -i eth0 port 8080 -w x.pcap握手、重传、RST
系统调用strace -p <pid> -T -f卡在哪个 syscall
文件句柄lsof -p <pid>、ulimit -n句柄泄漏、上限
内核日志dmesg -TOOM Killer、驱动报错

八、从现象到命令(排查路径) ​

现象先跑什么常见结论
CPU 100%top → top -Hp → jstack 对 nid死循环 / 频繁 GC / 锁竞争
load 高但 CPU 闲iostat -xz 1磁盘 IO 等待(D 状态进程)
内存持续涨free -h + smem + jmap -histo:live泄漏 / page cache / 堆外内存
进程被 killdmesg -T | grep -i oomOOM 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
epollNIO 的 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 为什么快注册一次 + 就绪回调