跳转至

操作系统面试题

40 道操作系统高频面试题,涵盖进程与线程、内存管理、 IO 模型、 Linux 命令等核心知识点


一、进程与线程( 12 题)

题目 1 :进程和线程的区别是什么?

特性 进程( Process ) 线程( Thread )
定义 资源分配的基本单位 CPU 调度的基本单位
地址空间 独立的地址空间 共享进程的地址空间
资源 拥有独立资源(文件描述符、内存等) 共享进程资源
创建开销 大(需要分配独立资源) 小(共享进程资源)
切换开销 大(需要切换页表等) 小(只需切换寄存器等)
通信方式 IPC (管道、消息队列、共享内存等) 直接读写共享变量
崩溃影响 一个进程崩溃不影响其他进程 一个线程崩溃可能导致整个进程崩溃
安全性 更安全(隔离性好) 需要同步机制(锁)

面试追问:进程间共享的资源有哪些? 进程间默认不共享资源,但可以通过 IPC 机制共享:共享内存、管道、消息队列、信号量等。子进程 fork 后会复制父进程的资源(写时复制 COW )。


题目 2 :什么是协程?和线程有什么区别?

协程( Coroutine ):是一种用户态的轻量级线程,由程序员/运行时控制调度,而非操作系统。

特性 线程 协程
调度 操作系统调度(抢占式) 用户态调度(协作式)
切换 内核态切换,开销大(~1-10μs ) 用户态切换,开销极小(~100ns )
内存 默认栈 1-8MB 几 KB (可动态增长)
数量 受限(通常数千个) 可创建数十万个
同步 需要锁 单线程内无需锁
Python
# 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 )
C
// 共享内存示例(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. 循环等待:多个进程形成资源等待的循环链

预防死锁(破坏必要条件): - 破坏请求与保持:一次性申请所有资源 - 破坏不可剥夺:得不到新资源时释放已占有的资源 - 破坏循环等待:按固定顺序申请资源

避免死锁: - 银行家算法:每次分配前检查是否会导致不安全状态

检测与恢复: - 检测:资源分配图是否有环 - 恢复:终止死锁进程 / 剥夺资源

Python
# 死锁示例
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 :进程有哪些状态?状态之间如何转换?

五状态模型:

Text Only
            创建
    ┌→ 就绪(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():创建一个与父进程几乎完全相同的子进程。

C
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 :什么是线程安全?如何实现?

线程安全:多个线程同时访问某个函数/数据结构时,不需要额外的同步措施就能保证正确性。

实现方法:

  1. 互斥锁( Mutex ):同一时刻只有一个线程能获得锁
Python
import threading  # 线程池/多线程:并发执行任务
lock = threading.Lock()

counter = 0

def increment():
    global counter
    with lock:
        counter += 1  # 临界区
  1. 读写锁( RWLock ):读共享,写独占
  2. 原子操作( Atomic ): CAS ( Compare-And-Swap )无锁操作
  3. 线程局部存储( Thread Local ):每个线程有自己的变量副本
  4. 不可变对象:数据创建后不可修改

面试追问:自旋锁和互斥锁的区别?什么时候用哪个? 互斥锁:获取不到锁时线程休眠让出 CPU ,适合临界区大或锁持有时间长的场景。 自旋锁:获取不到锁时忙等待(循环检查),不让出 CPU ,适合临界区小且锁持有时间短的场景。避免了线程切换开销。


二、内存管理( 10 题)

题目 13 :虚拟内存是什么?为什么需要?

虚拟内存:操作系统为每个进程提供的一个独立的、连续的地址空间,通过页表映射到物理内存。

为什么需要: 1. 隔离性:每个进程有独立地址空间,互不干扰 2. 更大的地址空间:虚拟地址空间可以大于物理内存(利用磁盘交换) 3. 简化编程:程序员不需要关心物理内存布局 4. 共享内存:多个进程的虚拟页可以映射到同一物理页 5. 内存保护:通过页表设置权限(读/写/执行)

地址转换过程:

Text Only
虚拟地址 → 页号(VPN) + 页内偏移(Offset)
       查页表
物理地址 = 页帧号(PFN) × 页大小 + 偏移(Offset)

面试追问: 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 |

Bash
# 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 :进程的内存布局是怎样的?

Text Only
高地址
┌──────────────┐
│   内核空间    │  用户不可访问
├──────────────┤
│     栈(Stack) │  局部变量、函数参数、返回地址(向下增长)
│      ↓       │
│              │
│      ↑       │
│    堆(Heap)  │  动态分配的内存(向上增长)
├──────────────┤
│   BSS段      │  未初始化的全局/静态变量(初始化为0)
├──────────────┤
│   数据段     │  已初始化的全局/静态变量
├──────────────┤
│   代码段     │  程序的机器指令(只读)
└──────────────┘
低地址

面试追问:栈和堆的区别? 栈:自动分配/释放,速度快,空间有限(通常几 MB ),存储局部变量。 堆:手动分配/释放(或 GC 回收),空间大,分配速度较慢,可能产生碎片。


题目 18 :什么是内存映射( mmap )?

mmap() 将文件或设备映射到进程的虚拟地址空间,通过内存操作直接读写文件。

优势: 1. 避免 read()/write() 的内核态-用户态数据拷贝 2. 多个进程可以映射同一文件实现共享内存 3. 读写操作由操作系统的页管理机制处理

适用场景: - 大文件读写 - 进程间共享内存 - 加载动态库(.so/.dll )

C
#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 ):

  1. 小块(<128KB ):使用 brk()系统调用扩展堆
  2. 空闲块组织为 bins (链表)
  3. 不同大小的 bin : fastbin, smallbin, largebin
  4. 分配时从合适的 bin 中找空闲块

  5. 大块(≥128KB ):使用 mmap()分配独立的内存区域

  6. 释放时直接 munmap()归还系统

  7. fastbin (<= 80 字节):单向链表, LIFO ,不合并相邻空闲块,极快


题目 22 :什么是 OOM Killer ?在什么情况下触发?

OOM ( Out of Memory ) Killer: Linux 内核在物理内存和 Swap 都不足时,选择并杀死某个进程来释放内存。

选择策略( oom_score ): - 内存使用量大的进程得分高(更容易被杀) - 可通过 /proc/<pid>/oom_adjoom_score_adj 调整 - 设置 oom_score_adj = -1000 可以禁止被杀

触发条件: 1. 物理内存用尽 2. Swap 空间也用尽 3. 无法通过回收 Page Cache 释放内存

Bash
# 查看进程的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()
Text Only
同步IO:应用参与数据拷贝(阻塞IO、非阻塞IO、IO多路复用、信号驱动IO)
异步IO:应用不参与数据拷贝(异步IO)

面试追问:同步 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 :

C
// 创建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 )是什么?

传统数据传输(读文件发送到网络):

Text Only
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket缓冲区 → 网卡
       (DMA拷贝)    (CPU拷贝)     (CPU拷贝)     (DMA拷贝)
共4次拷贝,4次用户态/内核态上下文切换(read和write各引起2次)

零拷贝( sendfile )

Text Only
磁盘 → 内核缓冲区 → Socket缓冲区 → 网卡
       (DMA拷贝)    (CPU拷贝)     (DMA拷贝)
共3次拷贝,0次用户态/内核态数据拷贝

DMA + sendfile + SG-DMA ( Linux 2.4+)

Text Only
磁盘 → 内核缓冲区 --------→ 网卡
       (DMA拷贝)           (SG-DMA拷贝)
仅2次DMA拷贝,CPU不参与数据拷贝

应用场景: Kafka 、 Nginx 、 Tomcat 的静态文件传输都使用了零拷贝。


题目 27 :什么是文件描述符( fd )?

文件描述符是 Linux 内核为每个打开的文件分配的非负整数编号,是进程访问文件的凭证。

默认的 fd : - 0 :标准输入( stdin ) - 1 :标准输出( stdout ) - 2 :标准错误( stderr )

Bash
# 查看进程的文件描述符
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 ,数据直接在磁盘和用户缓冲区之间传输

C
int fd = open("data.bin", O_RDONLY | O_DIRECT);

适用场景: - 数据库系统(如 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 :如何查看系统资源使用情况?

Bash
# 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 占用过高的问题?

Bash
# 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 :如何排查内存问题?

Bash
# 查看内存使用最多的进程
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 :常用的文本处理命令?

Bash
# 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 :如何查看和分析网络连接?

Bash
# 查看所有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 。

Bash
# 以指定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 定时任务如何使用?

Bash
# 编辑当前用户的定时任务
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 :如何查看和管理磁盘空间?

Bash
# 查看磁盘使用情况
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 子进程状态变化 忽略
Bash
# 发送信号
kill -15 <PID>    # 发送SIGTERM(优雅退出)
kill -9 <PID>     # 发送SIGKILL(强制杀死)
kill -HUP <PID>   # 发送SIGHUP(常用于重新加载配置)

# 常见处置顺序:先 SIGTERM(15),等待几秒,再视情况使用 SIGKILL(9)

题目 40 :什么是 inode ?文件系统的基本结构?

inode (索引节点):存储文件的元信息(不含文件名)。

inode 包含: - 文件大小、权限、所有者 - 时间戳(创建时间、修改时间、访问时间) - 数据块的指针 - 链接计数

文件系统结构:

Text Only
目录项(dentry) → inode → 数据块(data blocks)
文件名 + inode号   元信息    实际数据

硬链接 vs 软链接: | 特性 | 硬链接 | 软链接(符号链接) | |------|--------|-------------------| | inode | 相同 | 不同 | | 跨文件系统 | 不可以 | 可以 | | 链接目录 | 不可以 | 可以 | | 原文件删除 | 仍可访问 | 链接失效 |

Bash
ln file.txt hardlink      # 创建硬链接
ln -s file.txt softlink   # 创建软链接
ls -li                    # 查看inode号
stat file.txt             # 查看文件inode信息

总结

Text Only
操作系统核心知识
├── 进程与线程
│   ├── 进程/线程/协程的区别
│   ├── 进程间通信(IPC)
│   ├── 死锁与调度
│   └── 同步机制(锁、信号量)
├── 内存管理
│   ├── 虚拟内存与页表
│   ├── 页面置换算法
│   ├── 内存布局与分配
│   └── 内存泄漏检测
├── IO模型
│   ├── 五种IO模型
│   ├── IO多路复用(select/poll/epoll)
│   └── 零拷贝
└── Linux
    ├── 常用命令
    ├── 性能排查
    └── 文件系统

操作系统是所有系统软件的基础。深入理解 OS 原理不仅是面试必备,更是成为优秀工程师的关键。推荐阅读《深入理解计算机系统》( CSAPP )和《操作系统导论》( OSTEP )。

⚠️ 核验说明(2026-04-03):本页已完成 2026-04-03 人工复核。本页中的“最优”“必须”主要保留在操作系统定义、约束或安全前提里;信号处理顺序等工程建议已改成更稳妥的场景化口径。


最后更新日期: 2026-04-03