You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在块设备层统计VFS操作?追踪VFS到块IO的技术问询

如何用bpftrace跟踪VFS调用到块设备IO的关联链路?

我想要测试把部分文件系统数据放在快速存储设备(比如SSD)和普通机械硬盘上的性能差异,重点对比扩展属性(xattr)操作和常规读写操作的区别。

我尝试用bpftrace来跟踪单个VFS调用,但遇到了一个问题:当我用线程本地变量关联VFS调用和块设备请求时,发现捕获不到对应的块IO请求——推测实际的IO操作是由kworker进程执行的,导致tid无法关联上。

我的初始bpftrace代码如下:

kprobe:vfs_getxattr, kprobe:vfs_setxattr, kprobe:vfs_read*, kprobe:vfs_write* /comm == "some-process"/ { @start[tid] = nsecs; @name[tid] = func; }
tracepoint:block:block_rq_issue /@start[tid]/ { @bio[@name[tid]] = count(); }
kretprobe:vfs_getxattr, kretprobe:vfs_setxattr, kretprobe:vfs_read*, kretprobe:vfs_write* /@start[tid]/ { @count[@name[tid]] = count(); delete(@start[tid]); delete(@name[tid]); }

请问有没有办法能正确跟踪VFS调用到块设备IO的完整链路?


这个问题确实很常见——因为内核中很多IO操作会被异步调度给kworker线程执行,导致你用tid直接关联VFS调用和块IO请求的方法失效。这里有几个可行的解决方案,帮你建立VFS调用到块IO的关联链路:

1. 通过文件inode关联VFS调用和块IO

每个文件操作最终都会对应到具体的inode,你可以在VFS探针中记录inode,然后在块IO的tracepoint中提取bio对应的inode来关联:

// 在VFS调用时记录inode和操作类型
kprobe:vfs_getxattr, kprobe:vfs_setxattr, kprobe:vfs_read*, kprobe:vfs_write* /comm == "some-process"/ {
    // 从VFS函数参数中提取inode(不同函数的参数位置可能需要调整)
    $inode = ((struct file *)arg0)->f_inode;
    @vfs_ops[$inode] = func;
    @vfs_tid[$inode] = tid;
}

// 在块IO发出时,提取bio对应的inode并关联
tracepoint:block:block_rq_issue {
    $bio = args->bio;
    // 从bio中获取inode(需要内核版本支持,不同版本结构可能有差异)
    $inode = $bio->bi_inode;
    if (@vfs_ops[$inode]) {
        @io_count[@vfs_ops[$inode]] = count();
        // 可选:记录发起VFS调用的原始tid
        @io_origin_tid[@vfs_ops[$inode]] = @vfs_tid[$inode];
    }
}

// VFS调用结束后清理记录
kretprobe:vfs_getxattr, kretprobe:vfs_setxattr, kretprobe:vfs_read*, kretprobe:vfs_write* {
    $inode = ((struct file *)arg0)->f_inode;
    delete(@vfs_ops[$inode]);
    delete(@vfs_tid[$inode]);
}

注意:不同内核版本中struct bio的字段可能有变化,比如有些版本可能需要用$bio->bi_io_vec->bv_inode或者通过bdev反向查找,你可以用bpftrace -v查看内核结构定义。

2. 跟踪IO请求的发起进程链

另一种方法是利用内核的task_struct中的group_leader,找到发起IO的原始进程:

kprobe:vfs_getxattr, kprobe:vfs_setxattr, kprobe:vfs_read*, kprobe:vfs_write* /comm == "some-process"/ {
    @start[tid] = nsecs;
    @name[tid] = func;
    // 记录进程的tgid(线程组ID,对应用户态的PID)
    @tgid[tid] = tgid;
}

tracepoint:block:block_rq_issue {
    // 获取当前kworker所属的线程组ID
    $task = (struct task_struct *)bpf_get_current_task();
    $orig_tgid = $task->group_leader->tgid;
    // 遍历所有活跃的VFS调用,匹配线程组ID
    foreach ($tid in @start) {
        if (@tgid[$tid] == $orig_tgid) {
            @bio[@name[$tid]] = count();
            break;
        }
    }
}

kretprobe:vfs_getxattr, kretprobe:vfs_setxattr, kretprobe:vfs_read*, kretprobe:vfs_write* /@start[tid]/ {
    @count[@name[tid]] = count();
    delete(@start[tid]);
    delete(@name[tid]);
    delete(@tgid[tid]);
}

这种方法的好处是不需要依赖inode,但遍历哈希表可能会有一定的性能开销,适合低负载场景下的调试。

3. 利用文件系统专属tracepoint简化跟踪

你还可以结合tracepoint:fs:xattr_get、tracepoint:fs:xattr_set这类专门针对xattr操作的tracepoint,它们比kprobe更稳定,且能直接获取到操作的上下文信息,再搭配块IO的tracepoint,能更精准地关联操作和IO。


内容的提问来源于stack exchange,提问作者Alexandre Lécuyer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:44:51