Источник: Roadmap to Learning Linux Device Drivers
streetdogg · 29 марта 2025 · 1321 слово · 7 минут чтения
Большинство инженеров, пишущих драйверы устройств для Linux, не могут объяснить, как загружается ядро Linux или как загружается драйвер устройства. Этот пост подробно описывает, как следует учиться писать драйверы устройств, чтобы уметь рассуждать вплоть до уровня железа.
Я происхожу из мира микроконтроллеров и разработки прошивок и заинтересовался изучением драйверов устройств для Linux. Предсказывать и понимать поведение ядра мне было несложно, поскольку я привык размышлять о том, что система сделает в результате выполнения моего кода. Вот мои рекомендации о том, как следует учиться разработке драйверов для ядра.
Ядро спроектировано универсальным и предоставляет общие возможности операционной системы, такие как управление ресурсами, питанием и процессами, среди прочего. Разработчики ядра не обязательно знают, на каком оборудовании (CPU и периферии) ядро будет работать. По этим причинам оно не может напрямую загрузиться на конкретном оборудовании.
Модели программиста (programmer's model) — это абстракции оборудования, которые программисты могут использовать для управления им. Эти абстракции скрывают детали уровня схем и предоставляют интерфейсы, обычно регистры, из которых программист может читать и в которые может писать.
Большинство разработчиков ядра начинают не с этого; вместо этого они начинают с «написания простого модуля ядра». По моему мнению, это серьёзная ошибка. Хотя они затем могут написать базовый драйвер, по моему опыту, им сложно рассуждать о лежащих в основе механиках, что делает их драйверы хрупкими и нестабильными.
Модели программиста включают следующее:
-
Поток загрузки CPU: детали регистров
control,statusиgeneral-purpose, а также архитектуры набора инструкций (Instruction Set Architecture,ISA). -
Модель исключений: охватывает детали того, как CPU обрабатывает прерывания (внешние события) и исключения (внутренние события) и реагирует на них.
-
Модель памяти: объясняет, как CPU взаимодействует с подсистемой памяти и как выполняются чтение и запись.
-
Модель отладки: каждый CPU поставляется с оборудованием, помогающим исследовать регистры CPU.
Вам не нужно осваивать эти модели в совершенстве, но вы должны знать об их существовании. На каком бы CPU ни работало ядро, вы должны уметь погружаться в перечисленные выше модели и рассуждать о них.
Я рекомендую работать с CPU на базе ARM64 (AArch64). Они широко используются в серверах, персональных устройствах (телефонах, планшетах, часах и т.д.), автомобилях и не только.
Ядро Linux не загружается на CPU напрямую. Вместо этого его загружает прошивка, известная как bootloader (загрузчик).
В процессе загрузки обычно несколько стадий, и всё, что происходит до того, как ядро возьмёт CPU под контроль, называется загрузчиками. Термины вроде Primary, Secondary и Secure Resident — просто красивые названия, основанные на конкретной реализуемой функциональности, но в конечном счёте все они готовят систему к передаче ядру.
Что я имею в виду под конкретной функциональностью? Ну, например, код ROM обычно предзагружается с завода и, как правило, отвечает за включение критических частей системы. После этого он передаёт управление (направляя CPU к коду другого загрузчика) следующему загрузчику, который может выполнять проверки безопасности, устанавливать защищённую прошивку (например, доверенные среды исполнения), включать и тренировать DRAM и так далее. Этот загрузчик обычно передаёт управление ещё одному загрузчику либо находит и загружает ядро.
Умение загрузить CPU с нуля позволяет рассуждать о функциональности загрузчика. Кроме того, это хороший способ узнать CPU и все модели, перечисленные в предыдущем разделе.
Хороший подход — изучить последовательность загрузки выбранного процессора, а затем написать простую программу на ассемблере, выполняющую прыжок в C-функцию. Обычно я пишу драйвер для UART (Universal Asynchronous Receiver and Transmitter — универсальный асинхронный приёмопередатчик) на языке C и вывожу «Hello, World!» в последовательный терминал. C-функции, соответствующие драйверу UART, вызываются из ассемблерного кода.
Второе, что я делаю, — переключаю (toggle) пин GPIO (General Purpose Input/Output — ввод-вывод общего назначения). Это даёт мне представление о том, как нужно программировать периферию. Программирование CPU с помощью кода для «голого железа» (bare-metal) позволит освоить следующие важные навыки/темы:
-
Архитектура набора инструкций CPU.
-
Утилиты инструментария компилятора (gcc, gdb, ld, as и т.д.).
-
Инструменты сборки, скрипты компоновщика и Makefile.
-
Чтение даташита SoC (System On Chip — система на кристалле) 1.
Выше я отметил, что ядро не знает, на каком оборудовании (SoC) оно будет загружено. Ядру для корректной работы требуются определённые работоспособные аппаратные блоки. Например, системный таймер необходим для периодического переключения между процессами.
Вопрос, который мы должны задать себе: «Как ядро узнаёт об оборудовании, на котором оно работает?»
Ответ — дерево устройств (device tree)!
Это формат структуры данных, в котором представлена компоновка оборудования. Дерево устройств представлено в текстовом файле, который затем компилируется в нечто, называемое двоичным объектом дерева устройств (device tree binary object, dtbo). Для конкретного оборудования будет существовать исходник дерева устройств (написанный и отправленный в основную ветку исходников ядра инженерами соответствующего производителя SoC). Этот исходник компилируется компилятором дерева устройств и преобразуется в двоичный формат.
Следующий вопрос: как ядро получает двоичный объект дерева устройств, чтобы узнать о компоновке оборудования? Ответ: его передаёт загрузчик.
Файл dtbo помещается в определённое место в памяти, и его адрес записывается в назначенный регистр CPU перед передачей управления образу ядра. Как только образ ядра начинает выполняться, он считывает конфигурацию оборудования и определяет, какие драйверы нужно загрузить, среди прочего.
Дерево устройств кодирует, наряду с деталями оборудования, драйверы, которые ядро может использовать для управления устройством. Эти драйверы компилируются как часть образа ядра. Где же, спросите вы, находятся исходники драйверов устройств? В исходном коде ядра.
Стоит подумать о написании драйвера устройства для управления состоянием пина GPIO (предполагая, что к нему подключён светодиод), разработав драйвер устройства. Его написание познакомит вас с деталями того, как отображаемый в память ввод-вывод (memory-mapped I/O) может управляться изнутри драйвера и как драйвер должен быть связан с блоком через дерево устройств.
Критерием успеха может быть то, что светодиод должен включаться или выключаться в зависимости от чтений и записей в файл устройства.
Это действительно тот самый проект, который стоит выполнить, чтобы изучить драйверы устройств правильным способом. По моему мнению, лучшая платформа для этого — любой из одноплатных компьютеров (я предпочитаю Raspberry Pi).
Прерывания — очень частое явление в мире драйверов устройств, и умение настроить GPIO как источник прерываний и управлять им с помощью драйвера — это финальное ноу-хау, необходимое, прежде чем вы сможете назвать себя разработчиком драйверов устройств. Рассмотрите расширение драйвера из предыдущего раздела, чтобы использовать GPIO как источник прерываний.
Мне нравится программировать GPIO как источник прерываний, а затем генерировать прерывания нажатием кнопки, подключённой к нему.
Именно это «ванильные» разработчики драйверов устройств используют с самого начала. Средства ядра (kernel facilities) — это фреймворки и полезные возможности, которые ядро предоставляет для упрощения разработки драйверов устройств. Такие вещи, как очереди, рабочие очереди (work queue), очереди таймеров, мьютексы, семафоры и т.д., — примеры средств ядра.
Знание того, как ими пользоваться, не только поможет сделать сырой драйвер более надёжным и готовым к продакшену, но и расширит ваш набор навыков.
Для меня настоящий разработчик драйверов ядра может очень ясно рассуждать от процесса загрузки и вплоть до того, как драйвер управляет оборудованием. По моему мнению, семь шагов выше — это путь к достижению такого уровня мастерства.
- CPU обычно является частью System on Chip (системы на кристалле). ↩︎