块设备的操作函数并没有类似于字符驱动中的read 和write函数,要实现读写操作,只能在请求处理函数中实现。这就分为两种,是否要使用请求队列,请求队列的主要作用是管理和调度IO请求。在以下情况中,一般需要用到请求队队列:
- 多任务环境:多个任务同时对存储设备进行读写,请求队列可以对IO请求进行排序和调度
- 磁盘优化:磁盘是一种机械设备,其IO操作需要进行磁盘寻道等操作,非常耗时,请求队列可以将多个IO请求进行合并,可以减少磁盘的寻道次数,提高磁盘的吞吐量。
因此,本篇的重点便是使用请求队列来实现块设备的读写操作。
目录
一、请求处理函数的触发条件
二、从请求队列获取请求
三、开始进行读写操作
1、请求的结构
2、相关API
四、关闭请求
五、完整代码
一、请求处理函数的触发条件
若要对块设备进行读写操作,需要触发请求处理函数,随后块设备驱动才会调度并处理请求。那么在什么情况下会触发请求处理函数?以下是读写操作的常见触发条件。
读操作
一般涉及到访问文件内容、文件属性、文件权限的时候,会触发请求处理函数中的读操作。
① 读取数据
当应用程序执行文件读取操作(如 read 函数)时,它会向文件系统发送读取请求,并触发请求处理函数中的读操作。每一个请求内包含源数据(块设备扇区地址、读取字节数)以及目的地址(内存页地址、页偏移)。文件系统从块设备读取相应的文件块数据,并将其返回给应用程序。
② 目录遍历
当应用程序执行目录遍历操作(如 opendir、readdir 函数等)时,文件系统会在磁盘上读取目录的元数据,并返回给应用程序。
③打开文件
当应用程序打开一个文件时,文件系统需要读取磁盘上的文件元数据信息,如文件大小、访问权限等,以供应用程序使用。
写操作
一般涉及到文件内容的更新、文件创建等操作时,会触发请求处理函数中的写操作。
① 文件内容写入(追加、更新)
文件内容的写入(write)和追加(append)会向文件系统发送写入请求,并触发请求触发函数中的写操作。每一个请求内包含源数据(内存页地址、页偏移)以及目的地址(块设备扇区地址),文件系统会将应用程序提供的数据写入到指定扇区。
此外,除了文件内容的更新,文件属性(如访问权限)的更新也会触发请求处理函数。
② 文件创建
当应用程序创建一个新文件时,它会向文件系统发送创建文件的请求,并触发请求处理函数中的写操作。文件系统在磁盘上为新文件分配空间,并更新文件的元数据信息。
二、从请求队列获取请求
接下来便是正式开始实现请求处理函数,函数的形参为请求队列,对此,下面的第一步便是从请求队列中获取请求。涉及的API 在 <linux/blkdev.h> 中
void request_handle(struct request_queue *q);
① 方式一:获取 + 开启请求
正常来说,获取请求包含两步,第一步是获取请求,使用的API为 blk_peek_request
/*** @param q 请求队列* @return 成功返回获取到的请求结构体地址,失败返回 NULL*/
struct request *blk_peek_request(struct request_queue *q);
获取到请求以后,还没结束,需要开启请求,目的是将通知硬件设备开始处理当前请求,同时将当前请求标记为启动状态,以确保请求在处理的过程中不会被打断。其他还有数据预处理、设备准备等原因。
/*** @param rq 获取到的请求*/
void blk_start_request(struct request *rq);
② 方式二:一步到位
Linux内核提供了获取 + 开启一步到位的方式,使用的API为 blk_fetch_request,该函数其实就是对上述两个函数的使用做了一层封装。
/*** @param q 请求队列* @return 成功返回获取到的请求结构体地址,失败返回 NULL*/
struct request *blk_fetch_request(struct request_queue *q);
/*
struct request *blk_fetch_request(struct request_queue *q)
{struct request *rq;rq = blk_peek_request(q);if (rq)blk_start_request(rq);return rq;
}
*/
三、开始进行读写操作
在开始读写操作之前,需要先了解每一个请求的结构,因为既然要进行读写操作,我们需要知道源数据地址、目的地址、操作的字节数等信息。
1、请求的结构
每个request 中保存了多个 bio,bio保存着最终要读写的数据、地址等信息。bio 中的 bvec_iter 保存了块设备扇区起始地址、扇区大小等信息,bio 中的 bio_vec 保存了 RAM 页地址、剩余页长度以及页偏移等信息。
下面是 bio、bio_iter 和 bio_vec 三者之间的关系
2、相关API
获得扇区起始地址
/*** @param rq 指向请求结构体的指针* @return 成功返回获取到的扇区起始地址*/
sector_t blk_rq_pos(const struct request *rq);
获取请求所涉及的数据长度
/*** @param rq 指向请求结构体的指针* @return 成功返回数据长度,单位:字节*/
unsigned int blk_rq_bytes(const struct request *rq);
获取指定数据页的线性地址,即 RAM 中的起始页地址
/*** @param bio 指向bio结构体的指针* @return 成功返回指向指定数据页的地址*/
void *bio_data(struct bio *bio);
获取当前请求的数据传输方向(读操作、写操作)
/*** @param rq 指向请求结构体的指针* @return 返回数据传输方向。可以是 READ / WRITE*/
int rq_data_dir(struct request *rq);
四、关闭请求
与前面开启请求相对应,开启请求后将当前请求标记为开启状态,以确保在处理请求的过程中不会被打断;关闭请求意味着该请求的数据传输和相关操作已经完成,主要目的有两点:
- 通知请求完成:通知相关的应用程序或调用者操作已经结束,允许继续执行后续的操作
- 资源释放:在请求处理完成后,需要释放请求所占用的内存和其他资源,以便系统能够重新利用这些资源。
这里可以使用 __blk_end_request_cur 或者 blk_end_request_cur,两个函数的使用方法完全一样,他们之间的区别在于
① __blk_end_request_cur
__blk_end_request_cur是一个异步函数,它会立即结束当前的 I/O 请求,并触发相应的回调函数(例如,end_io)来进行后续处理。这意味着调用 __blk_end_request_cur 并不会等待请求的处理完成,而是立即返回,将请求的处理交给后续的回调函数来处理。
② blk_end_request_cur
blk_end_request_cur 与上面相反,是一个同步函数,它会等待当前的 I/O 请求的处理完成,并在处理完成后返回。它会等待直到请求的 end_io 回调函数执行完毕,或者出现超时或错误等情况。因此,在调用 blk_end_request_cur 的过程中,它会阻塞当前的执行线程,直到请求处理完成。
/*** @param rq 指向请求结构体的指针* @param error 代表请求处理结果,* - 若请求处理成功,应该将 error 设置为 0* - 若请求处理失败,则可以设置为其他非零值。*/
bool __blk_end_request_cur(struct request *rq, int error);
五、完整代码
将驱动代码重新编译并加入到内核,输入 fdisk -l,我们会发现请求处理函数被触发了,因为 fdisk -l 用于列出当前系统中所有的磁盘分区信息,在扫描设备的过程中,会涉及块设备的访问和信息读取,这就导致了请求处理函数被触发。
void request_handle(struct request_queue *q)
{printk("请求处理函数被触发!\n");struct request* req; // 处理请求int errors = 0; // 请求处理状态sector_t start = 0; // 操作扇区的起始地址int len = 0;while ((req = blk_fetch_request(q)) != NULL){// 获得扇区起始地址start = blk_rq_pos(req);// 当前请求所涉及的字节数len = blk_rq_cur_bytes(req);// 获得bio结构体中的缓冲区指针void* buffer = bio_data(req->bio);// 判断是读操作还是写操作if (rq_data_dir(req) == READ){memcpy((uint8_t*)buffer, blkdev.diskbuf, len);printk("从块设备读数据\n");}else if(rq_data_dir(req) == WRITE){memcpy(blkdev.diskbuf, (uint8_t*)buffer, len);printk("向块设备写数据\n");}// 结束当前请求__blk_end_request_cur(req, errors);}
}