CPU

BPF 跟踪工具可以提供更多细节信息,可回答以下这些问题:

  • 创建了哪些新进程?运行时长是多长?
  • 为什么 CPU 系统时间很高?是由于系统调用导致的吗?具体是哪些系统调用?
  • 线程每次唤醒时在 CPU 上花费多长时间?
  • 线程在运行队列中等待的时间有多长?
  • 运行队列最长的时候有多少线程在等待执行?
  • 不同 CPU 之间的运行队列是否均衡?
  • 为什么某个线程会主动脱离 CPU?脱离时间有多长?
  • 哪些软中断和硬中断占用了 CPU 时间?
  • 当其他运行队列中有需要运行的程序时,哪些 CPU 仍然处于空闲状态?
  • 应用程序处理每个请求时的 LLC 的命中率是多少?

测量 cpu 用量的各种事件源

事件类型 事件源
内核态函数 kprobes、kretprobes
用户态函数 uprobes、uretprobes
系统调用 系统调用跟踪点
软中断 irq:softirq* 跟踪点
硬中断 irq:irq_handler* 跟踪点
运行队列 workqueue 跟踪点(见第 14 章)
定时采样 PMC 或是基于定时器的采样器
CPU 电源控制事件 power 跟踪点
CPU 周期 PMC 数据

传统工具

  • uptime
  • top
  • mpstat
  • perf

perf vs bpf

graph TD
    perf[perf] -->|Collects| A[Hardware/Software Events]
    perf -->|Records| B[Sampled Data]
    perf -->|Analyzes| C[Performance Profiles]
    
    BPF[BPF/eBPF] -->|Executes| D[Custom Programs]
    BPF -->|Processes| E[In-Kernel Data]
    BPF -->|Enforces| F[Security Policies]

​​perf​​: ​​- Event recorder​​: Samples pre-defined system events

  • ​​Passive analysis​​: Collects data for post-processing
  • ​​Fixed capabilities​​: Limited to predefined event types ​​ BPF​​:
  • ​​Programmable runtime​​: Executes custom logic in kernel space
  • ​​Active processing​​: Filters/aggregates data in real-time
  • ​​Extensible​​: Creates new observability/security primitives

perf的事件处理

  • 内核态采样每个事件
  • 记录在buffer 中
  • 定期用户态全量拷贝
graph LR
    A[Event Trigger] --> B[Perf Ring Buffer]
    B --> C[Kernel Space]
    C --> D{Scheduled Copy}
    D -->|Full Event Data| E[User Space]
    E --> F[perf.data]
    style D stroke:#f90

bpf 事件记录

  • filter + agg 过滤事件
  • 记录在映射表中
  • 异步拷贝数据到用户态
graph LR
    A[Event Trigger] --> B[BPF Program]
    B -->|Filter/Aggregate| C[BPF Maps]
    C -->|Async Pull| D[User Space]
    D --> E[Processed Data]
    style B stroke:#09f
    style C stroke:#3f3

与 cpu 相关的工具

工具 分析对象 描述
execsnoop BCC/BT 调度 列出新进程的运行信息
exitsnoop BCC 调度 列出进程运行时长和退出的原因
runqlat BCC/BT 调度 统计 CPU 运行队列的延迟信息
runqlen BCC/BT 调度 统计 CPU 运行队列的长度
runqslower BCC 调度 当运行队列中等待时长超过阈值时打印
cpudist BCC 调度 统计在 CPU 上运行的时间
cpufreq 本书 CPU 按进程来采样 CPU 运行频率信息
profile BCC CPU 采样 CPU 运行的调用栈信息
offcputime BCC/本书 调度 统计线程脱离 CPU 时的跟踪信息和等待时长
syscount BCC/BT 系统调用 按类型和进程统计系统调用次数
argdist BCC 系统调用 可以用来进行系统调用分析
trace BCC 系统调用 可以用来进行系统调用分析
funccount BCC 软件 统计函数调用次数
softirqs BCC 中断 统计软中断时间
hardirqs BCC 中断 统计硬中断时间
smpcalls 本书 内核 统计 SMP 模式下的远程 CPU 调用信息
llcstat BCC PMC 按进程统计 LLC 命中率

runqlat 的 bpftrace 脚本

 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
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env bpftrace

#include <linux/sched.h>

BEGIN
{
        printf("Tracing CPU scheduler... Hit Ctrl-C to end.\n");
}

tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new
{
        @qtime[args->pid] = nsecs;
}

tracepoint:sched:sched_switch
{
        if (args->prev_state == TASK_RUNNING) {
                @qtime[args->prev_pid] = nsecs;
        }

        $ns = @qtime[args->next_pid];
        if ($ns) {
                @usecs = hist((nsecs - $ns) / 1000);
        }
        delete(@qtime[args->next_pid]);
}

END
{
        clear(@qtime);
}
root@node2:~#
root@node2:~# bpftrace -e '
#include <linux/sched.h>
tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new
{
        @qtime[args->pid] = nsecs;
}

tracepoint:sched:sched_switch
{
        if (args->prev_state == TASK_RUNNING) {
                @qtime[args->prev_pid] = nsecs;
        }

        $ns = @qtime[args->next_pid];
        if ($ns) {
                @usecs = hist((nsecs - $ns) / 1000);
        }
        delete(@qtime[args->next_pid]);
}

执行过程

sequenceDiagram
    participant Wakeup
    participant Runqueue
    participant ContextSwitch
    participant CPU

    Note right of Wakeup: Task becomes runnable
    Wakeup->>Runqueue: @qtime[pid] = nsecs (Timestamp recorded)
    
    Note over ContextSwitch: CPU switches tasks
    ContextSwitch->>Runqueue: For PREV task:
    alt Still runnable?
        ContextSwitch->>Runqueue: Update @qtime[prev_pid] = nsecs
    end
    ContextSwitch->>CPU: For NEXT task:
    ContextSwitch->>Runqueue: Get $ns = @qtime[next_pid]
    ContextSwitch->>CPU: Calculate latency: current_time - $ns
    ContextSwitch->>Histogram: Record to @usecs
    ContextSwitch->>Runqueue: Delete @qtime[next_pid]

一些 bpftrace 命令

 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
41
42
43
44
45
46
47
48
49
50
51
52
# 跟踪新进程,包括进程参数:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'


# 输出哪个进程执行了哪个新进程:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'


# 按进程统计系统调用的数量:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'


# 按系统调用的探针名字来统计系统调用的数量:
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'


# 按系统调用的函数名来统计系统调用的数量:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter {
    @[sym(*(kaddr("sys_call_table") + args->id * 8))] = count(); }'
	

# 以 99Hz 的频率采样正在运行的进程名:
bpftrace -e 'profile:hz:99 { @[comm] = count(); }'


# 以 49Hz 的频率采样进程 ID 为 189 的用户态调用栈信息:
bpftrace -e 'profile:hz:49 /pid == 189/ { @[ustack] = count(); }'	


# 采样所有的进程名和调用栈信息:
bpftrace -e 'profile:hz:49 { @[ustack, stack, comm] = count(); }'


# 按 99Hz 的频率采样正在运行的 CPU,并且以线性直方图输出:
bpftrace -e 'profile:hz:99 { @cpu = lhist(cpu, 0, 256, 1); }'


# 统计内核中以 "vfs_" 开头的函数调用频率:
bpftrace -e 'kprobe:vfs_* { @[func] = count(); }'


# 按名字和内核调用栈来统计 SMP 调用:
bpftrace -e 'kprobe:smp_call* { @[probe, kstack(5)] = count(); }'


# 按名字和内核调用栈来统计 Intel x2APIC 调用:
bpftrace -e 'kprobe:x2apic_send_IPI* { @[probe, kstack(5)] = count(); }'


# 跟踪通过 pthread_create () 创建的新线程:
bpftrace -e 'u:/lib/x86_64-linux-gnu/libpthread-2.27.so:pthread_create {
    printf("%s by %s (%d)\n", probe, comm, pid); }'

内存

PF 跟踪工具可以给各种内存行为提供更多的信息,可以回答以下问题:

  • 为什么进程的物理内存占用(RSS)不停增长?
  • 哪些代码路径会导致缺页错误的发生?
  • 缺页错误来自哪些文件?
  • 哪些进程阻塞于页换入操作?
  • 全系统范围内创建了哪些内存映射?
  • 内存溢出(OOM Kill)事件发生时系统状态如何?
  • 哪些应用程序代码路径正在申请内存分配?
  • 应用程序分配了哪些类型的对象?
  • 是否有分配一段时间后还是没有释放的内存?(这意味着可能是泄漏的内存。)
事件类型 事件源
用户态内存分配 使用 uprobes 跟踪内存分配器函数,使用 USDT probes 跟踪 libc
内核态内存分配 使用 kprobes 跟踪内存分配器函数,以及 kmem 跟踪点
堆内存扩展 brk 系统调用跟踪点
共享内存函数 系统调用跟踪点
缺页错误 kprobes、软件事件,以及 exception 跟踪点
页迁移 migration 跟踪点
页压缩 compaction 跟踪点
VM 扫描器 vmscan 跟踪点
内存访问周期 PMC

查看 libc 支持的 usdt

1
bpftrace -l 'usdt:/lib/x86_64-linux-gnu/libc.so.6:libc:*'s

如果你刚刚开始学习内存性能分析,那么下面是一个推荐采用的分析策略:

  1. 检查系统信息中是否有 OOM Killer 杀掉进程的信息(例如,使用 dmesg (1))。
  2. 检查系统中是否配置了换页设备,以及使用的换页空间大小;并且检查这些换页设备是否有活跃的 I/O 操作(例如,使用 swap (1)、iostat (1)、vmstat (1))。
  3. 检查系统中空闲内存的数量,以及整个系统的缓存使用情况(例如,使用 free (1))。
  4. 按进程检查内存用量(例如,使用 top (1) 和 ps (1))。
  5. 检查系统中缺页错误的发生频率,并且检查缺页错误发生时的调用栈信息,这可以解释 RSS 增长的原因。
  6. 检查缺页错误和哪些文件有关。
  7. 通过跟踪 brk () 和 mmap () 调用来从另一个角度审查内存用量。
  8. 从本章中列出的 BPF 工具中寻找合适的工具并执行。
  9. 使用 PMC 测量硬件缓存命中率和内存访问(最好启用 PEBS),以便分析导致内存 I/O 发生的函数和指令信息(例如,使用 perf (1))。

传统工具

工具 类型 描述
dmesg 内核日志 OOM Killer 事件的详细信息
swapon 内核统计数据 换页设备的使用量
free 内核统计数据 全系统的内存用量
ps 内核统计数据 每进程的统计信息,包括内存用量
pmap 内核统计数据 按内存段列出进程内存用量
vmstat 内核统计数据 各种各样的统计信息,包括内存
sar 内核统计数据 可以显示换页错误和页扫描的频率
perf 软件事件、硬件统计、硬件采样 内存相关的 PMC 统计信息和事件采样信息

BPF 工具

工具 来源 目标 介绍
oomkill BCC/BT OOM 展示 OOM Killer 事件的详细信息
memleak BCC 调度 展示可能有内存泄漏的代码路径
mmapsnoop 本书 系统调用 跟踪全系统的 mmap(2) 调用
brkstack 本书 系统调用 展示 brk() 调用对应的用户态代码调用栈
shmsnoop BCC 系统调用 跟踪共享内存相关的调用信息
faults 本书 Faults 按用户调用栈展示缺页错误
ffaults 本书 Faults 按文件名展示缺页错误
vmscan 本书 VM 测量 VM 扫描器的收缩和回收时间
drsnoop BCC VM 跟踪直接回收时间,并且显示延迟信息
swapin 本书 VM 按进程展示页换入信息
hfaults 本书 Faults 按进程展示巨页的缺页错误信息

ffaults 的 bpftrace代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/local/bin/bpftrace

#include <linux/mm.h>

kprobe:handle_mm_fault
{
    $vma = (struct vm_area_struct *)arg0;
    $file = $vma->vm_file->f_path.dentry->d_name.name;
    @[str($file)] = count();
}

vmscan.bt

 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
41
42
43
44
45
46
47
48
49
50
51
#!/usr/local/bin/bpftrace

tracepoint:vmscan:mm_shrink_slab_start { @start_ss[tid] = nsecs; }
tracepoint:vmscan:mm_shrink_slab_end /@start_ss[tid]/
{
    $dur_ss = nsecs - @start_ss[tid];
    @sum_ss = @sum_ss + $dur_ss;
    @shrink_slab_ns = hist($dur_ss);
    delete(@start_ss[tid]);
}

tracepoint:vmscan:mm_vmscan_direct_reclaim_begin { @start_dr[tid] = nsecs; }
tracepoint:vmscan:mm_vmscan_direct_reclaim_end /@start_dr[tid]/
{
    $dur_dr = nsecs - @start_dr[tid];
    @sum_dr = @sum_dr + $dur_dr;
    @direct_reclaim_ns = hist($dur_dr);
    delete(@start_dr[tid]);
}

tracepoint:vmscan:mm_vmscan_memcg_reclaim_begin { @start_mr[tid] = nsecs; }
tracepoint:vmscan:mm_vmscan_memcg_reclaim_end /@start_mr[tid]/
{
    $dur_mr = nsecs - @start_mr[tid];
    @sum_mr = @sum_mr + $dur_mr;
    @memcg_reclaim_ns = hist($dur_mr);
    delete(@start_mr[tid]);
}

tracepoint:vmscan:mm_vmscan_wakeup_kswapd { @count_wk++; }

tracepoint:vmscan:mm_vmscan_writepage { @count_wp++; }

BEGIN
{
    printf("%-10s %10s %12s %12s %6s %9s\n", "TIME",
        "S-SLABms", "D-RECLAIMms", "M-RECLAIMms", "KSWAPD", "WRITEPAGE");
}

interval:s:1
{
    time("%H:%M:%S");
    printf("    %10d %12d %12d %6d %9d\n",
        @sum_ss / 1000000, @sum_dr / 1000000, @sum_mr / 1000000,
        @count_wk, @count_wp);
    clear(@sum_ss);
    clear(@sum_dr);
    clear(@sum_mr);
    clear(@count_wk);
    clear(@count_wp);
}

每秒输出的列包括如下几个。

  • S-SLABms:收缩 slab 所花的全部时间,以毫秒为单位。这是从各种内核缓存中回收内存。
  • D-RECLAIMms:直接回收所花的时间,以毫秒为单位。这是前台回收过程,在此期间内存被换入磁盘中,并且内存分配处于阻塞状态。
  • M-RECLAIMms:内存 cgroup 回收所花的时间,以毫秒为单位。如果使用了内存 cgroups,此列显示当 cgroup 超出内存限制,导致该 cgroup 进行内存回收的时间。
  • KSWAPD:kswapd 唤醒的次数。
  • WRITEPAGE:kswapd 写入页的数量。

swapin 的 bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/local/bin/bpftrace

kprobe:swap_readpage
{
        @[comm, pid] = count();
}

interval:s:1
{
        time();
        print(@);
        clear(@);
}

hfaults.bt 的 bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing Huge Page faults per process... Hit Ctrl-C to end.\n");
}

kprobe:hugetlb_fault
{
    @[pid, comm] = count();
}

BCC 单行程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 根据用户态调用栈信息统计进程堆内存扩展(brk ()):
stackcount -U t:syscalls:sys_enter_brk

# 根据用户态调用栈信息统计缺页错误:
stackcount -U t:exceptions:page_fault_user

# 通过跟踪点来统计 vmscan 操作:
funccount 't:vmscan:*'

# 按进程展示 hugepage_madvise () 调用:
trace hugepage_madvise

# 统计页迁移事件:
funccount t:migrate:mm_migrate_pages

# 统计页压缩事件:
trace t:compaction:mm_compaction_begin

bpftrace 单行程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 根据用户态调用栈信息统计进程堆内存扩展(brk ()):bpftrace -e tracepoint:syscalls:sys_enter_brk { @[ustack, comm] = count(); }

# 按进程统计缺页错误:
bpftrace -e 'software:page-fault:1 { @[comm, pid] = count(); }'

# 根据用户态调用栈信息统计缺页错误:
bpftrace -e 'tracepoint:exceptions:page_fault_user { @[ustack, comm] = count(); }'

# 通过跟踪点来统计 vmscan 操作:
bpftrace -e 'tracepoint:vmscan:* { @[probe] = count(); }'

# 按进程展示 hugepage_madvise () 调用:
bpftrace -e 'kprobe:hugepage_madvise { printf("%s by PID %d\n", probe, pid); }'

# 统计页迁移事件:
bpftrace -e 'tracepoint:migrate:mm_migrate_pages { @ = count(); }'

# 统计页压缩事件:
bpftrace -e 't:compaction:mm_compaction_begin { time(); }'

文件系统

BPF 能够帮助回答以下问题:

  • 发往文件系统的请求有哪些?可以按类型分别计数。
  • 文件系统的读请求大小如何?
  • 有多少写 I/O 是同步请求?
  • 文件访问模式是随机请求还是顺序请求?
  • 哪些文件正在被访问?按进程和代码路径统计?按字节数和 I/O 数量统计?
  • 发生了哪些文件系统错误?哪些类型的文件错误,是哪个进程造成的?
  • 文件系统的延迟来源是哪里?是磁盘,某段代码调用路径,还是锁?
  • 文件系统延迟的分布情况如何?
  • Dcache 和 Icache 的命中率和命空率的比例如何?
  • 对读操作来说,页缓存的命中率如何?
  • 预读取/ 预缓存的效果如何? 这些是否需要调整?

I/O 服务栈

Linux 文件系统缓存

I/O 类型和事件源

I/O 类型 事件源
应用程序和库函数 I/O uprobes
系统调用 I/O 系统调用跟踪点
文件系统 I/O ext4(…) 跟踪点、kprobes
缓存命中(读)、写回(写) kprobes
缓存命空(读)、写入(写) kprobes
页缓存写回 writeback 跟踪点
物理磁盘 I/O block 跟踪点、kprobes
裸 I/O kprobes

传统工具,主要关注性能,几乎没有观察文件系统的

  • df
  • mount
  • strace
  • perf
  • fatrace
  • 基准测试工具: fio

BPF 工具

工具 来源 目标 介绍
opensnoop BCC/BT 系统调用 跟踪文件打开信息
statsnoop BCC/BT 系统调用 跟踪 stat(2) 调用的各种变体
syncsnoop BCC/BT 系统调用 跟踪 sync(2) 调用以及各种变体,带时间戳信息
mmapfiles 本书 系统调用 统计 mmap(2) 涉及的文件
scread 本书 系统调用 统计 read(2) 涉及的文件
fmapfault 本书 页缓存 统计文件映射相关的缺页错误
filelife BCC/本书 VFS 跟踪短时文件,按秒记录它们的生命周期
vfsstat BCC/BT VFS 统计常见的 VFS 操作
vfscount BCC/BT VFS 统计所有的 VFS 操作
vfssize 本书 VFS 展示 VFS 读/写的尺寸
fsrwstat 本书 VFS 按文件系统类型展示 VFS 读/写数量
fileslower BCC/本书 VFS 展示较慢的文件读/写操作
filetop BCC VFS 按 IOPS 和字节数排序展示文件
filetype 本书 VFS 按文件类型和进程显示 VFS 读/写操作
writesync 本书 VFS 按 sync 开关展示文件写操作
cachestat BCC 页缓存 页缓存相关统计
writeback BT 页缓存 展示写回事件和对应的延迟信息
dcstat BCC/本书 Dcache 目录缓存命中率统计信息
dcsnoop BCC/BT Dcache 跟踪目录缓存的查找操作
mountsnoop BCC VFS 跟踪系统中的挂载和卸载操作 (mount)
xfsslower BCC XFS 统计过慢的 XFS 操作
xfsdist BCC XFS 以直方图统计常见的 XFS 操作延迟
ext4dist BCC/本书 ext4 以直方图统计常见的 ext4 操作延迟
icstat 本书 Icache inode 缓存的命中率统计
bufgrow 本书 缓冲缓存 按进程和字节数统计缓冲缓存的增长
readahead 本书 VFS 展示预读取的命中率和效率

mmapfiles 的 bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/local/bin/bpftrace

#include <linux/mm.h>

kprobe:do_mmap
{
    $file = (struct file *)arg0;
    $name = $file->f_path.dentry;
    $dir1 = $name->d_parent;
    $dir2 = $dir1->d_parent;
    @[str($dir2->d_name.name), str($dir1->d_name.name),
        str($name->d_name.name)] = count();
}

scread 的 bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/local/bin/bpftrace

#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/fdtable.h>

tracepoint:syscalls:sys_enter_read
{
    $task = (struct task_struct *)curtask;
    $file = (struct file *)*($task->files->fdt->fd + args->fd);
    @filename[str($file->f_path.dentry->d_name.name)] = count();
}

fmapfault 的bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/local/bin/bpftrace

#include <linux/mm.h>

kprobe:filemap_fault
{
    $vf = (struct vm_fault *)arg0;
    $file = $vf->vma->vm_file->f_path.dentry->d_name.name;
    @[comm, str($file)] = count();
}

vfssize 的 bpftrace 代码

 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
#!/usr/local/bin/bpftrace

#include <linux/fs.h>

kprobe:vfs_read,
kprobe:vfs_readv,
kprobe:vfs_write,
kprobe:vfs_writev
{
    @file[tid] = arg0;
}

kretprobe:vfs_read,
kretprobe:vfs_readv,
kretprobe:vfs_write,
kretprobe:vfs_writev
/@file[tid]/
{
    if (retval >= 0) {
        $file = (struct file *)@file[tid];
        $name = $file->f_path.dentry->d_name.name;
        if ((($file->f_inode->i_mode >> 12) & 15) == DT_FIFO) {
            @[comm, "FIFO"] = hist(retval);
        } else {
            @[comm, str($name)] = hist(retval);
        }
    }
    delete(@file[tid]);
}

END
{
    clear(@file);
}

fsrwstat 的 bpftrace 代码

 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
#!/usr/local/bin/bpftrace

#include <linux/fs.h>

BEGIN
{
    printf("Tracing VFS reads and writes... Hit Ctrl-C to end.\n");
}

kprobe:vfs_read,
kprobe:vfs_readv,
kprobe:vfs_write,
kprobe:vfs_writev
{
    @[str(((struct file *)arg0)->f_inode->i_sb->s_type->name), func] = count();
}

interval:s:1
{
    time(); print(@); clear(@);
}

END
{
    clear(@);
}

filetype 的 bpftrace 代码

 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
#!/usr/local/bin/bpftrace

#include <linux/fs.h>

BEGIN
{
    // from uapi/linux/stat.h:
    @type[0xc000] = "socket";
    @type[0xa000] = "link";
    @type[0x8000] = "regular";
    @type[0x6000] = "block";
    @type[0x4000] = "directory";
    @type[0x2000] = "character";
    @type[0x1000] = "fifo";
    @type[0] = "other";
}

kprobe:vfs_read,
kprobe:vfs_readv,
kprobe:vfs_write,
kprobe:vfs_writev
{
    $file = (struct file *)arg0;
    $mode = $file->f_inode->i_mode;
    @[@type[$mode & 0xf000], func, comm] = count();
}

END
{
    clear(@type);
}

writesync 的 bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/local/bin/bpftrace

#include <linux/fs.h>
#include <asm-generic/fcntl.h>

BEGIN
{
    printf("Tracing VFS write sync flags... Hit Ctrl-C to end.\n");
}

kprobe:vfs_write,
kprobe:vfs_writev
{
    $file = (struct file *)arg0;
    $name = $file->f_path.dentry->d_name.name;
    if (((($file->f_inode->i_mode >> 12) & 15) == DT_REG)) {
        if ($file->f_flags & O_DSYNC) {
            @sync[comm, str($name)] = count();
        } else {
            @regular[comm, str($name)] = count();
        }
    }
}

writeback 的 bpftrace 代码

 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
41
42
43
44
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing writeback... Hit Ctrl-C to end.\n");
    printf("%-9s %-8s %-8s %-16s %s\n", "TIME", "DEVICE", "PAGES", "REASON", "ms");

    // see /sys/kernel/debug/tracing/events/writeback/writeback_start/format
    @reason[0] = "background";
    @reason[1] = "vmscan";
    @reason[2] = "sync";
    @reason[3] = "periodic";
    @reason[4] = "laptop_timer";
    @reason[5] = "free_more_memory";
    @reason[6] = "fs_free_space";
    @reason[7] = "forker_thread";
}

tracepoint:writeback:writeback_start
{
    @start[args->sb_dev] = nsecs;
    @pages[args->sb_dev] = args->nr_pages;
}

tracepoint:writeback:writeback_written
/@start[args->sb_dev]/
{
    $sb_dev = args->sb_dev;
    $s = @start[$sb_dev];
    $lat = $s ? (nsecs - $s) / 1000 : 0;
    $pages = @pages[args->sb_dev] - args->nr_pages;

    time("%H:%M:%S  ");
    printf("%-8s %-8d %-16s %d.%03d\n", args->name, $pages, @reason[args->reason], $lat / 1000, $lat % 1000);

    delete(@start[$sb_dev]);
    delete(@pages[$sb_dev]);
}

END
{
    clear(@reason);
    clear(@start);
}

dcstat 的 bpftrace 代码

 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
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing dcache lookups... Hit Ctrl-C to end.\n");
    printf("%10s %10s %5s\n", "REFS", "MISSES", "HIT%");
}

kprobe:lookup_fast { @hits++; }

kretprobe:d_lookup /retval == 0/ { @misses++; }

interval:s:1
{
    $refs = @hits + @misses;
    $percent = $refs > 0 ? 100 * @hits / $refs : 0;
    printf("%10d %10d %4d%%\n", $refs, @misses, $percent);
    clear(@hits);
    clear(@misses);
}

END
{
    clear(@hits);
    clear(@misses);
}

bufgrow 的 bpftrace 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/local/bin/bpftrace

#include <linux/fs.h>

kprobe:add_to_page_cache_lru
{
    $as = (struct address_space *)arg1;
    $mode = $as->host->i_mode;
    // match block mode, uapi/linux/stat.h:
    if ($mode & 0x6000) {
        @kb[comm] = sum(4);     // page size
    }
}

readahead 的 bpftrace 代码

 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
#!/usr/local/bin/bpftrace

kprobe:__do_page_cache_readahead    { @in_readahead[tid] = 1; }
kretprobe:__do_page_cache_readahead { @in_readahead[tid] = 0; }

kretprobe:__page_cache_alloc
/@in_readahead[tid]/
{
    @birth[retval] = nsecs;
    @rapages++;
}

kprobe:mark_page_accessed
/@birth[arg0]/
{
    @age_ms = hist((nsecs - @birth[arg0]) / 1000000);
    delete(@birth[arg0]);
    @rapages--;
}

END
{
    printf("\nReadahead unused pages: %d\n", @rapages);
    printf("\nReadahead used page age (ms):\n");
    print(@age_ms); clear(@age_ms);
    clear(@birth); clear(@in_readahead); clear(@rapages);
}

单行程序

例子

 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
41
# 按进程名跟踪通过 creat (2) 创建的文件:
trace 't:syscalls:sys_enter_creat "%s", args->pathname'

# 按文件名统计 newstat (2) 调用:
argdist -C 't:syscalls:sys_enter_newstat():char*:args->filename'

# 按系统调用方式统计 read 系统调用:
funccount 't:syscalls:sys_enter_*read*'

# 按系统调用方式统计 write 系统调用:
funccount 't:syscalls:sys_enter_*write*'

#展示 read () 系统调用的请求大小分布:
argdist -H 't:syscalls:sys_enter_read():int:args->count'

# 展示 read () 系统调用的实际读取字节数(以及错误):
argdist -H 't:syscalls:sys_exit_read():int:args->ret'

# 按错误代码统计 read () 系统调用的错误:
argdist -C 't:syscalls:sys_exit_read():int:args->ret:args->ret<0'

# 统计 VFS 调用:
funccount 'vfs_*'

# 统计 ext4 跟踪点:
funccount 't:ext4:*'

# 统计 XFS 跟踪点:
funccount 't:xfs:*'

# 按进程名和调用栈信息统计 ext4 文件读取操作:
stackcount ext4_file_read_iter

# 跟踪 ZFS spa_sync () 时间:
trace -T 'spa_sync "ZFS spa_sync()"'

# 按进程名和调用栈信息统计使用 read_pages 向存储设备进行的文件系统读取操作:
stackcount -P read_pages

# 按调用栈和进程名统计所有通过 ext4 向存储设备进行的读取操作:
stackcount -P ext4_readpages

例子程序

 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#按进程名统计通过 open (2) 打开的文件:
bpftrace -e '
t:syscalls:sys_enter_open { 
printf("%s %s\n", comm, str(args->filename)); 
}'

#按进程名统计通过 creat (2) 创建的文件:
bpftrace -e '
t:syscalls:sys_enter_creat { 
    printf("%s %s\n", comm, str(args->pathname)); 
}'

# 按文件名统计 newstat (2) 调用:
bpftrace -e '
t:syscalls:sys_enter_newstat {
   @[str(args->filename)] = count(); 
}'

# 按系统调用方式统计 read 系统调用:
bpftrace -e '
  tracepoint:syscalls:sys_enter_*read* { @[probe] = count(); 
}'


# 按系统调用方式统计 write 系统调用:
bpftrace -e '
  tracepoint:syscalls:sys_enter_*write* { @[probe] = count(); 
}'

# 展示 read () 系统调用的请求大小分布:
bpftrace -e '
  tracepoint:syscalls:sys_enter_read { @ = hist(args->count); 
}'

# 展示 read () 系统调用的实际读取字节数(以及错误):
bpftrace -e '
tracepoint:syscalls:sys_exit_read { 
  @ = hist(args->ret); 
}'

# 统计 VFS 调用:
bpftrace -e '
kprobe:vfs_* {
 @[probe] = count(); 
}'

# 统计 ext4 跟踪点:
bpftrace -e '
  tracepoint:ext4:* { @[probe] = count(); 
}'

# 统计 XFS 跟踪点:
bpftrace -e '
tracepoint:xfs:* {
 @[probe] = count(); 
}'

# 按进程名和调用栈信息统计 ext4 文件读取操作:
bpftrace -e '
kprobe:ext4_file_read_iter {
 @[comm] = count(); 
}'

# 按进程名和用户态调用栈信息统计 ext4 文件读取操作:
bpftrace -e '
kprobe:ext4_file_read_iter {
 @[ustack, comm] = count(); 
}'

磁盘

BPF 跟踪工具可以针对磁盘操作提供更多的信息,并能回答以下问题:

  • 具体都有哪些磁盘 I/O 请求?分别是什么类型的,各有多少,以及 I/O 请求的尺寸是多少?
  • 请求时长是多少?排队等待时长是多少?
  • 是否存在延迟超标的情况
  • 延迟分布是否呈多峰分布?
  • 是否有任何磁盘错误?
  • 具体发布了哪些 SCSI 命令?
  • 是否有任何超时情况?

要回答这些问题,可以通过跟踪 I/O 在块 I/O 软件栈中的传递过程来完成。

传统工具

  • iostat
  • perf
  • blktrace
  • SCSI 日志
  • dmesg
工具 来源 目标 介绍
biolatency BCC/BT 块I/O 以直方图形式统计块I/O延迟
biosnoop BCC/BT 块I/O 按PID和延迟阈值跟踪块I/O
biotop BCC 块I/O top工具的磁盘版:按进程统计块I/O
bitesize BCC/BT 块I/O 按进程统计磁盘I/O请求尺寸直方图
seeksize 本书 块I/O 展示I/O寻址(seek)的平均距离
biopattern 本书 块I/O 识别随机/顺序式磁盘访问模式
biostacks 本书 块I/O 展示磁盘I/O相关的初始化软件栈信息
bioerr 本书 块I/O 跟踪磁盘错误
mdflush BCC/BT MD 跟踪MD的写空请求
iosched 本书 I/O sched 统计I/O调度器的延迟
scsilatency 本书 SCSI 展示SCSI命令延迟分布情况
scsiresult 本书 SCSI 展示SCSI命令结果代码
nvmelatency 本书 NVME 统计NVME驱动程序的命令延迟

bpftrace 例子

seeksize

 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
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing block I/O requested seeks... Hit Ctrl-C to end.\n");
}

tracepoint:block:block_rq_issue
{
    if (@last[args->dev]) {
        // calculate requested seek distance
        $last = @last[args->dev];
        $dist = (args->sector - $last) > 0 ?
            args->sector - $last : $last - args->sector;

        // store details
        @sectors[args->comm] = hist($dist);
    }
    // save last requested position of disk head
    @last[args->dev] = args->sector + args->nr_sector;
}

END
{
    clear(@last);
}

biopattern

 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
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("%-8s %5s %5s %8s %10s\n", "TIME", "%RND", "%SEQ", "COUNT", "KBYTES");
}

tracepoint:block:block_rq_complete
{
    if (@lastsector[args->dev] == args->sector) {
        @sequential++;
    } else {
        @random++;
    }
    @bytes = @bytes + args->nr_sector * 512;
    @lastsector[args->dev] = args->sector + args->nr_sector;
}

interval:s:1
{
    $count = @random + @sequential;
    $div = $count;
    if ($div == 0) {
        $div = 1;
    }
    time("%H:%M:%S ");
    printf("%5d %5d %8d %10d\n", @random * 100 / $div,
        @sequential * 100 / $div, $count, @bytes / 1024);
    clear(@random); clear(@sequential); clear(@bytes);
}

END
{
    clear(@lastsector);
    clear(@random); clear(@sequential); clear(@bytes);
}

biostacks

 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
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing block I/O with init stacks. Hit Ctrl-C to end.\n");
}

kprobe:blk_account_io_start
{
    @reqstack[arg0] = kstack;
    @reqts[arg0] = nsecs;
}

kprobe:blk_start_request,
kprobe:blk_mq_start_request
/@reqts[arg0]/
{
    @usecs[@reqstack[arg0]] = hist(nsecs - @reqts[arg0]);
    delete(@reqstack[arg0]);
    delete(@reqts[arg0]);
}

END
{
    clear(@reqstack); clear(@reqts);
}

bioerr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing block I/O errors. Hit Ctrl-C to end.\n");
}

tracepoint:block:block_rq_complete
/args->error != 0/
{
    time("%H:%M:%S ");
    printf("device: %d,%d, sector: %d, bytes: %d, flags: %s, error: %d\n",
        args->dev >> 20, args->dev & ((1 << 20) - 1), args->sector,
        args->nr_sector * 512, args->rwbs, args->error);
}

iosched

 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
#!/usr/local/bin/bpftrace

#include <linux/blkdev.h>

BEGIN
{
    printf("Tracing block I/O schedulers. Hit Ctrl-C to end.\n");
}

kprobe:__elv_add_request
{
    @start[arg1] = nsecs;
}

kprobe:blk_start_request,
kprobe:blk_mq_start_request
/@start[arg0]/
{
    $r = (struct request *)arg0;
    @usecs[$r->q->elevator->type->elevator_name] = hist((nsecs - @start[arg0]) / 1000);
    delete(@start[arg0]);
}

END
{
    clear(@start);
}

scsilatency

 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
#!/usr/local/bin/bpftrace

#include <scsi/scsi_cmnd.h>

BEGIN
{
    printf("Tracing scsi latency. Hit Ctrl-C to end.\n");
    // SCSI opcodes from scsi/scsi_proto.h; add more mappings if desired:
    @opcode[0x00] = "TEST_UNIT_READY";
    @opcode[0x03] = "REQUEST_SENSE";
    @opcode[0x08] = "READ_6";
    @opcode[0x0a] = "WRITE_6";
    @opcode[0x0b] = "SEEK_6";
    @opcode[0x12] = "INQUIRY";
    @opcode[0x18] = "ERASE";
    @opcode[0x28] = "READ_10";
    @opcode[0x2a] = "WRITE_10";
    @opcode[0x2b] = "SEEK_10";
    @opcode[0x35] = "SYNCHRONIZE_CACHE";
}

kprobe:scsi_init_io
{
    @start[arg0] = nsecs;
}

kprobe:scsi_done,
kprobe:scsi_mq_done
/@start[arg0]/
{
    $cmnd = (struct scsi_cmnd *)arg0;
    $opcode = *$cmnd->req.cmd & 0xff;
    @usecs[$opcode, @opcode[$opcode]] = hist((nsecs - @start[arg0]) / 1000);
}

END
{
    clear(@start); clear(@opcode);
}

scsiresult

 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
41
42
43
44
45
46
47
48
49
50
51
52
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing scsi command results. Hit Ctrl-C to end.\n");

    // host byte codes, from include/scsi/scsi.h:
    @host[0x00] = "DID_OK";
    @host[0x01] = "DID_NO_CONNECT";
    @host[0x02] = "DID_BUS_BUSY";
    @host[0x03] = "DID_TIME_OUT";
    @host[0x04] = "DID_BAD_TARGET";
    @host[0x05] = "DID_ABORT";
    @host[0x06] = "DID_PARITY";
    @host[0x07] = "DID_ERROR";
    @host[0x08] = "DID_RESET";
    @host[0x09] = "DID_BAD_INTR";
    @host[0x0a] = "DID_PASSTHROUGH";
    @host[0x0b] = "DID_SOFT_ERROR";
    @host[0x0c] = "DID_IMM_RETRY";
    @host[0x0d] = "DID_REQUEUE";
    @host[0x0e] = "DID_TRANSPORT_DISRUPTED";
    @host[0x0f] = "DID_TRANSPORT_FAILFAST";
    @host[0x10] = "DID_TARGET_FAILURE";
    @host[0x11] = "DID_NEXUS_FAILURE";
    @host[0x12] = "DID_ALLOC_FAILURE";
    @host[0x13] = "DID_MEDIUM_ERROR";

    // status byte codes, from include/scsi/scsi_proto.h:
    @status[0x00] = "SAM_STAT_GOOD";
    @status[0x02] = "SAM_STAT_CHECK_CONDITION";
    @status[0x04] = "SAM_STAT_CONDITION_MET";
    @status[0x08] = "SAM_STAT_BUSY";
    @status[0x10] = "SAM_STAT_INTERMEDIATE";
    @status[0x14] = "SAM_STAT_INTERMEDIATE_CONDITION_MET";
    @status[0x18] = "SAM_STAT_RESERVATION_CONFLICT";
    @status[0x22] = "SAM_STAT_COMMAND_TERMINATED";
    @status[0x28] = "SAM_STAT_TASK_SET_FULL";
    @status[0x30] = "SAM_STAT_ACA_ACTIVE";
    @status[0x40] = "SAM_STAT_TASK_ABORTED";
}

tracepoint:scsi:scsi_dispatch_cmd_done
{
    @[@host[(args->result >> 16) & 0xff], @status[args->result & 0xff]] = count();
}

END
{
    clear(@status);
    clear(@host);
}

nvmelatency

 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
41
42
43
44
45
46
47
48
#!/usr/local/bin/bpftrace

#include <linux/blkdev.h>
#include <linux/nvme.h>

BEGIN
{
    printf("Tracing nvme command latency. Hit Ctrl-C to end.\n");
    // from linux/nvme.h:
    @iopcode[0x00] = "nvme_cmd_flush";
    @iopcode[0x01] = "nvme_cmd_write";
    @iopcode[0x02] = "nvme_cmd_read";
    @iopcode[0x04] = "nvme_cmd_write_uncor";
    @iopcode[0x05] = "nvme_cmd_compare";
    @iopcode[0x08] = "nvme_cmd_write_zeros";
    @iopcode[0x09] = "nvme_cmd_dsm";
    @iopcode[0x0d] = "nvme_cmd_resv_register";
    @iopcode[0x0e] = "nvme_cmd_resv_report";
    @iopcode[0x11] = "nvme_cmd_resv_acquire";
    @iopcode[0x15] = "nvme_cmd_resv_release";
}

kprobe:nvme_setup_cmd
{
    $req = (struct request *)arg1;
    if ($req->rq_disk) {
        @start[arg1] = nsecs;
        @cmd[arg1] = arg2;
    } else {
        @admin_commands = count();
    }
}

kprobe:nvme_complete_rq
/@start[arg0]/
{
    $req = (struct request *)arg0;
    $cmd = (struct nvme_command *)@cmd[arg0];
    $disk = $req->rq_disk;
    $opcode = $cmd->common.opcode & 0xff;
    @usecs[$disk->disk_name, @iopcode[$opcode]] = hist((nsecs - @start[arg0]) / 1000);
    delete(@start[tid]); delete(@cmd[tid]);
}

END
{
    clear(@iopcode); clear(@start); clear(@cmd);
}

bpftrace 程序

 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
# 统计块 I/O 跟踪点:
bpftrace -e 'tracepoint:block:* { @[probe] = count(); }'

# 以直方图方式统计块 I/O 尺寸:
bpftrace -e 't:block:block_rq_issue { @bytes = hist(args->bytes); }'

# 统计块 I/O 请求的用户态调用栈:
bpftrace -e 't:block:block_rq_issue { @[ustack] = count(); }'

# 统计块 I/O 的类型标记:
bpftrace -e 't:block:block_rq_issue { @[args->rwbs] = count(); }'

# 按 I/O 类型统计总字节数:
bpftrace -e 't:block:block_rq_issue { @[args->rwbs] = sum(args->bytes); }'

# 按设备和 I/O 类型跟踪块 I/O 错误:
bpftrace -e 't:block:block_rq_complete /args->error/ { printf("dev %d type %s error %d\n", args->dev, args->rwbs, args->error); }'

# 以直方图方式统计块 I/O plug 时间:
bpftrace -e 'k:blk_start_plug { @ts[arg0] = nsecs; } k:blk_flush_plug_list /@ts[arg0]/ { @plug_ns = hist(nsecs - @ts[arg0]); delete(@ts[arg0]); }'

# 统计 SCSI opcode:
bpftrace -e 't:scsi:scsi_dispatch_cmd_start { @opcode[args->opcode] = count(); }'

# 统计 SCSI 结果代码(包括全部 4 字节):
bpftrace -e 't:scsi:scsi_dispatch_cmd_done { @result[args->result] = count(); }'

# 统计 blk_mq 请求的 CPU 分布:
bpftrace -e 'k:blk_mq_start_request { @swqueues = lhist(cpu, 0, 100, 1); }'

# 统计 scsi 驱动程序函数:
bpftrace -e 'kprobe:scsi* { @[func] = count(); }'

# 统计 nvme 驱动程序函数:
funccount 'nvme*'

网络

linux 网络栈
3.jpg

传统网络性能工具使用的是内核内部的统计信息,以及网络抓包能力。BPF 跟踪工具可以提供更多信息,回答类似下面的问题:

  • 目前发生的网络套接字 I/O 有哪些,为什么会发生?对应的用户态调用栈是什么?
  • 有哪些新 TCP 连接被创建,是哪个进程创建的?
  • 目前是否有网络套接字、TCP,以及 IP 级的错误发生?
  • TCP 窗口的尺寸是多少?是否有 0 字节传送发生?
  • 各个软件栈层面的 I/O 尺寸分别是多少?发送给设备的 I/O 尺寸是多少?
  • 哪些包是被网络软件栈丢弃了的?原因是什么?
  • TCP 连接延迟、首字节延迟、连接时长分别是多少?
  • 内核网络软件栈各层之间的延迟是多少?
  • 网络包在 qdisc 队列中的等待时间是多长?在网络驱动程序内置队列中的等待时长是多长?
  • 目前正在使用哪些高层协议?

传统工具

工具 类型 介绍
ss 内核统计 网络套接字统计
ip 内核统计 IP统计
nstat 内核统计 网络软件栈统计
netstat 内核统计 显示网络软件栈统计和状态的复合工具
sar 内核统计 显示网络和其他统计信息的复合工具
nicstat 内核统计 网络接口统计
ethtool 驱动程序统计 网络接口驱动程序统计
tcpdump 抓包 抓包分析

BPF 网络相关工具表

工具 来源 目标 介绍
sockstat 本书 套接字 套接字统计信息总览
sofamily 本书 套接字 按进程统计新套接字协议
soprotocol 本书 套接字 按进程统计新套接字传输协议
soconnect 本书 套接字 跟踪套接字的IP协议主动连接的细节信息
soaccept 本书 套接字 跟踪套接字的IP协议被动连接的细节信息
socketio 本书 套接字 套接字细节信息统计,包括I/O统计
socksize 本书 套接字 按进程展示套接字I/O尺寸直方图
sormem 本书 套接字 展示套接字接收缓冲区用量和溢出情况
soconnlat 本书 套接字 统计IP套接字连接延迟,带调用栈信息
solstbyte 本书 套接字 统计IP套接字的首字节延迟
tcpconnect BCC/BT/本书 TCP 跟踪TCP主动连接(connect())
tcpaccept BCC/BT/本书 TCP 跟踪TCP被动连接(accept())
tcplife BCC/本书 TCP 跟踪TCP连接时长,带连接细节信息
tcptop BCC TCP 按目的地展示TCP发送和接收吞吐量
tcpretrans BCC/BT TCP 跟踪TCP重传,带地址和TCP状态
tcpsynbl 本书 TCP 以直方图展示TCP SYN积压队列
tcpwin 本书 TCP 跟踪TCP发送中的阻塞窗口的细节信息
tcpnagle 本书 TCP 跟踪TCP中nagle算法的用量,以及发送延迟
udpconnect 本书 UDP 跟踪本机发起的UDP连接
gethostlatency 本书/BT DNS 通过库函数调用跟踪DNS查找延迟
ipecn 本书 IP 跟踪IP入栈显式阻塞通知(ECN)的细节
superping 本书 ICMP 测量网络软件栈中的ICMP echo时间
qdisc-fq(…) 本书 qdiscs 展示FQ队列管理器的延迟
netsize 本书 网络 展示网络设备I/O尺寸
nettxlat 本书 网络 展示网络设备发送延迟
skbdrop 本书 skbs 跟踪sk_buff丢弃情况,带内核调用栈信息
skblife 本书 skbs 在网络软件栈各层之间跟踪sk_buff的延迟
ieee80211scan 本书 WiFi 跟踪IEEE 802.11 WiFi扫描情况

其他值得一提的 BPF 工具有如下几个

  • solisten (8):BCC 工具,可以打印套接字 listen () 调用的细节。
  • tcpstates (8):BCC 工具,在每个 TCP 连接状态变化时就打印一行输出,包括 IP 地址、端口信息,以及每个状态所经历的时间。
  • tcpdrop (8):BCC 和 bpftrace 工具,当在内核中通过 tcp_drop () 函数丢弃 tcp 包时,打印 IP 地址、TCP 状态信息,以及内核调用栈信息。
  • sofsdsnoop (8):BCC 工具,跟踪所有通过 UNIX 套接字传递的文件描述符信息。
  • profile (8):在第 6 章中有详细描述,通过采样内核调用栈信息来分析网络相关代码路径所占的时间比例。
  • hardirq (8) 和 softirq (8):第 6 章中有详细描述,可以测量网络硬中断和软中断所消耗的时间。
  • filetype (8):第 8 章中介绍过的工具,跟踪 vfs_read () 和 vfs_write (),通过 inode 识别网络套接字的读写。
 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
# 按错误代码统计失败的 socket 调用
argdist -C 't:syscalls:sys_exit_connect():int:args->ret:args->ret<0'

# 按用户态调用栈统计套接字 connect (2) 调用:
stackcount -U t:syscalls:sys_enter_connect

# 以直方图形式统计 TCP 发送的字节数:
argdist -H 'p:tcp_sendmsg(void *sk, void *msg, int size):int:size'

# 以直方图形式统计 TCP 接收的字节数:
argdist -H 'r:tcp_recvmsg():int:$retval:$retval>0'

# 统计所有的 TCP 函数的调用频率(对所有的 TCP 操作都添加额外开销):
funccount 'tcp_*'

# 以直方图形式统计 UDP 发送的字节数:
argdist -H 'p:udp_sendmsg(void *sk, void *msg, int size):int:size'

# 以直方图形式统计 UDP 接收的字节数:
argdist -H 'r:udp_recvmsg():int:$retval:$retval>0'

# 统计所有的 UDP 函数的调用频率(对所有的 UDP 操作都添加额外开销):
funccount 'udp_*'

# 统计网络包发送的调用栈信息:
stackcount t:net:net_dev_xmit

# 统计 ieee80211 层的函数的调用频率(对所有网络包都添加额外开销):
funccount 'ieee80211_*'

# 统计所有 ixgbevf 设备驱动函数的调用频率(对 ixgbevf 驱动添加额外开销):
funccount 'ixgbevf_*'

bpftrace 程序

 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 按 PID 和进程名统计套接字 accept (2) 调用:
bpftrace -e 't:syscalls:sys_enter_accept* { @[pid, comm] = count(); }'

# 按 PID 和进程名统计套接字 connect (2) 调用:
bpftrace -e 't:syscalls:sys_enter_connect { @[pid, comm] = count(); }'

# 按进程名和错误代码统计失败的 connect (2) 调用:
bpftrace -e 't:syscalls:sys_exit_connect /args->ret < 0/ { @[comm, - args->ret] = count(); }'

# 按用户态调用栈统计套接字 connect (2) 调用:
bpftrace -e 't:syscalls:sys_enter_connect { @[ustack] = count(); }'

# 按发送 / 接收、在 CPU 上运行的、进程名统计套接字发送和接收的次数:
bpftrace -e 'k:sock_sendmsg,k:sock_recvmsg { @[func, pid, comm] = count(); }'

# 按在 CPU 上运行的 PID 和进程名统计套接字发送和接收的字节数:
bpftrace -e 'kr:sock_sendmsg,kr:sock_recvmsg /(int32)retval > 0/ { @[pid, comm] = sum((int32)retval); }'

# 按在 CPU 上运行的 PID 和进程名统计 TCP connect 调用:
bpftrace -e 'k:tcp_v*_connect { @[pid, comm] = count(); }'

# 按在 CPU 上运行的 PID 和进程名统计 TCP accept 调用:
bpftrace -e 'k:inet_csk_accept { @[pid, comm] = count(); }'

# 统计 TCP 的发送和接收次数:
bpftrace -e 'k:tcp_sendmsg,k:tcp*recvmsg { @[func] = count(); }'

# 按在 CPU 上运行的 PID 和进程名统计 TCP 发送 / 接收的次数:
bpftrace -e 'k:tcp_sendmsg,k:tcp_recvmsg { @[func, pid, comm] = count(); }'

# 以直方图形式统计 TCP 发送的字节数:
bpftrace -e 'k:tcp_sendmsg { @send_bytes = hist(arg2); }'

# 以直方图形式统计 TCP 接收的字节数:
bpftrace -e 'kr:tcp_recvmsg /retval >= 0/ { @recv_bytes = hist(retval); }'

# 按类型与远端主机(仅支持 IPv4)统计 TCP 重传:
bpftrace -e 't:tcp:tcp_retransmit_* { @[probe, ntop(2, args->saddr)] = count(); }'

# 统计所有的 TCP 函数的调用频率(对所有的 TCP 函数添加额外开销):
bpftrace -e 'k:tcp_* { @[func] = count(); }'

# 按在 CPU 上运行的 PID 和进程名统计 UDP 发送和接收的次数:
bpftrace -e 'k:udp*_sendmsg,k:udp*_recvmsg { @[func, pid, comm] = count(); }'

# 以直方图形式统计 UDP 发送的字节数:
bpftrace -e 'k:udp_sendmsg { @send_bytes = hist(arg2); }'

# 以直方图形式统计 UDP 接收的字节数:
bpftrace -e 'kr:udp_recvmsg /retval >= 0/ { @recv_bytes = hist(retval); }'

# 统计所有的 UDP 函数的调用频率(为所有 UDP 包增加额外开销):
bpftrace -e 'k:udp_* { @[func] = count(); }'

# 统计发送数据包时的内核态调用栈:
bpftrace -e 't:net:net_dev_xmit { @[kstack] = count(); }'

# 按每个设备进行 CPU 直方图统计:
bpftrace -e 't:net:netif_receive_skb { @[str(args->name)] = lhist(cpu, 0, 128, 1); }'

# 统计所有 ieee80211 层函数的调用频率(对所有数据包增加额外开销):
bpftrace -e 'k:ieee80211_* { @[func] = count(); }'

# 统计所有 ixgbevf 设备驱动函数的调用频率(对所有 ixgbevf 添加额外开销):
bpftrace -e 'k:ixgbevf_* { @[func] = count(); }'

# 统计所有 iwl 设备驱动中的跟踪点的调用频率(对 iwl 添加额外开销):
bpftrace -e 't:iwlwifi:*;t:iwlwifi_io:* { @[probe] = count(); }'

安全

很多策略都是用了 BPF 来执行

  • seccomp:安全计算(seccomp)工具可以执行 BPF 程序(目前仅限于经典 BPF)来制定有关允许系统调用的决策
  • Cilium:Cilium 为工作负载 —— 不论是应用程序容器还是进程 —— 都提供了透明安全的网络连接和负载均衡功能
  • bpfilter:bpfilter 是一个用 BPF 完全替代 iptables 防火墙的概念证明程序
  • Landlock:Landlock 是一个基于 BPF 的安全模块,使用 BPF 来提供内核资源的细粒度访问控制
  • KRSI:内核运行时安全插桩(Kernel Runtime Security Instrumentation)是一个来自 Google 的 Linux 安全模块,用于可扩展的审计和策略执行

安全相关的工具表

工具 来源 目标 描述
execsnoop BCC/BT 系统调用 列出新程序的执行
elfsnoop 本书 内核 显示ELF文件加载
modsnoop 本书 内核 显示内核模块加载
bashreadline BCC/BT bash 列出输入的bash命令行命令
shellsnoop 本书 shells 镜像shell输出
ttysnoop BCC/本书 TTY 镜像tty输出
opensnoop BCC/BT 系统调用 列出打开的文件
eperm 本书 系统调用 统计失败的EPERM和EACCES系统调用
tcpconnect BCC/BT TCP 跟踪TCP出站连接(主动)
tcpaccept BCC/BT TCP 跟踪TCP入站连接(被动)
tcpreset 本书 TCP 显示TCP连接重置:检测端口扫描
capable BCC/BT 安全 跟踪内核安全能力检查
setuids 本书 系统调用 跟踪setuid系统调用:权限提升

bcc 程序

1
2
3
4
5
6
7
8
#为 PID 为 1234 的进程统计安全审计事件数:
funccount -p 1234 'security_*'

# 跟踪可插入身份验证模块(PAM)会话的开始:
trace 'pam:pam_start "%s: %s", arg1, arg2'

# 跟踪内核模块加载:
trace 't:module:module_load "load: %s", args->name'

bpftrace 程序

1
2
3
4
5
6
7
8
#为 PID 为 1234 的进程统计安全审计事件数:
bpftrace -e 'k:security_* /pid == 1234 { @[func] = count(); }'

# 跟踪可插入身份验证模块(PAM)会话的开始:
bpftrace -e 'u:/lib/x86_64-linux-gnu/libpam.so.0:pam_start { printf("%s: %s\n", str(arg0), str(arg1)); }'

# 跟踪内核模块加载:
bpftrace -e 't:module:module_load { printf("load: %s\n", str(args->name)); }'

bashreadline

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/local/bin/bpftrace

BEGIN
{
    printf("Tracing bash commands... Hit Ctrl-C to end.\n");
    printf("%-9s %-6s %s\n", "TIME", "PID", "COMMAND");
}

uretprobe:/bin/bash:readline
{
    time("%H:%M:%S ");
    printf("%-6d %s\n", pid, str(retval));
}

参考