这份 操作系统专题 面向后端学习和面试复习,整理操作系统基础、进程线程、进程间通信、锁与同步、内存管理、虚拟内存、零拷贝、I/O 多路复用、文件系统、Linux 和 Shell 相关内容。
适合谁看
- 正在系统学习操作系统基础的后端开发者。
- 准备校招、社招、中大厂操作系统面试题的同学。
- 对进程线程、死锁、内存管理、Linux 命令只会零散背诵的读者。
- 想为 Java 并发、JVM、数据库、网络编程打底的工程师。
这份 操作系统专题 面向后端学习和面试复习,整理操作系统基础、进程线程、进程间通信、锁与同步、内存管理、虚拟内存、零拷贝、I/O 多路复用、文件系统、Linux 和 Shell 相关内容。
写一个保存文件的接口,最直觉的写法是:拿到路径,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 到底是什么:
两个线程同时给同一个计数器加 1,看起来很小的一件事,最后结果却可能少加一次。
原因其实很简单。count++ 在源码里是一行,机器执行时通常要经历读取、计算、写回几个步骤。线程 A 刚读到旧值,还没写回;线程 B 也读到了同一个旧值。两边各自算出新值,最后写回的却是同一个结果。
为了避免这类并发问题,操作系统提供了锁和一系列同步机制。它们要解决的问题不只是一段代码能不能同时执行,还包括线程该不该阻塞、资源数量怎么控制、条件不满足时怎么等待。到了内核里,还要继续考虑中断、抢占、多 CPU、实时性和调度延迟。
这篇文章只讲操作系统视角下的同步机制。Java 里的 synchronized、ReentrantLock、AQS、CAS 和锁优化已经在 Java 锁详解 里展开过,这里不会重复那套内容。本文重点看 mutex、semaphore、condition variable、spinlock、futex 这些概念各自解决什么问题。等理解了这些同步原语,再去看 死锁详解,就更容易看懂“等待关系为什么会绕成环”。
Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。
这篇文章我会简单总结一下 Shell 编程基础知识,带你入门 Shell 编程!
本文示例适用于 bash 4.0+ 版本。不同版本的 bash 在某些特性上可能有差异,特别是:
${var:offset:length} 在较旧版本可能不支持。$((...)):bash 2.0+ 支持。面试里有个很常见的套路:先问你“Kafka 为什么快”“RocketMQ 为什么扛得住堆积”,等你答出顺序写、Page Cache、零拷贝之后,面试官顺势往下挖:“零拷贝具体省掉了哪几次拷贝?”“mmap 和 sendfile 有什么区别?”“splice 又是干嘛的?”
到这一步,很多人就开始打太极了。能背出“零拷贝就是不经过用户态”的不少,能把四次拷贝、两次 DMA、几次上下文切换的账算清楚的不多。
这篇文章就从一次文件发送说起:传统 I/O 会拷几次,零拷贝的“零”到底省在哪,mmap、sendfile、splice 三条路线分别省了什么,又各自要付出什么代价。