Как получить заполненность диска в Rust без df. Разбор statvfs, unsafe и /proc. Практические примеры для системного монитора.
Если вы пишете системный монитор или любой инструмент, которому нужно знать, сколько места осталось на диске, вы столкнётесь с проблемой: в /proc нет файла с этой информацией. В этой статье я покажу, как правильно получить заполненность через системный вызов statvfs, избегая типичных ошибок. Вы узнаете, почему df и btop показывают разные цифры, и как читать статистику I/O из /sys.
В Linux почти вся информация о системе доступна через виртуальные файлы в /proc и /sys. Но заполненность файловой системы — исключение. Она вычисляется ядром на лету из метаданных конкретной файловой системы. Единственный способ получить это число — системный вызов statvfs(2).
Популярная утилита df — это просто обёртка над этим вызовом. Запускать внешний процесс, чтобы получить число, которое ядро может отдать напрямую, — нерационально. Поэтому в моём проекте ratatop я решил сделать вызов самостоятельно.
Стандартная библиотека Rust не содержит обёртки над statvfs, поэтому придётся использовать FFI через крейт libc. Это означает работу с unsafe. Вот как выглядит функция получения общего и свободного места:
use std::ffi::CString;
fn usage(mount: &str) -> Option<(u64, u64)> {
let path = CString::new(mount).ok()?;
// SAFETY: path — валидная NUL-терминированная строка, живёт дольше вызова.
// vfs полностью инициализируется statvfs перед чтением.
// Ошибка возвращается через код возврата, который мы проверяем.
let vfs = unsafe {
let mut vfs: libc::statvfs = std::mem::zeroed();
if libc::statvfs(path.as_ptr(), &mut vfs) != 0 {
return None;
}
vfs
};
let frsize = vfs.f_frsize as u64;
let total = (vfs.f_blocks as u64).saturating_mul(frsize);
let free = (vfs.f_bavail as u64).saturating_mul(frsize);
Some((total, free.min(total)))
}Здесь три важных момента:
statvfs возвращает три счётчика блоков:
Разница между f_bfree и f_bavail может быть значительной. На моей системе с ext4:
total 91.1 GiB
bfree 23.7 GiB
bavail 19.0 GiBЭто 4.67 GiB, или 5.1% диска, которые свободны, но недоступны обычному пользователю. ext4 резервирует 5% для root, чтобы система могла работать даже при заполненном диске.
Какой счётчик использовать? btop и df используют f_bavail — это честный выбор для монитора: показывать реально доступное место. Иначе пользователь увидит 74%, а df — 79%, и решит, что инструмент сломан.
/proc/self/mounts может содержать десятки записей, но лишь немногие — реальные диски. Вместо ручного блэклиста типов ФС, читайте /proc/filesystems:
$ cat /proc/filesystems
nodev sysfs
nodev tmpfs
nodev proc
nodev cgroup2
ext4
vfatВсё, что помечено nodev, не связано с блочным устройством. Используйте это для фильтрации:
let mut types: Vec<String> = raw.lines()
.filter_map(|line| line.strip_prefix("nodev"))
.map(|name| name.trim().to_string())
.collect();Дополнительно проверяйте, что устройство монтирования начинается с '/': реальные разделы — это /dev/sda1, а псевдо-ФС имеют голые имена вроде proc.
Если путь содержит пробел, /proc/self/mounts показывает его как \040 (восьмеричный код пробела). Если передать такую строку в statvfs, вызов завершится ошибкой. Поэтому перед использованием нужно декодировать escape-последовательности:
fn unescape_mount_path(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
let mut octal = String::new();
for _ in 0..3 {
if let Some(d) = chars.next() { octal.push(d); }
}
if let Ok(code) = u32::from_str_radix(&octal, 8) {
out.push(char::from_u32(code).unwrap_or('?'));
}
} else {
out.push(c);
}
}
out
}Пропускная способность читается из /sys/block/<device>/stat. Это счётчики накопленным итогом:
234345 84965 16346864 796898 486240 189824 21513416 23255863 0 2406558 24197776Полезные поля: индекс 2 (секторы прочитано), 6 (секторы записано), 9 (io_ticks — миллисекунды, когда устройство было занято).
Две ловушки:
io_ticks позволяет вычислить утилизацию:
let busy_ms = counters.io_ticks.saturating_sub(previous.io_ticks) as f64;
disk.io_util = (busy_ms / (seconds * 1000.0) * 100.0).clamp(0.0, 100.0);saturating_sub обязателен: счётчики могут сбрасываться при переподключении устройства.
В btop swap отображается в блоке дисков, и это логично: вас интересует его заполненность. Но у swap нет файловой системы, поэтому statvfs не работает. Данные берутся из /proc/meminfo:
SwapTotal: 21092348 kB
SwapFree: 19857688 kBВ коде это отдельная структура с флагом is_swap, и для неё не рисуется график I/O, потому что этих данных просто нет. Важно отличать «ноль» от «неизвестно».
Попробуйте реализовать функцию usage() в своём проекте. Убедитесь, что вы используете f_bavail, а не f_bfree, иначе ваши цифры разойдутся с df. Добавьте фильтрацию nodev и декодирование путей. И не забывайте про saturating_sub при работе со счётчиками.
Мой код доступен на GitHub: ratatop. Загляните, если хотите увидеть полную реализацию.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →