Архитектура драйвера
Типичный протокол работы с внешним устройством состоит из анализа запроса, передачи команды устройству, ожидания прерывания по завершении этой команды, анализа результатов операции и формирования ответа внешнему устройству. Многие запросы не могут быть выполнены в одну операцию, поэтому анализ результатов операции может привести к выводу о необходимости передать устройству следующую команду.
Драйвер, реализующий этот протокол, естественным образом распадается на две нити: основную, которая осуществляет собственно обработку запроса, и обработчик прерывания. В зависимости от ситуации, основная нить может представлять собою самостоятельную нить, либо ее код может исполняться в рамках нити, сформировавшей запрос.
В примере 10.1 приводится скелет функции write () драйвера последовательного устройства в системе Linux. Скелет упрощенный (в частности, никак не решается проблема реентерабельности функции foo_write. Использованный механизм синхронизации с обработчиком прерывания также оставляет желать лучшего), но имеет именно такую архитектуру, которая была описана ранее. Текст цитируется по документу [HOWTO khg], перевод комментариев и дополнительные комментарии автора.
Пример 10.1. Скелет драйвера последовательного устройства для ОС Linux.
f* Основная нить драйвера */ static int foo_write(struct inode * inode, struct file * file, char * buf, int count) /* Получить идентификатор устройства: */ к/с в операционные системы unsigned int minor = MINOR(inode › i_rdev); unsigned long copy size; unsigned long total_bytes_written = 0; unsigned long bytes__written; /* Найти блок переменных состояния устройства */ struct foo_struct *foo = &foo_table[minor]; do { copy_size = (count <= FOO_BUFFER_SIZE? count: FOOJ3UFFER_'SIZE); /* Передать данные из пользовательского контекста */ memcpy_fromfs(foo › foo_buffer, buf, copy_size); while (copy_size) { /* Здесь мы должны инициализировать прерывания*/ if (some_error_has_occured) { /* Здесь мы должны обработать ошибку */ current › timeout = jiffies + FOO_INTERRUPT_TIMEOUT; /* Установить таймаут на случай, если прерывание будет пропущено */ interruptible_sleep_on (&f oo › foo_wait_queue); if (some_error_has_occured) { /* Здесь мы должны обработать ошибку */ bytes_written = foo › bytes_xfered; foo › bytes_written = 0; if (current › signal H ~current › blocked) { if (total_bytes_written + bytes__written) return total_bytes_written + bytes_written; else return – EINTR; /* Ничего не было записано, системный вызов был прерван, требуется повторная попытка */ O – Драйверы внешних устройств total_byr.c5_v;r:.i.U-.r. т= bytes_written; buf += bytes_written; count – = bytes_written; ) while (count > 0); return total_bytes_written; /* Обработчик прерывания */ static void foo__interrupt (int irq) { struct foo_struct *foo = &foo__table [foo_irq[irq] ]; /* Здесь необходимо выполнить все действия, которые должны быть выполнены по прерыванию. Флаг в foo__table указывает, осуществляется операция чтения или записи. */ /* Увеличить foo › bytes_xfered на количество фактически переданных символов * / if (буфер полон/пуст) wake_up_interruptible (&foo › foo_wait_queue); }
Примечание
Обратите внимание, что кроме инициализации устройства драйвер перед засыпанием еще устанавливает "будильник" – таймер, который должен разбудить процесс через заданный интервал времени. Это необходимо на случай, если произойдет аппаратная ошибка и устройство не сгенерирует прерывания. Если бы такой будильник не устанавливался, драйвер в случае ошибки мог бы заснуть навсегда, заблокировав при этом пользовательский процесс. В нашем случае таймер также используется, чтобы разбудить процесс, если прерывание произойдет до вызова interruptible_sleep_on основной нитью.
Многие устройства, однако, требуют для исполнения некоторых, даже относительно простых, операций, несколько команд и несколько прерываний. Так, при записи данных посредством контроллера гибких дисков, драйвер должен:
- включить мотор дисковода;
- дождаться, пока диск разгонится до рабочей скорости (большинство контроллеров генерируют по этому случаю прерывание);
- дать устройству команду на перемещение считывающей головки;
- дождаться прерывания по концу операции перемещения;
- запрограммировать ПДП и инициировать операцию записи;
- дождаться прерывания, сигнализирующего о конце операции.
Лишь после этого можно будет передать данные программе. Наивная реализация таких многошаговых операций могла бы выглядеть так (за основу по-прежнему взят код из [HOWTO khg], обработка ошибок опущена), как показано в примере 10.2.