Как системный вызов open(at) приводит к записи файла на диск?

Вопрос или проблема

Я стараюсь узнать как можно больше о взаимодействии между системными вызовами, VFS, обработкой драйверов устройств и, в конечном итоге, выполнением каких-либо операций конечным устройством. Я решил рассмотреть довольно тривиальный пример – создание файла – и попытаться понять основной процесс как можно подробнее.

Я написал немного кода на C, чтобы сделать немного больше, чем просто открыть (не существующий) файл для записи, скомпилировал его (без оптимизации) и заглянул в него с помощью strace, когда запустил. В частности, я хотел сосредоточиться на системном вызове openat и понять, почему и как этот вызов в конечном итоге смог не только создать объект файла / описание файла, но также фактически выполнить запись на диск (в качестве справки, файловая система EXT4, SATA HDD).

В общих чертах, исключая некоторые проверки и вспомогательные элементы, мое понимание процесса выглядит следующим образом (и, пожалуйста, поправьте меня, если я ошибаюсь!):

  • ELF загружается в память
  • libc загружается в память
  • вызывается fopen
  • libc выполняет открытие
  • вызывается системный вызов openat, с флагом O_CREAT среди прочих
  • Номер системного вызова помещается в регистр RAX
  • Аргументы системного вызова (например, путь к файлу и т.д.) помещаются в регистр RDI (и RSI, RDX и т.д. по мере необходимости)
  • Выпуск инструкции системного вызова и переход ЦП в кольцо 0
  • Код системного вызова, на который указывает регистр MSR_LSTAR, вызывается
  • регистры помещаются в стек ядра
  • Указатель функции из RAX вызывается с смещением в sys_call_table
  • Вызывается asmlinkage обертка для фактического кода системного вызова openat

И на этом этапе у меня недостаточно понимания, но в конечном итоге я знаю, что:

  1. Вызов open возвращает дескриптор файла, который уникален для процесса и поддерживается глобально в таблице дескрипторов файлов ядра
  2. FD сопоставляется с объектом описания файла
  3. Объект файла заполняется, среди прочих структур, структурой inode, inode_operations, file_operations и т.д.
  4. Таблица операций с файлами должна сопоставлять общие системные вызовы с соответствующими драйверами устройств для обработки соответствующих вызовов (так что, например, когда вызывается системный вызов write, вместо этого вызывается соответствующий вызов записи драйвера для устройства, на котором находится файл, например, SCSI драйвер)
  5. Это сопоставление основано на основных/меньшинских номерах для этого файла/устройства
  6. Где-то в процессе вызывается код, который вызывает инструкции, отправляемые на устройство для жесткого диска, которые отправляются контроллеру диска, что приводит к записи файла на жесткий диск, хотя не совсем ясно, происходит ли это через прерывания или DMA, или каким-либо другим способом ввода-вывода
  7. В конечном итоге контроллер диска отправляет сообщение обратно в ядро, чтобы сообщить, что все завершено, и ядро возвращает управление обратно в пользовательское пространство.

Я не слишком хорошо разбираюсь в исходном коде ядра, хотя и пытался немного, но ощущаю, что много чего не хватает. Мои вопросы следующие:

Я нашел некоторые функции, которые возвращают и уничтожают FD в исходном коде ядра, но не могу найти, где код, который на самом деле заполняет объект файла / описание файла для файла.

A) При системном вызове open или openat, когда создается новый файл, как заполняется структура файла? Откуда поступают данные? В частности, каким образом заполняются file_operations и inode_operations и т.д. для этого файла? Как ядро знает, заполняя эту структуру, что операции с файлами для этого конкретного файла должны быть от драйвера SCSI, например?

B) На каком этапе процесса – и особенно с ссылкой на исходный код – происходит переключение на драйвер устройства? Например, если был вызван ioctl или аналогичный, я ожидал бы какую-то ссылку на инструкцию, которая должна быть вызвана для соответствующего устройства, и некоторый адрес памяти для передачи данных, но я не могу найти, где это происходит.

Смотря на исходный код ядра, все, что я могу на самом деле найти – это код, который назначает новый FD, но ничего, что заполняло бы структуру файла, ничего, что вызывало бы соответствующие операции с файлами для передачи управления драйверу устройства.

Извините, что это действительно длинное описание, но я очень стараюсь узнать как можно больше, и хотя я имею базовые знания языка C, мне действительно трудно понять код других.

Надеюсь, кто-то с большим опытом, чем у меня, может помочь мне прояснить некоторые из этих вещей, так как я, похоже, наткнулся на образную стену. Любая помощь будет очень ценна.

Правка:
Надеюсь, следующие пункты прояснят, какие технические детали я ищу.

  • Системные вызовы open или openat принимают путь к файлу и флаги (при этом последний также получает FD, указывающий на директорию)
  • Когда также передается флаг O_CREAT, файл ‘создается’, если он не существует
  • На основе пути к файлу ядро может определить тип устройства, которым должен быть этот файл
  • Тип устройства обычно определяется по основным/меньшинским номерам – для файла, который уже существует, эти данные хранятся в структуре inode для файла (как элемент i_rdev) и в структуре stat для файла (как элементы st_dev для типа устройства файловой системы, на которой находится файл, и st_rdev для типа устройства самого файла)

Так что на самом деле, мои вопросы:

  1. Когда файл создается с помощью любого из системных вызовов open, соответствующая структура inode и stat также должна быть создана и заполнена – как эти системные вызовы open делают это (когда все, что у них есть на этом этапе, это путь к файлу и флаги)? Смотрят ли они на структуру inode или stat родительской директории и копируют соответствующие элементы этой структуры?

  2. На каком этапе (т.е. где в коде) это происходит?

  3. Насколько я понимаю, когда эти системные вызовы open вызываются, им нужно знать тип устройства, чтобы VFS знал, какой код драйвера устройства вызывать. При создании нового файла, где тип устройства еще не установлен в структурах объекта файла, что происходит? Какой код вызывается?

  4. Последовательность больше похожа на:

процесс пользователя пытается открыть новый файл -> open('/tmp/foo', O_CREAT) открыть -> найти структуру для ‘/tmp’, получить его тип устройства -> получить неиспользуемый FD -> заполнить структуру inode/stat, включая установку типа устройства родительского -> на основе типа устройства сопоставить операции с файлами / операции inode с кодом драйвера устройства -> вызвать код драйвера устройства для системного вызова open -> отправить соответствующую инструкцию контроллеру диска для записи нового файла на диск -> выполнить уборку, провести проверки и т.д. -> вернуть FD процессу-пользователю?

Вы в основном правы, но смешиваете некоторые нерелевантные детали (в основном из-за работы C).

Программа вызывает open(2) (системный вызов, т.е. вызов в ядро). Как это делается на самом деле, зависит от архитектуры и некоторых других деталей.

Внутренняя часть ядра системного вызова вызывается (опять же, детали зависят от архитектуры, точной версии операционной системы и т.д.) с аргументами (здесь запрашиваемый путь) и другой информацией, такой как учетные данные процесса.

На основе данного пути ядро знает, какая файловая система смонтирована на данном пути. Оно может выполнить обычный обход от корня вниз, проверяя на каждом шаге, есть ли у вызывающего стороны необходимые разрешения, существуют ли директории и так далее. Это может включать чтение данных с диска. Если все проверки пройдены, оно инициирует соответствующие изменения в структурах данных файловой системы для записи нового открытого файла, резервирования пространства, регистрации его в директории и так далее.

Изменения в структурах данных файловой системы (пока в памяти) предполагается, что в дальнейшем будут записаны на диск (или отправлены по сети и т.д.). Ядро будет ставить операции записи в очередь (если не требуется делать это немедленно) и вернется к пользователю.

Ответ или решение

Для понимания того, как системный вызов openat приводит к созданию файла и записи его на диск, важно рассмотреть ряд ключевых этапов, от вызова системного вызова до фактического выполнения операций записи на устройство. Давайте детализируем этот процесс.

1. Инициация системного вызова

Когда ваше приложение выполняет вызов openat, оно сначала обращается к библиотеке C (обычно libc), которая служит интерфейсом между пользовательскими приложениями и ядром операционной системы. Этот вызов включает указание пути к файлу и флагов, таких как O_CREAT, который указывает, что файл должен быть создан, если он не существует.

2. Переход к ядру

При вызове системного вызова происходит следующее:

  • Номер системного вызова сохраняется в регистре RAX.
  • Аргументы (например, путь к файлу, флаги) помещаются в соответствующие регистры (RDI, RSI и т.д.).
  • Происходит переключение из пользовательского режима (ring 3) в режим ядра (ring 0).

3. Обработка в ядре

Применение VFS (Virtual File System)

При переходе в режим ядра система использует механизм виртуальной файловой системы (VFS) для обработки входящего системного вызова. VFS предоставляет абстракцию для работы с различными файловыми системами и позволяет выполнить множество проверок, таких как:

  • Права доступа пользователя к указанному пути.
  • Наличие необходимых директорий в пути.

Когда производится поиск файла, VFS использует кэшированные данные, чтобы повысить производительность, обращаясь на диск только в случае необходимости.

Создание inode и file descriptor

  1. Определение файловой системы: Система определяет файловую систему, содержащую указанный файл, что происходит на основе пути к файлу. Если файл не существует и установлен флаг O_CREAT, VFS инициирует процесс создания нового inode.

  2. Создание inode: При создании нового файла происходит выделение нового inode, который хранит метаданные о файле, включая права доступа, владельца и т.д. Система также назначает номер устройства (major/minor) на основе предыдущих метаданных родительского каталога.

  3. Запись в директорий: Новый inode затем добавляется в структуру каталога (например, в /tmp), что позволяет файловой системе знать о существовании нового файла.

  4. Создание file descriptor: После успешного создания файла, пользовательское пространство получает уникальный дескриптор файла (FD), который ссылается на данный inode в памяти.

4. Определение операций файловой системы

Каждый inode ассоциирован с таблицей операций file_operations и inode_operations, которая определяет функции, вызываемые для чтения, записи и выполнения других операций с файлом. Эти операции настраиваются в зависимости от типа устройства (например, SCSI).

5. Вызов драйвера устройства

Когда приложение выполняет запись в файл (например, с помощью системного вызова write), происходит следующее:

  • Перевод команды: Системный вызов write снова вызывает обработчик в ядре, где VFS определяет, какая файловая система и драйвер устройства должны работать с записью.
  • Исполнение драйвера: Драйвер устройства получает управление, а все необходимые данные передаются ему. Это может происходить различными способами, такими как использование прерываний или DMA (прямого доступа к памяти).

6. Завершение записи на диск

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

Заключение

Процесс создания файла через системные вызовы, такие как openat, является сложным механизмом, объединяющим взаимодействие между различными системными компонентами: пользовательским приложением, библиотеками, ядром, файлами и драйверами устройств. Каждый этап требует точной координации и управления, чтобы обеспечить корректное выполнение операции.

Таким образом, основное внимание следует уделить как интерфейсу между пользовательским пространством и ядром, так и взаимодействию различных уровней абстракции, от приложений до физических устройств.

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

Капча загружается...