|
|
|||||||||||||||||||||||||||||
|
"Драйверы устройств в 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. Подведем итогИтак, благодаря тому, что мы рассмотрели методику создания жесткого диска и поэкспериментировали с его разделами, форматированием и другими низкоуровневыми операциями, выполняемыми на жестком диске, мы, на самом деле, изучили интересные драйверы блочных устройств. Спасибо за внимание. Теперь можно задавать вопросы - пожалуйста, не стесняйтесь, задавайте ваши вопросы и комментируйте. Ссылки по теме
|
|
|||||||