ГЛАВНАЯ МАН KDE СЗИ ПРОГРАММИРОВАНИЕ
Home Home

На главную страницу

Создание драйверов для ОС Linux


  1. Введение

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

Символьные устройства

Доступ к символьным устройствам похож на доступ к файлам, а задача драйвера символьного устройства - обеспечивать этот доступ. Символьные драйвера обычно способны обрабатывать такие системные вызовы как open, close, read, write. Примером устройств такого типа могут служить консоль и параллельные порты, поскольку с ними хорошо вести потоковый обмен информацией. Доступ к символьным устройствам может быть получен через файловую систему, например через /dev/tty1 или /dev/lp1.
Единственное отличие символьных устройств от обычных файлов - возможность всегда вернуться к уже просмотренной информации и пройтись вперёд, тогда как большинство символьных устройств функционируют как каналы данных, к которым вы можете обращаться только последовательно. Тем не менее существуют устройства выглядящие как область данных, по которой можно преспокойно продвигаться и вперёд и назад. Символьные устройства - самые распространённые из всех периферийных устройств и при разработке драйверов вам чаще всегопридётся столкиваться именно с ними.

Блочные устройства

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

Сетевые устройства

Не являясь потоково-ориентированными, сетевые устройства не так просто отобразить на файловую систему, как например /dev/tty1. В ОС Linux сетевым устройствам присваивается лишь уникальное имя (например eth0), поскольку в файловой системе для них нет соответствующего файла. Взаимодействие между ядром и сетевым драйвером в корне отличается от взаимодействия с драйвером блочного или символьного устройсва - вместо системных вызовов read и write ядро вызывает функции, относящиеся к передаче пакетов.

SCSI-устройства

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

  2. Методы включения драйверов в ядро

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

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

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

Динамическая установка драйвера в ядро операционной системы требует выполения следующих действий:

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

Резюмируя всё вышеизложенное, можно сказать, что любой код, который может быть добавлен к ядру, называется модулем. Ядро ОС Linux предоставляет возможность использования любого типа (или класса) модулей, включая в том числе и драйверы устройств. Каждый модуль состоит из некоторого объектного кода, который может быть динамически пристыкован к ядру посредством программы insmod и удалён с помощью rmmod.

Команда insmod связывет модуль с ядром во время работы системы. Если имя загружаемого модуля указано без директорий, insmod будет искать модуль в нескольких каталогах по умолчанию. Для изменения пути поиска необходимо задать переменную окружения MODPATH. Если же существует файл конфигурации модулей (как правило /etc/modules.conf или /etc/conf.modules), то модуль будет искаться в каталогах, указанных в этом файле. Также в файле конфигурации можно задавать псевдонимы для устройств, добавив в него строку:

alias iso9660 isofs

можно использовать команду insmod iso9660, несмотря на то, что соответствующего файла этого устройства нет. Файл конфигурации имеет достаточно гибкую структуру, позволяющую полностью контролировать процесс загрузки и выгрузки модулей; для его более углублённого изучения можно порекомендовать электронную интерактивную систему помощи ОС Linux (man 5 modules.conf).

Команда rmmod выгружает модуль, установленный ранее командой insmod, причём удаляться могут только те модули, на которые больше нет ссылок (ни одна программа не работает больше с этим модулем).

  3. Отличие драйверов от обычных приложений

В то время как обычные приложения представляют собой целостную задачу, выполняющуюся от начало и до конца, модуль сначала регистрирует себя для обслуживания будующих запросов и его "главная" функция завершается немедленно. Другими словами, задача функции init_module (точка входа модуля) состоит в приготовлении к последующему применению функций модуля; это выглядит так, как будто модуль сообщает системе: "Вот он я и вот что я могу делать". Вторая функция модуля, cleanup_module, вызывается непосредственно перед выгрузкой модуля. Она должна сообщить системе: "Меня больше нет, не давайте мне никаких заданий". Предполагается, что функция cleanup_module выполняет откат (восстанавливает ситуацию) в отношении тех действий, которые были выполнены функцией init_module. Таким образом модуль может быть безопасно выгружен. Способность выгрузки - одна из наиболее ценных возможностей для разработчика, потому что она значительно сокращает время на отладку - чтобы протестировать новую версию своего драйвера нет необходимости в продолжительной перезагрузки машины.

Язык Си позволяет использовать функции, которые не были определены в программе: при компилировании программы все подобные ссылки разрешаются через соответствующие библиотеки функций. printf, например, определён в стандартной библиотеке libc. Модуль, с другой стороны, может использовать функции, определённые только в самом ядре. Функция printk является версией printf, определённой в ядре, и поэтому может использоваться в модулях. Она ведёт себя аналогично оригиналу, но без поддержки вещественной арифметики.

Поскольку никаких библиотек к модулям не подключается, исходники никогда не должны включать обычные заголовочные файлы. Всё, что касается ядра определено в заголовочных файлах, которые могут быть найдены в каталогах include/linux, include/asm, include/net и include/scsi.

Модули ядра также должны отличаться от обычных программ более бережным отношением к пространству имён. При написании небольших приложений программисты мало задумываются о переменных и функциях, которым дают имена, что вызывает проблемы когда эти программы пытаются вставить в часть большого проекта. Разработчики, призванные заниматься подобными проектами затрачивают добрую часть своей работы только на запоминание всех этих "зарезервированных" имён и на поиск новых.

Полностью избежать подобных проблем нельзя, поскольку предугадывать, есть ли такое имя в ядре или нет, довольно обременительное занятие. Лучшим решением для этой проблемы является декларирование всех имён как static (не допускающее выхода имён за пределы функций) и использование тщательно продуманных префиксов для символов, которые вы собираетесь оставить глобальными. Использование префиксов даже для собственных имён в модуле может иногда помочь при отладке. По негласному соглашению префиксы используемые в ядре записываются в нижнем регистре и имеют длину 2 байта. Так, например, драйвер виртуальной памяти ядра /dev/kmem имеет префикс mm. Таким образом функции этого драйвера будут иметь названия mmopen, mmclose, mmread и mmwrite.

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

  4. Файлы устройств

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

Назначением файлов устройств является обеспечение связи процессов с драйверами устройств в составе ядра, а через них - установление связи с физическими устройствами. Достижение этой цели реализуется следующим образом.

Каждому драйверу устройства, отвечающему за работу с определённым типом аппаратных средств, назначается свой собственный старший (major) номер. Список драйверов и их старших номеров доступен в /proc/devices. Каждому физическому устройству, которое управляется данным драйвером назначается младший (minor) номер. В системе поддерживается каталог /dev для хранения имён специальных файлов, называемых файлами устройств. Каждый такой файл относится к некоторому устройству, независимо от того, инсталлировано оно или нет.

Например, если выполнить команду ls -l /dev/hd[ab]*, то можно получить информацию о всех разделах жестких IDE-дисков, которые могут быть подсоединены к машине. Заметим при этом, что для каждого из разделов используется один и тот же старший номер 3, а вот младший номер у всех разный. Все эти устройства были созданы с помощью команды mknod во время загрузки системы.

  5. Функции драйвера устройства

Драйвер устройства должен обрабатывать несколько функций, вызываемых, когда кто-либо пытается выполнить некоторые действия с соответствующим ему файлом устройства. Обращение к функциям происходит через структуру file_operations (описанную в файле include/linux/fs.h), которая создаётся при регистрации устройства и содержит в своём составе указатели на эти функции:

Рассмотрим каждую из этих функций подробнее:

lseek

Функция lseek вызывается, когда в специальном файле, представляющем устройство, появляется системный вызов lseek. Это функция перехода указателя файла на заданное смещение.

Аргументы функции:

lseek возвращает -errno в случае ошибки или положительное смещение после выполнения.

read и write

Функции read и write осуществляют обмен информацией с устройством, читая и записывая в него строку символов. Если функции read и write отсутствуют в структуре file_operatios, определенной в ядре, то в случае символьного устройства одноименные вызовы будут возвращать -EINVAL.

Аргументы функций:

readdir

Еще один элемент структуры, используемый для описания файловых систем так же, как драйверы устройств. Функция не нуждается в предопределении. Ядро возвращает -ENOTDIR в случае вызова readdir из специального файла устройства.

select

Функция select обычно используется для многократного чтения без использования последовательного вызова функций. Приложение делает системный вызов select, задавая ему список дескрипторов файлов. Затем ядро сообщает программе, при просмотре какого дескриптора она была активизирована. Также select иногда используется как таймер.

Аргументы функции:

ioctl

Функция ioctl осуществляет функцию передачи контроля ввода/вывода. Структура функции должна быть следующей: первичная проверка ошибок, затем переключение, дающее право контролировать все операции ввода/вывода. Номер ioctl находится в аргументе cmd, аргумент контролируемой команды находится в arg. Для работы с ioctl необходимо иметь подробное представление о контроле над вводом/выводом.

Аргументы функции:

Возвращаемое значение: -errno в случае ошибки, все другие значения определяются пользователем.

mmap

Аргументы функции:

open и release

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

Функция release вызывается лишь тогда, когда процесс закрывает последний файловый дескриптор. release может переустанавливать бит "занято". После вызова release, вы можете очистить куски выделенной kmalloc памятью под очереди процессов.

Аргументы функции:

  6. Встраивание драйверов в ядро

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

Для символьно-ориентированных драйверов надо положить файлы в каталог drivers/char, а в Makefile добавить следующие строки:

в секцию определений. После этого драйвер будет автоматически присоединён к ядру, конечно если определена переменная CONFIG_MYDRIVER. Для определения этой переменной необходимо найти файл Config.in в том же каталоге или в каталоге исходников ядра и добавить в него строку

bool 'My Driver Support' CONFIG_MYDRIVER n

При конфигурировании ядра ответив на этот вопрос положительно, вы получите ядро со встроенной поддержкой вашего драйвера.

Также необходимо, чтобы драйвер был зарегистрирован в системе, а для этого вставьте в файл init/main.c, в область инициализации драйверов строки:

Приведём простой пример драйвера, встраиваемого в ядро:

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

  7. Динамически устанавливаемые драйверы

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

Приведённая ниже программа hello.c - простой "Hello, world" модуль (который по большому счёту ничего и не делает).

Модуль ядра не является независимой исполняемой программой. Это объектный файл, который будет связан с ядром во время работы. Поэтому модули ядра следует компилировать с флагом -c. Кроме того, при компиляции модулей необходимо устанавливать ряд предопределённых ключей:

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

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

Есть и другие ключи, которые можно включать в зависимости от использованных флагов при компиляции ядра. Если вас интересует, как было скомпилировано ядро, то следует обратиться к файлу include/linux/config.h.

Скомпилировав модуль командой make мы получим объектный файл hello.o, который может быть теперь установлен в систему. Проверить работу модуля можно вызывая системные утилиты insmod и rmmod, как показано ниже.

Попутно заметим, что только пользователь root может загружать и выгружать модули.

  8. Передача параметров

  Передача параметров драйверам, встраиваемым в ядро

ОС Linux даёт возможность передавать драйверам устройств параметры как во во время загрузки ядра, так и жестко задавать во время компиляции. В первом случае это можно сделать, например, через LILO:

LILO boot: Linux mydriver=0x240,4

Обработать эти параметры можно с помощью следующей процедуры:

Для того чтобы связать параметр "mydriver=" и процедуру mydriver_setup, нужно модифицировать массив bootsetups[] в init/main.c: Заметьте, что процедура настройки вызывается раньше процедур инициализации. Этот механизм часто используется драйверами для установки линий IRQ и базового адреса устройства.

  Передача параметров динамически устанавливаемым драйверам

Значения параметров могут быть заданы во время загрузки программой insmod, которая поддерживает как целые числа, так и строковые константы. Эта программа может модифицировать все переменные, определённые в модуле. Например, если исходник содержит следующие значения: то следующая комманда может быть использована для загрузки модуля:

root# insmod skull skull_ival=666 skull_sval='the beast'

Следует заметить, что программа insmod может присваивать значения любым переменным модуля, вне зависимости от их типа (статические или глобальные) и даже от того, входят ли они в таблицу имён или нет.

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

  9. Пример реализации символьного драйвера

Рассмотрим пример реализации символьного драйвера в виде динамически устанавливаемого модуля. Модуль разделён на две части. В первой производится регистрация устройства, а во второй находится сам драйвер. Функция init_module вызывает module_register_chrdev, чтобы добавить драйвер устройства в таблицу символьных драйверов устройств ядра. Кроме того, функция возвращает старший номер, который будет использоваться при работе с драйвером. Функция cleanup_module, как уже упомяналось, снимает драйвер с регистрации.

Действия по регистрации и снятие с регистрации - обычная операция для этих двух функций. Б?льшая часть из того, что находится в составе ядра, никогда не запускается по собственной инициативе (как это происходит с процессами). Запуск производится либо процессами с помощью системных вызовов, либо аппаратурой с помощью прерываний, либо другими частями ядра (просто вызовом специальных функций). В результате при добавлении некоторого кода в состав ядра предполагается, что вы регистрируете его как некий обработчик для событий определённого типа. Когда код удаляется из ядра, предполагается, что вы снимаете его с регистрации.

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

Ещё один момент о котором надо помнить: нельзя выпонять команду rmmod в произвольные моменты времени. Причина заключается следующем. Если файл устройства открыт процессом и потом модуль ядра удаляется, то при последующем использовании к файлу обращение будет производится по адресу, где должна находится соответствующая функция обработчика, что приведёт к непредсказуемым последствиям, явно отрицательного характера. Стандартный метод решения этой проблемы заключается в использовании счётчика использования (счётчика ссылок). Значением счётчика является количество других модулей, использующих данный. Этот счётчик представлен последним числом в каждой строке файла /proc/modules. Если это число не равно нулю, то rmmod не сможет выгрузить данный модуль.

Счётчик ссылок доступен через маскросы MOD_INC_USE_COUNT и MOD_DEV_USE_COUNT, MOD_IN_USE.

После того как программа скомпилирована и загружена insmod'ом в систему, нам необходимо создать файл устройства, для которого она написана:

root# mknod foo 13 1

Здесь 13 - старший номер устройства, зарегистрированного в системе, выводится при установке модуля. 1 - младший номер устройства.


На главную страницу
Hosted by uCoz