"Драйверы устройств в Linux. Часть 15: Диск в оперативной памяти - экспериментируем с драйверами блочных устройств".Источник: rus-linux
В этой статье, которая является частью серии статей о драйверах устройств в Linux, будут проведены эксперименты с фиктивным жестким диском, расположенным в оперативной памяти, с помощью которого будет продемонстрирована работа драйверов блочных устройств. После вкусного обеда изучение теории тянет слушателей в сон. Поэтому давайте сразу начнем с кода. Исходный код диска в оперативной памятиДавайте создадим директорий DiskOnRAM, в котором будут находиться следующие шесть файлов: три с исходными кодами на языке С, два с заголовочными файлами для С и один файл Makefile. partition.h #ifndef PARTITION_H #define PARTITION_H #include <linux/types.h> extern void copy_mbr_n_br(u8 *disk); #endif partition.c #include <linux/string.h> #include "partition.h" #define ARRAY_SIZE(a) (sizeof(a) / sizeof(*a)) #define SECTOR_SIZE 512 #define MBR_SIZE SECTOR_SIZE #define MBR_DISK_SIGNATURE_OFFSET 440 #define MBR_DISK_SIGNATURE_SIZE 4 #define PARTITION_TABLE_OFFSET 446 #define PARTITION_ENTRY_SIZE 16 // sizeof(PartEntry) #define PARTITION_TABLE_SIZE 64 // sizeof(PartTable) #define MBR_SIGNATURE_OFFSET 510 #define MBR_SIGNATURE_SIZE 2 #define MBR_SIGNATURE 0xAA55 #define BR_SIZE SECTOR_SIZE #define BR_SIGNATURE_OFFSET 510 #define BR_SIGNATURE_SIZE 2 #define BR_SIGNATURE 0xAA55 typedef struct { unsigned char boot_type; // 0x00 - Inactive; 0x80 - Active (Bootable) unsigned char start_head; unsigned char start_sec:6; unsigned char start_cyl_hi:2; unsigned char start_cyl; unsigned char part_type; unsigned char end_head; unsigned char end_sec:6; unsigned char end_cyl_hi:2; unsigned char end_cyl; unsigned long abs_start_sec; unsigned long sec_in_part; } PartEntry; typedef PartEntry PartTable[4]; static PartTable def_part_table = { { boot_type: 0x00, start_head: 0x00, start_sec: 0x2, start_cyl: 0x00, part_type: 0x83, end_head: 0x00, end_sec: 0x20, end_cyl: 0x09, abs_start_sec: 0x00000001, sec_in_part: 0x0000013F }, { boot_type: 0x00, start_head: 0x00, start_sec: 0x1, start_cyl: 0x0A, // extended partition start cylinder (BR location) part_type: 0x05, end_head: 0x00, end_sec: 0x20, end_cyl: 0x13, abs_start_sec: 0x00000140, sec_in_part: 0x00000140 }, { boot_type: 0x00, start_head: 0x00, start_sec: 0x1, start_cyl: 0x14, part_type: 0x83, end_head: 0x00, end_sec: 0x20, end_cyl: 0x1F, abs_start_sec: 0x00000280, sec_in_part: 0x00000180 }, { } }; static unsigned int def_log_part_br_cyl[] = {0x0A, 0x0E, 0x12}; static const PartTable def_log_part_table[] = { { { boot_type: 0x00, start_head: 0x00, start_sec: 0x2, start_cyl: 0x0A, part_type: 0x83, end_head: 0x00, end_sec: 0x20, end_cyl: 0x0D, abs_start_sec: 0x00000001, sec_in_part: 0x0000007F }, { boot_type: 0x00, start_head: 0x00, start_sec: 0x1, start_cyl: 0x0E, part_type: 0x05, end_head: 0x00, end_sec: 0x20, end_cyl: 0x11, abs_start_sec: 0x00000080, sec_in_part: 0x00000080 }, }, { { boot_type: 0x00, start_head: 0x00, start_sec: 0x2, start_cyl: 0x0E, part_type: 0x83, end_head: 0x00, end_sec: 0x20, end_cyl: 0x11, abs_start_sec: 0x00000001, sec_in_part: 0x0000007F }, { boot_type: 0x00, start_head: 0x00, start_sec: 0x1, start_cyl: 0x12, part_type: 0x05, end_head: 0x00, end_sec: 0x20, end_cyl: 0x13, abs_start_sec: 0x00000100, sec_in_part: 0x00000040 }, }, { { boot_type: 0x00, start_head: 0x00, start_sec: 0x2, start_cyl: 0x12, part_type: 0x83, end_head: 0x00, end_sec: 0x20, end_cyl: 0x13, abs_start_sec: 0x00000001, sec_in_part: 0x0000003F }, } }; static void copy_mbr(u8 *disk) { memset(disk, 0x0, MBR_SIZE); *(unsigned long *)(disk + MBR_DISK_SIGNATURE_OFFSET) = 0x36E5756D; memcpy(disk + PARTITION_TABLE_OFFSET, &def_part_table, PARTITION_TABLE_SIZE); *(unsigned short *)(disk + MBR_SIGNATURE_OFFSET) = MBR_SIGNATURE; } static void copy_br(u8 *disk, int start_cylinder, const PartTable *part_table) { disk += (start_cylinder * 32 /* sectors / cyl */ * SECTOR_SIZE); memset(disk, 0x0, BR_SIZE); memcpy(disk + PARTITION_TABLE_OFFSET, part_table, PARTITION_TABLE_SIZE); *(unsigned short *)(disk + BR_SIGNATURE_OFFSET) = BR_SIGNATURE; } void copy_mbr_n_br(u8 *disk) { int i; copy_mbr(disk); for (i = 0; i < ARRAY_SIZE(def_log_part_table); i++) { copy_br(disk, def_log_part_br_cyl[i], &def_log_part_table[i]); } } ram_device.h #ifndef RAMDEVICE_H #define RAMDEVICE_H #define RB_SECTOR_SIZE 512 extern int ramdevice_init(void); extern void ramdevice_cleanup(void); extern void ramdevice_write(sector_t sector_off, u8 *buffer, unsigned int sectors); extern void ramdevice_read(sector_t sector_off, u8 *buffer, unsigned int sectors); #endif ram_device.c #include <linux/types.h> #include <linux/vmalloc.h> #include <linux/string.h> #include "ram_device.h" #include "partition.h" #define RB_DEVICE_SIZE 1024 /* sectors */ /* So, total device size = 1024 * 512 bytes = 512 KiB */ /* Array where the disk stores its data */ static u8 *dev_data; int ramdevice_init(void) { dev_data = vmalloc(RB_DEVICE_SIZE * RB_SECTOR_SIZE); if (dev_data == NULL) return -ENOMEM; /* Setup its partition table */ copy_mbr_n_br(dev_data); return RB_DEVICE_SIZE; } void ramdevice_cleanup(void) { vfree(dev_data); } void ramdevice_write(sector_t sector_off, u8 *buffer, unsigned int sectors) { memcpy(dev_data + sector_off * RB_SECTOR_SIZE, buffer, sectors * RB_SECTOR_SIZE); } void ramdevice_read(sector_t sector_off, u8 *buffer, unsigned int sectors) { memcpy(buffer, dev_data + sector_off * RB_SECTOR_SIZE, sectors * RB_SECTOR_SIZE); } ram_block.c /* Disk on RAM Driver */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/types.h> #include <linux/genhd.h> #include <linux/blkdev.h> #include <linux/errno.h> #include "ram_device.h" #define RB_FIRST_MINOR 0 #define RB_MINOR_CNT 16 static u_int rb_major = 0; /* * The internal structure representation of our Device */ static struct rb_device { /* Size is the size of the device (in sectors) */ unsigned int size; /* For exclusive access to our request queue */ spinlock_t lock; /* Our request queue */ struct request_queue *rb_queue; /* This is kernel's representation of an individual disk device */ struct gendisk *rb_disk; } rb_dev; static int rb_open(struct block_device *bdev, fmode_t mode) { unsigned unit = iminor(bdev->bd_inode); printk(KERN_INFO "rb: Device is opened\n"); printk(KERN_INFO "rb: Inode number is %d\n", unit); if (unit > RB_MINOR_CNT) return -ENODEV; return 0; } static int rb_close(struct gendisk *disk, fmode_t mode) { printk(KERN_INFO "rb: Device is closed\n"); return 0; } /* * Actual Data transfer */ static int rb_transfer(struct request *req) { //struct rb_device *dev = (struct rb_device *)(req->rq_disk->private_data); int dir = rq_data_dir(req); sector_t start_sector = blk_rq_pos(req); unsigned int sector_cnt = blk_rq_sectors(req); struct bio_vec *bv; struct req_iterator iter; sector_t sector_offset; unsigned int sectors; u8 *buffer; int ret = 0; //printk(KERN_DEBUG "rb: Dir:%d; Sec:%lld; Cnt:%d\n", dir, start_sector, sector_cnt); sector_offset = 0; rq_for_each_segment(bv, req, iter) { buffer = page_address(bv->bv_page) + bv->bv_offset; if (bv->bv_len % RB_SECTOR_SIZE != 0) { printk(KERN_ERR "rb: Should never happen: " "bio size (%d) is not a multiple of RB_SECTOR_SIZE (%d).\n" "This may lead to data truncation.\n", bv->bv_len, RB_SECTOR_SIZE); ret = -EIO; } sectors = bv->bv_len / RB_SECTOR_SIZE; printk(KERN_DEBUG "rb: Sector Offset: %lld; Buffer: %p; Length: %d sectors\n", sector_offset, buffer, sectors); if (dir == WRITE) /* Write to the device */ { ramdevice_write(start_sector + sector_offset, buffer, sectors); } else /* Read from the device */ { ramdevice_read(start_sector + sector_offset, buffer, sectors); } sector_offset += sectors; } if (sector_offset != sector_cnt) { printk(KERN_ERR "rb: bio info doesn't match with the request info"); ret = -EIO; } return ret; } /* * Represents a block I/O request for us to execute */ static void rb_request(struct request_queue *q) { struct request *req; int ret; /* Gets the current request from the dispatch queue */ while ((req = blk_fetch_request(q)) != NULL) { #if 0 /* * This function tells us whether we are looking at a filesystem request * - one that moves block of data */ if (!blk_fs_request(req)) { printk(KERN_NOTICE "rb: Skip non-fs request\n"); /* We pass 0 to indicate that we successfully completed the request */ __blk_end_request_all(req, 0); //__blk_end_request(req, 0, blk_rq_bytes(req)); continue; } #endif ret = rb_transfer(req); __blk_end_request_all(req, ret); //__blk_end_request(req, ret, blk_rq_bytes(req)); } } /* * These are the file operations that performed on the ram block device */ static struct block_device_operations rb_fops = { .owner = THIS_MODULE, .open = rb_open, .release = rb_close, }; /* * This is the registration and initialization section of the ram block device * driver */ static int __init rb_init(void) { int ret; /* Set up our RAM Device */ if ((ret = ramdevice_init()) < 0) { return ret; } rb_dev.size = ret; /* Get Registered */ rb_major = register_blkdev(rb_major, "rb"); if (rb_major <= 0) { printk(KERN_ERR "rb: Unable to get Major Number\n"); ramdevice_cleanup(); return -EBUSY; } /* Get a request queue (here queue is created) */ spin_lock_init(&rb_dev.lock); rb_dev.rb_queue = blk_init_queue(rb_request, &rb_dev.lock); if (rb_dev.rb_queue == NULL) { printk(KERN_ERR "rb: blk_init_queue failure\n"); unregister_blkdev(rb_major, "rb"); ramdevice_cleanup(); return -ENOMEM; } /* * Add the gendisk structure * By using this memory allocation is involved, * the minor number we need to pass bcz the device * will support this much partitions */ rb_dev.rb_disk = alloc_disk(RB_MINOR_CNT); if (!rb_dev.rb_disk) { printk(KERN_ERR "rb: alloc_disk failure\n"); blk_cleanup_queue(rb_dev.rb_queue); unregister_blkdev(rb_major, "rb"); ramdevice_cleanup(); return -ENOMEM; } /* Setting the major number */ rb_dev.rb_disk->major = rb_major; /* Setting the first mior number */ rb_dev.rb_disk->first_minor = RB_FIRST_MINOR; /* Initializing the device operations */ rb_dev.rb_disk->fops = &rb_fops; /* Driver-specific own internal data */ rb_dev.rb_disk->private_data = &rb_dev; rb_dev.rb_disk->queue = rb_dev.rb_queue; /* * You do not want partition information to show up in * cat /proc/partitions set this flags */ //rb_dev.rb_disk->flags = GENHD_FL_SUPPRESS_PARTITION_INFO; sprintf(rb_dev.rb_disk->disk_name, "rb"); /* Setting the capacity of the device in its gendisk structure */ set_capacity(rb_dev.rb_disk, rb_dev.size); /* Adding the disk to the system */ add_disk(rb_dev.rb_disk); /* Now the disk is "live" */ printk(KERN_INFO "rb: Ram Block driver initialised (%d sectors; %d bytes)\n", rb_dev.size, rb_dev.size * RB_SECTOR_SIZE); return 0; } /* * This is the unregistration and uninitialization section of the ram block * device driver */ static void __exit rb_cleanup(void) { del_gendisk(rb_dev.rb_disk); put_disk(rb_dev.rb_disk); blk_cleanup_queue(rb_dev.rb_queue); unregister_blkdev(rb_major, "rb"); ramdevice_cleanup(); } module_init(rb_init); module_exit(rb_cleanup); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Anil Kumar Pugalia <email@sarika-pugs.com>"); MODULE_DESCRIPTION("Ram Block Driver"); MODULE_ALIAS_BLOCKDEV_MAJOR(rb_major); Вы также можете загрузить Как и обычно, с помощью команды make соберем драйвер "диска в оперативной памяти"(dor.ko), объединив вместе три файла на С. Чтобы увидеть, как это делается, смотрите файл Makefile. Makefile # If called directly from the command line, invoke the kernel build system. ifeq ($(KERNELRELEASE),) KERNEL_SOURCE := /usr/src/linux PWD := $(shell pwd) default: module module: $(MAKE) -C $(KERNEL_SOURCE) SUBDIRS=$(PWD) modules clean: $(MAKE) -C $(KERNEL_SOURCE) SUBDIRS=$(PWD) clean # Otherwise KERNELRELEASE is defined; we've been invoked from the # kernel build system and can use its language. else obj-m := dor.o dor-y := ram_block.o ram_device.o partition.o endif Чтобы привести в исходное состояние файлы, используемые при сборке, выполните, как и обычно, команду make clean. Как только сборка будет завершена, выполните следующие три эксперимента (смотрите рис.1 - 3). Рис.1: Экспериментируем с драйвером "диска в оперативной памяти" Рис.2: xxd показывает первоначальные данные, находящиеся в первом разделе (/dev/rb1) Рис.3: Форматирование третьего раздела (/dev/rb3) Пожалуйста, обратите внимание, что все эти действия нужно выполнять с привилегиями пользователя root:
Теперь давайте изучим правилаМы всего лишь попробовали воспользоваться диском, созданным в оперативной памяти (disk on RAM - DOR), но при этом фактически не знаем правил, как это все происходит. Так что давайте попытаемся разобраться во всех подробностях этого процесса. В каждом из трех файлов .c представлена определенная часть драйвера; в ram_device.c и ram_device.h абстрагированы основные операции с памятью, такие vmalloc/vfree, memcpy и т. п., с помощью которых реализованы API таких операции, как инициализация/очистка, чтение/запись и т.д. В partition.c и partition.h реализованы функции, эмулирующие в DOR работу с таблицами различных разделов. С помощью этого кода предоставляется информация о разделах, например, номер раздела, его тип, размер и т. п., которая отображается с помощью функцииfdisk. Файл ram_block.c является основой реализации блочного драйвера, позволяющей отображать DOR в пользовательском пространстве в виде файлов блочного устройства (/dev/rb*). Другими словами, с помощью четырех из пяти файлов ram_device.* и partition.* формируется горизонтальный слой драйвер устройства, а с помощью файла ram_block.c формируется вертикальный (блочный) слой драйвера устройства. Итак, давайте разберемся в деталях. Основы драйверов блочных устройствКонцептуально, блочные драйверы очень похожи на драйверы символьных устройств, в частности в отношении следующего:
Итак, если вы уже знаете, как реализован символьный драйвер, вам будет проще понять реализацию блочных драйверов. Тем не менее, они, безусловно, не идентичны. Основные различия заключаются в следующем:
И все это является причиной различий в реализации. Давайте проанализируем ключевые фрагменты кода из файла ram_block.c, и начнем с конструктора драйвера rb_init(). Первым шагом будет регистрация 8-битного (блочного) старшего номера (что неявно означает регистрацию всех 256 8-битных младших номеров, связанных с ним). Функция для этого выглядит следующим образом: int register_blkdev(unsigned int major, const char *name); Здесь major является старшим номером, который должен регистроваться, а name является регистрационной меткой, отображаемой в директории/proc/devices. Интересно, что если в качестве первого параметра major передается 0, то функция register_blkdev() пытается выделить и зарегистрировать произвольный свободный старший номер; в случае успеха происходит возврат выделенного старшего номера. Соответствующая функция отмены регистрации выглядит следующим образом: void unregister_blkdev(unsigned int major, const char *name); Прототипы обеих этих функций находятся в <linux/fs.h>. На втором шаге в структуру block_device_operations (прототип в <linux/blkdev.h>) заносятся операции для работы с файлами устройств с зарегистрированными старшими номерами. Но эти операции мало похожи на операции с файлами символьных устройств; совпадения, как правило, незначительны. Если вдаваться в детали, то нет, что удивительно, даже таких операций, как чтение и запись. Но, поскольку, как мы уже знаем, блочный драйвер должен быть интегрирован с планировщиками ввода/вывода, реализация чтения и записи осуществляется с помощью так называемых очередей запросов. Таким образом, кроме операций для работы файлов устройств, также потребуется предоставить следующее:
Кроме того, нет отдельного интерфейса для создания файлов блочных устройств, так что также следует предоставить:
Наконец, нужно также предоставить два специальных значения, необходимые для характеризации блочных устройств, а именно:
Все эти операции регистрируются в структуре struct gendisk с помощью следующей функции: void add_disk(struct gendisk *disk); Соответствующая функция удаления delete выглядит следующим образом: void del_gendisk(struct gendisk *disk); Прежде, чем использовать функцию добавления диска add_disk(), нужно либо непосредственно, либо с помощью различных макросов/функций, таких какset_capacity(), инициальзировать различные поля структуры struct gendisk. Как минимум, нужно непосредственно инициализировать следующие поля - major,first_minor, fops, queue, disk_name. И даже перед тем, как эти поля будут инициализированы, нужно будет с помощью следующей функции выделить память под структуру struct gendisk: struct gendisk *alloc_disk(int minors); Здесь minors указывает общее количество разделов, поддерживаемых для этого диска. И соответствующая обратная функция будет выглядеть так: void put_disk(struct gendisk *disk); Прототипы всех этих функций имеются в <linux/genhd.h>. Очередь запросов и функция запросовПеред тем, как использовать функцию add_disk(), нужно также инициализировать очередь запросов и и занести ее в структуру struct gendisk. Инициализация очереди запросов осуществляется с помощью следующей функции: struct request_queue *blk_init_queue(request_fn_proc *, spinlock_t *); В качестве параметров мы указываем функцию обработки запросов и инициализируем механизм зашиты от одновременного доступа. Ниже приведена соответствующая функция работы с очередью: void blk_cleanup_queue(struct request_queue *); Функция запроса (обработки) должна быть определена с помощью следующего прототипа: void request_fn(struct request_queue *q); Она должна кодироваться так, чтобы для запроса использовался параметр q, например, следующим образом: struct request *blk_fetch_request(struct request_queue *q); Затем функция должна либо обработать запрос, либо инициализировать обработку. В любом случае блокировок возникать не должно, поскольку функция запроса вызывается не из контекста процесса обработки. Более того, внутри функции запроса должны использоваться только те функции, из-за которых не возникает блокировок в очереди запросов. Ниже приведен типичный пример обработки запроса, демонстрируемый на примере функции rb_request() из файла ram_block.c: while ((req = blk_fetch_request(q)) != NULL) /* Fetching a request */ { /* Processing the request: the actual data transfer */ ret = rb_transfer(req); /* Our custom function */ /* Informing that the request has been processed with return of ret */ __blk_end_request_all(req, ret); } Запросы и их обработкаНашей основной функцией будет функция rb_transfer(), в которой происходит анализ структуры struct request и, в соответствии с ним, выполняется фактическая передача данных. В этой структуре указывается, прежде всего, направление передачи данных, начальный сектор передаваемых данных, общее число секторов передаваемых данных и буфер, используемый для обмена данными. Для доступа к ним в структуре struct request предоставляются различные макросы: rq_data_dir(req); /* Operation type: 0 - read from device; otherwise - write to device */ blk_req_pos(req); /* Starting sector to process */ blk_req_sectors(req); /* Total sectors to process */ rq_for_each_segment(bv, req, iter) /* Iterator to extract individual buffers */ Макрос rq_for_each_segment() является специализированным, в котором с помощью команды iter происходит обращение к струтуре struct request (req) и при каждой итерации выполняется извлечение конкретных данных из буфера в структуру struct bio_vec (bv: basic input/output vector). А затем, когда на каждой итерации передача данных будет завершена, для выполнения соответствующей передачи данных будет использован, в зависимости от типа операции, один из следующих интерфейсов API из файла ram_device.c: void ramdevice_write(sector_t sector_off, u8 *buffer, unsigned int sectors); void ramdevice_read(sector_t sector_off, u8 *buffer, unsigned int sectors); Код функции rb_transfer() смотрите полностью в файле ram_block.c. Подведем итогИтак, благодаря тому, что мы рассмотрели методику создания жесткого диска и поэкспериментировали с его разделами, форматированием и другими низкоуровневыми операциями, выполняемыми на жестком диске, мы, на самом деле, изучили интересные драйверы блочных устройств. Спасибо за внимание. Теперь можно задавать вопросы - пожалуйста, не стесняйтесь, задавайте ваши вопросы и комментируйте. |