16.1. Регистрация |
Блочным драйверам, как и символьным драйверам, необходимо использовать набор интерфейсов регистрации, чтобы их устройства стали для ядра доступными. Концепции являются похожими, но все детали регистрации блочного устройства отличаются. У вас есть для изучения целый ряд новых структур данных и операций устройства.
Первым шагом, выполняемым большинством блочных драйверов является регистрация себя в ядре. Функцией для выполнения этой задачи является register_blkdev (которая объявлена в ):
int register_blkdev(unsigned int major, const char *name);
Аргументами являются старший номер, который будет использовать ваше устройство, и связанное с ним имя (которое ядро будет показывать в /proc/devices). Если major передаётся как 0, ядро выделяет новый старший номер и возвращает его вызвавшему. Как всегда, отрицательное возвращаемое значение из register_blkdev указывает, что произошла ошибка.
Соответствующая функция для отмены регистрации блочного драйвера:
int unregister_blkdev(unsigned int major, const char *name);
Здесь аргументы должны совпадать с переданными в register_blkdev или эта функция вернёт -EINVAL и ничего не отменит.
В ядре версии 2.6 вызов register_blkdev совершенно не обязателен. Функции, выполняемые register_blkdev, уменьшаются с течением времени; единственными задачами, выполняемыми этим вызовом на данном этапе являются: (1) выделение, если требуется, динамического старшего номера; (2) создание записи в /proc/devices. В будущих ядрах register_blkdev может быть полностью удалена. Между тем, однако, большинство драйверов до сих пор её вызывают; это традиция.
Хотя register_blkdev и может быть использована для получения основного номер, она не делает доступным системе никакого дискового накопителя. Существует отдельный интерфейс регистрации, который необходимо использовать для управления отдельными дисками. Использование этого интерфейса требует знания пары новых структур, поэтому с них мы и начинаем.
Символьные устройства делают свои операции доступными для системы с помощью структуры file_operations. Аналогичная структура используется и с блочными устройствами; это struct block_device_operations, которая объявлена в . Ниже приводится краткий обзор полей, находящихся в этой структуре; мы рассмотрим их более подробно снова, когда углубимся в детали драйвера sbull:
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
Функции, которые работают как и их эквиваленты символьного драйвера; они вызываются, когда устройство открываться и закрываться. Блочный драйвер мог бы реагировать на вызов открытия увеличением оборотов устройства, блокировкой дверцы (для съёмных носителей) и так далее. Если вы блокируете в устройстве носитель, вы, безусловно, должны разблокировать его методом release.
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
Метод, реализующий системный вызов ioctl. Однако, блочный уровень сначала перехватывает большое количество стандартных запросов; таким образом, большинство методов ioctl блочного драйвера довольно короткие.
int (*media_changed) (struct gendisk *gd);
Метод, вызываемый ядром, чтобы проверить, поменял ли пользователь накопитель в приводе, возвращает ненулевое значение, если это так. Очевидно, что этот метод применим только к приводам, которые поддерживают сменные носители (и достаточно умны, чтобы сделать флаг "накопитель быть заменён" доступным для драйвера); в других случаях он может быть опущен.
Аргумент struct gendisk это то, как ядро представляет себе один диск; мы будем рассматривать эту структуру в следующем разделе.
int (*revalidate_disk) (struct gendisk *gd);
Метод revalidate_disk вызывается в ответ на замену носителя; это даёт драйверу шанс выполнить любую работу, необходимую для того, чтобы подготовить к использованию новый накопитель. Функция возвращает значение int, но это значение ядром игнорируется.
struct module *owner;
Указатель на модуль, который владеет этой структурой; он должен, как правило, быть проинициализированным как THIS_MODULE.
Внимательные читатели, возможно, заметили интересное упущение из этого списка: нет функций, которые фактически читают или записывают данные. В блочной подсистеме ввода/вывода эти операции обрабатываются функцией request, которая заслуживает своего собственного большого раздела и будет рассмотрена далее в этой главе. Прежде чем мы сможем поговорить об обслуживании запросов, мы должны завершить наше обсуждение регистрации диска.
struct gendisk (объявленная в ) является представлением в ядре отдельного дискового устройства. На самом деле ядро так же использует структуры gendisk для представления разделов, но авторы драйверов не должны об этом знать. В struct gendisk есть несколько полей, которые должны быть проинициализированы блочным драйвером:
int major;
int first_minor;
int minors;
Поля, которые описывают номер(а) устройства, используемого диском. Как минимум, накопитель должен использовать по крайней мере один младший номер. Однако (и в большинстве случаев так должно быть), если ваш накопитель должен допускать разбиение, необходимо также выделить один младший номер для каждого возможного раздела. Общим значением для minors является 16, которое учитывает "полнодисковое" устройство и 15 разделов. Некоторые дисковые приводы используют 64 младших номеров для каждого устройства.
char disk_name[32];
Поле, которое должно содержать имя дискового устройства. Оно появляется в /proc/partitions и sysfs.
struct block_device_operations *fops;
Содержит операции устройства из предыдущего раздела.
struct request_queue *queue;
Структура, используемая ядром для управления запросами ввода/вывода для этого устройства; мы изучим её в разделе "Обработка запроса".
int flags;
(Редко используемый) набор флагов, описывающий состояние привода. Если ваше устройство имеет съёмный носитель, следует установить GENHD_FL_REMOVABLE. Приводы CD-ROM могут установить GENHD_FL_CD. Если по каким-то причинам вы не хотите, чтобы информация о разделах показывалась в /proc/partitions, установите GENHD_FL_SUPPRESS_PARTITION_INFO.
sector_t capacity;
Ёмкость этого диска, в секторах по 512 байт. Тип sector_t может быть размером в 64 бит. Драйвер не должен устанавливать это поле напрямую; вместо этого число секторов передаётся в set_capacity.
void *private_data;
Блочные драйверы могут использовать это поле для указания на свои собственные внутренние данные.
Ядро предоставляет небольшой набор функций для работы со структурами gendisk. Мы представить их здесь, а затем рассмотрим, как sbull использует их, чтобы сделать свои диски доступными системе.
struct gendisk представляет собой динамически создаваемую структуру, которая требует специальной манипуляции в ядре для инициализации; драйверы не могут создавать эту структуру сами. Вместо этого, вы должны вызвать:
struct gendisk *alloc_disk(int minors);
Аргумент minors должен быть числом младших номеров, используемых для этого диска; заметьте, что вы не сможете позже изменить поле minors и ожидать, что всё будет нормально работать.
Когда диск больше не нужен, он должен быть освобождён с помощью:
void del_gendisk(struct gendisk *gd);
gendisk является структурой со счётчиком ссылкой (она содержит kobject). Для манипулирования счётчиком ссылок доступны функции get_disk и put_disk, но драйверы никогда не должны этого делать. Как правило, вызов del_gendisk удаляет последнюю ссылку на gendisk, но это не гарантируется. Таким образом, вполне возможно, что структура могла бы продолжать свое существование (и ваши методы могли бы быть вызванными) после вызова del_gendisk. Однако, если вы удаляете структуру, когда нет пользователей (то есть после окончательного освобождения или в функции очистки вашего модуля), вы можете быть уверены, что вы не услышите о ней снова.
Выделенная структура gendisk не делает диск доступным системе. Чтобы это сделать, вы должны проинициализировать структуру и вызвать add_disk:
void add_disk(struct gendisk *gd);
Имейте в виду одну важную вещь: как только вы вызываете add_disk, диск становится "живым" и его методы могут быть вызваны в любое время. Фактически, первые такие вызовы произойдут, вероятно, ещё до того, как произойдёт возврат из add_disk; ядро будет читать первые несколько блоков в попытке найти таблицу разделов. Таким образом, вы не должны вызывать add_disk, пока ваш драйвер полностью не проинициализирован и не готов откликаться на запросы об этом диске.
Пора взяться за несколько примеров. Драйвер sbull (доступный на FTP сайте O'Reilly вместе с остальными примерами исходного кода) реализует расположенные в памяти виртуальные диски. Для каждого диска sbull выделяет (для простоты с помощью vmalloc) массив памяти; затем он делает массив доступным через блочные операции. Драйвер sbull может быть протестирован путем разбиения виртуального устройства, построения на нём файловых систем и монтирования его в иерархию системы.
Как и наши другие драйверы из примеров, sbull позволяет, чтобы основной номер указывался во время компиляции или во время загрузки модуля. Если номер не указан, он выделяется динамически. Так как для динамического выделения необходим вызов register_blkdev, sbull делает это:
sbull_major = register_blkdev(sbull_major, "sbull");
if (sbull_major <= 0) {
printk(KERN_WARNING "sbull: unable to get major number\n");
return -EBUSY;
}
Также как и другие виртуальные устройства, представленные в этой книге, устройство sbull описывается внутренней структурой:
struct sbull_dev {
int size; /* Размер устройства в секторах */
u8 *data; /* Массив данных */
short users; /* Как много пользователей */
short media_change; /* Флаг был ли заменён носитель? */
spinlock_t lock; /* Для взаимного исключения */
struct request_queue *queue; /* Очередь запросов устройства */
struct gendisk *gd; /* Структура gendisk */
struct timer_list timer; /* Для имитации замены носителя */
};
Чтобы проинициализировать эту структуру и сделать связанные устройства доступными системе требуются несколько шагов. Начнём с основной инициализации и выделения основной памяти:
memset (dev, 0, sizeof (struct sbull_dev));
dev->size = nsectors*hardsect_size;
dev->data = vmalloc(dev->size);
if (dev->data == NULL) {
printk (KERN_NOTICE "vmalloc failure.\n");
return;
}
spin_lock_init(&dev->lock);
Сначала:
struct sbull_dev {
...
int size; /* Размер устройства в секторах */
...
далее он инициализирован в байтах??!!
dev->size = nsectors * hardsect_size;
что он означает на самом деле?
Ответ автора:
Да, комментарий ошибочен; где-то по ходу программы это поле стало размером в байтах. Так что среди прочего это означает, что тип 'int' может не быть лучшим выбором - хотя вряд ли он переполнится с размерами, обычно используемыми с простыми RAM-дисками.
Важно создать и проинициализировать спин-блокировку до следующего шага, который является созданием очереди запросов. Мы рассмотрим этот процесс более подробно, когда доберёмся до обработки запросов; на данный момент достаточно сказать, что необходимым вызовом является:
dev->queue = blk_init_queue(sbull_request, &dev->lock);
Здесь sbull_request является нашей функцией request - функцией, которая фактически выполняет запросы блочного чтения и записи. Когда мы создаём очередь запросов, мы должны предусмотреть спин-блокировку, которая контролирует доступ к этой очереди. Блокировка осуществляется драйвером, а не общими частями ядра, потому что зачастую очереди запросов и другие структуры данных драйвера относятся к той же критической секции; как правило, они адресуются совместно. Как и любая функция, которая выделяет память, blk_init_queue может потерпеть неудачу, так что вы должны проверять возвращаемое значение, прежде чем продолжить.
После того как мы получили память для нашего устройства и очередь запросов, мы можем создать, проинициализировать и установить соответствующую структуру gendisk. Код, который выполняет эту работу:
dev->gd = alloc_disk(SBULL_MINORS);
if (! dev->gd) {
printk (KERN_NOTICE "alloc_disk failure\n");
goto out_vfree;
}
dev->gd->major = sbull_major;
dev->gd->first_minor = which*SBULL_MINORS;
dev->gd->fops = &sbull_ops;
dev->gd->queue = dev->queue;
dev->gd->private_data = dev;
snprintf (dev->gd->disk_name, 32, "sbull%c", which + 'a');
set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
add_disk(dev->gd);
Здесь SBULL_MINORS - это число младших номеров, которые поддерживает каждое устройство sbull. Когда мы устанавливаем первый младший номер для каждого устройства, мы должны принимать во внимание все числа, полученные предыдущими устройствами. Имена дисков, таким образом, устанавливаются так, что первый называется sbulla, второй sbullb и так далее. Пользовательское пространство может затем добавить номера разделов, так что третий раздел на втором устройстве мог бы быть /dev/sbullb3.
Когда всё будет создано, мы завершаем вызовом add_disk. Весьма вероятно, что некоторые наши методы будут вызваны до возвращения из add_disk, поэтому мы позаботились, чтобы этот вызов был самым последним шагом в инициализации нашего устройства.
Как мы уже отмечали ранее, ядро обрабатывает каждый диск, как линейный массив из секторов по 512 байт. Однако, не каждое оборудование использует такой размер сектора. Сделать так, чтобы устройство с другим размером сектора работало, не особенно трудно; это просто вопрос заботы о нескольких деталях. Устройство sbull экспортирует параметр hardsect_size, который может быть использован для изменения "аппаратного" размера сектора устройства; глядя на эту реализацию, вы сможете увидеть, как добавить такую поддержку в собственные драйверы.
Первая из этих деталей - сообщить ядру размер поддерживаемого вашим устройством сектора. Размер аппаратного сектора является одним из параметров в очереди запросов, а не структуры gendisk. Этот размер устанавливается вызовом blk_queue_hardsect_size сразу после того, как создана очередь:
blk_queue_hardsect_size(dev->queue, hardsect_size);
Как только это будет сделано, ядро придерживается аппаратного размера сектора вашего устройства. Все запросы ввода/вывода выровнены должным образом начале аппаратного сектора и длина каждого запроса является целым числом секторов. Вы должны помнить, однако, что ядро всегда внутри себя работает с секторами по 512 байт; таким образом, соответственно необходимо перевести все номера секторов. Так, например, когда sbull устанавливает ёмкость устройства в своей структуре gendisk, вызов выглядит так:
set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
KERNEL_SECTOR_SIZE представляет собой локально-заданную константу, которую мы используем для масштабирования между секторами ядра по 512 байт и независимого размера, который мы выбрали для использования. Такой вид расчёта появляется часто, когда мы смотрим на логику обработки запроса в sbull.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.