并非每种块设备都会用到请求队列,从上节可以知道,请求队列的作用是管理和调用IO请求,那么反过来想,如果IO请求较少,那就可以无需使用请求队列。在以下情况中,可以不使用请求队列。
单任务环境: 当系统中只有单个任务(线程或进程)需要对存储设备进行读写操作时,IO操作可以直接被发起,而无需经过请求队列进行调度。
IO操作不频繁: 当系统中的IO操作非常稀少并且不频繁时,IO操作可以被直接发起,并由底层设备来处理,而无需经过请求队列进行处理。
闪存设备:闪存设备通常由多个存储单元(通常是存储芯片)组成,每个存储单元可以存储大量的数据,通常以块(页)为单位进行读写,每个块的大小通常为几十到几百KB,因此,闪存设备可以直接以块为单位进行读写,无需像传统设备那样以扇区为单位进行读写。
本篇的重点是在不使用请求队列的情况下读写块设备,依然需要用到请求处理函数,只不过内部的逻辑与之前有所不同。
目录
一、制造请求
1、API 介绍
2、代码实现
二、请求处理函数
1、遍历 bio 结构退数组
2、获取扇区起始地址
3、获取内存页起始地址及页偏移
4、获取数据长度
5、读写操作实现
6、关闭 bio 操作
7、完整示例
现在不使用请求队列,所以下面要换一个请求处理函数的原型,使用 blk_queue_make_request 来注册一个新的请求处理函数。函数声明如下,这里虽然还注册了一个请求队列,但是我们后续不会用到这个请求队列。
/**
* @param q 请求队列
* @param hanle 请求处理函数指针
*/
void blk_queue_make_request(struct request_queue * q, make_request_fn * handle);
make_request_fn 函数指针的原型如下,q 为请求队列,bio 为 bio 结构体数组,bio 是一种描述块设备IO操作的数据结构,如起始地址、操作的数据长度、目的地址等信息都在这个结构体中。
ps:上一节在介绍每个请求的结构提到过。(详情参考上一篇的第三部分内容)
/**
* @param q 请求队列
* @param hanle 请求处理函数指针
*/
void (* make_request_fn)(struct request_queue *q, struct bio *bio);
注意:后续测试不会使用这个请求队列,这个请求队列只是以备不时之需。
这里就直接在之前的基础上进行修改,修改的地方只有一处,将原本的 “申请请求队列” 替换为 “制造请求”即可。其中 blk_alloc_queue 用于动态创建并初始化一个请求队列。
// 请求处理函数
void make_request_hanlde(struct request_queue *q, struct bio *bio)
{
}
// 驱动入口函数
static int __init blkdriver_init(void)
{
// ... ...
/*制造请求*/
blkdev.queue = blk_alloc_queue(GFP_KERNEL);
if(blkdev.queue == NULL)
{
del_gendisk(blkdev.gendisk);
unregister_blkdev(blkdev.major, blkdev_NAME);
return -1;
}
blk_queue_make_request(blkdev.queue, make_request_hanlde);
// ... ...
}
bio 中包含了两个重要结构体对象,一个是 bio_vec 结构体对象 bi_io_vec,保存了内存页的相关信息,如页地址、页偏移;另一个是 bvec_iter 结构体对象 bi_iter,保存了块设备扇区相关信息,如扇区地址。
遍历 bio 数组使用的是宏 bio_for_each_segment,本质是一个for 循环,bvec、bio 和 iter 都是 for 循环中的循环项,该宏的声明如下。
/*
* @param bvec: bio_vec结构体对象 (保存的是内存页信息)
* @param bio: bio 结构体指针
* @param iter: bvec_iter结构体对象 (保存的是扇区信息)
*/
#define bio_for_each_segment(bvec, bio, iter)
传入的 bvec、bio 和 iter 之间关系如下,随着循环的推进,bio 指针也会跟着变化。
iter = bio->bi_iter;
bvec = bio->bi_io_vec;
bio = bio + sizeof(bio);
因为这里是将虚拟内存 diskbuf 模拟成一个块设备,所以这里无需手动获取扇区地址。若是在使用真实块设备时,可以使用传入的 iter 循环项来获取扇区起始地址。
sector_t start = iter.bi_sector; // sector_t 本质是unsigned long 类型
获取页偏移可以直接通过 bvec 循环项获取。
int offset = bvec.bv_offset;
获取内存页起始地址有两种方法:
① bio_data 一步到位
bio_data 是内核提供的API,能够获取内存中数据页的地址,在上一篇也用到了,函数声明如下:
/**
* @param bio 指向bio结构体的指针
* @return 成功返回指向指定数据页的地址
*/
void *bio_data(struct bio *bio);
② 逐步获取
这种方法其实就是根据 bvec 循环项先获取到 page 结构体成员,然后将 page 转换成数据页地址
struct page* pagestruct = bvec.bv_page; // 先获取到与数据页相关的数据类型
void* buffer = page_address(pagestruct); // 从 page 结构体获取到页地址
在遍历 bio 数组时,bio_vec 的 bv_len 代表当前数据段的长度;bvec_iter 的 bi_size 是当前迭代器所指向的数据段的字节数,这两者是一致的。一般使用 bv_len
int len = bvec.bv_len;
这里的读写操作和上一篇基本一致,使用 bio_data_dir 来判断读写方向
if (bio_data_dir(bio) == WRITE)
memcpy(blkdev.diskbuf, buffer + offset, len);
else if (bio_data_dir(bio) == READ)
memcpy(buffer + offset, blkdev.diskbuf, len);
和上一篇 blk_end_request 一样,每次请求处理结束,需要做一些收尾工作,如释放用于 I/O 操作的资源、更新状态信息、通知上层文件系统或者块层,还可以处理错误情况。使用的API原型如下:
/**
* @param bio 指向bio结构体的指针
* @param error I/O操作的错误码
*/
void bio_endio(struct bio *bio, int error);
void make_request_hanlde(struct request_queue *q, struct bio *bio)
{
int offset = 0;
int len = 0;
struct bio_vec bvec; // 内存页信息
struct bvec_iter iter; // 块设备扇区信息
printk("请求处理函数被触发!\n");
bio_for_each_segment(bvec, bio, iter) {
// 扇区起始地址
// 内存页起始地址
void* buffer = page_address(pageAddr);
// 内存偏移
offset = bvec.bv_offset;
// 涉及的数据长度
len = bvec.bv_len;
if (bio_data_dir(bio) == WRITE)
{
memcpy(blkdev.diskbuf, buffer + offset, len);
}
else if (bio_data_dir(bio) == READ)
{
memcpy(buffer + offset, blkdev.diskbuf, len);
}
}
bio_endio(bio, 0);
}
替换请求处理函数以后,将块设备注册到内核无异常,输入 fdisk -l 可以看到我们注册的块设备
此外,fdisk -l 会触发请求处理函数,所以会打印预设的内容。