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

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

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

Пример 10.1 представляет собой упрощённый скелет функции write() драйвера последовательного устройства для системы Linux. В частности, в нём не решается проблема реентерабельности функции foo_write. Используемый механизм синхронизации с обработчиком прерывания также оставляет желать лучшего. Тем не менее, пример имеет ту же архитектуру, что была описана ранее. Текст цитируется из документа [HOWTO khg], с переводом комментариев и дополнительными замечаниями автора.

Пример 10.1. Скелет драйвера последовательного устройства для ОС Linux

/* Основная нить драйвера */
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 : FOO_BUFFER_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(&foo->foo_wait_queue);

                if (some_error_has_occured) {
                    /* Здесь мы должны обработать ошибку */
                    bytes_written = foo->bytes_xfered;
                    foo->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 foo_interrupt(int irq) {
    struct foo_struct *foo = &foo_table[foo_irq[irq]];

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

    /* Увеличить foo->bytes_xfered на количество фактически переданных символов */
    if (buffer_is_full_or_empty)
        wake_up_interruptible(&foo->foo_wait_queue);
}

Примечание: Обратите внимание, что кроме инициализации устройства драйвер перед засыпанием еще устанавливает "будильник" — таймер, который должен разбудить процесс через заданный интервал времени. Это необходимо на случай, если произойдет аппаратная ошибка и устройство не сгенерирует прерывания. Если бы такой будильник не устанавливался, драйвер в случае ошибки мог бы заснуть навсегда, заблокировав при этом пользовательский процесс. В нашем случае таймер также используется, чтобы разбудить процесс, если прерывание произойдет до вызова interruptible_sleep_on основной нитью.

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

Лишь после этого можно будет передать данные программе. Наивная реализация таких многошаговых операций могла бы выглядеть так (за основу по-прежнему взят код из [HOWTO khg], обработка ошибок опущена), как показано в примере 10.2.

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

#include <linux/module.h> /* Для всех модулей */
#include <linux/kernel.h> /* Для KERN_INFO */
#include <linux/init.h> /* Для macro-определений */
#include <linux/interrupt.h> /* Для обработчиков прерываний */
#include <linux/fs.h> /* Для работы с файловыми системами */

/* Обработчики прерываний в зависимости от состояния */
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) {
    /* Получить идентификатор устройства: */
    int minor = MINOR(inode->irdev);

    unsigned long copy_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_fromfs(fdd->fdd_buffer, 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, CALCULATE_TRACK(file));
                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; /* Ничего не было записано, системный вызов был прерван, требуется повторная попытка */
                }
            }

            fdd->handler = handle_dma_interrupt;
            setup_fdd_dma(fdd->fdd_buffer + bytes_written, copy_size);
            issue_write_command(fdd);
            current->timeout = jiffies + FDD_INTERRUPT_TIMEOUT;
            interruptible_sleep_on(&fdd->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]];

    if (fdd->handler != NULL) {
        fdd->handler(irq, fdd);
        fdd->handler = NULL;
    } else {
        /* Не наше прерывание? */
    }
}

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

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

Впрочем, нередко предлагается путь к обходу и этого ограничения: обработчикам прерываний разрешено создавать высокоприоритетные нити, которые начнут исполняться сразу же после того, как будут обслужены все прерывания. В дальнейшем мы будем называть эти высокоприоритетные нити 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_ok(fdd)) {
        fdd->handler = schedule_seek;
        retry_spinup();
    }
    if (fdd->current_track != CALCULATE_TRACK(fdd->file)) {
        fdd->handler = schedule_command;
        seek_head(fdd, CALCULATE_TRACK(file));
    } else {
        /* Мы уже на нужной дорожке */
        schedule_operation(fdd);
    }
}

void schedule_operation(fdd_struct *fdd) {
    if (fdd->current_track != CALCULATE_TRACK(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);
        issue_write_command(fdd);
        break;
    case FDD_READ:
        fdd->handler = handle_dma_read_interrupt;
        setup_fdd_dma(fdd->fdd_buffer+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_buffer+fdd->bytes_xfered, fdd->copy_size);
        issue_write_command(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_BUFFER_SIZE);
        /* Передать данные из пользовательского контекста */
        memcpy_fromfs(fdd->fdd_buffer, buf, fdd->copy_size);

        if (!motor_speed_ok(fdd)) {
            fdd->handler = schedule_seek;
            turn_motor_on(fdd);
        } else {
            schedule_seek(fdd);
        }

        current->timeout = jiffies + FDD_INTERRUPT_TIMEOUT;
        interruptible_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 fdd->total_bytes_written;
}

static struct tq_struct floppy_tq;

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

    if (fdd->handler != NULL) {
        void (*handler)(int irq, fdd_struct * fdd) = fdd->handler;

        floppy_tq.routine = (void *)(void *) handler;
        floppy_tq.parameter = (void *) fdd;
        fdd->handler = NULL;

        queue_task(&floppy_tq, &tq_immediate);
    } else {
        /* Не наше прерывание? */
    }
}

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

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