操作系统面试题¶
40 道操作系统高频面试题,涵盖进程与线程、内存管理、 IO 模型、 Linux 命令等核心知识点
一、进程与线程( 12 题)¶
题目 1 :进程和线程的区别是什么?¶
| 特性 | 进程( Process ) | 线程( Thread ) |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立的地址空间 | 共享进程的地址空间 |
| 资源 | 拥有独立资源(文件描述符、内存等) | 共享进程资源 |
| 创建开销 | 大(需要分配独立资源) | 小(共享进程资源) |
| 切换开销 | 大(需要切换页表等) | 小(只需切换寄存器等) |
| 通信方式 | IPC (管道、消息队列、共享内存等) | 直接读写共享变量 |
| 崩溃影响 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 安全性 | 更安全(隔离性好) | 需要同步机制(锁) |
面试追问:进程间共享的资源有哪些? 进程间默认不共享资源,但可以通过 IPC 机制共享:共享内存、管道、消息队列、信号量等。子进程 fork 后会复制父进程的资源(写时复制 COW )。
题目 2 :什么是协程?和线程有什么区别?¶
协程( Coroutine ):是一种用户态的轻量级线程,由程序员/运行时控制调度,而非操作系统。
| 特性 | 线程 | 协程 |
|---|---|---|
| 调度 | 操作系统调度(抢占式) | 用户态调度(协作式) |
| 切换 | 内核态切换,开销大(~1-10μs ) | 用户态切换,开销极小(~100ns ) |
| 内存 | 默认栈 1-8MB | 几 KB (可动态增长) |
| 数量 | 受限(通常数千个) | 可创建数十万个 |
| 同步 | 需要锁 | 单线程内无需锁 |
# Python协程示例
import asyncio
async def fetch_data(url): # async def定义异步函数;用await调用
print(f"开始请求 {url}")
await asyncio.sleep(1) # 模拟IO操作,让出执行权
print(f"请求完成 {url}")
return f"data from {url}"
async def main():
# 并发执行多个协程
tasks = [
fetch_data("url1"),
fetch_data("url2"),
fetch_data("url3"),
]
results = await asyncio.gather(*tasks) # await等待异步操作完成
print(results)
asyncio.run(main()) # asyncio.run()启动异步事件循环
面试追问: Go 的 goroutine 属于协程吗? Go 的 goroutine 是一种特殊的协程实现。它使用 M:N 调度模型( M 个 goroutine 映射到 N 个 OS 线程),由 Go 运行时(而非 OS )调度。 goroutine 具有协作式和抢占式混合调度的特点( Go 1.14 起支持基于信号的抢占)。
题目 3 :进程间通信( IPC )的方式有哪些?¶
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 管道( Pipe ) | 单向、字节流、父子进程间 | 简单的父子进程通信 |
| 命名管道( FIFO ) | 单向或双向、有名字、任意进程间 | 无亲缘关系的进程通信 |
| 消息队列 | 有格式的消息、异步 | 需要消息类型区分的场景 |
| 共享内存 | 最快、需要同步机制 | 大量数据的高速交换 |
| 信号量( Semaphore ) | 同步/互斥 | 控制对共享资源的访问 |
| 信号( Signal ) | 异步通知 | 进程控制( SIGKILL 、 SIGTERM ) |
| Socket | 可跨网络、双向 | 网络通信,也可用于本地( Unix Socket ) |
// 共享内存示例(POSIX)
#include <sys/mman.h>
#include <fcntl.h>
// 进程A:创建和写入
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
char *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
sprintf(ptr, "Hello from Process A");
// 进程B:读取
int fd = shm_open("/my_shm", O_RDONLY, 0666);
char *ptr = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
printf("Received: %s\n", ptr); // "Hello from Process A"
面试追问:哪种 IPC 方式最快?为什么? 共享内存最快。因为数据直接写入一块共享的内存区域,不需要数据在用户态和内核态之间拷贝。但需要使用信号量或互斥锁来同步访问。
题目 4 :什么是死锁?产生的条件和解决方法?¶
死锁:两个或多个进程互相持有对方需要的资源并等待,导致所有进程无法继续执行。
产生死锁的四个必要条件: 1. 互斥条件:资源同一时刻只能被一个进程使用 2. 请求与保持:进程持有资源的同时请求新资源 3. 不可剥夺:已分配的资源不能被强制收回 4. 循环等待:多个进程形成资源等待的循环链
预防死锁(破坏必要条件): - 破坏请求与保持:一次性申请所有资源 - 破坏不可剥夺:得不到新资源时释放已占有的资源 - 破坏循环等待:按固定顺序申请资源
避免死锁: - 银行家算法:每次分配前检查是否会导致不安全状态
检测与恢复: - 检测:资源分配图是否有环 - 恢复:终止死锁进程 / 剥夺资源
# 死锁示例
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread_1():
lock_a.acquire() # 占有A
lock_b.acquire() # 等待B ← 死锁!
lock_b.release()
lock_a.release()
def thread_2():
lock_b.acquire() # 占有B
lock_a.acquire() # 等待A ← 死锁!
lock_a.release()
lock_b.release()
# 解决方案:统一锁顺序
def thread_1_fixed():
lock_a.acquire() # 先A后B
lock_b.acquire()
lock_b.release()
lock_a.release()
def thread_2_fixed():
lock_a.acquire() # 也先A后B
lock_b.acquire()
lock_b.release()
lock_a.release()
面试追问:实际工作中如何排查死锁? 1. Java:
jstack <pid>查看线程堆栈,找到BLOCKED状态的线程和等待的锁 2. MySQL:SHOW ENGINE INNODB STATUS查看死锁日志 3. Go:runtime.SetBlockProfileRate()+ pprof 4. 日志中记录获取锁的时间,超时后自动释放
题目 5 :进程有哪些状态?状态之间如何转换?¶
五状态模型:
创建
↓
┌→ 就绪(Ready) ←──────────┐
│ ↓ (调度) │ (IO完成/事件发生)
│ 运行(Running) ──→ 阻塞(Blocked)
│ ↓ (时间片耗尽)
└──────┘
↓ (退出)
终止(Terminated)
- 创建→就绪:进程创建完成,等待 CPU 调度
- 就绪→运行:被调度器选中,获得 CPU
- 运行→就绪:时间片用完,或被更高优先级进程抢占
- 运行→阻塞:等待 IO 操作或资源
- 阻塞→就绪: IO 完成或获得资源
- 运行→终止:进程执行完毕或异常退出
面试追问: Linux 下进程有哪些状态?
R(Running/Runnable),S(Sleeping/可中断休眠),D(Disk Sleep/不可中断休眠),Z(Zombie/僵尸),T(Stopped/暂停),t(Traced/被跟踪)
题目 6 :什么是僵尸进程和孤儿进程?¶
僵尸进程( Zombie ): - 子进程退出后,父进程没有调用 wait()/waitpid() 回收子进程的退出状态 - 进程的 PCB (进程控制块)仍占用系统资源 - ps 中显示为 Z 状态
危害: 大量僵尸进程会耗尽 PID 和系统资源
解决: 1. 父进程调用 wait()/waitpid() 2. 安装 SIGCHLD 信号处理函数:signal(SIGCHLD, SIG_IGN) 3. 杀死父进程,让 init 进程(PID=1)接管回收
孤儿进程( Orphan ): - 父进程先于子进程退出 - 子进程被 init 进程(PID=1)收养 - init 进程会自动回收孤儿进程,危害较小
题目 7 :进程调度算法有哪些?¶
| 算法 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| FCFS (先来先服务) | 按到达顺序调度 | 简单公平 | 平均等待时间长 |
| SJF (短作业优先) | 执行时间最短的先调度 | 平均等待时间最短 | 可能饿死长作业 |
| RR (时间片轮转) | 每个进程分配相等的时间片 | 响应时间短 | 时间片选择影响性能 |
| 优先级调度 | 按优先级调度 | 紧急任务优先处理 | 低优先级可能饿死 |
| 多级反馈队列 | 多个队列不同优先级和时间片 | 兼顾响应时间和吞吐量 | 实现复杂 |
| CFS (完全公平调度器) | Linux 默认,基于虚拟运行时间 | 公平、高效 | - |
面试追问: Linux 的 CFS 调度器如何工作? CFS 使用红黑树维护可运行进程,树的键值是虚拟运行时间( vruntime )。每次调度选择 vruntime 最小的进程运行。优先级越高,时间流逝越慢( vruntime 增长越慢),从而获得更多 CPU 时间。
题目 8 :什么是上下文切换?开销有多大?¶
上下文切换: CPU 从一个进程/线程切换到另一个时,需要保存当前的执行状态并恢复目标的状态。
保存/恢复的内容: - CPU 寄存器(程序计数器、栈指针等) - 进程状态(就绪/运行/阻塞) - 内存映射(页表,进程切换时需要,线程切换不需要)
开销: - 线程切换:~1-10μs - 进程切换:~3-30μs (额外包含 TLB 刷新、页表切换) - 上下文切换本身的 CPU 指令 - TLB (页表缓存)失效导致的内存访问变慢(间接开销更大)
面试追问:如何减少上下文切换? 1. 减少线程数量(线程池复用线程) 2. 使用无锁数据结构(减少锁竞争导致的阻塞) 3. 使用协程(用户态切换) 4. CPU 亲和性绑定(减少 CPU 间迁移)
题目 9 :用户态和内核态的区别?什么时候会切换?¶
| 特性 | 用户态( User Mode ) | 内核态( Kernel Mode ) |
|---|---|---|
| 权限 | 受限,不能直接访问硬件 | 最高权限,可访问所有资源 |
| 地址空间 | 只能访问用户空间 | 可访问用户空间和内核空间 |
| 目的 | 运行用户程序 | 运行操作系统核心代码 |
从用户态切换到内核态的三种方式: 1. 系统调用( Syscall ):程序主动请求内核服务( read, write, fork 等) 2. 中断( Interrupt ):硬件设备触发(键盘输入、磁盘 IO 完成) 3. 异常( Exception ):程序执行异常(缺页中断、除零错误)
切换过程: 1. 保存用户态上下文(寄存器、 PC 、栈指针) 2. 切换到内核栈 3. 执行内核代码 4. 恢复用户态上下文
面试追问:为什么需要区分用户态和内核态? 安全隔离。如果用户程序可以直接操作硬件和内存,恶意程序可以破坏系统或其他程序。内核态作为守门人,所有敏感操作必须经过内核验证。
题目 10 :什么是系统调用?和普通函数调用有什么区别?¶
系统调用:用户程序请求操作系统内核提供服务的接口。
| 特性 | 系统调用 | 普通函数调用 |
|---|---|---|
| 执行模式 | 用户态→内核态→用户态 | 始终在用户态 |
| 开销 | 大(模式切换) | 小 |
| 安全检查 | 内核验证参数和权限 | 调用方负责 |
| 实现位置 | 内核代码 | 用户空间库 |
常见系统调用分类: - 进程控制:fork(), exec(), wait(), exit() - 文件操作:open(), read(), write(), close() - 网络通信:socket(), bind(), listen(), accept() - 内存管理:mmap(), brk(), munmap()
系统调用过程: 1. 用户程序调用 C 库的包装函数(如 glibc 的 write()) 2. 包装函数将系统调用号放入寄存器( x86_64: rax ) 3. 触发软中断(int 0x80)或使用syscall指令 4. CPU 切换到内核态,根据调用号查系统调用表 5. 执行内核函数 6. 返回结果,切回用户态
题目 11 : fork()的原理是什么?写时复制( COW )是什么?¶
fork():创建一个与父进程几乎完全相同的子进程。
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("I am child, PID=%d\n", getpid());
} else if (pid > 0) {
// 父进程
printf("I am parent, child PID=%d\n", pid);
} else {
// fork失败
perror("fork failed");
}
写时复制( Copy-On-Write, COW ): - fork 时不会立即复制父进程的内存,只复制页表 - 父子进程共享相同的物理页面,页面标记为只读 - 当任一进程尝试写入时,触发缺页中断,此时才复制该页面 - 大幅减少 fork 的时间和内存开销
面试追问: fork 和 vfork 的区别? vfork 不复制页表,子进程直接共享父进程的地址空间。子进程必须立即调用 exec()或 exit(),否则会破坏父进程的数据。 vfork 保证子进程先运行,父进程阻塞直到子进程调用 exec/exit 。
题目 12 :什么是线程安全?如何实现?¶
线程安全:多个线程同时访问某个函数/数据结构时,不需要额外的同步措施就能保证正确性。
实现方法:
- 互斥锁( Mutex ):同一时刻只有一个线程能获得锁
import threading # 线程池/多线程:并发执行任务
lock = threading.Lock()
counter = 0
def increment():
global counter
with lock:
counter += 1 # 临界区
- 读写锁( RWLock ):读共享,写独占
- 原子操作( Atomic ): CAS ( Compare-And-Swap )无锁操作
- 线程局部存储( Thread Local ):每个线程有自己的变量副本
- 不可变对象:数据创建后不可修改
面试追问:自旋锁和互斥锁的区别?什么时候用哪个? 互斥锁:获取不到锁时线程休眠让出 CPU ,适合临界区大或锁持有时间长的场景。 自旋锁:获取不到锁时忙等待(循环检查),不让出 CPU ,适合临界区小且锁持有时间短的场景。避免了线程切换开销。
二、内存管理( 10 题)¶
题目 13 :虚拟内存是什么?为什么需要?¶
虚拟内存:操作系统为每个进程提供的一个独立的、连续的地址空间,通过页表映射到物理内存。
为什么需要: 1. 隔离性:每个进程有独立地址空间,互不干扰 2. 更大的地址空间:虚拟地址空间可以大于物理内存(利用磁盘交换) 3. 简化编程:程序员不需要关心物理内存布局 4. 共享内存:多个进程的虚拟页可以映射到同一物理页 5. 内存保护:通过页表设置权限(读/写/执行)
地址转换过程:
面试追问: 32 位和 64 位系统的虚拟地址空间分别是多少? 32 位: 4GB (内核 1GB + 用户 3GB 或 内核 2GB + 用户 2GB )。 64 位:理论上 16EB ,实际使用 48 位( 256TB ), Linux 内核和用户各 128TB 。
题目 14 :页面置换算法有哪些?¶
当物理内存不足时,需要将某些页换出到磁盘,选择哪些页就是页面置换算法的任务。
| 算法 | 原理 | 优缺点 |
|---|---|---|
| OPT (最优) | 置换未来最长时间不使用的页 | 理论最优但无法实现 |
| FIFO (先进先出) | 置换最早加载的页 | 简单但有 Belady 异常 |
| LRU (最近最少使用) | 置换最近最长时间未使用的页 | 效果好但实现开销大 |
| Clock (时钟) | LRU 的近似,使用访问位 | 平衡效果和开销 |
| LFU (最不经常使用) | 置换访问频率最低的页 | 不适应访问模式变化 |
LRU 实现方式: 1. 链表+哈希:每次访问移到链表头,淘汰尾部 2. 时间戳:记录最后访问时间,淘汰最旧的 3. 近似 LRU : Clock 算法( Linux 使用的方式)
题目 15 :什么是缺页中断( Page Fault )?¶
缺页中断:程序访问的虚拟页没有映射到物理内存时, CPU 触发的中断。
处理流程: 1. CPU 检测到地址转换失败,触发缺页中断 2. 操作系统检查该虚拟地址是否合法 3. 如果合法:找一个空闲物理页帧,从磁盘加载数据 4. 如果物理内存满:使用页面置换算法选择一个页换出 5. 更新页表,重新执行触发中断的指令
缺页类型: - 软缺页( Minor ):页面在内存中但页表没映射(如 COW ) - 硬缺页( Major ):页面不在内存中,需要从磁盘读取
面试追问:如何减少缺页中断? 1. 增加物理内存; 2. 优化程序的内存访问模式(提升局部性); 3. 使用大页( Huge Pages )减少页表条目; 4. 预读取( Prefetch )。
题目 16 :什么是内存泄漏?如何检测和避免?¶
内存泄漏:程序动态分配的内存不再使用后没有释放,导致可用内存逐渐减少。
常见原因: 1. malloc/new 后忘记 free/delete 2. 对象引用循环(在 GC 语言中) 3. 全局集合不断增长 4. 资源句柄未关闭(文件、数据库连接)
检测工具: | 语言/工具 | 检测工具 | |-----------|----------| | C/C++ | Valgrind, AddressSanitizer(ASan) | | Java | JvisualVM, MAT, jmap + jhat | | Python | tracemalloc, objgraph | | Go | pprof |
# Valgrind检测内存泄漏
valgrind --leak-check=full ./my_program
# Go pprof
go tool pprof http://localhost:6060/debug/pprof/heap
避免方法: 1. C++使用智能指针( unique_ptr, shared_ptr ) 2. 使用 RAII ( Resource Acquisition Is Initialization ) 3. GC 语言注意避免引用循环,及时设为 null 4. 使用连接池管理资源
题目 17 :进程的内存布局是怎样的?¶
高地址
┌──────────────┐
│ 内核空间 │ 用户不可访问
├──────────────┤
│ 栈(Stack) │ 局部变量、函数参数、返回地址(向下增长)
│ ↓ │
│ │
│ ↑ │
│ 堆(Heap) │ 动态分配的内存(向上增长)
├──────────────┤
│ BSS段 │ 未初始化的全局/静态变量(初始化为0)
├──────────────┤
│ 数据段 │ 已初始化的全局/静态变量
├──────────────┤
│ 代码段 │ 程序的机器指令(只读)
└──────────────┘
低地址
面试追问:栈和堆的区别? 栈:自动分配/释放,速度快,空间有限(通常几 MB ),存储局部变量。 堆:手动分配/释放(或 GC 回收),空间大,分配速度较慢,可能产生碎片。
题目 18 :什么是内存映射( mmap )?¶
mmap() 将文件或设备映射到进程的虚拟地址空间,通过内存操作直接读写文件。
优势: 1. 避免 read()/write() 的内核态-用户态数据拷贝 2. 多个进程可以映射同一文件实现共享内存 3. 读写操作由操作系统的页管理机制处理
适用场景: - 大文件读写 - 进程间共享内存 - 加载动态库(.so/.dll )
#include <sys/mman.h> // 引入头文件
int fd = open("data.bin", O_RDONLY);
char *data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); // 指针:存储变量的内存地址
// 直接通过指针访问文件内容
printf("First byte: %c\n", data[0]);
munmap(data, file_size);
close(fd);
题目 19 :什么是 TLB ?为什么重要?¶
TLB ( Translation Lookaside Buffer ):页表缓存,缓存了最近使用的虚拟地址→物理地址的映射。
- 查 TLB 比查页表快得多( TLB 是硬件缓存,通常在 CPU 内部)
- TLB 命中:直接获得物理地址(~1 CPU 周期)
- TLB 未命中:需要查页表(多级页表可能需要 4-5 次内存访问)
TLB 失效的情况: 1. 进程切换时刷新 TLB ( PCID 可以缓解) 2. 内存映射变化(如 mmap/munmap ) 3. 容量有限导致的淘汰
面试追问:进程切换一定要刷新 TLB 吗? 早期是的,因为不同进程的页表不同。现代 CPU 支持 PCID ( Process Context Identifier ),给每个 TLB 条目标记进程 ID ,切换进程时不需要刷新,只需切换 PCID 。
题目 20 :什么是内存碎片?如何解决?¶
外部碎片:空闲内存被分散成很多小块,总量够用但没有一块足够大。 内部碎片:分配的内存块大于实际需要的大小,多余部分浪费。
解决方案: - 外部碎片:内存紧凑(移动已分配块),伙伴系统( Buddy System ), Slab 分配器 - 内部碎片:分配更合适大小的块, Slab 分配器
Linux 内存分配器: - 伙伴系统:管理物理页帧,按 2 的幂次分配 - Slab 分配器:在伙伴系统之上,为内核对象提供高效的内存分配 - 用户空间: glibc 的 ptmalloc2, jemalloc, tcmalloc
题目 21 : malloc 的底层实现原理?¶
glibc 的 malloc 实现( ptmalloc2 ):
- 小块(<128KB ):使用 brk()系统调用扩展堆
- 空闲块组织为 bins (链表)
- 不同大小的 bin : fastbin, smallbin, largebin
-
分配时从合适的 bin 中找空闲块
-
大块(≥128KB ):使用 mmap()分配独立的内存区域
-
释放时直接 munmap()归还系统
-
fastbin (<= 80 字节):单向链表, LIFO ,不合并相邻空闲块,极快
题目 22 :什么是 OOM Killer ?在什么情况下触发?¶
OOM ( Out of Memory ) Killer: Linux 内核在物理内存和 Swap 都不足时,选择并杀死某个进程来释放内存。
选择策略( oom_score ): - 内存使用量大的进程得分高(更容易被杀) - 可通过 /proc/<pid>/oom_adj 或 oom_score_adj 调整 - 设置 oom_score_adj = -1000 可以禁止被杀
触发条件: 1. 物理内存用尽 2. Swap 空间也用尽 3. 无法通过回收 Page Cache 释放内存
# 查看进程的OOM得分
cat /proc/<pid>/oom_score
# 保护重要进程不被OOM Kill
echo -1000 > /proc/<pid>/oom_score_adj
三、 IO 模型( 8 题)¶
题目 23 : Linux 的五种 IO 模型是什么?¶
| IO 模型 | 特点 | 调用方式 |
|---|---|---|
| 阻塞 IO | 等待数据+拷贝数据期间都阻塞 | read() 阻塞 |
| 非阻塞 IO | 等待数据不阻塞(轮询),拷贝数据阻塞 | read() + O_NONBLOCK |
| IO 多路复用 | 一个线程监控多个 fd | select/poll/epoll |
| 信号驱动 IO | 数据就绪时通知 | SIGIO 信号 |
| 异步 IO | 完全不阻塞,内核完成后通知 | aio_read() |
面试追问:同步 IO 和异步 IO 的本质区别? 同步 IO :应用程序在数据从内核缓冲区拷贝到用户缓冲区的过程中是阻塞的。异步 IO :整个过程(等待数据+拷贝数据)都不阻塞,内核完成后通知应用程序。
题目 24 : select 、 poll 和 epoll 的区别?¶
| 特性 | select | poll | epoll |
|---|---|---|---|
| fd 上限 | 1024 ( FD_SETSIZE ) | 无限(链表) | 无限 |
| 数据结构 | bitmap | 数组 | 红黑树+链表 |
| 效率 | O(n) 线性扫描 | O(n) 线性扫描 | O(1) 事件驱动 |
| 拷贝 | 每次调用拷入拷出 fd 集合 | 同左 | fd 只需拷入一次 |
| 触发方式 | 水平触发 | 水平触发 | 水平触发+边缘触发 |
epoll 核心 API :
// 创建epoll实例
int epfd = epoll_create1(0);
// 添加/修改/删除监听的fd
struct epoll_event ev; // struct结构体:自定义复合数据类型
ev.events = EPOLLIN | EPOLLET; // 边缘触发+读事件
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 等待事件
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
for (int i = 0; i < nfds; i++) {
handle(events[i].data.fd);
}
面试追问:水平触发( LT )和边缘触发( ET )的区别? LT ( Level Triggered ):只要缓冲区有数据可读,就会通知(每次 epoll_wait 都会返回)。 ET ( Edge Triggered ):只在状态变化时通知一次(从无数据变为有数据),必须一次读完所有数据。 ET 效率更高但编程更复杂,必须使用非阻塞 IO 。
题目 25 : Reactor 模式和 Proactor 模式的区别?¶
Reactor (同步 IO 多路复用): 1. 主线程用 epoll 监听 IO 事件 2. 有事件就绪时,分发给工作线程处理 3. 工作线程自己执行 IO 操作( read/write )
Proactor (异步 IO ): 1. 应用发起异步 IO 操作 2. 操作系统完成 IO 后通知应用 3. 应用直接处理已完成的数据
对比: - Reactor : IO 操作由应用线程执行(同步) - Proactor : IO 操作由操作系统执行(异步) - Linux 主要使用 Reactor ( epoll ), Windows 有完善的 Proactor ( IOCP )
常见的 Reactor 实现: - 单 Reactor 单线程: Redis - 单 Reactor 多线程:简单的多线程服务器 - 主从 Reactor : Netty (主 Reactor 接受连接,从 Reactor 处理 IO )
题目 26 :零拷贝( Zero-Copy )是什么?¶
传统数据传输(读文件发送到网络):
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket缓冲区 → 网卡
(DMA拷贝) (CPU拷贝) (CPU拷贝) (DMA拷贝)
共4次拷贝,4次用户态/内核态上下文切换(read和write各引起2次)
零拷贝( sendfile ):
DMA + sendfile + SG-DMA ( Linux 2.4+):
应用场景: Kafka 、 Nginx 、 Tomcat 的静态文件传输都使用了零拷贝。
题目 27 :什么是文件描述符( fd )?¶
文件描述符是 Linux 内核为每个打开的文件分配的非负整数编号,是进程访问文件的凭证。
默认的 fd : - 0 :标准输入( stdin ) - 1 :标准输出( stdout ) - 2 :标准错误( stderr )
# 查看进程的文件描述符
ls -la /proc/<pid>/fd/
# 查看系统fd限制
ulimit -n # 每个进程的限制(默认1024)
cat /proc/sys/fs/file-max # 系统级别限制
面试追问:文件描述符耗尽会怎样? 无法打开新的文件、 Socket 连接等。常见错误:"Too many open files"。解决:增大 ulimit 值(
ulimit -n 65535),修改 sysctl 配置,或者检查代码中是否有资源泄漏。
题目 28 :什么是直接 IO ( Direct IO )?¶
普通 IO:数据经过 Page Cache 缓存( read → Page Cache → 用户缓冲区)
直接 IO:绕过 Page Cache ,数据直接在磁盘和用户缓冲区之间传输
适用场景: - 数据库系统(如 MySQL InnoDB ):自己管理缓存,不需要 Page Cache - 大文件顺序读写: Page Cache 反而是负担
不适用场景: - 小文件随机读写: Page Cache 的缓存效果很好
题目 29 :什么是缓冲 IO 和非缓冲 IO ?¶
缓冲 IO:数据先写入用户空间的缓冲区(如 C 标准库的 FILE 缓冲区),积累到一定量后再调用系统调用写入内核。 - printf(), fprintf() 使用缓冲 IO - 全缓冲:缓冲区满或 fflush 时写入 - 行缓冲:遇到换行符时写入(终端输出) - 无缓冲:立即写入( stderr )
非缓冲 IO:每次调用直接触发系统调用。 - write(), read() 是非缓冲 IO
题目 30 : aio (异步 IO )在 Linux 中的实现状况?¶
Linux 原生 AIO ( io_submit/io_getevents ): - 仅支持 O_DIRECT 的文件 IO - 接口不够友好 - 使用较少
io_uring ( Linux 5.1+): - 真正的通用异步 IO 接口 - 支持文件 IO 、网络 IO - 使用用户态-内核态共享的环形缓冲区,避免系统调用开销 - 性能极好,被认为是 Linux IO 模型的未来
四、 Linux 常用命令( 10 题)¶
题目 31 :如何查看系统资源使用情况?¶
# CPU使用率
top # 实时动态查看
htop # 增强版top
mpstat -P ALL 1 # 每个CPU核心的使用率
# 内存使用
free -h # 内存和Swap使用情况
vmstat 1 # 虚拟内存统计
# 磁盘IO
iostat -x 1 # 磁盘IO统计
iotop # IO版的top
# 网络
netstat -tunlp # 监听端口
ss -tunlp # 更快的netstat替代
iftop # 网络流量实时监控
# 综合
dstat # CPU/磁盘/网络/内存综合监控
sar # 系统活动报告
题目 32 :如何排查 CPU 占用过高的问题?¶
# 1. 找到CPU占用高的进程
top -c # 按CPU排序,找PID
# 2. 查看进程的线程
top -Hp <PID> # 找到CPU占用高的线程TID
# 3. 将TID转为16进制
printf "%x\n" <TID> # 例如31420 → 7aac
# 4. 查看线程堆栈(Java进程)
jstack <PID> | grep "0x7aac" -A 30
# 或使用perf分析
perf top -p <PID> # 实时查看热点函数
perf record -p <PID> -g -- sleep 30 # 记录30秒
perf report # 分析报告
题目 33 :如何排查内存问题?¶
# 查看内存使用最多的进程
ps aux --sort=-%mem | head
# 查看进程的详细内存映射
pmap -x <PID>
cat /proc/<PID>/smaps
# 查看OOM日志
dmesg | grep -i "out of memory"
journalctl -k | grep -i oom
# Java堆内存分析
jmap -heap <PID> # 堆内存统计
jmap -histo <PID> | head -20 # 对象分布
jmap -dump:format=b,file=heap.bin <PID> # 导出堆转储
题目 34 :常用的文本处理命令?¶
# grep:搜索文本
grep -rn "error" /var/log/ # 递归搜索,显示行号
grep -E "error|warning" log.txt # 正则匹配
grep -v "INFO" log.txt # 排除匹配
# awk:文本处理
awk '{print $1, $4}' access.log # 打印第1和第4列
awk -F: '{print $1}' /etc/passwd # 指定分隔符
awk '$9==500' access.log # 过滤HTTP 500
# sed:流编辑
sed 's/old/new/g' file.txt # 全局替换
sed -n '10,20p' file.txt # 打印第10-20行
sed -i '/pattern/d' file.txt # 删除匹配行
# sort + uniq + wc
sort access.log | uniq -c | sort -rn | head # 统计并排序
# xargs:将标准输入转为命令参数
find . -name "*.log" | xargs rm
find . -name "*.py" | xargs grep "import"
题目 35 :如何查看和分析网络连接?¶
# 查看所有TCP连接
ss -tan # t=TCP, a=all, n=不解析域名
netstat -an | grep ESTABLISHED | wc -l # 统计连接数
# 按状态统计TCP连接
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # awk文本处理:按列提取和格式化数据
# 查看某端口的连接
ss -tan | grep :8080 # grep文本搜索:按模式匹配行
# 抓包分析
tcpdump -i eth0 port 80 -w /tmp/capture.pcap
tcpdump -i eth0 host 192.168.1.1 -nn
# DNS查询
nslookup example.com
dig example.com
# 连通性测试
ping -c 4 example.com
traceroute example.com
mtr example.com # 更好的traceroute
curl -v https://example.com # HTTP请求详情
题目 36 :什么是进程的 nice 值?如何调整进程优先级?¶
nice 值:影响进程 CPU 调度优先级的值,范围 -20 (最高)到 19 (最低),默认为 0 。
# 以指定nice值启动程序
nice -n 10 ./my_program # nice值10启动
# 修改已运行进程的nice值
renice -5 -p <PID> # 设置为-5(需要root提高优先级)
# 查看进程的nice值
ps -eo pid,ni,comm | head # |管道:将前一命令的输出作为后一命令的输入
top # NI列显示nice值
题目 37 : crontab 定时任务如何使用?¶
# 编辑当前用户的定时任务
crontab -e
# 定时任务格式:分 时 日 月 周 命令
# ┌──── 分钟 (0-59)
# │ ┌──── 小时 (0-23)
# │ │ ┌──── 日 (1-31)
# │ │ │ ┌──── 月 (1-12)
# │ │ │ │ ┌──── 星期 (0-7, 0和7都是周日)
# │ │ │ │ │
# * * * * * command
# 示例
0 2 * * * /backup.sh # 每天凌晨2点执行
*/5 * * * * /check.sh # 每5分钟执行
0 0 1 * * /monthly.sh # 每月1号执行
30 8 * * 1-5 /weekday.sh # 工作日8:30执行
题目 38 :如何查看和管理磁盘空间?¶
# 查看磁盘使用情况
df -h # 各分区使用情况
df -i # inode使用情况
# 查看目录大小
du -sh /var/log/ # 查看目录总大小
du -h --max-depth=1 / # 各一级目录大小
du -sh * | sort -rh | head # 当前目录下最大的文件/目录
# 清理磁盘
find /var/log -name "*.log" -mtime +30 -delete # 删除30天前的日志
journalctl --vacuum-size=100M # 清理systemd日志
题目 39 :信号( Signal )机制是什么?常用信号有哪些?¶
| 信号 | 编号 | 含义 | 默认动作 |
|---|---|---|---|
| SIGHUP | 1 | 终端断开 | 终止 |
| SIGINT | 2 | Ctrl+C | 终止 |
| SIGQUIT | 3 | Ctrl+\ | 终止+core dump |
| SIGKILL | 9 | 强制杀死 | 终止(不可捕获) |
| SIGSEGV | 11 | 段错误 | 终止+core dump |
| SIGTERM | 15 | 正常终止 | 终止(可捕获) |
| SIGSTOP | 19 | 暂停进程 | 暂停(不可捕获) |
| SIGCHLD | 17 | 子进程状态变化 | 忽略 |
# 发送信号
kill -15 <PID> # 发送SIGTERM(优雅退出)
kill -9 <PID> # 发送SIGKILL(强制杀死)
kill -HUP <PID> # 发送SIGHUP(常用于重新加载配置)
# 常见处置顺序:先 SIGTERM(15),等待几秒,再视情况使用 SIGKILL(9)
题目 40 :什么是 inode ?文件系统的基本结构?¶
inode (索引节点):存储文件的元信息(不含文件名)。
inode 包含: - 文件大小、权限、所有者 - 时间戳(创建时间、修改时间、访问时间) - 数据块的指针 - 链接计数
文件系统结构:
硬链接 vs 软链接: | 特性 | 硬链接 | 软链接(符号链接) | |------|--------|-------------------| | inode | 相同 | 不同 | | 跨文件系统 | 不可以 | 可以 | | 链接目录 | 不可以 | 可以 | | 原文件删除 | 仍可访问 | 链接失效 |
ln file.txt hardlink # 创建硬链接
ln -s file.txt softlink # 创建软链接
ls -li # 查看inode号
stat file.txt # 查看文件inode信息
总结¶
操作系统核心知识
├── 进程与线程
│ ├── 进程/线程/协程的区别
│ ├── 进程间通信(IPC)
│ ├── 死锁与调度
│ └── 同步机制(锁、信号量)
├── 内存管理
│ ├── 虚拟内存与页表
│ ├── 页面置换算法
│ ├── 内存布局与分配
│ └── 内存泄漏检测
├── IO模型
│ ├── 五种IO模型
│ ├── IO多路复用(select/poll/epoll)
│ └── 零拷贝
└── Linux
├── 常用命令
├── 性能排查
└── 文件系统
操作系统是所有系统软件的基础。深入理解 OS 原理不仅是面试必备,更是成为优秀工程师的关键。推荐阅读《深入理解计算机系统》( CSAPP )和《操作系统导论》( OSTEP )。
⚠️ 核验说明(2026-04-03):本页已完成 2026-04-03 人工复核。本页中的“最优”“必须”主要保留在操作系统定义、约束或安全前提里;信号处理顺序等工程建议已改成更稳妥的场景化口径。
最后更新日期: 2026-04-03