Одним из важнейших новшеств в ядре Linux® стали загружаемые
модули ядра (loadable kernel modules, LKM), появившиеся в версии 1.2.
Они обеспечивают ядру гибкость и функциональность. В этой статье мы
раскроем идеи, заложенные в эти модули, и узнаем, как эти независимые
объекты динамически становятся частью ядра Linux.
Ядро Linux относится к категории так называемых монолитных – это означает, что большая часть функциональности операционной системы называется ядром и запускается в привилегированном режиме. Этот подход отличен от подхода микроядра,
когда в режиме ядра выполняется только основная функциональность
(взаимодействие между процессами [inter-process communication, IPC],
диспетчеризация, базовый ввод-вывод [I/O], управление памятью), а
остальная функциональность вытесняется за пределы привилегированной
зоны (драйверы, сетевой стек, файловые системы). Можно было бы
подумать, что ядро Linux очень статично, но на самом деле все как раз
наоборот.
Ядро Linux динамически изменяемое – это
означает, что вы можете загружать в ядро дополнительную
функциональность, выгружать функции из ядра и даже добавлять новые
модули, использующие другие модули ядра. Преимущество загружаемых
модулей заключается в возможности сократить расход памяти для ядра,
загружая только необходимые модули (это может оказаться важным для
встроенных систем).
Linux – не единственное (и не
первое) динамически изменяемое монолитное ядро. Загружаемые модули
поддерживаются в BSD-системах, Sun Solaris, в ядрах более старых
операционных систем, таких как OpenVMS, а также в других популярных ОС,
таких как Microsoft® Windows® и Apple Mac OS X.
Анатомия модуля ядра
Загружаемые
модули ядра имеют ряд фундаментальных отличий от элементов,
интегрированных непосредственно в ядро, а также от обычных программ.
Обычная программа содержит главную процедуру (main)в отличие от
загружаемого модуля, содержащего функции входа и выхода (в версии 2.6
эти функции можно именовать как угодно). Функция входа вызывается,
когда модуль загружается в ядро, а функция выхода – соответственно при
выгрузке из ядра. Поскольку функции входа и выхода являются
пользовательскими, для указания назначения этих функций используются
макросы module_init и module_exit .
Загружаемый модуль содержит также набор обязательных и дополнительных
макросов. Они определяют тип лицензии, автора и описание модуля, а
также другие параметры. Пример очень простого загружаемого модуля
приведен на рисунке 1.
Рисунок 1. Код простого загружаемого модуля.
Версия
2.6 ядра Linux предоставляет новый, более простой метод создания
загружаемых модулей. После того как модуль создан, можно использовать
обычные пользовательские инструменты для управления модулями (несмотря
на изменения внутреннего устройства): insmod (устанавливает модуль),
rmmod (удаляет модуль),
modprobe (контейнер для
insmod и rmmod ),
depmod (для создания зависимостей между модулями) и
modinfo (для поиска значений в модулях макроса).
Анатомия объектного кода модуля ядра
Загружаемый
модуль представляет собой просто специальный объектный файл в формате
ELF (Executable and Linkable Format). Обычно объектные файлы
обрабатываются компоновщиком, который разрешает символы и формирует
исполняемый файл. Однако в связи с тем, что загружаемый модуль не может
разрешить символы до загрузки в ядро, он остается ELF-объектом. Для
работы с загружаемыми модулями можно использовать стандартные средства
работы с объектными файлами (которые в версии 2.6 имеют суффикс .ko,
от kernel object). Например, если вывести информацию о модуле утилитой
objdump, вы обнаружите несколько привычных разделов, в том числе .text (инструкции), .data (инициализированные данные) и .bss (Block Started Symbol или неинициализированные данные).
В
модуле также обнаружатся дополнительные разделы, ответственные за
поддержку его динамического поведения. Раздел .init.text содержит код module_init , а раздел .exit.text – код module_exit code (рисунок 2). Раздел .modinfo содержит тексты макросов, указывающие тип лицензии, автора, описание и т. д.
Рисунок 2. Пример загружаемого модуля с разделами ELF
Теперь,
изучив основы загружаемых модулей ядра, давайте разберемся, как модули
загружаются в ядро и как происходит управление ими внутри ядра.
Жизненный цикл загружаемого модуля ядра
Процесс загрузки модуля начинается в пользовательском пространстве с команды
insmod (вставить модуль). Команда
insmod определяет модуль для загрузки и выполняет системный вызов уровня пользователя init_module для начала процесса загрузки. Команда insmod
для ядра версии 2.6 стала чрезвычайно простой (70 строк кода) за счет переноса части работы в ядро. Команда
insmod не выполняет никаких действий по разрешению символов (вместе с командой kerneld ), а просто копирует двоичный код модуля в ядро при помощи функции init_module ; остальное делает само ядро.
Функция
init_module работает на уровне системных вызовов и вызывает функцию ядра
sys_init_module
(рисунок 3). Это основная функция для загрузки модуля, обращающаяся к
нескольким другим функциям для решения специальных задач. Аналогичным
образом команда rmmod выполняет системный вызов функции
delete_module , которая обращается в ядро с вызовом
sys_delete_module для удаления модуля из ядра.
Рисунок 3. Основные команды и функции, участвующие в загрузке и выгрузке модуля
Во
время загрузки и выгрузки модуля подсистема модулей поддерживает
простой набор переменных состояния для обозначения статуса модуля. При
загрузке модуля он имеет статус MODULE_STATE_COMING . Если модуль загружен и доступен, его статус –
MODULE_STATE_LIVE . Если модуль выгружен –
MODULE_STATE_GOING .
Подробности загрузки модуля
Теперь давайте взглянем на внутренние функции для загрузки модуля (рисунок 4). При вызове функции ядра
sys_init_module сначала выполняется проверка того, имеет ли вызывающий соответствующие разрешения (при помощи функции capable ). Затем вызывается функция load_module ,
которая выполняет механическую работу по размещению модуля в ядре и
производит необходимые операции . Функция load_module возвращает ссылку, которая
указывает на только что загруженный модуль. Затем он вносится в
двусвязный список всех модулей в системе, и все потоки, ожидающие
изменения состояния модуля, уведомляются при помощи специального
списка. В конце вызывается функция init() и статус модуля обновляется, чтобы указать, что он загружен и доступен.
Рисунок 4. Внутренний процесс загрузки модуля (упрощенно)
Внутренние процессы загрузки модуля представляют собой анализ и управление модулями ELF. Функция load_module
(которая находится в ./linux/kernel/module.c) начинает с выделения
блока временной памяти для хранения всего модуля ELF. Затем модуль ELF
считывается из пользовательского пространства во временную память при
помощи copy_from_user . Являясь объектом ELF, этот файл имеет очень специфичную структуру, которая легко поддается анализу и проверке.
Следующим
шагом является ряд "санитарных проверок" загруженного образа (является
ли ELF-файл допустимым? соответствует ли он текущей архитектуре? и так
далее). После того как проверка пройдена, образ ELF анализируется и
создается набор вспомогательных переменных для заголовка каждого
раздела, чтобы облегчить дальнейший доступ к ним. Поскольку базовый
адрес объектного файла ELF равен 0 (до перемещения), эти переменные
включают соответствующие смещения в блок временной памяти. Во время
создания вспомогательных переменных также проверяются заголовки
разделов ELF, чтобы убедиться, что загружаемый модуль корректен.
Дополнительные
параметры модуля, если они есть, загружаются из пользовательского
пространства в другой выделенный блок памяти ядра (шаг 4), и статус
модуля обновляется, чтобы обозначить, что он загружен (MODULE_STATE_COMING ).
Если необходимы данные для процессоров (согласно результатам проверки
заголовков разделов), для них выделяется отдельный блок.
В
предыдущих шагах разделы модуля загружались в память ядра (временную),
и было известно, какие из них используются постоянно, а какие могут
быть удалены. На следующем шаге (7) для модуля в памяти выделяется
окончательное расположение, и в него перемещаются необходимые разделы
(обозначенные в заголовках SHF_ALLOC или расположенные в
памяти во время выполнения). Затем производится дополнительное
выделение памяти размера, необходимого для требуемых разделов модуля.
Производится проход по всем разделам во временном блоке ELF,, и те из
них, которые необходимы для выполнения, копируются в новый блок. Затем
следуют некоторые служебные процедуры. Также происходит разрешение
символов, как расположенных в ядре (включенных в образ ядра при
компиляции), так и временных (экспортированных из других модулей).
Затем
производится проход по оставшимся разделам и выполняются перемещения.
Этот шаг зависит от архитектуры и соответственно основывается на
вспомогательных функциях, определенных для данной архитектуры
(./linux/arch/<arch>/kernel/module.c). В конце очищается кэш
инструкций (поскольку использовались временные разделы .text),
выполняется еще несколько служебных процедур (очистка памяти временного
модуля, настройка sysfs) и, в итоге, модуль возвращает load_module . Подробности выгрузки модуля
Выгрузка
модуля фактически представляет собой зеркальное отражение процесса
загрузки за исключением того, что для безопасного удаления модуля
необходимо выполнить несколько "санитарных проверок". Выгрузка модуля
начинается в пользовательском пространстве с выполнения команды rmmod (удалить модуль). Внутри команды
rmmod выполняется системный вызов
delete_module , который в конечном счете инициирует sys_delete_module внутри ядра . Основные операции удаления модуля показаны на рисунке 5.
Рисунок 5. Внутренний процесс выгрузки модуля (упрощенно).
При вызове функции ядра sys_delete_module
(с именем удаляемого модуля в качестве параметра) сначала выполняется
проверка того, имеет ли вызывающий соответствующие разрешения. Затем по
списку проверяются зависимости других модулей от данного модуля. При
этом используется список modules_which_use_me , содержащий
по элементу для каждого зависимого модуля. Если список пуст, т.е.
зависимостей не обнаружено, то модуль становится кандидатом на удаление
(иначе возвращается ошибка). Затем проверяется, загружен ли модуль.
Ничто не запрещает пользователю запустить команду rmmod
для модуля, который в данный момент устанавливается, поэтому данная
процедура проверяет, активен ли модуль. После нескольких дополнительных
служебных проверок предпоследним шагом вызывается функция выхода
данного модуля (предоставляемая самим модулем). В заключение вызывается
функция free_module .
К моменту вызова free_module
уже известно, что модуль может быть безопасно удален. Зависимостей не
обнаружено, и для данного модуля можно начать процесс очистки ядра.
Этот процесс начинается с удаления модуля из различных списков, в
которые он был помещен во время установки (sysfs, список модулей и
т.д.). Потом инициируется команда очистки, зависящая от архитектуры
(она расположена в ./linux/arch/<arch>/kernel/module.c). Затем
обрабатываются зависимые модули, и данный модуль удаляется из их
списков. В конце, когда с точки зрения ядра очистка завершена,
освобождаются различные области памяти, выделенные для модуля, в том
числе память для параметров, память для данных по процессорам и память
модуля ELF (core и
init ).
Оптимизация ядра для управления модулями
Для
многих приложений необходима возможность динамической загрузки модулей,
но выгружать их не обязательно. Это позволяет ядру быть динамическим
при запуске (загрузка модулей в зависимости от обнаруженных устройств),
но неизменным во время работы. Если не требуется выгружать модули после
загрузки, можно сделать несколько усовершенствований для сокращения
количества кода, необходимого для управления модулями. В этом случае
можно отключить параметр настройки ядра CONFIG_MODULE_UNLOAD , удалив тем самым значительную часть функциональности ядра, связанную с выгрузкой модулей. Дальнейшее изучение
Это
был лишь общий обзор процессов управления модулями в ядре. Лучшим
источником дополнительной информации об управлении модулями является
сам исходный код. Основные функции управления модулями содержатся в
./linux/kernel/module.c (и соответствующем файле заголовка
./linux/include/linux/module.h). Несколько функций, зависящих от
архитектуры, находятся в ./linux/arch/<arch>/kernel/module.c.
Наконец, функция автозагрузки ядра (которая автоматически загружает
модуль из ядра при необходимости) находится в файле
./linux/kernel/kmod.c. Эта функция включается при помощи параметра
настройки CONFIG_KMOD .
|