Написание модуля ядра Linux
Обновлено: 21.11.2024
Поскольку мы загружаем эти коды во время выполнения и они не являются частью официального ядра Linux, они называются загружаемыми модулями ядра (LKM), которые отличаются от «базового ядра». Базовое ядро находится в каталоге /boot и всегда загружается при загрузке нашей машины, тогда как LKM загружаются после того, как базовое ядро уже загружено. Тем не менее, эти LKM в значительной степени являются частью нашего ядра, и они взаимодействуют с базовым ядром для выполнения своих функций.
- драйвер устройства,
- драйвер файловой системы и
- Системные вызовы.
Итак, какие преимущества предлагают LKM?
Одним из основных преимуществ, которые они имеют, является то, что нам не нужно постоянно пересобирать ядро каждый раз, когда мы добавляем новое устройство или обновляем старое устройство. Это экономит время, а также помогает избежать ошибок в нашем базовом ядре. Полезное эмпирическое правило заключается в том, что мы не должны изменять наше базовое ядро, если у нас есть работающее базовое ядро.
Кроме того, это помогает в диагностике системных проблем. Например, предположим, что мы добавили модуль в базовое ядро (т. е. мы изменили наше базовое ядро, перекомпилировав его), и в модуле есть ошибка. Это вызовет ошибку при загрузке системы, и мы никогда не узнаем, какая часть ядра вызывает проблемы. В то время как если мы загружаем модуль во время выполнения и это вызывает проблемы, мы сразу узнаем о проблеме и можем выгрузить модуль, пока не исправим ее.
LKM очень гибкие в том смысле, что их можно загружать и выгружать с помощью единая линия команд. Это помогает экономить память, поскольку мы загружаем LKM только тогда, когда они нам нужны. Более того, они не медленнее базового ядра, потому что вызов любого из них — это просто загрузка кода из другой части памяти.
**Предупреждение: LKM не являются пользовательскими программами. Они являются частью ядра. Они имеют свободный доступ к системе и могут легко вывести ее из строя.
Итак, теперь, когда мы установили использование загружаемых модулей ядра, мы собираемся написать модуль ядра hello world. Это напечатает сообщение, когда мы загрузим модуль, и выходное сообщение, когда мы выгрузим модуль.
Первый пример, который всегда приводится в понимании любого языка программирования (или программирования ядра в нашем случае), — это написание примера hello world. Давайте напишем простую программу ядра, которая выводит "привет" и "до свидания".
Ниже приведен пример кода драйвера с комментариями, поясняющими, что делает каждая строка. Сохраните приведенный ниже код как first_code.c
Модуль ядра можно скомпилировать, написав простой Makefile. Мы разберемся с Makefile более подробно в другой статье. Текущий Makefile скомпилирует фрагмент кода. Поместите файл first_code.c и Makefile в одну папку.
Makefile показан ниже
Запустите команду make, как показано ниже. Вывод команды make также показан ниже.
Скомпилированный модуль ядра показан ниже.
Итак, первый модуль ядра скомпилирован. Теперь нам нужно загрузить модуль ядра в ядро Linux. Следующая команда «insmod» совершит подвиг. Пользователь также может просмотреть команду «modprobe», чтобы загрузить модуль.
Команда «modprobe» просмотрит зависимости, которые в настоящее время имеет текущий модуль, который пытались вставить, загрузит все зависимости, а затем попытается загрузить текущий модуль ядра
Команда «insmod» пытается загрузить только текущий модуль
Когда мы проверяем журнал dmesg, мы обнаруживаем, что введена точка входа модуля ядра
Не беспокойтесь о сообщении об ошибке. В сообщении об ошибке только говорится, что мы попытались принудительно загрузить модуль в ядро linux, который не является частью исходного кода ядра для этого выпуска. Мы видим, что распечатывается сообщение «heloo», указывающее, что был введен код инициализации для модуля и модуль был загружен.
Если мы хотим подтвердить, загружен ли модуль — выполняем команду «lsmod». Команда lsmod выводит список всех загруженных на данный момент модулей в ядре Linux.
«Используется» указывает, использует ли какой-либо другой модуль текущий модуль. В случае модуля first_code ни один другой модуль не использует модуль first_code.
Чтобы удалить модуль из ядра Linux, мы можем использовать команду «rmmod».
Команда dmesg показывает, что процедура де-инициализации была вызвана для модуля first_code в качестве сообщения «до свидания», а команда lsmod больше не показывает загруженный модуль first_code.
Как видно выше — в журнале dmesg отображается «сообщение до свидания», а также модуль first_code, который отображался в начале при вызове lsmod, больше не присутствует.
Поэтому в этой статье мы написали простой модуль ядра, загрузили и выгрузили модуль.
Программирование драйвера устройства для Linux требует глубокого понимания операционной системы и сильных навыков разработки. Чтобы помочь вам освоить эту сложную область, эксперты по разработке драйверов Apriorit создали это руководство.
Мы покажем вам, как написать драйвер устройства для Linux (версия ядра 5.3.0). При этом мы обсудим систему логирования ядра, принципы работы с модулями ядра, символьные устройства, структуру file_operations и доступ к пользовательской памяти из ядра. Вы также получите код простого драйвера для Linux, который можно дополнить любой необходимой вам функциональностью.
Эта статья будет полезна разработчикам, изучающим разработку драйверов для Linux.
Команда разработчиков драйверов
Содержание:
Начало работы с модулем ядра Linux
Ядро Linux написано на языках программирования C и Ассемблер. C реализует основную часть ядра, а Assembler реализует части, зависящие от архитектуры. Вот почему мы можем использовать только эти два языка для разработки драйверов устройств Linux. Мы не можем использовать C++, который используется для ядра Microsoft Windows, потому что некоторые части исходного кода ядра Linux (например, заголовочные файлы) могут включать ключевые слова из C++ (например, delete или new), а в ассемблере мы можем встретить такие лексемы, как как ' : : ' .
Существует два способа программирования драйвера устройства Linux:
- Скомпилируйте драйвер вместе с ядром, которое является монолитным в Linux.
- Реализуйте драйвер как модуль ядра, и в этом случае вам не нужно будет перекомпилировать ядро.
В этом руководстве мы разработаем драйвер в виде модуля ядра. Модуль — это специально разработанный объектный файл. При работе с модулями Linux связывает их с ядром, загружая в адресное пространство ядра.
Код модуля должен работать в контексте ядра. Это требует от разработчика очень внимательного отношения. Если разработчик допустит ошибку при реализации пользовательского приложения, в большинстве случаев это не вызовет проблем за пределами пользовательского приложения. Но ошибки в реализации модуля ядра приведут к проблемам на уровне системы.
К счастью для нас, ядро Linux устойчиво к некритическим ошибкам в коде модуля. Когда ядро сталкивается с такими ошибками (например, разыменование нулевого указателя), оно выводит сообщение oops — индикатор незначительных сбоев в работе Linux. После этого неисправный модуль выгружается, позволяя ядру и другим модулям работать в обычном режиме. Кроме того, вы можете анализировать журналы, которые точно описывают некритические ошибки. Имейте в виду, что продолжение выполнения драйвера после сообщения об ошибке может привести к нестабильности и панике ядра.
Ядро и его модули представляют собой единый программный модуль и используют единое глобальное пространство имен. Чтобы минимизировать пространство имен, вы должны контролировать то, что экспортируется модулем. Экспортируемые глобальные символы должны иметь уникальные имена и быть сведены к минимуму. Обычно используемый обходной путь — просто использовать имя модуля, экспортирующего символы, в качестве префикса для глобального имени персонажа.
Помня об этой базовой информации, давайте начнем писать наш драйвер для Linux.
Создание модуля ядра
Мы начнем с создания простого прототипа модуля ядра, который можно загружать и выгружать. Мы можем сделать это с помощью следующего кода:
Функция my_init является точкой входа для инициализации драйвера и вызывается во время запуска системы (если драйвер статически компилируется в ядро) или когда модуль вставляется в ядро. Функция my_exit — это точка выхода драйвера. Вызывается при выгрузке модуля из ядра Linux. Эта функция не работает, если драйвер статически скомпилирован в ядро.
Эти функции объявлены в заголовочном файле linux/module.h. Функции my_init и my_exit должны иметь одинаковые подписи, например следующие:
Теперь наш простой модуль готов. Давайте научим его логиниться в ядро и взаимодействовать с файлами устройства. Эти операции будут полезны при разработке драйверов ядра Linux.
Регистрация символьного устройства
Файлы устройств обычно хранятся в папке /dev. Они облегчают взаимодействие между пользовательским пространством и кодом ядра. Чтобы ядро получало что-либо, вы можете просто записать это в файл устройства, чтобы передать его модулю, обслуживающему этот файл. Все, что считывается из файла устройства, исходит из обслуживающего его модуля.
Существует две группы файлов устройств:
- Символьные файлы — небуферизованные файлы, которые позволяют считывать и записывать данные посимвольно.В этом руководстве мы сосредоточимся на этом типе файлов.
- Блочные файлы – буферизованные файлы, которые позволяют читать и записывать только целые блоки данных.
В системах Linux есть два способа идентификации файлов устройств:
- Основные номера устройств определяют модули, обслуживающие файлы устройств или группы устройств.
- Второстепенные номера устройств определяют определенные устройства среди группы устройств, определяемой старшим номером устройства.
Мы можем определить эти номера в коде драйвера или выделить их динамически. Если число, определенное как константа, уже использовалось, система вернет ошибку. Когда номер назначается динамически, функция резервирует этот номер, чтобы предотвратить его использование другими файлами устройств.
Чтобы зарегистрировать символьное устройство, нам нужно использовать функцию register_chrdev:
Здесь мы указываем имя и старший номер устройства для его регистрации. После этого устройство и структура file_operations будут связаны. Если мы присвоим 0 основному параметру, функция сама присвоит основной номер устройства. Если возвращаемое значение равно 0, это указывает на успех, а отрицательное число указывает на ошибку. Оба номера устройств указаны в диапазоне от 0 до 255.
Имя устройства — это строковое значение параметра name. Эта строка может передавать имя модуля, если он регистрирует одно устройство. Мы используем эту строку для идентификации устройства в файле /sys/devices. Операции с файлами устройств, такие как чтение, запись и сохранение, обрабатываются указателями функций, хранящимися в структуре file_operations. Эти функции реализуются модулем, и указатель на структуру модуля, идентифицирующую этот модуль, также хранится в структуре file_operations (подробнее об этой структуре в следующем разделе).
Структура файловых операций
В ядре Linux 5.3.0 структура file_operations выглядит следующим образом:
Если эта структура содержит функции, которые не требуются для вашего драйвера, вы все равно можете использовать файл устройства без их реализации. Указатель на нереализованную функцию можно просто установить в 0. После этого система позаботится о реализации функции и заставит ее вести себя нормально. В нашем случае мы просто реализуем функцию чтения.
Поскольку мы собираемся обеспечить работу только одного типа устройств с помощью нашего драйвера для Linux, наша структура file_operations будет глобальной и статической. После того, как он будет создан, нам нужно будет заполнить его статически следующим образом:
Объявление макроса THIS_MODULE содержится в заголовочном файле linux/export.h. Превратим макрос в указатель на модульную структуру нужного модуля. Позже мы напишем тело функции с прототипом, а пока у нас есть только указатель на него device_file_read:
Структура file_operations позволяет нам разработать несколько функций, которые будут регистрировать и отменять регистрацию файла устройства. Чтобы зарегистрировать файл устройства, мы используем следующий код:
device_file_major_number – это глобальная переменная, содержащая основной номер устройства. Когда срок службы драйвера истечет, эта глобальная переменная будет использоваться для отмены регистрации файла устройства.
В приведенном выше коде мы добавили функцию printk, которая регистрирует сообщения ядра. Обратите внимание на префиксы KERN_NOTICE и KERN_WARNING во всех перечисленных строках формата printk. УВЕДОМЛЕНИЕ и ПРЕДУПРЕЖДЕНИЕ указывают уровень приоритета сообщения. Уровни варьируются от незначительных (KERN_DEBUG) до критических (KERN_EMERG), предупреждающих о нестабильности ядра. Это единственное различие между функцией printk и библиотечной функцией printf.
Функция printk
Функция printk формирует строку, которую мы добавляем в циклический буфер. Оттуда демон klog читает его и отправляет в системный журнал. Реализация printk позволяет нам вызывать эту функцию из любой точки ядра. Используйте эту функцию осторожно, так как она может привести к переполнению кольцевого буфера, что означает, что самое старое сообщение не будет зарегистрировано.
Следующим шагом будет написание функции для отмены регистрации файла устройства. Если файл устройства успешно зарегистрирован, значение device_file_major_number не будет равно 0. Это значение позволяет нам отменить регистрацию файла с помощью функции unregister_chrdev, которую мы объявляем в файле linux/fs.h. Старший номер устройства — это первый параметр этой функции, за которым следует строка, содержащая имя устройства. Функции register_chrdev и unresister_chrdev имеют одинаковое содержимое.
Чтобы отменить регистрацию устройства, мы используем следующий код:
Следующим шагом в реализации функций для нашего модуля является выделение и использование памяти в пользовательском режиме. Давайте посмотрим, как это делается.
Использование памяти, выделенной в пользовательском режиме
Давайте посмотрим на параметр filep — указатель на файловую структуру. Эта файловая структура позволяет нам получить необходимую информацию о файле, с которым мы работаем, данные, связанные с этим файлом, и многое другое. Прочитанные данные размещаются в пользовательском пространстве по адресу, указанному вторым параметром — буфер. Количество байтов для чтения определяется в параметре len, и мы начинаем чтение байтов с определенного смещения, определенного в параметре смещения. После выполнения функции должно быть возвращено количество успешно прочитанных байтов. Затем мы должны обновить смещение.
Для работы с информацией из файла устройства пользователь выделяет специальный буфер в адресном пространстве пользовательского режима. Затем функция чтения копирует информацию в этот буфер. Адрес, на который указывает указатель из пользовательского пространства, и адрес в адресном пространстве ядра могут иметь разные значения. Вот почему мы не можем просто разыменовать указатель.
При работе с этими указателями у нас есть набор определенных макросов и функций, которые мы объявляем в файле linux/uaccess.h. Наиболее подходящая функция в нашем случае — это copy_to_user. Его название говорит само за себя: он копирует определенные данные из буфера ядра в буфер, выделенный в пространстве пользователя. Он также проверяет, действителен ли указатель и достаточно ли велик размер буфера. Вот код прототипа copy_to_user:
Прежде всего, эта функция должна получить три параметра:
- Указатель на буфер
- Указатель на источник данных
- Количество копируемых байтов
Если при выполнении есть какие-либо ошибки, функция вернет значение, отличное от 0. В случае успешного выполнения значение будет равно 0. Функция copy_to_user содержит макрос _user, который документирует процесс. Также эта функция позволяет узнать, правильно ли код использует указатели из адресного пространства. Это делается с помощью Sparse, анализатора статического кода. Чтобы убедиться, что он работает правильно, всегда помечайте указатели адресного пространства пользователя как _user.
Вот код для реализации функции чтения:
С этой функцией код для нашего драйвера готов. Теперь пришло время собрать модуль ядра и посмотреть, работает ли он должным образом.
Сборка модуля ядра
В современных версиях ядра makefile делает большую часть сборки за разработчика. Он запускает систему сборки ядра и предоставляет ядру информацию о компонентах, необходимых для сборки модуля.
Для модуля, созданного из одного исходного файла, требуется одна строка в make-файле. После создания этого файла вам нужно только инициировать систему сборки ядра с помощью команды obj-m := имя_исходного_файла.o. Как видите, здесь мы присвоили модулю имя исходного файла — файл *.ko.
При наличии нескольких исходных файлов для сборки ядра требуются только две строки:
Чтобы инициализировать систему сборки ядра и собрать модуль, нам нужно использовать команду make –C KERNEL_MODULE_BUILD_SYSTEM_FOLDER M=`pwd` modules. Чтобы очистить папку сборки, мы используем команду make –C KERNEL_MODULES_BUILD_SYSTEM_FOLDER M=`pwd` clean.
Система сборки модулей обычно находится в /lib/modules/`uname -r`/build. Теперь пришло время подготовить систему сборки модуля. Чтобы собрать наш первый модуль, выполните команду make modules_prepare из папки, где находится система сборки.
Наконец, мы объединим все, чему научились, в один make-файл:
Цель load загружает модуль сборки, а цель unload удаляет его из ядра.
В нашем руководстве мы использовали код из main.c и device_file.c для компиляции драйвера. Полученный драйвер называется simple-module.ko. Давайте посмотрим, как его использовать.
Загрузка и использование модуля
Чтобы загрузить модуль, мы должны выполнить команду make load из папки с исходным файлом. После этого в файл /proc/modules добавляется имя драйвера, а в файл /proc/devices добавляется устройство, которое регистрирует модуль. Добавленные записи выглядят следующим образом:
Первые три записи содержат имя добавленного устройства и основной номер устройства, с которым оно связано. Младший диапазон номеров (0–255) позволяет создавать файлы устройств в виртуальной файловой системе /dev.
Затем нам нужно создать файл специальных символов для нашего старшего номера с помощью команды mknod /dev/simple-driver c 250 0.
После того как мы создали файл устройства, нам нужно выполнить окончательную проверку, чтобы убедиться, что то, что мы сделали, работает должным образом. Для проверки мы можем использовать команду cat для отображения содержимого файла устройства:
Если мы видим содержимое нашего драйвера, значит он работает корректно!
Заключение
В этом руководстве мы показали, как написать простой драйвер для Linux. Вы можете найти полный исходный код этого драйвера в репозитории Apriorit GitHub.Если вам нужен более сложный драйвер устройства, вы можете взять это руководство за основу и добавить к нему дополнительные функции и контекст.
В Apriorit мы специализируемся на разработке ядра и драйверов Linux. Наши разработчики успешно поставили сотни сложных драйверов для Linux, Unix, macOS и Windows. Свяжитесь с нашей опытной командой, чтобы начать работу над вашим следующим проектом по разработке драйверов для Linux!
Описание
Узнайте, как писать высококачественный код модуля ядра, решайте распространенные проблемы программирования ядра Linux и разбирайтесь в основах внутреннего устройства ядра Linux
Основные особенности
- Узнайте, как писать код ядра с помощью платформы загружаемого модуля ядра.
- Изучите отраслевые методы эффективного выделения памяти и синхронизации данных в ядре.
- Понимать основы ключевых тем внутреннего устройства, таких как архитектура ядра, управление памятью, планирование ЦП и синхронизация ядра.
Описание книги
Программирование ядра Linux – это всеобъемлющее введение для новичков в разработке ядра и модулей Linux. Это простое в использовании руководство поможет вам в кратчайшие сроки приступить к написанию кода ядра. В этой книге используется новейшее ядро Linux версии 5.4 с долгосрочной поддержкой (LTS), которое будет поддерживаться с ноября 2019 года по декабрь 2025 года. действует в течение многих лет.
Эта книга по Linux начинается с демонстрации того, как собрать ядро из исходного кода. Далее вы узнаете, как написать свой первый модуль ядра, используя мощную структуру Loadable Kernel Module (LKM). Затем в книге рассматриваются ключевые темы внутреннего устройства ядра, включая архитектуру ядра Linux, управление памятью и планирование ЦП. Далее вы углубитесь в довольно сложную тему параллелизма в ядре, поймете проблемы, которые он может вызвать, и узнаете, как их можно решить с помощью различных технологий блокировки (мьютексы, спин-блокировки, атомарные операторы и операторы подсчета ссылок). Вам также будут полезны более продвинутые материалы по эффектам кеша, введение в техники без блокировок в ядре, предотвращение взаимоблокировок (с помощью lockdep) и методы отладки блокировок ядра.
К концу этой книги по ядру вы получите подробное представление об основах написания кода модуля ядра Linux для реальных проектов и продуктов.
Что вы узнаете
- Написать высококачественный модульный код ядра (инфраструктура LKM) для ядер 5.x
- Настроить и собрать ядро из исходного кода
- Изучите архитектуру ядра Linux
- Познакомьтесь с ключевыми внутренними аспектами управления памятью в ядре.
- Понимать и работать с различными API выделения/деаллока динамической памяти ядра
- Откройте для себя ключевые внутренние аспекты планирования ЦП в ядре.
- Получите понимание проблем параллелизма ядра
- Узнайте, как работать с ключевыми примитивами синхронизации ядра
Для кого эта книга
Эта книга предназначена для Linux-программистов, начинающих свой путь в разработке ядра Linux. Эта книга будет полезна разработчикам ядра и драйверов Linux, стремящимся решить частые и распространенные проблемы разработки ядра, а также понять его внутреннюю структуру. Требуется базовое понимание Linux CLI и программирования на C.
Читайте также: