深浅色
操作系统(背诵版)
1. 进程 / 线程 / 协程
- 进程:资源分配单位,独立地址空间,切换要换页表(贵)。
- 线程:CPU 调度单位,共享进程地址空间,切换要陷入内核。
- 协程:用户态调度,只保存少量寄存器,开销极小;Go 的 goroutine 是 M:N 调度。
- 追问:协程切换为什么比线程快?-> 不陷入内核、不换页表、栈小且可增长。
2. 虚拟内存
- 每个进程有独立虚拟地址空间,通过多级页表映射到物理页。
- 缺页中断:访问的页不在内存就从磁盘换入(major),或只是建立映射(minor)。
- 好处:进程隔离、可用空间超过物理内存、共享库、按需加载。
- 追问:TLB 是什么?-> 页表的高速缓存,命中就免去多级页表遍历。
3. 零拷贝
- 传统 read + write:4 次拷贝(磁盘->内核->用户->内核->网卡)+ 4 次上下文切换。
- sendfile:数据不经用户态,2 次拷贝 + 2 次切换;DMA gather 再省一次。
- mmap + write:3 次拷贝,省掉一次用户态拷贝。
- Kafka 用 sendfile,所以吞吐高。
- 追问:零拷贝真的是零吗?-> 指 CPU 拷贝为零,DMA 拷贝仍在。
4. I/O 多路复用
- select:fd 上限 1024,每次全量拷贝和遍历,O(n)。
- poll:用链表去掉上限,仍要遍历。
- epoll:内核维护红黑树 + 就绪链表,O(1) 取就绪事件。
- LT(水平触发):没处理完会重复通知;ET(边缘触发):只通知一次,必须一次读完,否则丢事件。
- 追问:epoll 为什么适合高并发?-> 不用遍历全部连接,只返回就绪的。
5. 用户态 / 内核态
- 系统调用、中断、异常会触发切换;要保存上下文,还可能污染 CPU 缓存。
- 优化思路:批量系统调用(io_uring)、用户态协议栈(DPDK)、减少数据拷贝。
- 追问:一次 read 有几次状态切换?-> 2 次(进入内核、返回用户态)。
6. 锁与 CAS
- 互斥锁:阻塞等待,适合临界区较长的场景。
- 自旋锁:忙等不睡眠,适合临界区极短且多核的场景。
- CAS 是无锁编程基础,但有 ABA 问题(加版本号解决)和自旋开销。
- 追问:CAS 为什么需要内存屏障?-> 保证可见性、禁止指令重排。
7. CPU 缓存与伪共享
- 缓存行通常 64 字节;两个线程改同一缓存行里的不同变量会互相失效,这就是伪共享。
- 解决:padding 补齐到缓存行(Go 里可以用占位数组)。
- 追问:为什么数组遍历比链表快?-> 连续内存,硬件预取有效,缓存命中率高。
8. page cache 与 fsync
- 写文件先进 page cache,内核异步刷盘,所以 write 返回不代表落盘。
- fsync 强制刷盘并等元数据;fdatasync 只保证数据。
- 数据库靠 WAL + fsync 保证持久性;Redis appendfsync everysec 是折中。
- 追问:断电会丢什么?-> page cache 里没刷盘的写入会丢,所以关键数据必须 fsync。