Ключевые слова:linux, driver, gcc, (найти похожие документы)
From: Mike Goblin <mgoblin@mail.ru.>
Date: Mon, 20 Sep 2004 18:21:07 +0000 (UTC)
Subject: Написание драйверов в Linux
Оригинал: http://genphys.phys.msu.ru/~dmitriyk/site/mv/driver0.html
Написание драйверов в Linux: первые шаги
Написание драйверов под Linux всегда казалось мне крайне сложным и
загадочным делом. При словах "Linux" и "драйвер" - перед глазами
сразу возникал образ небритого, взъерошенного программиста, сутками
просиживающего за изучением кода ядра. Но вот - два месяца назад мне
понадобилось написать такой драйвер самому. Процесс оказался вовсе не
сложным, и доставил мне массу удовольствия. Предлагаю и вам
попробовать свои силы в этом увлекательном занятии. Я использовал Red
Hat Linux 7.1 с ядром 2.4.2-2. Для других версий ядра, возможно,
потребуется внести некоторые коррективы.
Итак - что такое "драйвер устройства"? Это низкоуровневая программа,
содержащая специфический код для работы с устройством, которая
позволяет пользовательским программам (и самой ОС) управлять им
стандартным образом. Устройства можно разделить на:
Символьные. Чтение и запись устройства идет посимвольно. Примеры таких
устройств: клавиатура, последовательные порты.
Блочные. Чтение и запись устройства возможны только блоками, обычно по
512 или 1024 байта. Пример - жесткий диск.
Сетевые интерфейсы. Пример - сетевая карта (eth0). Отличаются тем, что
не отображаются на файловую систему, т.е. не имеют соответствующих
файлов в директории /dev, поскольку из-за специфики этих устройств
работа с сетевыми устройствами как с файлами неэффективна.
Для символьных и блочных устройств - взаимодействие с драйвером
реализуется через специальные файлы, расположенные в директории /dev.
Каждый файл устройства имеет два номера - старший, определяющий тип
устройства, и младший, определяющий конкретный номер устройства (в
системе может быть несколько устройств одного типа - например, жестких
дисков). Многие из старших номеров устройств уже зарезервированы; их
можно посмотреть в документации на ядро. В RedHat Linux 7.1 - это файл
/usr/share/doc/kernel-doc-2.4.2/devices.txt.
В первых версиях Linux драйвера устройств были "зашиты" в ядро.
Недостатки такого решения очевидны:
1. Драйвера, включенные в ядро, загружаются даже при отсутствии
устройства в системе - и потребляют системные ресурсы.
2. При подключении нового устройства (или новой версии драйвера)
требуется перекомпиляция ядра.
Наличие этих проблем привело к созданию в ядре механизма динамически
загружаемых модулей. Механизм дает возможность устанавливать драйвера
новых устройств "на лету" - без перекомпиляции ядра и даже
перезагрузки системы. Устанавливать можно не все драйвера, а лишь
драйвера для устройств, реально присутствующих в системе. Динамические
модули, в отличие от обычных программ, представляют собой объектные
файлы, скомпилированные по определенным правилам. Мы рассмотрим
создание такого модуля для символьного устройства.
Простейший модуль. Компиляция и установка модуля в систему. Текст
простейшего модуля представлен ниже (файл module.c). Строки файла
пронумерованы.
1 /*
2 ====================================
3 Пример простейшего модуля ядра
4 Mike Goblin mgoblin.mail.ru
5 2001
6 ====================================
7 */
8 #define MODULE
9 #define __KERNEL__
10 #include <module.h>
11
12 int init_module()
13 {
14 return 0;
15 }
16
17 void cleanup_module()
18 {
19 return;
20 }
Как видите - имеется две функции (они обязательны): int init_module()
- вызывается при загрузке модуля ядром. Если возвращаемое значение
"0", все нормально; иначе - произошла ошибка. void cleanup_module()
- вызывается при удалении модуля из системы. Строки 8-9 заставляют
компилятор генерировать код динамически загружаемого модуля. В
заголовочном файле module.h содержатся определения, необходимые для
создания динамического модуля. Ниже приведен текст Makefile для сборки
нашего модуля:
CC=gcc
MODFLAGS:= -O3 -Wall -DLINUX
module.o: module.c
$(CC) $(MODFLAGS) -c module.c
Директива -DLINUX говорит компилятору о необходимости генерировать код
под Linux. Ключ заставляет компилятор генерировать именно объектный, а
не исполняемый файл. Сборка осуществляется командой make в директории,
где лежит исходный файл module.c. Результат сборки - файл module.o.
Для установки модуля в систему необходимы права суперпользователя
root. Сама установка осуществляется командой insmod .
Просмотр установленных модулей доступен root-у по команде lsmod.
Удаление модуля (тоже с правами root) - rmmod .
Теперь - начнем совершенствовать наш модуль:
1. Неплохо было бы дать пользователю возможность понять, что и как
делает модуль, и как связаться с автором. Для этого в module.h
определены макросы MODULE_DESCRIPTION и MODULE_AUTHOR. Получить
информацию об авторе можно командой modinfo -a , описание модуля -
modinfo -d . Для выполнения этих команд опять же нужны права root.
2. Модуль может выводить на консоль сообщения; для этого есть функция
printk. Конечно, для реальных модулей это обычно ни к чему, однако
на этапе отладке такие сообщения бывают очень полезны.
3. Не всегда удобно называть функцию инициализации init_module, а
функцию выгрузки cleanup_module. В файле init.h определены функции
module_init () и module_exit (), позволяющие снять ограничение.
Новый код модуля приведен ниже:
/*
====================================
* Пример простейшего модуля ядра
Mike Goblin mgoblin.mail.ru
2001
====================================
*/
#define MODULE
#define __KERNEL__
#include <module.h> // определения для модуля
#include <init.h> // module_init и module_exit
#include <kernel.h> // printk
MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
MODULE_DESCRIPTION("Test module for linux kernel");
int module_start()
{
printk("This is a test module startup message\n");
return 0;
}
void module_stop()
{
printk("Module is dead\n");
return;
}
module_init(module_start);
module_exit(module_stop);
При загрузке и выгрузке модуля вы увидите в консоли тестовые сообщения
модуля.
Регистрация устройства и захват ресурсов Наш модуль ничего не делает,
да и вообще недоступен для программ. Чтобы сделать устройство
доступным - при загрузке модуля его необходимо зарегистрировать в
системе и указать используемые ресурсы. Для регистрации различных
типов устройств в заголовочном файле fs.h определены соответствующие
функции с префиксом register. Так, нам для регистрации нашего
"драйвера символьного устройства" необходимо использовать функцию
register_chrdev. Функция объявлена так:
extern int register_chrdev(unsigned int, const char *, struct file_operations *);
Первый параметр - старший номер файла устройства (тип устройства).
Если этот параметр равен 0, то функция возвращает свободный старший
номер для нашего типа устройства. Лучше так и делать, т.к. при этом
исключаются конфликты старших номеров устройств. Второй параметр - имя
устройства. Под этим именем устройство будет отображаться в списке
устройств. Третий параметр - структура с указателями на функции
драйвера. Здесь мы подходим к вопросу о том, как система работает с
драйвером. Драйвер хранит таблицу доступных функций, а система
вызывает эти функции, когда кто-либо пытается выполнить некоторые
действия (открытие, запись, чтение) с файлом устройства, который имеет
старший номер нашего устройства. Для символьного устройства - таблица
функций драйвера хранится в структуре file_operations. Эта структура и
передается при регистрации устройства.
Пока что мы будем передавать
структуру незаполненной (ведь функции работы с устройствами еще не
написаны). В примере, приведенном ниже, структура объявлена в строке
32. Драйвер регистрируется в строках 42-48. В строке 31 объявлена
переменная Major для хранения старшего номера устройства, получаемого
от ОС при регистрации. Переменная объявлена как static, т.к. старший
номер потребуется и при снятии регистрации перед выгрузкой модуля.
После регистрации драйвера, как правило, происходит поиск
присутствующих в системе устройств данного типа и их параметров
(номера прерываний, порты ввода-вывода и т.д.). Методика поиска - своя
для каждого типа устройств, поэтому трудно привести какой-либо код. В
нашем примере - будем считать, что есть два устройства, использующие
один диапазон портов ввода-вывода и одно прерывание. Первое из наших
устройств имеет младший номер 0, а второе - 1. Для дальнейшей работы -
создадим файлы данных устройств, набрав (с правами root) в директории
/dev команды:
mknod my_dev c 254 0
mknod my_dev c 254 1
Вместо 254 вам, возможно, понадобиться подставить другой старший номер
(он выдается модулем на консоль при загрузке). Ниже приведен новый
вариант нашего модуля.
1 /*
2 ===========================================
3 Пример захвата ресурсов модулем ядра
4 Mike Goblin mgoblin.mail.ru
5 2001
6 ===========================================
7 */
8 #define MODULE
9 #define __KERNEL__
10
11 #include <module.h>/определения для модуля
12 #include <init.h>/ module_init и module_exit
13 #include <kernel.h>/ printk
14 #include <fs.h> / Структуры драйвера
15 #include <ioport.h>/ захват портов ввода-вывода
16 #include <sched.h> / Захват прерывания
17 // Имя нашего устройства
18 #define DEV_NAME "my_dev"
19 // Порты ввода-вывода устройства
20 #define PORT_START 0x1000
21 #define PORT_COUNT 20
22 // Память устройства
23 #define MEM_START 0x10000000
24 #define MEM_COUNT 0x20
25 // Номер прерывания
26 #define IRQ_NUM 11
27 #define SUCCESS 0
28 MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
29 MODULE_DESCRIPTION("Test module for linux kernel");
30 // Старший номер файла устройства
31 static int Major;
32 struct file_operations Fops;
33 // Обработчик прерывания
34 void irq_handler(int irq, void *dev_id, struct pt_regs *regs)
35 {
35 return;
36 }
37 // Инициализация модуля
38 int module_start()
39 {
40 // Регистрация устройства
41 printk("Kernel: This is a test module startup message\n");
42 Major = register_chrdev(0, DEV_NAME, &Fops);
43 if (Major < 0) { // Проверка успешности регистрации
44 printk("Kernel: Register failed\n");
45 return Major;
46 }
47 printk("Kernel: Device registered. Major number is %d\n",Major);
48
49 // Захват портов ввода-вывода
50 printk("Kernel: Try allocating io ports\n");
51 if (check_region(PORT_START, PORT_COUNT))
52 {
53 printk("Kernel: Allocation io ports failed\n");
54 return -EBUSY;
55 }
56 request_region(PORT_START, PORT_COUNT, DEV_NAME);
60 printk ("Kernel: IO ports allocated\n");
61
62 // Захват памяти
63 if (check_mem_region(MEM_START, MEM_COUNT))
64 {
65 printk("Kernel: memio ports allocation failed\n");
66 release_region(PORT_START, PORT_COUNT);
67 return -EBUSY;
68 }
69 request_mem_region(MEM_START, MEM_COUNT, DEV_NAME);
70 printk ("Kernel: Memio ports allocated\n");
71 // Захват прерывания
72 if (request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL))
73 {
74 printk("Kernel: IRQ allocation failed\n");
75 release_mem_region(MEM_START, MEM_COUNT);
76 release_region(PORT_START, PORT_COUNT);
77 return -EBUSY;
78 }
79 printk ("Kernel: IRQ allocated\n");
80 return 0;
81 }
82 // Действия перед выгрузкой модуля
83 void module_stop()
84 {
85 // Снимаем захват портов ввода-вывода
86 release_region(PORT_START, PORT_COUNT);
87 printk("Kernel: release io ports\n");
88 // Снимаем захват памяти
89 release_mem_region(MEM_START, MEM_COUNT);
90 printk("Kernel: release memio ports\n");
91
92 // Освобождаем прерывание
93 free_irq(IRQ_NUM,NULL);
94 printk("Kernel: release irq\n");
95 // Снимаем регистрацию устройства
96 if (unregister_chrdev(Major, DEV_NAME) < 0){
97 printk("Kernel: unregister device failed\n");
98 }
99 printk("Kernel: device unregistered\n");
100 printk("Kernel: module3 is dead \n");
101 return;
102}
103 module_init(module_start);
104 module_exit(module_stop);
Захват портов ввода-вывода осуществляется в функции module_start
(строки 49-60). В строке 51 - проверяется доступность диапазона портов
(функцией check_region, объявленной в заголовочном файле ioport.h).
Первый параметр - начало диапазона, второй - количество запрашиваемых
портов. Если данный диапазон портов ввода-вывода не занят,
check_region возвращает 0. Непосредственно захват портов
осуществляется в строке 56, функцией request_region (которая также
определена в заголовочном файле ioport.h). Первые два параметра
аналогичны параметрам check_region. Третий параметр - имя устройства,
за которым будут закреплены порты.
Если вы не зарегистрируете порты, используемые устройством, ничего
страшного не произойдет: они все равно будут доступы из модуля (если
никакой другой модуль не зарегистрировал эти порты - от чего, увы,
никто не застрахован). Если вы зарегистрировали диапазон портов - не
забудьте его освободить при выгрузке модуля; иначе система будет
считать данный диапазон занятым, и запретит доступ к нему даже при
выгруженном модуле. Естественно, освобождение ресурсов происходит в
функции module_stop (освобождение диапазона портов ввода вывода
производится в строке 86 - вызовом release_region).
Многие из устройств имеют собственную память (например, видеокарты).
Регистрация блоков памяти устройства производится по тому же принципу,
что и регистрация портов ввода-вывода. Для регистрации используется
check_mem_region, request_mem_region, для снятия регистрации -
release_mem_region.
Захват прерывания (строки 71-79) осуществляется вызовом request_irq.
Первый параметр функции - номер прерывания, второй - обработчик
прерывания, четвертый - имя устройства, захватывающего прерывание. В
случае удачного захвата - функция возвращает 0. Освобождается
прерывание вызовом free_irq. Первый параметр - номер прерывания. Об
использовании остальных параметров функций request_irq и free_irq мы
поговорим чуть позже. В нашем примере мы захватываем 11-е прерывание,
т.к у меня на машине оно свободно.
В примере (для простоты) мы захватываем ресурсы от имени одного
устройства - my_dev; в реальной жизни нужно было бы захватить
отдельные диапазоны портов, памяти и прерывания для каждого из двух
наших устройств.
Открытие и закрытие устройства Следующий шаг - создание методов
открытия и закрытия устройств. Предварительно следует объявить
структуру для хранения информации о состоянии устройства и массив
таких структур - для хранения информации о состоянии конкретных
устройств, управляемых драйвером. Индексами в массиве будут младшие
номера наших устройств. Для простоты мы считаем, что младшие номера
начинаются с 0, и максимальное число таких устройств в системе - 2. В
реальности - где младшие номера могут идти не подряд - чаще приходится
создавать не массив, а связанный список обнаруженных устройств.
// Структура для хранения состояния устройства
struct dev_state
{
int dev_open; // Открыто ли устройство
ssize_t byte_read; // Сколько байт прочитано из устройства
ssize_t byte_write; // Сколько байт записано в устройство
};
// Массив для хранения информации о состоянии устройств
static struct dev_state state[MAX_INODE+1];
Код функции открытия устройства выглядит так (строки пронумерованы):
1 static int device_open(struct inode *inode, struct file *filp)
2 {
3 struct dev_state *dev_state;
4 printk("Kernel: try opening device w/minor number %d\n", MINOR(inode->i_rdev));
5 dev_state = &state[MINOR(inode->i_rdev)];
6 if(dev_state->dev_open)
7 {
8 printk("Devise busy\n");
9 return -EBUSY;
10 }
11 dev_state->dev_open++;
12 dev_state->byte_read = 0;
13 dev_state->byte_write = 0;
14
15 MOD_INC_USE_COUNT;
16 return SUCCESS;
17 }
В строке 5 мы получаем структуру состояния устройства. Заметьте, что
младший номер файла устройства (указывающий на то, которое из
устройств данного типа в системе открывается) мы получаем как
MINOR(inode->i_rdev). Затем, в строках 6-10, мы проверяем, не открыто
ли уже данное устройство (в нашем случае - запрещаем повторное
открытие устройства). Возможен и вариант, когда повторное открытие
разрешено, но его мы рассматривать не будем.
Если устройство еще не открыто - мы увеличиваем счетчик открытия
устройства и сбрасываем статистику переданных и принятых байт. Далее
(в строке 15) мы инкрементируем счетчик использования данного модуля.
Для этого используется макрос MOD_INC_USE_COUNT, объявленный в
заголовочном файле module.h. Счетчик ссылок используется для запрета
выгрузки модуля из памяти при наличии процессов, работающих с
функциями модуля. Поэтому необходимо внимательно относиться к
управлению счетчиком использования модуля; если его значение больше 0
- модуль невозможно будет выгрузить (не перезагружая компьютер). В
случае успешного открытия устройства - функция device_open возвращает
0.
Функция закрытия устройства выглядит следующим образом:
static int device_close(struct inode *inode, struct file *filp)
{
struct dev_state *dev_state;
printk("Kernel: try to close device w/minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
if(!dev_state->dev_open)
{
printk("Device not open\n");
return SUCCESS;
}
dev_state->dev_open--;
MOD_DEC_USE_COUNT;
return SUCCESS;
}
Как видно из кода, закрытие устройства мы выполняем всегда успешно;
ПРИ ЭТОМ уменьшаем на 1 количество открытий данного устройства и
декрементируем счетчик использования модуля.
В качестве примера открытия устройства пользовательской программой я
написал простое тестовое приложение:
/*Тестовое приложение*/
#include <module.h>
int main()
{
int fd;
printf("Test program\n");
fd = open("/dev/my_dev2",O_RDWR);
if (fd == -1) {
printf("open failed\n");
return -1;
}
printf("Device my_dev open\n");
close(fd);
printf("Device my_dev closed\n");
return 0;
}
Чтобы указать ядру на функции открытия и закрытия - заполним
соответствующие поля структуры file_operations:
struct file_operations Fops =
{
open: device_open,
release: device_close
};
Вот и все; теперь можете писать свой собственный драйвер!
Написание драйверов под Linux всегда казалось мне крайне сложным и
загадочным делом. При словах "Linux" и "драйвер" - перед глазами
сразу возникал образ небритого, взъерошенного программиста, сутками
просиживающего за изучением кода ядра. Но вот - два месяца назад мне
понадобилось написать такой драйвер самому. Процесс оказался вовсе не
сложным, и доставил мне массу удовольствия. Предлагаю и вам
попробовать свои силы в этом увлекательном занятии. Я использовал Red
Hat Linux 7.1 с ядром 2.4.2-2. Для других версий ядра, возможно,
потребуется внести некоторые коррективы.
Итак - что такое "драйвер устройства"? Это низкоуровневая программа,
содержащая специфический код для работы с устройством, которая
позволяет пользовательским программам (и самой ОС) управлять им
стандартным образом. Устройства можно разделить на:
Символьные. Чтение и запись устройства идет посимвольно. Примеры таких
устройств: клавиатура, последовательные порты.
Блочные. Чтение и запись устройства возможны только блоками, обычно по
512 или 1024 байта. Пример - жесткий диск.
Сетевые интерфейсы. Пример - сетевая карта (eth0). Отличаются тем, что
не отображаются на файловую систему, т.е. не имеют соответствующих
файлов в директории /dev, поскольку из-за специфики этих устройств
работа с сетевыми устройствами как с файлами неэффективна.
Для символьных и блочных устройств - взаимодействие с драйвером
реализуется через специальные файлы, расположенные в директории /dev.
Каждый файл устройства имеет два номера - старший, определяющий тип
устройства, и младший, определяющий конкретный номер устройства (в
системе может быть несколько устройств одного типа - например, жестких
дисков). Многие из старших номеров устройств уже зарезервированы; их
можно посмотреть в документации на ядро. В RedHat Linux 7.1 - это файл
/usr/share/doc/kernel-doc-2.4.2/devices.txt.
В первых версиях Linux драйвера устройств были "зашиты" в ядро.
Недостатки такого решения очевидны:
1. Драйвера, включенные в ядро, загружаются даже при отсутствии
устройства в системе - и потребляют системные ресурсы.
2. При подключении нового устройства (или новой версии драйвера)
требуется перекомпиляция ядра.
Наличие этих проблем привело к созданию в ядре механизма динамически
загружаемых модулей. Механизм дает возможность устанавливать драйвера
новых устройств "на лету" - без перекомпиляции ядра и даже
перезагрузки системы. Устанавливать можно не все драйвера, а лишь
драйвера для устройств, реально присутствующих в системе. Динамические
модули, в отличие от обычных программ, представляют собой объектные
файлы, скомпилированные по определенным правилам. Мы рассмотрим
создание такого модуля для символьного устройства.
Простейший модуль. Компиляция и установка модуля в систему. Текст
простейшего модуля представлен ниже (файл module.c). Строки файла
пронумерованы.
1 /*
2 ====================================
3 Пример простейшего модуля ядра
4 Mike Goblin mgoblin.mail.ru
5 2001
6 ====================================
7 */
8 #define MODULE
9 #define __KERNEL__
10 #include <module.h>
11
12 int init_module()
13 {
14 return 0;
15 }
16
17 void cleanup_module()
18 {
19 return;
20 }
Как видите - имеется две функции (они обязательны): int init_module()
- вызывается при загрузке модуля ядром. Если возвращаемое значение
"0", все нормально; иначе - произошла ошибка. void cleanup_module()
- вызывается при удалении модуля из системы. Строки 8-9 заставляют
компилятор генерировать код динамически загружаемого модуля. В
заголовочном файле module.h содержатся определения, необходимые для
создания динамического модуля. Ниже приведен текст Makefile для сборки
нашего модуля:
CC=gcc
MODFLAGS:= -O3 -Wall -DLINUX
module.o: module.c
$(CC) $(MODFLAGS) -c module.c
Директива -DLINUX говорит компилятору о необходимости генерировать код
под Linux. Ключ заставляет компилятор генерировать именно объектный, а
не исполняемый файл. Сборка осуществляется командой make в директории,
где лежит исходный файл module.c. Результат сборки - файл module.o.
Для установки модуля в систему необходимы права суперпользователя
root. Сама установка осуществляется командой insmod .
Просмотр установленных модулей доступен root-у по команде lsmod.
Удаление модуля (тоже с правами root) - rmmod .
Теперь - начнем совершенствовать наш модуль:
1. Неплохо было бы дать пользователю возможность понять, что и как
делает модуль, и как связаться с автором. Для этого в module.h
определены макросы MODULE_DESCRIPTION и MODULE_AUTHOR. Получить
информацию об авторе можно командой modinfo -a , описание модуля -
modinfo -d . Для выполнения этих команд опять же нужны права root.
2. Модуль может выводить на консоль сообщения; для этого есть функция
printk. Конечно, для реальных модулей это обычно ни к чему, однако
на этапе отладке такие сообщения бывают очень полезны.
3. Не всегда удобно называть функцию инициализации init_module, а
функцию выгрузки cleanup_module. В файле init.h определены функции
module_init () и module_exit (), позволяющие снять ограничение.
Новый код модуля приведен ниже:
/*
====================================
/Пример простейшего модуля ядра
/Mike Goblin mgoblin.mail.ru
/2001
====================================
*/
#define MODULE
#define __KERNEL__
#include <module.h> / определения для модуля
#include <init.h> / module_init и module_exit
#include <kernel.h> / printk
MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
MODULE_DESCRIPTION("Test module for linux kernel");
int module_start()
{
printk("This is a test module startup message\n");
return 0;
}
void module_stop()
{
printk("Module is dead\n");
return;
}
module_init(module_start);
module_exit(module_stop);
При загрузке и выгрузке модуля вы увидите в консоли тестовые сообщения
модуля.
Регистрация устройства и захват ресурсов Наш модуль ничего не делает,
да и вообще недоступен для программ. Чтобы сделать устройство
доступным - при загрузке модуля его необходимо зарегистрировать в
системе и указать используемые ресурсы. Для регистрации различных
типов устройств в заголовочном файле fs.h определены соответствующие
функции с префиксом register. Так, нам для регистрации нашего
"драйвера символьного устройства" необходимо использовать функцию
register_chrdev. Функция объявлена так:
extern int register_chrdev(unsigned int, const char *, struct file_operations *);
Первый параметр - старший номер файла устройства (тип устройства).
Если этот параметр равен 0, то функция возвращает свободный старший
номер для нашего типа устройства. Лучше так и делать, т.к. при этом
исключаются конфликты старших номеров устройств. Второй параметр - имя
устройства. Под этим именем устройство будет отображаться в списке
устройств. Третий параметр - структура с указателями на функции
драйвера. Здесь мы подходим к вопросу о том, как система работает с
драйвером. Драйвер хранит таблицу доступных функций, а система
вызывает эти функции, когда кто-либо пытается выполнить некоторые
действия (открытие, запись, чтение) с файлом устройства, который имеет
старший номер нашего устройства. Для символьного устройства - таблица
функций драйвера хранится в структуре file_operations.
Эта структура и
передается при регистрации устройства. Пока что мы будем передавать
структуру незаполненной (ведь функции работы с устройствами еще не
написаны). В примере, приведенном ниже, структура объявлена в строке
32. Драйвер регистрируется в строках 42-48. В строке 31 объявлена
переменная Major для хранения старшего номера устройства, получаемого
от ОС при регистрации. Переменная объявлена как static, т.к. старший
номер потребуется и при снятии регистрации перед выгрузкой модуля.
После регистрации драйвера, как правило, происходит поиск
присутствующих в системе устройств данного типа и их параметров
(номера прерываний, порты ввода-вывода и т.д.). Методика поиска - своя
для каждого типа устройств, поэтому трудно привести какой-либо код. В
нашем примере - будем считать, что есть два устройства, использующие
один диапазон портов ввода-вывода и одно прерывание. Первое из наших
устройств имеет младший номер 0, а второе - 1. Для дальнейшей работы -
создадим файлы данных устройств, набрав (с правами root) в директории
/dev команды:
mknod my_dev c 254 0
mknod my_dev c 254 1
Вместо 254 вам, возможно, понадобиться подставить другой старший номер
(он выдается модулем на консоль при загрузке). Ниже приведен новый
вариант нашего модуля.
1 /*
2 ===========================================
3 Пример захвата ресурсов модулем ядра
4 Mike Goblin mgoblin.mail.ru
5 2001
6 ===========================================
7 */
8 #define MODULE
9 #define __KERNEL__
10
11 #include <module.h>/определения для модуля
12 #include <init.h>/ module_init и module_exit
13 #include <kernel.h>/ printk
14 #include <fs.h> / Структуры драйвера
15 #include <ioport.h>/ захват портов ввода-вывода
16 #include <sched.h> / Захват прерывания
17 // Имя нашего устройства
18 #define DEV_NAME "my_dev"
19 // Порты ввода-вывода устройства
20 #define PORT_START 0x1000
21 #define PORT_COUNT 20
22 // Память устройства
23 #define MEM_START 0x10000000
24 #define MEM_COUNT 0x20
25 // Номер прерывания
26 #define IRQ_NUM 11
27 #define SUCCESS 0
28 MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
29 MODULE_DESCRIPTION("Test module for linux kernel");
30 // Старший номер файла устройства
31 static int Major;
32 struct file_operations Fops;
33 // Обработчик прерывания
34 void irq_handler(int irq, void *dev_id, struct pt_regs *regs)
35 {
35 return;
36 }
37 // Инициализация модуля
38 int module_start()
39 {
40 // Регистрация устройства
41 printk("Kernel: This is a test module startup message\n");
42 Major = register_chrdev(0, DEV_NAME, &Fops);
43 if (Major < 0) { // Проверка успешности регистрации
44 printk("Kernel: Register failed\n");
45 return Major;
46 }
47 printk("Kernel: Device registered. Major number is %d\n",Major);
48
49 // Захват портов ввода-вывода
50 printk("Kernel: Try allocating io ports\n");
51 if (check_region(PORT_START, PORT_COUNT))
52 {
53 printk("Kernel: Allocation io ports failed\n");
54 return -EBUSY;
55 }
56 request_region(PORT_START, PORT_COUNT, DEV_NAME);
60 printk ("Kernel: IO ports allocated\n");
61
62 // Захват памяти
63 if (check_mem_region(MEM_START, MEM_COUNT))
64 {
65 printk("Kernel: memio ports allocation failed\n");
66 release_region(PORT_START, PORT_COUNT);
67 return -EBUSY;
68 }
69 request_mem_region(MEM_START, MEM_COUNT, DEV_NAME);
70 printk ("Kernel: Memio ports allocated\n");
71 // Захват прерывания
72 if (request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL))
73 {
74 printk("Kernel: IRQ allocation failed\n");
75 release_mem_region(MEM_START, MEM_COUNT);
76 release_region(PORT_START, PORT_COUNT);
77 return -EBUSY;
78 }
79 printk ("Kernel: IRQ allocated\n");
80 return 0;
81 }
82 // Действия перед выгрузкой модуля
83 void module_stop()
84 {
85 // Снимаем захват портов ввода-вывода
86 release_region(PORT_START, PORT_COUNT);
87 printk("Kernel: release io ports\n");
88 // Снимаем захват памяти
89 release_mem_region(MEM_START, MEM_COUNT);
90 printk("Kernel: release memio ports\n");
91
92 // Освобождаем прерывание
93 free_irq(IRQ_NUM,NULL);
94 printk("Kernel: release irq\n");
95 // Снимаем регистрацию устройства
96 if (unregister_chrdev(Major, DEV_NAME) < 0){
97 printk("Kernel: unregister device failed\n");
98 }
99 printk("Kernel: device unregistered\n");
100 printk("Kernel: module3 is dead \n");
101 return;
102}
103 module_init(module_start);
104 module_exit(module_stop);
Захват портов ввода-вывода осуществляется в функции module_start
(строки 49-60). В строке 51 - проверяется доступность диапазона портов
(функцией check_region, объявленной в заголовочном файле ioport.h).
Первый параметр - начало диапазона, второй - количество запрашиваемых
портов. Если данный диапазон портов ввода-вывода не занят,
check_region возвращает 0. Непосредственно захват портов
осуществляется в строке 56, функцией request_region (которая также
определена в заголовочном файле ioport.h). Первые два параметра
аналогичны параметрам check_region. Третий параметр - имя устройства,
за которым будут закреплены порты. Если вы не зарегистрируете порты,
используемые устройством, ничего страшного не произойдет: они все
равно будут доступы из модуля (если никакой другой модуль не
зарегистрировал эти порты - от чего, увы, никто не застрахован). Если
вы зарегистрировали диапазон портов - не забудьте его освободить при
выгрузке модуля; иначе система будет считать данный диапазон занятым,
и запретит доступ к нему даже при выгруженном модуле. Естественно,
освобождение ресурсов происходит в функции module_stop (освобождение
диапазона портов ввода вывода производится в строке 86 - вызовом
release_region).
Многие из устройств имеют собственную память (например, видеокарты).
Регистрация блоков памяти устройства производится по тому же принципу,
что и регистрация портов ввода-вывода. Для регистрации используется
check_mem_region, request_mem_region, для снятия регистрации -
release_mem_region.
Захват прерывания (строки 71-79) осуществляется вызовом request_irq.
Первый параметр функции - номер прерывания, второй - обработчик
прерывания, четвертый - имя устройства, захватывающего прерывание. В
случае удачного захвата - функция возвращает 0. Освобождается
прерывание вызовом free_irq. Первый параметр - номер прерывания. Об
использовании остальных параметров функций request_irq и free_irq мы
поговорим чуть позже. В нашем примере мы захватываем 11-е прерывание,
т.к у меня на машине оно свободно.
В примере (для простоты) мы захватываем ресурсы от имени одного
устройства - my_dev; в реальной жизни нужно было бы захватить
отдельные диапазоны портов, памяти и прерывания для каждого из двух
наших устройств.
Открытие и закрытие устройства Следующий шаг - создание методов
открытия и закрытия устройств. Предварительно следует объявить
структуру для хранения информации о состоянии устройства и массив
таких структур - для хранения информации о состоянии конкретных
устройств, управляемых драйвером. Индексами в массиве будут младшие
номера наших устройств. Для простоты мы считаем, что младшие номера
начинаются с 0, и максимальное число таких устройств в системе - 2. В
реальности - где младшие номера могут идти не подряд - чаще приходится
создавать не массив, а связанный список обнаруженных устройств.
// Структура для хранения состояния устройства
struct dev_state
{
int dev_open; // Открыто ли устройство
ssize_t byte_read; // Сколько байт прочитано из устройства
ssize_t byte_write; // Сколько байт записано в устройство
};
// Массив для хранения информации о состоянии устройств
static struct dev_state state[MAX_INODE+1];
Код функции открытия устройства выглядит так (строки пронумерованы):
1 static int device_open(struct inode *inode, struct file *filp)
2 {
3 struct dev_state *dev_state;
4 printk("Kernel: try opening device w/minor number %d\n", MINOR(inode->i_rdev));
5 dev_state = &state[MINOR(inode->i_rdev)];
6 if(dev_state->dev_open)
7 {
8 printk("Devise busy\n");
9 return -EBUSY;
10 }
11 dev_state->dev_open++;
12 dev_state->byte_read = 0;
13 dev_state->byte_write = 0;
14
15 MOD_INC_USE_COUNT;
16 return SUCCESS;
17 }
В строке 5 мы получаем структуру состояния устройства. Заметьте, что
младший номер файла устройства (указывающий на то, которое из
устройств данного типа в системе открывается) мы получаем как
MINOR(inode->i_rdev). Затем, в строках 6-10, мы проверяем, не открыто
ли уже данное устройство (в нашем случае - запрещаем повторное
открытие устройства). Возможен и вариант, когда повторное открытие
разрешено, но его мы рассматривать не будем.
Если устройство еще не открыто - мы увеличиваем счетчик открытия
устройства и сбрасываем статистику переданных и принятых байт. Далее
(в строке 15) мы инкрементируем счетчик использования данного модуля.
Для этого используется макрос MOD_INC_USE_COUNT, объявленный в
заголовочном файле module.h. Счетчик ссылок используется для запрета
выгрузки модуля из памяти при наличии процессов, работающих с
функциями модуля. Поэтому необходимо внимательно относиться к
управлению счетчиком использования модуля; если его значение больше 0
- модуль невозможно будет выгрузить (не перезагружая компьютер). В
случае успешного открытия устройства - функция device_open возвращает
0.
Функция закрытия устройства выглядит следующим образом:
static int device_close(struct inode *inode, struct file *filp)
{
struct dev_state *dev_state;
printk("Kernel: try to close device w/minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
if(!dev_state->dev_open)
{
printk("Device not open\n");
return SUCCESS;
}
dev_state->dev_open--;
MOD_DEC_USE_COUNT;
return SUCCESS;
}
Как видно из кода, закрытие устройства мы выполняем всегда успешно;
ПРИ ЭТОМ уменьшаем на 1 количество открытий данного устройства и
декрементируем счетчик использования модуля.
В качестве примера открытия устройства пользовательской программой я
написал простое тестовое приложение:
/*Тестовое приложение*/
#include <module.h>
int main()
{
int fd;
printf("Test program\n");
fd = open("/dev/my_dev2",O_RDWR);
if (fd == -1) {
printf("open failed\n");
return -1;
}
printf("Device my_dev open\n");
close(fd);
printf("Device my_dev closed\n");
return 0;
}
Чтобы указать ядру на функции открытия и закрытия - заполним
соответствующие поля структуры file_operations:
struct file_operations Fops =
{
open: device_open,
release: device_close
};
Вот и все; теперь можете писать свой собственный драйвер!
Написание драйверов в Linux, часть 2.
В предыдущем номере мы начали рассказ о написании драйверов в ОС
Linux. Напомним, что мы рассмотрели пример простейшего модуля
Linux-драйвера -- рассказали о его компиляции, установке этого модуля
в систему регистрации драйвера, захвате ресурсов, открытии и закрытии
устройств. В продолжении темы -- обмен данными с устройством,
обработка прерываний, управляющие коды.
Чтение и запись.
Итак -- открывать устройство мы умеем; теперь научим его читать и
записывать данные. Приведем код методов записи и чтения полностью.
static ssize_t device_read(struct file *filp, char *buf, size_t buf_len, loff_t *offset){
struct inode* inode;
int count = buf_len;
struct dev_state *dev_state;
printk("Kernel: try to read\nKernel:
request reading %d bytes\n",buf_len);
inode = filp->f_dentry->d_inode;
printk("Kernel: minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
while(count--){
put_user(inb_p(PORT_START+10*MINOR(inode->i_rdev)), buf);
buf++;
}
dev_state->byte_read += buf_len;
printk("Kernel: read %d bytes\n", buf_len);
return buf_len;
}
static ssize_t device_write(struct file *filp, const char *buf, size_t buf_len,
off_t *offset){
struct inode* inode;
int count = buf_len;
unsigned char byte;
struct dev_state *dev_state;
printk("Kernel: try to write\nKernel:
request writing %d bytes\n",buf_len);
inode = filp->f_dentry->d_inode;
printk("Kernel: minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
while(count--){
get_user(byte, buf);
outb_p(byte, PORT_START+10*MINOR(inode->i_rdev));
buf++;
}
dev_state->byte_write +=buf_len;
printk("Kernel: written %d bytes\n", buf_len);
return buf_len;
}
Системе обязательно надо дать знать о наличии функций чтения и записи;
для этого добавим в структуру file_operations ссылки на эти функции:
struct file_operations Fops = {
open: devise_open,
release: device_close,
read: device_read,
write: device_write
};
При вызове операций чтения/записи нам передается буфер и его размер.
Из структуры file мы можем получить младший номер нашего устройства.
Чтением из порта ввода-вывода занимается вызов inb_p. Записью порта --
вызов outb_p(определны в asm/io.h).
Если вы внимательно изучили вышеприведенный код, то наверняка
спросите: а зачем для буфера производятся вызовы put_user при чтении и
get_user при записи? Для разрешения этого вопроса напомню, что
адресные пространства ядра системы и пользовательских процессов
различаются -- значит указатель на буфер корректен для
пользовательского процесса, но для ядра его непосредственное
использование недопустимо. Для решения этой проблемы и предназначены
функции get_user и put_user. Функция get_user получает байт из
переданного пользователем буфера, put_user помещает в этот буфер байт.
Функции определены в asm/uaccess.h.
В данном примере мы предполагали, что наше устройство всегода готово к
чтению или записи. На практике это не так. Чтобы "усыпить" модуль на
время, пока устройство не готово для чтения или записи, применяется
вызов функции interruptible_sleep_on(). Эта функция переводит модуль в
состояние, когда он пропускает временные слоты, отведенные ему для
работы системой. Это позволит повысить быстродействие системы и
снизить потребление ресурсов. "Пробуждается" модуль вызовом
wake_up_interruptible(), обычно из обработчика прерываний.
Обработка прерываний
Обработка прерываний -- несомненно один из самых интересных моментов
написания драйвера. Вернемся немного назад, к коду захвата прерывания
в функции module_start.
// Захват прерывания
if (request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL)){
printk("Kernel: IRQ allocation failed\n");
release_mem_region(MEM_START, MEM_COUNT);
release_region(PORT_START, PORT_COUNT);
return -EBUSY;
}
Первый параметр - номер прерывания, второй - функция обработчик, а вот
третий параметр - это флаги, чаще всего следующие занчения и их
комбинации:
SA_INTERRUPT -- говорит системе о том, что прерывание будет "быстрым",
тоесть во время него запрещена обработка всех других прерываний.
Отсутствие этого флага делает прерывание медленным, во время его
обработки могут возникать и обрабатываться другие прерывания. Если нет
острой необходимости -- лучше обрабатывать прерывания в "медленном
режиме".
SA_SHIRQ -- говорит о том, что данное прерывание может разделятся
несколькими устройствами. Для обеспечения совместного доступа к
прерыванию все устройства должны захватывать его с флагом SA_SHIRQ.
Четвертый параметр -- имя устройства, которое захватывает прерывание.
Пятый -- идентификатор устройства, передается обработчику прерывания,
чтобы при наличии нескольких устройств на одном прерывании можно было
понять, какое из устройств выдало прерывание.
Заготовка для обработчика выглядит так:
void irq_handler(int irq, void *dev_id, struct pt_regs *regs){
return;
}
Первый параметр --номер прерывания, второй -- идентификатор
устройства, третий -- регистры процессора. С первого взгляда -- ну
чего проще, заготовка для обработчика есть, прерывание возникает чаще
всего при готовности к записи/чтению - проверь, что из них доступно -
и записывай или читай на здоровье. Однако, смотрим внимательно, что
надо сделать в обработчике:
1. Проверить dev_id на предмет, наше ли это устройство;
2. Проанлазировать номер irq, на предмет наличия устройства на этом
прерывании(возможно ложное срабатывание);
3. Проверить, открыто ли устройство;
4. Проверить готовность к приему или передаче;
5. Проверить, есть ли в буфере данные для приема или передачи;
6. Передать или принять из устройства информацию.
А теперь вопрос, какие из этих действий относятся непосредственно к
обработке факта возникновения прерывания? 1, ну может 2. А время на
обработку каждого прерывания затрачивается, и нет гарантии, что во
время обработки одного прерывания не возникнет другое. В Linu
предлагается следующее решение: разделить обработчик на две части --
непосредственно обработчик(top half) и функция, которая может быть
вызвана позже для анализа прерывания(bottom half). Таким образом, в
top half мы выполняем лишь минимальные действия по обнаружению
прерывания и получению его параметров, а в bottom half все остальные
нужные действия. Код top half должен быть максимально компактным и
быстрым, чтобы не пропустить возникновение нового прерывания.
Для вызова bottom half обработчика используются очереди задач. Конечно
,можно создать и зарегестрировать собственную очередь задач, однако
такая очередь уже предопределеа в Linux -- это так называемая
immediate queue.
Таким образом, общий алгоритм работы top half следующий:
1. Выполнить действия по обнаружению достоверности прерывания и
выяснить факт принадлежности прерывания нашему устройству;
2. Если есть "горячие данные", которые могут быть потеряны при
возникновении следующего прерывания, считать их;
3. Поставить в immediate-очередь bottom half;
4. "Пометить" immediate-очередь для вызова в свободное время
процессора.
В качестве иллюстрации обработчика прерывания я приведу пример из
книги Ори Померанца "Ядро Linux. Программирование модулей". Данный
пример замещает обработчик прерывания клавиатуры. К сожалению нет
способа восстановить старый обработчик без перезагрузки, поэтому
данный код может быть потенциально опасен для вашей системы.
#define MODULE
#define __KERNEL__
#include <module.h>
// определения для модуля
#include <init.h>
// module_init и mdule_exit
#include <kernel.h>
// printk
#include <sched.h>
// захват прерывания
#include <tqueue.h>
// определения очереди
#include <interrupt.h>
// работа с прерыванием
#include <io.h>
// ввод и вывод портов
// имя нашего устройства
#define DEV_NAME "my_dev";
// номер прерывания
#define IRQ_NUM 1
// клавиатурное прерывание
#define SUCCESS 0
MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
MODULE_DESCRIPTION("Test module for linux kernel");
// bottom half обработчика анализирует, какая клавиша
// была нажата или отпущена и выдает сообщение об этом на консоль
static void got_char(void *scancode){
printk("Scan code %x %s.\n",
(int)*((char *) scancode) & 0x7f,
*((char *) scancode) &0x80?
"released":"pressed");
return;
}
// обработчик прерывания -- top half
void irq_handler(int irq, void *dev_id, struct pt_regs *regs){
static unsigned char scancode;
static struct tq_struct task = {
{},0,got_char,&scancode
};
unsigned char status;
printk("Oops, IRQ\n");
status = inb(0x64);
scancode = inb(0x60);
queue_task(&task, &tq_immediate);
mark_bh(IMMEDIATE_BH);
return;
}
// Инициализация модуля
int module_start(){
printk("Kernel: this is a test module startup message\n");
//убираем стандартный обработчик прерывания
free_irq(IRQ_NUM, NULL);
// захват прерывания
if(request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL)){
printk("Kernel: Allocation irq failed.\n");
return -EBUSY;
}
printk ("Kernel: IRQ allocated\n");
return SUCCESS;
}
// действия перед выгрузкой модуля
void module_stop(){
// снимаем захват irq
free_irq(IRQ_NUM, NULL);
printk("Kernel: release irq\n");
printk("Kernel: module is dead \n");
return;
}
module_init(module_start);
module_exit(module_stop);
Вызов free_irq выгружает стандартный обработчик. Функция irq_handler
-- содержит код top half; в нем queue_task ставит в очередь вызов
got_char, выполняющий функции bottom half. А активирует очередь
immediate вызов mark_bh.
Ioctl
Мы научились читать из устройства и запысывать в него, однако этим
потребности работы с устройством не ограничиваются. А что, если надо
получить какую-либо слжебную информацию или выполнить какие-то
манипуляции по настройке устройства? Для этого используют ioctl -
упрвляющие коды. Передача таких кодов устройству заставляет уего
выполнить запрошенные действия.
Чтобы значения кодов были доступны и модулую, их определение вынесено
в отдельный заголовочный файл our_module.h. Его содержимое приведено
ниже:
#ifndef OUR_MODULE_H
#define OUR_MODULE_H 1
#endif <module.h>
#include <ioctl.h>
#define MAJOR 42
/получение числа считанных байт
#define IOCTL_GET_RSTAT _IOR(MAJOR,0,ssize_t *)
//получение числа записанных байт
#define IOCTL_GET_WSTAT _IOR(MAJOR,1,ssize_t *)
//сброс статистики считанных байт
#define IOCTL_RESET_RSTAT _IOR(MAJOR,2,ssize_t *)
//сброс статистики записанных байт
#define IOCTL_RESET_WSTAT _IOR(MAJOR,3,ssize_t *)
//Структура для хранения состояния нашего устройства
struct dev_state{
int dev_open;
ssize_t byte_read;
ssize_t byte_write;
};
Допустим, мы хотим получить данные об общем количестве прочитанных и
записанных байт. Эти данные накапливаются при чтении-записи в
структуре состояния устройства dev_state.
В нашем файле определены четыре управляющих кода IOCTL_GET_RSTAT,
IOCTL_GET_WSTAT, IOCTL_RESET_RSTAT, IOCTL_RESET_WSTAT. код содержит
старший тип ioctl, неомер устройства, код команды и тип параметра
ioctl. Возможны четыре типа ioctl:
_IO - ioctl ез параметра;
_IOW - параметры копируются от пользователя в модуль;
_IOR - параметры заполняются модулем и передаются пользователю;
_IOWR - параметры могут передаваться в обе стороны.
Так как в коде ioctl присутствует старший номер устройства, то
динамическая регистрация устройства(с получением динамического
старшего номера устройств) невозможна. Регистрировать нжуно с заданным
старшим номером, в нашем лучае 42, т.е. при вызове функции
register_char_dev в качестве первого параметра передавать не 0, а
старший номер. Ниже приведена измененная часть кода регистрации
устройства:
int e;
//регистрация устройства
printk("Kernel: this is a test module startup message\n");
if((e=register_chrdev(MAJOR, DEV_NAME,&Fops))){
printk("Kernel: Register failed\n");
return e;
}
printk("Kernel: Device registered. Major number is %d\n", MAJOR);
Функция обработки ioctl в модуле выглядит так:
static int device_ioctl(struct inode *inode, struct file *filp, unsigned int ioctl, unsigned long param){
ssize_t *size = (ssize_t *)param;
printk("Kernel: ioctl request ");
switch(ioctl){
case IOCTL_GET_RSTAT:
printk("get read statistic \n");
*size = state[MINOR(inode->i_rdev)].byte_read;
break;
case IOCTL_GET_WSTAT:
printk("get write statistic \n");
*size = state[MINOR(inode->i_rdev)].byte_write;
break;
case IOCTL_RESET_RSTAT:
printk("reset read statistic \n");
state[MINOR(inode->i_rdev)].byte_read=0;
break;
case IOCTL_RESET_WSTAT:
printk("get write statistic \n");
state[MINOR(inode->i_rdev)].byte_write=0;
break;
}
return SUCCESS;
}
Думаю, что пространные комментарии не нужны. Только не забудьте
добавить в структуру file_operations ссылку на функцию ioctl:
struct file_operations Fops = {
open: device_open,
release: device_close,
read: device_read,
write: device_write,
ioctl: device_ioctl
};
Для иллюстрации применения ioctl я слегка доработал тестовое
приложение, теперь оно выглядит следующим образом:
/* тестовое приложение */
#include <module.h>
#include <ioctl.h>
#include "our_module.h";
int main(){
int fd;
size_t cnt = 0;
ssize_t cnt2 = 0;
char buffer[256];
printf("Test program\n");
fd = open("dev/mydev", O_RDWR);
if(fd == -1){
printf("open failed\n");
return -1;
}
printf("Device my_dev open\n");
cnt = read(fd, buffer, sizeof(buffer));
printf("Device my_dev read %d bytes\n", cnt);
cnt = write(fd, buffer, sizeof(buffer));
printf("Device my_dev write %d bytes\n", cnt);
if(ioctl(fd, IOCTL_GET_RSTAT, &cnt2)<0){
printf("ioctl failed\n");
}
printf("ioctl read stat is %d\n", cnt2);
if(ioctl(fd, IOCTL_GET_WSTAT, &cnt2)<0){
printf("ioctl failed\n");
}
printf("ioctl write stat is %d\n", cnt2);
close(fd);
printf("Device my_dev closed;\n");
return 0;
}
Литература:
1. Ори Померанц "Ядро Linux. Программирование модулей". Бесплатное
учебное пособие для начинающих. Часто встречается в интернете.
2. http://www.redhat.com/~johnson/devices.html - общие концепции и
начальные приемы написания модулей.
3. http://www.ssc.com/lj/issue23/1219.html - статья Алессандро
Руббини о написании модулей.
4. исходные тексты ядра - no comments.