Четверг, 2025-01-23, 1:15 AM
Статьи - HI - TECH
Приветствую Вас Гость | RSS
Главная страница Каталог статей Регистрация Вход
Меню сайта

Категории каталога
BIOS [9]
Статьи по BIOS
CD/DVD [6]
Статьи по CD/DVD
HDD - Жёсткие диски [8]
Статьи по HDD - жестким дискам
Модем [4]
Статьи по модемам
Блоки питания [5]
Статьи о блоках питания
Windows и общие вопросы [41]
Статьи по Windows и общим вопросам
Windows VISTA [8]
Статьи по Windos Vista
Linux [20]
Статьи по Linux

Наш опрос
Какую технологию вы предпочитаете?
Всего ответов: 250

Начало » Статьи » HI - TECH » Linux

The Linux Kernel Module Programming Guide
2.8. Сборка модулей под существующее ядро
Мы уже рекомендовали вам пересобрать свое ядро, включив некоторые полезные для отладки опции, например такие, как (MODULE_FORCE_UNLOAD) -- когда эта опция включена, то вы имеете возможность принудительной выгрузки модуля (посредством команды rmmod -f module_name), даже если ядро "считает" ваши действия небезопасными. Эта опция поможет вам сэкономить время на перезагрузках системы, в процессе отладки модуля.

Как бы то ни было, но ситуация может сложиться так, что вам потребуется загрузить модуль в ранее откомпилированное ядро, например, на другой системе, или в случае, когда вы не можете пересобрать ядро по каким либо соображениям. Если вы не предполагаете возникновение таких ситуаций, то можете просто пропустить эту часть главы.

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

insmod: error inserting 'your_module_name.ko': -1 Invalid module format

Более подробная информация будет помещена в файл /var/log/messages:

Jun 4 22:07:54 localhost kernel:
your_module_name: version magic '2.6.5-1.358custom 686
REGPARM 4KSTACKS gcc-3.3' should be
'2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'

Другими словами -- ваше ядро отказывается "принимать" ваш модуль из-за несоответствия версий (точнее -- из-за несоответствия сигнатур версий). Сигнатура версии сохраняется в объектном файле в виде статической строки, начинающейся со слова vermagic:. Эта строка вставляется во время компоновки модуля с файлом init/vermagic.o. Просмотреть сигнатуру версии (так же как и некоторые дополнительные сведения) можно посредством команды modinfo module.ko:

[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko
license: GPL
author: Peter Jay Salzman <p@dirac.org>
description: A sample driver
vermagic: 2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3
depends:

Для преодоления этого препятствия можно воспользоваться ключом --force-vermagic (команды modprobe, прим. перев.), но это решение потенциально опасно и совершенно неприменимо при распространении готовых модулей. Следовательно, вам придется пересобрать модуль в окружении идентичном тому, в котором было собрано целевое ядро. Вопрос: "Как это сделать?" и является темой для дальнейшего обсуждения в данной главе.

Прежде всего вам необходимо установить дерево с исходными текстами ядра той же версии, что и целевое ядро. Найдите файл конфигурации целевого ядра, как правило он располагается в каталоге /boot, под именем, что-то вроде config-2.6.x. Просто скопируйте его в каталог с исходными текстами ядра на своей машине.

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

VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 5
EXTRAVERSION = -1.358custom
...

Теперь запустите make, чтобы обновить информацию о версии:

[root@pcsenonsrv linux-2.6.x]# make
CHK include/linux/version.h
UPD include/linux/version.h
SYMLINK include/asm -> include/asm-i386
SPLIT include/linux/autoconf.h -> include/config/*
HOSTCC scripts/basic/fixdep
HOSTCC scripts/basic/split-include
HOSTCC scripts/basic/docproc
HOSTCC scripts/conmakehash
HOSTCC scripts/kallsyms
CC scripts/empty.o
...

Если вы не желаете полностью пересобирать ядро, то можете прервать процесс сборки (CTRL_C) сразу же после появления строки, начинающейся со слова SPLIT, поскольку в этот момент все необходимые файлы уже будут готовы. Перейдем в каталог с исходными текстами модуля и скомпилируем его. Теперь сигнатура версии модуля будет в точности соответствовать версии целевого ядра и будет загружено им без каких либо проблем.

Глава 3. Дополнительные сведения
3.1. Модули ядра и прикладные программы
Работа программы обычно начинается с исполнения функции main(). После выполнения всей последовательность команд программа завершает свою работу. Модули исполняются иначе. Они всегда начинают работу с исполнения функции init_module, или с функции, которую вы определили через вызов module_init. Это функция запуска модуля, которая подготавливает его для последующих вызовов. После завершения исполнения функции init_module модуль больше ничего не делает, он просто "сидит и ждет", когда ядро обратится к нему для выполнения специфических действий.

Вторая точка входа в модуль -- cleanup_module, вызывается непосредственно перед его выгрузкой. Она производит "откат" изменений, выполненных функцией init_module() и, как бы говорит ядру: "Я ухожу! Больше не проси меня ни о чем!".

Любой модуль обязательно должен иметь функцию инициализации и функцию завершения. Так как существует более чем один способ определить функции инициализации и завершения, я буду стараться использовать термины "начальная" и "конечная" функции, если я собьюсь и укажу названия init_module и cleanup_module, то думаю, что вы поймете меня правильно.

3.2. Функции, которые доступны из модулей
Как программист, вы знаете, что приложение может вызывать функции, которые не определены в самой программе. На стадии связывания (линковки) разрешаются все внешние ссылки, уходящие во внешние библиотеки. Функция printf -- одна из таких функций, которая определена в библиотеке libc.

Модули ядра в этом плане сильно отличаются от прикладных программ. В примере "Hello World" мы использовали функцию printk(), но не подключали стандартную библиотеку ввода-вывода. Модули так же проходят стадию связывания, но только с ядром, и могут вызывать только те функции, которые экспортируются ядром. Разрешение ссылок на внешние символы производится утилитой insmod. Если у вас есть желание взглянуть на список имен, экспортируемых ядром, загляните в файл /proc/kallsyms.

Здесь я хочу заострить ваше внимание на различиях между библиотечными функциями и системными вызовами. Библиотечные функции -- это верхний уровень, который работает в пространстве пользователя и обеспечивает более удобный интерфейс к функциям, которые выполняют основную работу -- системным вызовам. Системные вызовы работают в привилегированном режиме от имени пользователя и предоставляются самим ядром. Библиотечная функция printf() на первый взгляд выглядит как основная функция вывода, но все, что она фактически делает -- это формирует строку, в соответствии с заданным форматом, и передает ее низкоуровневому системному вызову write(), который и выводит строку на устройство стандартного вывода.

Как в этом можно убедиться? Да очень просто! Скомпилируйте следующую программу:

#include <stdio.h>
int main(void)
{
printf("hello");
return 0;
}

с помощью команды gcc -Wall -o hello hello.c и запустите ее командой strace hello. Впечатляет? Каждая строка, выводимая на экран, соответствует системному вызову. strace -- незаменимый инструмент для того, чтобы выяснить -- куда программа, пытается обратиться, включая такие сведения, как имена системных вызовов, передаваемые им аргументы и возвращаемые значения. Здесь вы должны увидеть строку, которая выглядит примерно так: write(1, "hello", 5hello). Это и есть то, что мы ищем. Т.е. скрытая от нас сторона вызова функции printf(). Возможно вы не знакомы с вызовом write(), поскольку большинство программистов предпочитает пользоваться стандартными библиотечными функциями (такими как fopen(), fputs(), fclose()). Если это так, тогда загляните в man 2 write. Второй раздел справочного руководства содержит описания системных вызовов (таких как kill(), read() и т.п.). В третьем разделе описываются библиотечные вызовы (такие как cosh(), random() и пр.).

Вы можете даже написать модули, которые подменяют системные вызовы ядра, вскоре мы продемонстрируем это. Взломщики довольно часто используют эту возможность для создания "черного хода" в систему или "троянов", но вы можете использовать ее в менее вредоносных целях, например заставить ядро выводить строку "Tee hee, that tickles!" ("Хи-хи, щекотно!") каждый раз, когда кто нибудь пробует удалить файл.

3.3. Пространство пользователя и пространство ядра
За доступ к ресурсам системы отвечает ядро, будь то видеоплата, жесткий диск или даже память. Программы часто конкурируют между собой за доступ к тем или иным ресурсам. Например, при подготовке этого документа, я сохраняю файл с текстом на жесткий диск, тут же стартует updatedb, чтобы обновить локальную базу данных. В результате мой vim и updatedb начинают конкурировать за обладание жестким диском. Ядро должно обслужить конкурирующие запросы, и "выстроить" их в порядке очередности. К тому же сам центральный процессор может работать в различных режимах. Каждый из режимов имеет свою степень "свободы" действий. Микропроцессор Intel 80386 имеет четыре таких режима, которые часто называют "кольцами". Unix использует только два из них: наивысший (нулевое кольцо, известное так же под названием привилегированный режим) и низший (пользовательский режим).

Вернемся к обсуждению библиотечных функций и системных вызовов. Как правило, программа обращается к библиотечным функциям, находясь в пользовательском режиме. Затем библиотечные функции обращаются к системным вызовам. Системные вызовы выступают от имени библиотечных функций, но работают в привилегированном режиме, так как они являются непосредственной частью ядра. Как только системный вызов завершает свою работу, он возвращает управление библиотечной функции и происходит обратный переход в пользовательский режим.

Обычно, о режимах исполнения, мы говорим как о пространстве ядра и пространстве пользователя. Эти два понятия охватывают не только два режима исполнения, но так же и то, что каждый из режимов имеет свое собственное отображение памяти -- свое собственное адресное пространство.

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

Основное назначение модулей -- расширение функциональности ядра. Код модуля исполняется в пространстве ядра. Обычно модуль реализует обе, рассмотренные выше задачи -- одни функции выполняются как часть системных вызовов, другие -- производят обработку прерываний.

3.4. Пространство имен
Когда вы пишете небольшую программку на C, вы именуете свои функции и переменные так, как вам это удобно. С другой стороны, когда вы разрабатываете некую программную единицу, входящую в состав большого программного пакета, любые глобальные переменные, которые вы вводите, становятся частью всего набора глобальных переменных пакета. В этой ситуации могут возникнуть конфликты имен. Внедрение большого количества имен функций и переменных, с глобальной областью видимости, значение которых не интутивно и трудноразличимо, приводит к "загрязнению" пространства имен. Программист, который работает с такими приложениями, тратит огромное количество умственных сил на то, чтобы запомнить "зарезервированные" имена и придумать свои уникальные названия.

Модули ядра компонуются с огромным программным пакетом -- ядром, поэтому проблема "загрязнения" пространства имен становится достаточно острой. Коллизии имен могут породить трудноуловимые ошибки, начиная от того, что модуль просто отказывается загружаться, и заканчивая весьма причудливыми сообщениями. Лучший способ избежать "загрязнения" пространства имен -- это объявлять все имена как static и использовать префиксы для придания уникальности именам с глобальной областью видимости. По соглашению об именовании, желательно, в качестве префиксов, использовать символы нижнего регистра. Если вы не можете какие-то имена объявить как static, то разрешить проблему можно посредством создания symbol table и регистрации ее в ядре. Эту тему мы обсудим ниже.

Файл /proc/kallsyms содержит все имена в ядре, с глобальной областью видимости, которые доступны для ваших модулей.

3.5. Адресное пространство
Управление памятью - очень сложная тема, она достаточно полно освещается в книге "Understanding The Linux Kernel", выпущенной издательством O'Reilly. Мы не собираемся делать из вас экспертов в области управления памятью, но вам действительно необходимо знать некоторые факты.

Если вы никогда не задумывалесь над тем, что означает слово segfault, то для вас скорее всего окажется сюрпризом тот факт, что указатели фактически не указывают на какой-то реальный участок физической памяти. В любом случе, эти адреса не являются реальными. Когда запускается процесс, ядро выделяет под него кусок физической памяти и передает его процессу. Эта память используется для размещения исполняемого кода, стека, переменных, динамической "кучи" и других вещей, о чем наверняка знают компьютерные гении. [3] Эта память начинается с логического адреса #0 и простирается до того адреса, который необходим. Поскольку области памяти, выделенные для разных процессов, не пересекаются, то каждый из процессов, обратившись к ячейке памяти с адресом, скажем 0xbffff978, получит данные из различных областей физической памяти! В даннм случае число 0xbffff978 можно рассматривать как смещение относительно начала области памяти, выделенной процессу. Как правило программы, подобные нашей "Hello World", не могут обратиться к памяти, занимаемой другим процессом, хотя существуют обходные пути, позволяющие добиться этого, но оставим пока эту тему для более позднего обсуждения.

Ядро тоже имеет свое собственное адресное пространство. Поскольку модуль по сути является частью ядра, то он так же работает в адресном пространстве ядра. Если ошибка segmentation fault, возникающая в приложении может быть отслежена и устранена без особых проблем, то в модуле подобная ошибка может стать фатальной для всей системы. Из-за незначительной ошибки в модуле вы рискуете "затоптать" ядро. Результат может быть самым плачевным. Поэтому будьте предельно внимательны!

Хотелось бы заметить, что это справедливо для любой операционной системы, которая построена на монолитном ядре. [4] Есть операционные системы, в основе которых лежит микроядро. В таких ОС каждый модуль получает свое адресное пространство. Примерами могут служить GNU Hurd и QNX Neutrino.

3.6. Драйверы устройств
Драйверы устройств являются одной из разновидностей модулей ядра. Они играют особую роль. Это настоящие "черные ящики", которые полностью скрывают детали, касающиеся работы устройства, и предоставляют четкий программный интерфейс для работы с аппаратурой. В Unix каждое аппаратное устройство представлено псевдофайлом (файлом устройства) в каталоге /dev. Этот файл обеспечивает средства взаимодействия с аппаратурой. Так, например, драйвер звуковой платы es1370.ko связывает файл устройства /dev/sound со звуковой платой Ensoniq IS1370. Пользовательское приложение, например mp3blaster может использовать для своей работы /dev/sound, ничего не подозревая о типе установленной звуковой платы.

3.6.1. Старший и младший номер устройства
Давайте взглянем на некоторые файлы устройств. Ниже перечислены те из них, которые представляют первые три раздела на первичном жестком диске:

# ls -l /dev/hda[1-3]
brw-rw---- 1 root disk 3, 1 Jul 5 2000 /dev/hda1
brw-rw---- 1 root disk 3, 2 Jul 5 2000 /dev/hda2
brw-rw---- 1 root disk 3, 3 Jul 5 2000 /dev/hda3

Обратили внимание на столбец с числами, разделенными запятой? Первое число называют "Старшим номером" устройства. Второе -- "Младшим номером". Старший номер говорит о том, какой драйвер используется для обслуживания аппаратного обеспечения. Каждый драйвер имеет свой уникальный старший номер. Все файлы устройств с одинаковым старшим номером управляются одним и тем же драйвером. Все из выше перечисленных файлов устройств имеют старший номер, равный 3, потому что все они управляются одним и тем же драйвером.

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

Устройства подразделяются на две большие группы -- блочные и символьные. Основное различие блочных и символьных устройств состоит в том, что обмен данными с блочным устройством производится порциями байт -- блоками. Они имеют внутренний буфер, благодаря чему повышается скорость обмена. В большинстве Unix-систем размер одного блока равен 1 килобайту или другому числу, являющемуся степенью числа 2. Символьные же устройства -- это лишь каналы передачи информации, по которым данные следуют последовательно, байт за байтом. Большинство устройств относятся к классу символьных, поскольку они не ограничены размером блока и не нуждаются в буферизации. Если первый символ в списке, полученном командой ls-l /dev, 'b', тогда это блочное устройство, если 'c', тогда -- символьное. Устройства, которые были приведены в примере выше -- блочные. Ниже приводится список некоторых символьных устройств (последовательные порты):

crw-rw---- 1 root dial 4, 64 Feb 18 23:34 /dev/ttyS0
crw-r----- 1 root dial 4, 65 Nov 17 10:26 /dev/ttyS1
crw-rw---- 1 root dial 4, 66 Jul 5 2000 /dev/ttyS2
crw-rw---- 1 root dial 4, 67 Jul 5 2000 /dev/ttyS3

Если вам интересно узнать, как назначаются старшие номера устройств, загляните в файл /usr/src/linux/documentation/devices.txt.

Все файлы устройств создаются в процессе установки системы с помощью утилиты mknod. Чтобы создать новое устройство, например с именем "coffee", со старшим номером 12 и младшим номером 2, нужно выполнить команду mknod /dev/coffee c 12 2. Вас никто не обязывает размещать файлы устройств в каталоге /dev, тем не менее, делается это в соответствии с принятыми соглашениями. Однако, при разработке драйвера устройства, на период отладки, размещать файл устройства в своем домашнем каталоге -- наверное не такая уж и плохая идея. Единственное -- не забудьте исправить место для размещения файла устройства после того, как отладка будет закончена.

Еще несколько замечаний, которые явно не касаются обсуждаемой темы, но которые мне хотелось бы сделать. Когда происходит обращение к файлу устройства, ядро использует старший номер файла, для определения драйвера, который должен обработать это обращение. Это означает, что ядро в действительности не использует и даже ничего не знает о младшем номере. Единственный, кто обеспокоен этим -- это сам драйвер. Он использует младший номер, чтобы отличить разные физические устройства.

Между прочим, когда я говорю "устройства", я подразумеваю нечто более абстрактное чем, скажем, PCI плата, которую вы можете подержать в руке. Взгляните на эти два файла устройств:

% ls -l /dev/fd0 /dev/fd0u1680
brwxrwxrwx 1 root floppy 2, 0 Jul 5 2000 /dev/fd0
brw-rw---- 1 root floppy 2, 44 Jul 5 2000 /dev/fd0u1680

К настоящему моменту вы можете сказать об этих файлах устройств, что оба они - блочные устройства, что обслуживаются одним и тем же драйвером (старший номер 2). Вы можете даже заявить, что они оба представляют ваш дисковод для гибких дисков, несмотря на то, что у вас стоит только один дисковод. Но почему два файла? А дело вот в чем, один из них представляет дисковод для дискет, емкостью 1.44 Мб. Другой -- тот же самый дисковод, но для дискет емкостью 1.68 Мб, и соответствует тому, что некоторые люди называют "суперотформатированным" диском ("superformatted" disk). Такие дискеты могут хранить больший объем данных, чем стандартно-отформатированная дискета. Вот тот случай, когда два файла устройства, с различным младшими номерами, фактически представляют одно и то же физическое устройство. Так что, слово "устройство", в нашем обсуждении, может означать нечто более абстрактное.

Глава 4. Файлы символьных устройств
4.1. Структура file_operations
Структура file_operations определена в файле linux/fs.h и содержит указатели на функции драйвера, которые отвечают за выполнение различных операций с устройством. Например, практически любой драйвер символьного устройства реализует функцию чтения данных из устройства. Адрес этой функции, среди всего прочего, хранится в структуре file_operations. Ниже приводится определение структуры, взятое из исходных текстов ядра 2.6.5:

struct file_operations {
struct module *owner;
loff_t(*llseek) (struct file *, loff_t, int);
ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t(*write) (struct file *,
const char __user *, size_t, loff_t *);
ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t,
loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int,
unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t(*readv) (struct file *, const struct iovec *, unsigned long,
loff_t *);
ssize_t(*writev) (struct file *, const struct iovec *, unsigned long,
loff_t *);
ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t,
void __user *);
ssize_t(*sendpage) (struct file *, struct page *, int, size_t,
loff_t *, int);
unsigned long (*get_unmapped_area) (struct file *, unsigned long,
unsigned long, unsigned long,
unsigned long);
};

Драйвер зачастую реализует далеко не все функции, предусмотренные данной структурой. Например, драйвер, который обслуживает видеоплату, не обязан выполнять операцию чтения каталога (readdir). Поля структуры, соответствующие нереализованным функциям, заполняются "пустыми" указателями -- NULL.

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

struct file_operations fops = {
read: device_read,
write: device_write,
open: device_open,
release: device_release
};

Однако, существует еще один способ заполнения структур, который описывается стандартом C99. Причем этот способ более предпочтителен. gcc 2.95, который я использую, поддерживает синтаксис C99. Вам так же следует придерживаться этого синтаксиса, если вы желаете обеспечить переносимость своему драйверу:

struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};

На мой взгляд все выглядит достаточно понятным. И еще, вы должны знать, что в любое поле структуры, которое вы явно не инициализируете, компилятор gcc запишет "пустой" указатель -- NULL. Указатель на struct file_operations обычно именуют как fops.

4.2. Структура file
Каждое устройство представлено в ядре структурой file, которая определена в файле linux/fs.h. Эта структура используется исключительно ядром и никогда не используется прикладными программами, работающими в пространстве пользователя. Это совершенно не то же самое, что и FILE, определяемое библиотекой glibc и которое в свою очередь в ядре нигде не используется. Имя структуры может ввести в заблуждение, поскольку она представляет абстракцию открытого файла, а не файла на диске, который представляет структура inode.

Как правило указатель на структуру file называют filp.

Загляните в заголовочный файл и посмотрите определение структуры file. Большинство имеющихся полей структуры, например struct dentry *f_dentry, не используются драйверами устройств, и вы можете игнорировать их. Драйверы не заполняют структуру file непосредственно, они только используют структуры, содержащиеся в ней.

4.3. Регистрация устройства
Как уже говорилось ранее, доступ к символьным устройствам осуществляется посредством файлов устройств, которые как правило располагаются в каталоге /dev. [5] Старший номер устройства говорит о том, какой драйвер с каким файлом устройства связан. Младший номер используется самим драйвером для идентификации устройства, если он обслуживает несколько таких устройств.

Добавление драйвера в систему подразумевает его регистрацию в ядре. Это означает -- получение старшего номера в момент инициализации модуля. Получить его можно вызовом функции register_chrdev(), определенной в файле linux/fs.h:

int register_chrdev(unsigned int major,
const char *name, struct file_operations *fops);

где unsigned int major -- это запрашиваемый старший номер устройства, const char *name -- название устройства, которое будет отображаться в /proc/devices и struct file_operations *fops -- указатель на таблицу file_operations драйвера. В случае ошибки, функция register_chrdev() возвращает отрицательное число. Обратите внимание: функции регистрации драйвера не передается младший номер устройства. Все потому, что ядро не обслуживает его -- это прерогатива драйвера.

А теперь вопрос: Как получить старший номер для своего устройства, чтобы случайно не "занять" уже существующий? Самый простой способ -- заглянуть в файл Documentation/devices.txt и выбрать один из неиспользуемых. Но это не самый лучший выход, потому что вы никогда не будете уверены в том, что выбранный вами номер не будет позднее официально связан с каким-либо другим устройством. Правильный ответ -- "попросить" ядро выделить вам динамический номер устройства.

Если вы передадите функции register_chrdev(), в качестве старшего номера, число 0, то возвращаемое положительное значение будет представлять собой, динамически выделенный ядром, старший номер устройства. Один из неприятных моментов здесь состоит в том, что вы заранее не можете создать файл устройства, поскольку старший номер устройства вам заранее не известен. Тем не менее, можно предложить ряд способов решения этой проблемы.

Драйвер может выводить сообщение в системный журнал (как это делает модуль "Hello World"), а вы затем вручную создадите файл устройства.

Для вновь зарегистрированного устройства, в файле /proc/devices появится запись. Вы можете найти эту запись и вручную создать файл устройства или можно написать небольшой сценарий, который выполнит эту работу за вас.

Можно "заставить" сам драйвер создавать файл устройства, с помощью системного вызова mknod, после успешной регистрации. А внутри cleanup_module() предусмотреть возможность удаления файла устройства с помощью rm.

4.4. Отключение устройства
Мы не можем позволить выгружать модуль по прихоти суперпользователя. Если файл устройства удалить после того как он будет открыт процессом, то может возникнуть ситуация когда процесс попытается обратиться к выгруженному драйверу (в конце концов процесс даже не подозревает, что такое могло произойти). В результате произойдет попытка обращения к тому участку памяти, где ранее находилась функция обработки запроса. Если вам повезет, то этот участок памяти окажется не затертым ядром и вы получите сообщение об ошибке. Если не повезет -- то произойдет переход в середину "чужой" функции. Результат такого "вызова" трудно предугадать заранее

Обычно, если какая-то операция должна быть отвергнута, функция возвращает код ошибки (отрицательное число). В случае с функцией cleanup_module() это невозможно, поскольку она не имеет возвращаемого значения. Однако, для каждого модуля в системе имеется счетчик обращений, который хранит число процессов, использующих модуль. Вы можете увидеть это число в третьем поле, в файле /proc/devices. Если это поле не равно нулю, то rmmod не сможет выгрузить модуль. Обратите внимание: вам нет нужды следить за состоянием счетчика в cleanup_module(), это делает система, внутри системного вызова sys_delete_module (определение функции вы найдете в файле linux/module.c). Вы не должны изменять значение счетчика напрямую, тем не менее, ядро предоставляет в ваше распоряжение функции, которые увеличивают и уменьшают значение счетчика обращений:

try_module_get(THIS_MODULE): увеличивает счетчик обращений на 1.

try_module_put(THIS_MODULE): уменьшает счетчик обращений на 1.

Очень важно сохранять точное значение счетчика! Если Вы каким-либо образом потеряете действительное значение, то вы никогда не сможете выгрузить модуль. Тут, милые мои мальчики и девочки, поможет только перезагрузка! Это обязательно случиться с вами, рано или поздно, при разработке какого-либо модуля!

4.5. chardev.c
Следующий пример создает устройство с именем chardev. Вы можете читать содержимое файла устройства с помощью команды cat или открывать его на чтение из программы (функцией open()). Посредством этого файла драйвер будет извещать о количестве попыток обращения к нему. Модуль не поддерживает операцию записи (типа: echo "hi" > /dev/chardev), но определяет такую попытку и сообщает пользователю о том, что операция записи не поддерживается.

Пример 4-1. chardev.c

/*
* chardev.c: Создает символьное устройство,
* доступное только для чтения
* возвращает сообщение, с указанием количества произведенных
* попыток чтения из файла устройства
*/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h> /* определение функции put_user */

/*
* Прототипы функций, обычно их выносят в заголовочный файл (.h)
*/
int init_module(void);
void cleanup_module(void);
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *,
const char *, size_t, loff_t *);

#define SUCCESS 0
/* Имя устройства, будет отображаться в /proc/devices */
#define DEVICE_NAME "chardev"
#define BUF_LEN 80 /* Максимальная длина сообщения */

/*
* Глобальные переменные, объявлены как static,
* воизбежание конфликтов имен.
*/

/* Старший номер устройства нашего драйвера */
static int Major;
/* Устройство открыто?
static int Device_Open = 0;
* используется для предотвращения одновременного
* обращения из нескольких процессов */
/* Здесь будет собираться текст сообщения */
static char msg[BUF_LEN];
static char *msg_Ptr;

static struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};

/*
* Функции
*/

int init_module(void)
{
Major = register_chrdev(0, DEVICE_NAME, &fops);

if (Major < 0) {
printk("Registering the character device failed with %d\n",
Major);
return Major;
}

printk("<1>I was assigned major number %d. To talk to\n", Major);
printk("<1>the driver, create a dev file with\n");
printk("'mknod /dev/chardev c %d 0'.\n", Major);
printk("<1>Try various minor numbers. Try to cat and echo to\n");
printk("the device file.\n");
printk("<1>Remove the device file and module when done.\n");

return 0;
}

void cleanup_module(void)
{
/*
* Отключение устройства
*/
int ret = unregister_chrdev(Major, DEVICE_NAME);
if (ret < 0)
printk("Error in unregister_chrdev: %d\n", ret);
}

/*
* Обработчики
*/

/*
* Вызывается, когда процесс пытается
* открыть файл устройства, например командой
* "cat /dev/chardev"
*/
static int device_open(struct inode *inode, struct file *file)
{
static int counter = 0;
if (Device_Open)
return -EBUSY;
Device_Open++;
sprintf(msg, "I already told you %d times Hello world!\n", counter++);
msg_Ptr = msg;
try_module_get(THIS_MODULE);

return SUCCESS;
}

/*
* Вызывается, когда процесс закрывает файл устройства.
*/
static int device_release(struct inode *inode, struct file *file)
{
Device_Open--; /* Теперь мы готовы обслужить другой процесс */

/*
* Уменьшить счетчик обращений, иначе, после первой
* же удачной попытки открыть файл устройства,
* вы никогда не сможете выгрузить модуль.
*/
module_put(THIS_MODULE);

return 0;
}

/*
* Вызывается, когда процесс пытается
* прочитать уже открытый файл устройства
*/
/* см. include/linux/fs.h */
static ssize_t device_read(struct file *filp,
/* буфер, куда надо положить данные */
char *buffer,
/* размер буфера */
size_t length,
loff_t * offset)
{
/*
* Количество байт, фактически записанных в буфер
*/
int bytes_read = 0;

/*
* Если достигли конца сообщения,
* вернуть 0, как признак конца файла
*/
if (*msg_Ptr == 0)
return 0;

/*
* Перемещение данных в буфер
*/
while (length && *msg_Ptr) {

/*
* Буфер находится в пространстве
* пользователя (в сегменте данных),
* а не в пространстве ядра, поэтому
* простое присваивание здесь недопустимо.
* Для того, чтобы скопировать данные,
* мы используем функцию put_user,
* которая перенесет данные из пространства
* ядра в пространство пользователя.
*/
put_user(*(msg_Ptr++), buffer++);

length--;
bytes_read++;
}

/*
* В большинстве своем, функции чтения
* возвращают количество байт, записанных в буфер.
*/
return bytes_read;
}

/*
* Вызывается, когда процесс пытается записать в устройство,
* например так: echo "hi" > /dev/chardev
*/
static ssize_t
device_write(struct file *filp, const char *buff, size_t len, loff_t * off)
{
printk("<1>Sorry, this operation isn't supported.\n");
return -EINVAL;
}

Пример 4-2. Makefile

obj-m += chardev.o

4.6. Создание модулей для работы с разными версиями ядра
Системные вызовы, которые суть есть основной интерфейс с ядром, как правило не изменяют свой синтаксис вызова от версии к версии. В ядро могут быть добавлены новые системные вызовы, но старые, практически всегда, сохраняют свое поведение, независимо от версии ядра. Делается это с целью сохранения обратной совместимости, чтобы не нарушить корректную работу ранее выпущенных приложений. В большинстве случаев, файлы устройств также останутся теми же самыми. С другой стороны, внутренние интерфейсы ядра могут изменяться от версии к версии.

Версии ядра подразделяются на стабильные (n.<четное_число>.m) и нестабильные (n.<нечетное_число>.m). Нестабильные версии несут в себе самые новые наработки, включая те, которые будут считаться ошибкой и те, которые претерпят существенные изменения в следующей версии. В результате, вы не можете доверять тому или иному интерфейсу, поскольку он может еще измениться (по этой причине я не посчитал нужным описывать их в этой книге -- слишком много работы, к тому же изменения происходят слишком быстро). От стабильных версий мы можем ожидать, что интерфейсы останутся неизменными, независимо от версии релиза (последнее число в номере версии -- m).

Итак, мы уже поняли, что между разными версиями ядра могут существовать весьма существенные отличия. Если у вас появится необходимость в создании модуля, который мог бы работать с разными версиями ядра, то можете воспользоваться директивами условной компиляции, основываясь на сравнении макроопределений LINUX_VERSION_CODE и KERNEL_VERSION. Для версии a.b.c, макрос KERNEL_VERSION вернет код версии, вычисленный в соответствии с выражением: 2^{16}a+2^{8}b+c. Макрос LINUX_VERSION_CODE возвращает текущую версию ядра.

В предыдущих версиях данного руководства, довольно подробно описывалось, как писать обратно совместимый код, с использованием директив условной компиляции. Но, начиная с этой версии, мы решили порвать с устоявшейся традицией. Теперь, если вы желаете писать модули под определенные версии ядра, обращайтесь к соответствующей версии руководства (LKMPG). Мы решили выпускать этот документ под версиями (номер версии и номер подверсии), совпадающими с версиями обсуждаемого ядра. Таким образом, разработчики, работающие под ядро 2.4.x, должны обращаться к LKMPG версии 2.4.x, работающие под ядро 2.6.x -- к LKMPG версии 2.6.x и т.д.

Категория: Linux | Добавил: webmaster (2006-12-15)
Просмотров: 425 | Рейтинг: 0.0 |

Всего комментариев: 0
Имя *:
Email *:
Код *:
Форма входа

Сервисы

Поиск по каталогу

Друзья сайта

| Ссылки 1 | Ссылки 2 | Ссылки 3 |
www.webmaster.clan.su Каталог+поисковая система be number one Bakililar.az Top Sites Сервис авто регистрации в
каталогах, статьи про раскрутку сайтов, web дизайн, flash, 
photoshop, хостинг, рассылки; форум, баннерная сеть, каталог 
сайтов, услуги продвижения и рекламы сайтов Скрипт для определения тиц (Яндекс CY: индекс цитирования). Определение pr (Google Pagerank). Проверить тиц pr сайта.
Copyright WebMaster.Clan © 2006 Бесплатный хостинг uCoz