0%

PIPA-rs 开发手记 (二):从 /proc 文件到 TUI,一次关于“可信度”的修行

在上一篇手记中,我们为 PIPA-rs 搭建了一副坚固的“骨架”——一个自律的、自动化的工程框架。现在,是时候为这副骨架注入第一股“生命力”了。我们的目标是:深入 pipa_collector,从零开始实现对系统核心指标的采集,并构建一个基础的 TUI 监控工具,作为 sartop 的一个微型替代品。

有人可能会问,Linux 上有那么多现成的工具和 crate,为什么非要选择一条最“难”的路——直接去解析 /proc 文件系统?

答案很简单,它源于 PIPA-rs 的核心理念:“零外部二进制依赖”和“超可靠”。我们不希望 PIPA-rs 的可靠性建立在对 sar 命令输出格式的脆弱假设上。我们希望直接与内核提供的数据源对话,并用自己的代码来保证每一次解析的健壮性。这不仅仅是重新造轮子,更是一次关于构建“可信度”的修行。

解析 /proc:为应对真实世界的混乱而设计

我的第一个目标是解析 /proc/stat/proc/meminfo。这听起来很简单,不就是读取文件、按空格分割字符串吗?但很快我就意识到,一个“玩具”解析器和一个“工业级”解析器的区别,就在于你如何处理真实世界中可能发生的各种混乱。

为了确保 pipa_collector 的健壮性,我从一开始就定下了一个目标:这个模块的测试覆盖率必须达到 100%。

要实现这个目标,关键在于一个经典的设计模式:将 I/O 与逻辑解耦

我将代码分成了两层:

  1. 纯粹的解析函数: 比如 parse_cpu_stats_from_line(line: &str),它接收一个字符串切片,返回一个 Result。这个函数不执行任何 I/O,它是确定的、可预测的,因此可以被轻松地 100% 测试。
  2. 负责 I/O 的函数: 比如 read_cpu_stats_from_path<P: AsRef<Path>>(path: P),它负责读取文件,然后调用上面的纯函数。

这种分离让测试变得非常简单。对于解析逻辑,我可以直接喂给它各种正常和异常的字符串;对于 I/O 函数,我可以使用 tempfile crate 在测试时创建一个临时的假文件。

而真正体现“为混乱而设计”思想的,是我为 pipa_collector 精心设计的自定义错误类型 PipaCollectorError

1
2
3
4
5
6
7
8
// crates/pipa_collector/src/system_stats.rs
#[derive(Debug)]
pub enum PipaCollectorError {
Io(io::Error),
Parse(ParseIntError),
InvalidFormat(String),
MissingData(String),
}

我没有简单地 unwrap() 或者抛出一个泛泛的 io::Error。我预想并用单元测试覆盖了各种 /proc 文件可能出现的“不完美”状态:

  • 格式错误 (InvalidFormat): 比如 cpu 字段被意外篡改 (test_parse_cpu_stats_invalid_prefix)。
  • 数据缺失 (MissingData): 比如内核输出被截断,字段不够 (test_parse_cpu_stats_not_enough_values)。
  • 数据损坏 (Parse): 比如 meminfo 中某一行的数据不是数字,但解析器不能因此崩溃,而是要能容错并继续 (test_parse_memory_stats_malformed_value)。

正是这种对细节和边缘案例的执念,才让“100% 覆盖率”这个数字变得有意义。它代表着 pipa_collector 从诞生之初,就准备好了去面对一个不那么理想的真实世界。

TUI 的进化:从“打印”思维到“绘制”思维

有了可靠的数据源,下一步就是把它呈现出来。我开始着手开发 pipa-rs monitor 命令,一个实时的 TUI 监视器。

我的 TUI 开发经历了一次典型的、从混乱到精确的进化:

  1. V1 - “滚动日志”阶段: 最初我只是简单地用 println! 把数据打印出来。结果可想而知,终端被不断刷屏,污染了命令历史。
  2. V2 - “伪 TUI” 阶段: 我学聪明了一点,在每次 println! 之前,先打印一个 ANSI 清屏码 \x1B[2J。这创造了一种“原地刷新”的假象,但很快我就发现,即使我用格式化字符串费力地对齐,布局在终端缩放时依然是一片混乱。

也正是这个无法解决的混乱,让我彻底停下来思考:问题的根源,或许根本不在于“怎么对齐”,而在于我从一开始就用错了方法。

我一直在用“打印”的思维来解决一个“绘制”的问题。只要我还在向终端“流式地”发送字符,并期望它能正确处理换行和对齐,我就永远无法获得精确的控制。我需要的,是一个真正的“画布”和一支“画笔”。

crossterm 库就是我找到的答案。它提供了两个核心工具,彻底改变了我的 TUI 范式:

  • 备用屏幕 (EnterAlternateScreen): 它为我的应用提供了一个独立的、干净的“画布”,退出时会自动恢复到之前的终端状态,解决了“污染历史”的问题。
  • 绝对光标定位 (cursor::MoveTo(x, y)): 这才是解决布局问题的“银弹”。我不再关心上一个字符打印在哪,而是每次都明确地告诉终端:“把光标移动到 (x, y),然后在这里画出你的内容。”

这次从“流式输出”到“绝对定位”的思维跃迁,最终让我构建出了那个稳定、专业、无闪烁的 TUI 界面。

被 TUI 掩盖的灵魂:计算逻辑的“可信度”

一个分析工具,其界面的华丽是“面子”,而其内在逻辑的准确性,才是赢得用户信任的“里子”。

pipa-rs monitor 的 TUI 之下,隐藏着一个关键函数 calculate_cpu_usage。其中两个看似微不足道的细节,正是 PIPA-rs 可信度的基石。

  1. total_delta == 0.0 的处理: 在一个极度空闲的系统上,或在一个极短的采样间隔内,/proc/stat 的累计值可能没有任何变化。如果没有 if total_delta == 0.0 这个检查,程序就会因为“除以零”而 panic。一个专业的性能工具,绝不能因为系统“太闲”而崩溃。这个检查,是健 robustness的体现。
  2. iowait 的归属判断: 我将 iowait 正确地归类为了“空闲时间”的一部分,因为它代表 CPU 没有在执行任务,而是在等待 I/O。如果错误地将其归为“繁忙时间”,就会在 I/O 密集型场景下严重高估 CPU 使用率,误导用户去排查一个根本不存在的 CPU 瓶颈。这个判断,是accuracy的体现。

尾声:第一个可用的“小工具”

经过一番折腾,我终于可以运行 pipa-rs monitor,看到从我自己编写的解析器中流出的数据,最终在我自己的 TUI 上实时刷新。

那一刻的满足感是巨大的。这个从输入到输出的完整闭环,标志着 PIPA-rs 不再只是一堆库代码,它已经成为了一个有用的、看得见摸得着的“小工具”。

虽然它还很简陋,但它的内核是可靠的,它的界面是专业的,它的每一次输出都值得信赖。

当然,monitor 只是一个“开胃菜”。真正的硬仗还在后面。下一篇手记,我们将挑战 PIPA-rs 的核心——与 perf_event_open 系统调用正面交锋,去实现 perf stat 的核心功能。

欢迎关注我的其它发布渠道