块设备驱动
引入
若块设备驱动程序也按以下字符设备驱动程序的简单思想来写:
APP : open, read, write. ----> 对应提供驱动程序的读写等函数
················································································
块设备驱动:drv_open, drv_read, drv_write
················································································
硬件 : 块设备。
1.硬盘
磁盘的读写其实非常快,慢在机械结构读写装置的定位上面,从一个“磁头”的某“柱面”某“扇区”读到数据后(步骤 R0),跳到另一个“磁头”的某“柱面”的某“扇区”去写(步骤 W),接着再跳回原“磁头”相同柱面的下一个“扇区”去读(步骤R1)。慢就慢在读写扇区的跳转过程中。若按“字符设备”中的“opne” ,“read”, "write"方式,则总体效率在硬盘的读写上会非常低。
上面过程是“R0”->“W”->“R1”,这个步骤跳转 2 次。
若优化这个步骤为:R0->R1->W。这个步骤跳转 1 次。这样效率会高些。
总结:先不执行而是放入队列,优化后再执行(对硬盘有这种要求)。用“字符设备驱动”程序那样读写时就会在硬盘上跳来跳去,整体效率会非常低。所以有必要引入“优化过程”。就是读写先不执行,先放到某个“队列”中去。(调整顺序)
2.flash
flash“块”里有一个一个的扇区。若按字符设备方法读写,要先写“扇区 0”和“扇区 1”。FLASH 要先擦除再写,擦除是整块整块的进行的。
写扇区 0 的过程:
①,要写时,先把这整块读到一个 buf 中。
②,然后修改 buf 中扇区 0 的数据。
③,这时再擦除整块。
④,再把修改过扇区 0 的数据的 buf 烧写到整块。
写扇区 1 的过程:
①,要写时,先把这整块读到一个 buf 中。
②,然后修改 buf 中扇区 1 的数据。
③,这时再擦除整块。
④,再把修改过扇区 1 的数据的 buf 烧写到整块
则那么要修改多个扇区时,会擦除烧写多次。总体效率也会低。
优化:
①,先不执行。
②,优化 - 合并后执行。
合并:合并后只需要一次。
a,读出整块到 buf 中。
b,在 buf 中修改扇区 0 和扇区 1。
c,擦除。
d,烧写。
总结:块设备不能像字符设备那样提供读写函数。
①,先把读写放入队列,先不执行。
②,优化后再执行。
块设备驱动框架
APP: open,read,write “1.txt”
-------------------------------------------------------------- 文件读写
文件系统:vfat,ext2,ext3,yaffs (把文件的读写转换成对扇区的读写)
------------------ll_rw_block----------------------------- 扇区读写
1. 把"读写"放入队列
2. 调用队列的处理函数(优化/调顺序/合并)
块设备驱动程序
---------------------------------------------------------------.
硬件: 硬盘、FLASH
应用程序读写一个普通的文件,最终要转换成操作硬件,由块设备驱动来操作硬件。引入文件系统把对普通文件的读写转换成对扇区的读写,有个通用的入口ll_rw_block(low level,底层读写块)。读写请求会放入队列优化后执行,ll_rw_block会把"读写"放入队列,调用队列的处理函数(优化/调顺序/合并)。我们不关心文件系统,关心块设备驱动程序。文件系统就是文件的组织格式,buffer.c位于fs的根目录,是个通用的文件。
分析ll_rw_block
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
//rw表示读/写,数据传输三要素:源、目的、长度放在buffer_head里面,buffer_head这是个数组,有nr个数组项。
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
submit_bh(rw, bh); //提交bh
struct bio *bio; //使用bh来构造bio(block input/output)块输入/出
submit_bio(rw, bio); //提交bio
// 通用的构造请求: 使用bio来构造请求(request)
generic_make_request(bio);
__generic_make_request(bio);
// 找到队列
request_queue_t *q = bdev_get_queue(bio->bi_bdev);
// 调用队列的"构造请求函数"
ret = q->make_request_fn(q, bio);
// 默认的函数是__make_request
__make_request
// 先尝试合并
elv_merge(q, &req, bio); //电梯调度算法
// 如果合并不成,使用bio构造请求
init_request_from_bio(req, bio);
// 把请求放入队列
add_request(q, req);
// 执行队列
__generic_unplug_device(q);
// 调用队列的"处理函数"
q->request_fn(q);
怎么写块设备驱动程序呢?
1 分配gendisk: alloc_disk
2 设置
2.1 分配/设置队列: request_queue_t // 队列提供读写能力
blk_init_queue //简单初始化
2.2 设置gendisk其他信息 //它提供块设备属性: 比如主设备号,容量等
3 注册: add_disk
struct gendisk {
int major; //主设备号
int first_minor; //第一个次设备号
char disk_name[32]; //名字
struct block_device_operations *fops; //空操作函数
struct request_queue *queue; //队列
sector_t capacity; //容量
......................
};
用blk_init_queue(do_ramblock_request, &ramblock_lock)函数分配一个队列,blk_init_queue初始化函数里提供了默认的构造请求的函数__make_request。
前面说把对文件的读写转换成块设备的读写,对这些扇区的读写会放到队列里面。把bh构造为bio,把bio放入队列里面,调用队列里面的构造请求的函数make_request_fn(q, bio),队列里面有个__make_request构造请求的函数,它有默认的函数。
当我们初始化队列的时候,它给我们提供默认的构造请求函数__make_request,当它把请求放入队列之后,以后会用到队列里的request_fn来处理,而request_fn是blk_init_queue函数中传入的第一个参数,所以最终会调用do_ramblock_request来处理那些请求。
struct request {
request_queue_t *q;
sector_t sector; //下一个要提交的扇区
unsigned int current_nr_sectors; //当前剩下要处理的扇区的个数, 长度
char *buffer; //写的源,读的目的
...................
内存模拟磁盘
参考:drivers\block\xd.c drivers\block\z2ram.c
static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;
static int major;
static DEFINE_SPINLOCK(ramblock_lock); //自旋锁
#define RAMBLOCK_SIZE (1024*1024) //容量1M
static unsigned char *ramblock_buf; //分配内存地址
static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo){
//获得几何属性 容量=heads*cylinders*sectors*512
geo->heads = 2; //磁头:有多少面
geo->cylinders = 32; //柱面:有多少环
geo->sectors = RAMBLOCK_SIZE/2/32/512; //扇区:一环有多少扇区
return 0;
}
static struct block_device_operations ramblock_fops = {
.owner = THIS_MODULE,
.getgeo = ramblock_getgeo, //获得几何属性
};
//处理队列函数,用内存模拟,读写用memcopy
static void do_ramblock_request(request_queue_t * q){
struct request *req;
while ((req = elv_next_request(q)) != NULL) { //以电梯调度算法从队列q里取出下一个请求
/* 数据传输三要素: 源,目的,长度 */
/* 源/目的: */
unsigned long offset = req->sector * 512; //下一个要提交的扇区。这就源或是目的
/* 目的/源: */
// req->buffer
/* 长度: */
unsigned long len = req->current_nr_sectors * 512; //当前要处理的扇区个数。这是长度
if (rq_data_dir(req) == READ){ //读
memcpy(req->buffer, ramblock_buf+offset, len);
}
else{
memcpy(ramblock_buf+offset, req->buffer, len);
}
end_request(req, 1); //1表示成功,0表示失败
}
}
static int ramblock_init(void){
/* 1. 分配一个gendisk结构体 */
ramblock_disk = alloc_disk(16); //次设备号个数: 分区个数+1
/* 2. 设置 */
/* 2.1 分配/设置队列: 提供读写能力 */
ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock); //参数:处理队列函数, 自旋锁
ramblock_disk->queue = ramblock_queue;
/* 2.2 设置其他属性: 比如容量 */
major = register_blkdev(0, "ramblock"); // 注册块设备,没有file_operation结构体,只是cat /proc/devices返回信息
ramblock_disk->major = major; //主设备号
ramblock_disk->first_minor = 0; //第一个次设备号
sprintf(ramblock_disk->disk_name, "ramblock"); //名字
ramblock_disk->fops = &ramblock_fops; //空操作函数
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512); //容量:以扇区(512字节)为单位
/* 3. 硬件相关操作 */
ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL); //分配内存
/* 4. 注册 */
add_disk(ramblock_disk);
return 0;
}
static void ramblock_exit(void){
unregister_blkdev(major, "ramblock");
del_gendisk(ramblock_disk);
put_disk(ramblock_disk);
blk_cleanup_queue(ramblock_queue);
kfree(ramblock_buf);
}
module_init(ramblock_init);
module_exit(ramblock_exit);
MODULE_LICENSE("GPL");
虽然用内存模拟磁盘没有磁头、柱面、扇区这些概念,但为了用这个老工具fdisk分区工具,要假装有这些东西。在ramblock_fops中设置.getgeo=ramblock_getgeo,获得几何属性。
块设备框架:
写块设备驱动程序,分配gendisk: alloc_disk结构体,设置分为两部分。一部分设置队列: request_queue_t提供读写能力,另外是设置gendisk其他信息比如容量,然后注册: add_disk就可以了。以后的操作不用操心,格式化或读写文件由文件系统这一层帮你把文件的读写转换成对扇区的读写。调用ll-rw-block,它会把读写放到队列里,会调用队列里的函数来处理,我们只要写好处理队列的函数do_ramblock_request。
上一篇: 递归分治--逆序/正序输出正数
下一篇: LCD 驱动 S3C2440A