【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)

并非每种块设备都会用到请求队列,从上节可以知道,请求队列的作用是管理和调用IO请求,那么反过来想,如果IO请求较少,那就可以无需使用请求队列。在以下情况中,可以不使用请求队列。

  • 单任务环境: 当系统中只有单个任务(线程或进程)需要对存储设备进行读写操作时,IO操作可以直接被发起,而无需经过请求队列进行调度。

  • IO操作不频繁: 当系统中的IO操作非常稀少并且不频繁时,IO操作可以被直接发起,并由底层设备来处理,而无需经过请求队列进行处理。

  • 闪存设备:闪存设备通常由多个存储单元(通常是存储芯片)组成,每个存储单元可以存储大量的数据,通常以块(页)为单位进行读写,每个块的大小通常为几十到几百KB,因此,闪存设备可以直接以块为单位进行读写,无需像传统设备那样以扇区为单位进行读写。

本篇的重点是在不使用请求队列的情况下读写块设备,依然需要用到请求处理函数,只不过内部的逻辑与之前有所不同。


目录

一、制造请求

1、API 介绍

2、代码实现

二、请求处理函数

1、遍历 bio 结构退数组

2、获取扇区起始地址

3、获取内存页起始地址及页偏移

4、获取数据长度

5、读写操作实现

6、关闭 bio 操作

7、完整示例


一、制造请求

1、API 介绍

现在不使用请求队列,所以下面要换一个请求处理函数的原型,使用 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);

注意:后续测试不会使用这个请求队列,这个请求队列只是以备不时之需。 

2、代码实现

这里就直接在之前的基础上进行修改,修改的地方只有一处,将原本的 “申请请求队列” 替换为 “制造请求”即可。其中 blk_alloc_queue 用于动态创建并初始化一个请求队列。

【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)_第1张图片

// 请求处理函数
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,保存了块设备扇区相关信息,如扇区地址。

【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)_第2张图片

1、遍历 bio 结构退数组

遍历 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);

2、获取扇区起始地址

因为这里是将虚拟内存 diskbuf 模拟成一个块设备,所以这里无需手动获取扇区地址。若是在使用真实块设备时,可以使用传入的 iter 循环项来获取扇区起始地址。

sector_t start = iter.bi_sector;    // sector_t 本质是unsigned long 类型

3、获取内存页起始地址及页偏移

获取页偏移可以直接通过 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 结构体获取到页地址

4、获取数据长度

在遍历 bio 数组时,bio_vec 的 bv_len 代表当前数据段的长度;bvec_iter 的 bi_size 是当前迭代器所指向的数据段的字节数,这两者是一致的。一般使用 bv_len

int len = bvec.bv_len;

5、读写操作实现

这里的读写操作和上一篇基本一致,使用 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);

6、关闭 bio 操作

和上一篇 blk_end_request 一样,每次请求处理结束,需要做一些收尾工作,如释放用于 I/O 操作的资源、更新状态信息、通知上层文件系统或者块层,还可以处理错误情况。使用的API原型如下:

/**
 * @param bio     指向bio结构体的指针
 * @param error   I/O操作的错误码
 */
void bio_endio(struct bio *bio, int error);

7、完整示例

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 可以看到我们注册的块设备 

【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)_第3张图片

此外,fdisk -l 会触发请求处理函数,所以会打印预设的内容。

【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)_第4张图片

你可能感兴趣的:(驱动开发)