这份 操作系统专题 面向后端学习和面试复习,整理操作系统基础、进程线程、进程间通信、锁与同步、内存管理、虚拟内存、零拷贝、I/O 多路复用、文件系统、Linux 和 Shell 相关内容。
适合谁看
- 正在系统学习操作系统基础的后端开发者。
- 准备校招、社招、中大厂操作系统面试题的同学。
- 对进程线程、死锁、内存管理、Linux 命令只会零散背诵的读者。
- 想为 Java 并发、JVM、数据库、网络编程打底的工程师。
这份 操作系统专题 面向后端学习和面试复习,整理操作系统基础、进程线程、进程间通信、锁与同步、内存管理、虚拟内存、零拷贝、I/O 多路复用、文件系统、Linux 和 Shell 相关内容。
线程 A 已经拿到了资源 1,线程 B 也拿到了资源 2。接下来,线程 A 想继续往下走,需要资源 2;线程 B 想继续往下走,又需要资源 1。
两个线程都没抛异常,也不是 CPU 把机器打满了。线上看到的现象可能只是几个请求一直不返回,线程池里的工作线程慢慢被占住。
这类问题麻烦就麻烦在这里:程序没有“算错”,而是卡在一条自己解不开的等待链上。
线程死锁说的就是这种情况:一组线程互相等对方释放资源,等待关系绕成闭环,参与其中的线程都没办法靠自己继续执行。
如果这些线程正好承载订单、支付、库存这类关键流程,外部看到的就不只是某个线程 BLOCKED 了,而是接口超时、队列堆积,甚至进程迟迟退不干净。
写一个保存文件的接口,最直觉的写法是:拿到路径,open 一个文件,把数据 write 进去,最后 close。文件少、并发低、机器不出故障的时候,这套流程看起来没什么难度。
可一到面试追问,问题就来了。open() 返回的 fd 到底指向什么?文件名是存在 inode 里吗?两个硬链接为什么能看到同一份内容?write() 返回成功,数据是不是已经落盘?日志文件删了,为什么 df -h 还是显示磁盘满?
这些问题都绕不开文件系统。它要把路径解析成文件对象,把文件的第 N 个字节定位到底层数据块,还要处理权限、缓存、删除、重命名和崩溃恢复。
写一个 TCP 服务端,最直觉的写法是:主线程 accept 一个连接,就丢给一个新线程去 read、处理、write。连接少的时候这套跑得很好。
可一旦连接数冲到上万,问题就来了。在不少 Linux 发行版里,新线程默认会预留数 MB 的栈空间,常见配置是 8 MB(实际值取决于 ulimit -s、运行库和线程属性)。一万个连接哪怕栈页是按需提交的,预留的地址空间、真正用到的栈页加上线程元数据叠起来也很可观;更要命的是几千上万个线程挤在几个 CPU 核上,光是线程间的上下文切换就把 CPU 啃掉一大半,真正干活的时间所剩无几。更别提大部分连接其实是空闲的——它们各自占着一个线程,却只是在那儿干等数据。
两个进程想交换一段数据,最直觉的想法是:A 进程把数据写到自己的内存里,然后 B 进程直接去读就行了。
不过,这在操作系统里行不通。每个进程都有独立的虚拟地址空间,A 进程里的 0x7f... 地址和 B 进程里的 0x7f... 地址并不是同一块内存。用户态进程之间不能随便互相摸内存,否则权限隔离也没法谈。
所以 IPC(Inter-Process Communication,进程间通信) 绕不开操作系统。
不要想得太复杂,我更习惯把 IPC 看成三件事:怎么传数据、怎么同步控制流、怎么做命名和权限检查。只记“管道、消息队列、共享内存”这些名字,很容易背完就忘。
简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。
通过以下三点可以概括 Linux 到底是什么:
打开一个普通进程的内存信息,你会看到很多看起来反直觉的数字:进程有自己的虚拟地址空间,地址范围可能很大;真正占用的物理内存又是另一回事;同一个动态库还可能被多个进程共享。
程序写代码时只是在访问地址,操作系统看到的却是一堆更具体的问题:这块内存给谁?能不能让别的进程碰?物理内存不够时换谁出去?释放之后留下的空洞还能不能继续用?
这就是内存管理要处理的事。小 G 建议不要一上来就背分页、分段、TLB 这些名词,先抓住一条线:操作系统把程序看到的地址和真实物理内存隔开,再用分配、映射、保护和回收把内存管起来。
很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!
这篇《操作系统常见面试题总结(上)》会先从操作系统基础讲起,再重点梳理 用户态/内核态、系统调用、进程和线程、进程间通信、进程调度、死锁 这些高频考点。它适合用来快速建立面试问题清单,也适合作为复习时查漏补缺的入口。
学习操作系统不只是为了背八股。缓存、调度、同步、内存映射、零拷贝、I/O 多路复用这些思想,在 Redis、Kafka、Nginx、Netty、JVM、数据库里都能看到影子。把底层机制想清楚,再理解上层框架和线上性能问题,会轻松很多。
这篇《操作系统常见面试题总结(下)》承接上篇,重点放在 内存管理、虚拟内存、分页分段、TLB、缺页中断、页面置换、I/O 多路复用、零拷贝、文件系统和磁盘调度。
如果说上篇更偏“进程、线程和并发控制”,这篇更偏“程序运行时到底怎么用内存、怎么做 I/O、怎么和文件系统交互”。这些内容看起来离业务代码比较远,但很多后端问题最终都会落到这里:为什么 mmap 能少一次拷贝?为什么 epoll 能撑住大量连接?为什么同一个虚拟地址在不同进程里互不影响?为什么频繁缺页会让系统变慢?
两个线程同时给同一个计数器加 1,看起来很小的一件事,最后结果却可能少加一次。
原因其实很简单。count++ 在源码里是一行,机器执行时通常要经历读取、计算、写回几个步骤。线程 A 刚读到旧值,还没写回;线程 B 也读到了同一个旧值。两边各自算出新值,最后写回的却是同一个结果。
为了避免这类并发问题,操作系统提供了锁和一系列同步机制。它们要解决的问题不只是一段代码能不能同时执行,还包括线程该不该阻塞、资源数量怎么控制、条件不满足时怎么等待。到了内核里,还要继续考虑中断、抢占、多 CPU、实时性和调度延迟。
这篇文章只讲操作系统视角下的同步机制。Java 里的 synchronized、ReentrantLock、AQS、CAS 和锁优化已经在 Java 锁详解 里展开过,这里不会重复那套内容。本文重点看 mutex、semaphore、condition variable、spinlock、futex 这些概念各自解决什么问题。等理解了这些同步原语,再去看 死锁详解,就更容易看懂“等待关系为什么会绕成环”。