面试追问地图

主问题必讲关键点下一层追问
进程/线程/协程资源、调度、切换成本用户线程与内核线程模型
上下文切换寄存器、栈、页表、缓存为什么线程切换通常更轻
IPC管道、消息队列、共享内存、Socket性能、同步和跨机器能力
调度算法吞吐、响应、公平时间片过大/过小会怎样
死锁互斥、占有等待、不可剥夺、循环等待检测、预防、银行家算法
虚拟内存页表、TLB、缺页多级页表为什么省内存、TLB miss 代价、大页与 THP 的坑
mallocbrk/mmap、分配器内存碎片、为何 RSS 不下降
page cache读写缓存、脏页回写、可回收dirty 参数、direct IO 场景、容器 OOMKilled
页面置换LRU、Clock 等抖动、工作集
IO 模型阻塞、非阻塞、复用、异步同步/异步与阻塞/非阻塞区别
epoll红黑树、就绪队列、回调ET/LT、惊群、必须非阻塞
零拷贝减少复制和上下文切换mmap/sendfile、适用限制
内存管理堆栈区别、写时复制、内存不足brk/mmap 选择、OOM 场景
并行 vs 并发逻辑并发 vs 物理并行多核利用率、并发模型
孤儿/僵尸进程父进程退出、未回收 PCBSIGCHLD 信号、双 fork 技巧
锁类型互斥锁、自旋锁、读写锁、RCU临界区长短与锁选择、写者饥饿
管程Monitor、条件变量、wait/signalJava synchronized 的管程模型
PV 操作信号量、P/V 原子语义生产者-消费者、读者-写者
文件系统inode、物理结构、目录结构硬链接 vs 软链接、按名存取流程
磁盘调度SSTF、SCAN、C-SCANSSD 时代的意义
缓冲区溢出栈溢出、返回地址覆盖栈金丝雀、DEP、ASLR

操作系统题要从“资源归谁管理”和“谁负责等待/唤醒”两个角度回答。


一、操作系统

用户态和内核态

  • 内核态 :可执行所有指令,访问所有硬件资源
  • 用户态 :只能执行部分指令,不能直接访问硬件
  • 划分原因:安全性、稳定性、隔离性

进程、线程、协程区别

维度进程线程协程
资源分配资源分配基本单位共享进程资源共享进程资源
调度非调度单位CPU 调度基本单位用户态调度
内存独立地址空间共享堆/全局变量共享堆
切换开销大(切换页表等)小(只切换栈/寄存器)极小(用户态)
稳定性崩溃不影响其他进程崩溃可能导致进程崩溃崩溃影响所在进程

进程五状态

创建 -> 就绪 -> 运行 -> (阻塞/结束)

  • 运行 -> 就绪:时间片用完
  • 运行 -> 阻塞:等待 I/O
  • 阻塞 -> 就绪:I/O 完成

进程间通信方式

  • 管道 :匿名管道(父子进程,单向)、命名管道(无关进程)
  • 消息队列 :内核中的消息链表,支持自定义数据类型
  • 共享内存 :最快的 IPC,直接映射同一块物理内存
  • 信号 :异步通信(如 SIGKILL、SIGSTOP 不可忽略)
  • 信号量 :P/V 操作,保护共享资源实现互斥/同步
  • Socket :不同主机间通信

线程间同步方式

  • 互斥锁 :加锁后其他线程阻塞等待
  • 读写锁 :读共享、写独占,适合读多写少
  • 自旋锁 :加锁失败忙等待(CAS 原子指令),适合临界区短
  • 条件变量 :等待/唤醒机制,与互斥锁配合使用
  • 信号量 :计数器控制资源访问次数

进程调度算法

  • FCFS :先来先服务,非抢占
  • SJF :短作业优先
  • HRRN :高响应比优先,兼顾长短作业
  • RR :时间片轮转(20ms~50ms)
  • HPF :最高优先级(静态/动态)
  • 多级反馈队列 :多队列 + 优先级 + 时间片,最实用

死锁四个必要条件

  • 互斥 :不能同时使用同一资源
  • 持有并等待 :持有资源的同时等待其他资源
  • 不可剥夺 :资源不能被强制抢占
  • 环路等待 :获取资源顺序构成环形链

避免方法 :资源有序分配法,破坏环路等待条件。

Java 层的死锁代码示例与 jstack 排查见并发编程

银行家算法

分配资源前预执行,检查是否存在安全序列。核心:不断检查剩余资源能否满足某进程的最大需求,能则加入安全序列并回收其资源,直到所有进程都能完成。

安全状态一定不会死锁,不安全状态不一定死锁

虚拟内存与物理内存

  • 虚拟地址通过 页表 映射到物理地址(MMU 硬件完成)
  • 虚拟地址 = 页号 + 页内偏移
  • Linux 页大小 4KB
  • 好处:进程内存可超过物理内存、进程间地址隔离、内存访问更安全

通用概念:虚拟地址→页→物理帧,是典型的间接层——同一模式见一致性哈希的虚拟节点、Redis 的哈希槽、Kafka 的分区。代价是每次访问多一次查表,所以必须有 TLB。

多级页表为什么省内存?TLB miss 有多贵?

频次 ★★★ · 难度 🟡

是什么:页表存”虚拟页号 → 物理页帧号”的映射。如果用单级大数组,32 位地址空间就要 2^20 项 × 4B = 4MB/进程,且必须一次性连续分配(数组按下标寻址,不能有洞),64 位下更是天文数字。多级页表把它拆成树——x86-64 用四级(PGD → PUD → PMD → PTE),48 位虚拟地址按 9+9+9+9+12 拆分,前四段逐级做目录索引,最后 12 位是 4KB 页内偏移:

47        39 38        30 29        21 20        12 11          0
+-----------+------------+------------+------------+-------------+
| PGD 索引 9 | PUD 索引 9 | PMD 索引 9 | PTE 索引 9 | 页内偏移 12 |
+-----------+------------+------------+------------+-------------+

为什么省内存:省的关键不是”表拆小了”,而是没用到的子树整棵不分配。进程通常只用到地址空间的极小部分,上级目录项为空时,其下辖的整段地址范围(一个 PGD 项管 512GB)就一张表都不用建,只有实际 mmap/brk 出来的区域才逐级建表。代价是一次地址翻译要串行访问 4 次内存(逐级 walk),比单级的 1 次更慢——这就是 TLB 存在的理由。

TLB miss 的代价:TLB(Translation Lookaside Buffer)是 MMU 内缓存”虚拟页 → 物理页”翻译结果的硬件缓存,命中约 1 个时钟周期;miss 则由硬件走四级 page walk,最坏 4 次额外内存访问(每次约百纳秒),一次访存变五次。TLB 只有几百到上千条目,按 4KB 页只能覆盖几 MB 热点——大内存随机访问的程序(数据库缓冲池、JVM 大堆)TLB miss 会成为隐形瓶颈。进程切换还会让 TLB 失效(现代 CPU 用 PCID/ASID 给条目打进程标签缓解),这是”进程切换比线程切换贵”的微观原因之一(见上文进程/线程对比表)。

大页(HugePage)怎么救:把页从 4KB 提到 2MB(或 1GB),一条 TLB 条目覆盖的内存扩大 512 倍,page walk 也少一级(2MB 页在 PMD 级直接命中):

维度4KB 普通页静态 HugePage(2MB)透明大页 THP
TLB 覆盖条目数 × 4KB×512×512
分配方式缺页时按需分配启动时预留,应用显式申请内核后台线程(khugepaged)自动合并
典型用户默认MySQL/Oracle 缓冲池、JVM -XX:+UseLargePages多数发行版默认开启,但 Redis/MongoDB 建议关闭
大内存下 TLB miss 多预留后普通进程用不了,需容量规划后台合并/拆分引入延迟毛刺;fork 后 COW 按 2MB 整页复制

数据库和 JVM 的堆是”大而稳定”的内存,最吃大页红利;而 Redis 官方要求关闭 THP:RDB/AOF 重写靠 fork + 写时复制共享内存(见Redis持久化一节与下文”写时复制”),开 THP 后子进程存续期间父进程改 1 字节就要复制整个 2MB 页,内存暴涨且写延迟抖动。

常见追问

  • 为什么不全用 1GB 大页?→ 内存管理粒度变粗:碎片浪费、COW/缺页的复制成本按页放大、大页不能 swap;只适合缓冲池/堆这类大而稳定的区域
  • 缺页中断和 TLB miss 是一回事吗?→ 不是。TLB miss 由硬件 walk 页表解决,不进内核;walk 到无效页表项才触发缺页中断进内核(分配物理页/从 swap 读回)
  • 怎么观察 TLB 和大页的效果?→ perf stat -e dTLB-load-misses 看 miss 率;/proc/meminfo 的 HugePages_* 看静态大页;/sys/kernel/mm/transparent_hugepage/enabled 看 THP 开关

通用概念:多级页表是稀疏数据的树形按需分配——不为”可能存在但没用到”的空间付费,同类有 radix tree、稀疏文件;TLB 是给慢路径加结果缓存,与 CPU cache、MySQL Buffer Pool 同一层套路,共同点是靠局部性把平均成本压到接近命中成本。

程序内存布局(低到高)

代码段 -> 数据段(已初始化) -> BSS 段(未初始化) -> 堆(向上增长) -> 文件映射段 -> 栈(8MB,向下增长)

堆和栈区别

维度
分配方式动态分配(malloc/mmap)编译器自动分配
管理手动释放,可能泄漏自动释放(函数返回)
大小较大固定(一般 8MB)
速度较慢

写时复制(Copy On Write)

fork 时子进程只复制页表不复制物理内存,父子进程共享物理页(标记只读)。当任一方写操作时,触发写保护中断,内核复制物理页并设置可读写权限。

节省物理内存 ,避免 fork 时大量物理内存复制导致的长时间阻塞。

通用概念:写时复制把读路径的锁全部省掉,代价转嫁给写路径——同一模式见 CopyOnWriteArrayList、Redis 的 RDB fork。

malloc brk vs mmap

  • < 128KB :brk(),堆顶指针向上移动
  • >= 128KB :mmap(),文件映射区分配

内存不足时会发生什么

  1. 后台回收(kswapd,异步)-> 2. 直接回收(同步,阻塞进程)-> 3. OOM Killer(杀死占用内存最高的进程)

可回收的内存 :文件页(干净页直接释放,脏页写回磁盘)、匿名页(通过 Swap 机制)

page cache 与 OOM killer:容器里的 JVM 为什么会无声被杀?

频次 ★★★ · 难度 🟡

是什么:page cache 是内核拿空闲内存缓存文件页的机制——读文件先查缓存命中,写文件默认先写缓存并标”脏”、再异步回写磁盘。free 里 buff/cache 很高不代表内存紧张,available 才是真实可用量(cache 大部分可随时回收让位)。

脏页回写参数vm.*,写延迟毛刺的来源):

参数默认含义
dirty_background_ratio10%脏页占比超过此值,后台 flusher 线程开始异步回写
dirty_ratio20%超过此值,正在写的进程被同步阻塞去回写——写延迟突刺就是它
dirty_expire_centisecs30s脏页最长驻留时间,到期必回写

大量写文件的服务(日志聚合、数据导出)要调低 background 值让回写更早更平滑,或应用层定期 fsync 控节奏——别攒到 dirty_ratio 触发同步阻塞。

direct IO 什么时候用O_DIRECT 绕过 page cache 直达磁盘。适用于自己管缓存的系统:MySQL 的 Buffer Pool 配 innodb_flush_method=O_DIRECT,避免”数据在 Buffer Pool 一份、page cache 又一份”的双重缓存浪费(见MySQL Buffer Pool 一节)。反例是 Kafka/RocketMQ——不自建缓存,重度依赖 page cache 做读写加速(见消息队列存储两节),顺序访问模式与内核预读天然契合。

cgroup 内存限制与容器里 JVM 被杀:容器的内存上限是 cgroup limit,statistics 计的是 RSS + cgroup 记账的 page cache;超限触发 cgroup OOM,直接 SIGKILL 组内评分最高的进程(通常就是 JVM)。表现为容器退出码 137、K8s 事件 OOMKilled,而 JVM 日志里没有任何异常——这不是 Java 的 OutOfMemoryError,是内核层面的击杀。排查链条:

  • JVM 总内存 = 堆 + 元空间 + 线程栈 + 直接内存 + CodeCache,-Xmx 只限住第一项(各块构成见Linux与工程化”容器内存与 JVM 堆”)
  • 老 JVM(8u191 前)看不见 cgroup 限制,按宿主机内存算默认堆大小,几乎必被杀;现代 JVM 用 UseContainerSupport(默认开)+ MaxRAMPercentage 按容器限额算
  • 两种 “OOM” 要分清:Java OOM(堆满,抛异常、可留 dump)vs cgroup OOM kill(进程总量超限,无声消失,去 dmesg / K8s Events 找)

常见追问

  • 容器里 free 为什么显示的是宿主机内存?→ /proc/meminfo 没有 namespace 化;要看 cgroup 接口文件(memory.limit_in_bytesmemory.stat),JVM 的容器感知读的也是它们
  • 退出码 137 怎么排查?→ 137 = 128 + 9(SIGKILL);先 dmesg | grep -i oom 或 K8s describe 确认 OOMKilled,再核算 JVM 各内存块之和与 limit 的差距,常见元凶是直接内存(Netty)和线程数
  • page cache 会把容器撑到 OOM 吗?→ cgroup 会先回收本组内可回收的 cache 再杀进程,正常不会;但 mmap 脏页回写不及时、tmpfs/shmem(不可回收)会成为例外

通用概念:page cache 体现”空闲资源不用白不用,但必须可随时归还”——同类有 JVM 堆的软引用缓存(见JVM四种引用);OOM killer 则是资源耗尽时的最后防线:宁可牺牲单个进程也不让整机挂死,与 Redis 内存淘汰(见Redis)、线程池拒绝策略(见并发编程)同属一层兜底设计。

页面置换算法

  • OPT :最优置换(理论,无法实现)
  • FIFO :先进先出
  • LRU :最近最久未使用(近似最优,开销大)
  • Clock :时钟算法(LRU 近似 + FIFO 改进,实用)
  • LFU :最不常用(考虑访问频率)

中断

CPU 暂停当前任务,处理事件后恢复执行。

  • 外部中断 :硬件产生。可屏蔽(INTR),不可屏蔽(NMI)
  • 内部中断 :软中断(系统调用)、异常(陷阱/故障/终止)
  • 系统调用:32 位 Linux 用 int 0x80,64 位用 syscall

I/O 模型

  • 阻塞 I/O :等待 I/O 完成才返回
  • 非阻塞 I/O :立即返回,轮询检查
  • I/O 多路复用 :select/poll/epoll,一个线程监听多个 I/O
  • 信号驱动 I/O :I/O 完成时发信号通知
  • 异步 I/O :内核完成 I/O 后通知应用

select/poll/epoll 区别

维度selectpollepoll
数据结构固定位图动态数组红黑树 + 就绪链表
文件描述符限制1024系统最大 fd
时间复杂度O(n) 遍历O(n) 遍历O(1) 事件驱动
用户/内核拷贝2 次2 次只需传入关注的 fd

epoll ET vs LT

  • LT(水平触发) :满足条件就通知,直到数据读完
  • ET(边缘触发) :只在状态变化时通知一次,需一次读完数据,一般搭配非阻塞 I/O

零拷贝

频次 ★★★★ · 难度 🟡 · 高频:阿里/字节/美团

零拷贝的目标:减少数据传输过程中的 CPU 拷贝次数和上下文切换次数。

传统 I/O(read + write):

HDD/SSD → DMA → 内核缓冲区 → CPU → 用户缓冲区 → CPU → Socket 缓冲区 → DMA → 网卡

4 次上下文切换(read/write 各 2 次),2 次 DMA 拷贝 + 2 次 CPU 拷贝,共 4 次拷贝。

零拷贝各方案对比:

方案系统调用上下文切换CPU 拷贝DMA 拷贝说明
传统 read + writeread() + write()4224 次拷贝,无优化
mmap + writemmap() + write()412用户态与内核态共享页缓存,省 1 次 CPU 拷贝
sendfilesendfile()20 (DMA sg 支持)2完全零拷贝,文件→Socket 最优方案
splicesplice()202两个 fd 间移动,不限于文件→Socket

关键细节:

  • mmap + write:将文件映射到内核地址空间,用户态直接读写页缓存,减少用户态↔内核态的数据拷贝;但存在页缓存与映射文件一致性问题、多线程并发写入可能触发 SIGBUS
  • sendfile(Linux 2.6.33+ 配合 DMA scatter-gather 实现真正零拷贝):数据从磁盘→页缓存→网卡,全程不需要 CPU 搬移
  • splice(Linux 2.6.17+):在两个文件描述符之间建立管道传输,不经用户态,适合代理/网关层转发数据

典型应用:

  • Kafka:生产者写入用 mmap(CommitLog 索引),消费者读取用 sendfile(日志传输),见消息队列
  • Nginx:静态文件用 sendfile(sendfile on;
  • Netty:FileRegion 封装 sendfile 零拷贝传输,见Netty与RPC

零拷贝是 Kafka 高吞吐、Netty 文件传输的底层支撑,应用侧见消息队列Netty与RPC

Linux 文件系统(VFS 与 inode)

频次 ★★★ · 难度 🟡 · 高频:阿里/字节

VFS(虚拟文件系统):

  • Linux 通过 VFS 层统一抽象所有文件系统(ext4、XFS、tmpfs、procfs 等)
  • 系统调用(open/read/write)→ VFS → 具体文件系统 → 物理存储
  • VFS 定义通用接口:超级块(super_block)、inode、dentry、file

inode(索引节点):

  • 存储文件元数据(权限、大小、时间戳、数据块指针),不包含文件名
  • 每个文件(目录也是一种文件)有唯一 inode 号
  • inode 数量有限(df -i 查看),小文件过多会耗尽 inode 导致”磁盘有空间但写不进”

硬链接 vs 软链接:

维度硬链接软链接(符号链接)
inode与原文件共享同一个 inode,本质是增加目录项新建文件,存目标路径字符串(自己的 inode)
依赖不依赖原文件名,原文件删除仍可访问原文件删除后断链
范围不能跨文件系统、不能链目录可跨文件系统、可链目录
命令ln target link_nameln -s target link_name

常见追问:

  • 为什么硬链接不能跨文件系统?→ inode 号只在同一个文件系统内唯一,不同文件系统的 inode 1 代表不同文件
  • 删除文件时底层做了什么?→ 减少 inode 的链接计数(i_link),计数为 0 且没有进程打开时才释放数据块——所以大文件被程序开着时,rm 后磁盘空间不立即释放,要等进程关闭
  • 文件描述符(fd)和 inode 关系?→ 进程的 fd 指向内核的 file 结构体,file 结构体指向 dentry,dentry 指向 inode;fd 是用户态看到的整数索引,file 是内核态维护的文件打开状态(偏移量、flag),inode 是持久化的文件元数据

二、进程与线程补充

并行和并发有什么区别?

是什么:并发是逻辑上同时发生(一个 CPU 快速切换,交替执行多个任务),并行是物理上同时发生(多个 CPU 核心真正同时执行)。并发是结构,并行是执行

维度并发并行
本质任务交替执行,宏观”同时”任务真正同时执行
硬件单核也能实现需要多核
关注点任务调度和协作执行效率提升
示例单核 CPU 处理多个网络连接多核 CPU 各跑一个线程

常见追问:Go 的 goroutine 是并发还是并行?→ 并发模型(goroutine 是逻辑上的并发体),但可以在多核上并行执行(GOMAXPROCS 控制并行度)。

什么是孤儿进程和僵尸进程?如何处理?

频次 ★★★ · 难度 🟡

是什么(先区分两种”异常”进程):

  • 孤儿进程:父进程先于子进程退出,子进程变”孤儿”,被 init(PID=1)进程收养。孤儿进程不是问题——init 会在子进程退出时调用 wait 回收它。
  • 僵尸进程:子进程已退出,但父进程没有调用 wait/waitpid 回收其退出状态,子进程的 PCB 仍残留在进程表中(状态为 Z)。僵尸进程是问题——占用 PID 和少量内核资源,大量僵尸会耗尽 PID 号段。

处理方法

  • 父进程尽早调用 wait/waitpid(同步阻塞等待)或注册 SIGCHLD 信号处理函数异步回收
  • 杀死父进程,僵尸进程被 init 收养并回收
  • 编程中推荐用双 fork 技巧:父进程 fork 出子进程后立即退出,让子进程被 init 收养,子进程退出时 init 自动回收

常见追问SIGCHLD 信号是什么?→ 子进程状态改变时(退出、暂停、继续),内核向父进程发送 SIGCHLD 信号。父进程可注册信号处理函数,在其中调用 waitpid 非阻塞回收。

进程的控制块 PCB 包含哪些信息?

是什么:PCB(Process Control Block)是内核为每个进程维护的数据结构,是进程的”身份证”和”档案”。

包含信息

  • 进程标识:PID、PPID(父进程 ID)
  • 处理器状态:通用寄存器、PC、PSW、栈指针
  • 进程调度信息:优先级、状态、时间片
  • 内存管理信息:页表基址寄存器、内存界限
  • 文件管理信息:打开的文件描述符表
  • 记账信息:CPU 时间、资源使用量

Linux 实现task_struct(定义在 include/linux/sched.h),包含上述所有字段,通过链表/红黑树组合组织。

操作系统的信号机制是什么?有什么作用?

是什么:信号是 OS 提供的一种异步通知机制——一个进程可以给另一个进程发送一个信号编号,接收方在合适时机响应。信号本质是软中断。

常见信号SIGKILL(9,强制杀死,不可忽略/捕获)、SIGTERM(15,优雅终止,可捕获做清理)、SIGINT(2,Ctrl+C)、SIGCHLD(17,子进程状态改变)、SIGSEGV(11,段错误)、SIGUSR1/2(用户自定义)。

信号处理方式:忽略(大多数信号);捕获(注册信号处理函数);默认(内核预定义动作,如终止进程)。信号处理函数中应只做异步信号安全的操作(如设标志位、写管道),不能在信号处理函数中调用 malloc/printf(不可重入)。


三、同步机制深入

介绍一下几种典型的锁?

频次 ★★★ · 难度 🟡

是什么:锁是并发编程中最基本的同步原语,不同锁在”拿不到锁时怎么办”上有不同的选择:

锁类型拿不到锁时的行为特点适用场景
互斥锁(mutex)阻塞睡眠,等锁释放后被唤醒睡眠/唤醒有上下文切换开销临界区较长(> 几百 ns)
自旋锁(spinlock)忙等待(while 循环 CAS),不释放 CPU无上下文切换,但空转耗 CPU临界区极短(几 ns~几十 ns)
读写锁(rwlock)读共享、写独占,写锁等待时,新读锁也需等待适合读多写少配置文件、缓存更新
RCU读完全无锁,写时复制新版本再原子替换指针读端零开销内核中路由表、网络数据包处理

互斥锁 vs 自旋锁的选择:临界区在几个指令周期内 → 自旋锁(避免睡眠/唤醒开销);临界区可能需要等待较久 → 互斥锁(不空转浪费 CPU)。Java 的 synchronized 升级链路(偏向锁→轻量锁→重量锁)本质就是”自旋→互斥”的自动切换,见并发编程

常见追问:读写锁的”写者饥饿”问题?→ 如果读锁一直不断,写锁就永远拿不到。改进方案:公平读写锁(写者排队时新读者也排队等待),或 RCU(读完全无锁,适合读多写极少的场景)。

PV 操作是什么?在进程同步中如何应用?

频次 ★★ · 难度 🟡

是什么:PV 操作是信号量(Semaphore)的两种原子操作。信号量是一个计数器,初值表示可用资源数。

  • P 操作(Proberen,荷兰语”尝试”)semaphore--,若减后 < 0,进程阻塞等待
  • V 操作(Verhogen,荷兰语”增加”)semaphore++,若加后 ≤ 0,唤醒一个等待进程

经典应用

  • 互斥:Semaphore mutex = 1;P(mutex) → 进入临界区 → V(mutex),实现互斥锁
  • 同步(生产者-消费者):empty = N(缓冲区空位),full = 0(已填充数)。生产者 P(empty) → 放入 → V(full);消费者 P(full) → 取出 → V(empty)
  • 读者-写者问题:用信号量控制读写互斥、写写互斥、读读允许

常见追问:P 和 V 操作为什么要原子?→ 非原子时,两个进程可能同时读到相同的 semaphore 值,都认为”还有资源”而同时进入临界区。硬件上通过关中断、TSL 指令或 CAS 原子指令实现。

管程是什么?在操作系统中有什么作用?

频次 ★★ · 难度 🟡

是什么:管程(Monitor)是一种高级同步机制,把共享数据和对共享数据的操作封装在一起,同一时刻只允许一个进程/线程在管程内执行。管程由编译器保证互斥,程序员不需要手动 P/V 操作。

组成:共享变量 + 条件变量 + 对共享变量操作的方法。

条件变量:当管程内的进程需要等待某个条件成立时,调用 wait() 释放管程并阻塞;当条件满足时,另一个进程调用 signal() 唤醒等待者。被唤醒的进程在重新获取管程锁后才能继续执行。

Java 的实现synchronized 关键字 + wait/notify/notifyAll 就是管程模型。每个 Java 对象关联一个 Monitor,synchronized 方法/块获取的就是 Monitor 锁,wait/notify 操作的是 Monitor 的条件变量。详见并发编程


四、调度算法补充

多级反馈队列调度算法是如何工作的?

频次 ★★ · 难度 🟡

是什么:多级反馈队列(Multilevel Feedback Queue)是最实用的通用调度算法,Windows 和 Linux 2.6 之前都采用它。核心思想:刚创建的进程放最高优先级,随着执行时间增加逐级降级:

队列 1(优先级最高,时间片 8ms)→ 用完降到队列 2
队列 2(优先级中,时间片 16ms)→ 用完降到队列 3
...
队列 N(优先级最低,时间片最长,FCFS)

三条规则:高优先级队列非空时不调度低优先级;进入的进程进最高优先级;时间片用完降级,IO 阻塞返回后保持或提升优先级。

为什么好用:自动区分”IO 密集型”(短时间片就阻塞,一直留在高优先级)和”CPU 密集型”(用完时间片,逐渐降到低优先级),无需事先知道进程类型。

作业调度和进程调度有什么区别?

维度作业调度(高级调度)进程调度(低级调度)
频率低频(几秒~几分钟一次)高频(几十 ms 一次)
对象作业(从外存加载到内存)就绪进程(分配 CPU)
作用控制内存中并发度决定哪个进程运行
现代系统分时/实时系统基本无此概念核心调度

五、文件系统

文件的逻辑结构和物理结构是什么?

是什么:逻辑结构是用户看到的文件组织形式(字节流 or 记录流),物理结构是文件在磁盘上的实际存储方式。

物理结构(文件在磁盘上怎么放):

  • 连续分配:文件占一连续磁盘块。顺序读写快,但外部碎片多、扩展难
  • 链接分配:每个块末尾存下一块地址。无外部碎片,但随机访问慢、一个块损坏导致后续丢失
  • 索引分配(Unix/Linux inode):用索引块集中存所有数据块指针,兼具随机访问和无碎片。多级索引:inode 有 12 个直接指针 + 1 个一级间接 + 1 个二级间接 + 1 个三级间接,小文件直接指针够用,大文件动态扩展级数

文件按名存取是如何实现的?

是什么:用户通过文件名访问文件,内核通过目录项将文件名映射到 inode。

实现流程

  1. 目录文件存(文件名 → inode 号)的映射
  2. 根据路径逐级查找目录项,找到目标文件的 inode 号
  3. inode 里存文件属性(所有者、权限、大小)和数据块指针
  4. 结合进程的打开文件表(fd → 内核文件表 → inode),完成读写

常见追问:硬链接和软链接(符号链接)的区别?→ 硬链接是多个目录项指向同一个 inode(不能跨文件系统、不能链目录);软链接是存目标路径的独立文件(可跨文件系统、可链目录,但目标删了链接就变”悬空链接”)。

文件的目录结构有哪些常见类型?如何实现文件共享?

目录结构:单级目录(全系统一个表,不支持重名)→ 两级目录(主目录 + 用户目录,可重名但无分组)→ 树形目录(多级,Linux/Windows 现行方式,可分组、可层层嵌套)→ 无环图目录(允许文件被多个目录共享,需引用计数避免删除正在被共享的文件)。

文件共享方式:硬链接(同一文件系统,共享 inode)、软链接(独立文件存路径,灵活但脆弱)、不同文件系统间需网络文件系统(NFS/SMB)。


六、设备管理与磁盘

磁盘调度算法有哪些?各有什么特点?

频次 ★★ · 难度 🟡

是什么:磁盘读写最耗时的是寻道(磁头移动到目标柱面),磁盘调度算法通过重新排序请求队列来减少平均寻道时间。

算法原理优点缺点
FCFS按请求顺序处理公平寻道距离长
SSTF(最短寻道)每次选离当前磁头最近的请求吞吐高可能饿死远端请求
SCAN(电梯算法)磁头从一端走到另一端,沿途处理请求避免饥饿两端的请求等得久
C-SCAN单向扫描,到头后快速回起点(不处理回程请求)各位置等待时间均匀回程空转
LOOK/C-LOOKSCAN 的改进版,不到头就反向(走到最远请求处就回头)实际使用

现代系统:机械硬盘(HDD)时代磁盘调度算法很重要;SSD 无机械寻道,算法收益大幅下降,主要靠 NCQ(Native Command Queueing)做批量优化。

什么是 SPOOLING 技术?在操作系统中有什么作用?

是什么:SPOOLING(Simultaneous Peripheral Operations On-Line)是假脱机技术——把低速 I/O 设备(如打印机)的独占访问变成共享访问。数据先写入磁盘缓冲区(输入井/输出井),由独立的后台进程(SPOOLING 守护进程)按队列顺序把数据从缓冲区送到物理设备。

为什么需要:打印机是独占设备,若一个进程直接占用打印机,其他进程必须等待——SPOOLING 让每个进程”以为”自己有独占打印机,实际是写到磁盘的输出井,后台逐份打印。

常见追问:打印缓冲区和 SPOOLING 是一回事吗?→ 缓冲区是内存中的临时存储,SPOOLING 是系统级的设备虚拟化,用磁盘做中间存储,缓冲只是其一部分。

什么是缓冲区溢出攻击?如何防范?

频率 ★★ · 难度 🟡

是什么:程序向栈/堆上的缓冲区写入超过其容量的数据,覆盖相邻内存(如函数的返回地址),攻击者精心构造数据使程序跳转到恶意代码。

典型场景:C 语言 gets() 不检查长度,输入超过缓冲区大小,覆盖栈上的返回地址,程序 ret 时跳转到攻击者注入的 shellcode。

防范措施

  • Stack Canary(栈金丝雀):函数入口在栈上返回地址和局部变量之间放一个随机值,函数返回前检查该值是否被改动。GCC 默认开启 -fstack-protector
  • DEP(数据执行保护)/NX bit:标记栈和堆所在内存页为不可执行,即使注入 shellcode 也无法执行
  • ASLR(地址空间布局随机化):每次加载程序时随机化栈、堆、库的基地址,攻击者猜不到 shellcode 的跳转目标地址
  • 使用安全函数fgets 代替 getsstrncpy 代替 strcpysnprintf 代替 sprintf