Содержание


Блочные устройства. Различные подходы к реализации обмена данными

Comments

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

Использование очереди для обработки запросов ввода-вывода

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

Листинг 1. Обработка запросов с помощью очереди (файл block_mod_s.c)
//функция, непосредственно выполняющая обмен данными
static int transfer( struct disk_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 ) {
    ERR( "beyond-end write (%ld %ld)\n", offset, nbytes );
    return -EIO;
  }
  if( write )
    memcpy( dev->data + offset, buffer, nbytes );
  else
     memcpy( buffer, dev->data + offset, nbytes );
  return 0;
}
//функция, организующая обработку запросов с использованием очереди
static void simple_request( struct request_queue *q ) {
  struct request *req;
  unsigned nr_sectors, sector;
  DBG( "entering simple request routine\n" );
  req = blk_fetch_request( q );
  while( req ) {
    int ret = 0;
    struct disk_dev *dev = req->rq_disk->private_data;
    if( !blk_fs_request( req ) ) {
      ERR( "skip non-fs request\n" );
      __blk_end_request_all( req, -EIO );
      req = blk_fetch_request( q );
      continue;
    }
    nr_sectors = blk_rq_cur_sectors( req );
    sector = blk_rq_pos( req );
    ret = transfer( dev, sector, nr_sectors, req->buffer, rq_data_dir( req ) );
    if( !__blk_end_request_cur( req, ret ) )
      req = blk_fetch_request( q );
  }
}

Ещё не обработанные или не завершённые запросы выбираются из очереди ядра request_queue вызовом:

struct request* req blk_fetch_request( struct request_queue* );

Запрос может потребовать выполнения специальных (управляющих) операций, не относящихся к чтению или записи и переданными «не тому» устройству, но такие запросы можно отфильтровать вызовом:

bool blk_fs_request( struct request* );

Если запрос признан корректным, то из него извлекаются следующие параметры:

  • blk_rq_pos( struct request* )— начальный сектор для обмена;
  • blk_rq_cur_sectors( struct request* )— число секторов подлежащих обмену;
  • (struct request*)->buffer— буфер в пространстве ядра;
  • rq_data_dir( struct request* )— направление обмена (запись или чтение).

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

Независимо от того, как был выполнен (удачно или нет) запрос, ядру следует сообщить о его завершении и переходе к следующему запросу из очереди. Для уведомления ядра используются вызовы:

__blk_end_request_all( struct request*, int errno );
__blk_end_request_cur( struct request*, int errno );

В параметре errno передаётся результат операции: 0 в случае успеха, отрицательный код (как принято в ядре) в случае ошибки. После этих вызовов текущий запрос считается обработанным и удаляется из очереди.

Примечание: В API блочных операций практически для всех вызовов «с подчёркиванием» существуют парные вызовы «без подчёркивания», например, __blk_end_request_all() соответствует blk_end_request_all(). Разница между ними состоит в том, что методы с подчёркиванием сами выполняют блокировку для монопольного использования очереди (мы инициализировали её в самом начале вместе с очередью). А при использовании методов без подчёркивания это должен сделать программист, так как эти методы должны выполняться с уже захваченной блокировкой. Подобный подход обеспечивает дополнительную гибкость при написании кода, но если механически заменить метод «с подчёркиванием» на «без подчёркивания» без соответствующих изменений в коде, то это приведёт к мгновенному зависанию системы.

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

Запрос с обработкой вектора BIO

Следующий вариант (значение параметра mode равно 1) хотя и сложнее по смыслу, но проще с точки зрения реализации.

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

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

Листинг 2. Обработка запросов с помощью вектора BIO (файл block_mod_s.c)
//функция для передачи одиночного BIO
static int xfer_bio( struct disk_dev *dev, struct bio *bio ) {
  int i, ret;
  struct bio_vec *bvec;
  sector_t sector = bio->bi_sector;
  DBG( "entering xfer_bio routine\n" );
  bio_for_each_segment( bvec, bio, i ) { //работаем с каждым сегментом независимо.
  char *buffer = __bio_kmap_atomic( bio, i, KM_USER0 );
  sector_t nr_sectors = bio_sectors( bio );
  ret = transfer( dev, sector, nr_sectors, buffer, bio_data_dir( bio ) == WRITE );
  if( ret != 0 ) return ret;
  sector += nr_sectors;
    __bio_kunmap_atomic( bio, KM_USER0 );
  }
  return 0;
}
//функция для передачи всего запроса
static int xfer_request( struct disk_dev *dev, struct request *req ) {
  struct bio *bio;
  int nsect = 0;
  DBG( "entering xfer_request routine\n" );
  __rq_for_each_bio( bio, req ) {
    xfer_bio( dev, bio );
    nsect += bio->bi_size / KERNEL_SECTOR_SIZE;
  }
  return nsect;
}
//функция для обработки запроса с использованием вектора BIO
static void full_request( struct request_queue *q ) {
  struct request *req;
  int sectors_xferred;
  DBG( "entering full request routine\n" );
  req = blk_fetch_request( q );
  while( req ) {
    struct disk_dev *dev = req->rq_disk->private_data;
    if( !blk_fs_request( req )) {
      ERR( "skip non-fs request\n" );
      __blk_end_request_all( req, -EIO );
      req = blk_fetch_request( q );
      continue;
    }
    sectors_xferred = xfer_request( dev, req );
      if( !__blk_end_request_cur( req, 0 ) )
        req = blk_fetch_request( q );
  }
}

Код функции-обработчика full_request() похож на код функции simple_request() из предыдущего листинга, только вместо функции transfer() вызывается функция xfer_request(), последовательно извлекающая структуры struct bio из тела запроса и вызывающая функцию xfer_bio() для каждой такой структуры. Эта функция извлекает параметры и осуществляет обмен данными для единичного сегмента, используя функцию transfer() из листинга 1.

Прямое выполнение запроса без использования очереди

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

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

  case RM_NOQUEUE:
    dev->queue = blk_alloc_queue( GFP_KERNEL );
    if( dev->queue == NULL ) goto out_vfree;
    blk_queue_make_request( dev->queue, make_request );
    break;

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

static void make_request( struct request_queue *q, struct bio *bio ) {
  struct disk_dev *dev = q->queuedata;
  int status = xfer_bio( dev, bio );
  bio_endio( bio, status );
}

Функцию xfer_bio() можно найти в листинге 2. На этом мы завершим рассмотрение различных способов обработки запросов ввода-вывода в модуле блочного устройства.

Неприятным фактом является то, что API блочных устройств достаточно быстро меняется, и то, что было работоспособно ещё в ядре 2.6.29, уже может не компилироваться в ядре 3.0. Некоторую, хотя и не полную, информацию по API ядра для блочных устройств можно найти по этой ссылке.

Практическое тестирование

Мы закончим рассмотрение модулей блочных устройств практическим тестированием созданных компонентов. Установим созданный модуль в систему:

# insmod block_mod_s.ko# ls -l /dev/x*
brw-rw---- 1 root disk 252,  0 нояб. 13 02:01 /dev/xda
...
brw-rw---- 1 root disk 252, 48 нояб. 13 02:01 /dev/xdd
# hdparm -g /dev/xdd
/dev/xdd:
 geometry      = 128/4/16, sectors = 8192, start = 16

Стандартным способом с помощью утилиты fdisk cоздадим некоторую структуру разделов:

# mkfs.vfat /dev/xdd
mkfs.vfat 3.0.12 (29 Oct 2011)
# fdisk -l /dev/xda
Диск /dev/xda: 4 МБ, 4194304 байт
4 heads, 16 sectors/track, 128 cylinders, всего 8192 секторов
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x2fb10b5b
 
Устр-во Загр     Начало       Конец       Блоки   Id  Система
/dev/xda1               1        2000        1000   83  Linux
/dev/xda2            2001        5000        1500    5  Расширенный
/dev/xda3            5001        8191        1595+  83  Linux
/dev/xda5            2002        3500         749+  83  Linux
/dev/xda6            3502        5000         749+  83  Linux
# ls -l /dev/x*
brw-rw---- 1 root disk 252,  0 нояб. 13 02:11 /dev/xda
brw-rw---- 1 root disk 252,  1 нояб. 13 02:11 /dev/xda1
...
brw-rw---- 1 root disk 252,  6 нояб. 13 02:11 /dev/xda6
brw-rw---- 1 root disk 252, 16 нояб. 13 02:01 /dev/xdb
brw-rw---- 1 root disk 252, 32 нояб. 13 02:01 /dev/xdc
brw-rw---- 1 root disk 252, 48 нояб. 13 02:06 /dev/xdd

Как видно из начала вывода, диск /dev/xdd был отформатирован в формате FAT-32 без разметки на разделы. Теперь отформатируем один раздел на диске /dev/xda.

# mkfs.ext2 /dev/xda1
mke2fs 1.42.3 (14-May-2012)
...
# fsck /dev/xda1
fsck из util-linux 2.21.2
e2fsck 1.42.3 (14-May-2012)
/dev/xda1: clean, 11/128 files, 38/1000 blocks
...
# fsck /dev/xdd
fsck из util-linux 2.21.2
dosfsck 3.0.12, 29 Oct 2011, FAT32, LFN
/dev/xdd: 0 files, 0/2036 clusters

Мы можем смонтировать диски из числа отформатированных в заранее созданные каталоги:

# ls /mnt
dska  dskb  dskc  dskd  dske  efi  iso
# mount -tvfat /dev/xdd /mnt/dskd# mount -text2 /dev/xda1 /mnt/dska# mount | grep /xd
/dev/xdd on /mnt/dskd type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=cp437,
iocharset=ascii,shortname=mixed,errors=remount-ro)
/dev/xda1 on /mnt/dska type ext2 (rw,relatime)
# df | grep /xd
/dev/xdd              4072            0     4072            0% /mnt/dskd
/dev/xda1              979           17      912            2% /mnt/dska

И проверить, как выполняется копирование файлов на созданных файловых системах.

# echo ++++++++++++++++++++ > /mnt/dska/f1# cp /mnt/dska/f1 /mnt/dskd/f3# cat /mnt/dskd/f3
+++++++++++++++++++++++
# tree /mnt/dsk*
/mnt/dska
|-- f1
`-- lost+found
/mnt/dskd
`-- f3
2 directories, 3 files

А вот что показывает сравнительное тестирование скорости созданных дисковых устройств с помощью стандартной GNU-утилиты для разных выбранных стратегий обработки запросов ввода/вывода:

# insmod block_mod_s.ko mode=0# hdparm -t /dev/xda
/dev/xda:
 Timing buffered disk reads:   4 MB in  0.01 seconds = 318.32 MB/sec

# insmod block_mod_s.ko mode=1# hdparm -t /dev/xda
/dev/xda:
 Timing buffered disk reads:   4 MB in  0.03 seconds = 116.02 MB/sec

# insmod block_mod_s.ko mode=2# hdparm -t /dev/xda
/dev/xda:
 Timing buffered disk reads:   4 MB in  0.01 seconds = 371.92 MB/sec

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

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=931980
ArticleTitle=Блочные устройства. Различные подходы к реализации обмена данными
publish-date=05302013