Ввод/вывод, управляемый прерыванием
Всякий раз, когда передача данных в или из управляемого оборудования может быть отложена по любой причине, автору драйвера следует реализовывать буферизацию. Буферы данных помогают отделить передачу и приём данных от системных вызовов write и read, а также повысить общую производительность системы.
Хороший буферный механизм приводит к вводу/выводу, управляемому прерываниями, в котором входной буфер заполнен во время прерывания и очищается процессом, которые читает устройство; выходной буфер заполняется процессами, которые пишут в устройство, и опустошается во время прерывания. Примером управляемого прерыванием вывода является реализация /dev/shortprint. Чтобы управляемая прерыванием передача данных происходила успешно, оборудование должно быть способно генерировать прерывания со следующей семантикой:
- Для ввода, устройство прерывает процессор, когда получены новые данные и они
готовы для получения процессором системы. Фактические действия для выполнения
зависят от того, использует ли устройство порты ввода/вывода, отображение на память,
или DMA. - Для вывода, устройство обеспечивает прерывание или когда оно готово принять новые
данные, или для подтверждения успешной передачи данных. Устройства,
использующие отображение на память, и DMA-совместимые устройства обычно
генерируют прерывания, чтобы сообщить системе, что они завершили работу с
буфером.
Отношения синхронизации между read или write и фактическим получением данных представлены в разделе "Блокирующие и неблокирующие операции" в Главе 6.
Мы уже несколько раз упоминали драйвер shortprint; теперь пришло время действительно посмотреть на него. Этот модуль реализует очень простой, ориентированной на вывод драйвер для параллельного порта; однако, этого достаточно, чтобы разрешить печать файлов. Однако, если вы решили проверить вывод этого драйвера, помните, что вы должны передать принтеру файл в формате, который он понимает; не все принтеры хорошо реагируют, когда получают поток произвольных данных.
Драйвер shortprint поддерживает одностраничный круговой буфера вывода. Когда процесс пользовательского пространства записывает данные в это устройство, эти данные поступают в буфер, но метод write не выполняет какой-либо фактический ввод/вывод. Вместо этого, ядро shortp_write выглядит следующим образом:
while (written < count) {
/* Выйти обратно, пока не освободится место в буфере. */
space = shortp_out_space( );
if (space <= 0) {
if (wait_event_interruptible(shortp_out_queue,
(space = shortp_out_space( )) > 0))
goto out;
}
/* Переместить данные в буфер. */
if ((space + written) > count)
space = count - written;
if (copy_from_user((char *) shortp_out_head, buf, space)) {
up(&shortp_out_sem);
return -EFAULT;
}
shortp_incr_out_bp(&shortp_out_head, space);
buf += space;
written += space;
/* Если вывод неактивен, сделать его активным. */
spin_lock_irqsave(&shortp_out_lock, flags);
if (! shortp_output_active)
shortp_start_output( );
spin_unlock_irqrestore(&shortp_out_lock, flags);
}
out:
*f_pos += written;
Доступ к круговому буферу контролирует семафор (shortp_out_sem); shortp_write получает этот семафор только перед вышеприведённым фрагментом кода. Удерживая семафора, она пытается передать данные в круговой буфер. Функция shortp_out_space возвращает размер доступного непрерывного пространства (так что нет необходимости беспокоиться о переполнении буфера); если этот размер равен 0, драйвер ждёт, пока не освободится некоторое пространство. Затем копирует в буфер столько данных, сколько может.
Как только появились данные для вывода, shortp_write должен гарантировать, что данные записываются в устройство. Фактическая запись выполняется функцией workqueue; shortp_write должен стартовать эту функцию, если она ещё не работает. После получения отдельной спин-блокировки, которая контролирует доступ к переменным, используемым на стороне потребителя выходного буфера (в том числе shortp_output_active), она вызывает в случае необходимости shortp_start_output. Тогда это всего лишь вопрос пометки, сколько данных было "записано" в буфер, и возвращения. Функция, которая начинает процесс вывода, выглядит следующим образом:
static void shortp_start_output(void)
{
if (shortp_output_active) /* Никогда не должно случиться */
return;
/* Установить на таймер 'пропущенное прерывание' */
shortp_output_active = 1;
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
/* И получить работу процесса. */
queue_work(shortp_workqueue, &shortp_work);
}
Реальность работы с аппаратурой такова, что вы можете иногда терять прерывания от устройства. Когда это случается, вы действительно не хотите, чтобы ваш драйвер остановился навсегда до перезагрузки системы; это недружественный способ поведения. Намного лучше понять, что прерывание было пропущено, подхватить куски и продолжить работу. С этой целью shortprint устанавливает таймер ядра, когда выводит данные на устройство. Если таймер истекает, мы, возможно, пропустили прерывание. Мы вскоре рассмотрим таймерную функцию, но в данный момент давайте разберёмся с функциональностью основного вывода. Это реализовано в нашей функции workqueue, которая, как вы можете видеть выше, планируется здесь. Суть этой функции выглядит следующим образом:
spin_lock_irqsave(&shortp_out_lock, flags);
/* Имеется ли что-то для записи? */
if (shortp_out_head == shortp_out_tail) { /* пусто */
shortp_output_active = 0;
wake_up_interruptible(&shortp_empty_queue);
del_timer(&shortp_timer);
}
/* Нет, пишем другой байт */
else
shortp_do_write( );
/* Если кто-то ждёт, можно разбудить их. */
if (((PAGE_SIZE + shortp_out_tail -shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE)
{
wake_up_interruptible(&shortp_out_queue);
}
spin_unlock_irqrestore(&shortp_out_lock, flags);
Поскольку мы имеем дело с общими переменными на стороне вывода, мы должны получить спин-блокировку. Затем посмотрим, есть ли дополнительные данные для отправки; если нет, мы обращаем внимание, что вывод больше не активен, удаляем таймер и пробуждаем любого, кто мог бы ожидать полной очистки очереди (такой вид ожидания завершается, когда устройство закрывается). Если, вместо этого, есть оставшиеся для записи данные, мы вызываем shortp_do_write для фактической отправки байта в аппаратуру.
Затем, после того, как мы смогли освободить место в выходном буфере, мы учитываем пробуждение любых процессов, ожидающих, чтобы добавить данные в этот буфер. Однако, мы не выполняем безоговорочное пробуждение; вместо этого, мы ждём минимального количества доступного места. Нет никакого смысла в пробуждении записи каждый раз, когда мы забираем один байт из буфера; стоимость пробуждения процесса, планирования его для запуска и помещения его обратно в сон слишком высока для этого. Наоборот, мы должны подождать, пока этот процесс сможет переместить значительный объём данных в буфер за раз. Эта техника обычна в буферированных, управляемых прерываниями драйверах.
Для полноты, вот код, который записывает данные в порт:
static void shortp_do_write(void)
{
unsigned char cr = inb(shortp_base + SP_CONTROL);
/* Что-от произошло; сбросить таймер */
mod_timer(&shortp_timer, jiffies + TIMEOUT);
/* Стробировать вывод байта в устройство */
outb_p(*shortp_out_tail, shortp_base+SP_DATA);
shortp_incr_out_bp(&shortp_out_tail, 1);
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr | SP_CR_STROBE, shortp_base+SP_CONTROL);
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr & ~SP_CR_STROBE, shortp_base+SP_CONTROL);
}
Здесь мы сбрасываем таймер, чтобы отразить тот факт, что мы добились некоторого прогресса, стробируем вывод байта в устройство и обновляем указатель кругового буфера.
Функция workqueue не повторяет сама себя непосредственно, поэтому в устройство будет записан только один байт. В определённый момент принтер, его медленным способом, будет потреблять байт и станет готовым к следующему; затем он прервёт процессор. Обработчик прерывания, используемых в shortprint, короткий и простой:
static irqreturn_t shortp_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
if (! shortp_output_active)
return IRQ_NONE;
/* Запомнить время и запланировать паузу для функции workqueue */
do_gettimeofday(&shortp_tv);
queue_work(shortp_workqueue, &shortp_work);
return IRQ_HANDLED;
}
Поскольку параллельный порт не требует явного подтверждения прерывания, всё, что обработчику прерывания действительно необходимо сделать, это сказать ядру снова запустить функцию workqueue.
Что делать, если прерывание никогда не приходит? Код драйвера, который мы видели до сих пор, просто бы остановился. Чтобы этого не произошло, мы устанавливаем обратный таймер несколько страниц назад. Функция, которая выполняется, когда это время закончится:
static void shortp_timeout(unsigned long unused)
{
unsigned long flags;
unsigned char status;
if (! shortp_output_active)
return;
spin_lock_irqsave(&shortp_out_lock, flags);
status = inb(shortp_base + SP_STATUS);
/* Если принтер всё ещё занят, мы просто сбрасываем таймер */
if ((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) {
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
spin_unlock_irqrestore(&shortp_out_lock, flags);
return;
}
/* Иначе мы, видимо, пропустили прерывание. */
spin_unlock_irqrestore(&shortp_out_lock, flags);
shortp_interrupt(shortp_irq, NULL, NULL);
}
Если не предполагается активного вывода, функция таймера просто возвращается; это предохраняет таймер от повторного самостоятельного старта при выключении. Затем после получения блокировки мы запрашиваем состояние порта; если он утверждает, что занят, он просто ещё не удосужился нас прервать, так что мы сбрасываем таймер и возвращаемся. Принтеры могут иногда занять очень много времени, чтобы сделать себя готовыми; примите во внимание принтер, который стоит без бумаги, пока кто-нибудь не пришёл после длинных выходных. В такой ситуации нечего делать, кроме как терпеливо ждать, пока что-то изменится.
Однако, если принтер утверждает, что готов, мы, видимо, пропустили своё прерывание. В этом случае мы просто вызываем наш обработчик прерывания вручную, чтобы процесс вывода снова получил движение.
Драйвер shortprint не поддерживает чтение из порта; вместо этого он ведёт себя как shortint и возвращает информацию о времени, когда происходили прерывания. Впрочем, реализация управляемого прерыванием метода read будет очень похожа на то, что мы видели. Данные из устройства читались бы в буфер драйвера; он бы копировал наружу в пользовательское пространство, только когда в буфере накапливался значительный объём данных, полностью удовлетворял запрос read или происходил бы какой-то вид превышения времени ожидания.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.