Компиляция и загрузка |
Предыдущая Содержание Следующая |
Пример “hello world” в начале этой главы включает краткую демонстрацию сборки модуля и загрузки его в систему. Существует, конечно, гораздо больше, чем тот процесс, который мы видели до сих пор. Этот раздел содержит более подробную информацию о том, как автор модуля превращает исходный код в работающую подсистему внутри ядра. Компиляция модулейВ первую очередь нам необходимо узнать немножко о том, как должны собираться модули. Процесс сборки модулей существенно отличается от используемого для приложений пользовательского пространства; ядро - это большая автономная программа с подробными и точными требованиями о том, как кусочки собирать вместе. Процесс сборки также отличен от того, как это делалось в предыдущих версиях ядра; новая система сборки проще в использовании и даёт более правильный результат, но он очень отличается от того, что использовался раньше. Система сборки ядра представляет собой сложного зверя и мы посмотрим только на крошечный кусочек. Файлы, находящиеся в директории Documentation/kbuild исходных кодов ядра, являются обязательными для чтения любым желающим понять всё, что на самом деле происходит под поверхностью. Есть некоторые предварительные условия, которые необходимо выполнить, прежде чем вы сможете собрать модули ядра. Во-первых, убедитесь, что вы имеете достаточно свежие версии компилятора, утилит модулей и других необходимых утилит. Файл Documentation/changes в каталоге документации ядра всегда содержит список необходимых версий утилит; вы должны проверить это перед тем, как двигаться дальше. Попытка построить ядро (и его модули) с неправильными версиями утилит может привести к бесконечным тонким, сложным проблемам. Заметим, что иногда слишком новая версия компилятора может быть столь же проблематичной, как и слишком старая; исходный код ядра делает много предположений о компиляторе и новые релизы могут иногда сломать что-то на некоторое время.
Если вы всё ещё не имеете под рукой дерева ядра или это ядро ещё не сконфигурировано и не собрано, теперь пора это сделать. Вы не сможете собрать загружаемый модуль для ядра версии 2.6 без этого дерева в своей файловой системе. Также полезно (но не обязательно), чтобы работающим ядром было то, для которого вы будете собирать модули.
После того как вы всё установили, создать Makefile для модуля просто. Фактически, для примера "hello world", приведённого ранее в этой главе, достаточно будет одной строчки:
obj-m := hello.o
Читателям, знакомым с make, но не с системой сборки ядра 2.6, вероятно, будет интересно, как работает этот Makefile. В конце концов, приведённая выше строчка выглядит не как обычный Makefile. Ответ конечно же в том, что всё остальное делает система сборки ядра. Определение выше (которое использует расширенный синтаксис, предоставляемый GNU make) заявляет, что требуется собрать один модуль из объектного файла hello.o. Результирующий модуль после сборки из объектного файла именуется как hello.ko.
Если вместо этого вы имеете модуль под названием module.ko, который создаётся из двух исходных файлов (называемых, скажем, file1.c и file2.c), правильными магическими строчками будут:
obj-m := module.o module-objs := file1.o file2.o
Чтобы Makefile, подобный приведённому выше, заработал, он должен быть вызван в более широком контексте системы сборки ядра. Если ваше дерево исходных текстов ядра находится, скажем, в каталоге ~/kernel-2.6, команда make, необходимая для сборки модуля (набранная в каталоге, содержащем исходник модуля и Makefile), будет:
make -C ~/kernel-2.6 M=`pwd` modules
Эта команда начинается со смены своего каталога на указанный опцией -C (то есть на ваш каталог исходных кодов ядра). Там она находит Makefile верхнего уровня ядра. Опция M= заставляет Makefile вернуться обратно в директорию исходников вашего модуля, прежде чем попытаться построить целевой модуль. Эта задача, в свою очередь, ссылается на список модулей, содержащихся в переменной obj-m, который мы определили в наших примерах как module.o. Ввод предыдущей команды make может стать через некоторое время утомительным, так что разработчики ядра разработали своего рода идиому Makefile, которая делает жизнь легче для модулей, собираемых вне дерева ядра. Весь фокус в том, чтобы написать Makefile следующим образом:
# Если KERNELRELEASE определён, значит вызов сделан из # системы сборки ядра и можно использовать её язык. ifneq ($(KERNELRELEASE),) obj-m := hello.o # Иначе вызов сделан прямо из командной # строки; вызвать систему сборки ядра. else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif
И снова мы видим расширенный синтаксис GNU make в действии. Этот Makefile читается при типичной сборке дважды. Когда Makefile вызывается из командной строки, он замечает, что переменная KERNELRELEASE не установлена. Он определяет расположение каталога исходных текстов ядра, воспользовавшись тем, что символическая ссылка build в директории установленных модулей указывает обратно на дерево сборки ядра. Если у вас на самом деле не работает ядро, для которого вы делаете сборку, можно добавить параметр KERNELDIR= в командную строчку, установив переменную KERNELDIR среды окружения или переписать строчку, которая задаёт KERNELDIR в Makefile. После того, как дерево исходных текстов ядра было найдено, Makefile запускает цель default:, которая запускает вторую команду make (параметризованную как $(MAKE) в Makefile), чтобы вызвать систему сборки ядра, как описано выше. При втором чтении Makefile устанавливает obj-m и о реальном создании модуля заботятся Makefile-ы ядра.
Этот механизм создания модулей может вам показаться немного громоздким и расплывчатым. Однако, когда вы привыкнете к нему, вы, скорее всего, оцените возможности, которые были запрограммированы в системе сборки ядра. Обратите внимание, что текст, приведённый выше, не является полным Makefile; реальный Makefile включает обычные набор целей по очистке от ненужных файлов, установке модулей и так далее. Для полноценной иллюстрации посмотрите Makefile-ы в каталоге с исходными кодами примеров. Загрузка и выгрузка модулейПосле сборки модуля следующим шагом является загрузка его в ядро. Как мы уже говорили, эту работу делает для вас insmod. Программа загружает код модуля и данные в ядро, которое, в свою очередь, выполняет функцию, аналогичную выполняемой ld, которой она связывает неразрешённые символы в модуле с таблицей символов ядра. В отличие от линкера (компоновщика), ядро, однако, не изменяет файл модуля на диске, а делает это с копией в памяти. insmod поддерживает несколько параметров командной строки (подробности смотрите на странице справки) и она может присваивать значения параметрам модуля до линковки его с текущим ядром. Таким образом, если модуль разработан правильно, он может быть сконфигурирован во время загрузки; конфигурация во время загрузки даёт пользователю больше гибкости, чем конфигурация во время компиляции, которая всё ещё иногда используется. Конфигурирование во время загрузки объясняется в разделе "Параметры модуля" далее в этой главе.
Заинтересованные читатели могут захотеть посмотреть, как ядро поддерживает insmod: это опирается на системный вызов, определённый в kernel/module.c. Функция sys_init_module выделяет память ядра для удержания модуля (эта память выделяется через vmalloc; смотрите раздел "vmalloc и друзья" в Главе 8); затем копирует текст модуля в эту область памяти, разрешает (определяет) ссылки в модуле на ядро через таблицу символов ядра и вызывает функцию инициализации модуля, чтобы дать выполнить остальное.
Если реально посмотреть исходный код ядра, вы обнаружите, что имена системных вызовов имеют префикс sys_. Это верно для всех системных вызовов, но не других функций; это полезно иметь в виду, когда делается быстрый поиск (греппинг, grepping от grep-служебной программы в Unix) системных вызовов в исходниках. Достойна быстрого упоминания утилита modprobe. modprobe, как и insmod, загружает модуль в ядро. Она отличается тем, что просматривает модуль, который необходимо загрузить, ссылается ли он на любые символы, которые в настоящее время не определены в ядре. В случае обнаружения любых таких ссылок, modprobe ищет другие модули, в которых определены соответствующие символы, по текущему пути поиска модулей. Когда modprobe находит такие модули (которые необходимы загружаемому модулю), она также загружает их в ядро. Если вы в этой ситуации взамен используете insmod, команда даст ошибку с сообщением “unresolved symbols” ("неизвестные символы"), оставленным в системном лог-файле.
Как упоминалось ранее, модули могут быть удалены из ядра утилитой rmmod. Обратите внимание, что удаление модуля завершится неудачей, если ядро считает, что модуль всё ещё используется (например, какая-то программа всё ещё имеет открытый файл для устройства, экспортированного модулями), или если ядро было сконфигурировано, чтобы запрещать удаление модулей. Можно сконфигурировать ядро, чтобы разрешить "принудительное" удаление модулей, даже если они выглядят занятыми. Однако, если вы достигли точки, где вы рассматриваете использовании этой опции, что-то, вероятно, пошло неправильно достаточно сильно, так что перезагрузка может быть лучшим способом действия.
Программа lsmod выдаёт список модулей, загруженных в данный момент в ядро. Также предоставляется некоторая другая информация, такая как любые другие модули, использующие определённый модуль. lsmod работает путём чтения виртуального файла /proc/module. Информацию о загруженных в данный момент модулях можно также найти в виртуальной файловой системе sysfs под /sys/module. Зависимость от версииИмейте в виду, что код вашего модуля должен быть перекомпилирован для каждой версии ядра, это связано, по крайней мере, с отсутствием modversions, не рассматриваемыми здесь, поскольку они больше для дистрибьютеров, чем для разработчиков. Модули сильно привязаны к структурам данных и прототипам функций, определённым в конкретной версии ядра; интерфейс, видимый модулю, может значительно меняться от одной версии ядра к другому. Это, конечно, особенно верно в отношении разрабатываемых ядер.
Ядро не только предполагает, что данный модуль был собран для подходящей версии ядра. Одним из шагов в процессе сборки является ссылка вашего модуля на файл (названный vermagic.o) из текущего дерева ядра; этот объект содержит достаточно много информации о ядре для которого был собран модуль, включая целевую версию ядра, версию компилятора и значения ряда важных параметров конфигурации. При попытке загрузить модуль эта информация может быть проверена на совместимость с работающим ядром. Если данные не совпадают, модуль не загружается, вместо этого вы увидите что-то вроде:
# insmod hello.ko Error inserting './hello.ko': -1 Invalid module format
Просмотр системных лог-файлов (/var/log/messages или того, что ваша система настроена использовать) выявит те проблемы, которые привели к невозможности загрузки модуля. Если вам необходимо скомпилировать модуль для определённой версии ядра, необходимо использовать систему сборки и дерево исходных текстов данной версии. Простое изменение переменной KERNELDIR в показанном ранее примере Makefile делает этот трюк. Интерфейсы ядра часто меняются между релизами. Если вы пишете модуль, который предназначен для работы с множеством версий ядра (особенно если он должен работать с основными релизами), вам, скорее всего, придётся использовать макросы и конструкции #ifdef, чтобы сделать код правильно собираемым. Данное издание этой книги рассказывает только об одной основной версии ядра, так что вы не часто встретите проверку версии в наших примерах кода. Но потребность в них иногда возникает. В таких случаях вы должны использовать определения, содержащиеся в linux/version.h. В этом заголовочном файле определены следующие макросы:
UTS_RELEASE Этот макрос заменяется на строку, которая описывает версию дерева. Например, "2.6.10".
LINUX_VERSION_CODE Этот макрос заменяется на бинарное представление версии ядра, один байт для каждой части номера версии. Например, код для 2.6.10 это 132618 (то есть, 0x02060a). (* Это позволяет использовать до 256 разрабатываемых версий между стабильными версиями.) С этой информацией вы можете (почти) легко определить, с какой версией ядра вы имеете дело.
KERNEL_VERSION(major,minor,release) Этот макрос используется для построения целого числа кода версии из отдельных номеров, которые представляют собой номер версии. Например, KERNEL_VERSION(2,6,10) заменяется на 132618. Этот макрос очень полезен, когда необходимо сравнить текущую версию и заданное контрольное значение.
Большинство зависимостей, базирующихся на версии ядра, могут быть обработаны условиями препроцессора, используя KERNEL_VERSION и LINUX_VERSION_CODE. Не следует, однако, делать проверку зависимостей от версии, покрывая код драйвера беспорядочным волосатыми условиями #ifdef; лучшим способом обращения с несовместимостями является скрытие их в специфичных заголовочных файлах. По общему правилу код, который зависит от версии (или платформы), должен быть скрыт за низкоуровневым макросом или функцией. Высокоуровневый код может просто вызывать эти функции, не заботясь о низкоуровневых деталях. Код, написанный таким образом, как правило, легче читать и более надёжен. Зависимость от платформыКаждая компьютерная платформа имеет свои особенности и разработчики ядра могут свободно использовать все особенности для достижения наилучшей производительности в результирующем объектном файле. В отличие от разработчиков приложений, которые должны линковать свой код с предварительно скомпилированными библиотеками и придерживаться конвенций о передачи параметров, разработчики ядра могут определить некоторым регистрам процессора определённые роли и они сделали это. Кроме того, код ядра может быть оптимизирован для определённого процессора в семействе процессоров, чтобы получить лучшее от целевой платформы: в отличие от приложений, которые часто распространяются в бинарном формате, заказная компиляции ядра может быть оптимизирована для определенного набора компьютеров.
Например, архитектура IA32 (x86) была разделена на несколько разных типов процессоров. Старый 80386 процессоров до сих пор поддерживается (пока), хотя его набор команд по современным меркам довольно ограничен. Более современные процессоры этой архитектуры ввели целый ряд новых возможностей, включая быстрые инструкции входа в ядро, межпроцессорную блокировку, копирование данных и так далее. Новые процессоры могут также при эксплуатации в соответствующем режиме использовать 36-х разрядные (или больше) физические адреса, что позволяет им адресовать более 4 ГБ физической памяти. Другие семейства процессоров предоставляют аналогичные усовершенствования. В зависимости от различных параметров настройки ядро может быть собрано так, чтобы использовать эти дополнительные возможности.
Очевидно, что если модуль предназначен для работы с данным ядром, он должен быть построен с таким же знанием целевого процессора, как и ядро. Вновь в игру вступает объект vermagic.o. При загрузке модуля ядро проверяет процессорно-зависимые параметры конфигурации для модуля и убеждается, что они соответствуют работающему ядру. Если модуль был скомпилирован с другими параметрами, он не загружается. Если вы планируете написать драйвер для общего распространения, вы вполне можете быть удивлены, как можно поддержать все эти различные варианты. Наилучший ответ, конечно, выпустить драйвер под GPL-совместимой лицензией и внести его в основное ядро. В противном случае, самым лучшим вариантом может быть распространение драйвера в форме исходников и набора скриптов для компиляции его на компьютере пользователя. Некоторые производители выпустили утилиты для упрощения этой задачи. Если вы должны распространять свой драйвер в бинарном виде, необходимо посмотреть на разные ядра, для которых вы будете распространять драйвер, и обеспечить версию модуля для каждого. Не забудьте принять во внимание ошибки ядер, которые могут быть исправлены после момента начала дистрибьюции. Затем существуют вопросы лицензирования, которые обсуждались в разделе "Лицензионное соглашение" в Главе 1. Как правило, дистрибьюция в исходной форме - наиболее простой способ проложить свой путь в мире. |
Предыдущая Содержание Следующая |