跳转至

操作系统面试题

操作系统面试题图

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  # 临界区
    

  2. 读写锁(RWLock):读共享,写独占

  3. 原子操作(Atomic):CAS(Compare-And-Swap)无锁操作
  4. 线程局部存储(Thread Local):每个线程有自己的变量副本
  5. 不可变对象:数据创建后不可修改

面试追问:自旋锁和互斥锁的区别?什么时候用哪个? 互斥锁:获取不到锁时线程休眠让出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)。