Драйвер устройства (или просто драйвер) -- это программа, обеспечивающая доступ к устройству. Разработка драйверов гораздо более сложная задача, чем написание обычных приложений. Поскольку любой динамически подгружаемый модуль драйвера присоединяется к ядру, малейшая ошибка в драйвере может вызвать крах системы. Распределение ресурсов -- одна из важнейших составляющих успеха для разработчиков драйверов. Ресурсы устройства -- это память для буферов ввода-вывода, векторы прерываний и порты ввода-вывода. Здесь будут рассмотрены способы безопасного распределения ресурсов для динамически загружаемых драйверов устройств в Linux.
Введение
В быстро развивающемся мире Информационных Технологий, постоянно появляются новые устройства и их разнообразие просто поражает. Доступ к периферии (жесткие диски, CD-ROM, терминалы, принтеры, сетевые адаптеры и пр.) осуществляется через подсистему ввода-вывода. Модули ядра, управляющие устройствами, называются "драйверы устройств". Подсистема ввода-вывода отвечает за перемещение данных между устройством и памятью. Все устройства ввода-вывода подразделяются на блочные и символьные, в зависимости от типа доступа. К символьным можно отнести клавиатуру, мышь, консоль и модем. Обмен с ними выглядит как поток байтов (символов). Блочные устройства (жесткие диски, CD-ROM и др.) передают и принимают данные целыми блоками.
Ядро взаимодействует с устройствами через соответствующие драйверы. Драйвер устройства -- это набор функций, используемых для его обслуживания. Одно из важнейших свойств Linux -- возможность динамической загрузки драйверов. При такой организации модуль драйвера становится частью ядра и может свободно обращаться к его функциям. Кроме того, динамически загруженный драйвер может быть так же динамически выгружен. Если драйвер не выгружен явным образом, то он остается в системе постоянно -- до следующей перезагрузки.
Как правило, работа драйвера заключается в передаче данных между компьютером и дополнительными устройствами. Ими могут быть устройства дополнительной памяти (например, Flash-модули прим. перев.), устройства связи, терминалы и т.п.. Возможны три варианта обработки ввода-вывода: PIO (PIO от англ. Programmed Input/Output -- режим программного ввода/вывода, когда вводом/выводом занимается процессор, что отнимает какую-то часть процессорного времени прим. перев.), ввод-вывод по прерываниям и DMA (DMA -- от англ. Direct Memory Access -- способ обмена данными между внешним устройством и памятью без участия процессора, что может заметно снизить на него нагрузку и повысить общую производительность системы прим. перев.). Передача данных между системой и устройствами может осуществляться двумя способами: через порты ввода-вывода и через отображаемую память. В этой статье обсуждаются основные положения организации ввода-вывода через отображаемую память и макроопределения, используемые при написании драйверов, для распределения областей памяти (идущие с примерами хорошо отлаженного кода). Поскольку драйвер является частью ядра, любая попытка вторгнуться в уже занятые адреса памяти приведет к краху системы. Таким образом, драйвер должен сначала проверить свободен ли заданный диапазон адресов и если нет, то вернуть управление системе с кодом ошибки. В противном случае этот диапазон резервируется за данным устройством.
Основы устройств ввода-вывода через отображаемую память
Драйверы накрепко связаны с аппаратурой обслуживаемого устройства. Они принимают на себя всю ответственность по взаимодействию CPU (CPU от англ. Central Processor Unit - Устройство Центрального Процессора, для архитектуры PC равносильно понятию "микропроцессор" прим. перев.) и устройства. Существует два основных способа передачи данных между устройством и ядром -- PIO и DMA. PIO задействует процессор, передача информации выполняется байт за байтом по мере готовности в процессе обработки прерываний, либо по опросу. Устройствам DMA передаются адрес источника (source address), адрес назначения (destination address) и размер блока данных. Устройство само перемещает данные в/из память(и). А после окончания передачи информации -- генерирует прерывание, чтобы уведомить ядро об окончании операции. Как правило, режим PIO используется для низкоскоростных устройств, таких как модемы, принтеры. И наоборот, для дисковых накопителей, графических терминалов используется режим DMA.
Для PIO-устройств есть два способа передачи данных. Выбор конкретного способа зависит от аппаратной архитектуры. Для архитектуры Intel x86 -- это порты ввода/вывода, для архитектуры Motorola 680x0 -- это ввод/вывод через отображаемую память. Кроме того, большинство устройств с шиной ISA (Industry Standard Architecture) поддерживают ввод/вывод через порты, в то время как устройства с шиной PCI (Peripheral Component Interconnect) поддерживают обмен через отображаемую память. Драйверу должны передаваться различные параметры, такие как адреса портов ввода/вывода или диапазон адресов в памяти. Иногда требуется передача драйверу дополнительных параметров, которые помогут ему обнаружить соответствующее устройство или включить/выключить некоторые специфические функции. В своей предыдущей статье на Linux Focus (http://linuxfocus.org/English/November2002/article264.meta.shtml), я рассказывал об основах управления устройствами и проблемах безопасного выделения портов ввода/вывода драйверам устройств в Linux. Так что снова к этой теме я возвращаться не буду. С точки зрения разработчика распределение отображаемой памяти для устройства во многом похоже на распределение портов ввода/вывода, поскольку внутренние механизмы распределения похожи друг на друга.
Макроопределения, используемые при выделении отображаемой памяти ввода/вывода
Проверка на занятость диапазона адресов выполняется следующим макросом:
int check_mem_region (unsigned long start, unsigned long length);
Где первый аргумент start -- начальный адрес, второй аргумент length -- размер блока памяти. Возвращаемое значение равно нулю, если диапазон адресов свободен, в противном случае возвращается значение меньше нуля.
Для "захвата" требуемого диапазона адресов отображаемой памяти используется макрос:
void request_mem_region (unsigned long start, unsigned long length, char *device_name);
Где char *device_name -- это название (имя) устройства, за которым резервируется требуемый диапазон памяти.
Перед тем как драйвер будет выгружен из памяти, он должен освободить память следующим макросом:
void release_mem_region (unsigned long start, unsigned long length);
Пример выделения области памяти ввода/вывода
#include static int Major, result; unsigned long start = 0, length = 0; MODULE_PARM (start, "l"); int Wipro_init (void) { result = check_mem_region (start, length); request_mem_region (start, length, "Wipro_device"); void Wipro_cleanup (void) { unregister_chrdev (Major, "Wipro_device"); module_init (Wipro_init); |
Первые четыре строки подключают заголовочные файлы, которые содержат объявления макроопределений и функций в ядре. Затем следуют объявления переменных. Макросы MODULE_PARM выполняют начальную установку переменных во время загрузки модуля. Этим макрокомандам передаются два параметра, первое -- имя переменной, второе -- тип переменной, в данном случае "l" означает long int. Сохраните исходный код примера в файле "io_mem.c".
В функциях Wipro_init и Wipro_cleanup производится инициализация и финализация (завершение работы) модуля драйвера. Функция Wipro_init сначала регистрирует устройство Wipro_device в системе, размещая старший номер устройства динамически. Затем она пробует зарезервировать заданную область памяти. Если заданный диапазон адресов уже занят, функция возвратит код ошибки, в противном случае она зарезервирует за устройством этот диапазон памяти. Функция Wipro_cleanup освобождает выделенную ранее память и возвращает старший номер устройства в систему (производит дерегистрацию устройства).
Скомпилируйте модуль. В результате должен получиться файл io_mem.o. На моей машине, с ядром 2.4, раскладка памяти и список устройств выглядят следующим образом:
$cat /proc/devices $cat /proc/iomem |
Загрузите модуль командой
$insmod ./io_mem.o start=0xeeee0000 length=0xeeee
Если загрузка прошла успешно, то можно увидеть, что устройство зарегистрировано под старшим номером 254 и за ним зарезервирован заданный диапазон памяти. Теперь перечень устройств и раскладка памяти выглядят следующим образом:
$cat /proc/devices $cat /proc/iomem |
Заключение
В этой статье мы обсудили важность безопасного выделения ресурсов для устройств с поддержкой ввода/вывода через отображаемую память в драйверах для Linux. Рассмотрели основы устройств ввода/вывода через отображаемую память и макрокоманды, используемые при распределении памяти. В качестве практического примера выделения ресурсов для устройства ввода/вывода через отображаемую память был приведен кусок кода из уже отлаженного драйвера. Был объяснен порядок регистрации устройства в системе и выделения ему диапазона адресов в памяти.
Благодарности
Я хотел бы выразить свою признательность Mr.V.Jayasurya и Dr. Sanjay Gupta, Talent Transformation, Wipro Technologies, India.Ссылки
1. Linux Device Drivers (2nd Edition), by Alessandro Rubini and Jonathan Corbet.Книга издательства O'Reilly :http://linux.oreilly.com/
[Online версию книги "Linux Device Drivers, 2nd Edition" вы найдёте по адресу http://www.xml.com/ldd/chapter/book/index.html. Прим.ред.]