The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Написание драйверов в Linux (linux driver gcc)


<< Предыдущая ИНДЕКС Исправить src / Печать Следующая >>
Ключевые слова: 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 [email protected]"); 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 [email protected]"); 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 [email protected]"); 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 [email protected]"); 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 [email protected]"); 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.

<< Предыдущая ИНДЕКС Исправить src / Печать Следующая >>

Обсуждение [ RSS ]
  • 1.1, Аноним (-), 19:50, 30/06/2011 [ответить]  
  • +/
    Часть статьи дублируется
     
  • 1.2, burder (ok), 22:46, 22/11/2011 [ответить]  
  • +/
    Уже вижу Вас бородатым с взъерошенными волосами,сколько суток не спали....
     
  • 1.3, zonch (?), 12:20, 06/05/2012 [ответить]  
  • +/
    Большое спасибо.Хоть ядро сейчас у меня лично уже 3.3.4 ,но статья в образовательном плане актуальности не потеряла,очень помогла!
     
  • 1.4, Maksim Kurbatov (?), 14:11, 23/10/2012 [ответить]  
  • +/
    добрый день. не могли бы вы мне объяснить зачем при возврате ошибок к макросу добавляется знак -
    "-EBUSY"
     
     
  • 2.5, Bob (??), 16:34, 01/02/2013 [^] [^^] [^^^] [ответить]  
  • +/
    чтобы отрицательное значение возвращать
     

  • 1.6, Костярин (?), 04:57, 27/12/2014 [ответить]  
  • +/
    круто-круто
     
  • 1.7, Евгений (??), 14:09, 05/02/2015 [ответить]  
  • +/
    Мне понравилось. Параллельно смотрел в код драйвера (который не работает как положено) - хот начал понимать откуда растут ноги...
     

     Добавить комментарий
    Имя:
    E-Mail:
    Заголовок:
    Текст:




    Партнёры:
    PostgresPro
    Inferno Solutions
    Hosting by Hoster.ru
    Хостинг:

    Закладки на сайте
    Проследить за страницей
    Created 1996-2024 by Maxim Chirkov
    Добавить, Поддержать, Вебмастеру