Готовый перевод Linux Device Drivers 3 / Драйверы устройств Линукс (третье издание): 16.3. Обработка запросов

Внимание! Этот перевод, возможно, ещё не готов, так как модераторы установили для него статус «перевод редактируется»

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

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

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

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

Прототип метода request:

void request(request_queue_t *queue);

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

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

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

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

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

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

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

В примере драйвера sbull, представлены несколько различные
методы для обработки запросов. По умолчанию, sbull использует метод
sbull_request, который является простым примером 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->current_nr_sectors,
req->buffer, rq_data_dir(req));
end_request(req, 1);
}
}

В функции используется структура struct request. Структура
передает нам на исполнение запросы блочного ввода-вывода. Позже, мы
рассмотрим ее подробнее.

Ядро предоставляет функцию elv_next_request для получения
первого, не выполненного запроса в очереди. Если в очереди нет запросов
для исполнения, функция возвращает NULL. Имейте в виду, что
elv_next_request не удаляет запрос из очереди. Если вы вызовете ее
дважды без промежуточных операций, то оба раза она вернет указатель
на одну и туже struct request. Это простой режим, в котором запросы
изымаются из очереди, только когда их обработка завершена.

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

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

Когда мы получаем запрос не от файловой системы, мы заносим в
succeeded значение «0», это показывает, что запрос не может быть
обработан. В противном случае вызывается sbull_transfer для передачи
данных. Для этого структура struct request предоставляет поля:

sector_t sector;

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

unsigned long nr_sectors;
Количество передающихся (512-байтных) секторов.

char *buffer;
Указатель на буфер, из которого или в который будут передаваться
данные. Этот указатель на виртуальный адрес ядра, и может быть
непосредственно разыменован (dereferenced) при необходимости.

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 реализуется законченный драйвер простого
RAM-диска. Однако это все еще не настоящий драйвер, он не может
применяться для многих типов устройств по двум причинам.

Во-первых, sbull выполняет запросы синхронно, по одному.
Высокопроизводительное дисковое устройство может обслуживать
несколько запросов одновременно. Контроллер диска может выбирать
наиболее оптимальную последовательность обработки запросов.
Обрабатывая только первый запрос в очереди, мы никогда не обработаем
одновременно несколько запросов. Для использования возможности
работать более чем с одним запросом, необходимо изучить очередь
запросов и структуру request. В этом помогут следующие подразделы.

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

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

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

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

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

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

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

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

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

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

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

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

Аргументами является функция request и spinlock для контроля
доступа к очереди. Функция выполняет выделение памяти, поэтому может
претерпеть неудачу. Вы должны всегда проверять возвращаемое значение
перед использованием очереди.

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

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

void blk_cleanup_queue(request_queue_t *);

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

16.3.3.2 Функции работы с очередью

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

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

struct request *elv_next_request(request_queue_t *queue);

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

Для реального удаления запроса из очереди используется функция:

void blkdev_dequeue_request(struct request *req);

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

Если удаленный запрос требуется положить обратно в очередь,
используйте функцию:

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

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

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

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);

Функция сообщает ядру наибольший физический адрес, с которым
ваше устройство может произвести ПДП. Если запрос содержит
ссылку на память выше этого адреса, для этой операции
используется дополнительный буфер. Конечно, это снижает
производительность, и этого следует избегать везде, где возможно.
Вы можете предоставить любой разумный адрес в этом аргументе
или использовать один из предопределенных символов:
BLK_BOUNCE_HIGH - использовать дополнительный буфер для верхней
памяти, BLK_BOUNCE_ISA – драйвер может осуществлять ПДП только
в 16Мбайтной ISA зоне, или BLK_BOUNCE_ANY - драйвер может
осуществлять ПДП в любой зоне. По умолчанию используется
значение BLK_BOUNCE_ANY.

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
указывают как много физических сегментов (несмежных областей
системной памяти) может содержаться в одном запросе. По
умолчанию, оба эти параметра равны 128. И последняя функция
blk_queue_max_segment_size сообщает ядру максимальный размер
(в байтах) сегмента в запросе, значение по умолчанию 65536 байт.

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);

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

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

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

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

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

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

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

16.3.4.1 Структура bio

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

Структура bio определена в файле <linux></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,
количество аппаратных сегментов после выполнения ПДП.

В основе структуры 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

Непосредственная работа с массивом 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 -
содержит номер текущего сегмента. Эти значения могут использоваться
для настройки ПДП (как альтернатива использования функции
blk_rq_map_sg, описанной в подразделе «Блочные запросы и ПДП»). Если
вам нужно обращаться непосредственно к страницам, то вы должны
обеспечить существование в ядре виртульного адреса. Для этого можно
использовать функции:

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

Это низкоуровневые функции, позволяющиеам непосредственно
отобразить отменить отображение буфера, который предоставляется i-ым
элементом bio_vec. kmap создается атомарно. При вызове необходимо
обеспечить подходящее место использования, как описано разделе
«Отображение памяти и структура страниц», главы 15.

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

struct page *bio_page(struct bio *bio);
Возвращает указатель на структуру
следующую для передачи страницу.

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, является указателем.
Также имейте в виду, что нельзя отобразить одновременно более
одного сегмента.

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

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

Теперь мы знакомы с идеологией работы структуры bio. Теперь мы
можем глубже рассмотреть структуру 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 Очередь с частично удовлетворенным запросом

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

16.3.4.3 Экранирование запросов

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

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

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

void blk_queue_ordered(request_queue_t *queue, int flag);
Чтобы показать то, что ваш драйвер использует экранирование
запросов, установите параметр flag в не нулевое значение.

Фактическая реализация экранирования запросов является просто
проверкой флага ассоциированного со структурой request. Для
выполнения этой проверки предоставляется макрос:
int blk_barrier_rq(struct request *req);

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

16.3.4.4 Исключение повторных попыток исполнения запросов.

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

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

int blk_noretry_request(struct request *req);

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

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

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

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

int end_that_request_first(struct request *req, int success, int count); Эта функция сообщает, что ваш драйвер завершил передачу еще
count секторов, начиная с того места, где была закончена последняя
операция. Если ввод-вывод был успешен, передайте в параметр success
значение «1». Если возникла ошибка, то передайте «0».

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

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

void end_that_request_last(struct request *req);
blkdev_dequeue_request информирует того, кто ожидает завершения
запроса, и повторно использует структуру request. Функция должна
вызываться при захваченной блокировки очереди.

В нашем простом примере sbull, мы не используем этих функций.
Вместо этого вызывается 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,
реализующего RAM-диск, это не так. Поэтому в следующем подразделе
мы покажем более сложную версию функции для sbull, в которой не
происходит вызова add_disk_randomness.

16.3.5.1 Работа с bio

Вы теперь знаете достаточно для написания драйвера блочного
устройства, который работает со структурой bio, чтобы выполнить запрос.
Если драйвер sbull загружается с параметром request_mode равным «1»,
то он, вместо простой функции request, которая была показана выше,
регистрирует функцию 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; /* всегда возвращаем "succeed" */
}

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

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

16.3.5.2. Запросы и ПДП (Прямой Доступ к Памяти)

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

int blk_rq_map_sg(request_queue_t *queue, struct request *req,
struct scatterlist *list);
Функция заносит в list, сегменты из полученного запроса. А, перед
добавлением, объединяет смежные сегменты, что освобождает вас от этой
работы. Функция возвращает количество элементов в списке. В третьем
аргументе возвращается scatterlist, который может использоваться
для передачи в dma_map_sg (для более подробной информации смотрите
секцию «Сегментированное отображение» главы 15).

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

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

clear_bit(QUEUE_FLAG_CLUSTER, &queue->queue_flags);

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

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

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

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

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

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

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

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

Здесь в bytes содержится количество байт переданых к настоящему
времени. Это значение может быть меньше количества данных bio в
целом. В этом случае вы сигнализируете о частичном выполнении, и
обновляете внутренний указатель текущего буфера в bio. Параметр 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 будет снова
передано в функцию. Поэтому пакетирующий (“stacking”) драйвер может
модифицировать поле bi_bdev, чтобы оно указывало на другое устройство, изменить значение начального сектора и вернуться из функции. Затем
подсистема блочных устройств передаст bio новому устройству. Имеется
вызов bio_split, он может использоваться, чтобы разбить 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 делает это так:
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,
драйвер перекрывает специальный метод очереди запросов и выполняет
большую часть работы.

Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется

Переведено на Нотабеноиде
http://notabenoid.org/book/11832/38377

Переводчики: hardorsoft

Настройки

Готово:

100.00% КП = 1.0