ГЛАВНАЯ | МАН | KDE | СЗИ | ПРОГРАММИРОВАНИЕ |
![]() ![]() |
|
| |||
1. Введение |
2. Методы включения драйверов в ядро |
Традиционно для встраивания драйвера в ядро ОС Linux требуется перекомпиляция ядра и перезапуск системы. Принципиально эта процедура не отличается от компиляции обычной программы - все компоненты ядра являются объектными модулями и редактор связей объединяет их с объектным модулем драйвера для получения исполняемого файла. В этом случае драйвер встраивается в ядро статически, т. е. независимо от фактического наличия устройства в системе и ряда других причин код и данные драйвера будут присутствовать в ядре ОС Linux до следующей перекомпиляции.
Однако тенденция развития современных операционных систем заключается в предоставлении возможности динамического расширения функциональности ядра. Не является исключением и ОС Linux. Это, в частности, относится к файловой системе, драйверам устройств и сетевым протоколам. Возможность работы с новыми перифирийными устройствами без необходимости перекомпиляции ядра обеспечивается загружаемыми драйверами устройств. Вместо того чтобы встраивать модуль драйвера, основываясь на статических таблицах и интерфейсах, ядро содержит набор функций, позволяющих загрузить необходимые драйверы и, соответственно, выгрузить их, когда необходимость работы с данным устройством отпадёт. При этом структуры данных для доступа к драйверам устройств также являются динамическими.
Динамическая установка драйвера в ядро операционной системы требует выполения следующих действий:
1. Размещение и динамическое связывание символов драйвера. Эта операция аналогична загрузке динамических библиотек и выполняется специальным загрузчиком. |
2. Инициализация драйвера и устройства. |
3. Добавление точек входа драйвера в соответствующий коммутатор устройств, отвечающий за выбор требуемого драйвера в момент обращения к устройству. |
4. Установка обработчика прерываний драйвера. |
Резюмируя всё вышеизложенное, можно сказать, что любой код, который может быть добавлен к ядру, называется модулем. Ядро ОС Linux предоставляет возможность использования любого типа (или класса) модулей, включая в том числе и драйверы устройств. Каждый модуль состоит из некоторого объектного кода, который может быть динамически пристыкован к ядру посредством программы insmod и удалён с помощью rmmod.
Команда insmod связывет модуль с ядром во время работы системы. Если имя загружаемого модуля указано без директорий, insmod будет искать модуль в нескольких каталогах по умолчанию. Для изменения пути поиска необходимо задать переменную окружения MODPATH. Если же существует файл конфигурации модулей (как правило /etc/modules.conf или /etc/conf.modules), то модуль будет искаться в каталогах, указанных в этом файле. Также в файле конфигурации можно задавать псевдонимы для устройств, добавив в него строку:
можно использовать команду insmod iso9660, несмотря на то, что соответствующего файла этого устройства нет. Файл конфигурации имеет достаточно гибкую структуру, позволяющую полностью контролировать процесс загрузки и выгрузки модулей; для его более углублённого изучения можно порекомендовать электронную интерактивную систему помощи ОС Linux (man 5 modules.conf).
3. Отличие драйверов от обычных приложений |
Язык Си позволяет использовать функции, которые не были определены в программе: при компилировании программы все подобные ссылки разрешаются через соответствующие библиотеки функций. printf, например, определён в стандартной библиотеке libc. Модуль, с другой стороны, может использовать функции, определённые только в самом ядре. Функция printk является версией printf, определённой в ядре, и поэтому может использоваться в модулях. Она ведёт себя аналогично оригиналу, но без поддержки вещественной арифметики.
Поскольку никаких библиотек к модулям не подключается, исходники никогда не должны включать обычные заголовочные файлы. Всё, что касается ядра определено в заголовочных файлах, которые могут быть найдены в каталогах include/linux, include/asm, include/net и include/scsi.
Модули ядра также должны отличаться от обычных программ более бережным отношением к пространству имён. При написании небольших приложений программисты мало задумываются о переменных и функциях, которым дают имена, что вызывает проблемы когда эти программы пытаются вставить в часть большого проекта. Разработчики, призванные заниматься подобными проектами затрачивают добрую часть своей работы только на запоминание всех этих "зарезервированных" имён и на поиск новых.
Полностью избежать подобных проблем нельзя, поскольку предугадывать, есть ли такое имя в ядре или нет, довольно обременительное занятие. Лучшим решением для этой проблемы является декларирование всех имён как static (не допускающее выхода имён за пределы функций) и использование тщательно продуманных префиксов для символов, которые вы собираетесь оставить глобальными. Использование префиксов даже для собственных имён в модуле может иногда помочь при отладке. По негласному соглашению префиксы используемые в ядре записываются в нижнем регистре и имеют длину 2 байта. Так, например, драйвер виртуальной памяти ядра /dev/kmem имеет префикс mm. Таким образом функции этого драйвера будут иметь названия mmopen, mmclose, mmread и mmwrite.
Ещё одно отличие модулей от приложений заключается в обработке исключений: в то время как ошибки в приложении не могут принести сколь-нибудь заметного ущерба, ошибки ядра фатальны по меньшей мере для текущего процесса, если не для всей системы.
4. Файлы устройств |
Назначением файлов устройств является обеспечение связи процессов с драйверами устройств в составе ядра, а через них - установление связи с физическими устройствами. Достижение этой цели реализуется следующим образом.
Каждому драйверу устройства, отвечающему за работу с определённым типом аппаратных средств, назначается свой собственный старший (major) номер. Список драйверов и их старших номеров доступен в /proc/devices. Каждому физическому устройству, которое управляется данным драйвером назначается младший (minor) номер. В системе поддерживается каталог /dev для хранения имён специальных файлов, называемых файлами устройств. Каждый такой файл относится к некоторому устройству, независимо от того, инсталлировано оно или нет.
Например, если выполнить команду ls -l /dev/hd[ab]*, то можно получить информацию о всех разделах жестких IDE-дисков, которые могут быть подсоединены к машине. Заметим при этом, что для каждого из разделов используется один и тот же старший номер 3, а вот младший номер у всех разный. Все эти устройства были созданы с помощью команды mknod во время загрузки системы.
5. Функции драйвера устройства |
struct file_operations { |
int (*lseek) (struct inode *, struct file *, off_t, int); |
ssize_t (*read) (struct inode *, struct file *, char *, int); |
ssize_t (*write) (struct inode *, struct file *, char *, int); |
int (*readdir) (struct inode *, struct file *, struct dirent *, int); |
int (*select) (struct inode *, struct file *, int, select_table *); |
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned int); |
int (*mmap) (struct inode *, struct file *, unsigned long, size_t, int, unsigned long); |
int (*open) (struct inode *, struct file *); |
void (*release) (struct inode *, struct file *); |
}; |
Рассмотрим каждую из этих функций подробнее:
Аргументы функции:
struct inode * inode /* Указатель на структуру inode для этого устройства */ |
struct file * file /* Указатель на файловую структуру для данного устройства */ |
off_t offset /* Смещение */ |
int origin /* 0 = смещение от начала, 1 = смещение от текущей позиции, 2 = смещение от конца*/ |
lseek возвращает -errno в случае ошибки или положительное смещение после выполнения.
Аргументы функций:
struct inode * inode /* Указатель на структуру inode для этого устройства */ |
struct file * file /* Указатель на файловую структуру для данного устройства */ |
char * buf /* Буфер символов для чтения-записи */ |
int count /* Число байтов для чтения-записи */ |
Аргументы функции:
struct inode * inode /* Указатель на структуру inode для этого устройства */ |
struct file * file /* Указатель на файловую структуру для данного устройства */ |
int sel_type /* Тип совершаемого действия: * SEL_IN - чтение, * SEL_OUT - запись, * SEL_EX - удаление */ |
select_table * wait /* Если wait = NULL, функция select проверяет, готово ли * устройство и возвращается в случае отсутствия готовности. * Если wait не равен NULL, select замораживает процесс и * ждет, пока устройство не будет готово. */ |
Аргументы функции:
struct inode * inode /* Указатель на структуру inode для этого устройства */ |
struct file * file /* Указатель на файловую структуру для данного устройства */ |
unsigned int cmd /* Команда, над которой осуществляется контроль */ |
unsigned int arg /* Это аргумент для команды, определяется пользователем.*/ |
Возвращаемое значение: -errno в случае ошибки, все другие значения определяются пользователем.
Аргументы функции:
struct inode * inode /* Указатель на структуру inode для этого устройства */ |
struct file * file /* Указатель на файловую структуру для данного устройства */ |
unsigned long addr /* Начальный адрес блока, используемого mmap */ |
size_t len /* Общая длина блока */ |
int prot /* Принимает значения: * PROT_READ читаемый кусок, * PROT_WRITE перезаписываемый кусок, * PROT_EXEC кусок, доступный для запуска, * PROT_NONE недоступный кусок */ |
unsigned long off /* Смещение, от которого производится перестановка */ |
Функция release вызывается лишь тогда, когда процесс закрывает последний файловый дескриптор. release может переустанавливать бит "занято". После вызова release, вы можете очистить куски выделенной kmalloc памятью под очереди процессов.
Аргументы функции:
struct inode * inode /* Указатель на структуру inode для этого устройства */ |
struct file * file /* Указатель на файловую структуру для данного устройства */ |
6. Встраивание драйверов в ядро |
Для символьно-ориентированных драйверов надо положить файлы в каталог drivers/char, а в Makefile добавить следующие строки:
ifdef CONFIG_MYDRIVER |
OBJS := $(OBJS) my_driver.o |
SRCS := $(SRCS) my_driver.c |
endif |
bool 'My Driver Support' CONFIG_MYDRIVER n
При конфигурировании ядра ответив на этот вопрос положительно, вы получите ядро со встроенной поддержкой вашего драйвера.
Также необходимо, чтобы драйвер был зарегистрирован в системе, а для этого вставьте в файл init/main.c, в область инициализации драйверов строки:
#ifdef CONFIG_MYDRIVER |
mem_start = mydriver_init(memstart); |
#endif |
Приведём простой пример драйвера, встраиваемого в ядро:
/* mydriver.c |
* |
* "Hello, world" - версия встраиваемая в ядро. |
*/ |
#include < linux/kernel.h > |
#include< linux/sched.h > |
#include< linux/tty.h > |
#include < linux/signal.h > |
#include < linux/errno.h > |
#include < asm/io.h > |
#include < asm/segment.h > |
#include < asm/system.h > |
#include < asm/irq.h > |
unsigned long mydriver_init(unsigned int mem_start) { |
printk("Hello, world\n"); |
return mem_start; |
} |
7. Динамически устанавливаемые драйверы |
Приведённая ниже программа hello.c - простой "Hello, world" модуль (который по большому счёту ничего и не делает).
/* hello.c |
* |
* "Hello, world" - версия модуля ядра. |
*/ |
#include < linux/kernel.h > /* Для выполнения работы в ядре */ |
#include < linux/module.h > /* Для создания модуля */ |
/* Точка входа - инициализация модуля */ |
int init_module(void) { |
printk("(1)Hello, world\n"); |
/* Если код возврата не нуль, то функция init_module завершилась неудачно и модуль ядра не будет загружен */ |
return 0; |
} |
/* Деинициализация модуля */ |
void cleanup_module(void) { |
printk("Goodbye, cruel world\n"); |
} |
Модуль ядра не является независимой исполняемой программой. Это объектный файл, который будет связан с ядром во время работы. Поэтому модули ядра следует компилировать с флагом -c. Кроме того, при компиляции модулей необходимо устанавливать ряд предопределённых ключей:
__KERNEL__ - указывает заголовочным файлам, что данный код будет выполняться в режиме ядра, а не как часть пользовательского процесса.
MODULE - указывает, что требуются соответствующие определения для модулей ядра.
Есть и другие ключи, которые можно включать в зависимости от использованных флагов при компиляции ядра. Если вас интересует, как было скомпилировано ядро, то следует обратиться к файлу include/linux/config.h.
# Makefile для модуля "Hello, world" |
CC = gcc |
CFLAGS := -Wall -DMODULE -D__KERNEL__ |
hello.o: hello.c |
$(CC) $(CFLAGS) -c hello.c |
echo insmod hello.o to install |
echo rmmod to delete |
Попутно заметим, что только пользователь root может загружать и выгружать модули.
root# make |
root# insmod hello.o |
Hello, World! |
root# rmmod hello.o |
Goodbye, cruel world |
root# |
8. Передача параметров |
Передача параметров драйверам, встраиваемым в ядро |
LILO boot: Linux mydriver=0x240,4
Обработать эти параметры можно с помощью следующей процедуры:
static int mydriver_base = DEFAULT_BASE; |
static int mydriver_irq = DEFAULT_IRQ; |
void mydriver_setup(char *str, int ints) { |
if (ints[0] == 2) { |
mydriver_base = ints[1]; |
mydriver_irq = ints[2]; |
} |
} |
struct { |
char *str; |
void (*setup_func)(char *, int *); |
} bootsetups[] = { |
{ "reserve=", reserve_setup }, |
. |
. |
. |
#ifdef CONFIG_MYDRIVER |
{ "mydriver=", mydriver_setup } |
#endif |
. |
. |
. |
{ 0, 0 } |
}; |
Передача параметров динамически устанавливаемым драйверам |
int skull_ival = 0; |
char *skull_sval; |
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.
/* chrdev.c |
* |
* Пример драйвера символьного устройства |
*/ |
#include < linux/kernel.h > |
#include < linux/module.h > |
#include < linux/fs.h > /* Определения для символьного устройства */ |
#include < asm/uaccess.h > /* put_user */ |
#define DEVICE_NAME "chardev" |
#define BUF_LEN 80 |
static int isOpen = 0; |
static char msg[BUF_LEN] |
static char *msg_ptr; |
/* Функция вызывается всякий раз при попытке открыть файл устройства */ |
static int device_open(struct inode *inode, struct file *file) { |
static int counter = 0; |
printk("device_open(%p, %p)\n", inode, file); |
/* Вывести major и minor устройства */ |
printk("Device: %d.%d\n", inode-i_rdev, inode-i_rdev & 0xff); |
/* Не допускать работу двух процессов одновременно */ |
if (isOpen) return -EBUSY; isBusy++; |
/* Инициализировать буфер. |
* Cледует быть предельно внимательным и не допускать |
* переполнения буферов, при работе в ядре это особенно важно! |
*/ |
sprintf(msg, "If I told you once, I told you %d times - hello, world", counter++); |
msg_ptr = msg; |
/* Гарантировать, что модуль не будет удаляться, пока файл открыт */ |
MOD_INC_USE_COUNT; |
return 0; |
} |
/* Закрыть файл устройства */ |
static int device_close(struct inode *inode, struct file *file) { |
prink("device_close(%p, %p)", inode, file); |
/* Разрешить работу с файлом */ |
isBusy--; |
MOD_DEC_USE_COUNT; |
return 0; |
} |
/* Прочитать файл */ |
static ssize_t device_read(struct *file, 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++; |
} |
printk("Read %d bytes, %d left\n", bytes_read, length); |
return bytes_read; |
} |
/* Записать в файл */ |
static ssize_t device_write(struct *file, const char *buffer, size_t length, loff_t *offset) { |
/* Возвратить код ошибки "не поддерживается" */ |
return -EINVAL; |
} |
/* Старший номер устройства */ |
static int major; |
/* Структура, содержащая указатели на функции, |
* вызываемые при совершении некоторых действий с файлом устройства. |
* Если в поле NULL, то данная функция не реализована |
*/ |
struct file_operations ops = { |
NULL, /* seek */ |
device_read, /* read */ |
device_write, /* write */ |
NULL, /* readdir */ |
NULL, /* select */ |
NULL, /* ioctl */ |
NULL, /* mmap */ |
device_open, /* open */ |
NULL, /* ? */ |
device_close /* close */ |
}; |
/* Инициализация модуля - регистрация символьного устройства*/ |
int init_module(void) { |
major = module_register_chrdev(0, DEVICE_NAME, &ops); |
/* Отрицательные значения означают ошибку */ |
if (major < 0) { |
printk("Registering the character device failed with %d\n", major); |
return major; |
} |
/* Вывести major и инструкцию по созданию файла устройства */ |
printk("The major device number is %d\n", major); |
printk("Use this to create device file:\n"); |
printk("mknod < name > c %d < minor >\n", major); |
return 0; |
} |
/* Деинсталлировать модуль */ |
void cleanup_module() { |
int ret; |
/* Снять с регистрации устройство */ |
ret = module_unergister_chrdev(major, DEVICE_NAME); |
/* В случае ошибки при снятии с регистрации, выдать сообщение об этом */ |
if (ret < 0) |
printk("Error while unregister character device: %d\n", ret); |
} |
root# mknod foo 13 1
Здесь 13 - старший номер устройства, зарегистрированного в системе, выводится при установке модуля. 1 - младший номер устройства.