Виртуальные файловые системы в Linux: Зачем они нужны и как они работают?

Виртуальные файловые системы в Linux: Зачем они нужны и как они работают?

Публикация приурочена к началу профессионального курса «Администратор Linux» от образовательного проекта OTUS.

Курс не для новичков, для поступления нужны базовые знания по сетям и установке Linux на виртуалку. Обучение длится 5 месяцев, после чего успешные выпускники курса смогут пройти собеседования у партнеров. Проверьте себя на вступительном тесте и смотрите программу детальнее: https://otus.pw/NajH/

            Виртуальные файловые системы выполняют роль некой волшебной абстракции, которая позволяет философии Linux говорить, что «всё является файлом».

            Что такое файловая система? Опираясь на слова одного из первых контрибьюторов и авторов Linux Робера Лава (https: //www.pearson.com/us/higher-education/program/Love-Linux-Kernel-Development-3rd-Edition/PGM202532.html), «Файловая система – это иерархическое хранилище данных, собранное в соответствии с определенной структурой». Как бы то ни было, это определение в равной мере хорошо подходит для VFAT (Virtual File Allocation Table), Git и Cassandra (http: //cassandra.apache.org) (база данных NoSQL ). Так что именно определяет такое понятие, как «файловая система»?

            Основы файловой системы

            Ядро Linux имеет определенные требования к сущности, которая может считаться файловой системой. Она должна реализовывать методы open(), read() и write() для постоянных объектов, которые имеют имена. С точки зрения объектно-ориентированного программирования https: //lwn.net/Articles/444910/), ядро определяет обобщенную файловую систему (generic filesystem) в качестве абстрактного интерфейса, а эти три большие функции считаются «виртуальными» и не имеют конкретного определения. Соответственно, реализация файловой системы по умолчанию называется виртуальной файловой системой (VFS).

            Если мы можем открывать, читать и записывать в сущность, то эта сущность считается файлом, как мы видим из примера в консоли сверху.

            Феномен VFS лишь подчеркивает наблюдение, характерное для Unix-подобных систем, которое гласит, что «всё является файлом». Подумайте, насколько странно, что тот маленький пример сверху с /dev/console показывает, как на самом деле работает консоль. На картинке изображена интерактивная Bash сессия. Отправка строки в консоль (virtual console device) отображает ее на виртуальном экране. VFS имеет другие, еще более странные свойства. Например, она дает возможность осуществлять поиск по ним (https: //lwn.net/Articles/22355/).

            Знакомые нам системы, такие как ext4, NFS и /proc имеют три важные функции в структуре данных С, которая называется file_operations (https: //git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fs.h). Кроме того, определенные файловые системы расширяют и переопределяют функции VFS привычным объектно-ориентированным способом. Как отмечает Роберт Лав, абстракция VFS позволяет пользователям Linux беспечно копировать файлы в или из сторонних операционных систем или абстрактных сущностей, таких как pipes, не беспокоясь об их внутреннем формате данных. Со стороны пользователя(userspace) с помощью системного вызова процесс может копировать из файла в структуры данных ядра с помощью метода read() одной файловой системы, а затем использовать метод write () другой файловой системы для вывода данных.

            Определения функций, которые принадлежат к базовым типам VFS, находятся в файлах fs/*.c (https ://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs) исходного кода ядра, в то время как подкаталоги fs/ содержат определенные файловые системы. В ядре также содержатся сущности, такие как cgroups, /dev и tmpfs, которые требуются в процессе загрузки и поэтому определяются в подкаталоге ядра init/. Заметьте, что cgroups, /dev и tmpfs не вызывают «большую тройку» функций file_operations, а напрямую читают и пишут в память.

            На приведенной ниже диаграмме показано, как userspace обращается к различным типам файловых систем, обычно монтируемых в системах Linux. Не показаны такие конструкции как pipes, dmesg и POSIX clocks, которые также реализуют структуру file_operations, доступ к которым проходит через слой VFS.

            VFS — это «слой оболочки» между системными вызовами и реализациями определенных file_operations, таких как ext4 и procfs. Функции file_operations могут взаимодействовать либо с драйверами устройств, либо с устройствами доступа к памяти. tmpfs, devtmpfs и cgroups не используют file_operations, а напрямую обращаются к памяти.

            Существование VFS обеспечивает возможность переиспользовать код, так как основные методы, связанные с файловыми системами, не должны быть повторно реализованы каждым типом файловой системы. Переиспользование кода – широкоприменяемая практика программных инженеров! Однако, если повторно используемый код содержит серьезные ошибки (https: //lwn.net/Articles/774114/), от них страдают все реализации, которые наследуют общие методы.

/tmp: Простая подсказка

Простой способ обнаружить, что VFS присутствуют в системе – это ввести mount | grep -v sd | grep -v :/, что покажет все смонтированные (mounted) файловые системы, которые не являются резидентами на диске и не NFS, что справедливо на большинстве компьютеров. Одним из перечисленных маунтов(mounts) VFS, несомненно, будет /tmp, верно?

Все знают, что хранение / tmp на физическом носителе – безумие! Источник: httpss: //tinyurl.com/ybomxyfo

Почему нежелательно хранить /tmp на физическом носителе? Потому что файлы в /tmp являются временными, а устройства хранения медленнее, чем память, где создается tmpfs. Более того, физические носители более подвержены износу при перезаписи, чем память. Наконец, файлы в /tmp могут содержать конфиденциальную информацию, поэтому их исчезновение при каждой перезагрузке является неотъемлемой функцией.

К сожалению, некоторые скрипты инсталляции Linux дистрибутивов создают /tmp на устройстве хранения по умолчанию. Не отчаивайтесь, если это произошло и с вашей системой. Выполните несколько простых инструкций с Arch Wiki (https://wiki.archlinux.org/index.php/Tmpfs), чтобы это исправить, и помните о том, что память выделенная для tmpfs становится недоступной для других целей. Другими словами, система с гигантской tmpfs и большими файлами в ней может израсходовать всю память и упасть. Другая подсказка: во время редактирования файла /etc/fstab, помните о том, что он должен заканчиваться новой строкой, иначе ваша система не загрузится.

/proc и /sys

Помимо /tmp, VFS (виртуальные файловые системы), которые наиболее знакомы пользователям Linux – это /proc и /sys. (/dev располагается в общей памяти и не имеет file_operations). Почему именно эти два компонента? Давайте разберемся в этом вопросе.

procfs создает снимок мгновенного состояния ядра и процессов, которые он контролирует для userspace. В /proc ядро выводит информацию о том, какими средствами оно располагает, например, прерывания, виртуальная память и планировщик. Кроме того, /proc/sys – это место, где параметры, настраиваемые с помощью команды sysctl, доступны для userspace. Статус и статистика отдельных процессов выводится в каталогах /proc/<PID>.

Здесь /proc/meminfo-это пустой файл, который тем не менее содержит ценную информацию.

Поведение /proc файлов показывает, какими непохожими могут быть дисковые файловые системы VFS. С одной стороны, /proc/meminfo содержат информацию, которую можно посмотреть командой free. С другой же, там пусто! Как так получается? Ситуация напоминает знаменитую статью под названием «Существует ли луна, когда на нее никто не смотрит? Реальность и квантовая теория» (http://www-f1.ijs.si/~ramsak/km1/mermin.moon.pdf), написанную профессором физики Корнельского университета Дэвидом Мермином в 1985 году. Дело в том, что ядро собирает статистику памяти, когда происходит запрос к /proc, и на самом деле в файлах /proc ничего нет, когда никто туда не смотрит. Как сказал Мермин (https://en.wikiquote.org/wiki/David_Mermin), «Фундаментальная квантовая доктрина гласит, что измерение, как правило, не выявляет ранее существовавшего значения измеряемого свойства.» (А над вопросом про луну подумайте в качестве домашнего задания!)

Кажущаяся пустота procfs имеет смысл, поскольку располагающаяся там информация динамична. Немного другая ситуация с sysfs. Давайте сравним, сколько файлов размером не менее одного байта есть в /proc и в /sys.

Procfs имеет один файл, а именно экспортированную конфигурацию ядра, которая является исключением, поскольку ее нужно генерировать только один раз за загрузку. С другой стороны, в /sys лежит множество более объемных файлов, многие из которых занимают целую страницу памяти. Обычно файлы sysfs содержат ровно одно число или строку, в отличие от таблиц информации, получаемой при чтении таких файлов, как /proc/meminfo.

Цель sysfs – предоставить свойства доступные для чтения и записи того, что ядро называет «kobjects» в userspace. Единственная цель kobjects – это подсчет ссылок: когда удаляется последняя ссылка на kobject, система восстановит ресурсы, связанные с ним. Тем не менее, /sys составляет большую часть знаменитого «stable ABI для userspace(https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/ABI/stable)» ядра, которое никто никогда, ни при каких обстоятельствах не может «сломать» (https://lkml.org/lkml/2012/12/23/75). Это не означает, что файлы в sysfs статичны, что противоречило бы подсчету ссылок на нестабильные объекты.

Стабильный двоичный интерфейс приложений ядра (kernel’s stable ABI) ограничивает то, что может появиться в /sys, а не то, что на самом деле присутствует в данный конкретный момент. Листинг разрешений на файлы в sysfs обеспечивает понимание того, как конфигурируемые параметры устройств, модулей, файловых систем и т.д. могут быть настроены или прочитаны. Делаем логический вывод, что procfs также является частью stable ABI ядра, хотя это не указано явно в документации (https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/ABI/stable).

Файлы в sysfs описывают одно конкретное свойство для каждой сущности и могут быть читаемыми, перезаписываемыми или и то и другое сразу. «0» в файле говорит о том, что SSD не может быть удален.

Как наблюдать за VFS с помощью инструментов eBPF и bcc

Самый простой способ понять, как ядро оперирует файлами sysfs – это посмотреть за этим на практике, а самый простой способ понаблюдать за ARM64 – это использовать eBPF. eBPF (сокращение от Berkeley Packet Filter) состоит из виртуальной машины, запущенной в ядре httpss://events.linuxfoundation.org/sites/events/files/slides/bpf_collabsummit_2015feb20.pdf), которую привилегированные пользователи могут запрашивать (query) из командной строки. Исходники ядра сообщают читателю, что может сделать ядро; запуск инструментов eBPF в загруженной системе показывает, что на самом деле делает ядро.

К счастью, начать использовать eBPF достаточно легко с помощью инструментов bcc (https://github.com/iovisor/bcc), которые доступны в качестве пакетов из общего дистрибутива Linux (https://github.com/iovisor/bcc/blob/master/INSTALL.md) и подробно задокументированы Бернардом Греггом (http://brendangregg.com/ebpf.html). Инструменты bcc – это скрипты на Python с маленькими вставками кода на С, это означает, что каждый, кто знаком с обоими языками может с легкостью их модифицировать. В bcc/tools есть 80 Python скриптов, а это значит, что скорее всего разработчик или системный администратор сможет подобрать себе что-нибудь подходящее для решения задачи.

Чтобы получить хотя бы поверхностное представление о том, какую работу выполняют VFS в запущенной системе, попробуйте vfscount или vfsstat. Это покажет, допустим, что десятки вызовов vfs_open() и «его друзей» происходят буквально каждую секунду.

vfsstat.py – это скрипт на Python, со вставками С кода, который просто считает вызовы функций VFS.

Приведем более тривиальный пример и посмотрим, что бывает, когда мы вставляем USB-флеш накопитель в компьютер и его обнаруживает система.

С помощью eBPF можно посмотреть, что происходит в /sys, когда вставлен USB-флеш накопитель. Здесь показан простой и сложный пример.

В примере, показанном сверху, bcc инструмент trace.py (https://github.com/iovisor/bcc/blob/master/tools/trace_example.txt) выводит сообщение, когда запускается команда sysfs_create_files(). Мы видим, что sysfs_create_files() был запущен с помощью kworker потока в ответ на то, что флешка была вставлена, но какой файл при этом создался? Второй пример показывает всю мощь eBPF. Здесь trace.py выводит обратную трассировку ядра (kernel backtrace) (опция -K) и имя файла, который был создан sysfs_create_files(). Вставка в одиночных высказываниях – это код на С, включающий легко распознаваемую строку формата, обеспечиваемую Python скриптом, который запускает LLVM just-in-time компилятор. Эту строку он компилирует и выполняет в виртуальной машине внутри ядра. Полная сигнатура функции sysfs_create_files () должна быть воспроизведена во второй команде, чтобы строка формата могла ссылаться на один из параметров. Ошибки в этом фрагменте кода на С приводят к распознаваемым ошибкам C-компилятора. Например, если пропущен параметр -l, то вы увидите «Failed to compile BPF text.» Разрабочики, которые хорошо знакомы с С и Python, найдут инструменты bcc простыми для расширения и изменения.

Когда USB-накопитель вставлен, обратная трассировка ядра покажет, что PID 7711 – это поток kworker, который создал файл «events» в sysfs. Соответственно, вызов с sysfs_remove_files() покажет, что удаление накопителя привело к удалению файла events, что соответствует общей концепции подсчета ссылок. При этом, просмотр sysfs_create_link () с eBPF во время вставки USB-накопителя покажет, что создано не менее 48 символьных ссылок.

Так в чем же смысл файла events? Использование cscope (http://northstar-www.dartmouth.edu/doc/solaris-forte/manuals/c/user_guide/cscope.html) для поиска __device_add_disk() (https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/block/genhd.c#n665), показывает, что она вызывает disk_add_events (), и либо «media_change», либо «eject_request» могут быть записаны в файл событий. Здесь блочный слой ядра информирует userspace о появлении и извлечении «диска». Обратите внимание, насколько информативен этот метод исследования на примере вставки USB-накопителя по сравнению с попытками выяснить, как все работает, исключительно из исходников.

Корневые файловые системы только для чтения делают возможными встроенные устройства

Конечно, никто не выключает сервер или свой компьютер, вытаскивая вилку из розетки. Но почему? А все потому что смонтированные файловые системы на физических устройствах хранения могут иметь отложенные записи, а структуры данных, записывающие их состояние, могут не синхронизироваться с записями в хранилище. Когда это случается, владельцам системы приходится ждать следующей загрузки для запуска утилиты fsck filesystem-recovery и, в худшем случае, потерять данные.

Тем не менее, все мы знаем, что многие IoT устройства, а также маршрутизаторы, термостаты и автомобили теперь работают под управлением Linux. Многие из этих устройств практически не имеют пользовательского интерфейса, и нет никакого способа выключить их «чисто». Представьте себе запуск автомобиля с разряженной батареей, когда питание управляющего устройства на Linux (https://wiki.automotivelinux.org/_media/eg-rhsa/agl_referencehardwarespec_v0.1.0_20171018.pdf) постоянно скачет вверх-вниз. Как получается, что система загружается без длинного fsck, когда двигатель наконец начинает работать? А ответ прост. Встроенные устройства полагаются на корневую файловую систему только для чтения (https://elinux.org/images/1/1f/Read-only_rootfs.pdf) (сокращенно ro-rootfs (read-only root fileystem)).

ro-rootfs предлагают множество преимуществ, которые менее очевидны, чем неподдельность. Одно из преимуществ заключается в том, что вредоносное ПО не может писать в /usr или /lib, если ни один процесс Linux не может туда писать. Другое заключается в том, что в значительной степени неизменяемая файловая система имеет решающее значение для полевой поддержки удаленных устройств, поскольку вспомогательный персонал пользуется локальными системами, которые номинально идентичны системам на местах. Возможно, самым важным (но и самым коварным) преимуществом является то, что ro-rootfs заставляет разработчиков решать, какие системные объекты будут неизменными, еще на этапе проектирования системы. Работа с ro-rootfs может быть неудобной и болезненной, как это часто бывает с переменными const в языках программирования, но их преимущества легко окупают дополнительные накладные расходы.

Создание rootfs только для чтения требует некоторых дополнительных усилий для разработчиков встраиваемых систем, и именно здесь на сцену выходит VFS. Linux требует, чтобы файлы в /var были доступны для записи, и, кроме того, многие популярные приложения, которые запускают встроенные системы, будут пытаться создать конфигурационные dot-files в $HOME. Одним из решений для конфигурационных файлов в домашнем каталоге обычно является их предварительная генерация и сборка в rootfs. Для /var один из возможных подходов — это смонтировать его в отдельный раздел, доступный для записи, в то время как сам / монтируется только для чтения. Другой популярной альтернативой является использование связывемых или накладываемых маунтов (bind or overlay mounts).

Связываемые и накладываемые маунты, использование их контейнерами

Выполнение команды man mount – лучший способ узнать про связываемые и накладываемые маунты, которые дают разработчикам и системным администраторам возможность создавать файловую систему по одному пути, а затем предоставлять ее приложениям в другом. Для встроенных систем это означает возможность хранить файлы в /var на флеш-накопителе, доступном только для чтения, но накладываемое или связываемое монтирование пути из tmpfs в /var при загрузке позволит приложениям записывать туда заметки(scrawl). При следующем включении изменения в /var будут утеряны. Накладываемое монтирование создает объединение между tmpfs и нижележащей файловой системой и позволяет делать якобы изменения существующих файлов в ro-tootf тогда как связываемое монтирование может сделать новые пустые tmpfs папки видимыми как доступные для записи в ro-rootfs путях. В то время как overlayfs это правильный (proper) тип файловой системы, связываемое монтирование реализовано в пространстве имен VFS (https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/sharedsubtree.txt).

Основываясь на описании накладываемого и связываемого монтирования, никто не удивляется что Linux контейнеры(https://coreos.com/os/docs/latest/kernel-modules.html) активно их используют. Давайте понаблюдаем, что происходит, когда мы используем systemd-nspawn(https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html) для запуска контейнера, используя инструмент mountsnoop от bcc.

Вызов system-nspawn запускает контейнер во время работы mountsnoop.py.

Посмотрим, что получилось:

Запуск mountsnoop во время «загрузки» контейнера показывает, что среда выполнения контейнера сильно зависит от связываемого монтирования. (Отображается только начало длинного вывода)

Здесь systemd-nspawn предоставляет выбранные файлы в procfs и sysfs хоста в контейнер как пути в его rootfs. Кроме MS_BIND флага, который устанавливает связывающее монтирование, некоторые другие флаги в монтируемой системе определяют взаимосвязь между изменениями в пространстве имен хоста и контейнера. Например, связываемое монтирование может либо пропускать изменения в /proc и /sys в контейнер, либо скрывать их в зависимости от вызова.

Заключение

Понимание внутреннего устройства Linux может казаться невыполнимой задачей, так как само ядро содержит гигантское количество кода, оставляя в стороне приложения пользовательского пространства Linux и интерфейсы системных вызовов в библиотеках на языке C, таких как glibc. Один из способов добиться прогресса — прочитать исходный код одной подсистемы ядра с акцентом на понимание системных вызовов и заголовков, обращенных к пространству пользователя, а также основных внутренних интерфейсов ядра, к примеру, таблица file_operations. Файловые операции обеспечивают принцип «все является файлом», поэтому управление ими особенно приятно. Исходные файлы ядра на языке C в каталоге верхнего уровня fs/ представляют реализацию виртуальных файловых систем, которые являются слоем оболочки, обеспечивающим широкую и относительно простую совместимость популярных файловых систем и устройств хранения. Монтирование со связыванием и накладыванием через пространства имен Linux — это волшебство VFS, которое делает возможным создание контейнеров и корневых файловых систем только для чтения. В сочетании с изучением исходного кода, средство ядра eBPF и его интерфейс bcc делают исследование ядра проще, чем когда-либо.


Нашли ошибку в тексте? Выделите фрагмент текста и нажмите Ctrl+Enter

Удаленная настройка для серверов и сайтов 24/7

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *