Make tutorials 50–53 safer to operate (#225)

* Make tutorials 50-53 safer to operate

* Make pacer conflict recovery interface-safe

* Keep tutorial prose on the user path

* Allow focused excerpts for long user-space loaders

---------

Co-authored-by: LinuxDev9002 <i@littlefisher.me>
This commit is contained in:
云微
2026-07-22 01:36:44 -07:00
committed by GitHub
parent b0d2c80dd5
commit 2cdcd8b0de
27 changed files with 1601 additions and 615 deletions

View File

@@ -1,8 +1,10 @@
# eBPF 教程:使用 fsession 追踪慢速 vfs_read 调用
当一个文件服务出现读延迟尖峰时,应用层计时只能告诉你请求变慢了,却无法区分是内核在 I/O 上阻塞还是用户态逻辑耗时。真正有用的问题是:哪个线程发起了读操作、请求了多少字节、调用返回了什么、这一次 `vfs_read` 花了多长时间?
当一个文件服务出现读延迟尖峰时,应用层计时只能告诉你请求变慢了,却无法区分是内核在 `vfs_read` 中耗时还是用户态逻辑耗时。真正有用的问题是:哪个线程发起了读操作、涉及哪个 VFS 对象、请求了多少字节、调用返回了什么、这一次 `vfs_read` 花了多长时间?
本教程展示如何使用 Linux 7.0 引入的 **fsession** 机制测量 `vfs_read` 调用延迟。fsession 是一种新的 eBPF 程序类型,它在函数进入时执行一次、返回时再执行一次,并提供内置的调用级存储来关联这两个阶段。我们构建的工具会在函数进入时记录时间戳、返回时计算延迟,按进程和阈值过滤后,通过 ring buffer 上报慢读事件。
本教程展示如何使用 Linux 7.0 引入的 **fsession** 机制测量 `vfs_read` 调用延迟。fsession 是一种新的 eBPF 程序类型,它在函数进入时执行一次、返回时再执行一次,并提供内置的调用级存储来关联这两个阶段。工具会在函数进入时记录时间戳、返回时计算延迟,按进程和阈值过滤后,通过 ring buffer 上报对象类型和 `major:minor:inode` 身份。
`vfs_read` 不只处理普通文件,pipe、字符设备等文件对象也会经过它。因此慢事件表示某次 `vfs_read` 较慢,并不必然说明存储较慢。应结合输出中的 `type` 和 `major:minor:inode` 身份,区分普通文件读、FIFO 以及其他 VFS 对象。
> 完整源码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/52-fsession-latency>
@@ -110,6 +112,10 @@ struct latency_event {
unsigned long long requested;
long long result;
unsigned long long latency_ns;
unsigned int device_major;
unsigned int device_minor;
unsigned long long inode;
unsigned int mode;
char comm[FSESSION_COMM_LEN];
};
@@ -131,9 +137,11 @@ struct latency_stats {
`FSESSION_COMM_LEN` 设为 16,与内核的 `TASK_COMM_LEN` 一致。
每个事件还携带 `device_major`、`device_minor`、`inode` 和 `mode`。BPF 程序把内核原始 `s_dev` 编码拆成 12 位主设备号和 20 位次设备号,再结合 `i_ino` 得到稳定的 VFS 对象身份;用户态根据 `i_mode` 输出 `regular`、`fifo`、`character` 等对象类型。
### BPF 程序
`fsession_latency.bpf.c` 是工具的核心。我们逐段分析。
`fsession_latency.bpf.c` 是工具的核心。下面是完整的内核态程序:
```c
// SPDX-License-Identifier: GPL-2.0
@@ -142,10 +150,108 @@ struct latency_stats {
#include "vmlinux.h"
#undef bpf_session_is_return
#undef bpf_session_cookie
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "fsession_latency.h"
#define KERNEL_MINOR_BITS 20
#define KERNEL_MINOR_MASK ((1U << KERNEL_MINOR_BITS) - 1)
char LICENSE[] SEC("license") = "GPL";
const volatile __u64 threshold_ns;
const volatile __u32 target_tgid;
struct latency_stats stats;
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
/*
* The repository vmlinux.h snapshot predates the ctx argument on these
* kfunc prototypes. Rename those stale declarations while including the
* snapshot, then provide the Linux 7.0 signatures below.
*/
extern bool bpf_session_is_return(void *ctx) __ksym;
extern __u64 *bpf_session_cookie(void *ctx) __ksym;
SEC("fsession/vfs_read")
int BPF_PROG(measure_vfs_read, struct file *file, char *buf, size_t count,
loff_t *pos, ssize_t ret)
{
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u64 *started = bpf_session_cookie(ctx);
struct latency_event *event;
struct inode *inode;
__u32 device;
__u64 latency;
if (!bpf_session_is_return(ctx)) {
if (target_tgid && pid_tgid >> 32 != target_tgid) {
*started = 0;
return 0;
}
*started = bpf_ktime_get_ns();
return 0;
}
if (!*started)
return 0;
latency = bpf_ktime_get_ns() - *started;
__sync_fetch_and_add(&stats.calls, 1);
if (ret < 0)
__sync_fetch_and_add(&stats.errors, 1);
if (latency < threshold_ns)
return 0;
__sync_fetch_and_add(&stats.slow, 1);
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
if (!event) {
__sync_fetch_and_add(&stats.dropped, 1);
return 0;
}
__builtin_memset(event, 0, sizeof(*event));
event->pid = (__u32)pid_tgid;
event->tgid = pid_tgid >> 32;
event->requested = count;
event->result = ret;
event->latency_ns = latency;
inode = BPF_CORE_READ(file, f_inode);
if (inode) {
device = BPF_CORE_READ(inode, i_sb, s_dev);
event->device_major = device >> KERNEL_MINOR_BITS;
event->device_minor = device & KERNEL_MINOR_MASK;
event->inode = BPF_CORE_READ(inode, i_ino);
event->mode = BPF_CORE_READ(inode, i_mode);
}
bpf_get_current_comm(event->comm, sizeof(event->comm));
bpf_ringbuf_submit(event, 0);
return 0;
}
```
下面继续逐段解释同一个程序。
```c
// SPDX-License-Identifier: GPL-2.0
#define bpf_session_is_return bpf_session_is_return_vmlinux_snapshot
#define bpf_session_cookie bpf_session_cookie_vmlinux_snapshot
#include "vmlinux.h"
#undef bpf_session_is_return
#undef bpf_session_cookie
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "fsession_latency.h"
#define KERNEL_MINOR_BITS 20
#define KERNEL_MINOR_MASK ((1U << KERNEL_MINOR_BITS) - 1)
char LICENSE[] SEC("license") = "GPL";
```
@@ -189,6 +295,8 @@ int BPF_PROG(measure_vfs_read, struct file *file, char *buf, size_t count,
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u64 *started = bpf_session_cookie(ctx);
struct latency_event *event;
struct inode *inode;
__u32 device;
__u64 latency;
```
@@ -235,20 +343,29 @@ TGID 在 `bpf_get_current_pid_tgid()` 返回值的高 32 位;低 32 位是线
return 0;
}
__builtin_memset(event, 0, sizeof(*event));
event->pid = (__u32)pid_tgid;
event->tgid = pid_tgid >> 32;
event->requested = count;
event->result = ret;
event->latency_ns = latency;
inode = BPF_CORE_READ(file, f_inode);
if (inode) {
device = BPF_CORE_READ(inode, i_sb, s_dev);
event->device_major = device >> KERNEL_MINOR_BITS;
event->device_minor = device & KERNEL_MINOR_MASK;
event->inode = BPF_CORE_READ(inode, i_ino);
event->mode = BPF_CORE_READ(inode, i_mode);
}
bpf_get_current_comm(event->comm, sizeof(event->comm));
bpf_ringbuf_submit(event, 0);
return 0;
}
```
对于慢调用,递增 `slow` 计数器并尝试在 ring buffer 中预留空间。如果预留失败(缓冲区满),递增 `dropped` 让用户知道有事件丢失。成功后填入各字段并提交。
对于慢调用,递增 `slow` 计数器并尝试在 ring buffer 中预留空间。如果预留失败(缓冲区满),递增 `dropped` 让用户知道有事件丢失。成功后先清零事件,复制调用字段,再从 `file->f_inode` 读取对象身份并提交。
注意 `count`(请求的字节数)可以直接访问,不需要在进入时存储。这就是 fsession 的优势:函数参数在返回阶段仍然可用。
注意 `count` 和 `file` 都可以直接访问,不需要在进入时存储。这就是 fsession 的优势:函数参数在返回阶段仍然可用。工具故意不解析路径,因为路径可能改名或有多个别名。对于普通文件,可先用设备号确定挂载点,再按 inode 搜索,例如 `find /mount -xdev -inum INODE -print`。
### 用户空间加载器
@@ -268,16 +385,18 @@ static int handle_event(void *context, void *data, size_t size)
{
const struct latency_event *event = data;
printf("EVENT comm=%-16s tgid=%u pid=%u requested=%llu result=%lld "
"latency_us=%llu\n",
event->comm, event->tgid, event->pid, event->requested,
event->result, event->latency_ns / 1000);
printf("EVENT comm=%-16s tgid=%u pid=%u object=%u:%u:%llu type=%s "
"requested=%llu result=%lld latency_us=%llu\n",
event->comm, event->tgid, event->pid,
event->device_major, event->device_minor, event->inode,
file_type(event->mode), event->requested, event->result,
event->latency_ns / 1000);
events_printed++;
return 0;
}
```
每个事件打印进程名、ID、请求字节数、返回值和延迟(微秒)。
每个事件打印进程名、ID、VFS 身份和类型、请求字节数、返回值和延迟(微秒)。
**干净关闭序列**:
```c
@@ -332,14 +451,12 @@ Usage: ./fsession_latency [--threshold-us USEC] [--duration SEC] [--pid TGID] [-
### 输出示例
以下输出在运行内核 `7.0.0-rc2+` 的 x86_64 环境中采集。集成测试执行 64 次普通文件读取加一次延迟 50 ms 的管道读取。管道读超过 10 ms 阈值后出现在事件流中:
例如,一个 Python 服务等待 FIFO,而写入端在 50 ms 后响应时,会产生下面的输出:
```console
Tracing vfs_read for 1 seconds; threshold=10000 us; pid=selected
EVENT comm=python3 tgid=1257 pid=1257 requested=1 result=1 latency_us=50160
EVENT comm=python3 tgid=1245 pid=1245 object=0:16:784 type=fifo requested=1 result=1 latency_us=50246
SUMMARY calls=66 slow=1 errors=0 dropped=0 events=1
TEST-SUMMARY miss_calls=0 high_threshold_calls=66 high_threshold_slow=0 threshold_calls=66 threshold_slow=1 events=1 dropped=0
PASS: PID filtering, threshold miss, and slow-read reporting behaved as expected
```
`SUMMARY` 行显示:
@@ -349,18 +466,6 @@ PASS: PID filtering, threshold miss, and slow-read reporting behaved as expected
- 0 个事件被丢弃
- 1 个事件被打印
### 运行测试
```bash
sudo make test
```
集成测试验证:
- **PID 过滤**:针对不存在的 TGID 产生零调用
- **阈值过滤**:很高的阈值(10 秒)使快速读取产生零慢事件
- **事件上报**:故意延迟的管道读取(50ms)在 10ms 阈值下触发事件
- **计数不变式**:阈值为 0 时,`slow == events + dropped`(每个达到阈值的调用要么被投递要么被计入丢弃)
## 环境要求
| 要求 | 详情 |
@@ -372,7 +477,7 @@ sudo make test
| 架构 | 已在 x86_64 上测试 |
| 权限 | root |
运行时行为在 x86_64 内核 `7.0.0-rc2+` 上测试。上游合并提交为 `f17b474e36647c23801ef8fdaf2255ab66dd2973`。
上游合并提交为 `f17b474e36647c23801ef8fdaf2255ab66dd2973`。
## 扩展方向
@@ -395,6 +500,8 @@ sudo make test
3. **无状态泄漏**:内核管理 cookie 生命周期,不需要清理,不可能泄漏
4. **返回时可访问参数**:函数参数在返回阶段仍然可用,无需显式存储
在这个 `vfs_read` 示例中,最后一点还让返回阶段可以报告 VFS 对象的设备号、inode 和类型。应把结果理解为通用 VFS 调用延迟,并用对象身份继续定位,而不是假设每个慢事件都是磁盘问题。
模式很简单:检查 `bpf_session_is_return()`,用 `bpf_session_cookie()` 管理调用级状态,直接访问参数和返回值。这个模式适用于任何需要关联函数进入和退出的场景。
> 如果你想深入了解 eBPF,请查看我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或访问我们的网站 <https://eunomia.dev/tutorials/>。