VFS — единый интерфейс над ext4, xfs, btrfs, tmpfs
Когда ваша программа делает open("/var/log/app.log") — ей всё равно, на каком физическом диске этот файл, в какой файловой системе он лежит, экспортирован ли он через NFS, или вообще это псевдо-файл из /proc. Один и тот же open() syscall работает везде. Это Virtual File System (VFS) — абстракция в ядре Linux, благодаря которой существуют десятки разных файловых систем под одним API.
VFS — одна из самых элегантных абстракций в Linux. Она появилась как ответ на простую проблему: в 80-90-е годы появлялось много новых файловых систем (ext2, NFS, ISO 9660 для CD, FAT для USB), и каждое приложение могло захотеть с ними работать. Без VFS пришлось бы каждой программе знать про все ФС. С VFS приложение видит один API, а ядро транслирует вызовы в конкретный driver. Это и есть «дешёвая абстракция»: API простой, реализаций много.
В этом уроке: что такое VFS на уровне kernel, как работают inode/dentry/file structures, что физически делает mount, и почему /proc — это тоже файловая система (но без диска).
Архитектура: один API, много реализаций
Когда open("/foo/bar") приходит в kernel, VFS делает path lookup: проходит по компонентам пути, находя соответствующие inode (которые могут принадлежать разным ФС из-за mount-точек). В итоге получает inode конкретной ФС и вызывает её специфический open метод. Дальше read/write через указатели функций.
Три ключевые структуры: inode, dentry, file
VFS оперирует тремя главными структурами:
inode (index node) — метаданные о файле: размер, владелец, права, время модификации, указатели на блоки данных. Один файл = один inode, независимо от имени. Имя файла — это отдельная сущность.
dentry (directory entry) — связь имени и inode. Один dentry на каждое имя файла. Когда вы делаете ls /foo, kernel ищет dentry «foo» в директории «/», находит её, идёт по ссылке на inode.
file — открытый дескриптор. Когда вы делаете open(), kernel создаёт struct file — это экземпляр «открытого файла». Содержит позицию чтения, флаги, ссылку на inode. Несколько file могут указывать на один inode (если файл открыт несколько раз).
# inode конкретного файла:
stat /etc/hostname
# File: /etc/hostname
# Size: 12 Blocks: 8 IO Block: 4096 regular file
# Device: 802h/2050d Inode: 786433 Links: 1
# Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
# Access: 2026-05-18 09:30:15.000000000 +0000
# Modify: 2024-02-15 12:00:00.000000000 +0000
# Change: 2024-02-15 12:00:00.000000000 +0000
# Видите 'Inode: 786433' -- это номер inode в этой ФС
# Размер 12 байт, права 644, два timestamp-а
# Открытые file descriptors процесса:
ls -la /proc/self/fd/
# 0 -> /dev/pts/0 (stdin)
# 1 -> /dev/pts/0 (stdout)
# 2 -> /dev/pts/0 (stderr)
# 3 -> /etc/hostname
# Каждый fd -- это struct file в ядре, с ссылкой на dentry/inode
dentry cache в ядре — одна из главных оптимизаций. Каждый name lookup без dcache был бы дорогим (поход на диск). С dcache часто-используемые пути резолвятся в наносекунды.
mount — как ФС встают в дерево
Linux имеет одно глобальное дерево файлов с корнем /. Разные физические устройства и разные ФС присоединяются в это дерево через mount. После mount-а соответствующего drive, файлы под mount-point становятся видны через единое дерево.
# Все mount-точки:
mount
# /dev/nvme0n1p2 on / type ext4 (rw,relatime)
# proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
# sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
# tmpfs on /run type tmpfs (rw,nosuid,nodev,size=783584k,mode=755)
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,size=7906112k)
# tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,inode64)
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# Современный читаемый формат:
findmnt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/nvme0n1p2 ext4 rw,relatime
# |-/proc proc proc rw,nosuid,nodev
# |-/sys sysfs sysfs rw,nosuid,nodev
# |-/tmp tmpfs tmpfs rw,nosuid,nodev,size=7G
# ...
findmnt — более удобный, чем mount без аргументов. Показывает иерархию.
Виртуальные ФС: /proc и /sys
/proc и /sys — это файловые системы, но без диска. Их файлы генерируются ядром по требованию.
# /proc/cpuinfo не существует на диске -- генерируется на лету:
cat /proc/cpuinfo | head
# processor : 0
# vendor_id : GenuineIntel
# cpu family : 6
# model : 158
# model name : Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz
# ...
# Это не файл -- это API к kernel state.
# При каждом cat ядро формирует текст заново.
ls -la /proc/cpuinfo
# -r--r--r-- 1 root root 0 May 18 10:00 /proc/cpuinfo
# ^
# размер 0 -- ядро не знает заранее, сколько будет
stat /proc/cpuinfo
# Size: 0 Blocks: 0 -- никаких блоков на диске
/proc/[pid]/ — информация о конкретном процессе:
# Полный набор данных о процессе:
ls /proc/self/
# attr/ cmdline comm cwd environ exe fd/ fdinfo/ limits maps
# mem mounts net/ ns/ oom_score smaps stack stat status ...
cat /proc/self/status | head
# Name: bash
# State: R (running)
# Tgid: 12345
# Pid: 12345
# PPid: 12340
# Uid: 1000 1000 1000 1000
# Gid: 1000 1000 1000 1000
# VmSize: 17452 kB
# VmRSS: 4824 kB
/sys — интерфейс к kernel objects (devices, modules, drivers):
# Информация о CPU:
ls /sys/devices/system/cpu/cpu0/
# cache/ cpufreq/ topology/ online ...
cat /sys/class/net/eth0/operstate
# up -- состояние сетевого интерфейса
# Что-то можно даже записать (управление kernel-ом):
echo 1 > /sys/class/leds/something/brightness
echo 7000 > /proc/sys/vm/swappiness
Это и есть мощь VFS: ядро экспортирует state как файлы, тулзы (cat, grep, awk) сразу работают с этим без специальных API. Принцип «everything is a file».
sysctl — более красивая обёртка над /proc/sys. `sysctl net.ipv4.ip_forward` это то же, что `cat /proc/sys/net/ipv4/ip_forward`. Но cat работает в любом контейнере, sysctl может быть не установлен.
Mount options и mount namespaces
mount — мощная команда с десятками опций:
# Стандартный mount с опциями:
mount -o ro,noexec,nosuid,nodev /dev/sdb1 /mnt/data
# ^^ ^^^^^^ ^^^^^^ ^^^^^
# read-only, нельзя exec, нельзя setuid, без device-nodes
# Remount без отмонтирования (изменить опции):
mount -o remount,rw /mnt/data
# Bind mount -- сделать каталог видимым в другом месте:
mount --bind /home/user/work /mnt/work
# Теперь /mnt/work показывает то же, что /home/user/work
# Mount image-файла как ФС (через loop device):
mount -o loop /tmp/disk.img /mnt/image
Опции типа noexec, nosuid — security-классика. noexec запрещает запускать программы из этой ФС (защита от executable malware в /tmp). nosuid отключает setuid-биты.
Mount namespaces — ключевая Linux фишка для контейнеров. Каждый namespace имеет своё дерево mount-ов. Это позволяет:
- Docker контейнеру видеть свой root (как chroot, но мощнее).
- Запустить процесс в изоляции, где /tmp — свой, не общий с host.
- Создать read-only mount для чувствительных директорий в одном namespace.
# Создать новый mount namespace:
unshare --mount --propagation private -- bash
# Внутри новой shell:
mount -t tmpfs none /mnt # tmpfs тут не виден из host
ls /mnt # пусто
# В другом терминале (host): ls /mnt -- не видно tmpfs
exit
systemd с PrivateTmp=yes использует mount namespaces, чтобы каждый сервис имел свой /tmp.
Namespaces в основе контейнеров: mount, PID, netfstab — автоматический mount при загрузке
/etc/fstab — описание, какие ФС должны быть смонтированы при загрузке:
cat /etc/fstab
# <device> <mount point> <fstype> <options> <dump> <pass>
# UUID=abc-123-def / ext4 defaults,relatime 0 1
# UUID=def-456-abc /home ext4 defaults,relatime 0 2
# UUID=... none swap sw 0 0
# tmpfs /tmp tmpfs defaults,size=4G 0 0
UUID — стабильный идентификатор раздела (blkid показывает). Лучше device-paths типа /dev/sda1, потому что устройства могут перенумероваться (особенно USB).
mount -a — смонтировать все из fstab. Используется при загрузке через systemd-fstab-generator.
Попробуй сам
Исследуйте VFS-иерархию своей системы:
# Все mount-точки и их типы:
findmnt --types ext4,xfs,btrfs,tmpfs
# Размер каждой ФС:
df -h
# Filesystem Size Used Avail Use% Mounted on
# /dev/nvme0n1p2 500G 100G 400G 20% /
# tmpfs 7.7G 12K 7.7G 1% /tmp
# tmpfs 7.7G 120M 7.6G 2% /dev/shm
# Использование inode-ов (отдельно от disk space):
df -i
# На ext4 ФС inode-количество фиксировано при создании!
# Можно исчерпать inodes, при свободном диске:
# df -h показывает 50% used
# df -i показывает 100% used
# touch file -> No space left on device
Поиграйте с mount namespaces:
# Создать tmpfs только для текущей shell:
sudo unshare --mount bash
mount -t tmpfs -o size=100M none /tmp
df -h /tmp # 100 МБ tmpfs
# В другом терминале (host) -- df /tmp обычный
# exit вернёт shell в обычный namespace
Посмотрите на /proc и /sys:
# Размеры всегда 0 -- это не файлы, это API:
ls -la /proc/cpuinfo /proc/meminfo /proc/version
# -r--r--r-- 1 root root 0 May 18 10:00 /proc/cpuinfo
# -r--r--r-- 1 root root 0 May 18 10:00 /proc/meminfo
# -r--r--r-- 1 root root 0 May 18 10:00 /proc/version
# Но cat работает -- ядро генерирует содержимое:
cat /proc/cpuinfo | wc -l # обычно несколько десятков строк
cat /proc/meminfo | grep MemFree