Обработка запроса

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

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

 

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

Введение в метод request

Метод request блочного драйвера имеет следующий прототип:

 

void request(request_queue_t *queue);

 

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

 

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

 

dev->queue = blk_init_queue(sbull_request, &dev->lock);

 

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

 

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

 

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

Простой метод request

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

 

static void sbull_request(request_queue_t *q)

{

    struct request *req;

 

    while ((req = elv_next_request(q)) != NULL) {

        struct sbull_dev *dev = req->rq_disk->private_data;

        if (! blk_fs_request(req)) {

            printk (KERN_NOTICE "Skip non-fs request\n");

            end_request(req, 0);

            continue;

        }

        sbull_transfer(dev, req->sector, req->nr_sectors,

                    req->buffer, rq_data_dir(req));

        end_request(req, 1);

    }

}

 

Эта функция представляет структуру struct request. Мы будем детально рассматривать struct request позже; сейчас же достаточно сказать, что это она представляет для выполнения нами блочный запрос ввода/вывода.

 

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

 

Блочная очередь запросов может содержать запросы, которые на самом деле не перемещают блоки на или с диска. Такие запросы могут включать зависящие от производителя низкоуровневые диагностические операции или инструкции, касающиеся режимов специализированных устройств, таких как режим пакетной записи для записываемого носителя. Большинство блочных драйверов не знают, как обрабатывать такие запросы и просто их не выполняют; sbull работает таким же образом. Вызов blk_fs_request сообщает нам, видим ли мы запрос файловой системы, который перемещает блоки данных. Если запрос не является запросом файловой системы, мы передаём его в end_request:

 

void end_request(struct request *req, int succeeded);

 

Когда мы определяем, что запросы не файловой системы, мы передаём succeeded как 0, чтобы указать, что мы завершили запрос не успешно. В противном случае, для фактического перемещения данных мы вызываем sbull_transfer, используя набор полей, предусмотренных в структуре request:

 

sector_t sector;

Указатель начального сектора на нашем устройстве. Запомните, что этот номер сектора, как и все такие номера, передаваемые между ядром и драйвером, выражается в секторах по 512 байт. Если ваше оборудование использует другой размер сектора, необходимо sector соответственно отмасштабировать. Например, если оборудование использует секторы по 2048 байт, необходимо разделить номер начального сектора на четыре перед помещением его в запрос для оборудования.

 

unsigned long nr_sectors;

Количество (512 байтных) секторов, которые будут переданы.

 

char *buffer;

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

 

rq_data_dir(struct request *req);

Этот макрос извлекает из запроса направление передачи; нулевое возвращаемое значение означает чтение из устройства и ненулевое возвращаемое значение означает запись в устройство.

 

Используя эту информацию, драйвер sbull может осуществлять фактическую передачу данных простым вызовом memcpy - в конце концов, наши данные уже находятся в памяти. Функция, которая выполняет эту операцию копирования (sbull_transfer), также выполняет  масштабирование размеров секторов и гарантирует, что мы не пытаемся копировать за предел нашего виртуального устройства:

 

static void sbull_transfer(struct sbull_dev *dev, unsigned long sector,

        unsigned long nsect, char *buffer, int write)

{

    unsigned long offset = sector*KERNEL_SECTOR_SIZE;

    unsigned long nbytes = nsect*KERNEL_SECTOR_SIZE;

 

    if ((offset + nbytes) > dev->size) {

        printk (KERN_NOTICE "Beyond-end write (%ld %ld)\n", offset, nbytes);

        return;

    }

    if (write)

        memcpy(dev->data + offset, buffer, nbytes);

    else

        memcpy(buffer, dev->data + offset, nbytes);

}

 

С помощью этого кода sbull реализует завершённое, простое, находящееся в ОЗУ дисковое устройство. Это, однако, по нескольким причинам не реалистичный драйвер для многих типов устройств.

 

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

 

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

 

Однако, драйвер sbull получает всю эту работу и просто её игнорирует. За раз передаётся только один буфер, это означает, что  наибольшая одиночная передача почти никогда не превышает размера одной страницы. Блочный драйвер может работать гораздо лучше, по  сравнению с этим, но это требует более глубокого понимания структур request и структур bio, на которых построены запросы.

 

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

Очереди запросов

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

 

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

 

Очереди запросов также реализуют интерфейс подключения модулей, который позволяет использовать несколько планировщиков (scheduler) ввода/вывода (или транспортёров (elevator)). Работой планировщика ввода/вывода является предоставление запросов ввода/вывода вашему драйверу таким образом, чтобы максимизировать производительность. С этой целью большинство планировщиков ввода/вывода накапливают пакеты запросов, сортируют их в порядке увеличения (или уменьшения) значения блочного указателя, и предоставляют драйверу запросы в таком порядке. Головка диска, когда предоставляется упорядоченный список запросов, проходит свой путь от одного конца диска к другому, подобно полному транспортёру, движущемуся в одном направлении, пока все его "запросы" (люди, ожидающие, чтобы выйти) не выполнены. Ядро версии 2.6 имеет "планировщик со сроком завершения", который прикладывает усилия к тому, чтобы каждый запрос выполнялся в течение заданного максимального времени, и "упреждающий планировщик", который фактически ненадолго задерживает устройство после запроса на чтение в ожидании того, что будет получено почти сразу другое смежное чтение. На момент написания, планировщиком по умолчанию является упреждающей планировщик, который, кажется, даёт лучшую производительность интерактивной системе.

 

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

 

Очереди запросов имеют тип struct request_queue или request_queue_t. Этот тип и многие функции, которые с ним работают, определены в <linux/blkdev.h>. Если вы заинтересованы в реализации очередей запросов, вы можете найти большую часть кода в drivers/block/ll_rw_block.c и elevator.c.

Создание и удаление очереди

Как мы видели в нашем коде примера, очередь запросов является динамической структурой данных, которая должна быть создан подсистемой блочного ввода/вывода. Функцией для создания и инициализации очереди запросов является:

 

request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);

 

Аргументы, конечно, функция request для этой очереди и спин-блокировка, которая управляет доступом к этой очереди. Эта функция выделяет память (совсем немного памяти, на самом деле) и может из-за этого закончится неудачно; вам следует всегда проверять возвращаемое значение, прежде чем пытаться использовать эту очередь.

 

В рамках инициализации очереди запросов вы можете установить поле queuedata (которое является указателем void *) в любое значение, которое вам нравится. Это поле эквивалентно в очереди запросов полю private_data, которое мы уже видели в других структурах.

 

Чтобы вернуть очередь запросов системе (как правило, при выгрузке модуля), вызовите blk_cleanup_queue:

 

void blk_cleanup_queue(request_queue_t *);

 

После этого вызова ваш драйвер больше не увидит запросов от данной очереди и не должен не ссылаться на неё снова.

Функции для очереди

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

 

Функцией, которая возвращает процессу следующий запрос, является elv_next_request:

 

struct request *elv_next_request(request_queue_t *queue);

 

Мы уже видели эту функцию в простом примере sbull. Она возвращает процессу указатель на следующий запрос (определённый планировщиком ввода/вывода) или NULL, если больше не осталось запросов для обработки. elv_next_request оставляет запрос в очереди, но помечает его как активный; эта метка предотвращает попытку планировщика ввода/вывода объединить с ним другие запросы, когда вы начинаете его выполнять.

 

Чтобы действительно удалить запрос из очереди, используйте blkdev_dequeue_request:

 

void blkdev_dequeue_request(struct request *req);

 

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

 

Если вам необходимо по какой-то причине поместить убранный из очереди запрос обратно в очереди  вы можете вызвать:

 

void elv_requeue_request(request_queue_t *queue, struct request *req);

Функции управления очередью

Блочный уровень экспортирует набор функций, которые могут быть использованы драйвером для управления того, как работает очередь запросов. Эти функции включают в себя:

 

void blk_stop_queue(request_queue_t *queue);

void blk_start_queue(request_queue_t *queue);

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

 

void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);

Функция, которая сообщает ядру наивысший физический адрес, по которому устройство может выполнять DMA. Если приходит запрос, содержащий ссылку на памяти выше этого предела, для этой операции будет использоваться возвратный буфер; это, конечно, дорогой способ выполнения блочного ввода/вывода и его следует по возможности избегать. Вы можете предоставить в этом аргументе любые разумные физические адреса или использовать заранее определённые символы BLK_BOUNCE_HIGH (использовать возвратные буферы для страниц верхней памяти), BLK_BOUNCE_ISA (драйвер может выполнять DMA только в 16 Мб зоне ISA), или BLK_BOUNCE_ANY (драйвер может выполнять DMA по любому адресу). Значением по умолчанию является BLK_BOUNCE_HIGH.

 

void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);

void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max);

void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);

void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);

Функции, устанавливающие параметры, описывающие запросы, которые могут быть выполнены этим устройством. blk_queue_max_sectors может быть использована для установки максимального размера любого запроса в секторах (по 512 байт); по умолчанию 255. blk_queue_max_phys_segments и blk_queue_max_hw_segments управляют тем, как много физических сегментов (не смежных областей в системной памяти), могут содержаться в одном запросе. Используйте blk_queue_max_phys_segments, чтобы сообщить, с каким количеством сегментов ваш драйвер подготовлен справиться; это может быть, например, размер статически созданного списка разборки. blk_queue_max_hw_segments, напротив, является максимальным количеством сегментов, которые может обработать само устройство. Оба этих параметров по умолчанию равны 128. Наконец, blk_queue_max_segment_size сообщает ядру, насколько большим в байтах может быть любой отдельный сегмент запроса; по умолчанию 65.536 байт.

 

blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);

Некоторые устройства не могут обрабатывать запросы, которые пересекают границы определённого размера памяти; если ваше устройство является одним из таких, используйте эту функцию, чтобы сообщить о такой границе ядру. Например, если ваше устройство имеет проблемы с запросами, которые пересекают границу 4 Мб, передайте в маске 0x3fffff. Маска по умолчанию 0xffffffff.

 

void blk_queue_dma_alignment(request_queue_t *queue, int mask);

Функция, которая сообщает ядру об ограничении на выравнивание памяти, накладываемом устройством на передачи DMA. Все запросы создаются с заданным выравниванием и размер запроса также совпадает с выравниванием. Маска по умолчанию 0x1ff, которая приводит к тому, что все запросы выровнены по границам в 512 байт.

 

void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);

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

Анатомия запроса

В нашем простом примере мы столкнулись со структурой request. Однако, мы едва поцарапали поверхность этой сложной структуры данных. В этом разделе мы посмотрим некоторые детали того, как блочные запросы ввода/вывода представлены в ядре Linux.

 

Каждая структура request представляет собой один блочный запрос ввода/вывода, хотя она могла быть образована в результате слияния на более высоком уровне нескольких самостоятельных запросов. Секторы, передаваемые на любой конкретный запрос, могут быть распределены по всей основной памяти, хотя они всегда соответствуют на блочном устройстве набору последовательных секторов. Запрос представлен в виде набора сегментов, каждый из которых соответствует одному буферу в памяти. Ядра может объединить несколько запросов, связанных со смежными секторами на диске, но оно никогда не объединяет внутри одной структуры request операции чтения и записи. Ядро также удостоверяется, что запросы не объединены, если результат будет нарушать любое из ограничений очереди запросов, описанных в предыдущем разделе.

 

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

Структура bio

Когда ядро, в виде файловой системы, подсистемы виртуальной памяти, или системного вызова решает, что набор блоков должен быть передан в или от устройства блочного ввода/вывода, оно для описания этой операции помещает их вместе в структуру bio. Затем эта структура передаётся коду блочного ввода/вывода, который объединяет её с существующей структурой request или, в случае необходимости, создаёт новую. Структура bio содержит в себе всё, что необходимо блочному драйверу для выполнения запроса без ссылки на процесс пользовательского пространства, который был причиной начала этого запроса.

 

Структура bio, которая определена в <linux/bio.h>, содержит ряд полей, которые могут быть использованы авторами драйверов:

 

sector_t bi_sector;

Первый (512 байтный) сектор, передаваемый для этой bio.

 

unsigned int bi_size;

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

 

unsigned long bi_flags;

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

 

unsigned short bio_phys_segments;

unsigned short bio_hw_segments;

Число физических сегментов, содержащихся в этой bio и количество сегментов, видимых оборудованием после выполнения DMA отображения, соответственно.

 

Ядро bio, однако, представляет собой массив, названный bi_io_vec, который состоит из следующей структуры:

 

struct bio_vec {

    struct page *bv_page;

    unsigned int bv_len;

    unsigned int bv_offset;

};

 

Рисунок 16-1 показывает, как все эти структуры связать вместе. Как вы можете видеть, к тому времени, как блок запроса ввода/вывода превращается в структуру bio, он разбит на отдельные страницы физической памяти. Всё, что необходимо сделать драйверу, это достигнуть  данных через этот массив структур (в них есть bi_vcnt) и передать данные внутри каждой страницы (но только len байт, начиная с offset).

 

Рисунок 16-1. Структура bio

Рисунок 16-1. Структура bio

 

Работа непосредственно с массивом bi_io_vec не приветствуется в интересах того, чтобы разработчики ядра могли в будущем изменить структуру bio, не нарушая чего-либо. Чтобы выполнить это, для облегчения процесса работы со структурой bio предоставлен набор макросов. Всё начинается с bio_for_each_segment, который просто просматривает каждую необработанную запись в массиве bi_io_vec. Этот макрос следует использовать следующим образом:

 

int segno;

struct bio_vec *bvec;

 

bio_for_each_segment(bvec, bio, segno) {

    /* Делаем что-нибудь с этим сегментом */

}

 

Внутри этого цикла bvec указывает на текущую запись bio_vec и segno является текущим номером сегмента. Эти значения могут быть использованы для создания передач DMA (альтернативный способ с использованием blk_rq_map_sg описан в разделе "Блочные запросы и DMA"). Если вам необходимо получить доступ к страницам напрямую, вы должны сначала убедиться, что надлежащий виртуальный адрес ядра существует; для этого вы можете использовать:

 

char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);

void __bio_kunmap_atomic(char *buffer, enum km_type type);

 

Эта низкоуровневая функция позволяет вам напрямую отобразить буфер, находящийся в данной bio_vec, как указано индексом i. Создаётся атомарная kmap; вызывающий должен предоставить для использования соответствующий слот (как описано в разделе "Карта памяти и структура page" в Главе 15).

 

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

 

struct page *bio_page(struct bio *bio);

Возвращает указатель на структуру page, представляющую страницу, которая будет передана следующей.

 

int bio_offset(struct bio *bio);

Возвращает смещение внутри страницы для передаваемых данных.

 

int bio_cur_sectors(struct bio *bio);

Возвращает число секторов, которые будут переданы из текущей страницы.

 

char *bio_data(struct bio *bio);

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

 

char *bio_kmap_irq(struct bio *bio, unsigned long *flags);

void bio_kunmap_irq(char *buffer, unsigned long *flags);

bio_kmap_irq возвращает виртуальный адрес ядра для любого буфера, независимо от того, находится ли он в верхней или нижней памяти. Используется атомарная kmap, поэтому драйвер не может спать, пока это отображение активно. Для отключения буфера используйте bio_kunmap_irq. Обратите внимание, что аргумент flags передаётся здесь через указатель. Отметим также, что поскольку используется атомарная kmap, вы не можете отобразить за раз более одного сегмента.

 

Все эти функции описаны только для доступа в "текущий" буфер - первый буфер, который, как ядро знает, не был передан. Драйверы часто хотят работать через несколько буферов в bio до завершения сигнализации о завершении для любого из них (с помощью end_that_request_first, которая будет описана в ближайшее время), так что часто эти функции бесполезны. Для работы с внутренностями структуры bio существуют несколько других макросов (смотрите для подробностей <linux/bio.h>).

Поля структуры запроса

Теперь, когда мы имеем представление о том, как работает структура bio, мы можем углубиться в struct request и посмотреть, как работает обработка запроса. Поля этой структуры включают в себя:

 

sector_t hard_sector;

unsigned long hard_nr_sectors;

unsigned int hard_cur_sectors;

Поля, которые отслеживают секторы, которые драйвер до сих пор не завершил. Первый сектор, который не был передан, хранится в hard_sector, общим числом секторов для передачи является hard_nr_sectors, а числом секторов, оставшихся в текущей bio, является hard_cur_sectors. Эти поля предназначены для использования только внутри блочной подсистемы; драйверам не следует  их использовать.

 

struct bio *bio;

bio является связным списком структур bio для этого запроса. Вы не должны обращаться напрямую к этому полю; вместо того используйте rq_for_each_bio (описанную ниже).

 

char *buffer;

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

 

unsigned short nr_phys_segments;

Число отдельных сегментов, занятых этим запросом в физической памяти после того, как были объединены соседние страницы.

 

struct list_head queuelist;

Структура связного списка (описанного в разделе "Связные списки" в Главе 11), которая связывает запрос с очередью запросов. Если (и только если) вы удаляете запрос из очереди с помощью blkdev_dequeue_request, вы можете использовать голову этого списка для отслеживания запроса в внутреннем списке, поддерживаемом вашим драйвером.

 

Рисунок 16-2 показывает, как собраны вместе структура запроса и его компоненты структур bio. На рисунке, этот запрос был частично выполнен; поля cbio и buffer указывают на первую bio, которая ещё не была передана.

 

Рисунок 16-2. Очередь запросов с частично выполненным запросом

Рисунок 16-2. Очередь запросов с частично выполненным запросом

 

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

Барьерные запросы

Чтобы улучшить производительность ввода/вывода, блочный уровень переупорядочивает запросы перед тем, как ваш драйвер их увидит. Ваш драйвер тоже может изменить порядок запросов, если для этого есть основания. Часто это переупорядочивание происходит путём передачи нескольких запросов к диску, позволяя оборудованию выяснить оптимальный порядок. Однако, существует проблема с неограниченным переупорядочиванием запросов: некоторые приложения требуют гарантий того, что определённые операции будут завершены до того, как начались другие. Менеджер реляционной база данных, например, должен быть абсолютно уверен, что их информация  журналирования была скинута на диск до выполнения транзакции с содержимым базы данных. Журналирующие файловые системы, которые сейчас используются в большинстве систем Linux, имеют очень похожие ограничения на порядок выполнения. Если некорректные операции переупорядочены, результатом может быть серьёзное необнаруживаемое повреждение данных.

 

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

 

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

 

void blk_queue_ordered(request_queue_t *queue, int flag);

 

Чтобы указать, что ваш драйвер реализует барьерные запросы, установите параметр flag в ненулевое значение.

 

Фактическая реализация барьерных запросов - это просто вопрос проверки на соответствующий флаг в структуре request. Для выполнения такой проверки была предоставлен макрос:

 

int blk_barrier_rq(struct request *req);

 

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

Неповторяемые запросы

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

 

Если ваш драйвер принимает во внимание повтор неудавшегося запроса, сначала он должен сделать вызов:

 

int blk_noretry_request(struct request *req);

 

Если этот макрос возвращает ненулевое значение, ваш драйвер должен просто прервать запрос с кодом ошибки, вместо его повтора.

Функции завершения запроса

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

 

Когда ваше устройство завершило передачу некоторых или всех секторов в запросе ввода/вывода, оно должно проинформировать блочную подсистему с помощью:

 

int end_that_request_first(struct request *req, int success, int count);

 

Данная функция сообщает блочному коду, что ваш драйвер закончил передачу count секторов, начиная с того, где вы последний раз были прерваны. Если этот ввод/вывод был успешным, success передаётся как 1, иначе передаётся 0. Заметьте, что вы должны просигнализировать о завершении в порядке от первого секторе до последнего; если ваш драйвер и устройство и как-то скрытно выполняют запросы не по порядку, вам придётся хранить нестандартное состояние выполнения до завершения передачи промежуточных секторов.

 

Возвращаемое значение end_that_request_first показывает, были ли переданы все секторы в этом запросе или нет. Возвращаемое значение 0 означает, что были переданы все секторы и что запрос выполнен. В этот момент вы должны удалить запрос из очереди с помощью blkdev_dequeue_request (если вы ещё этого не сделали) и передать его в:

 

void end_that_request_last(struct request *req);

 

end_that_request_last информирует ожидающих запрос, что он завершён и удаляет структуру request; она должна быть вызвана при удержании блокировки очереди.

 

В нашем простом примере sbull, мы не использовали какую-либо из перечисленных выше функций. Этот пример, вместо этого, вызывает end_request. Чтобы показать последствия этого вызова, вот вся функция end_request, как она выглядит в ядре версии 2.6.10:

 

void end_request(struct request *req, int uptodate)

{

    if (!end_that_request_first(req, uptodate, req->hard_cur_sectors)) {

        add_disk_randomness(req->rq_disk);

        blkdev_dequeue_request(req);

        end_that_request_last(req);

    }

}

 

Функция add_disk_randomness использует время блочных запросов ввода/вывода, чтобы способствовать энтропии системного пула случайных чисел; она должна вызываться только если время обращения к диску действительно случайное. Это верно для большинства механических устройств, но это не относится к виртуальным устройствам, находящимся в памяти, таким как sbull. По этой причине, более сложная версия sbull, показанная в следующем разделе, не вызывает add_disk_randomness.

Работа с bios

Теперь вы знаете достаточно, чтобы написать блочный драйвер, который работает непосредственно со структурами bio, которые составляют запрос. Однако, пример может помочь. Если драйвер sbull загружен с параметром request_mode, установленным в 1, он регистрирует функцию request, осведомлённую о bio, вместо простой функции, которую мы видели выше. Эта функция выглядит следующим образом:

 

static void sbull_full_request(request_queue_t *q)

{

    struct request *req;

    int sectors_xferred;

    struct sbull_dev *dev = q->queuedata;

 

    while ((req = elv_next_request(q)) != NULL) {

        if (! blk_fs_request(req)) {

            printk (KERN_NOTICE "Skip non-fs request\n");

            end_request(req, 0);

            continue;

        }

        sectors_xferred = sbull_xfer_request(dev, req);

        if (! end_that_request_first(req, 1, sectors_xferred)) {

            blkdev_dequeue_request(req);

            end_that_request_last(req);

        }

    }

}

 

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

 

static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)

{

    struct bio *bio;

    int nsect = 0;

 

    rq_for_each_bio(bio, req) {

        sbull_xfer_bio(dev, bio);

        nsect += bio->bi_size/KERNEL_SECTOR_SIZE;

    }

    return nsect;

}

 

Здесь мы вводим ещё один макрос: rq_for_each_bio. Как и следовало ожидать, этот макрос просто проходит через каждую структуру bio в запросе, давая нам указатель, который мы можем передать в sbull_xfer_bio для передачи. Эта функция выглядит следующим образом:

 

static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)

{

    int i;

    struct bio_vec *bvec;

    sector_t sector = bio->bi_sector;

 

    /* Работаем с каждым сегментом независимо. */

    bio_for_each_segment(bvec, bio, i) {

        char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);

        sbull_transfer(dev, sector, bio_cur_sectors(bio),

                    buffer, bio_data_dir(bio) == WRITE);

        sector += bio_cur_sectors(bio);

        __bio_kunmap_atomic(bio, KM_USER0);

    }

    return 0; /* Всегда "успешно" */

}

 

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

 

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

Блочные запросы и DMA

Если вы работаете над высокопроизводительным блочным драйвером, скорее всего вы будете использовать для фактической передачи данных DMA. Блочный драйвер может, безусловно, пройти через структуры bio, как описано выше, создать отображения DMA для каждого из них, и передать результат в устройство. Существует, однако, более простой способ, если ваше устройство может выполнять ввод/вывод с разборкой/сборкой. Функция:

 

int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);

 

заполняет данный list полным набором сегментов данного запроса. Сегменты, которые являются соседними в памяти, объединяются до включения в лист разборки, так что вам не требуется пытаться находить их самостоятельно. Возвращаемое значение является числом записей в списке. Эта функция также передает обратно, в своём третьем аргументе, список разборки, подходящий для dma_map_sg. (Смотрите раздел "Преобразования разборки/сборки" в Главе 15 для более подробной информации о dma_map_sg.)

 

Ваш драйвер должен выделить место для хранения списка разборки перед вызовом blk_rq_map_sg. Список должен иметь возможность удерживать по крайней мере, так много записей, сколько имеет физических сегментов запрос; поле nr_phys_segments в struct request хранит такой счётчик, который не будет превышает максимальное число физических сегментов, заданных с помощью blk_queue_max_phys_segments.

 

Если вы не хотите, чтобы blk_rq_map_sg объединяла смежные сегменты, вы можете изменить заданное по умолчанию поведение таким вызовом:

 

clear_bit(QUEUE_FLAG_CLUSTER, &queue->queue_flags);

 

Некоторые дисковые SCSI драйверы отмечают таким образом свою очередь запросов, поскольку они не получают выгоды от объединения запросов.

Работа без очереди запросов

Ранее мы обсуждали работу, выполняемую ядром для оптимизации порядка запросов в очереди; эта работа включает в себя сортировку запросов и, возможно, даже остановку очереди, чтобы позволить поступить ожидаемым запросам. Эти методы помогают системной производительности при работе с реальным вращающимся дисковым накопителем. Однако, они полностью бесполезны с устройством, подобным sbull. Многие блочно-ориентированные устройства, такие как массивы флэш-памяти, считыватели мультимедийных карт, используемых в цифровых камерах и диски в ОЗУ имеют настоящую производительность при произвольном доступе и не получают пользы от расширенной логики очередей запросов. Другие устройства, такие, как программные RAID массивы или виртуальные диски, созданные менеджерами логических разделов, не имеют характеристик производительности, для которых оптимизированы очереди запросов блочного уровня. Для такого рода устройства было бы лучше принимать запросы непосредственно от блочного уровня, и совсем не возиться с очередью запросов.

 

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

 

typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);

 

Обратите внимание, что очередь запросов по-прежнему присутствует, хотя она фактически никогда не будет принимать какие-либо запросы. Функция make_request принимает в качестве своего основного параметра структуру bio, которая представляет собой один или несколько буферов, подлежащих передаче. Функция make_request может выполнить одно из двух: оно может либо непосредственно выполнить передачу, либо может перенаправить запрос на другое устройство.

 

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

 

void bio_endio(struct bio *bio, unsigned int bytes, int error);

 

Здесь bytes является количеством байтов, которое вы передали до этого. Она может быть меньше, чем количество байт, предоставленных bio в целом; таким образом, вы можете сообщить о частичном завершении и обновить внутренние указатели "текущего буфера" в bio. Вы должны либо вызвать bio_endio снова, как только ваше устройство выполнит дальнейший процесс, или просигнализировать об ошибке, если вам не удаётся выполнить запрос. Ошибки указываются передачей ненулевого значения параметра error; эта значение, как правило, код ошибки, такой как -EIO. make_request должна вернуть 0, независимо от того, успешен ли ввод/вывод.

 

Если sbull загружается с request_mode=2, он работает с функцией make_request. Так как sbull уже имеет функцию, которая может передать одну bio, функция make_request очень проста:

 

static int sbull_make_request(request_queue_t *q, struct bio *bio)

{

    struct sbull_dev *dev = q->queuedata;

    int status;

 

    status = sbull_xfer_bio(dev, bio);

    bio_endio(bio, bio->bi_size, status);

    return 0;

}

 

Обратите внимание, что вы никогда не должны вызывать bio_endio из обычной функции request; вместо этого работа выполняется функцией end_that_request_first.

 

Некоторым блочным драйверам, таким как реализующим менеджеры разделов и программные RAID массивы, действительно необходимо перенаправить запрос другому устройству, которое выполняет фактический ввода/вывод. Написание такого драйвера выходит за рамки этой книги. Заметим, однако, что если функция make_request возвращает ненулевое значение, bio передаётся снова. "Эшелонированный"  драйвер может, следовательно, изменить поле bi_bdev, чтобы указать на другое устройство, изменить начальное значение сектора, а затем вернуться; затем блочная система передаёт bio новому устройству. Существует также вызов bio_split, которые может быть использован для разделения bio на несколько кусков, для передачи более чем одному устройству. Хотя, если параметры очереди установлены должным образом, в расщеплении bio таким образом почти никогда нет необходимости.

 

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

 

request_queue_t *blk_alloc_queue(int flags);

 

Эта функция отличается от blk_init_queue в том, что она фактически не создаёт очередь для хранения запросов. Аргумент flags представляет собой набор флагов создания, которые будут использоваться при выделении для очереди памяти; обычно верным значением является GFP_KERNEL. После того, как вы имеете очередь, передайте её в вашу функцию make_request для blk_queue_make_request:

 

void blk_queue_make_request(request_queue_t *queue, make_request_fn *func);

 

Код sbull, устанавливающий функцию make_request, выглядит следующим образом:

 

dev->queue = blk_alloc_queue(GFP_KERNEL);

if (dev->queue == NULL)

    goto out_vfree;

blk_queue_make_request(dev->queue, sbull_make_request);

 

Для любопытных, некоторое время, проведённое в копании в drivers/block/ll_rw_block.c показывает, что все очереди имеют функцию make_request. Версия по умолчанию, generic_make_request, обрабатывает объединение bio в структуру request. Предоставляя  собственную функцию make_request, драйвер в действительности только подменяет данный метод очереди запросов и выполняет многое из этой работы.

 

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