Linux 系统编程与调试工具面试题 —— 从文件 IO 到性能火焰图的实战问答
覆盖文件IO(open/read/write/mmap)、信号处理(sigaction)、进程控制(fork/exec/daemon)、GDB调试(断点/core dump/多线程)、Valgrind/ASan/TSan、strace/perf/火焰图、io_uring,25 道高频题附命令速查
Linux 系统编程和调试能力是后端/基础架构岗的硬核加分项——会写 CRUD 的人很多,但能用 strace 追问题、GDB 调 core dump、perf 画火焰图的人,才是团队里的”救火队长”。
这篇文章从文件 IO → 信号 → 进程 → 调试 → 性能五条线展开,每道题都带可直接运行的命令,帮你从”理论派”进化为”实战派”。
📌 关联阅读:操作系统面试题 · C++ 对象模型面试题
第一部分:文件 IO 与内存映射
Q1:open() 返回的文件描述符(fd)本质是什么?内核怎么管理 fd?
记忆点:fd = 进程文件描述符表的数组下标
1
2
3
4
5
6
7
8
9
10
11
进程 PCB
┌────────────────────┐
│ fd_table[] │
│ [0] ──→ stdin │ 系统级 打开文件表
│ [1] ──→ stdout │ ┌──────────────┐
│ [2] ──→ stderr │ │ file 对象 │ inode 表
│ [3] ──→ ─────────────→ │ offset: 1024 │──→ ┌────────┐
│ [4] ──→ ... │ │ flags: O_RDWR│ │ inode │
└────────────────────┘ │ ref_count: 2 │ │ size │
└──────────────┘ │ blocks │
└────────┘
三层结构:
| 层级 | 数据结构 | 作用 |
|---|---|---|
| 进程级 | fd_table[] | 数组下标就是 fd,指向打开文件表 |
| 系统级 | struct file | 记录偏移量、打开模式、引用计数 |
| 文件系统级 | struct inode | 文件元数据、磁盘块位置 |
面试加分:fork() 后父子进程共享同一个 struct file(引用计数 +1),所以偏移量也共享。dup2() 也是共享同一个 file 对象。
Q2:read/write 与 mmap 有什么区别?什么场景用 mmap 更好?
记忆点:read/write = 两次拷贝,mmap = 零拷贝(用户态直接访问页缓存)
1
2
3
4
5
6
7
8
9
10
read/write 流程: mmap 流程:
磁盘 → 页缓存 → 用户缓冲区 磁盘 → 页缓存 ←→ 用户虚拟地址
拷贝1 拷贝2 直接映射,无拷贝
用户空间 ┌──────────┐ 用户空间 ┌──────────┐
│ buf[4096]│ ← 拷贝2 │ 虚拟地址 │
└──────────┘ │ ↕ 直接 │
内核空间 ┌──────────┐ 内核空间 │ 页缓存 │
│ 页缓存 │ ← 拷贝1 └──────────┘
└──────────┘
对比速查表:
| 维度 | read/write | mmap |
|---|---|---|
| 拷贝次数 | 2 次 | 0 次(缺页中断后直接映射) |
| 系统调用 | 每次 read/write 都进内核 | 只在 mmap/munmap 时进内核 |
| 随机访问 | 需要 lseek + read | 直接指针运算 |
| 适用场景 | 顺序读写、小文件 | 大文件随机访问、进程间共享 |
| 缺点 | 频繁系统调用开销 | 缺页中断开销、信号处理复杂 |
实战命令:
1
2
3
4
5
# 查看进程的内存映射
cat /proc/<pid>/maps
# 典型 mmap 用法
void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
面试加分:数据库(如 MongoDB 早期版本)和日志系统常用 mmap;但 mmap 的缺页中断不可控,对延迟敏感的场景(如交易系统)反而不适合。
Q3:O_DIRECT 和 O_SYNC 分别解决什么问题?
记忆点:O_DIRECT = 绕过页缓存,O_SYNC = 写入持久化保证
1
2
3
普通写: write() → 页缓存 → (异步刷盘) ← 可能丢数据
O_SYNC: write() → 页缓存 → 立即刷盘 ← 保证持久化
O_DIRECT:write() → 绕过页缓存 → 直接写磁盘 ← 应用自己管缓存
| 标志 | 绕过缓存 | 持久化保证 | 典型场景 |
|---|---|---|---|
| 默认 | ✗ | ✗ | 普通应用 |
| O_SYNC | ✗ | ✓ | 数据库 WAL |
| O_DIRECT | ✓ | ✗ | 数据库自管缓存 |
| O_DIRECT + O_SYNC | ✓ | ✓ | 最严格的持久化 |
面试加分:MySQL InnoDB 的 innodb_flush_method=O_DIRECT 就是绕过 OS 页缓存,用自己的 Buffer Pool 管理缓存,避免双重缓存浪费内存。
Q4:零拷贝技术有哪几种?sendfile 和 splice 的区别?
记忆点:零拷贝 = 减少 CPU 参与的数据拷贝次数
1
2
3
4
5
6
7
8
9
10
11
传统方式(4 次拷贝 + 4 次上下文切换):
read(): 磁盘 → 内核缓冲 → 用户缓冲 2次拷贝 + 2次切换
write(): 用户缓冲 → Socket缓冲 → 网卡 2次拷贝 + 2次切换
sendfile(2 次拷贝 + 2 次上下文切换):
sendfile(): 磁盘 → 内核缓冲 → Socket缓冲 → 网卡
DMA拷贝 CPU拷贝 DMA拷贝
sendfile + DMA gather(0 次CPU拷贝):
sendfile(): 磁盘 → 内核缓冲 --------→ 网卡
DMA拷贝 只传描述符 DMA gather
三种零拷贝方式:
| 方式 | 原理 | 限制 | 典型应用 |
|---|---|---|---|
| mmap + write | 映射页缓存到用户空间 | 仍有 1 次 CPU 拷贝 | 数据库 |
| sendfile | 内核态直接传输 | 只能 fd → socket | Nginx 静态文件 |
| splice | 管道中转,内核态传输 | 必须有一端是管道 | 代理服务器 |
实战:Nginx 配置 sendfile on; 就是启用 sendfile 零拷贝,静态文件性能提升显著。Kafka 也用 sendfile 实现高吞吐消费。
第二部分:信号处理
Q5:signal() 和 sigaction() 有什么区别?为什么推荐 sigaction?
记忆点:signal() = 不可靠,sigaction() = 可控且可移植
| 维度 | signal() | sigaction() |
|---|---|---|
| 处理函数重置 | 某些系统处理后自动重置为 SIG_DFL | 不会重置 |
| 系统调用中断 | 行为不确定(是否自动重启) | SA_RESTART 可控制 |
| 信号屏蔽 | 无法指定处理期间屏蔽的信号 | sa_mask 精确控制 |
| 可移植性 | 不同 Unix 行为不同 | POSIX 标准,行为一致 |
1
2
3
4
5
6
7
// 推荐写法
struct sigaction sa;
sa.sa_handler = handler_func;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM); // 处理 SIGUSR1 时屏蔽 SIGTERM
sa.sa_flags = SA_RESTART; // 被中断的系统调用自动重启
sigaction(SIGUSR1, &sa, NULL);
面试加分:永远不要在信号处理函数中调用非异步信号安全的函数(如 printf、malloc)。安全的做法是设置一个 volatile sig_atomic_t 标志位,主循环中检查。
Q6:SIGCHLD 信号有什么用?不处理会怎样?
记忆点:不处理 SIGCHLD → 子进程变僵尸进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
父进程 fork() 子进程:
子进程退出后:
┌──────────┐ ┌──────────────┐
│ 父进程 │ 不调用 wait │ 子进程(僵尸) │
│ running │ ←── SIGCHLD ──│ Z (zombie) │
│ │ │ 只剩 PCB 占位 │
└──────────┘ └──────────────┘
正确做法:
signal(SIGCHLD, SIG_IGN); // 方式1:忽略,内核自动回收
// 或
sigaction 中用 SA_NOCLDWAIT // 方式2:不产生僵尸
// 或
void handler(int sig) { // 方式3:在处理函数中循环 waitpid
while (waitpid(-1, NULL, WNOHANG) > 0);
}
为什么 waitpid 要用循环? 因为信号不排队——多个子进程同时退出时,SIGCHLD 可能只收到一次。循环 waitpid 确保回收所有已退出子进程。
Q7:如何优雅地关闭一个多线程服务?
记忆点:专用信号线程 + 全局标志位 + 有序清理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
主线程启动时:
1. 阻塞所有工作线程的 SIGTERM/SIGINT(pthread_sigmask)
2. 创建专用信号线程(sigwait 等待信号)
收到 SIGTERM:
┌─────────────┐ 设置标志 ┌─────────────┐
│ 信号线程 │ ──────────────→ │ g_running=0 │
│ sigwait() │ └──────┬──────┘
└─────────────┘ │
↓
各工作线程检查标志
┌───┐ ┌───┐ ┌───┐
│ T1│ │ T2│ │ T3│
└─┬─┘ └─┬─┘ └─┬─┘
↓ ↓ ↓
完成当前任务后退出
↓ ↓ ↓
pthread_join 等待全部退出
↓
关闭文件/释放资源/退出
1
2
3
4
5
6
7
8
9
10
11
12
13
// 关键代码骨架
volatile sig_atomic_t g_running = 1;
void* signal_thread(void* arg) {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGINT);
int sig;
sigwait(&set, &sig); // 阻塞等待
g_running = 0; // 通知所有工作线程
return NULL;
}
面试加分:这就是 Nginx 的优雅退出方式——master 进程收到信号后,给 worker 进程发 SIGQUIT,worker 处理完当前请求后退出。
第三部分:进程控制
Q8:fork() 之后,父子进程的内存是如何共享的?
记忆点:fork() 用 COW(Copy-On-Write),不是真的复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fork() 刚完成时:
┌────────────┐ ┌────────────┐
│ 父进程 │ │ 子进程 │
│ 页表 ───────┼───┐ ┌──┼── 页表 │
└────────────┘ │ │ └────────────┘
↓ ↓
┌──────────┐
│ 物理页面 │ ← 两个页表指向同一物理页
│ (只读标记) │ 任何一方写入时触发 COW
└──────────┘
子进程写入 page A 后:
┌────────────┐ ┌────────────┐
│ 父进程 │ │ 子进程 │
│ 页A → 原页 │ │ 页A → 新页 │ ← 只复制被修改的页
│ 页B → 共享 │ │ 页B → 共享 │ ← 未修改的继续共享
└────────────┘ └────────────┘
COW 的好处:
fork() + exec()模式下,子进程马上替换地址空间,COW 避免了无用的内存复制- 只有实际写入的页才会复制,大幅节省内存
面试加分:Redis 的 BGSAVE 就利用了 COW——fork 出子进程做 RDB 快照,父进程继续服务。如果父进程写入密集,COW 会导致大量页复制,内存可能翻倍。所以 Redis 建议预留 50% 内存给 COW。
Q9:exec 家族函数有什么区别?怎么记?
记忆点:l/v = 参数形式,e = 环境变量,p = PATH 搜索
1
2
3
4
5
6
7
exec 家族命名规则:
exec + [l|v] + [p] + [e]
l = list 参数逐个列出 execl(path, arg0, arg1, NULL)
v = vector 参数用数组 execv(path, argv[])
p = PATH 搜索 PATH execlp("ls", "ls", "-l", NULL)
e = environ 自定义环境变量 execle(path, arg0, NULL, envp[])
| 函数 | 参数形式 | 搜索 PATH | 自定义环境 |
|---|---|---|---|
execl | list | ✗ | ✗ |
execv | vector | ✗ | ✗ |
execlp | list | ✓ | ✗ |
execvp | vector | ✓ | ✗ |
execle | list | ✗ | ✓ |
execve | vector | ✗ | ✓(唯一系统调用) |
面试加分:只有 execve 是真正的系统调用,其他都是 C 库的封装。
Q10:如何写一个 daemon 进程?每一步的原因是什么?
记忆点:双 fork + setsid + 重定向 fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
步骤与原因:
1. fork() + 父进程退出
└→ 子进程不是进程组组长,才能调用 setsid()
2. setsid()
└→ 创建新会话,脱离控制终端
3. 再次 fork() + 父进程退出
└→ 不是会话首进程,永远不会获取控制终端
4. chdir("/")
└→ 避免占用挂载点,导致无法 umount
5. umask(0)
└→ 不继承父进程的文件创建掩码
6. 关闭所有 fd / 重定向 0,1,2 到 /dev/null
└→ 脱离终端,避免向已关闭的终端写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void daemonize() {
pid_t pid = fork();
if (pid > 0) exit(0); // 父进程退出
setsid(); // 新会话
pid = fork();
if (pid > 0) exit(0); // 再次 fork
chdir("/");
umask(0);
// 重定向标准IO
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) close(fd);
}
面试加分:现代 Linux 用 systemd 管理守护进程,不需要手动 daemonize。systemd 提供了日志(journald)、自动重启、依赖管理等功能。但面试考的是你理解每一步的原因。
第四部分:GDB 调试
Q11:GDB 调试的核心命令速查表?
记忆点:断点→运行→查看→导航→修改五类命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
启动与附加:
gdb ./program # 调试可执行文件
gdb -p <pid> # 附加到运行中的进程
gdb ./program core # 分析 core dump
gdb -tui ./program # TUI 模式(带源码窗口)
断点管理:
b main # 函数断点
b file.c:42 # 行号断点
b func if x > 10 # 条件断点
watch var # 监视点(变量变化时停)
catch throw # 捕获 C++ 异常抛出
info b # 查看所有断点
delete 2 # 删除 2 号断点
disable/enable 3 # 禁用/启用断点
运行控制:
r [args] # 运行
c # 继续
n # 单步(不进入函数)
s # 单步(进入函数)
fin # 执行到当前函数返回
until 50 # 执行到第 50 行
查看信息:
p var # 打印变量
p/x var # 十六进制显示
p *arr@10 # 打印数组前 10 个元素
bt # 调用栈
bt full # 调用栈 + 局部变量
frame 3 # 切换到 3 号栈帧
info locals # 当前函数局部变量
info threads # 所有线程
thread 2 # 切换到 2 号线程
内存查看:
x/16xb &var # 16 字节,十六进制
x/4xw &var # 4 个 word,十六进制
x/s str # 字符串
x/10i $pc # 反汇编 10 条指令
Q12:程序崩溃了,如何用 core dump + GDB 定位问题?
记忆点:开启 core → 复现崩溃 → bt 看栈 → frame 看变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
步骤 1:开启 core dump
ulimit -c unlimited # 允许生成 core
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
步骤 2:复现崩溃,生成 core 文件
步骤 3:GDB 分析
gdb ./myprogram /tmp/core.myprogram.12345
(gdb) bt # 看崩溃时的调用栈
#0 0x004005a3 in process_data (ptr=0x0) at main.c:42
#1 0x004005f1 in handle_request (req=0x7fff...) at server.c:88
#2 0x00400634 in main () at server.c:120
(gdb) frame 0 # 切到崩溃帧
(gdb) p ptr # 看变量值
$1 = (char *) 0x0 # → 空指针!
(gdb) list # 看源码上下文
常见崩溃原因速查:
| 信号 | 含义 | 常见原因 |
|---|---|---|
| SIGSEGV | 段错误 | 空指针、野指针、栈溢出 |
| SIGABRT | 主动终止 | assert 失败、double free |
| SIGFPE | 算术错误 | 除以零、整数溢出 |
| SIGBUS | 总线错误 | 未对齐访问、mmap 文件被截断 |
面试加分:生产环境建议用 coredumpctl(systemd)管理 core dump,带自动压缩和保留策略。
Q13:如何用 GDB 调试多线程程序?怎么排查死锁?
记忆点:info threads → thread apply all bt → 看锁状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
多线程调试命令:
info threads # 查看所有线程
thread 2 # 切换到线程 2
thread apply all bt # 所有线程的调用栈(排查死锁神器)
set scheduler-locking on # 只运行当前线程(避免其他线程干扰)
死锁排查流程:
(gdb) thread apply all bt
Thread 2 (LWP 12346):
#0 __lll_lock_wait () ← 卡在锁上
#1 pthread_mutex_lock ()
#2 transfer (from=A, to=B) ← 线程2: 持有锁A, 等待锁B
Thread 1 (LWP 12345):
#0 __lll_lock_wait () ← 也卡在锁上
#1 pthread_mutex_lock ()
#2 transfer (from=B, to=A) ← 线程1: 持有锁B, 等待锁A
→ 经典的 ABBA 死锁!
不用 GDB 的快速排查:
1
2
3
4
5
6
7
8
9
# 方式1:pstack(生产环境常用)
pstack <pid>
# 方式2:gdb 一键附加
gdb -batch -ex "thread apply all bt" -p <pid>
# 方式3:/proc 查看线程状态
ls /proc/<pid>/task/ # 所有线程
cat /proc/<pid>/task/<tid>/status # 线程状态
第五部分:内存检测工具
Q14:Valgrind 能检测哪些内存问题?怎么看报告?
记忆点:Valgrind = 内存错误全家桶,速度慢 10-50x
1
2
3
4
5
6
7
8
9
# 基本用法
valgrind --leak-check=full --show-leak-kinds=all ./myprogram
# 典型输出解读
==12345== Invalid read of size 4 ← 越界读
==12345== at 0x4005A3: main (test.c:10)
==12345== Address 0x5204044 is 0 bytes after a block of size 4 alloc'd
==12345== at 0x4C2AB80: malloc (vg_replace_malloc.c:299)
==12345== by 0x400593: main (test.c:8)
Valgrind 能检测的问题:
| 问题类型 | 示例 | Valgrind 报告关键词 |
|---|---|---|
| 内存泄漏 | malloc 后没 free | “definitely lost” |
| 越界访问 | 数组下标越界 | “Invalid read/write” |
| 使用未初始化 | 读取未初始化变量 | “Conditional jump depends on uninitialised” |
| double free | 同一块内存 free 两次 | “Invalid free” |
| 悬空指针 | free 后继续使用 | “Invalid read” + “was freed” |
泄漏分类:
- definitely lost:确定泄漏,没有任何指针指向
- indirectly lost:被 definitely lost 的块引用
- possibly lost:有内部指针指向,但可能不是有意的
- still reachable:程序退出时仍有指针指向(通常不算泄漏)
Q15:ASan、TSan、MSan 分别检测什么?和 Valgrind 比有什么优势?
记忆点:Sanitizer = 编译时插桩,快但需要重新编译
1
2
3
4
5
# 编译时开启(GCC/Clang 都支持)
gcc -fsanitize=address -g test.c # ASan: 地址错误
gcc -fsanitize=thread -g test.c # TSan: 数据竞争
gcc -fsanitize=memory -g test.c # MSan: 未初始化读取(仅 Clang)
gcc -fsanitize=undefined -g test.c # UBSan: 未定义行为
对比速查表:
| 工具 | 检测对象 | 性能开销 | 需要重编译 | 优势 |
|---|---|---|---|---|
| Valgrind | 内存错误全家桶 | 10-50x 慢 | 不需要 | 不改代码即可用 |
| ASan | 越界、UAF、泄漏 | 2x 慢 | 需要 | 速度快,错误描述清晰 |
| TSan | 数据竞争 | 5-15x 慢 | 需要 | 唯一实用的竞态检测工具 |
| MSan | 未初始化内存读取 | 3x 慢 | 需要 | Valgrind 也能做但更慢 |
| UBSan | 整数溢出、空指针 | 几乎无 | 需要 | 开销极小 |
ASan 输出示例:
1
2
3
4
5
6
7
8
9
10
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014
READ of size 4 at 0x602000000014 thread T0
#0 0x4005a3 in main test.c:10
#1 0x7f... in __libc_start_main
0x602000000014 is located 0 bytes after 4-byte region [0x602000000010,0x602000000014)
allocated by thread T0 here:
#0 0x7f... in malloc
#1 0x400593 in main test.c:8
面试加分:Google 内部所有 C++ 代码的 CI 都开启 ASan + TSan。建议把 -fsanitize=address,undefined 加到 Debug 构建的默认选项中。
第六部分:追踪与性能分析
Q16:strace 能做什么?怎么快速定位”程序卡住了”的问题?
记忆点:strace = 系统调用追踪器,程序卡住时第一反应
1
2
3
4
5
6
7
8
9
# 追踪运行中的进程
strace -p <pid> -f -t
# 常用选项
strace -e trace=file ./prog # 只看文件相关调用
strace -e trace=network ./prog # 只看网络相关调用
strace -c ./prog # 统计各系统调用耗时
strace -T ./prog # 每个调用的耗时
strace -o output.txt ./prog # 输出到文件
实战:程序卡住了
1
2
3
4
5
$ strace -p 12345
futex(0x7f..., FUTEX_WAIT, ...) = ? ← 卡在 futex → 锁竞争/死锁
poll([{fd=3, events=POLLIN}], 1, -1) ← 卡在 poll → 等待网络/IO
read(0, ← 卡在 read → 等待 stdin 输入
nanosleep({tv_sec=3600}, ...) ← 卡在 sleep → 程序在 sleep
strace -c 输出(找性能瓶颈):
1
2
3
4
5
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- --------
89.42 0.182912 91 2000 write ← 瓶颈!
5.31 0.010862 5 2000 read
3.12 0.006381 3 2000 open
面试加分:ltrace 追踪的是库函数调用(如 malloc、printf),和 strace 互补。
Q17:perf 工具怎么用?如何生成火焰图?
记忆点:perf = 性能分析瑞士军刀,火焰图 = 可视化热点函数
1
2
3
4
5
6
7
8
9
10
11
# 步骤 1:采样
perf record -g -p <pid> -- sleep 30 # 采样 30 秒
perf record -g ./myprogram # 或直接运行采样
# 步骤 2:查看报告
perf report # 交互式查看热点函数
# 步骤 3:生成火焰图
perf script > perf.out # 导出采样数据
stackcollapse-perf.pl perf.out > folded.out
flamegraph.pl folded.out > flamegraph.svg
火焰图怎么看:
1
2
3
4
5
6
7
8
9
10
11
12
┌──────────────────────────────────┐
│ main() │ ← 底部 = 调用者
├──────────────┬───────────────────┤
│ process() │ handle_io() │ ← 宽度 = CPU 占比
├──────┬───────┤ │
│sort()│hash() │ │ ← 顶部 = 实际消耗 CPU 的函数
└──────┴───────┴───────────────────┘
看火焰图三步法:
1. 找最宽的"平顶"(顶部宽 = 该函数自身消耗 CPU 多)
2. 看它的调用路径(从下往上,理解为什么被调用)
3. 思考能否减少调用次数或优化函数本身
perf 其他常用命令:
1
2
3
perf stat ./prog # 总体统计(IPC、缓存命中率等)
perf top # 实时热点(类似 top)
perf stat -e cache-misses,cache-references ./prog # 缓存命中分析
面试加分:perf stat 输出的 IPC(Instructions Per Cycle)是性能关键指标——IPC < 1 通常意味着 CPU 在等待内存(cache miss),IPC > 2 说明流水线利用率高。
Q18:如何用 perf 分析缓存性能和分支预测?
记忆点:cache miss 看数据局部性,branch miss 看分支模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 缓存分析
perf stat -e L1-dcache-loads,L1-dcache-load-misses,\
LLC-loads,LLC-load-misses ./prog
# 输出示例
1,000,000 L1-dcache-loads
200,000 L1-dcache-load-misses # L1 命中率 80% ← 不理想
50,000 LLC-loads
10,000 LLC-load-misses # LLC 命中率 80%
# 分支预测分析
perf stat -e branches,branch-misses ./prog
# 输出示例
5,000,000 branches
500,000 branch-misses # 10% 分支预测失败 ← 较高
优化方向:
| 指标 | 正常范围 | 问题原因 | 优化方法 |
|---|---|---|---|
| L1 cache miss > 5% | < 3% | 数据局部性差 | 重排数据结构、SOA 替代 AOS |
| LLC miss 高 | 越低越好 | 工作集太大 | 减小数据量、预取 |
| 分支预测失败 > 5% | < 2% | 分支不可预测 | 用 __builtin_expect、CMOV |
第七部分:进阶工具
Q19:ltrace、ftrace、bpftrace 各自的定位是什么?
记忆点:用户态库 / 内核态函数 / 全能可编程
| 工具 | 追踪层 | 典型用途 | 性能开销 |
|---|---|---|---|
| strace | 系统调用 | 进程卡住排查 | 中等 |
| ltrace | 用户态库函数 | 看 malloc/free 调用模式 | 中等 |
| ftrace | 内核函数 | 内核调试、调度分析 | 低 |
| bpftrace | 全层(eBPF) | 可编程追踪,取代上面所有 | 极低 |
bpftrace 示例:
1
2
3
4
5
6
7
8
9
10
11
12
# 统计系统调用次数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# 追踪 open() 的文件名
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
printf("%s → %s\n", comm, str(args->filename));
}'
# 统计 malloc 分配大小分布
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc {
@sizes = hist(arg0);
}'
面试加分:eBPF 是近年 Linux 最重要的技术革新之一——它让你在内核中安全地运行自定义代码,不需要写内核模块。Netflix、Facebook、Cloudflare 都在大规模使用。
Q20:什么是 io_uring?它解决了什么问题?
记忆点:io_uring = 异步 IO + 零系统调用开销
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
传统 IO 模型的问题:
epoll_wait() → read() → write() → epoll_wait() → ...
系统调用 系统调用 系统调用 系统调用
每次 IO 操作都要进出内核,上下文切换开销大
io_uring 模型:
┌──────────────────────────────────────────┐
│ 共享内存 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 提交队列 SQ │ │ 完成队列 CQ │ │
│ │ (用户写入) │ │ (内核写入) │ │
│ │ ┌───┐ ┌───┐ │ │ ┌───┐ ┌───┐ │ │
│ │ │req│ │req│ │ │ │res│ │res│ │ │
│ │ └───┘ └───┘ │ │ └───┘ └───┘ │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────┘
↑ 用户态写 ↑ 用户态读
无需系统调用! 无需系统调用!
核心优势:
| 维度 | epoll + read/write | io_uring |
|---|---|---|
| 系统调用 | 每次 IO 至少 1 次 | 批量提交,甚至 0 次 |
| 数据拷贝 | 需要 read 拷贝数据 | 支持固定 buffer,减少拷贝 |
| 异步支持 | read/write 仍是同步 | 真正的内核级异步 |
| 适用范围 | 网络 IO | 网络 + 磁盘 + 定时器 |
三个核心概念:
| 概念 | 说明 |
|---|---|
| SQE (Submission Queue Entry) | 用户填写的 IO 请求 |
| CQE (Completion Queue Entry) | 内核返回的 IO 结果 |
| io_uring_setup / io_uring_enter | 创建/提交的系统调用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基本使用流程
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
// 提交读请求
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, size, offset);
io_uring_submit(&ring);
// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
int bytes_read = cqe->res;
io_uring_cqe_seen(&ring, cqe);
面试加分:io_uring 已被应用于 RocksDB、Nginx(实验性)。在高 IOPS 场景下,io_uring 比 epoll 性能提升 30%+。
第八部分:实战排查场景
Q21:线上服务 CPU 100%,怎么一步步排查?
记忆点:top → 线程 → 栈 → 火焰图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
排查流程图:
发现 CPU 100%
│
↓
top -Hp <pid> ←── 找到 CPU 最高的线程 TID
│
↓
printf '%x\n' <tid> ←── TID 转十六进制
│
↓
┌──────────────────────────────┐
│ 方式1: jstack <pid> (Java) │
│ 方式2: pstack <pid> (C/C++) │
│ 方式3: gdb -batch -ex │
│ "thread apply all bt" -p │
└──────────────┬───────────────┘
│
↓
看线程在干什么
┌──────┼──────┐
↓ ↓ ↓
用户态 内核态 等待IO
死循环 频繁sys 磁盘慢
│ │ │
↓ ↓ ↓
perf strace iostat
record -p pid -x 1
实战命令序列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 找到 CPU 高的进程
top -c
# 2. 找到 CPU 高的线程
top -Hp <pid>
# 3. 快速看线程栈
pstack <pid> # 或 gdb -batch -ex "thread apply all bt" -p <pid>
# 4. 如果需要更多信息
perf record -g -p <pid> -- sleep 10
perf report
# 5. 生成火焰图确认热点
perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg
Q22:线上服务内存持续增长(内存泄漏),怎么排查?
记忆点:确认增长 → 分析分配 → 定位泄漏点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 确认内存增长趋势
pidstat -r -p <pid> 1 # 每秒看 RSS 变化
watch -n 1 "cat /proc/<pid>/status | grep VmRSS"
# 2. 看内存分配地图
pmap -x <pid> # 看各段内存大小
cat /proc/<pid>/smaps # 详细内存映射
# 3. 使用 Valgrind(开发环境)
valgrind --leak-check=full --log-file=leak.txt ./prog
# 4. 使用 ASan(开发环境,更快)
gcc -fsanitize=address -g prog.c && ./a.out
# 5. 生产环境:jemalloc + prof
LD_PRELOAD=/usr/lib/libjemalloc.so MALLOC_CONF="prof:true,prof_prefix:jeprof" ./prog
jeprof --svg ./prog jeprof.*.heap > heap.svg
内存泄漏排查决策树:
1
2
3
4
5
6
7
内存增长
├── RSS 增长,但 heap 不变 → mmap 泄漏(检查 /proc/pid/maps)
├── heap 增长
│ ├── malloc 次数 > free 次数 → 忘记释放
│ ├── malloc = free 但内存不降 → 内存碎片
│ └── 缓存无限增长 → 需要淘汰策略(LRU)
└── 不是泄漏 → 业务数据正常增长
Q23:磁盘 IO 慢,如何定位问题?
记忆点:iostat → 哪个盘 → 哪个进程 → 读还是写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 整体 IO 状况
iostat -xz 1
# 关注:%util(利用率)、await(等待时间)、r/s w/s(IOPS)
# 关键指标解读
Device r/s w/s rMB/s wMB/s await %util
sda 500 200 50.0 20.0 8.5 95.2 ← 磁盘几乎打满
# 2. 找到哪个进程在做 IO
iotop -oP # 实时看 IO 排名
pidstat -d 1 # 每秒统计各进程 IO
# 3. 看具体在读写什么文件
strace -e trace=read,write,open -p <pid>
# 或
lsof -p <pid> # 看打开的文件列表
# 4. 对比顺序 vs 随机 IO
perf record -e block:block_rq_issue -p <pid>
perf script | sort | uniq -c | sort -rn | head
IO 性能关键指标:
| 指标 | 含义 | HDD 正常值 | SSD 正常值 |
|---|---|---|---|
| await | 平均等待时间 | < 20ms | < 2ms |
| %util | 磁盘利用率 | < 80% | < 80% |
| IOPS | 每秒 IO 次数 | 100-200 | 10K-100K |
| 吞吐 | MB/s | 100-200 | 500-3000 |
第九部分:Linux 网络调试
Q24:网络不通时的排查步骤?
记忆点:分层排查:物理→链路→网络→传输→应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
排查层次(从下往上):
Layer 1-2 物理/链路层:
ip link show # 网卡是否 UP
ethtool eth0 # 物理连接状态
Layer 3 网络层:
ip addr show # IP 地址配置
ip route show # 路由表
ping <target> # 可达性
traceroute <target> # 路径追踪
mtr <target> # 持续追踪(ping + traceroute)
Layer 4 传输层:
ss -tlnp # 查看监听端口
ss -tnp # 查看已建立连接
ss -s # 连接统计
netstat -i # 网卡统计(丢包等)
iptables -L -n -v # 防火墙规则
Layer 7 应用层:
curl -v <url> # HTTP 调试
tcpdump -i eth0 port 80 # 抓包
wireshark # 图形化分析
Q25:tcpdump 抓包速查和常见分析场景?
记忆点:tcpdump = 网络层的 strace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 基本用法
tcpdump -i eth0 -nn # 不解析域名和端口名
tcpdump -i eth0 port 80 # 指定端口
tcpdump -i eth0 host 10.0.0.1 # 指定主机
tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0' # 只看 SYN 包
# 保存和分析
tcpdump -i eth0 -w capture.pcap # 保存到文件
tcpdump -r capture.pcap # 读取文件
tcpdump -i eth0 -c 100 -w debug.pcap # 只抓 100 个包
# 常用组合过滤
tcpdump -i eth0 'src host 10.0.0.1 and dst port 3306' # 来自某IP到MySQL
tcpdump -i eth0 'tcp[tcpflags] & (tcp-rst) != 0' # 只看 RST 包
常见网络问题与抓包特征:
| 问题 | tcpdump 特征 | 根因 |
|---|---|---|
| 连接超时 | 只有 SYN,没有 SYN-ACK | 防火墙/服务未监听 |
| 连接被拒 | SYN → RST | 端口未监听 |
| 连接重置 | 传输中出现 RST | 应用崩溃/防火墙 |
| 重传多 | [TCP Retransmission] | 网络丢包 |
| 延迟高 | SYN 到 SYN-ACK 时间长 | 网络延迟/服务端慢 |
命令速查总表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──────────────────────────────────────────────────────────────┐
│ Linux 调试工具速查 │
├──────────────┬───────────────────────────────────────────────┤
│ 场景 │ 工具 & 命令 │
├──────────────┼───────────────────────────────────────────────┤
│ 程序崩溃 │ gdb + core dump, dmesg │
│ 程序卡住 │ strace -p, gdb attach, pstack │
│ CPU 高 │ top -Hp, perf record, 火焰图 │
│ 内存泄漏 │ valgrind, ASan, pmap, /proc/pid/smaps │
│ 内存越界 │ ASan, valgrind │
│ 数据竞争 │ TSan, helgrind (valgrind) │
│ 死锁 │ gdb thread apply all bt, pstack │
│ 磁盘 IO 慢 │ iostat, iotop, strace │
│ 网络不通 │ ping, ss, tcpdump, iptables │
│ 文件描述符 │ lsof -p, /proc/pid/fd │
│ 系统调用追踪 │ strace, ltrace │
│ 内核追踪 │ ftrace, bpftrace │
│ 性能总览 │ perf stat (IPC, cache miss) │
│ 函数热点 │ perf record + perf report │
│ 全局概览 │ dstat, vmstat, sar │
└──────────────┴───────────────────────────────────────────────┘
面试口诀速记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
文件三层表:fd表 → file表 → inode表
零拷贝三剑客:mmap、sendfile、splice
信号处理用 sigaction,signal 不可靠
fork 用 COW,只在写时复制
daemon 双 fork 加 setsid
GDB 四步走:断点 → 运行 → 看栈 → 查变量
core dump 三步:开 ulimit → 复现 → bt 看栈
死锁看 thread apply all bt
Valgrind 全但慢,ASan 快要编译
TSan 查竞态,UBSan 查未定义
程序卡住用 strace
CPU 高用 perf 画火焰图
内存涨用 valgrind 或 jemalloc prof
IO 慢用 iostat + iotop
网络问题分层查,tcpdump 是最后武器
这篇文章覆盖了 Linux 系统编程与调试的核心知识点。建议在面试前,至少在虚拟机里亲手跑一遍每个工具——读十遍不如敲一遍。