09 - 操作系统实战与面试¶
⏰ 建议学习时间:2 小时 🎯 难度等级:⭐⭐⭐ 📋 前置知识:操作系统全部章节(01-08)
📋 本章目录¶
🎯 学习目标¶
- 掌握Linux性能分析工具链(top、vmstat、iostat、strace、perf)
- 掌握内核参数调优方法
- 系统性准备操作系统面试题(35道含详细答案)
Linux性能分析工具¶
进程级工具¶
top / htop¶
top 是实时进程监控工具,各关键字段含义:
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 | 共享内存大小 |
htop 是 top 的增强版,支持彩色显示、鼠标操作、树形进程视图。
常用交互命令: - P —— 按CPU排序 - M —— 按内存排序 - 1 —— 显示各核CPU使用率 - k —— 杀死进程
ps aux(进程状态)¶
STAT列进程状态:
| 状态码 | 名称 | 含义 |
|---|---|---|
R | Running | 正在运行或在运行队列中 |
S | Sleeping | 可中断睡眠(等待事件完成) |
D | Disk sleep | 不可中断睡眠(通常等待I/O) |
Z | Zombie | 僵尸进程(已终止但父进程未回收) |
T | Stopped | 被信号停止(如SIGSTOP、SIGTSTP) |
t | Tracing stop | 被调试器追踪停止 |
附加标志:s=会话首进程 l=多线程 +=前台进程组 <=高优先级 N=低优先级
/proc/[pid]/ 目录结构¶
/proc/1234/
├── cmdline # 启动命令行
├── cwd -> ... # 当前工作目录(符号链接)
├── environ # 环境变量
├── exe -> ... # 可执行文件路径(符号链接)
├── fd/ # 文件描述符目录(每个fd是符号链接)
├── fdinfo/ # 文件描述符详细信息
├── maps # 内存映射(虚拟地址空间布局)
├── mem # 进程内存(可读写)
├── stat # 进程状态信息(机器可读)
├── status # 进程状态信息(人类可读)
├── task/ # 线程信息
└── io # I/O统计
常用排查命令:
# 查看进程打开的文件数
ls /proc/1234/fd | wc -l
# 查看进程内存映射
cat /proc/1234/maps
# 查看进程的资源限制
cat /proc/1234/limits
系统级工具¶
vmstat(虚拟内存统计)¶
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监控)¶
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(系统活动报告)¶
# 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(内核日志)¶
# 查看内核消息
dmesg | tail -20 # |管道:将前一命令的输出作为后一命令的输入
# 查看OOM Killer日志
dmesg | grep -i "oom" # grep文本搜索:按模式匹配行
# 查看硬件错误
dmesg | grep -i "error"
# 时间戳可读显示
dmesg -T | tail -20
调试工具¶
strace(系统调用追踪)¶
# 追踪进程的系统调用
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(库调用追踪)¶
perf(性能计数器)¶
# 实时查看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 | 网卡接收队列长度 |
调优命令¶
# 临时生效
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服务器典型调优¶
# /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),内容是目标文件的路径。类似快捷方式。删除原文件后软链接失效(悬空链接)。可以跨文件系统,可以链接目录。
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)