ГлавнаяБлогМониторинг дисков в Rust: вызов statvfs без unsafe
Алгоритмы

Мониторинг дисков в Rust: вызов statvfs без unsafe

Как получить заполненность диска в Rust без df. Разбор statvfs, unsafe и /proc. Практические примеры для системного монитора.

Al
Редакция Algolitalgolit.ru
10 мин чтения31 июля 2026 г.

Зачем читать эту статью

Если вы пишете системный монитор или любой инструмент, которому нужно знать, сколько места осталось на диске, вы столкнётесь с проблемой: в /proc нет файла с этой информацией. В этой статье я покажу, как правильно получить заполненность через системный вызов statvfs, избегая типичных ошибок. Вы узнаете, почему df и btop показывают разные цифры, и как читать статистику I/O из /sys.

Почему /proc не содержит данных о заполненности

В Linux почти вся информация о системе доступна через виртуальные файлы в /proc и /sys. Но заполненность файловой системы — исключение. Она вычисляется ядром на лету из метаданных конкретной файловой системы. Единственный способ получить это число — системный вызов statvfs(2).

Популярная утилита df — это просто обёртка над этим вызовом. Запускать внешний процесс, чтобы получить число, которое ядро может отдать напрямую, — нерационально. Поэтому в моём проекте ratatop я решил сделать вызов самостоятельно.

Первый unsafe-блок: вызов statvfs

Стандартная библиотека 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)))
}

Здесь три важных момента:

  • CString::new обязателен: C ожидает NUL-терминированную строку, а Rust-строка таковой не является. Без этого вы рискуете прочитать память за пределами строки.
  • std::mem::zeroed() безопасен, потому что statvfs инициализирует все поля перед возвратом успеха. Но даже при частичной записи мы получим нули, а не мусор со стека.
  • Комментарий SAFETY — это не украшение. В Rust каждая unsafe-операция должна сопровождаться объяснением, почему она безопасна. Это заставляет перечислить все допущения и проверить их.

Ваш диск врёт вам на 5%

statvfs возвращает три счётчика блоков:

  • f_blocks — всего блоков;
  • f_bfree — свободно блоков (включая резерв для root);
  • f_bavail — доступно непривилегированному процессу.

Разница между 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
}

I/O статистика из /sys

Пропускная способность читается из /sys/block/<device>/stat. Это счётчики накопленным итогом:

234345 84965 16346864 796898 486240 189824 21513416 23255863 0 2406558 24197776

Полезные поля: индекс 2 (секторы прочитано), 6 (секторы записано), 9 (io_ticks — миллисекунды, когда устройство было занято).

Две ловушки:

  • Сектор всегда 512 байт, независимо от реального размера блока устройства. Умножать на 4096 — ошибка.
  • Разделы вложены: для nvme0n1p2 путь будет /sys/block/nvme0n1/nvme0n1p2/stat.

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 обязателен: счётчики могут сбрасываться при переподключении устройства.

Swap — диск, который не диск

В 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. Загляните, если хотите увидеть полную реализацию.

#Rust#системное программирование#statvfs#мониторинг#Linux
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →