Иллюстрированный самоучитель по Computer Network The Hands
d51f8a0c

Архитектура драйвера


Архитектура драйвера

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

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

В примере 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.

    Пример 10.2. Простой драйвер контроллера гибкого диска

    /* Обработчики прерываний в зависимости от состояния */ void handle_spinup_interrupt(int irq, fdd_struct *fdd) {

    if (motor_speed_ok(fdd)) wake_up_interruptible((&fdd->fdd_wait_queue);

    void handle_seek_interrupt(int irq, fdd_struct *fdd) {

    if (verify_track(fdd)) wake_up_interruptible((&fdd->fdd_wait_queue);

    void handle_dma_interrupt(int irq, fdd_struct *fdd) {

    /* Увеличить fdd->bytes_xfered на количество фактически переданных символов */

    if (буфер полон/пуст) wake_up_interruptible(&fdd->fdd_wait_queue);

    /* Основная нить драйвера */

    static int fdd_write(struct inode * inode, struct file * file, char * buf, int count)

    10. Драйверы внешних устройств

    /* Получить идентификатор устройства: */ = MINOR ( inode->irdev) ;

    unsigned long ccpy_size;

    unsigned long total_bytes_written = 0;

    unsigned long bytes_written;

    int state;

    /* Найти блок переменных состояния устройства */ struct fdd_struct *fdd = &fdd_table [minor] ;

    do { copy_size = (count <= FDD__BUFFER_SIZE ?

    count : FDD_BUFFER_SIZE) ;

    /* Передать данные из пользовательского контекста */ memcpy_f rornfs (fdd->fdd_buf fer, buf, copy_size) ;

    while (copy_size) { if ( !motor_speed_ok (fdd) ) { fdd->handler = handle__spinup_interrupt; turn_motor_on (fdd) ;

    current->timeout = jiffies + FDD_INTERRUPT_TIMEOUT; interruptible_sleep_on (&fdd->fdd_wait_queue) ; if (current->signal & -current->blocked) { if (total_bytes_written)

    return total_bytes_written; else

    return -EINTR; /* Ничего не было записано, системный вызов был прерван, требуется повторная попытка */

    if (fdd->current_track != CALCULATE_TRACK(file)) { fdd->handler = handle_seek_interrupt; seek_head (fdd, CALCU1ATE__TRACK (f ile) ) ; current->timeout = jiffies + FDD_INTERRUPTjriMEOUT; interruptible_sleep_on(&fdd->fdd__wait_queue); if (current->signal & ~current->blocked) ( if (total bytes written)

    Введение в операционныесист^

    return total_bytes_written; else

    return -EINTR; /* Ничего не было записано, системный вызов был прерван, требуется повторная попытка */

    fdd->handler = handle_dma_interrupt;

    setup_fdd_dma(fdd->fdd_buffer+bytes_xfered, copy_size) issue_write_command(fdd) ;

    current->timeout = jiffies + FDD_INTEKRUPT_TIMEOUT; interruptible_sleep_on (Sfdd->fdd_wait_queue) ;

    bytes_written = fdd->bytes_xfered; fdd->bytes_written = 0;

    if (current->signal & ~current->blocked) { if (total_bytes_written + bytes_written)

    *

    return total_bytes_written + bytes_written; else

    return -EINTR; /* Ничего не было записано, системный вызов был прерван, требуется повторная попытка */

    total_bytes_written += bytes_written; buf += bytes__written; count -= bytes_written;

    } while (count > 0) ; return total bytes written;

    /* Обработчик прерывания */ static void fdd_interrupt(int irq) { struct fdd_struct *fdd = &fdd_table[fdd_irq[irq]];

    f (fdd->ha:idier != NULL) { fdd->handier(irq, fdd); fdd->handIer=MULL;

    } else

    {

    /* He наше прерывание? */

    }

    }

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

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

    Обработчик, таким образом, должен выполнять лишь те операции, которые требуется выполнить немедленно. В частности, многим устройствам требуется так или иначе объяснить, что прерывание обработано, чтобы они сняли сигнал запроса прерывания. Если этого не сделать, после возврата из обработчика и обусловленного этим снижения приоритета ЦПУ, обработчик будет вызван опять.

    Впрочем, нередко предлагается путь к обходу и этого ограничения: обработчикам прерываний разрешено создавать высокоприоритетные нити, которые начнут исполняться сразу же после того, как будут обслужены все прерывания. В дальнейшем мы будем называть эти высокоприоритетные нити fork-процессами (этот термин используется в VMS. Другие ОС, хотя и используют аналогичные понятия, часто не имеют внятной терминологии для их описания).

    Fork-процессы в VMS

    С точки зрения планировщика VMS, fork-процесс представляет собой нить с укороченным контекстом. Вместо обычного дескриптора процесса (РСВ — Process Control Block) используется UCB — Unit Control Block, блок управления устройством. Укорочение заключается в том, что эта нить может работать только с одним банком виртуальной памяти из трех, имеющихся у процессора VAX, а именно с системным (полный список банков памяти VAX приведен в главе 5); таким образом, при переключении контекста задействуется меньше регистров диспетчера памяти. Fork-процесс имеет более высокий приоритет, чем пользовательские процессы, и может быть вытеснен только более приорцтСб ным fork-процессом и обработчиком прерывания.

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

    Пример 10.3. Более сложный драйвер контроллера гибкого диска

    /* Обработчики прерываний в зависимости от состояния */ void schedule_seek (fdd__struct *fdd)

    if ( !motor_speed_pk (fdd) ) {

    fdd->handler = schedule_seek;

    retry_spinup ( ) ; }

    if (fdd->current_track != CALCULATEJTRACK (fdd->f ile) ) fdd->handler = schedule_command; seek_head(fdd, CALCULATE_TRACK (f ile) } ; } else

    /* Мы уже на нужной дорожке */ schedule operation (fdd) ;

    void schedule_operation(fdd_struct *fdd) {

    if (fdd->current_track != CALCULATEJTRACK(fdd->file)) { fdd->handler = schedule_operation; retry_seek(fdd); return; }

    switch(fdd->operation) ( case FDD_WRITE:

    fdd->handler = handle_dma_write_interrupt; setup_fdd_dma(fdd->fdd_buffer+fdd->bytes__xfered, fdd->copy_size)

    I issue_write_coromand (fdd) ; break; case FDD_READ:

    fdd->handler = handle_dma_read_interrupt;

    setup_fdd_dma (fdd->fdd_buf fer-t-fdd->bytes_xfered, fdd->copy_size)

    issue_read_command (fdd) ;

    break; /* Здесь же мы должны обрабатывать другие команды,

    требующие предварительного SEEK */

    void handle_dma_write_interrupt (fdd_struct *fdd)

    ( /* Увеличить fdd->bytes_xfered на количество фактически

    переданных символов * /

    if (буфер полон/пуст)

    /* Здесь мы не можем передавать данные из пользовательского

    адресного пространства . Надо будить основную нить * /

    wake_up_interruptible (&fdd->fdd_wait_queue) ; else {

    fdd->handler = handle__dma__write_interrupt;

    setup_fdd__dma (fdd->fdd_buf fer+fdd->bytes_xfered, fdd->copy_size)

    issue_write_corranand(fdd) ;

    /* Основная нить драйвера */

    static int fdd_write (struct inode * inode, struct file * file,

    char * buf, int count) (

    /* Получить идентификатор устройства: */ unsigned int minor = MINOR ( inode->i_rdev) ; /* Обратите внимание, что почти все переменные основной нити

    "переехали" в описатель состояния устройства */ /* Найти блок переменных состояния устройства */ struct fdd struct *fdd = &fdd table [minor] ;

    fdd->total_bytes_written = 0; fdd->operation = FDD_WRITE;

    do { fdd->copy_size = (count <= FDD_BUFFER_SIZE ?

    count : FDD_BOFFER_SIZE);

    /* Передать данные из пользовательского контекста */ memcpy_fromfs(fdd->fdd_buffer, buf, copy_size);

    if (!motor_5peed_ok()) (

    fdd->handler = schedule_seek;

    turn_motor_on(fdd); } else

    schedule_seek(fdd) ;

    current->timeout = jiffies + FDD_INTERRUPT__TIMEOUT; inte.rruptible_sleep_on(&fdd->fdd_wait_queue); if (current->signal & ~current->blocked) { if (fdd->total_bytes_written+fdd->bytes__written)'

    return fdd->total_bytes_written+fdd->bytes_written; else

    return -EINTR; /* Ничего не было записано,

    системный вызов был прерван, требуется повторная попытка */

    fdd->total_bytes_written += fdd->bytes_written; fdd~>buf += fdd->bytes_written; count -= fdd->bytes_written;

    } while (count > 0) ; return total bytes written;

    static struct tq_struct floppy_tq;

    /* Обработчик прерывания */ static void fdd interrupt(int irq)

    truct fdcl struct *fdd = &fdd_table [fdd_irq [irq] ] ;

    Af (fdd->ha!,;;ier != NULL) {

    void (Chandler)(int irq, fdd_struct * fdd) ;

    f]_0ppy_tq. routine = (void *)(void *) fdd->handler;

    floppy tq.parameter = (void *)fdd;

    fdd->handler=NULL;

    queue_task(sfloppy_tq, &tq_immediate); } else

    { /* He наше прерывание? */

    }

    }

    Видно, что теперь наш драйвер представляет собой последовательность функций, вызываемых обработчиком прерываний. Обратите внимание, что если мы торопимся, очередную функцию можно вызывать и непосредственно в обработчике, а не создавать для нее fork-процесс посредством queue_task. Но самое главное, на что нам следует обратить внимание — последовательность этих функций не задана жестко: каждая из функций сама определяет, какую операцию вызывать следующей. В том числе, она может решить, что следующая операция может состоять в вызове той же самой функции. В примере 10.3 мы используем эту возможность для простой обработки ошибок: повтора операции, которая не получилась.

    Для того чтобы понять, что же у нас получилось, какие возможности нам открывает такая архитектура и как ими пользоваться, нам следует сделать экскурс в одну из важных областей теории программирования.


    Архитектура драйвера

    Драйвер, таким образом, состоит из основной нити, обработчика прерывания, и, возможно, одной или нескольких высокоприоритетных нитей, создаваемых обработчиком. Все эти нити совместно (и, как правило, гарантируя взаимоисключение) исполняют более или менее сложный конечный автомат, состояния которого соответствуют этапам выполнения очередного запроса к устройству.

    Как правило, первое состояние автомата обрабатывается основной нитью драйвера, а последующие — обработчиком прерываний. В финальном состоянии автомата мы сообщаем процессу, породившему запрос, что запрос отработан. В зависимости от протокола взаимодействия этого процесса основной нитью драйвера, такое сообщение может осуществляться как fork-процессом, так и пробуждением основной нити.

    Драйвер IDE/ATA для Linux

    В примере 10.5 приведена основная функция обработки запроса и функция об работки прерывания, используемая при записи нескольких секторов. Обе эти функции вызываются драйвером контроллера IDE/ATA, который представляет собой диспетчер запросов к подключенным к контроллеру устройствам.

    Структура *hwgroup представляет собой блок переменных состояний контроллера устройства. Эта структура содержит также указатель на текущий запрос к устройству. Информации, содержащейся в этих структурах, достаточно, чтобы очередная функция конечного автомата драйвера узнала все, необходимое ей для выполнения очередного этапа запроса. В данном случае конечный автомат весьма прост и состоит из многократного вызова функции ide_multiwrite, копирующей в контроллер очередной блок данных. Условием завершения автомата служат ошибка контроллера либо завершение запроса. Функции ide__dma_read, ide_dma_write, ide_read и ide_write, исполняемые машиной состояний при обработке других запросов не приводятся.

    Пример 10.5. Фрагменты драйвера диска IDE/ATA ОС Linux 2.2, перевод комментариев автора

    /*

    * ide_multwrite() передает приводу блок из не более, чем mcount

    * секторов как часть многосекторной операции записи. *

    * Возвращает 0 при успехе. *

    * Обратите внимание, что мы можем быть вызваны из двух контекстов -

    * контекста do_rw и контекста IRQ. IRQ (Interrupt Request,

    * запрос прерывания)может произойти в любой

    * момент после того, как мы выведем полное количество секторов,

    * поэтому мы должны обновлять состояние _до_ того, как мы выведем

    * последнюю часть данных! */

    int ide_multwrite (ide_drive__t *drive, unsigned int mcount) {

    ide_hwgroup_t *hwgroup= HWGROUP(drive);

    'struct request *rq = &hwgroup->wrq;

    do {

    char *buffer;

    int nsect = rq->current_nr_sectors;

    if (nsect > mcount)

    nsect = mcount; mcount -= nsect; buffer = rq->buffer;

    rq->sector += nsect; rq->buffer += nsect « 9; rq->nr_sectors -= nsect; rq->current nr sectors -= nsect;

    /* Переходим ли мы к следующему bh после этого? */ if (!rq->current_nr_sectors) {

    struct buffer_head *bh = rq->bh->b_reqnext;

    /* Завершиться, если у нас кончились запросы V if (!bh) {

    mcount = 0; } else (

    rq->bh = bh;

    rq->current_nr_sectors = bh->b_size » 9;

    rq->buffer = bh->b_data;

    /*

    * Теперь мы все настроили, чтобы прерывание

    * снова вызвало нас после последней передачи. */

    idedisk_output_data(drive, buffer, nsect«7); } while (mcount);

    return 0;

    /*

    * multwrite_intr() — обработчик прерывания многосекторной записи */

    static ide_startstop_t multwrite_intr (ide_drive_t *drive) {

    byte stat;

    ir.t i;

    ide_hwgroup_t *hwgroup = HWGROUP(drive);

    struct request *rq = &hwgroup->wrq;

    if (OK_STAT(stat=GET_STAT(),DRIVE_READY,drive->bad_wstat)) { if (stat & DRQ_STAT) { /*

    * Привод требует данных. Помним что rq -

    * копия запроса. */

    if (rq->nr_sectors) {

    if (ide_multwrite(drive, drive->mult_count))

    return ide_stopped; «

    ide_set__handler (drive, &multwrite_intr, WAIT_CMD, NULL); return ide_started; }

    } else { /*

    * Если копирование всех блоков завершилось,

    * мы можем завершить исходный запрос. */

    if ( ! rq->nr__sectors) { /* all done? */ rq = hwgroup->rq; for (i = rq->nr_sectors; i > 0;){ i -= rq->current_nr_sectors; ide_end_request(1, hwgroup); } return ide stopped;

    return ide_stopped; /* Оригинальный код делал это здесь (?) */

    ! ьнешних

    [return ide_errcr(drive, "multwrite_intr", stat);

    /*

    i do rw disk() передает команды READ и WRITE приводу,

    * используя LBA если поддерживается, или CHS если нет, для адресации

    * секторов. Функция do_rw_disk также передает специальные запросы.

    */

    static ide_startstop__t do_rw_disk (ide_drive_t *drive, struct request *rq, unsigned long block)

    { if (IDE_CONTROL_REG)

    OUT_BYTE (drive->ctl, IDE_CONTROL_REG) ; OUT_BYTE (rq->nr_sectors, IDE_NSECTOR_REG) ; if (drive->select.b.lba) (

    OUT_BYTE (block, IDE_SECTOR_REG) ;

    OUT_BYTE (block»=8, IDE_LCYL_REG) ;

    OUT_BYTE (block»=8, I DE_HC YL_REG ) ;

    OUT_BYTE( ( (block»8) &0x0f) I drive->select . all, IDE_SELECT_REG) ; } else f

    unsigned int sect, head, cyl, track;

    track = block / drive->sect;

    sect = block % drive->sect + 1;

    ODT^BYTE (sect, IDE__SECTOR_REG) ;

    head = track % drive->head;

    cyl = track / drive->head;

    OUT__BYTE (cyl, IDE_LCYL_REG) ;

    OUT_BYTE (cyl»8, IDE_HCYL_REG) ;

    OUT_BYTE (head I drive->select .all, IDE_SELECT_REG) ;

    if (rq->cmd == READ) { ^#ifdef CONFIG_BLK_DEV_IDEDMA

    if (drive- >using_dma && ! (HWIF (drive) ->dmaproc (ide_dma_read, drive))

    return ide_started; #endif /* CONFIG_BLK_DEV_IDEDMA */

    ide_set_handler (drive, iread_intr, WAIT_CMD, NULL) ; OUT_BYTE(drive->mult_count ? WIN_MULTREAD : WIN_READ, IDE COMMAND REG) ;

    ''—-^

    return ide started;

    if (rq->cmd == WRITE) (

    ide_startstop_t startstop; lifdef CONFIG_BLK_DEV_IDEDMA

    if (drive->using_drna && !(HWIF(drive)->dmaproc(ide dma^write,

    drive)))

    return ide_started; lendif /* CONFIG_BLK_DEV_IDEDMA */

    OUT_BYTE(drive->mult_COUnt ? WIN_MULTWRITE : WIN_WRITE,

    IDE_COMMAND_REG); if (ide_wait_stat(Sstartstop, drive, DATA_READY, drive->bad_wstat,

    WAIT^DRQ)) ( printk(KERN_ERR "%s: no DRQ after issuing %s\n", drive->na:r.e,

    drive->mult_count ? "MULTWRITE" : "WRITE"); return startstop;

    if (!drive->unmask)

    __cli(); /* только локальное ЦПУ */

    if (drive->mult_count) (

    ide_hwgroup_t *hwgroup = HWGROUP(drive);

    /*

    * Эта часть выглядит некрасиво, потому что мы ДОЛЖНЫ установить

    * обработчик перёд выводом первого блока данных.

    * Если мы обнаруживаем ошибку (испорченный список буферов)

    * в ide_multiwrite(),

    * нам необходимо удалить обработчик и таймер перед возвратом.

    * К счастью, это НИКОГДА не происходит (правильно?).

    * Кажется, кроме случаев, когда мы получаем ошибку... */

    hwgroup->wrq = *rq; /* scratchpad */

    ide_set_handler (drive, &multwrite_intr, WAIT__CMD, NULL);

    if (ide_multwrite(drive, drive->mult_count)) {

    unsigned long flags;

    spin_lock_irqsave (&io__request_lock, flags) ;

    hwgroup->handler = NULL;

    del_timer(&hwgroup->timer);

    spin unlock_irqrestore(&io_request_lock, flags);

    return ide_stopped;

    Глава 10. Драйверы внешних

    } else {

    ide_set_handler (drive, &write_intr, WAIT_CMD, NULL); idedisk_output_data(drive, rq->buffer, SECTOR_WORDS);

    }

    i return ide_started;

    )

    i'-printk (KERN_ERR "%s: bad command: %d\n", drive->name, rq->cmd)

    ide_end_request(0, HWGROUP(drive)); return ide_stopped;



    Содержание раздела