Введение
Виртуальная Файловая Система (называемая также Коммутатором Виртуальной Файловой Системы) является программным слоем, который предоставляет программам, выполняющимся в пользовательском пространстве, интерфейс взаимодействия с файловой системой. VFS-коммутатор также реализует в ядре Linux уровень абстракции для унификации подключения к нему разнообразных файловых систем.
Системные вызовы VFS , , , , и так далее, вызываются из контекста работающего процесса. Filesystem locking описана в . (VFS system calls open(2), stat(2), read(2), write(2), chmod(2) and so on are called from a process context. Filesystem locking is described in the document Documentation/filesystems/locking.rst.)
VFS обеспечивает системные вызовы , , и другие похожие. Аргумент , который передаётся на вход этим системным вызовам, используется VFS-системой для поиска вхождений в кэше записей каталога (называемым также dentry cache, или ещё короче dcache). Это обеспечивает сверхбыстрый механизм трансляции (filename) в определённый . Кэш записей каталога необходим только для увеличения производительности, поэтому он хранится в оперативной памяти и никогда не записывается на диск.
Dentry-кэш предназначен для обзора всего файлового пространства. А так как большинство компьютеров не могут вместить все записи dentry в оперативную память, то некоторое количество записей в dentry-кэше может отсутствовать. Поэтому, иногда в процессе сопоставления частей pathname с соответствующими dentry, VFS-коммутатору, возможно, придётся заодно создать некоторое количество dentry, и только после этого загрузить требуемый inode.
Объект Inode / The Inode Object
Отдельный dentry обычно указывает на inode, который является специальной структурой файловой системы, описывающей её файлы-объекты. Объект inode предоставляет различные сведения о файле (). В частности:
- – id устройства, содержащего файл;
- – номер этой inode в таблице inodes;
-
– тип объекта ():
- обычный файл с данными;
- directory;
- block device;
- …
- – количество жёстких ссылок (имён файлов) на этот inode.
Inodes могут находиться на дисках (на файловых системах блочных устройств), либо в оперативной памяти (на псевдо-файловых системах). Inodes, находящиеся на дисках, при необходимости копируются в оперативную память, а при каких-либо в них изменениях, обновлённая копия записывается обратно на диск. Несколько dentry могут указывать на один inode (в случае жёстких ссылок и тому подобное).
Для поиска какого-либо inode необходимо, чтобы для его родительского inode-каталога, VFS вызвала метод . Этот метод установлен конкретной реализацией ФС, к которой относится требуемый inode. (This method is installed by the specific filesystem implementation that the inode lives in.) С момента получения VFS-коммутатором dentry (а следовательно и соответствующего inode), мы можем делать с ним всякие скучные вещи, типа открыть файл посредством системного вызова , или получить данные inode через . Из пользовательского пространства системный вызов работает довольно просто: как только VFS заимел соответствующий dentry, он передаёт данные из inode обратно в userspace (The stat(2) operation is fairly simple: once the VFS has the dentry, it peeks at the inode data and passes some of it back to userspace.).
Объект File / The File Object
Открытие какого-либо файла требует ещё одной операции: выделение файловой структуры (это реализация файловых дескрипторов на стороне ядра). Свеже выделенная файловая структура инициализируется с указателем на соответствующий dentry и на набор файловых операций функций-членов. (The freshly allocated file structure is initialized with a pointer to the dentry and a set of file operation member functions.) Они берутся из сведений, содержащихся в соответствующей inode. Применяемый к файлу метод вызывается тогда, как только конкретная реализация файловой системы сможет сделать это. Вы можете видеть, что это ещё одна функция выполняемая VFS-коммутатором. Файловая структура помещается в таблицу дескрипторов файлов для вызывающего процесса.
Чтение, запись и закрытие файла (и другие подобные операции VFS) осуществляются с помощью соответствующих методов, перечисленных в подходящей к требуемому файлу файловой структуре, каковая структура становится доступной из пространства пользователя через использование конкретного файлового дескриптора. Dentry остаётся в использовании, пока файл открыт, что, в свою очередь, означает, что VFS inode остаётся в использовании.
Реализации
Положение уровня VFS в различных частях стека хранения ядра Linux .
Джон Хайдеманн разработал стек VFS под SunOS 4.0 для экспериментальной файловой системы Ficus . Этот дизайн предусматривал повторное использование кода среди типов файловых систем с различной, но схожей семантикой ( например , файловая система с шифрованием может повторно использовать весь код именования и управления хранением файловой системы без шифрования). Хайдеманн адаптировал эту работу для использования в 4.4BSD как часть своей дипломной работы; потомки этого кода лежат в основе реализации файловой системы в современных производных BSD, включая macOS .
Другие виртуальные файловые системы Unix включают переключатель файловой системы в System V Release 3 , Generic File System в Ultrix и VFS в Linux . В OS / 2 и Microsoft Windows механизм виртуальной файловой системы называется устанавливаемой файловой системой .
Механизм файловой системы в пользовательском пространстве (FUSE) позволяет коду пользовательского пространства подключаться к механизму виртуальной файловой системы в Linux, NetBSD , FreeBSD , OpenSolaris и macOS.
В Microsoft Windows виртуальные файловые системы также могут быть реализованы с помощью расширений пространства имен оболочки пользовательского уровня ; однако они не поддерживают программные интерфейсы приложений доступа к файловой системе самого низкого уровня в Windows, поэтому не все приложения смогут получить доступ к файловым системам, реализованным как расширения пространства имен. KIO и GVfs / GIO предоставляют аналогичные механизмы в средах рабочего стола KDE и GNOME (соответственно) с аналогичными ограничениями, хотя их можно заставить использовать методы FUSE и, следовательно, беспрепятственно интегрировать в систему.
Основы файловых систем
Ядро Linux требует, чтобы во всём, что считается файловой системой были реализованы методы open(), read(), и write()для постоянных объектов, у которых есть имена. С точки зрения объективно ориентированного программирования, ядро считает файловую систему абстрактным интерфейсом, в котором определены эти виртуальные функции без реализации. Таким образом реализация файловой системы на уровне ядра называется VFS (Virtual Filesystem).
Мы можем открыть, прочитать и записать в файл.
Термин VFS лежит в основе всем известного утверждения о том, что в Unix-подобных системах всё является файлом. Подумайте о том, насколько странно, что приведенная выше последовательность действий с файлом /dev/console работает. На снимке показан интерактивный сеанс Bash в виртуальном терминале TTY. При отправке строки устройству виртуальной консоли, она появляется на виртуальном экране. VFS имеет и другие, даже более странные свойства. Например, в таких файлах можно выполнять поиск.
В таких популярных файловых системах как Ext4, NFS и даже в подсистеме /proc реализованы три основные функции в структуре данных на языке Си, которая называется file_operations. Кроме того, некоторые файловые системы расширяют и переопределяют функции VFS подобным объективно ориентированным способом. Как утверждает Роберт Лав, абстракция VFS позволяет пользователям Linux копировать файлы из других операционных систем или абстрактных объектов, таких как каналы не беспокоясь об их внутреннем формате данных. В пространстве пользователя с помощью системного вызова read() процессы могут копировать содержимое файла в структуры ядра из одной файловой системы, а затем использовать системный вызов write() в другой файловой системе, чтобы записать полученные данные в файл.
Определения функций, относящиеся к VFS находятся в файлах fs/*.c в исходном коде ядра. Подкаталоги fs/ же содержат различные файловые системы. Ядро также содержит объекты, похожие на файловые системы, это cgroups, /dev и tmpfs, которые нужны на раннем этапе загрузки системы и поэтому определены в подкаталоге исходников init/
Обратите внимание, что они не вызывают функции большой тройки из file_operations, зато они могут непосредственно читать и записывать в память
На схеме ниже наглядно показано, как из пространства пользователя можно получить доступ к большинству файловых систем. На рисунке нет каналов, таймера POSIX и dmesg, но они тоже используют методы из структуры file_operations и работают через VFS:
Существование VFS способствует повторному использованию кода, поскольку основные методы для работы с файловыми системами не переопределяются в каждой файловой системе. Это широко используемая практика, однако если в таком коде есть ошибки, то от них страдают все реализации, использующие общие методы.
Однофайловые виртуальные файловые системы
Иногда виртуальная файловая система относится к файлу или группе файлов (не обязательно внутри конкретной файловой системы), которые действуют как управляемый контейнер, который должен обеспечивать функциональность конкретной файловой системы за счет использования программного обеспечения. Примерами таких контейнеров являются CBFS Storage или однофайловая в эмуляторе, таком как PCTask или так называемый WinUAE , Oracle VirtualBox , Microsoft Virtual PC , VMware .
Основное преимущество файловой системы этого типа заключается в том, что она централизована и легко удаляется. Однофайловая виртуальная файловая система может включать все основные функции, ожидаемые от любой файловой системы (виртуальной или иной), но доступ к внутренней структуре этих файловых систем часто ограничивается программами, специально написанными для использования однофайловой виртуальной файловой системы. файловая система (вместо реализации через драйвер, обеспечивающий универсальный доступ). Еще один серьезный недостаток — относительно низкая производительность по сравнению с другими виртуальными файловыми системами. Низкая производительность в основном связана с затратами на перемешивание виртуальных файлов при записи или удалении данных из виртуальной файловой системы.
Реализация однофайловых виртуальных файловых систем
Прямые примеры однофайловых виртуальных файловых систем включают эмуляторы, такие как PCTask и WinUAE, которые инкапсулируют не только данные файловой системы, но также эмулируют структуру диска. Это позволяет легко относиться к установке ОС, как и к любому другому программному обеспечению, — переносить ее со съемного носителя или по сети.
PCTask
Амигу эмулятор PCTask эмулировать Intel PC на основе машины с тактовой частотой 4,77 МГц (а позже SX с тактовой частотой 25 МГц). Пользователи PCTask могли создать файл большого размера в файловой системе Amiga, и к этому файлу можно было бы получить виртуальный доступ из эмулятора, как если бы это был настоящий жесткий диск ПК. Файл может быть отформатирован в файловой системе FAT16 для хранения обычных файлов MS-DOS или Windows.
WinUAE
ОАЭ для ОС Windows , WinUAE , позволяют большие отдельные файлы на Windows , следует рассматривать как Amiga файловых систем. В WinUAE этот файл называется жестким файлом .
ОАЭ также могут рассматривать каталог в файловой системе хоста ( Windows , Linux , macOS , AmigaOS ) как файловую систему Amiga.
Основы файловых систем
Ядро Linux требует, чтобы во всём, что считается файловой системой были реализованы методы open(), read(), и write() для постоянных объектов, у которых есть имена. С точки зрения объективно ориентированного программирования, ядро считает файловую систему абстрактным интерфейсом, в котором определены эти виртуальные функции без реализации. Таким образом реализация файловой системы на уровне ядра называется VFS (Virtual Filesystem).
Мы можем открыть, прочитать и записать в файл.
Термин VFS лежит в основе всем известного утверждения о том, что в Unix-подобных системах всё является файлом. Подумайте о том, насколько странно, что приведенная выше последовательность действий с файлом /dev/console работает. На снимке показан интерактивный сеанс Bash в виртуальном терминале TTY. При отправке строки устройству виртуальной консоли, она появляется на виртуальном экране. VFS имеет и другие, даже более странные свойства. Например, в таких файлах можно выполнять поиск.
В таких популярных файловых системах как Ext4, NFS и даже в подсистеме /proc реализованы три основные функции в структуре данных на языке Си, которая называется file_operations. Кроме того, некоторые файловые системы расширяют и переопределяют функции VFS подобным объективно ориентированным способом. Как утверждает Роберт Лав, абстракция VFS позволяет пользователям Linux копировать файлы из других операционных систем или абстрактных объектов, таких как каналы не беспокоясь об их внутреннем формате данных. В пространстве пользователя с помощью системного вызова read() процессы могут копировать содержимое файла в структуры ядра из одной файловой системы, а затем использовать системный вызов write() в другой файловой системе, чтобы записать полученные данные в файл.
Определения функций, относящиеся к VFS находятся в файлах fs/*.c в исходном коде ядра. Подкаталоги fs/ же содержат различные файловые системы. Ядро также содержит объекты, похожие на файловые системы, это cgroups, /dev и tmpfs, которые нужны на раннем этапе загрузки системы и поэтому определены в подкаталоге исходников init/
Обратите внимание, что они не вызывают функции большой тройки из file_operations, зато они могут непосредственно читать и записывать в память
На схеме ниже наглядно показано, как из пространства пользователя можно получить доступ к большинству файловых систем. На рисунке нет каналов, таймера POSIX и dmesg, но они тоже используют методы из структуры file_operations и работают через VFS:
Существование VFS способствует повторному использованию кода, поскольку основные методы для работы с файловыми системами не переопределяются в каждой файловой системе. Это широко используемая практика, однако если в таком коде есть ошибки, то от них страдают все реализации, использующие общие методы.
Слежение за VFS
Самый простой способ узнать как ядро управляет файлами в sysfs — это посмотреть на это всё в действии. А самый простой способ это сделать на ARM64 или x86_64 — это использование eBPF. eBPF (extended Berkeley Packet Filter) — состоит из виртуальной машины, работающей на уровне ядра, к которой привилегированные пользователи могут обращаться из командной строки. Исходный код ядра показывает читателю как ядро может что-то сделать. Инструменты eBPF показывают как на самом деле всё происходит.
К счастью, начать работу с eBPF довольно просто с помощью инструментов bcc, для которых доступны пакеты в множестве дистрибутивов. Инструменты bcc — это скрипты на Python с небольшими фрагментами кода на Си, а это значит, что любой кто знаком с этим языком может их модифицировать. На данный момент существует около 80 скриптов на Python в bcc, поэтому каждый найдёт то, что ему надо.
Чтобы получить общее представление о работе VFS в работающей системе используйте простые скрипты vfscount и vfsstat, которые покажут, что каждую секунду выполняются десятки вызовов vfs_open() и подобных функций:
В качестве менее общего примера, давайте посмотрим что происходит, когда к работающей системе подключается USB накопитель:
В первом примере на этом снимке скрипт trade.py выводит сообщение всякий раз, когда вызывается функция sysfs_create_files(). Вы можете видеть, что эта функция была вызвана процессом kworker после подключения USB, но какой файл был создан? Следующий пример иллюстрирует полную силу eBPF. Скрипт trade.py выводит трассировку вызовов ядра (опция -K), а также имя файла, созданного функцией sysfs_create_files(). Фрагмент в одинарных кавычках — это строка кода на Си, которую Python скрипт компилирует и выполняет внутри виртуальной машины в ядре. Полную сигнатуру функции sysfs_create_files () надо воспроизвести во втором примере чтобы можно было ссылаться на один из её параметров в функции вывода. Ошибки в этом коде вызовут ошибки компиляции.
Когда USB-накопитель вставлен, появляется трассировка вызовов ядра, показывающая что один из потоков kworker с PID 7711 создал файл с именем events в sysfs. При попытке отслеживать вызов sysfs_remove_files() вы увидите, что извлечение флешки приводит к удалению файла events в соответствии с идеей отслеживания ссылок. Отслеживание sysfs_create_link() во время подключения USB накопителя показывает что создается не менее 48 ссылок.
Зачем же нужен файл events? Используя инструмент cscope можно найти функцию __device_add_disk(), которая вызывает функцию disk_add_events(). А та в свою очередь может записать в файл «media_change» или «eject request». Здесь ядро сообщает в пользовательское пространство о появлении или исчезновении диска. Это намного информативнее, чем просто анализ исходников.
/tmp
Самый простой способ вывести все виртуальные файловые системы, это выполнить такую команду:
Она выведет все смонтированные файловые системы, которые не связанны с физическим или сетевым диском. Оной из первых точек монтирования виртуальных файловых систем будет /tmp. Так почему не рекомендуется хранить содержимое /tmp на диске? Потому что файлы из /tmp временные, а постоянные хранилища намного медленнее памяти, где находится tmpfs. Кроме того, физические устройства более подвержены износу от частой записи, в отличие от оперативной памяти. И наконец, файлы в /tmp могут содержать конфиденциальную информацию, поэтому их лучше удалять при каждой перезагрузке.
Организация работы с двумя и более файловыми системами
Разработчики операционных систем стремятся обеспечить пользователя возможностью работать сразу с несколькими файловыми системами. В этом понимании файловая система состоит из многих составляющих, в число которых входят и файловые системы в традиционном понимании.
На верхнем уровне располагается так называемый переключатель файловых систем. Он обеспечивает интерфейс между запросами приложения и конкретной файловой системой, к которой обращается это приложение. Переключатель файловых систем преобразует запросы в формат, воспринимаемый следующим уровнем — уровнем файловых систем.
Каждый компонент уровня файловых систем выполнен в виде драйвера соответствующей файловой системы и поддерживает определенную организацию файловой системы. Переключатель является единственным модулем, который может обращаться к драйверу файловой системы. Приложение не может обращаться к нему напрямую. Каждый драйвер файловой системы в процессе собственной инициализации регистрируется у переключателя, передавая ему таблицу точек входа, которые будут использоваться при последующих обращениях к файловой системе.
Для выполнения своих функций драйверы файловых систем обращаются к подсистеме ввода-вывода, образующей следующий слой. Подсистема ввода-вывода — это составная часть файловой системы, которая отвечает за загрузку, инициализацию и управление всеми модулями низших уровней файловой системы. Обычно эти модули представляют собой драйверы портов, которые непосредственно занимаются работой с аппаратными средствами. Кроме этого подсистема ввода-вывода обеспечивает некоторый сервис драйверам файловой системы, что позволяет им осуществлять запросы к конкретным устройствам. Подсистема ввода-вывода должна постоянно присутствовать в памяти и организовывать совместную работу иерархии драйверов устройств. В эту иерархию могут входить драйверы устройств определенного типа (драйверы жестких дисков или накопителей на лентах), драйверы, поддерживаемые поставщиками (такие драйверы перехватывают запросы к блочным устройствам и могут частично изменить поведение существующего драйвера этого устройства, например, зашифровать данные), драйверы портов, которые управляют конкретными адаптерами.
Большое число уровней архитектуры файловой системы обеспечивает авторам драйверов устройств большую гибкость — драйвер может получить управление на любом этапе выполнения запроса — от вызова приложением функции, которая занимается работой с файлами, до того момента, когда работающий на самом низком уровне драйвер устройства начинает просматривать регистры контроллера. Многоуровневый механизм работы файловой системы реализован посредством цепочек вызова.
В ходе инициализации драйвер устройства может добавить себя к цепочке вызова некоторого устройства, определив при этом уровень последующего обращения. Подсистема ввода-вывода помещает адрес целевой функции в цепочку вызова устройства, используя заданный уровень для того, чтобы должным образом упорядочить цепочку. По мере выполнения запроса, подсистема ввода-вывода последовательно вызывает все функции, ранее помещенные в цепочку вызова.
Внесенная в цепочку вызова процедура драйвера может решить передать запрос дальше — в измененном или в неизмененном виде — на следующий уровень, или, если это возможно, процедура может удовлетворить запрос, не передавая его дальше по цепочке.
Способ организации файловой системы в Linux
Благодаря такому подходу, добавление поддержки какой-нибудь новой файловой системы не потребует вносить соответствующих изменений в само ядро ОС.
Виртуальная файловая система (сокр. «VFS» от англ. «Virtual File System») — это специальный слой абстракции, предоставляющий программный интерфейс (единый набор команд) для взаимодействия между ядром и конкретной реализацией файловой системы.
поддерживает различные типы файловых систем (ext3, ext4, ReiserFS, Btrfs, XFS и многие другие). На сегодняшний день наиболее часто используемой файловой системой является ext4, поэтому в данной статье основной упор будет сделан именно на нее.
Примечание: В Linux практически все объекты представлены в виде файлов (например, каталоги, принтеры, разделы диска, устройства и т.д.). Это делает еще более важным изучение того, как работает файловая система Linux.
Седлаем SD-карту
В этой части мы будем взаимодействовать с существующим драйвером контроллера SD-карты для Raspbrerry Pi 3, используя Foreign function interface или FFI для краткости. О FFI в Rust можно читнуть в . Помимо этого мы создадим глобальный дескриптор для файловой системы в нашей операционной системе. Работать будем в основном в .
Foregin Function Interface
FFI в Rust позволяет коду взаимодействовать с программным обеспечением, написанным на других языках программирования и наоборот. Внешние, по отношению к Rust, элементы объявляются в блоке :
Тут объявляется внешняя функция и внешняя глобальная переменная . Использовать их можно следующим образом:
Обратите внимание, что тут требуется использовать блок. Rust требует этого, поскольку он не может гарантировать правильность указанных объявлений
Компилятор слепо подставляет эти вызовы функций и взаимодействия с переменными. Другими словами, как и в любых других случаях использования небезопасного кода, Rust предполагает, что вы всё сделали правильно. При этом всём на этапе линковки символы и должны существовать. Иначе программа не соберётся.
Для вызова функции Rust из внешнего кода, местоположение функции (адрес в памяти) должно быть экспортировано в качестве определённого символа. Внутри Rust может свободно искажать (mangles) символы, которые присваиваются функциям. Для управления версиями и всем таким. Получается, что по умолчанию нельзя узнать заранее, какой символ будет присвоен каждой функции и следовательно мы не сможем вызвать эту функцию из внешнего кода. Для предотвращения этого произвола процесса мы можем добавить атрибут :
Затем программа на (например) Няшном Си может вызвать эту функцию таким образом:
Драйвер SD-карты
Мы предоставили предварительно скомпилированную библиотеку с драйвером SD-карты как . Помимо этого эта библиотека включена в процесс сборки. Т.е. библиотека уже связана с ядром. Кроме того в предоставлены объявления всего, что экспортирует эта библиотека.
Сама библиотека зависит от функции , которую она ожидает найти в нашем ядрышке. Функция должна отправлять процессор в сон на указанное количество микросекунд. Вам нужно будет создать и экспортировать эту функцию для успешной линковки. В Няшном Си объявление этой функции выглядит следующим образом:
Задача — обернуть внешний небезопасный API в безопасный Rust-код. Реализуйте структуру , которая инициализирует контроллер SD-карты в методе . Затем реализуйте трейт для . Вам нужно будет использовать для взаимодействия с внешними элементами. Проверьте свою реализацию, вручную прочитав MBR прямо из . Убедитесь, что прочитанные байтики соответсвуют ожидаемым. Когда всё заработает так, как ожидается, переходите к следущему разделу.
Файловая система
В этой части мы будем инициализировать глобальную файловую систему для использования нашим ядром. Основная работа в .
Как и аллокатор памяти, файловая система является глобальным ресурсом. Мы хотим, чтоб оно было доступно всегда и везде. Для того, чтоб это работало, мы создали глобальную переменную в файле . Как и аллокатор, наша ФС начинает свою работу в неинициализированном состоянии.
На текущий момент у нас есть файловая система и драйвер диска. Пришло время связать их вместе. Доделайте реализацию структуры из , используя при этом файловую систему FAT32 и наши биндинги к драйверу SD-карты. Вы должны инициализировать файловую систему при помощи (реализующей ) в функции . Затем реализуйте трейт для структуры, переведя все вызовы на . И в конце убедитесь, что инициализируете файловую систему из после аллокатора памяти.
Проверьте свою реализацию, распечатав содежримое корневого каталога () вашей SD-карты. Как только всё заработает так, как вы ожидаете — переходите к следующему этапу.