跳到主要内容
磁盘占用居然没有 /proc 文件可读:statvfs 踩坑笔记

磁盘占用居然没有 /proc 文件可读:statvfs 踩坑笔记

abanana
abanana

· 阅读约 5 分钟

今天继续照着 Maneshwar 写 ratatop 的第三天记录往下复现,主题是给监控加上磁盘占用、吞吐和 swap。本来以为磁盘占用是最没悬念的一块——不就是一个除法吗——结果光“用哪个字段当分子分母”这一件事就绕了两次,顺手记一下。

先说最反直觉的一个发现:Linux 没有把文件系统的满度暴露成 /proc 或 /sys 下面的一个文件。CPU 有 /proc/stat,内存有 /proc/meminfo,磁盘 I/O 有 /sys/block/<device>/stat,唯独“这个盘还剩多少空间”没有对应的文件可读,只能走 statvfs 这个系统调用。我一开始也不信,翻了一圈确实没有。原理是这样:statvfs 拿的是一个路径,返回的是挂载点所在文件系统的统计,它天生是“按文件系统抽象”的接口,而 /proc、/sys 那套是“按内核对象暴露”的思路,两边对不上。

麻烦在 Rust 这边。标准库没有 statvfs 的封装,要用就得通过 libc crate 直接调系统调用,于是我在这个项目里第一次写了 unsafe 块。第一坑是字符串:C 要求 NUL 结尾的字符串,Rust 的字符串是带长度前缀的,直接把 &str 传过去是不行的,得先转成 CString:

let c_path = CString::new(mount_path.as_str())?;
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };

第二坑是那个 mem::zeroed。一开始没搞懂为什么要先把结构体整个置零,后来才想明白——statvfs 只负责往这个缓冲里写,成功时内核会把字段填满,但万一调用失败,缓冲里留着的就是未初始化的垃圾值,后面读到的数字就不知道是从哪来的。先置零,至少失败路径上不会读到随机内存。

unsafe 块还有一条硬规矩:每个 unsafe 块上面必须写 SAFETY 注释,写清楚“为什么这段是安全的”。我以前觉得这是仪式感,这次自己写了一遍才体会到它的用处——写注释的过程就是逼自己把不变量说出口:这里指针为什么有效、缓冲为什么够大、失败时会发生什么。说不出来,就说明你其实没搞懂这段代码,那这段代码就不该合并。这条记下来了,下次应该用得上。

真正让我停下来重跑了一遍的,是 f_bfree 和 f_bavail 的区别。statvfs 返回三个块计数:f_blocks 是总块数,f_bfree 是真正没用的块,f_bavail 是非特权进程能用的块。听着差不多?在我自己的根分区上跑出来的实际数字是:总共 91.1 GiB,f_bfree 算出来 23.7 GiB 空闲,f_bavail 只有 19.0 GiB,差了 4.67 GiB,正好是整盘的 5.1%——因为 ext4 默认给 root 保留了 5% 的空间。

于是同一个盘,用 f_bfree 算使用率是 74.0%,用 f_bavail 算是 79.1%,差了整整五个百分点。就因为选了不同的字段。我验证了一下,btop 和 df 报的都是 f_bavail 口径的数,ratatop 也选了这个——理由是它反映的是普通进程实际能用的空间,这才是你在终端里关心的事。你可以自己拿 df -h 和自己的 statvfs 结果对一遍,两个口径的差值除以总量,应该正好是你文件系统的保留比例。

下面是识别“哪些挂载是真磁盘”的部分,两个过滤条件叠着用。第一个,解析 /proc/filesystems,把所有标了 nodev 的类型排除掉——nodev 表示这个文件系统背后没有块设备,proc、sysfs 这类伪文件系统都在这一档。第二个,挂载记录里的设备字段必须以 / 开头,真分区是 /dev/nvme0n1p2 这种路径,伪文件系统就是一个裸名字 proc。只靠其中一个都不够,两个一起才能把列表收干净。

这里还有个我完全没想到的细节:内核在挂载路径里遇到空格、tab、换行、反斜杠时,会用八进制转义,比如空格写成 \040。所以从 /proc 拿到的路径不能直接用,得先把这些转义解码回去,再转 CString 交给 statvfs。我的挂载点里没有空格,这一步在我机器上验证不了效果,只能照着实现并写上注释——这里我也没法给出实测输出,老实说清楚。

I/O 吞吐走 /sys/block/<device>/stat,两个点值得划出来。一是扇区计数永远按 512 字节算,跟设备物理块大小无关,别拿 4K 去乘。二是这个文件在整盘一层有,往下一层的分区目录里也有,所以代码先看顶层、再往下扫一层。另外 io_ticks 这个字段是“忙碌毫秒数”,用它的差值除以流逝的墙钟毫秒,就是设备利用率百分比。

还有一个我打算直接抄走的细节:算差值时用 saturating_sub 而不是普通减法。设备拔掉再插回来,计数器会归零,普通减法直接下溢,saturating_sub 会得到 0,曲线断一下而不是爆炸。这个坑我也是绕了两次才躲开——第一次看到吞吐突然变成天文数字,还以为是采样逻辑写错了。

评论区还有一条提醒我觉得比正文还值钱:statvfs 在 NFS 或 FUSE 挂载上可能无限期阻塞,一个卡住的调用会把整个 TUI 冻住。应对要么把这类挂载过滤掉,要么把调用丢到单独线程加超时。ratatop 目前没处理这个,我也没处理——留一个坑,等哪天真在挂了 NFS 的机器上跑崩了再回来补。

划重点:第一,磁盘使用率不是客观事实,f_bfree 和 f_bavail 能差出五个百分点,选字段之前先想清楚你要回答哪个问题;第二,unsafe 块的 SAFETY 注释不是形式主义,写不出来就是没读懂;第三,单调计数器求差值一律 saturating_sub,代价为零,收益是拔设备时不下溢。这篇就记到这里,你可以拿 df -h 对一下自己根分区的保留比例,如果数字对不上,欢迎回来留言,我一起补一条笔记。

abanana
abanana

把自己踩过的坑整理成一篇能复现的笔记,写给三个月前的自己看。

查看主页 →