1 什么是块设备?
块设备将数据存储在固定大小的块中,每个块的大小通常在512-32768字节之间。
如磁盘、SD卡都是常见的块设备。
2.块设备VS字符设备
块设备和字符设备大区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的基本单元为
字节。
块设备能够随机访问,而字符设备则只能顺序访问。
1. 扇区(Sectors):任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512byte。
2. 块(Blocks):由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组成。
3. 段(Segments):由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。
综合上描述:块设备驱动是基于扇区(sector)来访问底层物理磁盘,基于块(block)来访问上层文件系统。扇区一般是2的n次方大小,典型为512B,内
核也要求块是2的n次方大小,且块大小通常为扇区大小的整数倍,并且块大小要小于页面大小,典型大小为512B、1K或4K。
2. 块设备驱动层(Block Device Driver)在总体结构中扮演的角色。
块设备的应用在Linux中是一个完整的子系统。
【块设备驱动是以何种方式对块设备进行访问的?】
在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用request结构体描述。但对于一些磁盘设备而言请求的速
度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。
在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交I/O请求,
I/O调度程序将磁盘资源分配给系统中所有挂起的块I/O请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求
到设备,关于更多详细的I/O调度知识这里就不深加研究了。
【块设备驱动又是怎样维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系呢?】
通用块层(Generic Block Layer)负责。在通用块层中,通常用一个bio结构体来对应一个I/O请求,它代表了正在活动的以段(Segment)链表形式组织
的块IO操作,对于它所需要的所有段又用bio_vec结构体表示。
【块设备驱动又是怎样对底层物理磁盘进行访问的呢?】
Linux提供了一个gendisk数据结构体,用他来表示一个独立的磁盘设备或分区。在gendisk中有一个类似字符设备中file_operations的硬件操作结构
指针,他就是block_device_operations结构体,他的作用相信大家已经很清楚了。
VFS
是对各种具体文件系统的一种封装,为用户程序访问文件提供统一的接口。
disk cache
当用户发起文件访问请求的时候,首先会到disk cache中寻找是否被缓存了,如果在cache中,则直接从cache中读取。如果数据不在缓存中,就必须
要到具体的文件系统中读取数据了。
mapping layer
1. 首先确定文件系统的block size,然后计算所请求的数据包含多少个block
2. 调用具体文件系统的函数来访问文件的inode,确定所请求的数据在磁盘上的逻辑块地址。
generic block layer
Linux内核为块设备抽象了统一的模型,把块设备看做是由若干个扇区组成的额数据空间。
上层的读写请求在通用块层(generic block layer)被构造成一个或多个bio结构。
I/O scheduler Layer
I/O调度层负责将I/O操作进行排序,采用某种算法(如:电梯调度算法)来高效地处理操作。
电梯调度算法的基本原则:
如果电梯现在朝上运动,如果当前楼层的上方和下放都有请求,则先响应所有上方的请求,然后才向下响应下放的请求;如果电梯向下运动,则刚好
相反。
block device driver
块设备驱动程序通过发送命令给磁盘控制器实现真正的数据传输。
重要结构体和函数
linux内核使用该结构体来描述块设备
struct gendisk {
int major; /* major number of driver */
int first_minor;
int minors;
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode)
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
int node_id;
};
void add_disk(struct gendisk *disk)
类似于字符设备的file_operation结构体
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
…………
};
块设备如何处理IO请求?
通过request来表示等待处理的块设备IO请求
struct request {
struct list_head queuelist;
unsigned int __data_len; /* total data len */
sector_t __sector; //要操作的首个扇区
struct bio *bio; //请求的bio结构体的链表
struct bio *biotail; //请求的bio结构体的链表尾
}
主要是要知道操作的扇区,
对文件操作,能计算出对应的扇区,然后转换成对应的request,到了驱动这一层,就能清晰知道位于哪个扇区,以及要访问多少个扇区。
请求队列
请求队列就是IO请求request所形成的队列,在linux内核中用以下结构体描述
struct request_queue{
}
内核提供了一系列函数用来操作请求队列:
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
初始化请求队列,一般在块设备驱动的模块加载函数中调用
回调函数终实现对磁盘操作
blk_cleanup_queue(struct request_queue * q)
清除请求队列,这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
static inline struct request *__elv_next_request(struct request_queue *q)
返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。
该函数不会清除请求,它仍然将这个请求保留在队列上,因此连续调用多次,只会返回同一个请求结构体。
void blk_dequeue_request(struct request * rq)
从队列中删除1个请求。
【分析实例1】
操作步骤
1. insmod
2. ls /dev/simp_blkdev
3. mkfs.ext3 /dev/simp_blkdev 格式化
4. mkdir -p /mnt/blk
5. mount /dev/simp_blkdev /mnt/blk/
6. cp /etc/init.d/* /mnt/blk
7. ls /mnt/blk
8. umount /mnt/blk
9. ls /mnt/blk
【内核解析】
通用块层形成请求 终调用到请求队列的回调函数
make_request()
__make_request()
void generic_make_request(struct bio *bio)
-》q->make_request_fn(q, bio);
加载模块的初始化请求队列函数代码跟踪
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
{
return blk_init_queue_node(rfn, lock, NUMA_NO_NODE);
}
struct request_queue *
blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id)
{
struct request_queue *uninit_q, *q;
uninit_q = blk_alloc_queue_node(GFP_KERNEL, node_id);
if (!uninit_q)
return NULL;
q = blk_init_allocated_queue(uninit_q, rfn, lock);
if (!q)
blk_cleanup_queue(uninit_q);
return q;
}
struct request_queue *
blk_init_allocated_queue(struct request_queue *q, request_fn_proc *rfn,
spinlock_t *lock)
{
q->request_fn = rfn;
blk_queue_make_request(q, blk_queue_bio);
}
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
{
q->make_request_fn = mfn;
}
实际是调用函数blk_queue_bio(),制造请求的函数主要是操作bio
【先解释下这个函数功能】
对每一个扇区数据的访问就是一个bio
IO调度器可以将若干个连续的bio合并到一起形成request
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
struct bvec_iter bi_iter;
unsigned int bi_phys_segments;
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* 实际的 vec列表 */
}
bio_vec 反应的是用户的操作信息
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
blk_queue_bio函数中,使用了IO调度器(elevator)将多个bio的访问顺序进行优化,调整,合并为一个request,然后提交给用户指定的函数处理。
但是对于ramdisk、U盘、记忆棒之类的设备,并不存在磁盘所面临的寻道时间。因此对这样的“块设备”而言,一个I/O调度器不但发挥不了作用,反
而其本身将白白耗费掉不少内存和CPU。
解决办法:驱动程序自己实现request_queue所需的make_request_fn函数,不使用blk_queue_bio
如何自己实现?
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
分配请求队列,对于flash、ram盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配1个“请求队列”
;
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
绑定请求队列和制造请求函数
分析不使用调度器的 代码
第1个例子内核来了直接交给我们处理,绕过调度算法【电梯调度算法】【block】
第2个例子是先调用内核的__make_request函数 ,然后再调用我们注册的函数
块设备驱动的I/O请求处理的两种方式:
块设备驱动的I/O请求处理有两种方式,分别是使用请求队列和不使用请求队列。那么这两种方式有什么不同呢?在第2点中已讲到使用请求队列有助
于提高系统的性能,但对于一些完全可随机访问的块设备(如:Ram盘等)使用请求队列并不能获得多大的益处,这时候,通用块层提供了一种无队列的
操作模式,使用这种模式,驱动必须提供一个制造请求函数。我们还是用代码来区别它们吧。
使用请求队列:【同步到代码中】
static int __int ramdisk_init(void)
{
/*块设备驱动注册*/
register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
/*使用请求队列的方式*/
ramdisk_queue = blk_init_queue(ramdisk_do_request, NULL);
/*分配gendisk*/
.........
/*初始化gendisk*/
.........
/*添加gendisk到系统中*/
.........
}
/*请求处理函数,请求队列的处理流程如下:
*首先:从请求队列中拿出一条请求
*其次:判断这一条请求的方向,是向设备写还是读,然后将数据装入缓冲区
*后:通知请求完成*/
static void ramdisk_do_request(struct request_queue_t *queue)
{
struct request *req;
/*使用循环一条请求一条请求的来处理,elv_next_request函数是遍历队列中的每一条请求*/
while(req = elv_next_request(queue) != NULL)
{
/*判断要传输数据的总长度大小是否超过范围*/
if ((req->sector + req->current_nr_sectors) << 9 > RAMDISK_SIZE)
{
/*如果超过范围就直接报告请求失败*/
end_request(req, 0);
continue;
}
/*判断请求处理的方向*/
switch (rq_data_dir(req))
{
case READ:
memcpy(req->buffer, disk_data + (req->sector << 9), req->current_nr_sectors << 9);
end_request(req, 1);/*报告请求处理成功*/
break;
case WRITE:
memcpy(disk_data + (req->sector << 9), req->buffer, req->current_nr_sectors << 9);
end_request(req, 1);/*报告请求处理成功*/
break;
default:
break;
}
}
}
不使用请求队列,制造请求函数:
static int __int ramdisk_init(void)
{
/*块设备驱动注册*/
ramdisk_major = register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
/*使用制造请求的方式,先分配ramdisk_queue*/
ramdisk_queue = blk_alloc_queue(GFP_KERNEL);
/*再绑定请求制造函数*/
blk_queue_make_request(ramdisk_queue, &ramdisk_make_request);
/*分配gendisk*/
.........
/*初始化gendisk*/
.........
/*添加gendisk到系统中*/
.........
}
/*绑定请求制造函数。注意:第一个参数仍然是请求队列,但在这里实际不包含任何请求。
所以这里要处理的重点对象的bio中的每个bio_vec,他表示一个或多个要传送的缓冲区。*/
static int ramdisk_make_request(struct request_queue_t *queue, struct bio *bio)
{
int i;
struct bio_vec *bvec;
void *disk_mem;
void *bvec_mem;
/*在遍历段之前先判断要传输数据的总长度大小是否超过范围*/
if((bio->bi_sector << 9) + bio->bi_size > RAMDISK_SIZE)
{
/*如果超出范围就通知这个bio处理失败*/
bio_endio(bio, 0, -EIO);
return 0;
}
/*获得这个bio请求在块设备内存中的起始位置*/
disk_mem = disk_data + (bio->bi_sector << 9);
/*开始遍历这个bio中的每个bio_vec*/
bio_for_each_segment(bvec, bio, i)
{
/*因bio_vec中的内存地址是使用page *描述的,故在高端内存中需要用kmap进行映射后才能访问,
再加上在bio_vec中的偏移位置,才是在高端物理内存中的实际位置*/
bvec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
/*判断bio请求处理的方向*/
switch(bio_data_dir(bio))
{
case READ:
case READA:
memcpy(bvec_mem, disk_mem, bvec-> bv_len);
break;
case WRITE :
memcpy(disk_mem, bvec_mem, bvec-> bv_len);
break;
default :
kunmap(bvec->bv_page);
}
/*处理完每一个bio_vec都应把kmap映射的地址取消掉*/
kunmap(bvec->bv_page);
/*累加当前bio_vec中的内存长度,以确定下一个bio_vec在块设备内存中的位置*/
disk_mem += bvec->bv_len;
}
/*bio中所有的bio_vec处理完后报告处理结束*/
bio_endio(bio, bio->bi_size, 0);
return 0;
}
四、块设备驱动(RamDisk)实现步骤详解
其实从上面的结构体关系图就可以看出这就是块设备驱动程序的整体的结构了,当然这只是较简单的块设备驱动了,现在我们就即将要做的就是
实现一个简单的RamDisk块设备驱动了。其实告诉大家我这里为什么只实现一个简单的块设备驱动,因为我是要为以后的MMC/SD卡驱动、Nand flash驱
动等做一些前提准备的。好了,还是先了解一下什么是RamDisk吧。
RamDisk是将Ram中的一部分内存空间模拟成一个磁盘设备,以块设备的访问方式来访问这一片内存,达到数据存取的目的。RamDisk设备在Linux
设备文件系统中对应的设备文件节点一般为:/dev/ram%d。
1. 建立驱动代码文件my2440_ramdisk.c,实现驱动模块的加载和卸载,步骤如下:
加载部分:分配请求队列及绑定请求制造函数 -> 分配及初始化gendisk -> 添加gendisk -> 注册块设备驱动。
卸载部分:清除请求队列 -> 删除gendisk -> 注销块设备驱动。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/blkdev.h>
#include <linux/bio.h>
#define RAMDISK_MAJOR 0 /*主设备号设置0让内核动态产生一个主设备号*/
#define RAMDISK_NAME "my2440_ramdisk" /*设备名称*/
#define RAMDISK_SIZE (4 * 1024 * 1024) /*虚拟磁盘的大小,共4M*/
static int ramdisk_major = RAMDISK_MAJOR; /*用来保存动态分配的主设备号*/
static struct class *ramdisk_class; /*定义一个设备类,好在/dev下动态生成设备节点*/
static struct gendisk *my2440_ramdiak; /*定义一个gendisk结构体用来表示一个磁盘设备*/
static struct request_queue *ramdisk_queue; /*定义磁盘设备的请求队列*/
unsigned char *disk_data;/*定义一个指针来表示ramdisk块设备在内存中的域*/
/*块设备驱动操作结构体,其实不需要做什么操作,这里就设置为空*/
static struct block_device_operations ramdisk_fops =
{
.owner = THIS_MODULE,
};
static int __init ramdisk_init(void)
{
int ret;
/*块设备驱动注册, 注意这个块设备驱动的注册在2.6内核中是可选的,
该函数由内核提供。这里使用是为了获得一个动态生成的主设备号*/
ramdisk_major = register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
if(ramdisk_major <= 0)
{
return ramdisk_major;
}
/*动态创建一个设备节点,跟字符型设备一样*/
ramdisk_class = class_create(THIS_MODULE, RAMDISK_NAME);
if(IS_ERR(ramdisk_class))
{
ret = -1;
goto err_class;
}
device_create(ramdisk_class, NULL, MKDEV(ramdisk_major, 0), NULL, RAMDISK_NAME);
/*RamDisk属真正随机访问的设备,因此不使用请求队列的处理方式,而使用制造请求的方式*/
ramdisk_queue = blk_alloc_queue(GFP_KERNEL);/*分配ramdisk_queue*/
if(!ramdisk_queue)
{
ret = -ENOMEM;
goto err_queue;
}
blk_queue_make_request(ramdisk_queue, &ramdisk_make_request);/*绑定请求制造函数*/
/*分配gendisk,该函数由内核提供,参数为磁盘设备的次设备号数量(或者磁盘的分区数量)
注意一个分区就代表一个次设备,这里指定数量后以后就不能被修改了*/
my2440_ramdiak = alloc_disk(1);
if(!my2440_ramdiak)
{
ret = -ENOMEM;
goto err_alloc;
}
/*初始化gendisk*/
my2440_ramdiak->major = ramdisk_major; /*这里指定的主设备号就是在上面动态获取的主设备号*/
my2440_ramdiak->first_minor = 0; /*指定第一个次设备号为0*/
my2440_ramdiak->fops = &ramdisk_fops; /*指定块设备驱动对底层硬件操作的结构体指针,定义在后面来讲*/
my2440_ramdiak->queue = ramdisk_queue; /*指定初始化好的请求队列*/
sprintf(my2440_ramdiak->disk_name, RAMDISK_NAME);/*指定磁盘设备的名称*/
/*设置磁盘设备的容量大小,该函数由内核提供。
注意该函数是以512字节为1个扇区单位进行处理的,因为内核要求如此*/
set_capacity(my2440_ramdiak, RAMDISK_SIZE >> 9);/*右移9位就是除以512*/
/*添加gendisk到系统中, 该函数由内核提供*/
add_disk(my2440_ramdiak);
return 0;
/*错误处理*/
err_class:
unregister_blkdev(ramdisk_major, RAMDISK_NAME);
err_queue:
device_destroy(ramdisk_class, MKDEV(ramdisk_major, 0));
class_destroy(ramdisk_class);
err_alloc:
blk_cleanup_queue(ramdisk_queue);
return ret;
}
static void __exit ramdisk_exit(void)
{
/*删除磁盘设备*/
del_gendisk(my2440_ramdiak);
put_disk(my2440_ramdiak);
/*清除请求队列*/
blk_cleanup_queue(ramdisk_queue);
/*清除设备类*/
device_destroy(ramdisk_class, MKDEV(ramdisk_major, 0));
class_destroy(ramdisk_class);
/*注销块设备*/
unregister_blkdev(ramdisk_major, RAMDISK_NAME);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Huang Gang");
MODULE_DESCRIPTION("My2440 RamDisk Driver");
RamDisk属真正随机访问的设备,因此没有使用请求队列的处理方式,而是使用制造请求的方式。制造请求处理函数实现如下:
/*绑定请求制造函数。注意:第一个参数仍然是请求队列,但在这里实际不包含任何请求。
所以这里要处理的重点对象的bio中的每个bio_vec,他表示一个或多个要传送的缓冲区。*/
static int ramdisk_make_request(struct request_queue_t *queue, struct bio *bio)
{
int i;
struct bio_vec *bvec;
void *disk_mem;
void *bvec_mem;
/*在遍历段之前先判断要传输数据的总长度大小是否超过范围*/
if((bio->bi_sector << 9) + bio->bi_size > RAMDISK_SIZE)
{
/*如果超出范围就通知这个bio处理失败*/
bio_io_error(bio);
return 0;
}
/*获得这个bio请求在块设备内存中的起始位置*/
disk_mem = disk_data + (bio->bi_sector << 9);
/*开始遍历这个bio中的每个bio_vec*/
bio_for_each_segment(bvec, bio, i)
{
/*因bio_vec中的内存地址是使用page *描述的,故在高端内存中需要用kmap进行映射后才能访问,
再加上在bio_vec中的偏移位置,才是在高端物理内存中的实际位置*/
bvec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
/*判断bio请求处理的方向*/
switch(bio_data_dir(bio))
{
case READ:
case READA:
memcpy(bvec_mem, disk_mem, bvec->bv_len);
break;
case WRITE :
memcpy(disk_mem, bvec_mem, bvec->bv_len);
break;
default :
kunmap(bvec->bv_page);
}
/*处理完每一个bio_vec都应把kmap映射的地址取消掉*/
kunmap(bvec->bv_page);
/*累加当前bio_vec中的内存长度,以确定下一个bio_vec在块设备内存中的位置*/
disk_mem += bvec->bv_len;
}
/*bio中所有的bio_vec处理完后报告处理结束*/
bio_endio(bio, 0);
return 0;
}