Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Очереди выполнения прерывания
Драйверам устройств в Linux приходится работать с прерываниями, генерируемыми устройствами, которым они предоставляют интерфейс. Прерывания запускают обработчики прерываний в драйвере устройства и заставляют весь выполняемый в данный момент код - как пользовательский, так и код ядра - прервать свое выполнение. Поэтому желательно, чтобы обработчик прерывания драйвера устройства выполнялся как можно быстрее для предотвращения длительных остановок процессов ядра. Тем не менее это приводит к стандартной дилемме обработки прерываний: как нам обрабатывать прерывания, которые требуют выполнить достаточно много работы? Стандартный ответ заключается в использовании функций верхней половины и нижней половины. Функции верхней половины - это быстрые обработчики, прерывающие и планирующие функции нижней половины, в которых выполняется основная работа при возникновении такой возможности. Обычно функции верхней половины запускаются при отключенных прерываниях, для того чтобы обработчик прерывания не был прерван тем же прерыванием. Поэтому драйвер устройства не обрабатывает рекурсивные прерывания. Функции нижней половины обычно запускаются при включенных прерываниях, поэтому другие прерывания могут быть обработаны во время выполнения основной работы. В предыдущих ядрах Linux это разделение на функции верхней половины и нижней половины также называлось быстрыми и медленными прерываниями, которые обрабатывались очередями задач. Теперь в ядре Linux 2.6 появилась концепция очередей выполнения, которые в данный момент являются стандартным способом работы с прерываниями нижней половины. Когда ядро получает прерывание, процессор останавливает выполнение текущей задачи и немедленно обрабатывает прерывание. Когда процессор входит в этот режим, он обычно переводится в контекст прерывания. Ядро в контексте прерывания определяет, какому обработчику прерываний передать управление. Когда драйвер устройства хочет обработать прерывания, он использует request_irq () для запроса номера прерывания и регистрирует функцию-обработчик, которая будет вызвана при обнаружении этого прерывания. Регистрация обычно выполняется во время инициализации модуля. Функции Обход исходников обработчиков верхней половины регистрируются с помощью request_irq(), выполняют минимум работы и помещают выполняемую далее работу в очередь выполнения. Как и request_irq() в верхней половине, очереди выполнения обычно регистрируются во время инициализации модуля. Они могут инициализироваться статически с помощью макроса DECLARE_WORK () или структуры выполнения, которую можно выделить и инициализировать динамически с помощью вызова INIT_WORK (). Вот определение этих макросов: 3 0 #define DECLARE_WORK(n, f, d) \ 31 struct work_struct n = ________ WORK_INITIALIZER(n, f, d) 45 #define INIT_WORK(_work, _func, _data) \ 46 do { \ 47 INIT_LIST_HEAD(& (_work)-> entry); \ 48 (_work)-> pending =0; \ 49 PREPARE_WORK((_work), (_func), (_data)); \ 50 init_timer(& (_work)-> timer); \ 51 } while (0) Оба макроса получают следующие аргументы: • п или work. Имя создаваемой и инициализируемой структуры выполнения. • f или func. Функция, запускаемая, когда структура выполнения удаляется из очереди выполнения. • d или data. Хранит данные, передаваемые в функцию f или func, когда они запускаются. Функция-обработчик прерывания, зарегистрированная в register_irq(), далее принимает прерывание и посылает соответствующие данные из обработчика прерывания верхней половины в нижнюю половину с помощью настройки раздела данных work_struct и вызова schedule_work () для очереди выполнения. Присутствующий в очереди выполнения код функции работает в контексте процесса и поэтому может выполнять работу, которую невозможно выполнить в контексте прерывания, такую, как копирование из пользовательского пространства или в него или сон. Тасклеты похожи на очереди выполнения, но работают внутри контекста прерывания. Они полезны, когда вам нужно выполнить немного работы в нижней половине и вы хотите сохранить служебные обработчики прерываний верхней половины и нижней половины. Тасклеты инициализируются с помощью макроса DECLARE_TASKLET (): include/linux/interrupt.h 136 #define DECLARE_TASKLET(name, func, data) \ Глава 10 • Добавление вашего кода в ядро 137 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT (0), func, data } • name. Имя создаваемой структуры тасклета. • func. Вызываемая планировщиком функция тасклета. • data. Хранит данные, передаваемые в func при выполнении тасклета. Для планирования тасклета используется tasklet_schedule (): include/linux/interrupt.h 171 extern void FASTCALL(_ tasklet_schedule(struct tasklet_struct *t) ); 173 static inline void tasklet_schedule(struct tasklet_struct *t) 174 { 175 if (! test_and_setjDit(TASKLET_STATE__SCHED, & t-> state) ) 177 } • tasklet_struct. Имя тасклета, создаваемого с помощью DECLARE_TASKLET (). В обработчике прерывания верхней половины вы можете вызвать tasklet_schedule () и гарантировать, что когда-нибудь в будущем определенная в тасклете функция будет выполнена. Тасклеты отличаются от очередей выполнения тем, что различные тасклеты могут одновременно выполняться на разных процессорах. Если тасклет уже запланирован и перепланирован снова перед его выполнением, то он будет выполнен только один раз. Так как тасклеты выполняются в контексте прерывания, они не могут спать или копировать данные в пользовательское пространство. Из-за работы в контексте прерывания, если различным тасклетам требуется общаться друг с другом, они могут делать это только с помощью циклических блокировок. Системные вызовы Существуют и другие способы добавления кода в ядро помимо драйверов устройств. Системные вызовы ядра Linux (syscalls) представляют собой метод, с помощью которого программы пользовательского пространства могут получать доступ к службам ядра и аппаратному обеспечению системы. Программам пользовательского режима доступны многие библиотечные С-функции и один или несколько системных вызовов для выполнения отдельных функций. На самом деле получить доступ к системным вызовам можно и из кода ядра. По своей натуре реализация системных вызовов является аппаратно-специфической. На архитектуре Intel все системные вызовы используют программное прерывание 0x80. Параметры системных вызовов передаются через регистры общего назначения. Реализа- 10.1 Обход исходников ция системных вызовов на архитектуре х86 ограничивает количество параметров пятью. Если требуется больше 5 параметров, можно передать указатель на блок параметров. Во время выполнения ассемблерной инструкции int 0x80 вызывается специальная функция ядра с помощью механизма обработки исключений процессора. Другие типы драйверов До сих пор все драйверы устройств, с которыми мы имели дело, были символьными устройствами. Их намного проще понять, но вы можете захотеть написать и другие драйверы, которые общаются с ядром по-другому. Блочные устройства похожи на символьные устройства в способе доступа к ним через файловую систему; /dev/hda - это файл устройства в файловой системе для первичного ШЕ-диска. Блочные устройства регистрируются и удаляются из системы так же, как и символьные устройства, с помощью функций register_blkdev() и unregister_blkdev(). Основная разница между блочными устройствами и символьными устройствами заключается в том, что блочные устройства не предоставляют собственной функциональности для чтения и записи; вместо этого они используют метод запросов. Ядро 2.6 претерпело несколько серьезных изменений в подсистеме блочных устройств. Старые функции, такие, как block__read() и block_write(), а также структуры ядра, такие, как blk_size и blksize_size, убраны. Этот раздел фокусируется исключительно на реализации блочных устройств в ядре 2.6. Если вам нужно, чтобы ядро Linux работало с дисковым устройством (или подобным диску устройством), вам нужно написать драйвер блочного устройства. Драйвер должен информировать ядро о том, с каким диском оно имеет дело. Это делается с помощью структуры gendisk: include/linux/genhd.h 82 struct gendisk { 83 int major; /*старший номер драйвера */ 84 int first_minor; 85 int minors; 86 char disk_name[32]; /* имя старшего драйвера */ 87 struct hd_struct **part; /* [индексация по младшему] */ 88 struct block_device_operations *fops; 89 struct request_queue *queue; 90 void *private_data; 91 sector_t capacity; Глава 10 • Добавление вашего кода в ядро Строка 83 ma j or - это старший номер блочного устройства. Он может устанавливаться статически или может динамически генерироваться с помощью register_ blkdev (), как и в случае с символьным устройством. Строки 84-85 first__minor и minors используются для определения количества разделов в блочном устройстве; minors содержит максимальное количество младших номеров имеющихся устройств; f irst_minor содержит первый младший номер устройства для блочного устройства. Строка 86 disk_name - это 32-символьное имя для блочного устройства. Оно появляется в файловой системе /dev, sysf s и /proc/partitions. Строка 87 hd_struct устанавливает разделы, связанные с блочным устройством. Строка 88 fops - это указатель на структуру block_operations, содержащую операции open, release, ioctl, media_changed и revalidate__disk. (См. include/linux/f s.h.) В ядре 2.6 каждое устройство имеет собственный набор операций. Строка 89 reques t_cueue - это указатель на очередь, помогающую управлять связанными с устройством операциями. Строка 90 private_data указывает на информацию, недоступную из блочной подсистемы ядра. Обычно используется для сохранения данных, используемых в низкоуровневых устройствозависимых операциях. Строка 91 capacity - это размер блочного устройства в секторе из 512 байт. Если устройство является съемным, таким, как флоппи-диск или CD, capacity 0 означает, что диск отсутствует. Если ваше устройство не использует секторы 512 байт, вам нужно установить эту переменную в соответствующий эквивалент. Например, если выше устройство имеет тысячу 256-байтовых секторов, это эквивалентно пятистам 512-байтовых секторов. Дополнительно для того, чтобы иметь структуру gendisk, блочному устройству нужна структура циклической блокировки для использования с очередью запроса. Обход исходников Как циклическая блокировка, так и поля структуры gendisk должны быть инициализированы драйвером устройства. (Демонстрацию инициализации диска в памяти драйвером блочного устройства можно найти по адресу http: //en.wikipedia.org/ wiki/Ram_disk.) После инициализации устройства и готовности к обработке запросов для добавления блочного устройства в систему должна быть вызвана функция add_disk(). Наконец, если блочное устройство будет использоваться в качестве источника энтропии для системы, модуль инициализации может также вызывать и add_disk_randomness (). (Более подробную информацию см. в drivers/char/ random, с.) Теперь, когда мы раскрыли основы инициализации блочного устройства, мы можем рассмотреть его добавление, выход и очистку драйвера блочного устройства. В ядре Linux 2.6 это делается очень просто. del_gendisk (struct gendisk) удаляет gendisk из файловой системы и очищает информацию о разделах. За этим вызовом должен следовать put disk (struct gendisk), освобождающий ссылку в ядре на gendisk. Блочное устройство удаляется с помощью вызоваunregister_blkdev(int major, char [16] device_name), что позволяет далее освободить структуру gendisk. Также нам нужно освободить связанную с драйвером блочного устройства очередь выполнения. Это делается с помощью blk_cleanup_queue (struct * request_ queue). ПРИМЕЧАНИЕ. Если вы можете только ссылаться на очередь выполнения через структуру gendisk, необходимо вызывать blk_cleanup_queue перед освобождением gendisk. В обзоре инициализации и выключения блочного устройства мы можем легко избежать разговора о специфике очереди выполнения. Но теперь, когда драйвер настроен, нужно что-то сделать, а очередь выполнения - это именно то, что предоставляет основные функции для чтения и записи блочного устройства. include/linux/blkdev.h 576 extern request_queue_t *blk_init_queue(request_fn_proc *, spinlock_t *); Строка 576 Для создания очереди выполнения мы используем blk_init_queue и передаем указатель на циклическую блокировку для управления доступом к очереди и указатель на функцию запроса, вызываемого при доступе к устройству. Функция доступа должна иметь следующий прототип: Глава 10 • Добавление вашего кода в ядро static void my_request__function( request_queue *q); Внутренняя реализация функции запроса обычно использует несколько вспомогательных функций. Для определения следующего обрабатываемого процесса вызывается функция elv_next_request () и возвращается указатель на структуру запроса или возвращается нуль, если следующего запроса нет. В ядре 2.6, драйвер блочного устройства работает через структуру ВЮ в структуре запроса. ВЮ расшифровывается как Block I/O (блочный ввод-вывод) и полностью определяется в include/linux/bio.h. Структура ВЮ содержит указатель на список структур biovec, определенных следующим образом: include/linux/bio.h 47 struct bio_vec { 48 struct page *bv_page; 49 unsigned int bv_len; 50 unsigned int bv_offset; 51 }; Каждый biovec использует свою структуру страниц для хранения буферов данных, в которые в конечном счете пишутся и считываются данные с диска. Ядро 2.6 имеет несколько вспомогательных bio для перемещения по данным, хранящимся в структуре bio. Для определения размера операции ВЮ, вы можете также обратиться к структуре bio_size в структуре ВЮ для получения результата в байтах или использовать макрос bio_sectors () для получения размера в секторах. Тип блочной операции, READ или WRITE, можно определить с помощью bio_data_dir (). Для перемещения по списку biovec в структуре ВЮ используется макрос bio_f or_each_segment (). Внутри цикла по мере углубления в biovec можно использовать и другие макросы - bio_page (), bio_of f set (), bio_curr_sectors () и bio_data(). Более подробную информацию можно найти в include/ linux. bio. h и Documentation/block/biodoc. txt. Некоторые комбинации информации, содержащейся в biovec и структурах страниц, позволяют вам определить, какие данные нужно читать и какие записывать на блочное устройство. Низкоуровневые детали того, как читать и писать на устройство, связаны с аппаратным обеспечением этого драйвера блочного устройства. Теперь мы знаем, как работать со структурой ВЮ, и нам остается только понять, как работать со структурой списка запросов структур ВЮ. Это делается с помощью другого макроса - rq_f or_each__bio. 10.1 Обход исходников include/linux/blkdev.h 495 #define rq_for_each_bio(„bio, rq) \ 496 if ((rq-> bio)) \ 497 for (_bio = (rq)-> bio; _bio; _bio = bio-> bi_next) Строка 495 bio - это текущая структура BIO, a rq - итератор для перемещения по ней. После обработки каждой структуры ВЮ драйвер должен обновить ядро в соответствии со своим прогрессом. Это делается с помощью end_that_request_f irst (). include/linux/blkdev.h 557 extern int end_that_request_first(struct request *, int, int); Строка 557 Первый аргумент int должен быть ненулевым во избежание возникновения ошибки, а второй аргумент int представляет количество секторов, обработанных устройством. Когда end_that_request__f irst () возвращает 0, все процессы обработаны и необходимо произвести очистку. Это делается с помощью вызова blkdev_dequeue_ request () и end_that_request_last () в том же порядке - обе функции получают запрос как единственный аргумент. После этого функция запроса выполняет свою работу, а блочная подсистема использует функцию очереди запроса драйвера блочного устройства для выполнения дисковой операции. Устройству нужно обработать несколько функций ioctl, так как наш диск в памяти обрабатывает разделы, хотя это тоже зависит от типа блочного устройства. Этот раздел касается только основ блочного устройства. Существуют перехватчики Linux для операций DMA, кластеризация, подготовка команд очереди запросов и другие особенности более сложных блочных устройств. Теперь вы можете переходить к чтению документации из Documentation/block. Модель устройства и sysfs В ядре Linux 2.6 появилась новая модель устройств, тесно связанная с sysfs. Модель устройства хранит набор внутренних данных, связанных с устройствами и драйверами в системе. Система следит за существованием этих устройств и разбивает их на классы: блоки, ввод, шины и т. д. Кроме этого, система следит за тем, какие драйверы существуют и как они связаны с управляемыми ими устройствами. Модель устройства присутствует в ядре, a sysfs - это окно в эту модель. Из-за того что некоторые устройства Глава 10 • Добавление вашего кода в ядро и драйверы не показывают себя через sysf s, о sysf s стоит думать как об общедоступной демонстрации модели устройств ядра. Некоторые устройства имеют несколько вхождений в sysf s. В модели устройства хранится только копия данных, хотя получить доступ к этим данным можно множеством способов, как показано в символической ссылке в дереве sysf s. Иерархия sysf s связана со структурами ядра kobj ее t и kset. Модель достаточно сложна, но большинство написанных драйверов не опускаются до реализации ее мелких деталей1. С помощью концепции атрибутов sysf s вы работаете с kobject в абстрактном режиме. Атрибуты - это части устройства или модели драйвера, к которым можно получить доступ через файловую систему sysf s. Они могут быть внутренней переменной модуля, контролирующей, как модуль управляет задачами, или могут быть напрямую связаны с различными аппаратными настройками. Например, RF-передатчик может иметь базовую частоту, на которой он работает, в то время как индивидуальная настройка реализована в виде отступа от базовой частоты. Изменение базовой частоты может быть выполнено с помощью изменения атрибута модуля в драйвере RF в sysf s. При доступе к атрибуту sysf s вызывает функцию для обработки этого доступа, show () для чтения и store () для записи. Существует одностраничное ограничение на размер данных, которые можно передать в функции show () или store (). Вооружившись пониманием того, как работает sysf s, мы можем перейти к тонкостям того, как драйвер регистрируется в sys f s, отображает атрибуты и регистрирует специальные функции show () и store () для работы с атрибутами, к которым мы получаем доступ. Первой задачей является определение, к какому классу относится ваше новое устройство и под какой класс попадает его драйвер (например, usb_device/ net_devi.ee, pci_device, sys_device и т. д.). Все эти структуры имеют поле char *name; sysf s употребляет это поле с именем для демонстрации нового устройства в иерархии sysf s. После выделения и именования структуры устройства вам нужно создать инициализацию структуры devicer_driver. include/linux/device.h 102 struct device_driver { 103 char * name; 104 struct bus_type * bus; 105 10 6 struct semaphore unload_sem; 107 struct kobject kobj; 108 struct list_head devices; 109 '■ Обратитесь к файлу Documentation/f ilesystems/sysf s. txt в исходниках ядра. Обход исходников 110 int (*probe) (struct device * dev); 111 int (*remove) (struct device * dev); 112 void (*shutdown) (struct device * dev); 113 int (*suspend) (struct device * dev, u32 state, u32 level); 114 int (*resume) (struct device * dev, u32 level); 115 }; Строка 103 name связана с отображаемым именем драйвера в иерархии sysf s. Строка 104 bus обычно заполняется автоматически; автору драйвера не нужно из-за волноваться. Строки 105-115 Программисту не нужно заполнять оставшиеся поля. Они должны инициализировать автоматически на уровне шины. Мы должны зарегистрировать наш драйвер во время инициализации с помощью вызова driver_register (), который передает большую часть работы bus_add_ driver (). Аналогично при выходе из драйвера нужно добавить вызов driver_unregister (). drivers/base/driver.с 8 6 int driver__register(struct device_driver * drv) 87 { 88 INIT_LIST_HEAD(Scdrv-> devices); 89 init_MUTEX_LOCKED(& drv-> unload_sem); 90 return bus_add_driver(drv); 91 } После регистрации драйвера атрибуты драйвера могут быть созданы с помощью структур driver_attribute и вспомогательных макросов DRIVER_ATTR: include/linux/device.h 133 #define DRIVER_ATTR(_name, _mode, _show, _store) \ 13 4 struct driver_attribute driver_attr_##_name = { \ 135.attr = {.name = _ stringify(_name), .mode = _mode, .owner = THIS_MODULE }, \ 13 6.show = _show, \ 137.store = _store, \ 138 }; Глава 10 • Добавление вашего кода в ядро Строка 135 name - это имя атрибута драйвера; mode - это битовая карта, описывающая уровень защищенности атрибута; include/linux/stat.h содержит большинство из этих режимов, примерами которых могут служить S_IRUGO (только чтение) и S_IWUSR (доступ на запись только для root). Строка 136 show - имя функции драйвера, используемой при чтении атрибута через sysf s. Если чтение не разрешено, следует использовать NULL. Строка 137 store - это имя функции драйвера, используемой при записи атрибутов через sysf s. Если запись не разрешена, следует использовать NULL. Функции драйвера, реализующие show() и store () для специального драйвера, должны иметь приведенные ниже прототипы: include/linux/sysfs.h 34 struct sysfs_ops { 35 ssize_t (*show)(struct kobject *, struct attribute *, char *); 36 ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t); 37 }; Вспомните, что размер читаемых и записываемых через sysf s данных атрибутов ограничен PAGE_SIZE байтами. Функции атрибутов драйвера show () и store () должны учитывать это ограничение. Эта информация должна позволить вам добавлять базовую функциональность в драйвер устройства ядра. Подробности реализации sysf s и kobj ect можно прочитать в директории Documentation/device-model. Другим типом драйверов устройств являются драйверы сетевых устройств. Сетевые устройства передают и получают пакеты данных и могут быть не обязательно аппаратными устройствами - устройство обратной связи (loopback) - это программное сетевое устройство. Написание кода Л Основы устройств Когда вы создаете драйвер устройства, он связывается с операционной системой через некоторый элемент (файл) в файловой системе. Этот элемент имеет старший номер, который показывает для ядра, какой драйвер использовать, когда на файл ссылаются. Этот Написание кода 545 файл имеет также младший номер, который может использовать сам драйвер для уточнения информации. Когда драйвер устройства загружен, он регистрирует свой старший номер. Эту регистрацию можно увидеть через /proc/devices. lkp# less /proc/devices Character devices:
Это число вводится в /proc/devices, когда драйвер регистрирует себя в ядре; для символьных устройств оно вызывается функцией register_chrdev (). include/linux/fs.h 1: int register_chrdev(unsigned int major, const char *name, 2: struct file_operations *fops) • major. Старший номер регистрируемого устройства. Если ma j or равен 0, ядро динамически назначает старшее число, не конфликтующее с другими загруженными модулями. • name. Строка представления устройства в дереве /dev файловой системы. • fops. Указатель на структуру файловых операций, определяющих, какие операции можно выполнить на зарегистрированном устройстве. Использование 0 в качестве старшего номера связано с методом создания номеров устройств для устройств, которые не устанавливают старшие номера (приводы IDE всегда используют 3, SCSI - 8, флоппи-дисководы - 2). С помощью динамического назначения Глава 10 • Добавление вашего кода в ядро старших номеров мы можем избежать проблемы выбора старшего номера, который уже был выбран другим устройством1. Последовательность создания узла файловой системы немного усложняется из-за того, что после загрузки модуля мы должны проверить, какой старший номер назначается устройству. Например, во время тестирования устройства вам нужно сделать следующее: lkp@lkp# insmod my_module.o lkp@lkp# less /proc/devices 1 mem 233 my_module lkp@lkp# mknod с /dev/my_moduleO 233 0 lkp@lkp# mknod с /dev/my_modulel 233 1 Этот код показывает, как мы можем вставлять наш модуль с помощью команды insmod, которая инсталлирует загружаемый модуль в запущенное ядро. Код нашего модуля содержит следующие строки: static int my_module_major=0; module_param(my_module_major/ int, 0); result = register_chrdev(ray_module_major/ " my_module", & my_module_fops); Первые две строки демонстрируют, как мы создаем старшее число по умолчанию со значением 0 для динамического назначения и даем возможность пользователю его переопределять с помощью переменной my_rnodule_major в качестве параметра: include/linux/moduleparam.h 1: /* Это фундаментальная функция для регистрации параметров загрузки/ модуля; perm устанавливает видимость в driverfs: 000 значит, что его нет; read-биты значат, что он читаемый; write значит, что он перезаписываемый. */ /* Вспомогательные функции: byte, short, ushort, int, uint, long, ulong, charp, bool или invbool, или XXX, если вы определите param_get_XXX, param_set_XXX и param_check_XXX. */ 1 Функция register_chrdev() возвращает назначенный старший номер. Он может быть полезным для получения информации при динамическом назначении старших номеров. 10.2 Написание кода 2: #define module_param(name, type, perm) В предыдущих версиях Linux макросом module_param был MODULE_PARM; в версии 2.6 он упразднен и вместо него нужно использовать module_param. • name. Строка, используемая для доступа к переменной параметра. • type, тип значения, сохраняемого в параметре name. • perm. Имя параметра, видимого в sysf s. Если вы не знаете, как его отображать в sysf s, используйте значение 0, что означает, что параметр не будет виден через sysf s. Вспомните, что мы передаем в register_chdev() указатель на структуру fops. Это говорит ядру, что функции обрабатываются ядром. Мы определяем только те функции, которые обрабатывает модуль. Мы объявляем, что read, write, ioctl и open-корректные операции для зарегистрированного нами устройства, и добавляем следующий код: struct file_operations my_mod_fops = { .read = my_mod_read, .write = my_mod_write, .ioctl = my_mod_ioctl, .open = my_niod_open/ }; Символьное экспортирование Во время написания сложного драйвера устройства может возникнуть необходимость в экспорте некоторых объявленных в драйвере символов для использования другим модулем ядра. Обычно это используется в низкоуровневых драйверах, над которыми надстраиваются драйверы более высоких уровней. При загрузке драйвера устройства любые символы экспорта помещаются в таблицу символов ядра. Загружаемые последовательно драйверы могут использовать символы, экспортированные предыдущими драйверами. Когда модули зависят друг от друга, становится важным порядок их загрузки; вызов insmod провалится, если символы, используемые модулем высокого уровня, отсутствуют. В ядре Linux 2.6 программисту драйверов доступны два макроса для экспорта символов: include/linux/module.h 187 #define EXPORT_SYMBOL(sym) \ Глава 10 • Добавление вашего кода в ядро 188 __ EXPORT_SYMBOL(sym, " " ) 190 #define EXPORT_SYMBOL_GPL(sym) \ 191 __ EXPORT_SYMBOL(sym, и_gp1" ) Макрос EXPORT_SYMBOL позволяет данному символу стать видимым для других частей ядра с помощью помещения его в таблицу символов ядра. EXPORT_SYMBOL_GPL позволяет только модули, объявленные совместимыми с лицензией GPL в своем атрибуте MODULE_LICENSE. (См. полный список лицензий в include/linux/module.h.) IOCTL До сих пор мы в основном имели дело с драйверами устройств, выполняющими действия по чтению и записи только со своего собственного устройства. Что произойдет, когда у вас будет устройство, способное делать больше чем чтение и запись данных? Или у вас есть устройство, которое может выполнять разные операции чтения и записи? Или ваше устройство требует интерфейса для аппаратного контроля? В Linux драйверы устройств обычно используют для решения этих проблем метод ioctl. ioctl - это системный вызов, позволяющий драйверам устройств обрабатывать специальные команды, которые можно использовать для управления каналами ввода-вывода. Вызов ioctl драйвера устройства должен следовать за определением в структуре file_operations: include/linux/fs.h 863 struct file_operations { 872 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); Из пользовательского пространства вызов функции ioctl определен следующим образом: int ioctl (int d, int request, ...); Третий аргумент в определении пользовательского пространства является нетипи-зированным указателем на память. Через него данные передаются из пользовательского пространства в реализацию ioctl драйвера устройства. Это может сложно звучать, но на самом деле использовать ioctl с драйвером достаточно просто. Во-первых, мы хотим определить, какие номера IOCTL верны для нашего устройства. Мы должны проконсультироваться с файлом Documentation/ioctl-num-ber. txt и выбрать код, не используемый машиной. Проконсультировавшись с текущим Написание кода файлом 2.6, мы увидим, что код ioctl для чд' сейчас не используется. В нашем драйвере мы требуем это в следующем коде: #define MYDRIVER_IOC_MAGIC Лд' Для каждого получаемого драйвером управляющего сообщения нам нужно определить уникальный номер ioctl. Он основан на только что определенном магическом числе: #define MYDRIVER_IOC_OPl _I0(MYDRIVER_IOC_MAGIC, 0) #define MYDRIVER_IOC_OP2 _IOW(MYDRIVER_IOC_MAGIC, 1) #define MYDRIVER_IOC_OP3 _IOW(MYDRIVER_IOC_MAGIC, 2) #define MYDRIVER_IOC_OP4 _I0RW(MYDRIVER_I0C_MAGIC, 3) Четырем перечисленным операциям (opl, op2, орЗ и op4) назначены уникальные номера ioctl, с использованием определенных в include/asm/ioctl.h макросов и MYDRIVER_IOC_MAGIC, являвшимся нашим магическим номером ioctl. Документация красноречиво сообщает о том, что это значит: 6 Если вы добавляете новые ioctl's в ядро, вам нужно использовать макрос 7 _10 определенный в < linux/ioctl.h>: 8
an ioctl with no parameters an ioctl with write parameters (copy_from_user) an ioctl with read parameters (copy_to_user) an ioctl with both write and read parameters. и 'read' с пользовательской точки зрения выглядят как 15 системные вызовы 'write' и 'read'. Для примера, SET_FOO ioctl будет 16 _IOW, так как ядро будет читать данные из пользовательского 17 пространства, a GET_F00 ioctl будет _IOR, так как ядро будет 18 записывать данные в пользовательское пространство. Из пользовательского пространства мы можем вызвать команду ioctl следующим образом: ioctl(fd, MYDRIVER_I0C_0P1/ NULL); ioctl(f d, MYDRIVER_I0C_0P2, & mydata); ioctl(fd, MYDRIVER_I0C_0P3, mydata); ioctl(fd, MYDRIVER_I0C_0P4, & mystruct); Глава 10 • Добавление вашего кода в ядро Программе пользовательского пространства необходимо знать, какую ioctl команду (в нашем случае MYDRIVER_I0C_0P1... MYDRIVER_I0C_0P4) и тип аргументов команды ожидать. Мы должны вернуть значение с помощью кода возврата системного вызова ioctl или мы можем интерпретировать параметр как указатель на набор для чтения. В последнем случае помните, что указатель связан с разделом в пользовательском пространстве памяти, которая должна быть скопирована из ядра или в ядро. Простейшим способом перемещения памяти между пользовательским пространством и пространством ядра в функции ioctl является применение функций put_user () и get_user (), определенных следующим образом: include/asm-i38б/uaccess.h * get_user: - Получение простых переменных из пользовательского * пространства. * @х: переменная для сохранения результата. * @ptr: Исходный адрес в пользовательском пространстве. * put_user: - Запись простого значения в пользовательское пространство. * @х: Значение для копирования в пользовательское пространство. * @ptr: Адрес назначения в пользовательском пространстве. put_user () и get_user () проверяют, чтобы память пользовательского пространства была доступна для чтения или записи во время вызова. Существует еще одно дополнительное ограничивающее условие, которое вы можете захотеть добавить к функциям вашего драйвера устройства: аутентификация. Одним из способов проверки того, что процессу, который вызывает вашу функцию ioctl, разрешено это делать, является использование совместимостей. Обычно для авторизации используется CAP_SYS_ADMIN: include/linux/capability.h
Позволяет настройку ключа сообщения безопасности */ Позволяет администрировать устройство random */ Позволяет увидеть и сконфигурировать квоты диска */ Позволяет настроить syslog ядра (поведение printk */ Позволяет установить domainname */ Позволяет установить hostname */ Позволяет вызвать bdflushO */ Позволяет установить mount () и umountO для нового smb-соединения */ Позволяет некоторые autofs root ioctls */ Позволяет nfsservctl */ Позволяет VM86_REQUEST_IRQ */ 213 /* Позволяет читать-записывать pci config на alpha процессорах*/ Написание кода
214 /* Позволяет irix_prctl Hamips (setstacksize) */ 215 /* Позволяет обрабатывать весь кtin на m68k (sys_cacheflush) */ Позволяет удаление семафоров */ Используется вместо CAP_CHOWN для " chown" очередей сообщений IPC, семафоров и разделяемой памяти */ Позволяет блокирование-разблокирование сегментов разделяемой памяти */ Позволяет включать-выключать свопинг */ Позволяет обмануть pids для переданного мандата сокета */ Позволяет установку буферов readahead и flushing для блочных устройств */ Позволяет установку геометрии для драйвера флопи-диска */ Позволяет настроить включение-выключение DMA on/off в драйвере Позволяет администрирование устройств md (обычно выше, но иногда и дополнительные ioctls) */ Позволяет настраивать драйверы ide */ Позволяет получить доступ к устройству nvram */ Позволяет администрирование apm_bios, последовательного и bttv (TV) устройства*/ Позволяет использовать команды производителя в драйвере поддержки isdn CAPI */ Позволяет считывать нестандартизированные порции пространства настройки pci */ Позволяет отладку DDI debug ioctl для драйвера sbpcd */ 233 /* Позволяет настраивать последовательные порты */ 234 /* Позволяет посылать сырые команды qic-117 */ 235 /* Позволяет включить/выключить теговые запросы для контроллера SCSI и посылки 23 6 специальных SCSI-команд*/ 237 /* Позволяет настроить ключ раскодирования для циклической файловой системы */ 238 239 #define CAP_SYS_ADMIN 21 Многие другие совместимости из include/linux/compatibility.h могут лучше подходить для вашего драйвера, но CAP_SYS_AMIN вмещает их все. Для проверки совместимости вызываемого процесса с вашим драйвером вы можете добавить нечто наподобие следующего кода: if (! capable(CAP_SYS_ADMIN)) { return -EPERM; Глава 10 • Добавление вашего кода в ядро Популярное:
|
Последнее изменение этой страницы: 2016-03-25; Просмотров: 734; Нарушение авторского права страницы