Современное развитие операционной системы Linux неразрывно связано с постоянным совершенствованием и расширением функциональности ядра. Одним из краеугольных камней этого процесса являются системные вызовы — интерфейс между пользовательским пространством и ядром, обеспечивающий доступ к ресурсам и сервисам операционной системы. За годы эволюции ядра Linux было введено множество системных вызовов, однако возникла фундаментальная проблема: как обеспечить их расширяемость и гибкость для поддержки новых функций без создания множества малозначительных, часто дублирующих друг друга, вызовов? Эта проблема стала предметом активного обсуждения сообщества, особенно на конференции Linux Plumbers 2020, где ведущие разработчики, такие как Кристиан Браунер и Алекса Сараи, представили современные взгляды и практики в области проектирования расширяемых системных вызовов. Одним из ключевых наблюдений стало повторяющееся явление, когда для решения новых задач разработчики создавали отдельные системные вызовы вместо расширения существующих. Примером стала история работы с переименованием файлов в Linux: начиная с простого rename(), затем появился renameat(), а позже и renameat2().
Каждый новый вызов был вызван необходимостью поддержки функций, которые не вписывались в старый интерфейс. Такой подход, помимо усложнения пользовательского пространства, приводит к раздробленности и трудностям с поддержкой различных версий ядра. В рамках обсуждений было выделено два традиционных пути расширения системных вызовов. Первый — использование мультиплексоров, когда один системный вызов выполняет несколько функций, определяемых специальным параметром. Второй — создание множества специализированных вызовов, каждый из которых реализует отдельную функцию.
Оба подхода имеют свои минусы: мультиплексоры затрудняют поддержку и усложняют библиотеки, а множественные вызовы приводят к дополнительной нагрузке на пользователя и разработчиков. Для решения этих проблем предложена концепция "расширяемых структур" (extensible structs), уже реализованная, например, в системном вызове openat2(). Суть метода заключается в том, что параметры системного вызова инкапсулируются в одной структуре, указатель на которую и её размер передаются в ядро. Размер структуры выступает в роли своеобразной версии, позволяющей ядру определить, какие поля доступны и должны быть обработаны. Такой подход обеспечивает двустороннюю совместимость: если пользовательское пространство передает структуру меньшего размера, ядро интерпретирует её как старую версию, заполняя отсутствующие поля нулями и сохраняя старое поведение.
Если ядро старое, а пользовательское пространство передало более крупную структуру, ядро проверит дополнительные поля, и если они не равны нулю, вызов завершится ошибкой, сигнализируя о неподдерживаемых функциях. Таким образом обеспечивается плавный переход и совместимость как для старых, так и для новых версий системного вызова. Обсуждения также затрагивали типизацию параметра flags, при этом предпочтение отдавалось unsigned int из-за его поведения при расширении знака, что может вызвать непредвиденные эффекты при использовании знаковых типов. Важно, что конвенции требуют от системных вызовов базовой возможности расширения, чтобы избежать необходимости введения новых вызовов ради поддержки дополнений. Однако расширяемые структуры не решают другой важный вопрос — как пользовательскому пространству узнавать, какие именно функции поддерживаются на текущем ядре.
Текущая практика требует проб и ошибок с обходом вызовов при недоступности новых функций, что дорого и неудобно. Одна из предложенных идей — введение отдельного системного вызова, который позволил бы запрашивать поддерживаемые ядром функции конкретного системного вызова. Архитектура такого вызова предполагает передачу номера системного вызова и битовой карты флагов, в которой каждое битовое поле соответствует отдельной функции или расширению. Таким образом, пользовательское пространство может эффективно узнать, какие функции доступны, без необходимости избыточных проверок и резервного копирования. Кроме того, обсуждалась возможность внедрения noop-флага в существующие вызовы, позволяющего «опрашивать» ядро без выполнения каких-либо действий, но данный подход рассматривался как своего рода мультиплексор, что противоречит основной идеологии новых рекомендаций.
Поэтому поклонники простоты склоняются к выделению отдельного системного вызова для определения возможностей, что облегчает поддержку и разработку устойчивого пользовательского ПО. Важным техническим аспектом является безопасность. Некоторые участники обсуждений подняли вопрос об инспекции указателей, встраиваемых в структуры параметров, особенно при использовании механизмов безопасности, таких как seccomp. В настоящее время фильтры seccomp не способны полноценно анализировать содержимое структур с указателями, что создает сложности для безопасного контроля вызовов. Однако исследователи и разработчики работают над расширением возможностей seccomp для поддержки таких сценариев, включая создание копий структур с последующей проверкой до передачи параметров в ядро.
Также обсуждался вопрос о том, почему размер структуры передается как отдельный параметр, а не хранится внутри самой структуры. Сторонники отдельного параметра аргументируют это тем, что в случае, когда структура переходит через различные слои пользовательского пространства, размер может изменяться, и выделенный параметр помогает избежать ошибок и атак, связанных с некорректным определением длины структуры. Важным итогом дискуссий стало общее согласие, что в будущем необходимо категорически отказаться от мультиплексорных системных вызовов, поскольку они усложняют реализацию в пользовательском пространстве, создают проблемы для библиотек и повышают вероятность ошибок. Вместо этого приемлемыми считаются вызовы с базовой возможностью расширения через флаги и структурированные параметры. Для развития и внедрения этих конвенций на уровне ядра планируется обновить официальную документацию, что позволит разработчикам ориентироваться на эти лучшие практики при создании новых системных вызовов.