跳转至

09 - 操作系统实战与面试

操作系统实战与面试知识矩阵图

建议学习时间:2 小时 🎯 难度等级:⭐⭐⭐ 📋 前置知识:操作系统全部章节(01-08)


📋 本章目录


🎯 学习目标

  • 掌握Linux性能分析工具链(top、vmstat、iostat、strace、perf)
  • 掌握内核参数调优方法
  • 系统性准备操作系统面试题(35道含详细答案)

Linux性能分析工具

进程级工具

top / htop

top 是实时进程监控工具,各关键字段含义:

Text Only
top - 15:30:01 up 5 days,  3:22,  2 users,  load average: 1.05, 0.70, 0.53
Tasks: 256 total,   1 running, 255 sleeping,   0 stopped,   0 zombie
%Cpu(s): 12.3 us,  3.1 sy,  0.0 ni, 83.2 id,  1.0 wa,  0.0 hi,  0.4 si,  0.0 st
MiB Mem :  16384.0 total,   2048.0 free,  10240.0 used,   4096.0 buff/cache
MiB Swap:   8192.0 total,   7168.0 free,   1024.0 used.   5120.0 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 1234 root      20   0  512820  65432  12340 S  25.3   0.4   5:30.21 java
字段 含义
load average ⅕/15分钟平均负载(等待CPU的进程数)
us 用户态CPU时间
sy 内核态CPU时间
ni 低优先级(nice)进程CPU时间
id 空闲CPU时间
wa 等待I/O的CPU时间(过高说明磁盘瓶颈)
hi 硬件中断处理时间
si 软件中断处理时间
st 虚拟机被偷走的CPU时间
VIRT 虚拟内存总量(代码+数据+共享库+交换区)
RES 常驻物理内存大小(实际使用量)
SHR 共享内存大小

htoptop 的增强版,支持彩色显示、鼠标操作、树形进程视图。

常用交互命令: - P —— 按CPU排序 - M —— 按内存排序 - 1 —— 显示各核CPU使用率 - k —— 杀死进程

ps aux(进程状态)

Bash
ps aux | head
# USER  PID %CPU %MEM  VSZ   RSS TTY  STAT START  TIME COMMAND

STAT列进程状态

状态码 名称 含义
R Running 正在运行或在运行队列中
S Sleeping 可中断睡眠(等待事件完成)
D Disk sleep 不可中断睡眠(通常等待I/O)
Z Zombie 僵尸进程(已终止但父进程未回收)
T Stopped 被信号停止(如SIGSTOP、SIGTSTP)
t Tracing stop 被调试器追踪停止

附加标志:s=会话首进程 l=多线程 +=前台进程组 <=高优先级 N=低优先级

/proc/[pid]/ 目录结构

Bash
/proc/1234/
├── cmdline      # 启动命令行
├── cwd -> ...   # 当前工作目录(符号链接)
├── environ      # 环境变量
├── exe -> ...   # 可执行文件路径(符号链接)
├── fd/          # 文件描述符目录(每个fd是符号链接)
├── fdinfo/      # 文件描述符详细信息
├── maps         # 内存映射(虚拟地址空间布局)
├── mem          # 进程内存(可读写)
├── stat         # 进程状态信息(机器可读)
├── status       # 进程状态信息(人类可读)
├── task/        # 线程信息
└── io           # I/O统计

常用排查命令

Bash
# 查看进程打开的文件数
ls /proc/1234/fd | wc -l

# 查看进程内存映射
cat /proc/1234/maps

# 查看进程的资源限制
cat /proc/1234/limits


系统级工具

vmstat(虚拟内存统计)

Bash
vmstat 1 5   # 每1秒采样一次,共5次
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0  12340 234560  67890 890120    0    0    12    45  230  450 15  5 78  2  0
字段 类别 含义 关注点
r 进程 运行队列中等待CPU的进程数 >CPU核数则CPU瓶颈
b 进程 不可中断睡眠的进程数 >0说明有I/O等待
swpd 内存 已使用的交换区大小(KB) 持续增长说明内存不足
free 内存 空闲内存(KB)
si 交换 换入(swap in)速率(KB/s) >0说明内存紧张
so 交换 换出(swap out)速率(KB/s) >0说明内存紧张
bi I/O 块设备读入速率(blocks/s)
bo I/O 块设备写出速率(blocks/s)
in 系统 每秒中断次数
cs 系统 每秒上下文切换次数 过高说明线程过多或锁竞争
us CPU 用户态CPU百分比
sy CPU 内核态CPU百分比 >30%需关注
id CPU 空闲CPU百分比
wa CPU 等待I/O的CPU百分比 >20%说明I/O瓶颈

iostat(磁盘I/O监控)

Bash
iostat -xd 1     # 查看磁盘扩展统计,每秒刷新
Device   r/s   w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  await  svctm  %util
sda     25.0  45.0  400.0   720.0    1.2     8.5    12.5    2.3   16.1

关键指标: - await:I/O请求的平均等待时间(ms),包含排队时间 - svctm:I/O请求的平均服务时间(ms),不含排队时间(已废弃,参考用) - %util:设备利用率,接近100%说明磁盘满负荷

sar(系统活动报告)

Bash
# CPU使用率历史
sar -u 1 5

# 内存使用率
sar -r 1 5

# 网络流量
sar -n DEV 1 5

# 磁盘I/O
sar -d 1 5

# 查看历史数据(默认存储在/var/log/sa/)
sar -u -f /var/log/sa/sa07   # 查看7号的CPU记录

dmesg(内核日志)

Bash
# 查看内核消息
dmesg | tail -20  # |管道:将前一命令的输出作为后一命令的输入

# 查看OOM Killer日志
dmesg | grep -i "oom"  # grep文本搜索:按模式匹配行

# 查看硬件错误
dmesg | grep -i "error"

# 时间戳可读显示
dmesg -T | tail -20

调试工具

strace(系统调用追踪)

Bash
# 追踪进程的系统调用
strace -p 1234

# 追踪指定命令
strace ls -l /tmp

# 常用选项组合
strace -f -e trace=network -p 1234   # -f追踪子进程, -e过滤网络相关调用
strace -c ls                          # 统计各系统调用的时间/次数
strace -T -e trace=open,read,write ls # -T显示每个调用的耗时

# 输出到文件(方便分析)
strace -o output.log -f -p 1234

常用trace过滤: - -e trace=file —— 文件操作(open, stat, chmod...) - -e trace=network —— 网络操作(socket, connect, send...) - -e trace=process —— 进程操作(fork, exec, wait...) - -e trace=memory —— 内存操作(mmap, brk...) - -e trace=signal —— 信号操作

ltrace(库调用追踪)

Bash
# 追踪库函数调用
ltrace ./myprogram

# 带时间戳
ltrace -t ./myprogram

# 统计调用次数
ltrace -c ./myprogram

perf(性能计数器)

Bash
# 实时查看CPU热点函数
perf top

# 统计程序的性能指标
perf stat ./myprogram
# 输出:CPU周期、指令数、IPC、缓存命中率等

# 采样记录
perf record -g ./myprogram       # -g记录调用图
perf report                      # 查看报告

# 核心使用场景
perf record -F 99 -p 1234 -g -- sleep 30   # 以99Hz频率采样30秒
perf report --stdio              # 文本形式查看

/proc 文件系统关键文件

文件 内容 常用命令
/proc/meminfo 系统内存详情 cat /proc/meminfo \| head
/proc/cpuinfo CPU信息 grep "model name" /proc/cpuinfo
/proc/loadavg 系统负载 cat /proc/loadavg
/proc/net/tcp TCP连接状态 cat /proc/net/tcp
/proc/net/dev 网络接口统计 cat /proc/net/dev
/proc/diskstats 磁盘I/O统计 cat /proc/diskstats
/proc/sys/ 内核参数(可调) sysctl -a
/proc/vmstat 虚拟内存统计 cat /proc/vmstat \| grep pg

Linux内核参数调优

常用sysctl参数

参数 默认值 推荐值 说明
vm.swappiness 60 10~30 交换倾向(0=尽量不用swap, 100=积极swap)。数据库服务器建议设为10,Redis建议设为1
vm.overcommit_memory 0 视场景 0=启发式(内核估算可用内存); 1=总是允许(不检查,fork友好); 2=严格不超出(swap+ratio*RAM)。Redis建议设1
net.core.somaxconn 128 65535 listen()的全连接队列最大长度。高并发服务器必须调大
net.ipv4.tcp_max_syn_backlog 1024 65535 TCP半连接队列最大长度。防SYN Flood攻击时配合syncookies
fs.file-max 系统相关 ≥655350 系统级文件描述符最大值。配合ulimit -n设置进程级限制
net.ipv4.tcp_tw_reuse 0 1 允许将TIME_WAIT的socket重新用于新的TCP连接(客户端有效)
net.ipv4.tcp_tw_recycle 0 不建议开启 快速回收TIME_WAIT。NAT环境下会导致连接异常,Linux 4.12+已移除
net.ipv4.ip_local_port_range 32768 60999 1024 65535 本地端口范围。高并发客户端需调大
net.ipv4.tcp_keepalive_time 7200 600 TCP keepalive检测间隔(秒)
net.core.netdev_max_backlog 1000 65535 网卡接收队列长度

调优命令

Bash
# 临时生效
sysctl -w vm.swappiness=10

# 永久生效(写入配置文件)
echo "vm.swappiness = 10" >> /etc/sysctl.conf
sysctl -p  # 重新加载

# 文件描述符限制
# 系统级
sysctl -w fs.file-max=655350

# 进程级(修改 /etc/security/limits.conf)
# *    soft    nofile    65535
# *    hard    nofile    65535

# 查看当前进程的限制
ulimit -a
ulimit -n     # 文件描述符限制

高并发Web服务器典型调优

Bash
# /etc/sysctl.conf 推荐配置
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_fin_timeout = 15
vm.swappiness = 10
fs.file-max = 655350

常见面试题汇总(35道含详细答案)

进程与线程

1. 进程 vs 线程 vs 协程的区别?

进程是操作系统资源分配的基本单位,拥有独立的地址空间、文件描述符表、信号处理器等。进程间通信需要IPC机制(管道、共享内存等),创建和切换开销大(需切换页表、刷新TLB等)。

线程是CPU调度的基本单位,同一进程内的线程共享地址空间和文件描述符,但拥有独立的栈、寄存器、线程局部存储。线程切换只需保存/恢复寄存器和栈指针,开销比进程小得多。线程间通信可直接读写共享内存(但需要同步)。

协程是用户态的轻量级线程,由应用程序自己调度,不需要内核参与。协程切换不涉及系统调用,开销极小(仅保存/恢复少量寄存器)。一个线程内可以有成千上万个协程。Go的goroutine、Python的asyncio、Lua的coroutine都是协程实现。协程适合I/O密集型场景,不适合CPU密集型。

2. 进程间通信方式有哪些?各自优缺点?

方式 优点 缺点 适用场景
管道(Pipe) 简单,自动同步 半双工,仅限父子进程 简单的数据流传输
命名管道(FIFO) 不限亲缘关系 半双工,效率一般 不相关进程间通信
消息队列 格式化消息,可异步 有大小限制,拷贝开销 结构化数据传输
共享内存 速度最快(无拷贝) 需自行同步 大量数据共享
信号量 实现同步/互斥 信息量少 进程同步
信号(Signal) 异步通知 信息量极少 事件通知(如SIGKILL)
Socket 可跨网络,双向 开销较大 网络通信/跨机器IPC

3. 线程间如何同步?

线程同步的主要机制:① 互斥锁(Mutex)——保护临界区,同一时刻只有一个线程可以持有。② 信号量(Semaphore)——控制多个线程对有限资源的访问。③ 条件变量(Condition Variable)——线程等待某个条件成立后继续执行,配合mutex使用。④ 读写锁(RWLock)——允许多个读者并发但写者独占。⑤ 屏障(Barrier)——所有线程到达后一起继续。⑥ 原子操作(Atomic/CAS)——无锁方式实现简单的原子更新。选择依据:简单互斥用mutex,等待条件用condition,读多写少用rwlock,简单计数用atomic。

4. 用户态线程 vs 内核态线程?

用户态线程(User-Level Thread)由用户空间的线程库管理,内核不感知,切换快(不经过系统调用),但一个线程阻塞会导致整个进程阻塞(因为内核只看到一个执行单元),且无法利用多核。

内核态线程(Kernel-Level Thread)由内核直接管理和调度,一个线程阻塞不影响其他线程,能充分利用多核,但创建和切换需要系统调用,开销较大。

现代系统多采用混合模型:Go语言的M:N模型(M个goroutine映射到N个内核线程),Linux的NPTL采用1:1模型(每个pthread对应一个内核线程)。

5. fork()的工作原理?子进程复制了什么?

fork()创建调用进程的副本。子进程获得父进程的几乎所有副本:地址空间(代码段、数据段、堆、栈)、打开的文件描述符(共享底层文件表项)、信号处理器、环境变量、当前工作目录等。

现代系统使用写时复制(COW):fork后父子进程共享物理页面(标记为只读),只有当某方试图写入时才实际复制该页面。这使得fork的开销大大降低(只复制页表,不复制物理内存)。

不会复制的:进程ID(子进程获得新PID)、文件锁、pending信号、定时器。

fork返回值:父进程得到子进程PID,子进程得到0,失败返回-1。

6. 僵尸进程和孤儿进程?如何处理?

僵尸进程(Zombie):子进程已终止但父进程还没有调用wait()/waitpid()回收其退出状态。此时子进程的PCB仍然保留(状态Z),占用PID和少量内核资源。大量僵尸进程会耗尽PID资源。

处理方法:① 父进程调用wait/waitpid ② 捕获SIGCHLD信号并在处理函数中调用waitpid ③ 双fork法(父→子→孙,子立即退出,孙变为孤儿被init收养)④ kill父进程使僵尸变为孤儿被init回收。

孤儿进程(Orphan):父进程先于子进程终止,子进程被init进程(PID=1)收养。孤儿进程不是问题——init会自动回收它们的退出状态。

7. 守护进程是什么?如何创建?

守护进程(Daemon)是在后台运行的长期服务进程,与终端脱离,通常在系统启动时启动。如sshd、crond、nginx。

创建步骤:① fork()并让父进程退出(脱离终端)② setsid()创建新会话(成为会话首进程)③ 再次fork()并让父退出(确保不是会话首进程,不会获得控制终端)④ chdir("/")(避免占用可卸载的文件系统)⑤ umask(0)(获得完全的文件权限控制)⑥ 关闭所有文件描述符 ⑦ 重定向stdin/stdout/stderr到/dev/null。现代系统推荐使用systemd管理守护进程。

内存管理

8. 虚拟内存的意义?

虚拟内存的核心价值:① 扩展地址空间——每个进程拥有独立的完整地址空间(32位=4GB,64位=256TB),不受物理内存大小限制。② 内存保护——进程间地址空间隔离,一个进程不能访问另一个进程的内存。③ 简化编程——程序员不需要关心物理内存布局和其他程序的内存使用。④ 高效共享——共享库只需在物理内存中保存一份,映射到多个进程的虚拟地址空间。⑤ 按需加载——程序不需要全部加载到内存,只有访问时才调入。通过页面置换实现"用少量物理内存运行大程序"。

9. 虚拟地址到物理地址的完整转换过程?

以x86-64四级页表为例:虚拟地址48位有效,分为5部分——PML4索引(9bit) + PDPT索引(9bit) + PD索引(9bit) + PT索引(9bit) + 页内偏移(12bit)。

过程:① CR3寄存器指向PML4表基址 → ② 用PML4索引找到PDPT表基址 → ③ 用PDPT索引找到PD表基址 → ④ 用PD索引找到PT表基址 → ⑤ 用PT索引找到物理页帧号 → ⑥ 物理页帧号 + 页内偏移 = 物理地址。

每一级都检查页表项的有效位(Present位),若为0则触发缺页中断(Page Fault),由OS调入该页面或触发段错误。

TLB(Translation Lookaside Buffer)缓存了最近的虚拟→物理映射,命中时直接得到结果,无需访问页表。TLB命中率通常>99%。

10. 页面置换算法比较?

算法 原理 优缺点
OPT 置换将来最久不被使用的页面 最优但不可实现(需预知未来)
FIFO 置换最早进入的页面 简单但有Belady异常(帧数增加缺页率反升)
LRU 置换最久未被访问的页面 接近OPT,但实现开销大(需精确时间戳)
Clock 近似LRU,用引用位+循环指针 实现简单、效果好,Linux实际使用
LFU 置换访问频率最低的页面 对访问模式变化不敏感

Linux实际使用的是改进的Clock算法(基于Active/Inactive链表),结合了LRU的思想和Clock的高效实现。

11. malloc的实现原理?

glibc的malloc实现(ptmalloc2):当请求小块内存时(<128KB),使用brk()系统调用扩展堆顶指针。当请求大块内存时(≥128KB),使用mmap()在进程的虚拟地址空间中建立匿名映射。

内部维护bins数据结构管理空闲块:Fast Bins(小块快速分配,单链表,不合并)、Small Bins(<512B,双链表)、Large Bins(≥512B,按大小排序)、Unsorted Bin(刚释放的块暂存区)。分配策略:优先从对应bin查找,找不到则切割更大的块或向系统申请新内存。free()时将块放入对应bin,相邻空闲块合并防止碎片。多线程环境使用arena(分配区)减少锁竞争。

12. 内存泄漏如何检测?

常用检测方法:① Valgrind(Linux)——运行valgrind --leak-check=full ./program,检测未释放的堆内存,给出泄漏位置和调用栈。② AddressSanitizer(ASan)——编译时加-fsanitize=address,运行时检测泄漏、溢出、UAF等。③ mtrace——glibc内置工具,通过hook malloc/free记录分配和释放。④ 查看/proc/[pid]/status中的VmRSS增长趋势。⑤ jmap/MAT(Java)——分析堆转储找到未回收的大对象。⑥ pmap命令查看进程内存映射变化。系统级可通过监控free命令输出或/proc/meminfo中的可用内存持续下降来发现。

13. OOM Killer机制?

当Linux系统物理内存和交换区都耗尽时,OOM Killer(Out-Of-Memory Killer)被触发。它根据oom_score评分选择并杀死进程以释放内存。评分因素:进程占用的内存量(越大分越高)、进程运行时间(越短分越高)、进程优先级。可通过/proc/[pid]/oom_score_adj调节(-1000到1000,-1000表示永不被杀)。查看日志:dmesg | grep -i "oom"vm.overcommit_memory=2可关闭overcommit从而避免OOM,但fork()等操作可能失败。

文件系统

14. 硬链接与软链接的区别?

硬链接(Hard Link):新的目录项指向同一个inode,与原文件共享inode和数据块。删除原文件后硬链接仍有效(inode引用计数>0)。不能跨文件系统,不能链接目录。

软链接/符号链接(Symbolic Link):创建一个新文件(新inode),内容是目标文件的路径。类似快捷方式。删除原文件后软链接失效(悬空链接)。可以跨文件系统,可以链接目录。

Bash
ln file hardlink        # 硬链接
ln -s file softlink     # 软链接
ls -li                  # 查看inode号,硬链接inode相同

15. inode是什么?

inode(索引节点)是Unix/Linux文件系统中存储文件元数据的数据结构。每个文件对应一个inode,包含:文件大小、所有者(UID/GID)、权限(rwx)、时间戳(创建/修改/访问时间)、硬链接数、数据块指针(直接指针+间接指针+双重/三重间接指针)。inode不包含文件名——文件名存储在目录项中(目录项=文件名→inode号的映射)。每个文件系统有固定数量的inode,用完后即使磁盘有空间也无法创建新文件。

16. 文件描述符是什么?

文件描述符(File Descriptor, fd)是进程内标识已打开文件的非负整数。本质上是进程fd表的索引,fd表的每个条目指向系统级的文件表项(包含偏移量、状态标志),文件表项再指向inode。

每个进程默认有三个fd:0(stdin)、1(stdout)、2(stderr)。open()返回的fd从3开始递增。fd是有限资源,受ulimit -n限制(默认1024),高并发服务器需调大。

I/O

17. select/poll/epoll区别?(重点)

select:① 有最大fd数限制(FD_SETSIZE=1024)② 每次调用需将fd_set从用户态拷贝到内核态 ③ 返回后需遍历整个fd_set找到就绪的fd ④ 时间复杂度O(n)。

poll:① 用链表存储取消了1024限制 ② 仍需每次拷贝、线性遍历 ③ 本质和select区别不大。

epoll:① 用红黑树存储监听的fd,通过epoll_ctl添加/删除(每个fd只需拷贝一次)② 就绪的fd通过回调机制加入就绪链表 ③ epoll_wait只返回就绪的fd,O(1)效率 ④ 支持边缘触发(ET),减少系统调用次数。

总结:活跃连接数少的场景三者差异不大,活跃连接数多但总连接数大时epoll远优于select/poll。Nginx、Redis都使用epoll。

18. 阻塞I/O vs 非阻塞I/O vs 异步I/O?

阻塞I/O:调用read/write时,若数据未就绪则线程挂起(等待+拷贝两阶段全阻塞)。编程简单但浪费线程资源。

非阻塞I/O:若数据未就绪立即返回EAGAIN/EWOULDBLOCK,应用需反复轮询。通常配合I/O多路复用使用,不单独使用。数据拷贝阶段仍然阻塞。

异步I/O:发起I/O后立即返回,内核完成等待和拷贝后通过信号或回调通知应用。两阶段都不阻塞,是真正的异步。Linux的io_uring是成熟的异步I/O实现。

前两者都是"同步I/O"(应用参与数据拷贝),只有异步I/O是真正的"异步"(内核完成数据拷贝)。

19. 零拷贝技术原理?

传统数据传输(read+write)涉及4次数据拷贝、4次上下文切换:磁盘→内核缓冲区(DMA)→用户缓冲区(CPU)→Socket缓冲区(CPU)→网卡(DMA)。

零拷贝技术避免数据在用户态中转:① mmap将内核缓冲区映射到用户空间(减少一次CPU拷贝)② sendfile在内核态直接从文件描述符传输到Socket(减少上下文切换)③ sendfile + DMA gather copy直接从页缓存DMA到网卡(零CPU拷贝)。Kafka的高吞吐依赖sendfile,Nginx的静态文件传输也使用sendfile。Java中通过FileChannel.transferTo()使用。

20. Reactor模式 vs Proactor模式?

Reactor(反应器):I/O多路复用监听事件就绪 → 分发给handler → handler自己完成实际的I/O读写(同步非阻塞)。代表:Nginx、Redis、Netty。

Proactor(前摄器):由OS/框架完成实际I/O操作 → 操作完成后通知handler直接处理数据(异步)。代表:Windows IOCP、Boost.Asio。

Reactor需要用户代码进行实际的read/write,Proactor由系统完成I/O后通知用户。Linux下由于AIO支持不成熟(io_uring改善中),主流Web服务器多用Reactor模式。

调度与同步

21. 常见CPU调度算法?

FCFS(先来先服务)——简单但可能导致"护航效应"(长任务阻塞短任务)。② SJF/SRTF(最短作业/剩余时间优先)——平均等待时间最优,但可能饥饿。③ 优先级调度——灵活但可能优先级反转。④ Round Robin(时间片轮转)——时间片到就切换,公平但时间片大小难定(太大退化为FCFS,太小切换开销大)。⑤ 多级反馈队列(MLFQ)——多个优先级队列,新进程入高优先级队列,用完时间片降级。兼顾响应时间和吞吐量。Linux CFS使用红黑树+虚拟运行时间实现公平调度。

22. 什么是优先级反转?

低优先级线程L持有锁R,高优先级线程H需要锁R被阻塞。此时中优先级线程M就绪可以抢占L的CPU,导致H间接被M阻塞(高等中,荒谬!)。著名案例:1997年火星探路者号因优先级反转导致系统不断重启。

解决:① 优先级继承——H等待时临时将L的优先级提升到H的级别,防止M抢占L。② 优先级天花板——将锁的优先级设为所有可能持有该锁的线程中最高优先级。Linux的rt_mutex实现了优先级继承。

23. 死锁的四个必要条件?如何避免?

四个必要条件:① 互斥条件——资源一次只能被一个进程使用。② 请求与保持——进程持有资源的同时请求新资源并等待。③ 不可剥夺——已获得的资源不能被强行回收。④ 循环等待——存在进程的资源等待环。

预防(破坏条件):破坏②——一次性申请所有资源;破坏③——允许抢占(如数据库中超时回滚);破坏④——资源有序分配(编号,按序申请)。

避免:银行家算法——每次分配前检查是否仍处于安全状态(是否存在安全序列使所有进程都能完成)。

检测与恢复:资源分配图检测环路,发现死锁后终止部分进程或抢占资源。

24. CAS操作原理?ABA问题?

CAS(Compare-And-Swap)是CPU硬件提供的原子指令。语义:比较内存地址的当前值与期望值,若相等则写入新值并返回成功,否则返回失败。应用程序通过自旋(循环重试)实现无锁的原子更新。Java的AtomicInteger.compareAndSet()底层就是CAS。

ABA问题:线程1读到值A,线程2先改为B再改回A。线程1的CAS认为值没变而成功,但实际中间发生了变化。在链表等数据结构中可能导致错误。解决:使用版本号(每次修改版本号+1,CAS同时比较版本号),Java提供AtomicStampedReference

25. 自旋锁 vs 互斥锁?

自旋锁:获取失败时不挂起线程,而是在循环中反复尝试。优点:无线程上下文切换开销。缺点:持续消耗CPU。适用:持锁时间极短(纳秒到微秒级)的多核场景。Linux内核保护短临界区常用spin_lock。

互斥锁:获取失败时线程被挂起(阻塞),直到锁可用时被唤醒。优点:不消耗CPU。缺点:有两次上下文切换(阻塞+唤醒)。适用:持锁时间较长或单核CPU。

选择依据:如果临界区时间 < 两次线程切换时间(通常几微秒),用自旋锁;否则用互斥锁。Linux的mutex实现采用自适应策略:先短暂自旋,自旋失败后再挂起。

综合

26. 用户态与内核态的区别?如何切换?

用户态(Ring 3):只能访问用户空间内存,不能执行特权指令(操控硬件、修改页表等)。普通应用程序运行在用户态。

内核态(Ring 0):可以访问所有内存、执行所有指令。操作系统内核运行在内核态。

切换方式:① 系统调用(用户主动请求内核服务,如read/write/fork)② 异常(如缺页中断、除零错误)③ 外部中断(如时钟中断、I/O完成中断)。切换过程:保存用户态的CPU上下文(寄存器、栈指针、程序计数器)→ 切换到内核栈 → 执行内核代码 → 恢复用户态上下文。开销:通常需要几微秒(寄存器保存/恢复 + TLB刷新)。

27. 系统调用的流程?

以Linux x86-64为例:① 用户程序将系统调用号放入rax寄存器,参数放入rdi, rsi, rdx, r10, r8, r9。② 执行syscall指令,CPU切换到内核态。③ 内核根据系统调用号在sys_call_table中查找对应处理函数。④ 执行内核函数。⑤ 将返回值放入rax,执行sysret返回用户态。

glibc的包装函数隐藏了这些细节,如read()实际调用__NR_read系统调用。现代Linux还使用vDSO机制将部分简单系统调用(gettimeofday)映射到用户空间,直接执行无需切换到内核态。

28. 中断的处理流程?

① CPU在每个指令周期末检查中断信号 → ② 发现中断,通过中断向量表(IDT)找到对应的中断服务程序(ISR) → ③ 保存当前CPU上下文(寄存器、标志位、PC)到内核栈 → ④ 切换到内核态 → ⑤ 执行ISR(上半部),处理紧急任务 → ⑥ 如果有非紧急工作,通过softirq/tasklet/workqueue交给下半部处理 → ⑦ 恢复被中断的上下文 → ⑧ 返回被中断的程序继续执行。

Linux将中断处理分为上半部(硬中断,快速执行关键操作,不可中断)和下半部(软中断/tasklet/工作队列,处理耗时操作,可中断),以缩短关中断时间。

29. CPU上下文切换的过程?开销?

上下文切换指从一个进程/线程切换到另一个。过程:① 保存当前进程的CPU上下文(PC、SP、通用寄存器、浮点寄存器等)到该进程的PCB/TCB。② 更新当前进程状态(运行→就绪/阻塞)。③ 选择下一个进程(调度器决策)。④ 更新新进程状态(就绪→运行)。⑤ 恢复新进程的CPU上下文。⑥ 如果是进程切换(非同进程内的线程切换),还需切换页表(更新CR3)并刷新TLB。

开销:通常几到几十微秒。直接开销包括寄存器保存/恢复、调度算法。间接开销包括TLB失效(进程切换后缓存不命中增加)、CPU缓存冷启动。线程切换比进程切换快(无需切换页表和刷新TLB)。

30. 什么是DMA?

DMA(Direct Memory Access,直接内存访问)允许外设直接与内存之间传输数据,无需CPU逐字节参与。CPU只需设置DMA控制器的参数(源地址、目的地址、传输长度、方向),DMA控制器即可自主完成批量传输,完成后通过中断通知CPU。

DMA解放了CPU,使其在数据传输期间可执行其他计算任务。现代计算机中磁盘I/O、网络I/O、GPU数据传输都依赖DMA。没有DMA的话CPU需要在每字节传输时介入(程序I/O或中断驱动I/O),严重浪费CPU计算能力。

31. 一个程序从编译到运行经历了什么?

预处理(Preprocessing):展开宏定义、处理#include、条件编译。输出.i文件。② 编译(Compilation):将C/C++代码转为汇编代码。进行词法分析、语法分析、语义分析、中间代码生成、代码优化。输出.s文件。③ 汇编(Assembly):将汇编代码转为机器代码。输出目标文件.o(ELF格式,包含代码段、数据段、符号表、重定位表)。④ 链接(Linking):将多个目标文件和库文件合并。解析符号引用(静态链接合并代码,动态链接记录依赖)。输出可执行文件。⑤ 加载(Loading):OS创建进程,分配虚拟地址空间,将可执行文件的各段映射到虚拟内存,设置入口点。⑥ 运行:从_start(C运行时入口)开始执行,初始化后调用main()

32. 什么是写时复制(COW)?

Copy-On-Write是一种延迟复制的优化策略。fork()后父子进程共享同一份物理内存页面,这些页面被标记为只读。当任一进程试图写入某页面时,CPU触发页错误(Protection Fault),内核在错误处理中复制该页面,让两个进程各持有独立的副本,然后将需要写入的页面标记为可写。

优点:① fork极快(只复制页表,不复制物理页)② 如果子进程立即exec(加载新程序),完全不需要复制父进程的内存。应用场景:fork+exec(Shell执行命令)、Redis RDB持久化(fork子进程保存数据时父进程继续处理请求,修改的页面才被复制)。

33. 什么是内存映射文件?

内存映射文件(Memory-mapped file)通过mmap()系统调用将文件内容映射到进程的虚拟地址空间。访问映射区域就像访问内存一样,OS自动处理缺页调入和脏页写回。

优点:① 避免read/write的数据拷贝(直接在内核页缓存上操作)② 多个进程可以映射同一文件实现高效共享内存 ③ 适合大文件的随机访问。典型应用:动态链接库加载(.so/.dll)、数据库的文件I/O(如SQLite、MongoDB的存储引擎)、进程间共享内存(MAP_SHARED)。

34. Linux的进程调度器CFS原理?

CFS(Completely Fair Scheduler,完全公平调度器)是Linux 2.6.23+的默认调度器。核心思想:每个进程维护一个虚拟运行时间(vruntime),vruntime最小的进程获得CPU。

实现:用红黑树存储所有可运行进程,按vruntime排序。调度时选择最左节点(vruntime最小的进程)。进程运行后vruntime增加,增加量与权重(nice值)成反比——高优先级进程的vruntime增长慢(获得更多CPU时间),但所有进程的vruntime最终趋于相等(公平)。

时间复杂度:选择下一个进程O(1)(红黑树最左节点),插入/删除O(log n)。CFS消除了传统调度器的固定时间片概念,用目标延迟(sched_latency)动态计算每个进程应运行的时间。

35. 什么是cgroup?Namespace?(容器基础)

Namespace(命名空间) 提供进程级的资源隔离。Linux支持8种namespace:

Namespace 隔离内容
PID 进程ID(容器内PID 1=容器的init)
Network 网络栈(IP地址、路由表、端口)
Mount 文件系统挂载点
UTS 主机名和域名
IPC 进程间通信(消息队列、信号量)
User 用户和组ID
Cgroup cgroup根目录
Time 系统时钟(Linux 5.6+)

Cgroup(Control Groups) 提供资源限制和统计。可以限制:CPU使用量(cpu.cfs_quota_us)、内存上限(memory.limit_in_bytes)、磁盘I/O带宽(blkio)、网络带宽等。

Docker/Kubernetes底层就是Namespace + Cgroup:Namespace让容器内的进程"以为"自己在独立系统中运行,Cgroup确保容器不会耗尽宿主机资源。


操作系统知识在开发中的应用

数据库

  • B+树索引 ↔ 文件系统:B+树的设计考虑了磁盘I/O特性(节点大小=页大小,减少磁盘读取次数)。数据库页面管理类似OS的页面管理
  • 事务 ↔ 日志文件系统:数据库的WAL(Write-Ahead Logging)与文件系统的日志机制(ext4 journal)原理相同——先写日志再更新数据,崩溃后通过日志恢复
  • Buffer Pool ↔ 页面缓存:数据库的缓冲池管理(LRU、Clock)直接借鉴了OS的页面置换算法
  • MVCC ↔ COW:多版本并发控制的思想与写时复制类似

Web服务器

  • epoll ↔ Nginx:Nginx使用epoll的边缘触发模式,单进程处理数万并发连接
  • 多进程 ↔ Apache:Apache的prefork模型使用fork()创建工作进程,每个进程处理一个请求
  • sendfile ↔ 静态文件:Nginx/Apache使用sendfile()零拷贝传输静态文件
  • Reactor模式:基于I/O多路复用的事件驱动模型,Redis、Node.js、Netty都采用

容器技术

  • Namespace ↔ 隔离:Docker使用Linux Namespace实现进程、网络、文件系统等维度的隔离
  • Cgroup ↔ 资源限制:Docker使用Cgroup限制容器的CPU、内存、I/O等资源使用
  • UnionFS ↔ 镜像分层:Docker镜像的分层存储基于联合文件系统(OverlayFS),类似COW
  • seccomp ↔ 安全:限制容器内可使用的系统调用,减少攻击面

分布式系统

  • CAP ↔ 一致性模型:OS中的缓存一致性协议(MESI)是分布式一致性问题的缩影
  • 选举算法 ↔ 分布式锁:类似OS中的互斥机制,但需要解决网络分区问题
  • 日志复制 ↔ WAL:Raft/Paxos的日志复制机制借鉴了数据库和文件系统的日志思想
  • 分布式调度 ↔ 进程调度:K8s的Pod调度器参考了OS调度器的设计(资源感知、公平性、亲和性)

延伸阅读

  • 📖 《性能之巅》(Systems Performance)—— Brendan Gregg
  • 📖 《深入理解Linux内核》(Understanding the Linux Kernel)
  • 📖 《Linux内核设计与实现》(Linux Kernel Development)
  • 📖 《操作系统概念》(Operating System Concepts)
  • 🔗 Brendan Gregg的Linux性能工具图
  • 🔗 Linux Insides
  • 🔗 kernel.org Documentation
  • 📖 《CSAPP》(Computer Systems: A Programmer's Perspective)