一、块设备简介
1、块设备
块设备将数据存储在固定大小的块中,每个块的大小通常在512字节到32768字节之间。磁盘、SD卡都是常见的块设备。
2、块设备VS字符设备
# 块设备和字符设备最大的区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的基本单元为字节。
# 块设备能够随机访问,而字符设备则只能顺序访问。
块设备体系架构:
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(定义于<linux/genhd.h>)来描述块设备。
struct gendisk
{
int major; //主设备号
int first_minor; //次设备号
int minors;
char disk_name[DISK_NAME_LEN]; //驱动名
struct block_device_operations *fops;
struct request_queue *queue; //请求队列
...... ...... ...... ......
int node_id;
}
设备注册
Linux内核使用add_disk函数向内核注册块设备驱动
void add_disk(struct gendisk *gd)
设备操作
字符设备通过file_operations结构来定义使它所支持的操作,块设备使用一个类似的结构:struct block_device_operations.
struct block_device_operations
{
int (*open)(struct block_device *, fmode_t);
int (*release)(struct gendisk *, fmode_t);
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
...... ....... ...... ....... ......................................
};
IO请求
在Linux内核中,使用struct request来表示等待处理的块设备I/O请求。
struct request
{
struct list_head queuelist; //链表结构
sector_t sector; //要操作的首个扇区
unsigned long nr_sectors; //要操作的扇区数目
struct bio *bio; //请求的bio结构体的链表
struct bio *biotail; //请求的bio结构体的链表尾
........................................................................................................................
}
请求队列
简单的讲,请求队列就是IO请求request所形成的队列,在Linux内核中struct request_queue描述。
内核提供了一系列函数用来操作请求队列:
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) //初始化请求队列,一般在块设备驱动的模块加载函数中调用。
void blk_cleanup_queue(request_queue_t *q) //清除请求队列,这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
struct request *elv_next_request(request_queue_t *queue) //返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,因此连续调用它2次,2次会返回同一个请求结构体。
void blkdev_dequeue_request(struct request *req) //从队列中删除1个请求。
实例代码simple-blk.c是ramdisk的驱动,也就是基于内存的磁盘驱动
块设备驱动测试
1、insmod simple-blk.ko
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
数据访问流程
struct request_queue
{
.........
make_request_fn *make_request_fn;
.........
}
数据访问流程:
BIO
1个struct bio代表1次块设备I/O请求,IO调度器可将连续的bio合并成1个请求struct request.
struct bio
{
sector_t bi_sector; //要访问的第1个扇区
unsigned int bi_size; //以字节为单位所需传输的数据大小
struct bio_vec *bi_io_vec; //实际的vec列表
..........................................................................................................................................
}
struct bio_vec
{
struct page *bv_page; //页指针
unsigned int bv_len; //传输的数据长度
unsigned int bv_offset; //偏移量
}
_make_request
在_make_request函数中,使用了IO调度器(elevator)将多个bio的访问顺序进行优化,调整,合并为一个request,然后提交给用户指定的函数处理。但是对于ramdisk、U盘、记忆棒之类的设备,并不存在磁盘所面临的寻道时间(没有机械的磁头)。因此对这样的“块设备”而言,一个I/O调度器不但发挥不了作用,反而其本身将白白耗掉不少内存和CPU。
解决办法:驱动程序自己实现request_queue所需要的make_request_fn函数,不使用_make_request
请求队列
request_queue_t *blk_alloc_queue(int gfp_mask) //分配“请求队列”,对于FLASH、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配1个“请求队列”。
void blk_queue_make_request(request_queue_t *q, make_request_fn * mfn)//绑定“请求队列”和“制造请求”函数。
三、SD卡驱动测试
见课件。
可参考:http://m.blog.csdn.net/blog/tangkai177/8532152