Глава 4. Написание вашего первого модуля ядра - LKM, Часть I

Содержание

Глава 4. Написание вашего первого модуля ядра - LKM, Часть I
Технические требования
Осваиваем архитектуру ядра - часть 1
Пространство пользователя и пространство ядра
API библиотек и системных вызовов
Составляющие пространства ядра
Пользуемся LKM
Инфраструктура LKM
Модули ядра внутри дерева исходного кода ядра
Написание нашего самого первого модуля ядра
Введение в наш код C LKM Hello, world
Разбиваем его на части
Заголовки ядра
Макросы модуля
Точки входа и выхода
Возвращаемые значения
Соглашение возврата 0/-E
Макросы ERR_PTR и PTR_ERR
Ключевые слова __init и __exit
Распространённые операции в модулях ядра
Сборка модуля ядра
Исполнение модуля ядра
Быстрый первый взгляд на printk() ядра
Перечисление всех живых модулей ядра
Выгрузка определённого модуля из памяти ядра
Наш удобный сценарий lkm
Разбираемся с ведением журнала ядра и printk
Применение закольцованного буфера в памяти ядра
Ведение журнала ядра и journalctl systemd
Применение уровней регистрации printk
Удобные макросы pr_
Запись на консоль
Запись вывода в консоль Raspberry Pi
Включение сообщений ядра pr_debug()
Ограничение по скорости экземпляров printk
Генерация сообщений ядра из пространства пользователя
Стандартизация вывода printk через макро pr_fmt
Переносимость и формат спецификаторов printk
Разбираемся с основами модуля ядра Makefile
Выводы
Вопросы
Дальнейшее чтение

Добро пожаловать в ваше путешествие изучения фундаментальных вопросов разработки ядра Linux - инфраструктуру LKM (Loadable Kernel Module, загружаемого модуля ядра) - и то, как пользоваться пользовтелем модуля и аdтором модуля, которыми обычно являются программисты драйвера устройства. Эта тема достаточно обширна и следовательно разделена на две главы - эту и следующую.

В данной главе мы начнём выполним быстрый обзор основ архитектуры ядра Linux, что поможет нам разобраться с самой инфраструктурой. Затем мы рассмотрим чем полезны модули ядра и напишем собственный пример LKM Hello, world, соберём и исполним его. Мы рассмотрим как в журнал регистраций ядра записываются сообщения и разберёмся с тем как применять Makefile LKM. К концу этой главы вы изучите основы архитектуры ядра Linux и собственно архитектуру LKM, применяя их для написания простого, но законченного фрагмента кода ядра.

В этой главе мы рассмотрим следующие рецепты:

  • Понимание основ архитектуры ядра - часть I

  • Изученеи LKM

  • Написание нашего самого первого модуля ядра

  • Общие операции в модулях ядра

  • Основы журнала регистраций ядра и printk

  • Понимание основ Makefile модля ядра

Технические требования

Если вы тщательно следовали Главе 1, Настройка рабочего пространства ядра, основные технические предварительные требования, которым вы следовали, уже ыполнены. (Эта глава к тому же упоминает различные инструменты и продукты с открытым исходным кодом; я определённо рекомендую вам просмотреть её ещё разок.) Для вашего удобства мы суммируем здесь некоторые ключевые моменты.

Для борки и применения некого модуля ядра, требуемый вами дистрибутив Linux (или настроенная система), как минимум, нуждается в установке следующих двух компонентов:

  • Цепочки инструментов: Она содержит компиллятор, ассемблер, компоновщик/ загрузчик, библиотеку C и разные прочие элементы и фрагменты. Когда сборка выполняется для вашей локальной системы, как мы это предполагаем сейчас, тогда всякий современный дистрибутив будет обладать предустановленным естественным набором цепочки инструментов. Если же это не так, просто установите пакет gcc для своего дистрибутива, чего должно вполне хватить: в Ubuntu или системе Linux на основе Debian воспользуйтесь следующим:

    
    $ sudo apt install gcc
    		
  • Заголовки ядра:Эти заголовки будут применяться в процессе компиляции. В действительности вы устанавливаете в системе некий пакет в сцепке не только с устанавливаемыми заголовками ядра, но также и с прочими необходимыми частями и фрагментами (например, Makefile ядра). И снова, современный дистрибутив Linux будет/ должен обладать предварительно установленными необходимыми заголовками ядра. Если это не так (вы можете проверит это воспользовавшись dpkg(1), как это показано здесь), просто установите необходимый пакет для своего дистрибутива; в Ubuntu или системе Linux на основе Debian примените это:

    
    $ sudo apt install linux-headers-generic
    $ dpkg -l | grep linux-headers | awk '{print $1, $2}'
    ii linux-headers-5.3.0-28
    ii linux-headers-5.3.0-28-generic
    ii linux-headers-5.3.0-40
    ii linux-headers-5.3.0-40-generic
    ii linux-headers-generic-hwe-18.04
    $
    		

    Здесь, своей второй командой мы применяем утилиту dpkg(1) просто чтобы убедиться что пакеты linux-headers уже установлены.

    [Совет]Совет

    В некоторых дистрибутивах этот пакет может носить название kernel-headers-<ver#>. Кроме того, для разработки непосредственно в Raspberry Pi установите соответствующий пакет заголовков ядра с названием raspberrypi-kernel-headers.

Полное дерево исходного кода для этой книги доступно в репозитории GitHub, а код для этой главы расположен в каталоге ch4. Мы определённо ожидаем от вас клонирование следующего:


git clone https://github.com/PacktPublishing/Linux-Kernel-Programming.git
		

Весь код для данной главы расположен в каталоге тёзке chn (где n это номер главы: в данном случае, это ch4/).

Осваиваем архитектуру ядра - часть 1

В данном разделе мы приступим к погружению в своё понимание ядра. Более конкретно, здесь мы окунаемся в то что представляют собой пространство ядра и пользователя и те основные подсистемы и различные компоненты, которые и составляют само ядро Linux. Эти сведения имеют дело с более высоким уровнем абстракции на данный момент и умышленно оставляются краткими. В материю самого ядра мы окунёмся глубже в Главе 6, Существенные моменты внутреннего устройства ядра - Процессы и Потоки.

Пространство пользователя и пространство ядра

Современные микропроцессоры, как минимум, поддерживают два привилегированных уровня. В качестве примеров реального мира, семейство x86[-64] Intel/ AMD поддерживает четыре привилегированных уровня (они носят название уровней кольца), а семейство микропроцессоров ARM (32- битных) поддерживает до семи (ARM именует их режимами исполнения; шесть являются привилегированными и один не привилегированный).

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

  • Пространство пользователя: Для приложений, исполняемых в непривилегированном режиме пользователя

  • Пространтсво ядра: Для ядра и всех его составляющих, выполняющихся в привилегированном режиме - режиме ядра

Следующий рисунок показывает такую базовую архитектуру:

 

Рисунок 4-1


Основа архитектуры - два привилегированных узла

Далее следуют некоторые подробности об архитектуре системы Linux; продолжайте чтение.

API библиотек и системных вызовов

Пространство пользователя для выполнения своей работы зачастую полагается на API (Application Programming Interfaces, Интерфейсы прикладных программ). Библиотека, по существу, некий набор или архив API, позволяющий вам использовать стандартизованные, хорошо написанные, протестированные надлежащим образом интерфейсы (и применять обычные преимущества: не изобретать велосипед, переносимость, стандартизацию и тому подобное). Системы Linux имеют несколько библиотек; в системах корпоративного класса не редко даже сотни. Причём, как вы увидите из них все приложения (исполняемые) пользовательского режима Linux "автоматически связываются" с одной важно и всегда применяемой библиотекой: glibc - GNU standard C library. Однако библиотеки доступны лишь в пользовательском режиме; само ядро не обладает библиотеками (дополнительно об этом в нашей следующей главе).

Примерами библиотечных API выступают хорошо знакомые printf(3) (возвращаем вас к Главе 1, Настройка рабочего пространства ядра, тому разделу страниц руководства, в котором можно обнаружить этот API), scanf(3), strcmp(3), malloc(3) и free(3).

Теперь некий ключевой момент: когда пользователь и ядро являются разными адресными пространствами и пребывают в разных уровнях привилегий, как некий пользовательский процесс способен выполнить доступ к своему ядру? Короткий вариант ответа через системные вызовы. Некий системный вызов (system call) это особенный API, в том плане, что именно он выступает тем единственным допустимым (синхронным) способом для процессов пространства пользователя выполнять доступ к своему ядру. Иными словами, системные вызовы выступают той самой единственной точкой входа для своего пространства ядра. Они обладают возможностью переключения из непривилегированного режима пользователя в привилегированный режим ядра (дополнительно об этом и монолитной архитектуре в Главе 6, Существенные моменты внутреннего устройства ядра - Процессы и Потоки, в разделе Разбираемся с контекстами процессов и прерываний. Примерами системных вызовов могут служить fork(2), execve(2), open(2), read(2), write(2), socket(2), accept(2), chmod(2)) и тому подобные.

[Совет]Совет

Взгляните на все API библиотек и системных вызовов в их страницах руководства в Интернете:

- API библиотек, раздел 3 man

- API системных вызовов, раздел 2 man

Тут мы подчёркиваем, что в действительности пользовательские приложения и их ядро взаимодействуют исключительно через системные вызовы; именно они выступают таким интерфейсом. В данной книге мы не будем далее погружаться в эти подробности. Если вы дополнительно интересуетесь этими вопросами, будьте добры обратиться к книге Packt Hands-On System Programming with Linux, в особенности к Chapter 1, Linux System Architecture.

Составляющие пространства ядра

Естественно, эта книга сосредотачивается на пространстве самого ядра. На сегодняшний день ядро Linux достаточно большая и сложная бестия. Изнутри оно состоит из нескольких главных подсистем и различных компонентов. Широкий перечень подсистем и компонентов выдаёт следующий список:

  • Сердцевина ядра: Этот код обрабатывает типичную центральную деятельность всякой современной операционной системы, включая процессы (пользователя и ядра) а также создание/ уничтожение потоков, планирование ЦПУ, примитивы синхронизации, работу с сигналами, таймеры, обработку прерываний, пространства имён, cgroup, поддержку модулей, криптографию и многое иное.

  • Управление памятью (MM, Memory Management): Обрабатывает всю связанную с памятью работу, включая установку и сопровождение VAS (Virtual Address Spaces, Виртуальные адресные пространства) ядра и процесса.

  • VFS (для поддержки файловой системы): VFS (Virtual Filesystem Switch) это некий абстрактный уровень над реализациями реальных файловых систем внутри самого ядра Linux (таких как ext[2|4], vfat, reiserfs, ntfs, msdos, iso9660, JFFS2 и UFS).

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

  • Стек сетевого протокола: Linux знаменит своей точной, прописанной до буквы RFC, высококачественной реализацией распространённых (и не очень) сетевых протоколов на всех уровнях своей модели, причём, скорее всего, наиболее известным является TCP/IP.

  • Поддержка IPC (Inter-Process Communication) (Межпроцесного взаимодействия): Здесь осуществляется реализация механизмов IPC; Linux подерживает очереди сообшений, разделяемую память, семафоры (как более старые SystemV, так и более новые POSIX), а также прочие механизмы IPC.

  • Поддержку звука: Здесь располагаются все реализации кода аудио, начиная с встраиваемого программного обеспечения, вплоть до драйверов и кодеков.

  • Поддержка виртуализации: Linux превратился в крайне популярный как среди крупных, так и среди мелких поставщиков облачных решений и одной из основных причин выступает его высококачественный механизм виртуализации с малым размером в памяти, KVM (Kernel-based Virtual Machine).

Всё это составляет необходимые главные подсистемы ядра; кроме того, мы ещё имеем следующее:

  • Специфичный для архитектуры (arch) код, что означает особенный для ЦПУ код

  • Инициализацию ядра

  • Инфраструктуры безопасности

  • Большое число типов драйверов устройств

[Совет]Совет

Вернитесь к Сборка ядра Linux 5.4 из исходного кода - Часть I, разделу Краткий тур по дереву исходного кода ядра, который предоставляет схему дерева исходного кода ядра, относящегося ко всем основным подсистемам и прочим компонентам.

Хорошо известно, что ядро Linux следует архитектуре монолитного ядра. По сути, монолитная архитектура это когда все компоненты ядра (которые мы уже упоминали в этом разделе) обитают в общем адресном пространстве ядра (или сегменте ядра) и совместно используют его. Это отчётливо видно из следующей схемы:

 

Рисунок 4-2


Пространство ядра - главные подсистемы и блоки

Другой факт, о котором вам надлежит знать это то, что эти адресные пространства, естественно виртуальные адресные пространства, а не физические. Само ядро будет (при помощи такого оборудования как MMU/ TLB/ кэши) устанавливать соответствие, причём на уровни грануляции страниц, виртуальных страниц кадрам физических страниц. Оно осуществляет это с применением таблицы страниц ядра хозяина (master) для соответствия виртуальных страниц ядра физическим кадрам и, для каждого отдельного действующего процесса, оно устанавливает соответствие значениям виртуальных страниц процесса кадрам физических страниц через индивидуальные для каждого процесса таблицы.

[Замечание]Замечание

Более глубокое рассмотрение всего самого необходимого для архитектуры управления памятью и внутреннего устройства ожидают вас в Главе 6, Существенные моменты внутреннего устройства ядра - Процессы и Потоки (и идущих следом дополнительных главах).

Теперь, когда мы обладаем базовым пониманием пространств пользователя и ядра, давайте двинемся дальше и начнём своё путешествие в инфраструктуру LKM.

Пользуемся LKM

Проще говоря, модуль ядра это средство предоставления функциональности уровня ядра не прибегая к работе внутри дерева исходного кода ядра.

Представьте себе ситуацию, при которой вам требуется добавить поддержку функциональной возможности в само ядро Linux - возможно некий новый драйвер устройства для использования определённой микросхемы периферийного оборудования, некой новой файловой системы или какого- то нового планировщика ввода/ вывода. Один из способов выполнения этого достаточно очевиден: обновить имеющееся дерево исходного кода ядра этим новым кодом, собрать его и проверить это.

Хотя это может показаться простым, в действительности это большой объём работы - всякое изменение в том коде, который мы написали, каким бы незначительным оно не было, потребует повторной сборки всего образа ядра и последующей перезагрузки вашей системы для его проверки. Должен иметься более ясный чистый и простой способ; и он действительно имеется - инфраструктура LKM!

Инфраструктура LKM

Инфраструктура LKM это средство компиляции некого фрагмента кода ядра за пределами самого дерева исходного ода ядра, часто называемое кодом "вне- дерева", оставляющее его независимым от самого ядра в неком ограниченном смысле и затем вставляемого или подключаемого в память ядра, заставляя его запуститься и выполнить своё задание, с последующим его удалением (или отключением) из памяти ядра.

Сам исходный код модуля ядра обычно состоит из одного или более исходный файлов C, файлов заголовков и некого Makefile, затем собирается (естественно, посредством make(1)) в некий модуль ядра. Такой модуль ядра сам по себе это всего лишь некий файл двоичного объекта, а не исполняемый двоичный файл. В Linux 2.4 и более ранних версиях, название файла такого модуля ядра обладало суффиксом .o; в современном Linux 2.6 и старше, вместо этого он обладает суффиксом .ko (kernel object). После его построения вы можете вставить этот .ko - модуль ядра - в само действующее ядро времени исполнения, эффективно превращая его в часть своего ядра.

[Замечание]Замечание

Обратите внимание на то, что через инфраструктуру LKM могут предоставляться не все функциональные возможности ядра. Некоторая центральная функциональность, например, такая как код планировщика ЦПУ, управление памятью сигналами, таймер, пути кода управления прерываниями и тому подобное, могут разрабатываться исключительно внутри самого ядра. Аналогично, модулю ядра допускается выполнять доступ лишь к некому подмножеству всего API полного ядра; дополнительно об этом чуть позднее.

Вы можете задать вопрос: как я выполняю вставку некого объекта в своё ядро? Давайте оставим это простым - ответ такой: через insmod(8). На данный момент давайте опустим подробности (они будут пояснены в последующем разделе Исполнение модуля ядра). Приводимая ниже схема предоставляет некий обзор первой сборки и последующей вставки какого- то модуля ядра в память ядра:

 

Рисунок 4-3


Сборка и последующая вставка модуля ядра в память ядра

[Совет]Совет

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

Наш модуль ядра загружается в память ядра и обитает в ней, то есть, под него выделяется самим ядром именно VAS ядра (нижняя половина Рисунка 4.3) в области пространства. Не ошибитесь, это код ядра и он исполняется с полномочиями ядра. Таким образом, вам, как разработчику ядра (или драйвера), не приходится всякий раз повторно настраивать и собирать, а также перезапускать свою систему. Всё что вам приходится делать, это изменять свой код соответствующего модуля ядра, повторно собрать его, удалить старую копию из памяти (если она там имеется) и вставить свою новую версию. Это сберегает время и повышает производительность.

Одной из причин преимуществ модулей ядра заключается в том, что они сами по себе поддаются динамической конфигурации продукта. Например, модули ядра могут быть разработаны для предоставления различных функциональных возможностей по разной цене; некий сценарий, вырабатывающий окончательный образ для встраиваемого продукта может установить заданный набор модулей ядра в зависимости от той цены, которую готов заплатить потребитель. Вот ещё один пример того, как данная технология применяется в отладке или сценарии устранения неисправностей: модуль ядра может применяться для динамической выработки диагностики и регистрационных записей отладки в неком уже имеющемся продукте. Такие технологии как kprobes и подобные её именно это и позволяют.

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

Модули ядра внутри дерева исходного кода ядра

На самом деле, объект модуля ядра не является чем- то совсем не знакомым для нас. В Главе 3, Сборка ядра Linux 5.4 из исходного кода - Часть II мы собирали модули ядра как часть процесса сборки своего ядра и устанавливали их.

Вспомните, что эти модули ядра являются частью исходного кода ядра и настраивались как модули через выбор M в приглашении на ввод настроек меню ядра с тремя состояниями. Они устанавливались в каталоги под /lib/modules/$(uname -r)/. Поэтому, чтобы увидеть слегка больше относящегося к установленным модулям ядра под нашим исполняемым в настоящее время гостевым ядром Ubuntu 18.04.3 LTS, мы можем сделать следующее:


$ lsb_release -a 2>/dev/null |grep Description
Description:    Ubuntu 18.04.3 LTS
$ uname -r
5.0.0-36-generic
$ find /lib/modules/$(uname -r)/ -name "*.ko" | wc -l
5359 
		

Ладно, парни из Canonical и ещё откуда- то там плотно подошли к делу. Более пяти тысяч модулей ядра... Только задумайтесь об этом: дистрибуторы не могут знать на будущее в точности какое именно периферийное оборудование некий пользователь в конце концов применяет (в особенности в обычных компьютерах, таких как системы на основе x86). Модули ядра служат удобным средством для поддержки гигантских количеств оборудования без безумного раздувания самого файла образа ядра (к примеру, bzImage или zImage).

Все установленные модули ядра для нашей системы Ubuntu Linux обитают внутри каталога /lib/modules/$(uname -r)/kernel, что можно обнаружить тут:


$ ls /lib/modules/5.0.0-36-generic/kernel/
arch/  block/  crypto/  drivers/  fs/  kernel/  lib/  mm/  net/  samples/  sound/  spl/  ubuntu/  virt/  zfs/
$ ls /lib/modules/5.4.0-llkd01/kernel/
arch/  crypto/  drivers/  fs/  net/  sound/
$
		

Здесь, глядя на верхний уровень каталога kernel/ под /lib/modules/$(uname -r) для своего ядра дистро (Ubuntu 18.04.3 LTS исполняет ядро 5.0.0-36-generic), мы видим, что имеется большое число вложенных каталогов и в буквальном смысле в них упаковано несколько тысяч модулей ядра. напротив, для собранного нами ядра (за подробностями обратитесь к Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I и Главе 3, Сборка ядра Linux 5.4 из исходного кода - Часть II) их имеется гораздо меньше. Из нашего обсуждения в Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I, вы вспомните, что преднамеренно воспользовались целью localmodconfig чтобы сохранить свою сборку небольшой и быстрой. Таким образом, в нашем индивидуальном ядре 5.4.0 имеется всего лишь 60 с небольшим собранных для него модулей.

Одна из отраслей, в которой наблюдается довольно интенсивное применение модулей ядра это драйверы устройств. В качестве образца, давайте взглянем на драйвер сетевого устройства, который спроектирован в виде модуля ядра. Вы можете отыскать какое- то их число (также и совместно со знаменитыми иенами производителей!) в папке своего дистро ядра kernel/drivers/net/ethernet:

 

Рисунок 4-4


Пространство ядра - главные подсистемы и блоки

Во многих ноутбуках на основе Intel распространённым является адаптер ethernet NIC (Network Interface Card, платы сетевого интерфейса) Intel 1GbE. Управляющий ею драйвер сетевого устройства носит название драйвера e1000. Наш гость x86-64 Ubuntu 18.04.3 (запущенный в ноутбуке хоста x86-64) показывает что он и в самом деле применяет этот драйвер:


$ lsmod | grep e1000
e1000                 139264  0
		

В скором времени мы более детально изучим утилиту lsmod(8) ('list modules'). Что нам более важно, мы можем обнаружить, что это модуль ядр! Как относительно того чтобы получить некие дополнительные сведения по этому конкретному модулю ядра? Это достаточно просто осуществляется при помощи утилиты modinfo(8) (для лучшей читаемости мы усекли здесь её подробный вывод):


$ ls -l /lib/modules/5.0.0-36-generic/kernel/drivers/net/ethernet/intel/e1000
total 220
-rw-r--r-- 1 root root 221729 Nov 12 16:16 e1000.ko
$ modinfo /lib/modules/5.0.0-36-generic/kernel/drivers/net/ethernet/intel/e1000/e1000.ko
filename:       /lib/modules/5.0.0-36-generic/kernel/drivers/net/ethernet/intel/e1000/e1000.ko
version:        7.3.21-k8-NAPI
license:        GPL v2
description:    Intel(R) PRO/1000 Network Driver
author:         Intel Corporation, 
srcversion:     C521B82214E3F5A010A9383
alias:          pci:v00008086d00002E6Esv*sd*bc*sc*i*
[...]
name:           e1000
vermagic:       5.0.0-36-generic SMP mod_unload 
[...]
parm:           copybreak:Maximum size of packet that is copied to a new 
                buffer on receive (uint)
parm:           debug:Debug level (0=none,...,16=all) (int)
		

Эта утилита modinfo(8) позволяет нам прицепиться к двоичному образу модуля ядра и выделить некоторые относящиеся к нему подробности; дополнительные сведения относительно применения modinfo в нашем следующем разделе.

[Совет]Совет

Другим способом получения полезных сведений в вашей системе, включая сведения относительно загруженных в настоящее время модулей ядра является работа через утилиту systool(1). Для некого установленного модуля ядра (подробнее об установке модуля ядра далее в нашей следующей главе в разделе Автоматически загружаемые модули при запуске системы) выполнение systool -m <module-name> -v раскроет сведения о нём. Относительно подробностей применения systool(1) загляните в его страницу руководства.

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

Итак, раз вы желаете изучить как писать драйвер устройства Linux, файловую систему или некий межсетевой экран, вам надлежит изучить ка писать некий модуль ядра, тем самым применяя мощную инфраструктуру LKM ядра. Это именно то чем мы и займёмся далее.

Написание нашего самого первого модуля ядра

При представлении нового языка или темы программирования стало широко распространённой традицией вычислительного программирования имитировать оригинальную программу K&R Hello, world в качестве самого первого фрагмента кода. Я счастлив следовать этой почитаемой традиции и представить мощную инфраструктуру LKM. В этом разделе вы изучите основные этапы кодирования образца LKM. Мы подробно поясним этот код.

Введение в наш код C LKM Hello, world

Без всякой дополнительной суеты вот некий простой код C Hello, world, реализуемый по канонам инфраструктуры LKM ядра Linux:

[Замечание]Замечание

По причинам удобства чтения и стеснённости пространства здесь отображается лишь части нашего исходного кода. Чтобы просмотреть исходный код целиком, собрать его и исполнить, весь исходный код для этой книги доступен в её репозитории GiytHub. Мы определённо ожидаем что вы клонируете его: git clone https://github.com/PacktPublishing/Linux-Kernel-Programming.git.


// ch4/helloworld_lkm/hellowworld_lkm.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

MODULE_AUTHOR("<insert your name here>");
MODULE_DESCRIPTION("LLKD book:ch4/helloworld_lkm: hello, world, our first LKM");
MODULE_LICENSE("Dual MIT/GPL");
MODULE_VERSION("0.1");

static int __init helloworld_lkm_init(void)
{
    printk(KERN_INFO "Hello, world\n");
    return 0;     /* success */
}

static void __exit helloworld_lkm_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
}

module_init(helloworld_lkm_init);
module_exit(helloworld_lkm_exit);
 	   

Вы можете испробовать этот образец модуля ядра Hello, world прямо сейчас! Просто выполните cd в правильный каталог исходного кода, как показано тут и воспользуйтесь нашим вспомогательным сценарием lkm для его сборки и запуска:


$ cd <...>/ch4/helloworld_lkm
$ ../../lkm helloworld_lkm
Version info:
Distro:     Ubuntu 18.04.3 LTS
Kernel: 5.0.0-36-generic
[...]
dmesg
[ 5399.230367] Hello, world
$
		

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

Разбиваем его на части

Последующие подразделы достаточно хорошо поясняют каждую строку нашего предыдущего кода C Hello, world. Помните, что несмотря на то, что данная программа кажется очень маленькой и тривиальной, имеется много чего относящегося к ней и вокруг инфраструктуры LKM для понимания. Вся оставшаяся часть этой главы сосредоточена на этом и вдаётся в мельчайшие подробности. Я настоятельно рекомендую вам потратить своё время на то чтобы вначале прочесть и усвоить все эти основополагающие моменты. Это безмерно поможет вам в дальнейшем, возможно, в ситуациях сложных для отладки.

Заголовки ядра

Для нескольких файлов заголовков мы пользуемся #include. В отличии от разработки приложений 'C' в пространстве пользователя, это заголовки ядра (как мы уже упоминали в своём разделе Технические требования). Из Главы 3, Сборка ядра Linux 5.4 из исходного кода - Часть II вспомните, что модули ядра устанавливались в особенной ветви, доступной на запись root. Давайте проверим её снова (здесь мы выполняем работу из своей гостевой ВМ Ubuntu x86_64 с ядром дистро 5.0.0-36-generic):


$ ls -l /lib/modules/$(uname -r)/
total 5552
lrwxrwxrwx  1 root root      39 Nov 12 16:16 build -> /usr/src/linux-headers-5.0.0-36-generic/
drwxr-xr-x  2 root root    4096 Nov 28 08:49 initrd/
[...] 
		

Обратите внимание на символьную или программную ссылку с названием build. Она указывает на место необходимых заголовков ядра в нашей системе. В нашем предыдущем коде мы пребывали под /usr/src/linux-headers-5.0.0-36-generic/! Как вы увидите, мы предоставим эти сведения в Makefile, применяемый для сборки своего модуля ядра. (Кроме того, некоторые системы обладают аналогичной программной ссылкой с названием source.)

[Замечание]Замечание

Пакет kernel-headers или linux-headers распаковывает некое ограниченное дерево исходного кода ядра, обычно в /usr/src/.... Этот код, тем не менее, не полон вследствии применения нами фразы limited дерева исходного кода. Это обусловлено тем, что полное дерево исходного кода не требуется для целей сборки модулей - то что выделяется из пакетов, так это только необходимые модулям компоненты (сами заголовки, соответствующие Makefiles и тому подобное).

Самой первой строкой в нашем модуле ядра Hello, world является #include <linux/init.h>.

Наш компилятор выполняет её разрешение отыскивая упомянутый ранее файл заголовка ядра в /lib/modules/$(uname -r)/build/include/. Таким образом, посредством следующей программной ссылки build мы можем обнаружить, что он подхватывает этот файл заголовка:


$ ls -l /usr/src/linux-headers-5.0.0-36-generic/include/linux/init.h
-rw-r--r-- 1 root root 9704 Mar  4  2019 /usr/src/linux-headers-5.0.0-36-generic/include/linux/init.h
		

Тому же самому следуют и прочие включённые в исходный код модуля ядра заголовки ядра.

Макросы модуля

Далее у нас имеется несколько модулей макросов из MODULE_FOO(); большинство из них достаточно понятны интуитивно:

  • MODULE_AUTHOR(): Определяет значение автора этого модуля ядра

  • MODULE_DESCRIPTION(): Кратко описывает назначение функции данного LKM

  • MODULE_LICENSE(): Предписывает значение лицензии (лицензий), под которыми выпускается данный модуль ядра

  • MODULE_VERSION(): Определяет значение (локальной) версии этого модуля ядра

В отсутствии самого исходного кода, как эти сведения будут предоставлены конечному пользователю (или потребителю)? Ах, да, наша утилита modinfo(8) выполняет именно это! Эти макросы и их сведения могут показаться тривиальными, однако они важны в проектах и продуктах. На эти сведения, к примеру, полагается производитель, устанавливающий лицензии (открытого исходного кода), под которыми исполняется данный код, применяя вывод grep для modinfo во всех установленных модулях ядра.

Точки входа и выхода

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


module_init(helloworld_lkm_init);
module_exit(helloworld_lkm_exit);
 	   

Такой код module_[init|exit]() это макрос, определяющий, соответственно, необходимые точки входа и выхода. Значением параметра для каждого выступает некая ссылка на функцию. В современных компиляторах C мы можем просто определять значение названия соответствующей функции. Таким образом, в нашем коде применимо следующее:

  • Функция helloworld_lkm_init(): это точка входа.

  • Функция helloworld_lkm_exit(): это точка выхода.

Вы можете просто представлять себе эти точки входа и выхода просто как пару конструктор/ деструктор для некого модуля ядра. Технически говоря, это, естественно, не наш случай, поскольку это не код объектно- ориентированного C++, это простой C. Тем не менее, это полезная аналогия.

Возвращаемые значения

Обратите внимание на следующие внешние признаки функций init и exit:


static int  __init <modulename>_init(void);
static void __exit <modulename>_exit(void);
 	   

В качестве достойной практики кодирования мы пользуемся форматом именования функций в виде <modulename>_[init|exit](), где <modulename> заменяется значением названия этого модуля ядра. Вы осознаете, что такое соглашение именования это всего лишь соглашение и, с технической точки зрения оно не обязательно, однако оно интуитивно понятно, а потому полезно. Очевидно, что ни одна из подпрограмм не получает никаких параметров.

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

Теперь давайте перейдём к важному соглашению, коему следуют для возвращаемого значения функции init модуля ядра.

Соглашение возврата 0/-E

Функция init соответствующего модуля ядра служит для возврата некого значения с типом int; это ключевой момент. Само ядро Linux вовлечено в стиль или соглашение, если хотите, в отношении возвращаемых из него значений (имеется в виду, из самого пространства ядра в соответствующее пространство пользователя процесса). Инфраструктура LKM следует тому, что в просторечии носит название соглашения 0/-E:

  • В случае успеха возвращается целое значение 0.

  • В случае отказа возвращается отрицательная величина того значения, которое вы бы желали установить для глобального не инициализированного целого errno пространства пользователя.

    [Замечание]Замечание

    Имейте в виду, что errno это глобальная переменная, располагающаяся в VAS пользовательского процесса внутри неинициализированного сегмента данных. За очень редкими исключениями, при всяком отказе системного вызова Linux возвращается -1, а для errno устанавливается некое положительное значение, соответствующее величине кода ошибки; эта работа выполняется кодом glibc, который "склеивает" код в пути возврата syscall..

    Более того, величина значения errno это в реальности некий индекс в глобальной таблице сообшений об ошибках на английском (const char * const sys_errlist[]); именно таким образом такие подпрограммы как perror(3) и strerror[_r](3) и им подобные способны выводить на печать диагностику отказов.

    Между прочим, вы можете взглянуть на поный перечень кодов ошибок, доступный изнутри таких файлов заголовка (дерева исходного кодя ядра): include/uapi/asm-generic/errno-base.h и include/uapi/asm-generic/errno.h.

Быстрый пример того как выполнять возврат из функции init модуля ядра поможет прояснить это: допустим, функция init нашего модуля ядра пытается динамически выделить некую память ядра (подробности этого API kmalloc() и всё прочее, естественно, будут рассмотрены в последующих главах; игнорируйте, пожалуйста, их пока). Далее мы можем кодировать это следующим образом:


[...]
ptr = kmalloc(87, GFP_KERNEL);
if (!ptr) {
    pr_warning("%s:%s:%d: kmalloc failed!\n", __FILE__, __func__, __LINE__);
    return -ENOMEM;
}
[...]
return 0;   /* success */
		

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

  1. Во- первых, мы выделяем некое предостережение printk. В действительности, в этом конкретном случае "out of memory" - это педантично и не обязательно. Само ядро несомненно выделит достаточно диагностических сведений если уж выделение памяти пространства ядра завершено отказом! Для дополнительных подробностей обратитесь по следующей ссылке; мы делаем это просто потому, что это самое начало обсуждения и для преемственности читателя.

  2. Возвращаем величину значения -ENOMEM:

    • Тот уровень, на который в действительности будет возвращено это значение в пространстве пользователя в действительности это glibc; он обладает неким "склеивающим" кодом, который умножает это значение на -1 и устанавливает для него глобальное целое errno.

    • Теперь, системный вызов [f]init_module(2) возвратит -1, что указавает на отказ (это происходит по причине того, что insmod(8) в действительности осуществляет этот системный вызов, как мы это вскоре обнаружим).

    • errno будет установлено в ENOMEM, отражая тот факт, что получен отказ вставки модуля ядра по причине отказа в выделении памяти.

И наоборот, наша инфраструктура ожидает что соответствующая функция init вернёт значение 0 в случае успеха. По существу, в более древних версиях ядра, отказ в возврате 0 в случае успеха вызывал бы внезапную выгрузку такого модуля ядра из памяти ядра. В наши дни такое удаление модуля ядра не происходит, однако само ядро испускает предупредительное сообщение относительно такого факта, что было возвращено некое подозрительное не нулевое значение.

О подпрограмме уборки особенно и нечего сказать. Она не получает никаких параметров и ничего не возвращает (void). Её задание состоит в выполнении любых необходимых чисток прежде чем этот модуль ядра выгрузится из памяти ядра.

[Совет]Совет

Не включая макро module_exit() в свой модуль ядра, вы превращаете в невозможную вообще его выгрузку (хотя конечно остаются останов системы или перезагузка). Занимательно... (Я предлагаю вам испытать это в качестве маленького упражнения!)

Естественно: нет ничего проще: такое поведение гарантировано предотвращает выгрузку только когда ваше ядро собрано с флагом CONFIG_MODULE_FORCE_UNLOAD, установленным в Disabled (по умолчанию).

Макросы ERR_PTR и PTR_ERR

После обсуждения возврата значений вы теперь понимаете что соответствующая процедура init своего модуля ядра обязана возвращать некое целое. Что если вы желаете вернуть вместо этого некий указатель? На помощь нам приходит встроенная функция ERR_PTR(), позволяя возвращать некий указатель замаскированный под целое число, просто приводя его значение к void *. В действительности он поступает лучше: вы можете проверить наличие некоторой ошибки при помощи встроенной функции IS_ERR() (которая на самом деле всего выводит бедет ли полученное значение пребывать в диапазоне [-1 до -4095]), кодирует некое отрицательное значение ошибки в указатель посредством встроенной функции ERR_PTR() и выполняет выборку этого значения по такому указателю при помощи обратной процедуры PTR_ERR().

В качестве простого примера рассмотрим представленный здесь код вызывающей стороны. На этот раз у нас имеется (образец) функции myfunc(), возвращающей некий указатель (на структуру с названием mystruct) а не некое целое:


struct mystruct * myfunc(void)
{
    struct mystruct *mys = NULL;
    mys = kzalloc(sizeof(struct mystruct), GFP_KERNEL);
    if (!mys)
        return ERR_PTR(-ENOMEM);
    [...]
    return mys;
}
		

Собственно код вызывающей стороны таков:


[...]
gmys = myfunc();
if (IS_ERR(gmys)) {
    pr_warn("%s: myfunc alloc failed, aborting...\n", OURMODNAME);
    stat = PTR_ERR(gmys); /* sets 'stat' to the value -ENOMEM */
    goto out_fail_1;
}
[...]
return stat;
out_fail_1:
    return stat;
}
		

Вашему вниманию, встроенные функции ERR_PTR(), PTR_ERR() и IS_ERR(), все они пребывают внутри файла include/linux/err.h (заголовка ядра). Документация ядра обсуждает соглашения возврата функции ядра. К тому же вы можете найти примеры использования этих функций в коде crypto/api-samples внутри самого дерева исходного кода ядра.

Ключевые слова __init и __exit

Незначительный остаток: какие в точности макросы __init и __exit мы наблюдаем внутри характерных черт своих предыдущих функций? В целом это вставляемые нашим компоновщиком атрибуты оптимизации памяти.

Макро __init определяет для кода некий раздел init.text. Аналогично, все определяемые атрибутом _initdata данные проходят в раздел init.data. Общим момент здесь состоит в том, что собственно код и данные в функции init используются в точности только в процессе инициализации. После её активизации, она никогда более не будет вызываться; итак, будучи вызваной, она затем вычищается (через free_initmem()).

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

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

Распространённые операции в модулях ядра

Теперь давайте окунёмся в то как в точности вы можете собирать, загружать и выгружать некий модуль ядра. Совместно с этим, мы также пройдёмся по основам относительно чрезвычайно полезного API ядра printk(), подробностям того как перечислять загруженные в данный момент модули ядра при помощи lsmod(8), а также удобному сценарию для автоматизации некоторых распространённых в процессе разработки модуля ядра задач. Итак, приступим!

Сборка модуля ядра

[Замечание]Замечание

Мы определённо побуждаем вас испробовать наш пример упражнения модуля ядра Hello, world (если вы этого ещё пока не сделали)! Для этого мы предполагаем что вы уже клонировали GitHub репозиторий этой книги. Если это не так, сделайте, пожалуйста, это прямо сейчас (за подробностями отсылаем вас к разделу Технические требования).

Здесь мы покажем, шаг за шагом, как в точности вы можете собрать, а затем и вставить наш первый модуль ядра в память ядра. И снова, быстро вспомните: мы выполняем эти шаги в гостевой ВМ x86-64 Linux (под Oracle VirtualBox 6.1), запущенной под дистрибутивом Ubuntu 18.04.3 LTS:

  1. Измените каталог и подкаталог на исходный код этой книги. Наш самый первый модуль ядра обитает в своём собственном каталоге (как и должно быть!) с названием helloworld_lkm:

    
    cd <book-code-dir>/ch4/helloworld_lkm 
    		
    [Совет]Совет

    <book-code-dir>, естественно, та папка, в которую вы клонировали репозиторй GitHub этой книги; в нашем случае (взгляните на Рисунок 4.5), вы можете видеть что это /home/llkd/book_llkd/Linux-Kernel-Programming/.

  2. Теперь проверим свой базовый код:

    
    $ pwd
    <book-code-dir>/ch4/helloworld_lkm
    $ ls -l
    total 8
    -rw-rw-r-- 1 llkd llkd 1211 Jan 24 13:01 helloworld_lkm.c
    -rw-rw-r-- 1 llkd llkd  333 Jan 24 13:01 Makefile
    $
    		
  3. Соберём его при помощи make:

     

    Рисунок 4-5


    Перечисление и сборка нашего самого первого модуля ядра Hello, world

Наш предыдущий снимок экрана показывает что необходимый нам модуль ядра был успешно собран. Это файл ./helloworld_lkm.ko. (Кроме того, обратите внимание, что мы загрузились из своего персонального ядра 5.4.0, собранное в наших предыдущих главах, а следовательно и собрали свой модуль ядра под это ядро).

Исполнение модуля ядра

Чтобы получить свой модуль ядра запущенным, вам потребуется сначала загрузить его в пространство памяти ядра. Это носит название вставки соответствующего модуля в память ядра.

Получение соответствующего модуля ядра в сегменте ядра Linux может выполняться несколькими способами, каждый из которых в конечном счёте сводится к активации одного из системных вызовов [f]init_module(2). Для удобства существует несколько утилит- обёрток, которые и выполняют это (или же вы всегда можете написать свою). Мы воспользуемся популярной утилитой insmod(8) (произносится как "insert module"), приводимой ниже; основным параметром для insmod выступает название пути к вставляемому модулю ядра:


$ insmod ./helloworld_lkm.ko 
insmod: ERROR: could not insert module ./helloworld_lkm.ko: Operation not permitted
$
		

Она отказывает! На самом деле, это должно быть очевидно, почему так происходит. Задумайтесь об этом: вставка кода в само ядро, в самом непосредственном смысле, даже ещё выше чем быть root (суперпользователем) в своей системе - и опять, я напоминаю вам: это код ядра и он будет исполняться с полномочиями ядра. Если любому пользователю позволительно вставлять или удалять модули ядра, хакерам выпал бы счастливый случай! Развёртывание вредоносного кода стало бы достаточно тривиальным делом. Поэтому, по причинам безопасности, только обладая доступом root вы можете вставлять или извлекать модули ядра.

[Замечание]Замечание

Технически говоря, пребывание в качестве root подразумевает, что значение Bold/ RUID/EUID (Real и/ или Effective UID) имеет особое нулевое значение. Но не только этим всё ограничивается, всякое современное ядро "понимает" некий поток как обладающий определёнными возможностями (посредством современной и вышестоящей Модели возможностей POSIX); только некий процесс/ поток с возможностью CAP_SYS_MODULE способен загружать (выгружать) модули ядра. За дополнительными сведениями мы отсылаем своего читателя к странице руководства capabilities(7).

Итак, давайте снова попытаемся вставить свой модуль ядра в память, на этот раз с полномочиями root через sudo(8):


$ sudo insmod ./helloworld_lkm.ko
[sudo] password for llkd:
$ echo $?
0
		

Теперь это сработало! Как мы уже намекали ранее, наша утилита insmod(8) работает через активацию системного вызова [f]init_module(2). Когда наша утилита insmod(8) (а на самом деле системный вызов [f]init_module(2)) может отказать?

Имеется несколько вариантов:

  • Полномочия: Не исполняется от имени root или в отсутствии установленной возможности CAP_SYS_MODULE (errno <- EPERM)

  • Это ядро настраивалось в рамках файловой системы proc, /proc/sys/kernel/modules_disabled, настроенной в значение 1 (по умолчанию оно установлено в 0).

  • Модуль ядра обладает тем же самым названием, которое уже имеется в памяти ядра (errno <- EEXISTS).

Ладно, всё выглядит прекрасно. Полученным от $? результатом был 0, что подразумевает, что предыдущая команда оболочки оказалась успешной. Великолепно, однако где же наше сообщение Hello, world? Читаем дальше!

Быстрый первый взгляд на printk() ядра

Чтобы испустить некое сообщение, разработчик C пространства ядра часто применяет проверенный временем API glibc printf(3) (или, при написании кода C++ cout). Однако, важно понимать, что в пространстве ядра нет никаких библиотек. Следовательно, у нас просто нет доступа к старому доброму API printf(). Вместо этого, по существу внутри самого ядра был реализован API ядра printk() (любопытствуете где находится его код? он здесь, внутри самого дерева исходного кода ядра: kernel/printk/printk.c:printk()).

Испускание некого сообщения посредством API printk() простое и очень похожее на его осуществление через printf(3). В нашем образце модуля ядра вот где происходит это действо:


printk(KERN_INFO "Hello, world\n");
 	   

Хотя на первый взгляд и очень похожий на printf, printk в действительности достаточно отличается. Что касается схожести, его API принимает в качестве его параметра некий формат строки. Этот формат строки во многом достаточно идентичен формату в printf.

Но на этом схожесть заканчивается. Основное ключевое отличие между printf и printk таково: API библиотеки printf(3) пространства пользователя работает с форматированием текста в виде запроса и активации системного вызова write(2), который, в свою очередь, действительно выполняет некую запись в устройство stdout, которым, по умолчанию, является окно Терминала (или консольное устройство). API ядра printk также форматирует свой текст согласно запрашиваемому, однако его получатель вывода отличается. Он пишет по крайней мере в одно место - самое первое из приводимого ниже списка - а возможно и некоторые иные:

  • Буфер ведения журнала ядра в оперативной памяти (энергозависимый)

  • Файл регистрации, файл журнала своего ядра (энергонезависимый)

  • Устройство консоли

[Замечание]Замечание

На данный момент мы опустим внутренние подробности, относящиеся к работе printk. Кроме того, игнорируйте, пожалуйста маркер KERN_INFO внутри API printk; вскоре мы обсудим всё это в достаточной степени.

Когда вы испускаете сообщение через printk, гарантируется что вывод проходит в буфер регистрации в памяти ядра (оперативная память). По существу, это и составляет сам журнал ядра. Важно отметить, что вы никогда не обнаружите вывод printk непосредственно при работе в графическом режиме с неким исполняемым процессом X сервера (средой по умолчанию при работе с типичным дистрибутивом Linux). Поэтому, очевидный вопрос такой: как ознакомиться с содержимым буфера регистраций ядра? Имеется несколько способов. На данный момент мы просто воспользуемся самым быстрым и простым путём.

Воспользуйтесь утилитой dmesg(1)! По умолчанию, dmesg выдаст в stdout дамп всего содержимое всего буфера журнала ядра. Здесь мы рассмотрим внутри него самые последние две строки буфера журнала ядра:


$ dmesg | tail -n2
[ 2912.880797] hello: loading out-of-tree module taints kernel.
[ 2912.881098] Hello, world
$
		

Вот оно, наконец, наше сообщение Hello, world!

[Замечание]Замечание

Вы можете просто проигнорировать на данный момент сообщение loading out-of-tree module taints kernel.. По причинам безопасности большинство современных дистро будут помечать своё ядро как tainted (запятнанное, буквально, "заражённое" или "загрязнённое") при вставке модуля ядра стороннего разработчика "не из дерева" (или без подписи). (Ладно, на самом деле это ничто иное как простое прикрытие, стоящее за словами: "если с этого момента что- то пойдёт не так, мы не несём ответственности и тому подобное"; ну, вы поняли основную мысль {Прим. пер.: подробнее о безопасном запуске в наших переводах Практики загрузки Йогеш Бабар и Руткиты и буткиты. Противодействие современному вредоносному ПО и угрозам следующего поколения Алекса Матросова, Евгения Родионова и Сергея Братуса}).

Для небольшого рзнообразия вот снимок экрана нашего модуля ядра Hello, world, вставленного и удалённого (подробности позднее) в госте x86-64 CentOS 8 исполняемым под ядром 5.4 Linux LTS (который мы персонально собираем как это показано в наших первой и второй главах):

 

Рисунок 4-6


Снимок экрана, отображающий нашу работу с модулем ядра Hello, world в госте x86-64 CentOS 8

Внутри своего журнала ядра, как это отображает утилита dmesg(1), значения чисел в самом левом столбце это простая временная метка в формате [seconds.microseconds], времени, прошедшего после запуска системы (хотя, однако, и не рекомендуется рассматривать его как абсолюно точное). Между прочим, эта временная метка является переменной Kconfig - параметра настроек ядра - с названием CONFIG_PRINTK_TIME; его можно перекрывать при помощи параметра ядра printk.time.

Перечисление всех живых модулей ядра

Вернёмся обратно к своему модулю ядра: до сих пор мы собирали его, загружали в своё ядро и убеждались в активации его точки входа, функции helloworld_lkm_init(), тем самым исполняя имеющийся API printk. Итак, в данный момент, чем он в реальности занят? Ладно, ничем в действительности; этот модуль ядра в целом (к счастью?) сидит себе в памяти ядра не делая абсолютно ничего. На практике мы можем увидеть его при помощи утилиты lsmod(8):


$ lsmod | head
Module                  Size  Used by
helloworld_lkm         16384  0
isofs                  32768  0
fuse                  139264  3
tun                    57344  0
[...]
e1000                 155648  0
dm_mirror              28672  0
dm_region_hash         20480  1 dm_mirror
dm_log                 20480  2 dm_region_hash,dm_mirror
dm_mod                151552  11 dm_log,dm_mirror
$
		

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

  • Самый первый столбец отображает название соответствующего модуля ядра.

  • Второй столбец это (статический) размер в байтах, который он занимает в ядре.

  • Третий столбец это счётчик применений данного модуля

  • Необязательная четвёртая колонка (и дополнительные, которые могут следовать далее) поясняется в нашей следующей главе (в разделе Разбираемся с составлением стека модуля. Кроме того, в последних ядрах Linux x86-64, как мимнимум 16кБ памяти ядра выглядит как получаемые модулем ядра.)

итак, отлично: на данный момент вы успешно собрали, загрузили и исполнили свой самый первый модуль ядра в памяти ядра и в целом он работает: что дальше? Ну, с этим ничего особенного! Мы просто в следующем разделе изучим как выгружать его. Конечно, впереди ещё много чего... придерживайтесь чтения!

Выгрузка определённого модуля из памяти ядра

Для выгрузки своего модуля ядра мы воспользуемся удобной утилитой rmmod(8) (remove module):


$ rmmod 
rmmod: ERROR: missing module name.
$ rmmod helloworld_lkm
rmmod: ERROR: could not remove 'helloworld_lkm': Operation not permitted
rmmod: ERROR: could not remove module helloworld_lkm: Operation not permitted
$ sudo rmmod helloworld_lkm
[sudo] password for llkd: 
$ dmesg |tail -n2
[ 2912.881098] Hello, world
[ 5551.863410] Goodbye, world
$
		

Основным параметром для rmmod(8) выступает название нашего модуля ядра (которое отобраается в самом первом столбце lsmod(8)), не полного имени пути. Очевидно, что как и для insmod(8), для достижения успеха нам требуется исполнять свою утилиту rmmod(8) в качестве пользователя root.

Здесь мы также можем видеть, благодаря нашему rmmod, активируется подпрограмма выхода (или "деструктор") функции helloworld_lkm_exit() из соответствующего модуля ядра. Она в свою очередь активирует printk, который испускает сообщение Goodbye, world (которое можно просмотреть при помощи dmesg).

Когда может отказать rmmod (отметим, что внутренне он превращается в системный вызов delete_module(2))? Вот некоторые варианты:

  • Полномочия: Когда он не исполняется от имени root или при отсутствии возможности CAP_SYS_MODULE (errno <- EPERM).

  • Если код самого модуля ядра и/ или данные используются другим модулем (при наличии зависимостей; это подробно рассматривается в разделе Выполняем попытку составления стека модуля нашей следующей главы) или этот модуль в настоящий момент используется процессом (либо потоком), тогда значение счётчика применений этого модуля будет положительным и rmmod завершится отказом (errno <- EBUSY).

  • Сам модуль ядра не определяет процедуры выхода (или деструктора) при помощи макро module_exit() и отключён параметр настройки ядра CONFIG_MODULE_FORCE_UNLOAD

Некоторые удобные утилиты, связанные с управлением модулем ни что иное, как символические (программные) ссылки на единственную утилиту kmod(8) (аналогично популярной утилите busybox). Такими оболочками выступают lsmod(8), rmmod(8), insmod(8), modinfo(8), modprobe(8) и depmod(8). Давайте взглянем на некоторые из них:


$ ls -l $(which insmod) ; ls -l $(which lsmod) ; ls -l $(which rmmod)
lrwxrwxrwx 1 root root 9 Oct 24 04:50 /sbin/insmod -> /bin/kmod
lrwxrwxrwx 1 root root 9 Oct 24 04:50 /sbin/lsmod -> /bin/kmod
lrwxrwxrwx 1 root root 9 Oct 24 04:50 /sbin/rmmod -> /bin/kmod
$ 
		

Обратите внимание на то, что точное местоположение этих утилит (/bin, /sbin или /usr/sbin) может отличаться для различных дистрибутивов.

Наш удобный сценарий lkm

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


#!/bin/bash
# lkm : a silly kernel module dev - build, load, unload - helper wrapper script
[...]
unset ARCH
unset CROSS_COMPILE
name=$(basename "${0}")

# Display and run the provided command.
# Parameter(s) : the command to run
runcmd()
{
    local SEP="------------------------------"
    [ $# -eq 0 ] && return
    echo "${SEP}
$*
${SEP}"
    eval "$@"
    [ $? -ne 0 ] && echo " ^--[FAILED]"
}

### "main" here
[ $# -ne 1 ] && {
  echo "Usage: ${name} name-of-kernel-module-file (without the .c)"
  exit 1
}
[[ "${1}" = *"."* ]] && {
  echo "Usage: ${name} name-of-kernel-module-file ONLY (do NOT put any extension)."
  exit 1
}
echo "Version info:"
which lsb_release >/dev/null 2>&1 && {
  echo -n "Distro: "
  lsb_release -a 2>/dev/null |grep "Description" |awk -F':' '{print $2}'
}
echo -n "Kernel: " ; uname -r
runcmd "sudo rmmod $1 2> /dev/null"
runcmd "make clean"
runcmd "sudo dmesg -c > /dev/null"
runcmd "make || exit 1"
[ ! -f "$1".ko ] && {
  echo "[!] ${name}: $1.ko has not been built, aborting..."
  exit 1
}
runcmd "sudo insmod ./$1.ko && lsmod|grep $1"
runcmd dmesg
exit 0
		

Принимая во внимание название модуля ядра в качестве параметра - без какой бы то ни было части расширения (например, .c) - наш сценарий lkm выполняет некоторые проверки допустимости, отображает некоторые сведения о версии и затем применяет обёртку функции bash runcmd() для отображения названия и запуска заданной команды, в результате чего будет без головной боли исполнен рабочий поток clean/build/load/lsmod/dmesg. Давайте испробуем свой самый первый модуль ядра:


$ pwd
<...>/ch4/helloworld_lkm
$ ../../lkm
Usage: lkm name-of-kernel-module-file (without the .c)
$ ../../lkm helloworld_lkm
Version info:
Distro:          Ubuntu 18.04.3 LTS
Kernel: 5.0.0-36-generic
------------------------------
sudo rmmod helloworld_lkm 2> /dev/null
------------------------------
[sudo] password for llkd: 
------------------------------
sudo dmesg -C
------------------------------
------------------------------
make || exit 1
------------------------------
make -C /lib/modules/5.0.0-36-generic/build/ M=/home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm modules
make[1]: Entering directory '/usr/src/linux-headers-5.0.0-36-generic'
  CC [M]  /home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm/helloworld_lkm.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm/helloworld_lkm.mod.o
  LD [M]  /home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm/helloworld_lkm.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.0.0-36-generic'
------------------------------
sudo insmod ./helloworld_lkm.ko && lsmod|grep helloworld_lkm
------------------------------
helloworld_lkm         16384  0
------------------------------
dmesg
------------------------------
[ 8132.596795] Hello, world
$
		

Всё сделано! Не забудьте выгрузить свой модуль ядра при помощи rmmod(8).

Поздравляем! Вы теперь изучили как написать и испытать простой модуль Hello, world. Однако предстоит ещё много работы, прежде чем вы сможете почивать на лаврах; в нашем следующем разделе рассматриваются более важные подробности относитльно ведения журнала ядра и универсального API printk.

Разбираемся с ведением журнала ядра и printk

Применение закольцованного буфера в памяти ядра

Ведение журнала ядра и journalctl systemd

Применение уровней регистрации printk

Удобные макросы pr_

Запись на консоль

Запись вывода в консоль Raspberry Pi

Включение сообщений ядра pr_debug()

Ограничение по скорости экземпляров printk

Генерация сообщений ядра из пространства пользователя

Стандартизация вывода printk через макро pr_fmt

Переносимость и формат спецификаторов printk

Разбираемся с основами модуля ядра Makefile

Выводы

Вопросы

Дальнейшее чтение