操作系统面试题¶
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):同一时刻只有一个线程能获得锁
-
读写锁(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)。