Linux-21 面试题汇总
前置阅读:Linux-20 线上问题排查实战案例
本篇是整个系列的收尾。前 20 篇每篇末尾都有对应主题的面试题,这里做跨主题的汇总:先给一张速查表用于突击复习,然后是分难度的问答,最后是综合设计题和答题框架。
面向的是后端开发工程师的面试场景——重点不是背命令参数,而是能否把「原理 → 现象 → 排查手段」串成体系。
1. 三十秒速查表
面试前扫一遍,能立刻想起答案的跳过,想不起来的回去看对应篇目。
1.1 进程与调度
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| 进程和线程的区别 | 内核里都是 task_struct,差别只在 clone() 时共享哪些资源 |
06 |
| 为什么 fork 之后还要 exec | 中间那个窗口用来做重定向、降权、设 namespace | 06 |
| fork 会复制整个内存吗 | 不会,写时复制(COW),只有被修改的页才真复制 | 06 |
| 僵尸进程是什么 | 已退出但父进程没 wait(),占 PID 不占内存 |
06 |
| 僵尸怎么清理 | 杀父进程(被 init 收养后自动回收),kill 僵尸本身无效 |
06 |
| D 状态为什么 kill -9 杀不掉 | 内核已把 DMA 地址交给硬件,此阶段不响应任何信号 | 06 |
| CFS 怎么实现公平 | vruntime 最小的先跑,权重越高 vruntime 涨得越慢 | 07 |
| 进程切换比线程切换贵在哪 | 多了切页表,导致 TLB 和 cache 失效(隐性成本) | 07 |
| 自愿 vs 非自愿上下文切换 | 自愿=等资源(IO/锁),非自愿=被抢占(CPU 不够) | 07 |
| load 高但 CPU 空闲 | Linux 的 load 算 R+D,D 状态等 IO 也计入 |
07 |
1.2 内存
| 问题 | 一句话答案 | 详见 |
|---|---|---|
free 只剩 200MB 是否快 OOM |
不是,要看 available(含可回收的 cache) |
10 |
| VSZ 和 RSS 的区别 | VSZ 是虚拟地址空间,RSS 是常驻物理内存 | 10 |
| 为什么所有进程 RSS 加起来超过物理内存 | 共享页被重复计算,要看 Pss |
10 |
| swappiness=60 是什么意思 | 回收时换出匿名页 vs 丢 cache 的权重,不是「用到 60% 才 swap」 | 10 |
| K8s 为什么要关 swap | swap 让 cgroup 内存限制失效,OOM 行为不可预测 | 10 |
write() 返回成功数据落盘了吗 |
没有,只到 page cache,要 fsync 才保证持久化 |
10 |
| 主要缺页 vs 次要缺页 | major 要读磁盘(50μs~10ms),minor 只是建映射(~1μs) | 10 |
| 怎么确认进程被 OOM Killer 杀了 | dmesg -T | grep -i "killed process",应用日志必然是空的 |
10 |
| 为什么数据库要关 THP | 内存规整会造成不可预测的延迟毛刺 | 10 |
1.3 文件与 IO
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| 硬链接和软链接的区别 | 硬链接共享 inode,软链接是存路径字符串的独立文件 | 02 |
| 硬链接为什么不能跨文件系统 | inode 号只在单个文件系统内唯一 | 02 |
rm 之后空间一定释放吗 |
不一定,还要「链接数为 0 且无进程持有」 | 02 |
df 和 du 对不上 |
已删除但被进程持有(lsof +L1),或被挂载点遮盖 |
11 |
| 磁盘有空间但创建文件失败 | inode 耗尽,df -i 确认 |
02 |
| atime/mtime/ctime 的区别 | mtime 管内容,ctime 管元数据,atime 管读取 | 02 |
%util 100% 是否磁盘满载 |
SSD 上不是,要看 await 和 aqu-sz |
11 |
| ext4 和 xfs 怎么选 | xfs 动态 inode、并发写更好但不能缩容;ext4 能缩容 | 11 |
| 怎么原子替换一个文件 | 写临时文件 + rename()(同文件系统内原子) |
02 |
ulimit -n 改了没用 |
看 /proc/PID/limits;systemd 服务要配 LimitNOFILE |
11 |
1.4 IO 模型与网络
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| 五种 IO 模型的区别 | 差别全在「等数据」和「拷贝数据」两阶段是否阻塞 | 12 |
| 为什么「非阻塞 IO」也是同步的 | 阶段二(内核拷到用户空间)仍然阻塞 | 12 |
| epoll 为什么比 select 快 | fd 常驻内核(红黑树)+ 回调挂就绪链表,开销与活跃数相关 | 12 |
| ET 和 LT 的区别 | ET 只在状态变化时通知一次,必须循环读到 EAGAIN |
12 |
| ET 为什么必须用非阻塞 fd | 否则最后一次 read 会挂住整个事件循环 | 12 |
| 惊群问题怎么解决 | 现代方案是 SO_REUSEPORT(每 worker 独立队列) |
12 |
| Go 为什么能用阻塞写法支撑十万连接 | runtime 底层用 epoll,阻塞的是 goroutine 不是 OS 线程 | 12 |
CLOSE_WAIT 堆积说明什么 |
应用漏了 close(),纯代码 bug,调内核参数无效 |
13 |
TIME_WAIT 堆积怎么处理 |
根治是用连接池减少建连;tcp_tw_reuse 是辅助 |
13 |
refused 和 timeout 的区别 |
refused=到了但没监听;timeout=包被丢弃(防火墙) | 13 |
| 小包正常大包超时 | MTU 不匹配导致 PMTU 黑洞 | 13 |
1.5 系统调用与内核
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| 为什么要分用户态和内核态 | 保护——不能让应用直接碰硬件和任意内存 | 09 |
| 一次系统调用多贵 | 100~200ns,是普通函数调用的 100 倍 | 09 |
malloc 是系统调用吗 |
不是,用户态内存池,只在池子不够时才调 brk/mmap |
09 |
| vDSO 是什么 | 内核映射到进程地址空间的代码,让取时间不用陷入内核 | 09 |
sendfile 零拷贝省了什么 |
省掉两次内核↔用户态的内存拷贝和两次上下文切换 | 09 |
strace 为什么慢 |
ptrace 让每次系统调用停两次,慢 10~100 倍 |
09 |
1.6 信号与权限
| 问题 | 一句话答案 | 详见 |
|---|---|---|
kill -9 和 kill -15 的区别 |
15 可捕获能优雅关闭,9 不可捕获会丢数据 | 08 |
| 哪些信号不能捕获 | 只有 SIGKILL(9) 和 SIGSTOP(19) |
08 |
| 可靠信号和不可靠信号 | 1~31 不排队(会合并),32~64 排队 | 08 |
| 信号处理函数里为什么不能 printf | 异步插入执行,会破坏 stdio 的锁和 malloc 的堆状态 | 08 |
SIGPIPE 为什么危险 |
默认动作是终止进程,一个客户端断连就能杀死服务 | 08 |
| K8s 滚动更新为什么有 502 | 摘流量和发 SIGTERM 并行,endpoints 传播需要时间 | 08 |
| 容器为什么收不到 SIGTERM | PID 1 不执行默认动作;shell 形式的 CMD 让 sh 成了 PID 1 | 08 |
| 只读文件为什么能删掉 | 删除改的是目录内容,看的是目录的 w 权限 |
03 |
目录的 x 权限是什么 |
「通行权」——能穿过它访问里面的东西 | 03 |
| SUID 是什么 | 以文件属主身份运行,passwd 靠它写 /etc/shadow |
03 |
| 容器里挂载卷权限报错 | 容器 uid 就是宿主机 uid,比对数字而非用户名 | 03 |
1.7 容器与运维
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| 容器和虚拟机的本质区别 | 容器共享宿主机内核,就是被 namespace+cgroup 限制的普通进程 | 19 |
容器里 nproc 为什么是宿主机核数 |
/proc 大部分内容没被 namespace 隔离,要读 cgroup |
19 |
cpu.max 和 cpu.weight 的区别 |
前者硬上限(会限流),后者只在争抢时生效 | 19 |
| 容器 CPU 不高但延迟有尖刺 | 查 cpu.stat 的 nr_throttled,通常是并发度没按配额设 |
19 |
| 怎么进入容器排查网络 | nsenter -t PID -n,用宿主机的工具 |
19 |
| systemd 比 SysV 好在哪 | 用 cgroup 精确跟踪服务,而非靠 pid 文件猜 | 14 |
After= 和 Requires= 的区别 |
前者管顺序,后者管依赖,两个维度独立 | 14 |
| 为什么脚本在 cron 里失败 | cron 的 PATH 极简,不读 .bashrc |
15 |
| logrotate 后应用还在写旧文件 | 重命名不改 inode,要发信号让应用重新 open() |
15 |
| 磁盘满怎么应急 | truncate -s 0 而不是 rm |
15 |
2. 分难度问答
2.1 初级(必须答对)
这一层考的是「有没有实际用过 Linux」。答不上来会直接被判定为缺乏实操经验。
Q:ps aux 和 ps -ef 有什么区别?
两套历史遗留的语法:aux 是 BSD 风格(选项不加横杠),-ef 是 UNIX 风格。输出的列不同——aux 有 %CPU/%MEM,-ef 有 PPID。记住「aux 看资源、-ef 看父子关系」就够用。实际排查更常用 ps -eo pid,rss,pcpu,stat,args --sort=-rss,自定义列 + 排序。注意 COMMAND 列会被终端宽度截断,要看完整命令用 ps auxww 或 cat /proc/PID/cmdline | tr '\0' ' '。
Q:怎么查看端口被哪个进程占用?
ss -tlnp | grep :8080 # 推荐,-n 必须加否则要做 DNS 解析很慢
lsof -i :8080 # 备选
netstat -tlnp | grep :8080 # net-tools 已停止维护,很多系统没装
ss 比 netstat 快得多——netstat 逐行解析 /proc/net/tcp,连接多时极慢;ss 通过 netlink 从内核批量取数据。
Q:chmod 755 和 chmod +x 有什么区别?
755 是绝对赋值,一次性把权限设为 rwxr-xr-x,会覆盖原有的所有权限位;+x 是增量修改,只给三类身份都加执行位,其他位不变。递归场景下有个重要区别:chmod -R 755 dir/ 会把所有 .txt 也变成可执行,而 chmod -R u+rwX,go+rX dir/ 的大写 X 只给目录和已有执行位的文件加 x,不会误伤普通文件。
Q:软链接和硬链接怎么创建?有什么区别?
ln -s target linkname # 软链接
ln target linkname # 硬链接
硬链接是给同一个 inode 加一个名字,ls -li 能看到两者 inode 号相同、链接数都是 2;软链接是独立的新文件,内容是目标路径字符串(所以它的大小 = 路径字符数),文件类型是 l。硬链接不能跨文件系统(inode 号只在单个文件系统内唯一)、不能指向目录(会形成环);软链接两者都可以,还能指向不存在的目标(悬空链接)。
Q:怎么查看磁盘和内存使用情况?
df -h # 磁盘空间
df -i # 磁盘 inode <- 容易忘,海量小文件时是这个满了
free -h # 内存,看 available 不看 free
du -xh --max-depth=1 /var | sort -rh | head # 找大目录,-x 不跨文件系统
Q:怎么实时查看日志?
tail -f app.log
tail -F app.log # 大写 F:文件被轮转后自动重新打开 <- 更实用
tail -f app.log | grep ERROR
journalctl -u myapp -f # systemd 服务
tail -F 等价于 --follow=name --retry,logrotate 切割日志后它会自动跟上新文件,而 -f 会一直盯着旧的 inode。
Q:> 和 >> 的区别?2>&1 是什么意思?
> 覆盖写入(会先截断文件),>> 追加。2>&1 表示「把 fd 2(stderr)指向 fd 1 当前指向的地方」。位置很关键:cmd > f 2>&1 两者都进文件;cmd 2>&1 > f 只有 stdout 进文件、stderr 仍在终端——因为重定向从左到右执行,2>&1 复制的是当时 fd 1 的指向。
Q:怎么在文件里查找内容?
grep -rn "keyword" /path # 递归 + 行号
grep -rn --include="*.go" "TODO" . # 限定文件类型
grep -C 5 "ERROR" app.log # 显示上下文 <- 看日志必用
grep -c "ERROR" app.log # 只计数
grep -v "DEBUG" app.log # 反选
Q:怎么让程序在后台运行且不受终端断开影响?
nohup ./app > app.log 2>&1 & # 忽略 SIGHUP
setsid ./app > app.log 2>&1 & # 创建新会话,更彻底
tmux new -s work # 推荐:可以重新连回去看输出
终端断开时内核给会话内所有进程发 SIGHUP,默认动作是终止。nohup 让进程忽略这个信号,setsid 让进程脱离原会话(PPID 变 1)。长期服务应该用 systemd 而不是 nohup——有自动重启、日志管理、资源限制。
2.2 中级(区分度最高)
这一层考的是「有没有排查过线上问题」,也是面试中最能分出层次的部分。
Q:一台机器上服务变慢了,你怎么排查?
按 60 秒定位法,先用零开销的计数器型工具收敛范围:
uptime # ① 负载趋势,三个数字的变化方向比绝对值重要
dmesg -T | tail -20 # ② 先排除 OOM、磁盘错误、网卡重置这类硬故障
vmstat 1 5 # ③ 全局定位
vmstat 的判读顺序:r 列 vs 核数(CPU 排队)→ b 列(IO 阻塞)→ si/so(换页)→ wa(IO 等待)→ st(虚拟化偷取)→ us vs sy(用户态还是内核态)。
然后按方向深入:mpstat -P ALL 1 看核间均衡(能发现 top 看不出的单核软中断瓶颈)→ pidstat -urdw 定位进程 → iostat -xz/free -h 看具体资源 → 最后才用 perf/strace 这类有开销的追踪工具。核心原则是分层排除、每一步都有明确判据,不要跳层猜。
Q:CPU 使用率 100%,怎么定位到具体代码?
第一步是区分 %usr 和 %system(pidstat -u -p PID),这决定后面的方向:
%usr 高 -> 业务代码在计算 -> 用 profiler
Java: jstack + 把 LWP 转十六进制找 nid
Go: pprof CPU profile
通用: perf top / 火焰图
%system 高 -> 系统调用或上下文切换 -> pprof 帮不上忙(它看不到内核态)
用 strace -c -f 看系统调用分布
常见元凶:日志无缓冲(每条一次 write)、小块 IO 没批量化
然后 ps -Lo pid,lwp,pcpu -p PID --sort=-pcpu 定位到线程。如果多个线程的栈几乎一致,说明它们卡在同一段代码上,这是最容易定位的情况。
Q:内存持续增长,怎么判断是不是泄漏、泄漏在哪?
# ① 确认是真实占用还是缓存假象
grep -E "^(Rss|Pss|Private_Dirty):" /proc/PID/smaps_rollup
# Private_Dirty 占比高 = 真实的进程独占内存
# ② 对比运行时堆大小与总占用
# Go: curl localhost:6060/debug/pprof/heap?debug=1 看 HeapAlloc vs Sys
# Java: jcmd PID VM.native_memory summary
关键判断:如果运行时报告的堆没涨而 RSS 在涨,泄漏就在堆外。Go 的四个方向:HeapAlloc 同步涨是堆内对象泄漏;StackSys 大 + NumGoroutine 大是goroutine 泄漏;Sys 大但堆栈都不大是CGO/mmap 泄漏。Java 则要用 NMT 拆解——Thread 大是线程数失控,Internal 大通常是 DirectByteBuffer。
Q:load average 20 但 CPU 使用率只有 5%,什么原因?
因为 Linux 的 load 统计 R(可运行)和 D(不可中断睡眠)两种状态,而其他 Unix 只算 R。所以一堆进程卡在 D 状态等磁盘 IO 时,load 很高但不消耗 CPU。确认方法:vmstat 的 b 列(阻塞进程数)和 wa 列同时高,load ≈ r + b。再用 ps -eo pid,stat,wchan | awk '$2 ~ /^D/' 看卡在哪个内核函数——io_schedule 是本地磁盘,nfs_* 是网络存储不通。这种情况加 CPU 完全无效,要解决的是磁盘。
Q:怎么判断磁盘是不是瓶颈?
不能只看 %util。%util 的定义是「至少有一个请求在处理的时间占比」,源自机械盘单磁头时代;NVMe 有几十上千个并行队列,100% 可能只用了 5% 的能力。真饱和要三个条件同时成立:
① await 超过设备基准的 3~5 倍 (NVMe < 1ms,SATA SSD < 5ms,云盘 < 10ms)
② aqu-sz > 1 且持续增长 (请求在排队)
③ IOPS 或吞吐接近设备标称上限
再用 rareq-sz/wareq-sz(平均请求大小)判断负载类型:4KB 左右是随机小 IO(瓶颈是 IOPS),1MB 左右是顺序大 IO(瓶颈是带宽)。定位到进程用 pidstat -d 或 iotop -oPa。
Q:CLOSE_WAIT 和 TIME_WAIT 大量堆积,分别怎么处理?
完全不同的两回事,千万不要混。
CLOSE_WAIT 出现在被动关闭方:对端已发 FIN,内核在等本地应用调用 close()。堆积纯粹是应用 bug,最常见是错误路径提前 return 漏了关闭资源(Go 里忘了 defer resp.Body.Close())。调任何内核参数都无效——CLOSE_WAIT 没有超时机制。
TIME_WAIT 出现在主动关闭方,是 TCP 协议要求的正常状态(等 2MSL,Linux 上 60 秒),作用是确保最后的 ACK 到达 + 让旧连接残包消散。堆积的根治方向是减少连接创建(用连接池/长连接),内核参数只是辅助:tcp_tw_reuse=1、扩大 ip_local_port_range。绝对不要用 tcp_tw_recycle——它在 NAT 环境下会导致随机连接失败,4.12 已彻底移除。
Q:服务报 Too many open files,怎么排查?
# ① 确认限制是否真的生效 —— 只信 /proc/PID/limits
cat /proc/PID/limits | grep -i "open files"
# ulimit -n 只反映当前 shell,与服务进程无关
# systemd 服务不读 limits.conf(那是 PAM 机制),要配 LimitNOFILE
# ② 看 fd 类型分布 —— 直接指向问题
ls -l /proc/PID/fd | awk '{print $NF}' | sed 's/:\[.*//' | sort | uniq -c | sort -rn
# socket 多 -> 连接泄漏,继续查 ss -tan 的状态分布
# 普通文件多 -> 文件句柄泄漏
如果 socket 多且状态是 CLOSE_WAIT,就是应用漏 close();如果是 ESTAB 且都指向同一个下游,可能是连接池配置过大或没复用。
Q:df 显示磁盘满了,但 du 加起来对不上,为什么?
三种原因,按可能性排序:① 已删除但仍被进程持有(最常见)——rm 只删目录项,进程还持有 fd 时 inode 和数据块不释放,所以 du 数不到而 df 仍算,用 lsof +L1 找 NLINK=0 的文件;② 被挂载点遮盖的文件——先往某目录写了文件,后来挂载了新盘到该目录,用 mount --bind / /mnt/x 后 du 排查;③ ext4 的预留块(默认给 root 留 5%,正常现象,可用 tune2fs -m 1 调低)。应急处理用 truncate -s 0 而不是 rm——保留 inode,空间立即释放且不需要重启进程。
Q:怎么让一个服务优雅关闭?K8s 里为什么滚动更新会有 502?
优雅关闭的三步顺序不能错:① 先停止接收新流量 → ② 处理完存量请求 → ③ 最后关闭依赖。应用侧收到 SIGTERM 后先让 readiness 探针返回 503,sleep 几秒等流量摘除传播,然后 srv.Shutdown(ctx) 等存量请求完成,最后按依赖反序关闭 DB、消息消费者、trace。
502 的原因是摘流量和发 SIGTERM 是并行发生的:Pod 标记 Terminating 后,Endpoint Controller 开始移除 endpoints,同时 kubelet 发 SIGTERM。但 endpoints 变更传播到所有节点的 kube-proxy/ingress 需要几百毫秒到几秒,这期间到达的请求会被拒绝。解法是 preStop 里 sleep 5(应用此时仍正常服务)。时间预算必须自洽:preStop + Shutdown 超时 + 关资源 < terminationGracePeriodSeconds。验证看退出码——0 是优雅退出,137(128+9)说明被 SIGKILL 打断了。
Q:写一个 Shell 脚本要注意什么?
必须以 set -euo pipefail 开头:-e 命令失败立即退出(避免 cd 失败后继续 rm -rf *)、-u 引用未定义变量报错(防住「变量拼错 → rm -rf $DIR/data 变成 rm -rf /data」)、-o pipefail 管道任一环失败整体算失败(否则 mysqldump | gzip 里 dump 失败会被当成成功,得到空备份)。
但要知道 set -e 的失效边界:在 if/while 条件位置、&&/|| 链的非末环、以及函数在条件上下文中被调用时函数内部的 -e 也失效——所以关键操作要显式 if ! cmd; then exit 1; fi。其他要点:变量引用永远加双引号、转发参数用 "$@" 不用 $*、用 [[ ]] 不用 [ ]、不要 for f in $(ls)、trap cleanup EXIT 保证清理、flock 防重复执行、shellcheck 进 CI。
2.3 高级(深度题)
这一层考的是对内核机制的理解深度,以及能否把多个知识点串联起来。
Q:详细说一下 epoll 的实现原理,以及它为什么比 select/poll 高效。
select 的三个问题:fd 上限 1024(FD_SETSIZE 是编译期常量)、每次调用要把整个 fd 集合从用户态拷进内核、返回后要 O(n) 遍历找就绪的 fd。poll 用数组替代位图去掉了数量限制、events/revents 分离免去重复填充,但拷贝和遍历两个核心问题没解决。
epoll 内核里有两个关键结构:
struct eventpoll {
struct rb_root_cached rbr; // 红黑树:所有被监听的 fd
struct list_head rdllist; // 双向链表:已就绪的 fd
wait_queue_head_t wq; // 阻塞在 epoll_wait 的进程
};
工作流程:epoll_ctl(ADD) 把 fd 插入红黑树(O(log n))并给它注册一个回调 ep_poll_callback;网卡数据到达、协议栈处理完后唤醒等待队列,触发回调,回调把这个 fd 直接挂进就绪链表;epoll_wait 只需检查就绪链表空不空,非空就把就绪 fd 拷给用户。
高效的本质是三点缺一不可:fd 集合常驻内核(不用每次拷贝)+ 回调机制(内核不需要遍历所有 fd 去检查状态)+ 就绪链表(用户不用遍历)。所以 epoll 的开销只与活跃连接数相关,与总连接数无关——十万长连接里同时活跃只有几百个时,select 要检查 10 万个而 epoll 只处理 500 个。这恰好契合推送、IM、网关这类场景。
epoll 不是永远最优:连接数少(< 100)且几乎全部活跃时,epoll_ctl 的额外系统调用开销可能让它比 select 更慢。
Q:说一下 Linux 的内存管理,从虚拟地址到物理页的完整路径。
进程看到的所有地址都是虚拟的。虚拟内存带来四个好处:隔离(越界访问触发 SIGSEGV)、超额分配(可以「分配」比物理内存更多的空间)、共享(共享库、COW)、连续假象。
地址翻译:x86_64 用 4 级页表(5.x 支持 5 级),48 位虚拟地址被切成 PGD|PUD|PMD|PTE|页内偏移,每级查一次内存——一次翻译最多访问 4 次内存,开销无法接受。所以 CPU 有 TLB 缓存「虚拟页→物理页」的映射:命中约 1 个时钟周期,未命中要走页表 100~300ns。这就是进程上下文切换比线程贵的根本原因——切页表(写 CR3)导致 TLB 大面积失效。
物理页的分配是懒惰的:malloc(1GB) 时内核只在 vm_area_struct 里记一笔,VSZ 增加 1GB 但 RSS 增加 0;进程首次写入某地址时 MMU 发现页表无映射,触发缺页中断,内核才分配物理页。缺页分两种:次要缺页(页已在内存,只是没建映射,约 1μs)和主要缺页(要从磁盘读,SSD 50~150μs,机械盘 5~10ms)。pidstat -r 的 majflt/s 持续大于 0 是危险信号。
大页(Huge Page) 把页大小提到 2MB/1GB,同样的 TLB 条目能覆盖 500 倍内存。但透明大页(THP)对数据库有害——内核后台做内存规整时会产生几百毫秒的延迟毛刺,所以 MongoDB、Redis、PostgreSQL 都要求关闭。注意区分 THP(自动、有害)和静态预留的 HugePages(手动、对数据库有益)。
Q:一个 HTTP 请求从网卡到应用,内核做了什么?
① 网卡收到数据帧,通过 DMA 写入 ring buffer(不经过 CPU)
② 网卡发起硬中断,CPU 执行中断处理程序(极简,只做标记)
③ 触发软中断 NET_RX,ksoftirqd 在软中断上下文里处理协议栈
-> 从 ring buffer 取出 skb
-> 逐层剥离:链路层 -> IP 层(校验、路由)-> TCP 层
④ TCP 层按四元组查找对应的 socket
-> 校验序列号、处理 ACK、更新滑动窗口
-> 数据放入该 socket 的接收缓冲区
⑤ 唤醒等待在这个 socket 上的进程
-> 如果用 epoll:触发 ep_poll_callback,把 fd 挂进就绪链表
-> epoll_wait 返回,应用得到通知
⑥ 应用调用 read(),陷入内核,copy_to_user() 把数据从
socket 缓冲区拷到用户 buffer
几个可优化的点:③ 的软中断如果集中在一个 CPU 上会成为瓶颈(mpstat -P ALL 看 %soft,用 RPS/RSS 分散);⑥ 的拷贝可以用 sendfile/splice 零拷贝省掉;每个 IO 一次系统调用的开销可以用 io_uring 批量提交摊薄。
Q:容器的隔离机制是怎么实现的?有哪些不彻底的地方?
容器 = 普通进程 + namespace(视图隔离)+ cgroup(资源限制)+ pivot_root(文件系统隔离)。八种 namespace 里,PID/NET/MNT/UTS/IPC 是容器默认开启的,USER namespace 默认不开——这意味着容器内的 uid 0 就是宿主机的 uid 0,逃逸后直接是 root。
不彻底的地方主要有三类:
① /proc 大部分内容没被隔离——/proc/cpuinfo、/proc/meminfo、/proc/loadavg、/proc/stat 读到的都是宿主机数据。后果是 JVM 按宿主机内存算堆必然 OOM、Go 的 GOMAXPROCS 取宿主机核数导致大量 CPU 限流。只有 /proc/net/*(NET namespace)和 /proc/<pid>/*(PID namespace)是准的。
② 内核参数大多是全局的——容器里改不了 net.ipv4.tcp_* 之类的 sysctl(少数被 namespace 化的除外)。
③ 共享内核带来的攻击面——内核漏洞可能导致逃逸,隔离性弱于虚拟机;一个容器触发 kernel panic 会拖垮整机。
缓解手段:runAsNonRoot: true + capabilities.drop: ALL + readOnlyRootFilesystem + seccomp(收益/成本比远好于开 user namespace);宿主机部署 lxcfs 让 /proc 显示 cgroup 数据;需要强隔离用 gVisor/Kata Containers(用户态内核或轻量虚拟机)。
Q:说一下 cgroup v2 相比 v1 的改进,以及 PSI 的价值。
v1 是每个控制器一个独立层级,同一进程在不同控制器下可属于不同 cgroup,导致「这组进程用了多少资源」难以回答。更实际的问题是内存和 IO 无法协同——脏页回写发生在内核线程里,v1 无法把这部分 IO 归属到产生它的 cgroup,所以 buffered write 的限流基本无效。v2 改为单一统一层级,一个进程只属于一个 cgroup,内存和 IO 能协同。v2 特有的坑是 cgroup.subtree_control——父 cgroup 必须先写 +cpu +memory 授权,否则子 cgroup 里不会出现 cpu.max。
PSI(Pressure Stall Information) 是 v2 新增的指标,直接量化因资源不足损失的时间比例:some 是至少一个任务在等的时间占比,full 是所有任务都在等的占比。相比 load average 的优势在于语义明确——load 只是「R+D 进程数」,无法区分「8 个进程但 CPU 够用」和「8 个进程激烈争抢」;而 cpu.pressure 的 some avg10=12.34 明确说明最近 10 秒有 12.34% 的时间至少有一个任务在等 CPU。full > 0 意味着整个 cgroup 完全停滞过,是严重信号。这是判断是否需要扩容最准确的指标。
Q:为什么容器里 CPU 使用率不高但延迟有周期性尖刺?
CPU 限流(throttling)。查 /sys/fs/cgroup/cpu.stat 的 nr_throttled / nr_periods 比例。机制是:
容器 limits.cpu=2 -> 每 100ms 周期给 200ms 配额
应用有 16 个线程(GOMAXPROCS 取了宿主机核数)
v
16 个线程并行,在前 12.5ms 就把 200ms 配额耗尽
v
剩下 87.5ms 被强制暂停,完全不处理请求 -> P99 尖刺
v
平均 CPU 使用率算下来很低 -> 监控看不出问题
根因是运行时并发度与 cgroup 配额不匹配。修复:Go 引入 go.uber.org/automaxprocs、Java 加 -XX:+UseContainerSupport(JDK 10+ 默认)、或用 Downward API 注入 GOMAXPROCS=limits.cpu。
另一个角度的修复是放宽 limits.cpu——CPU 是可压缩资源(不够就变慢),内存是不可压缩资源(不够只能 OOM)。所以延迟敏感的在线服务应该 requests.cpu 设准、limits.cpu 设宽松或不设,而 limits.memory 必须设。
Q:strace、perf、eBPF 的原理差异是什么?为什么 eBPF 开销小得多?
strace 基于 ptrace:内核在目标进程每次进入和退出系统调用时停下它,唤醒 tracer 读取寄存器和参数,处理完再恢复。每次系统调用额外增加两次上下文切换和两次进程停止/恢复,开销 10~100 倍——只能短时用、必须配 timeout 和 -c。
perf 基于采样:默认按 CPU 周期(PMU 硬件计数器)定期抓调用栈,开销通常 < 5%。它能看到内核态函数,这是相对语言级 profiler 的关键优势——%system 高时 pprof 完全无能为力。
eBPF 在内核内挂钩子并直接聚合,这是它开销最小的根本原因:strace 要把每个事件传到用户态再统计,eBPF 直接在内核里累加计数器和直方图,只把最终结果传出来,数据量差几个数量级。而且它能观测系统调用 + 内核函数 + 用户态函数 + 硬件事件,还能用 hist() 在内核里生成延迟直方图——这点很重要,因为平均值会掩盖长尾,「平均 1ms」可能是「99% 是 0.1ms,1% 是 100ms」,后者才是 P99 问题的根源。
Q:火焰图怎么看?on-CPU 和 off-CPU 火焰图的区别?
火焰图的横轴不是时间,而是把采样到的调用栈按函数名排序后堆叠,宽度 = 该函数在采样中出现的比例 = 占 CPU 的比例;纵轴是调用栈深度。所以要看宽的不看高的——栈深只说明调用层次多,宽的平顶(plateau)才是 CPU 实际消耗的地方。
on-CPU 火焰图只能看到「CPU 在做什么」,看不到「进程在等什么」。很多性能问题是等待而非计算——等锁、等 IO、等网络。这需要 off-CPU 火焰图(用 offcputime 采集进程离开 CPU 的时间和栈)。**规律:CPU 使用率高看 on-CPU,延迟高但 CPU 不高看 off-CPU。**两张图合起来才是完整的性能画像。
常见问题是出现大量 [unknown]:原因是二进制被 strip 或缺 frame pointer。C/C++ 加 -fno-omit-frame-pointer -g,Go 不要用 -ldflags="-s -w",Java 用 async-profiler,Python 用 py-spy。
3. 综合设计题
这类题考的是能否把知识串成体系。答题时先讲思路和分层,再落到具体命令和参数,不要一上来就报菜名。
3.1 「线上服务突然变慢,你怎么排查?」
回答框架:先分层定位资源 → 再定位进程 → 再定位代码 → 每一步说清判据。
第一层:确认现象与范围(30 秒)
· 是全部接口还是某个接口?-> 决定是系统问题还是业务逻辑问题
· 是所有实例还是单个实例?-> 单实例通常是资源/宿主机问题
· 什么时候开始?有没有发布/配置变更?-> 先看时间相关性
第二层:定位到资源(60 秒定位法)
uptime 负载趋势,三个数字的变化方向
dmesg -T | tail 先排除 OOM、磁盘错误、网卡重置
vmstat 1 5 按 r -> b -> si/so -> wa -> st -> us/sy 顺序判读
mpstat -P ALL 1 核间均衡(发现单核软中断瓶颈)
判据:
r > 核数 -> CPU 不足
b > 0 且 wa 高 -> 磁盘 IO
si/so ≠ 0 -> 内存不足在换页
st > 5% -> 云主机被超卖
sy >> us -> 系统调用/上下文切换异常
第三层:定位到进程
pidstat -urdw 1 CPU/内存/磁盘/上下文切换四个维度
ss -s 连接状态分布
第四层:定位到代码
%usr 高 -> jstack / pprof / perf top / 火焰图
%system 高 -> strace -c -f 看系统调用分布
自愿切换高 -> strace 看是否 futex(锁竞争)
容器环境 -> 先查 cpu.stat 的 nr_throttled
必须补充的三个「容易被漏掉的方向」(说出这些能明显区分层次):
① 容器 CPU 限流 —— 使用率不高但延迟有尖刺,查 nr_throttled
② page cache 命中率骤降 —— 有批处理任务冲掉了缓存,查 cachestat
③ 下游依赖变慢 —— 本机资源全正常,用 curl -w 分解各阶段耗时
收尾时主动引出一个深度点:「如果这几层都正常,我会怀疑是锁竞争——用 pidstat -w 看自愿上下文切换是否异常高,然后 strace -c 确认 futex 占比。这类问题的特征是 CPU 和 IO 都不高但吞吐上不去。」
3.2 「设计一个日志采集方案,要求不丢日志且不影响业务」
回答框架:写入路径 → 采集路径 → 可靠性 → 资源隔离 → 容量规划。
① 应用写入侧
· 【必须】用户态缓冲(bufio / logback 的 AsyncAppender)
否则每条日志一次 write 系统调用 —— 第 9 篇的实战案例里
400 万次 write/秒 吃掉了 77% 的 CPU
· 异步写 + 有界队列 + 队列满时的降级策略(丢弃 DEBUG 而不是阻塞业务)
· 结构化输出(JSON),避免采集端做复杂正则解析
② 日志轮转
· 优先让应用自己管(logback 的 SizeAndTimeBasedRollingPolicy + totalSizeCap)
· 用外部 logrotate 时必须配 postrotate 发信号让应用重开文件
+ delaycompress(第 15 篇);能发信号就不要用 copytruncate(会丢日志)
· 容器环境更简单:直接写 stdout,交给容器运行时
③ 采集侧
· Agent 用 DaemonSet 部署,读取容器的 stdout 日志文件
· 【关键】记录 offset 并持久化,Agent 重启后从断点续传
· 处理 inode 变化:文件被轮转后要能识别是新文件而非同一文件
④ 可靠性
· 至少一次投递 + 下游幂等(用 trace_id 去重)
· 本地磁盘缓冲:下游不可用时先落盘,恢复后补发
· 背压:下游慢时 Agent 减速而不是无限堆积内存
⑤ 资源隔离 —— 这一点最容易被忽略
· Agent 用 cgroup 限制 CPU 和 IO:
systemd 里 CPUQuota=50% + IOSchedulingClass=idle
· 避免 Agent 读日志时冲掉业务的 page cache
-> posix_fadvise(POSIX_FADV_DONTNEED)(第 18 篇的实战案例)
⑥ 容量规划
· 单机日志量 × 保留天数 = 磁盘需求
· 限制 journald:SystemMaxUse + ForwardToSyslog=no(避免写两份)
· 磁盘水位告警 85%,且监控「已删除未释放」(第 20 篇案例四)
主动补充失败场景:「最容易出事的是磁盘写满导致业务一起挂。所以我会给日志分区单独挂盘,或者用 cgroup 的 io.max 限制 Agent 的写入带宽,并且设置 totalSizeCap 做硬性上限——宁可丢日志也不能让业务受影响。」
3.3 「怎么让一个服务支撑十万长连接?」
回答框架:内核参数 → 内存预算 → IO 模型 → 中断与 CPU → 监控。
① fd 限制(第 11 篇)
· systemd unit 里 LimitNOFILE=1048576
不要改 limits.conf —— systemd 不读它
· 【验证】cat /proc/PID/limits,不看 ulimit -n
② 内核参数
net.core.somaxconn = 65535 accept 队列
net.ipv4.tcp_max_syn_backlog = 65535 半连接队列
net.core.netdev_max_backlog = 32768 网卡队列
net.ipv4.ip_local_port_range = 10000 65000
fs.file-max 系统级 fd 总数
· 【关键】实际 backlog = min(应用传的值, somaxconn),两边都要改
③ 内存预算 —— 这是硬约束,必须算
单连接内核内存 4 ~ 10 KB
应用层读写缓冲 各 4 KB(可调小)
----------------------------
10 万连接 ≈ 1 ~ 2 GB
100 万连接 ≈ 10 ~ 20 GB,加上应用对象至少要 32GB 机器
④ IO 模型(第 12 篇)
· epoll + LT 模式(nginx/Redis 都用 LT,够用且不容易出 bug)
· 多进程/多线程 + SO_REUSEPORT,每个 worker 独立 accept 队列
-> 零惊群、负载天然均衡
· Go 直接用 goroutine(runtime 底层就是 epoll,2KB 栈起步)
⑤ 中断与 CPU 亲和性(第 18 篇)
· mpstat -P ALL 1 确认软中断没有集中在单核
· 开启多队列网卡 RSS:ethtool -L eth0 combined 8
· 或用 RPS 把软中断分散:echo ffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
· worker 绑核减少 cache 失效
⑥ 应用层
· 心跳保活,及时清理死连接(否则 fd 只增不减)
· 连接数上限保护,超过就拒绝新连接而不是 OOM
· 【必须】处理 SIGPIPE(第 8 篇)——一个客户端异常断连
就能杀死整个服务进程
⑦ 监控
· fd 使用率(70% 告警,不要等 100%)
· ss -s 的连接状态分布,特别是 CLOSE_WAIT
· nstat 的 ListenOverflows(accept 队列溢出)
主动指出容量的真实瓶颈:「十万连接本身不难,难的是每连接的内存开销和活跃连接的处理能力。如果这十万连接里有一万是活跃的,那 QPS 需求可能是几十万——这时候瓶颈就从连接数变成了单核的事件处理能力(约 5~20 万事件/秒),需要横向扩展而不是继续调单机参数。」
3.4 「服务被 OOM Killer 杀了,怎么定位和解决?」
回答框架:确认是 OOM → 区分哪一级 → 拆解内存构成 → 定位泄漏点 → 加防护。
① 确认是 OOM(应用日志必然是空的,因为收到的是 SIGKILL)
dmesg -T | grep -i "killed process"
容器:kubectl describe pod 看 Reason: OOMKilled,退出码 137
② 区分 cgroup OOM 还是宿主机 OOM
dmesg 里 "Memory cgroup out of memory" -> 容器超了自己的 limit
-> 扩节点无用,要么放宽 limit
要么减少实际占用
dmesg 里 "Out of memory: Killed" -> 整机内存耗尽
容器内:cat /sys/fs/cgroup/memory.events 看 oom_kill 计数
③ 拆解内存构成 —— 决定往哪个方向查
cgroup: grep -E "^(anon|file|slab|sock) " /sys/fs/cgroup/memory.stat
anon 高 -> 真实占用(堆、栈、堆外),不可回收 <- 真凶
file 高 -> page cache,内核会先回收,通常不至于 OOM
进程: grep -E "^(Rss|Pss|Private_Dirty):" /proc/PID/smaps_rollup
④ 语言层面拆解
Java: jcmd PID VM.native_memory summary(需要 -XX:NativeMemoryTracking)
-> Heap / Metaspace / Thread / Code / Internal(DirectMemory) 分别多少
Go: curl localhost:6060/debug/pprof/heap?debug=1
-> 对比 HeapAlloc 和 Sys;StackSys 大 + NumGoroutine 大 = goroutine 泄漏
⑤ 常见根因
· JVM 只设了 -Xmx,没给堆外留空间(线程栈、DirectByteBuffer 是重灾区)
· 线程池用了 newCachedThreadPool(线程数无上限)
· goroutine 泄漏(阻塞在 channel 或没设超时)
· 容器 /dev/shm 默认 64MB 且算进 cgroup 限制
⑥ 修复与防护
· Java: -XX:MaxRAMPercentage=55(不写死 -Xmx)+ MaxDirectMemorySize
+ MaxMetaspaceSize + 有界线程池
· Go: GOMEMLIMIT = limit × 0.9
· K8s: requests = limits(Guaranteed QoS,最不容易被驱逐)
· 关键服务设 OOMScoreAdjust=-500 降低被选中的概率
· 监控 container_memory_working_set_bytes / limit > 85% 告警
<- 注意监控这个而不是 JVM 堆使用率,堆健康时容器也可能 OOM
主动说明容器内存预算的算法:「关键是理解容器限额不等于堆大小。JVM 的实际占用 = 堆 + Metaspace + 线程栈(线程数 × Xss)+ CodeCache + DirectMemory + GC 元数据,所以 -Xmx 只应占容器限额的 50~60%,留 40% 给非堆部分。我见过最典型的案例是 4GB 容器配 -Xmx2g,看起来很安全,但 1000 个线程的栈就占了 1GB,加上 DirectByteBuffer 623MB,总共 4.3GB,必然 OOM。」
4. 易错点速查
面试时说错这些会直接暴露没有实操经验。
| ❌ 错误说法 | ✅ 正确 |
|---|---|
free 里 free 少就是内存不够 |
看 available,Linux 会主动用空闲内存做 page cache |
%util 100% 就是磁盘满载 |
SSD 上要看 await 和 aqu-sz,%util 定义源自机械盘 |
| load 高就是 CPU 不够 | Linux 的 load 算 R+D,D 状态等 IO 也计入 |
kill -9 是标准的停止服务方式 |
先 SIGTERM 让应用优雅关闭,超时才 SIGKILL |
僵尸进程要 kill -9 掉 |
僵尸已经死了,要杀父进程让 init 收养回收 |
D 状态进程可以 kill -9 |
不行,内核在此阶段不响应任何信号 |
改 /etc/security/limits.conf 就能提高服务的 fd 限制 |
systemd 不读它,要配 LimitNOFILE,验证看 /proc/PID/limits |
ulimit -n 显示 65535 就是生效了 |
那只是当前 shell,进程的真实限制在 /proc/PID/limits |
rm 掉大日志就能释放磁盘 |
进程持有 fd 时空间不释放,要用 truncate -s 0 |
TIME_WAIT 多了要开 tcp_tw_recycle |
绝对不要,NAT 下会随机连接失败,4.12 已移除 |
CLOSE_WAIT 多了调内核参数 |
那是应用漏 close(),内核参数完全无效 |
| 非阻塞 IO 是异步 IO | 非阻塞 IO 是同步的,阶段二仍然阻塞 |
| epoll 一定比 select 快 | 连接少且几乎全活跃时,epoll_ctl 的开销可能让它更慢 |
| ET 模式性能一定更好 | nginx/Redis 都用 LT;ET 的编程复杂度带来的 bug 风险更高 |
| 容器就是轻量级虚拟机 | 容器共享宿主机内核,是普通进程;虚拟机有独立内核 |
容器里 top 看到的是容器的负载 |
是宿主机的,/proc/loadavg 没被 namespace 隔离 |
容器 -Xmx 设成容器限额就好 |
只能占 50~60%,要给线程栈、DirectMemory、Metaspace 留空间 |
chmod 777 能解决权限问题 |
那是拆掉安全边界;要搞清访问者身份再用属主或属组授权 |
| 文件只读就删不掉 | 删除看的是目录的 w 权限,与文件权限无关 |
cd 是外部命令 |
是 Shell 内建命令,所以 which cd 找不到 |
for f in $(ls) 遍历文件 |
会被词分割和通配符展开搞坏,用 for f in ./* |
sed -i 是原地修改 |
实际是「新建+重命名」,inode 会变、硬链接会断 |
| 系统调用和函数调用差不多 | 差 100 倍(150ns vs 1.5ns),所以要批量化 |
strace 可以随便挂在生产服务上 |
慢 10~100 倍,必须配 timeout 和 -c |
5. 答题框架
被问到开放性问题时,用这些框架组织回答,比零散罗列命令显得体系化得多。
5.1 「你怎么排查 CPU 100%」
① 定位进程:ps --sort=-pcpu / top
② 【关键一步】分 %usr 和 %system —— 这决定了后面的所有方向
%usr 高 -> 业务代码在算,用 profiler(jstack/pprof/perf)
%system 高 -> 系统调用或上下文切换,用 strace -c
③ 定位线程:ps -Lo pid,lwp,pcpu --sort=-pcpu
④ 定位代码:Java 把 LWP 转十六进制找 jstack 的 nid;Go 用 pprof
⑤ 容器环境额外查:cpu.stat 的 nr_throttled(限流会造成使用率失真)
然后主动引出深度点:「如果 %usr 高但 profiler 显示不出明显热点,我会看 perf stat 的 IPC——低于 1.0 说明 CPU 在等内存而不是在计算,这时候优化方向是数据局部性(改数据结构布局)而不是算法复杂度。」
5.2 「说说你对 Linux 内存管理的理解」
① 抽象层:虚拟内存的四个好处(隔离/超额分配/共享/连续假象)
② 翻译层:4 级页表 + TLB,以及这为什么让进程切换比线程切换贵
③ 分配层:懒惰分配 —— malloc 只记账,首次写入才缺页中断分配物理页
④ 回收层:page cache 可回收(所以看 available 不看 free)、
swap 的权衡、OOM Killer 的评分机制
⑤ 观测层:VSZ/RSS/PSS 的区别、majflt 的意义、cgroup 的 memory.stat
主动引出实践:「实际排查中最有用的是三个判断:available 而不是 free;Private_Dirty 判断是否真实泄漏;容器里看 anon 还是 file 高来决定是否会 OOM。」
5.3 「线上出问题你怎么定位」
① 先看现象范围:全部还是部分?所有实例还是单个?何时开始?有无变更?
② 分层排除,不跳层:
网络类 -> 网卡 -> 路由 -> ping -> 端口 -> 防火墙 -> 监听地址 -> 安全组
性能类 -> 全局(vmstat) -> 资源(iostat/free) -> 进程(pidstat) -> 代码
③ 每一步要有明确判据(不说「看起来有点忙」,说「await 是基准的 245 倍」)
④ 先留证据再重启:jstack / pprof / /proc/PID/stack,
重启后现场就没了
⑤ 修完验证「配置真的生效」:/proc/PID/limits、jcmd VM.flags、退出码
⑥ 事后补监控:问「下次能不能在爆炸前发现」,不能就加指标
这个框架的价值在最后两条——大部分人只讲到「怎么查」,讲到「怎么验证生效」和「怎么让下次更快」才显示出工程成熟度。
5.4 「你平时怎么做性能优化」
① 先有基线,再谈优化 —— 没有压测数据的优化是盲目的
② 用 USE 方法论定位瓶颈资源(Utilization / Saturation / Errors)
关键认知:饱和度比使用率重要(SSD 的 %util 100% 可能很闲)
③ 一次只改一项,改完重新压测对比
④ 分清可压缩资源(CPU)和不可压缩资源(内存)
-> CPU 不够变慢,内存不够直接 OOM,所以 limits 策略不同
⑤ 减少系统调用(批量化、零拷贝)比优化业务逻辑常常收益更大
⑥ 压测要注意协调遗漏 —— 用 wrk2 固定速率,否则测出的延迟偏乐观
6. 系列回顾
| 篇目 | 核心内容 |
|---|---|
| 01 认识 Linux 与目录结构 | 内核与发行版、FHS 设计逻辑、/proc 与 /sys、命令查找心智模型、man 分节 |
| 02 文件、链接与归档压缩 | inode 三层结构、三个时间、硬软链接、unlink 机制、find、tar 与压缩选型 |
| 03 用户、权限与安全模型 | UID 本质、rwx 两套语义、umask、SUID/SGID/Sticky、ACL、sudo、容器 uid |
| 04 文本处理三剑客与正则 | BRE/ERE/PCRE、grep 高频用法、sed 行编辑与 -i 陷阱、awk 关联数组 |
| 05 Shell 编程与脚本工程化 | set -euo pipefail 及其失效边界、引号、fd 与重定向、管道子 shell、trap |
| 06 进程管理与作业控制 | task_struct、fork/exec/wait、五种进程状态、僵尸与孤儿、进程组与会话 |
| 07 进程调度与上下文切换 | CFS 与 vruntime、nice 权重、三种上下文切换、自愿/非自愿、平均负载 |
| 08 信号与进程间通信 | 信号表、可靠/不可靠、异步信号安全、SIGPIPE、优雅关闭、六种 IPC 选型 |
| 09 用户态、内核态与系统调用 | 特权环、系统调用完整路径、glibc wrapper、vDSO、strace、零拷贝 |
| 10 内存管理 | 虚拟内存与页表、缺页中断、available 的含义、脏页回写、OOM Killer |
| 11 文件系统与磁盘 IO | VFS 四对象、ext4 vs xfs、挂载选项、LVM 扩容、iostat 判读、fd 与 ulimit |
| 12 IO 模型与 epoll | 两阶段模型、五种 IO 模型、同步异步的准确定义、epoll 内核结构、ET/LT、惊群 |
| 13 网络配置与排查 | ip 命令族、路由匹配、ss 状态判读、iptables 四表五链、curl -w 计时 |
| 14 启动流程与 systemd | 完整启动链、unit 详解、Type 选择、After vs Requires、journalctl |
| 15 定时任务、日志与包管理 | cron 三个坑、systemd timer、logrotate 的 inode 问题、包管理、动态库 |
| 16 SSH 远程访问原理与实战 | 三阶段握手、密钥登录原理、TOFU 风险、三种端口转发、ProxyJump、连接复用 |
| 17 SSH 密钥管理与多账号实战 | 算法选择、多密钥规划、密钥优先级、IdentitiesOnly、ssh-agent、known_hosts |
| 18 性能分析工具链与方法论 | USE 方法论、60 秒定位法、sar 历史数据、perf 与火焰图、eBPF、压测陷阱 |
| 19 namespace 与 cgroup | 容器本质、八种 namespace、nsenter 排查、cgroup v2 与 PSI、手写迷你容器 |
| 20 线上问题排查实战案例 | 八个真实故障的完整排查,含症状索引表和通用排查心法 |
| 21 面试题汇总 | 本篇 |
写在最后
这个系列的目的不是让你记住多少命令,而是建立从现象到原理再到排查手段的映射。
真正有用的是那些反复出现的判断依据:
%usr vs %system -> 业务代码 vs 系统调用
r 列 vs b 列 -> CPU 排队 vs IO 阻塞
available vs free -> 真实可用 vs 完全空闲
anon vs file -> 不可回收 vs 可回收
CLOSE_WAIT vs TIME_WAIT -> 应用 bug vs 协议正常状态
refused vs timeout -> 没监听 vs 被丢包
退出码 0 / 143 / 137 -> 优雅退出 / 未捕获 TERM / 被 KILL
这些判断的共同点是:它们能在几秒内把排查范围缩小一个数量级。剩下的具体命令和参数,man 和搜索引擎随时能查,但方向判断错了,再熟练的命令也是在错误的路上狂奔。
另外一个容易被忽略的点:排查能力的上限是可观测性的上限。每次故障之后除了修 bug,还要问一句「下次这个问题能不能在爆炸前被发现」——如果不能,就补一个监控指标。这比记住任何案例都更有价值。
xingliuhua