Реализация обработчика

Предыдущая  Содержание  Следующая V*D*V

До сих пор мы изучали регистрацию обработчика прерывания, но ничего не писали. Вообще-то, в обработчике нет ничего необычного -это обычный код Си.

 

Единственной особенностью является то, что обработчик работает во время прерывания и, следовательно, испытывает некоторые ограничения в том, что может делать. Эти ограничения являются такими же, какие мы видели для таймеров ядра. Обработчик не может передавать данные в или из пространства пользователя, так как не выполняется в контексте процесса. Обработчики также не могут делать ничего, что могло бы заснуть, например, вызвать wait_event, выделять память ни с чем другим, кроме GFP_ATOMIC, или блокировать семафор. Наконец, обработчик не может вызвать schedule.

 

Ролью обработчика прерываний является предоставление обратной связи устройству о приёме прерывания и прочитать или записать данные в зависимости от смысла обслуживаемого прерывания. Первый шаг обычно состоит в очистке бита на интерфейсной плате; большинство аппаратных устройств не будет генерировать другие прерывания, пока их бит "ожидание прерывания" не очищен. В зависимости от того, как работает ваше оборудование, этот шаг может выполняться последним, а не первым, здесь нет всеобъемлющего правила. Некоторые устройства не требуют этого шага, так как они не имеют бита "ожидание прерывания"; такие устройства составляют меньшинство, хотя параллельный порт является одним из них. По этой причине short ничего не делает для очистки такого бита.

 

Типичной задачей для обработчика прерывания является пробуждение спящих процессов устройства, если сигналы прерывания являются событиями, которые они ждут, такими, как появление новых данных.

 

В случае примера захвата кадров, процесс мог бы получать последовательность изображений постоянно читая устройство; перед чтением каждого фрейма вызов read блокируется, а обработчик прерывания пробуждает процесс при поступлении каждого нового кадра. Это предполагает, что захват прерывает процессор для сигнализации об успешном появлении каждого нового кадра.

 

Программист должен быть внимательным, чтобы написать процедуру, которая выполняется в минимальное количество времени, независимо от того, быстрый это или медленный обработчик. Если должно быть выполнено долгое вычисление, лучшим подходом является использование микрозадачи (tasklet) или очереди задач (workqueue) для выполнения вычислений в безопасное время (в разделе "Верхние и нижние половины" мы увидим, как работа может быть отложена таким образом).

 

Наш код примера в short отвечает на прерывание вызывая do_gettimeofday и печатая текущее время в круговой буфер размером со страницу. Затем он пробуждает любой читающий процесс, поскольку теперь есть доступные для чтения данные.

 

irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    struct timeval tv;

    int written;

 

    do_gettimeofday(&tv);

 

    /* Пишем 16 байтовый отчёт. Подразумеваем, что PAGE_SIZE кратна 16 */

    written = sprintf((char *)short_head,"%08u.%06u\n",

                      (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));

    BUG_ON(written != 16);

    short_incr_bp(&short_head, written);

    wake_up_interruptible(&short_queue); /* пробудить любой процесс чтения */

    return IRQ_HANDLED;

}

 

Этот код, хотя и прост, показывает типичную работу обработчика прерываний. Он, в свою очередь, вызывает short_incr_bp, которая определена так:

 

static inline void short_incr_bp(volatile unsigned long *index, int delta)

{

    unsigned long new = *index + delta;

    barrier( ); /* Не оптимизировать эти две строки вместе */

    *index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;

}

 

Эта функция была написана тщательно, чтобы обернуть указатель в круговом буфере никогда не получая неправильные значения. Чтобы заблокировать оптимизацию компилятором двух разных строчек функции, здесь используется вызов barrier. Без барьера компилятор мог бы принять решение оптимизировать, удалив переменную new, и выполнить присвоение непосредственно *index. Такая оптимизация может привести к неправильному значение индекса в течение короткого периода, когда он оборачивается. Приняв меры для предотвращения получения некорректного значения другими потоками, мы можем безопасно, без блокирования, манипулировать указателем кругового буфера.

 

Файлом устройства, используемого для чтения заполненного во время прерывания буфера, является /dev/shortint. Это специальный файл устройства не был рассмотрен в Главе 9 вместе с /dev/shortprint, поскольку его использование является специфичным для обработки прерываний. Внутренняя организация /dev/shortint специально предназначена для генерации прерывания и отчётности. Запись в  устройство генерирует одно прерывание для каждого байта; чтение устройства даёт время, когда было сообщено о каждом прерывании.

 

Если соединить вместе контакты 9 и 10 разъёма параллельного порта, вы сможете генерировать прерывания, устанавливая самый старший бит байта данных параллельного порта. Это может быть достигнуто записью бинарных данных в /dev/short0 или записью чего-нибудь в /dev/shortint. (* Устройство shortint выполняет свою задачу поочередно записывая в параллельный порт 0x00 и 0xFF.)

 

Следующий код реализует read и write для /dev/shortint:

 

ssize_t short_i_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)

{

    int count0;

    DEFINE_WAIT(wait);

 

    while (short_head == short_tail) {

        prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE);

        if (short_head == short_tail)

            schedule( );

        finish_wait(&short_queue, &wait);

        if (signal_pending (current)) /* сигнал поступил */

            return -ERESTARTSYS; /* предложить уровню файловой системы обработать его */

    }

    /* count0 - число читаемых байтов данных */

    count0 = short_head - short_tail;

    if (count0 < 0) /* wrapped */

        count0 = short_head + PAGE_SIZE - short_tail;

    if (count0 < count) count = count0;

 

    if (copy_to_user(buf, (char *)short_tail, count))

        return -EFAULT;

    short_incr_bp (&short_tail, count);

    return count;

}

 

ssize_t short_i_write (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)

{

    int written = 0, odd = *f_pos & 1;

    unsigned long port = short_base; /* вывод в регистр-защёлку параллельных данных */

    void *address = (void *) short_base;

 

    if (use_mem) {

        while (written < count)

            iowrite8(0xff * ((++written + odd) & 1), address);

    } else {

        while (written < count)

            outb(0xff * ((++written + odd) & 1), port);

    }

 

    *f_pos += count;

    return written;

}

 

Другой специальный файл устройства, /dev/shortprint, использует параллельный порт для управления принтером; вы можете использовать его, если хотите избежать соединения контактов 9 и 10 разъёма D-25. Реализация write в shortprint использует круговой буфер для хранения данных, которые будут печататься, а реализация read аналогична только что показанной (так что вы можете читать время, которое забирает ваш принтер, чтоб съесть каждый символ).

 

В целях поддержки принтерных операций, обработчик прерывания был незначительно изменён относительно показанного добавлением возможности отправить следующий байт данных в принтер, если для передачи имеется больше данных.

Аргументы обработчика и возвращаемое значение

Обработчику прерывания передаются три аргумента: irq, dev_id и regs, хотя short и игнорирует их. Давайте посмотрим на роль каждого из них.

 

Номер прерывания (int irq) полезен как информация, которую вы можете напечатать в журнале сообщений, если таковой имеется. Второй аргумент, void *dev_id, является видом данных клиента; аргумент void * передаётся в request_irq и этот же указатель затем передаётся обратно как аргумент для обработчика, когда происходит прерывание. Обычно передаётся указатель на вашу структуру данных устройства в dev_id, так что драйверу, который управляет несколькими экземплярами одинаковых устройств, не требуется какого-либо дополнительного кода в обработчике прерывания, чтобы выяснить, какое устройство отвечает за текущее событие прерывания.

 

Типичное использование аргумента в обработчике прерывания выглядит следующим образом:

 

static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    struct sample_dev *dev = dev_id;

    /* теперь `dev' указывает на правильный объект оборудования */

    /* .... */

}

 

Типичный код open, связанный с этим обработчиком, выглядит следующим образом:

 

static void sample_open(struct inode *inode, struct file *filp)

{

    struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev);

    request_irq(dev->irq, sample_interrupt,

                0 /* flags */, "sample", dev /* dev_id */);

    /*....*/

    return 0;

}

 

Последний аргумент, struct pt_regs *regs, используется редко. Он содержит снимок контекста процессора перед тем, как процессор вошёл в код прерывания. Регистры могут быть использованы для мониторинга и отладки; для обычных задач драйвера устройства они обычно не требуются.

 

Обработчики прерываний должны вернуть значение указывающее, было ли прерывание обработано на самом деле. Если обработчик обнаружил, что его устройство действительно требует внимания, он должен вернуть IRQ_HANDLED; в противном случае возвращаемое значение должно быть IRQ_NONE. Вы можете также сгенерировать возвращаемое значение этим макросом:

 

IRQ_RETVAL(handled)

 

где handled отличен от нуля, если вы были в состоянии обработать прерывание. Возвращаемое значение используется ядром для выявления и пресечения ложных прерываний. Если ваше устройство не даёт вам способа узнать, действительно ли прервало оно, вы должны вернуть IRQ_HANDLED.

Разрешение и запрет прерываний

Есть моменты, когда драйвер устройства должен блокировать генерацию прерываний на какой-то (в надежде на короткий) период времени (мы видели одну такую ситуацию в разделе "Спин-блокировки" в Главе 5). Зачастую прерывания должны быть заблокированы при удержании спин-блокировки во избежании взаимоблокировки системы. Есть способы запрета прерываний, которые не осложняют спин-блокировки. Но прежде чем обсудить их, отметим, что запрет прерываний должен быть сравнительно редкой деятельностью даже в драйверах устройств и эта техника никогда не должна использоваться в качестве механизма взаимного исключения внутри драйвера.

Запрет одного прерывания

Иногда (но редко!) драйверу необходимо отключить доставку прерывания для определённой линии прерывания. Ядро предлагает для этой цели три функции, все они объявлены в <asm/irq.h>. Эти функции являются частью API ядра, поэтому мы их опишем, но их использование не рекомендуется в большинстве драйверов. Среди прочего, вы не можете отключить разделяемые линии прерываний, а на современных системах разделяемые прерывания являются нормой. Как говорилось, вот они:

 

void disable_irq(int irq);

void disable_irq_nosync(int irq);

void enable_irq(int irq);

 

Вызов любой из этих функций может обновить маску указанного irq в программируемом контроллере прерываний (programmable interrupt controller, PIC), таким образом, запрещая или разрешая указанное прерывание на всех процессорах. Вызовы этих функций могут быть вложенными, если disable_irq вызвана два раза подряд, требуется дважды вызвать enable_irq для действительного разрешения прерывания заново. Можно вызывать эти функции из обработчика прерывания, но разрешение вашего собственного прерывания при его обработке обычно нехорошая практика.

 

disable_irq не только отключает данное прерывание, но также ждёт завершения выполняющегося в настоящее время обработчика прерывания, если таковой имеется. Помните, что если поток, вызывая disable_irq, удерживает какой-либо ресурс (такой, как спин-блокировки), который необходим обработчику прерывания, система может заблокироваться. disable_irq_nosync отличается от disable_irq тем, что она возвращается немедленно. Таким образом, использование disable_irq_nosync немного быстрее, но может оставить драйвер открытым для состояний гонок.

 

Но зачем же отключать прерывание? Придерживаясь параллельного порта, давайте посмотрим на сетевой интерфейс plip. Устройство plip использует простой параллельный порт для передачи данных. Так как из разъёма параллельного порта могут быть прочитаны только пять бит, они интерпретируются как четыре бита данных и тактовый/настроечный сигнал. Когда первые четыре бита пакета переданы инициатором (интерфейсом отправки пакета), для прерывания процессора в случае принимающего интерфейса поднимается тактовая линия. Затем для обработки вновь прибывших данных вызывается обработчик plip.

 

После приведения устройства в готовность осуществляется передача данных, используя линию настройки для тактирования новых данных для приёмного интерфейса (это может не быть лучшей реализацией, но это необходимо для совместимости с другими пакетными драйверами, использующими параллельный порт). Производительность была бы невыносимой, если бы принимающему интерфейсу пришлось обрабатывать два прерывания для каждого принимаемого байта. Таким образом, драйвер выключает прерывание во время приёма пакета; вместо этого для доставки данных используется цикл опроса и задержки.

 

Аналогичным образом, из-за того, что линия настройки от приёмника до передатчика используется для подтверждения приёма данных, передающий интерфейс запрещает свою линию прерывания во время передачи пакета.

Запрет всех прерываний

Что делать, если вам необходимо запретить все прерывания? В версии ядра 2.6 можно отключить всю обработку прерываний на текущем процессоре с одной из следующих двух функций (которые определены в <asm/system.h>):

 

void local_irq_save(unsigned long flags);

void local_irq_disable(void);

 

Вызов local_irq_save запрещает доставку прерывания на текущий процессор после сохранения текущего состояния прерывания в flags. Обратите внимание, что flags передаётся непосредственно, не по указателю; так происходит потому что local_irq_save на самом деле реализована как макрос, а не как функция. local_irq_disable выключает местную доставку прерывания без сохранения состояния; вы должны использовать эту версию только если вы знаете, что прерывания уже не были запрещены где-то ещё.

 

Включение прерываний снова осуществляется с помощью:

 

void local_irq_restore(unsigned long flags);

void local_irq_enable(void);

 

Первая версия восстанавливает то состояние, которое было сохранено во flags функцией local_irq_save, а local_irq_enable разрешает прерывания безоговорочно. В отличие от disable_irq, local_irq_disable не отслеживает множественные вызовы. Если в цепочке вызова запретить прерывания может потребоваться более чем одной функции, должна быть использована local_irq_save.

 

В ядре версии 2.6 не существует способа запретить все прерывания глобально во всей системе. Разработчики ядра решили, что цена отключения всех прерываний слишком высока и что в любом случае нет никакой необходимости для такой возможности. Если вы работаете со старым драйвером, который делает вызовы таких функций, как cli и sti, необходимо обновить его для использования правильного блокирования, прежде чем он будет работать под версией 2.6.

Предыдущая  Содержание  Следующая