Содержание


Блочные устройства. Создание и регистрация блочных устройств в системе

Comments

В цикле "Разработка модулей ядра Linux" мы практически не затрагивали тему драйверов блочных устройств. Это было связано с тем, что перед обычным разработчиком редко возникает задача разработки драйвера блочного устройства. Но, судя по отзывам и вопросам читателей, это утверждение оказалось ошибочным, и к этой теме имеется определенный интерес. Поэтому в данном миницикле мы вернёмся к рассмотрению драйверов блочных устройств.

Блочные устройства

Блочные устройства в UNIX и Linux — это устройства хранения с произвольным доступом, над которыми размещаются файловые системы. Отсюда и вытекают отличия блочных устройств от символьных устройств, которые были рассмотрены в данной статье. Эти различия в основном связаны с двумя факторами:

  1. Для блочных устройств первостепенным фактором является производительность (т.е. скорость). Для символьных устройств производительность может быть не так важна, так как многие символьные устройства могут работать ниже своей максимальной скорости, и производительность системы в целом от этого не пострадает. Но для блочных устройств скорость — это одно из конкурентных преимуществ операционной системы на рынке. Поэтому основная часть усилий при разработке блочного модуля сосредотачивается на обеспечении требуемой производительности.
  2. Символьные устройства получают запросы ввода-вывода из процессов пространства пользователя, выполняющих операции read() или write(). Блочные устройства могут получать запросы ввода-вывода как из пользовательских процессов, так и из кода ядра (модулей ядра), например, при монтировании дисковых устройств. Поэтому запросы ввода-вывода должны сначала попасть в блочную подсистему ввода-вывода ядра, и только затем быть переданными ею непосредственно драйверу, осуществляющему обмен с устройством.

Блочное устройство обеспечивает обмен блоками данных. Блок— это единица данных фиксированного размера. Размер блока определяется ядром, но чаще всего он совпадает с размером страницы аппаратной архитектуры, и для 32-битной архитектуры x86 составляет 4096 байт. Оборудование хранит данные на физических носителях, разбитых на сектора. Исторически сложилось так, что последние несколько десятилетий аппаратное обеспечение создавалось для работы с секторами размером 512 байт. В последние годы в новых устройствах появилась тенденция оперировать с большими секторами (4096 байт).

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

Регистрация устройства

Регистрация блочного устройства во многом совпадает с регистрацией символьного устройства, но с коррекцией на специфику блочных устройств, так, регистрация устройства и его последующее удаление инициируются вызовами API:

int register_blkdev( unsigned major, const char* name );
void unregister_blkdev( unsigned major, const char* name );

Аналогично символьным устройствам, в качестве параметра major в вызов register_blkdev() может быть передано:

  • принудительное значение, которое следует присвоить в качестве старшего номера для устройств такого класса;
  • нулевое значение, что позволит системе присвоить major первое свободное значение.

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

В качестве name в этот вызов передаётся родовое имя класса устройств, например, для дисков xda, xdb, ... , создаваемых в примере ниже, это будет "xd". Регистрация имени устройства создаёт соответствующую запись в файле /proc/devices, но не создаёт самого устройства в /dev:

$ cat /proc/devices | grep xd
252 xd

Начиная с ядра 2.6 и старше, в принципе, регистрацию с помощью register_blkdev() можно и не проводить, но обычно это делается, как дань традиции.

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

spinlock_t xda_lock;
struct request_queue* xda_request_queue;
...
spin_lock_init( &xda_lock );
if( !( xda_request_queue = blk_init_queue( &xda_request_func, &xda_lock ) ) ) {

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

static void xda_request_func( struct request_queue *q ) { ... }

Отметим, что обработка запросов с помощью очереди ядра не является единственным способом обеспечения операций чтения-записи. Но такой способ используют 95% драйверов. Остающиеся 5% реализуют прямое выполнение запросов по мере их поступления, и такой способ также будет рассмотрен далее.

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

  • создать для этого устройства структуру struct gendisk, описанную в файле linux/genhd.h и являющуюся описанием каждого диска в ядре;
  • заполнить эту структуру и зарегистрировать её в ядре.

Примечание: Экземпляром точно такой же структуры в ядре будет описываться каждый раздел (partition) диска, создаваемый с помощью утилит fdisk или parted. Но разработчику не нужно беспокоится об этих экземплярах, так как их автоматически создадут утилиты, производящие разбивку диска.

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

struct gendisk* alloc_disk( int minors );
void del_gendisk( struct gendisk* );

Параметр minors в вызове alloc_disk() указывает числом младших номеров, резервируемых для отображения этого диска и всех его разделов в каталоге /dev.

Примечание: Как показывает практика, динамическое изменение поля minors в существующей структуре struct gendisk, не приведет к желаемому результату. Это связано с тем, что процесс инициализации очень сложен, и в зависимости от значения minors системой выделяется соответствующее число слотов (дочерних структур struct gendisk), т.е. изменение значения поля minors "постфактум" смысла не имеет и может даже оказаться вредным.

Диски с разметкой MBR и GPT

Параметр minors в вызове alloc_disk() заслуживает отдельного рассмотрения, так как если задать для minors значение 4, то с помощью fdisk вы сможете создать до 4-х первичных (primary) разделов в MBR этого диска (/dev/xda1, …, /dev/xda4). Но вы не сможете в этом случае создать ни единого расширенного (extended) раздела, потому, что первый же созданный логический (logical) раздел внутри расширенного получит номер 5:

# fdisk -l /dev/xda
...
Устр-во Загр     Начало       Конец       Блоки   Id  Система
/dev/xda1               1        4000        2000   83  Linux
/dev/xda2            4001        8191        2095+   5  Расширенный
/dev/xda5            4002        8191        2095   83  Linux
# ls -l /dev/xda*
brw-rw---- 1 root disk 252, 0 нояб. 12 10:44 /dev/xda
brw-rw---- 1 root disk 252, 1 нояб. 12 10:44 /dev/xda1
brw-rw---- 1 root disk 252, 2 нояб. 12 10:44 /dev/xda2
brw-rw---- 1 root disk 252, 5 нояб. 12 10:46 /dev/xda5

Примечание: Пользователи часто интересуются, почему никакими сторонними средствами при разбивке диска не удаётся создать больше 16 разделов. Хотя fdisk не умеет этого делать, но, по определению, цепочка вложенных extended разделов может быть безграничной, и сторонние средства разбивки позволяют создавать такие вложенные структуры расширенных разделов. На самом деле, в принципе создать большее число разделов на физическом диске можно, но драйвер, поддерживающий этот диск, не способен отобразить эти созданные разделы.

Всё сказанное выше относится к способу разбивки диска в стандарте разметки MBR (Master Boot Record), который используется уже на протяжении последних 35 лет. Но в настоящее время происходит давно назревший переход к разметке диска в стандарте GPT (GUID Partition Table). Основные отличительные стороны GPT в контексте рассмотрения модулей блочных устройств заключаются в том, что:

  • используются только первичные разделы;
  • число таких разделов может доходить до 128;
  • полностью изменены идентификаторы типов разделов и они теперь 32-битные (например, для Linux файловых систем — 8300 вместо прежних 83).

Так как утилита fdisk не поддерживает диски, отформатированные в GPT, то для работы с ними используются утилиты parted (gparted) или gdisk:

$ sudo parted /dev/sdb
GNU Parted 3.0
Используется /dev/sdb
(parted) print
Модель: Ut163 USB2FlashStorage (scsi)
Диск /dev/sdb: 1011MB
Размер сектора (логич./физич.): 512B/512B
Таблица разделов: gpt
Disk Flags:.

Номер Начало  Конец   Размер  Файловая система Имя            Флаги
1     1049kB  53,5MB  52,4MB  ext2             EFI System     загрузочный, legacy_boot
10    53,5MB   578MB   524MB                   Microsoft basic data
20    578MB   1011MB   433MB                   Linux filesystem

Утилитами для работы с GPT-дисками можно создать до 128 разделов (или любое число дисков с номерами раздела 1...128). Ниже показаны 18 разделов на диске, созданных с помощью gdisk:

$ sudo gdisk -l /dev/sdf
GPT fdisk (gdisk) version 0.8.4

Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.
Disk /dev/sdf: 7827456 sectors, 3.7 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): C9533CB0-A119-429D-84D8-2B5C1DEA7E30
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 7827422
Partitions will be aligned on 2048-sector boundaries
Total free space is 5984189 sectors (2.9 GiB)

Number  Start (sector)    End (sector)    Size   Code  Name
 21           2048          104447    50.0 MiB   8300  Linux filesystem
...
 29         821248          923647    50.0 MiB   8300  Linux filesystem
101        1128448         1230847    50.0 MiB   8300  Linux filesystem
102        1230848         1333247    50.0 MiB   8300  Linux filesystem
..
108        1845248         1947647    50.0 MiB   8300  Linux filesystem
109        1947648         2050047    50.0 MiB   8300  Linux filesystem

А вот как структура этого GPT-диска отображается в Linux-дистрибутиве Fedora 17.

$ ls /dev/sdf*
/dev/sdf     /dev/sdf103  /dev/sdf106  /dev/sdf109  /dev/sdf23  /dev/sdf26  /dev/sdf29
...
/dev/sdf102  /dev/sdf105  /dev/sdf108  /dev/sdf22   /dev/sdf25  /dev/sdf28

Из показанного очевидно, что при переходе на новый стандарт форматирования ожидаются серьезные изменения, которые следует учитывать при создании модулей блочных устройств. Например, в качестве значения параметра minors в вызове alloc_disk() должно указываться 128.

Заполнение структуры

Мы остановились на создании структуры struct gendisk, и теперь можно вернуться к заполнению её полей. Так как эта структура весьма объёмна, то отметим только те поля, которые нужно заполнить под свой драйвер (есть ещё несколько полей, упомянутых в следующем примере, которые требуют "технического" заполнения):

struct gendisk {
        int major;
        int first_minor;
        int minors;
        char disk_name[DISK_NAME_LEN];
...
        const struct block_device_operations *fops;
...
}

Перечислим важнейшие поля:

  • major— старший номер устройства, который присваивается устройству принудительно или возвращается в результате вызова register_blkdev( 0, ... );
  • minors— максимальное число разделов, обслуживаемых диском; это поле заполняется вызовом alloc_disk() и впоследствии не должно меняться;
  • first_minors— номер minor, представляющий сам диск в /dev (например, dev/xda), последующие разделы диска (в пределах их числа minors) получат соответствующие младшие номера, например, для /dev/xda5 будет использован номер first_minor+5:
    # ls -l /dev/xda*
    brw-rw---- 1 root disk 252, 0 нояб. 12 10:44 /dev/xda
    brw-rw---- 1 root disk 252, 1 нояб. 12 10:44 /dev/xda1
    brw-rw---- 1 root disk 252, 2 нояб. 12 10:44 /dev/xda2
    brw-rw---- 1 root disk 252, 5 нояб. 12 10:46 /dev/xda5
  • disk_name— имя диска, с которым он будет отображаться в /dev или родовое имя устройств, так как драйвер может обслуживать более одного привода устройства; указывается в вызове типа:
    register_blkdev( major, MY_DEVICE_NAME );

    Теперь мы можем персонифицировать имя для конкретного привода, как показано ниже:

    snprintf( disk_name, DISK_NAME_LEN - 1, "xd%c",  'a' + i );

    В дальнейшем эти имена появятся в /proc:

    $ cat /proc/partitions
    major minor  #blocks  name
    
       8        0   58615704 sda
       8        1   45056000 sda1
       8        2    3072000 sda2
       8        3   10485760 sda3
    ...
     252        0       4096 xda
     252        1       2000 xda1
     252        2          1 xda2
     252        5       2095 xda5
     252       16       4096 xdb
    ...
     252       48       4096 xdd
  • fops— адрес таблицы операций устройства, которую мы детально обсудим в следующих статьях.

Кроме заполнения полей структуры struct gendisk примерно в этом месте выполняется установка полной ёмкости устройства, выраженной в секторах по 512 байт:

inline void set_capacity( struct gendisk*, sector_t size );

Завершение регистрации

Теперь структура struct gendisk выделена и заполнена, но диск ещё не готов к использованию. Чтобы завершить подготовительные операции, следует вызвать метод add_disk( struct gendisk* gd), передав в качестве параметра подготовленную структуру gd.

Вызов add_disk() достаточно «опасный», так как, как только он произойдёт, диск становится активным и его методы могут быть вызваны в любое время. На самом же деле, первые вызовы происходят ещё до того, как произойдёт возврат из самого вызова add_disk() (это можно увидеть в системном журнале). Эти вызовы связаны с попытками ядра вычитать начальные сектора диска в поисках таблицы разделов (MBR или GDT). Если к моменту вызова add_disk() драйвер ещё не полностью или некорректно инициализирован, то, с большой вероятностью, вызов приведёт к ошибке драйвера и, через некоторое время, к полному краху системы.

Заключение

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


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


Похожие темы


Комментарии

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

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