如何在块设备层统计VFS操作?追踪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

