文章

Linux 系统编程与调试工具面试题 —— 从文件 IO 到性能火焰图的实战问答

覆盖文件IO(open/read/write/mmap)、信号处理(sigaction)、进程控制(fork/exec/daemon)、GDB调试(断点/core dump/多线程)、Valgrind/ASan/TSan、strace/perf/火焰图、io_uring,25 道高频题附命令速查

Linux 系统编程与调试工具面试题 —— 从文件 IO 到性能火焰图的实战问答

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/writemmap
拷贝次数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 → socketNginx 静态文件
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);

面试加分:永远不要在信号处理函数中调用非异步信号安全的函数(如 printfmalloc)。安全的做法是设置一个 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自定义环境
execllist
execvvector
execlplist
execvpvector
execlelist
execvevector✓(唯一系统调用)

面试加分:只有 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/writeio_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-20010K-100K
吞吐MB/s100-200500-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 系统编程与调试的核心知识点。建议在面试前,至少在虚拟机里亲手跑一遍每个工具——读十遍不如敲一遍。

本文由作者按照 CC BY 4.0 进行授权