Linux块设备驱动实验

直接参考【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81

本文仅作为个人笔记使用,方便进一步记录自己的实践总结。

前面我们都是在学习字符设备驱动,本章我们来学习一下块设备驱动框架,块设备驱动是Linux 三大驱动类型之一。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本章我们重点学习一下块设备相关驱动概念,不涉及到具体的存储设备。最后,我们使用 ALPHA 开发板板载 RAM 模拟一个块设备,学习块设备驱动框架的使用。

什么是块设备?

块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:

①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。

②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。

这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块设备寿命引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命。

字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。

块设备结构的不同其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。

块设备几个重要结构体

1、注册块设备

和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:

int register_blkdev(unsigned int major, const char *name)

函数参数和返回值含义如下:

major:主设备号。

name:块设备名字。

返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。

2、注销块设备

和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:

void unregister_blkdev(unsigned int major, const char *name)

函数参数和返回值含义如下:

major:要注销的块设备主设备号。

name:要注销的块设备名字

返回值:无。

block_device 结构体

linux 内 核 使 用 block_device 表 示 块 设 备 , block_device 为 一 个 结 构 体 , 定 义 在include/linux/fs.h 文件中,结构体内容如下:

待补充。

对于 block_device 结构体,我们重点关注一下第 21 行的 bd_disk 成员变量,此成员变量为gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。

gendisk 结构体

linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:

待补充。

我们简单看一下 gendisk 结构体中比较重要的几个成员变量:

第 5 行,major 为磁盘设备的主设备号。

第 6 行,first_minor 为磁盘的第一个次设备号。

第 7 行,minors 为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,次设备号不同。

第 21 行,part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型,disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。

第 24 行,fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样,是块设备驱动中的重点!

第 25 行,queue 为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。

编写块的设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数,我们来看一下一些常用的 API 函数。

1、申请 gendisk

使用 gendisk 之前要先申请,allo_disk 函数用于申请一个 gendisk,函数原型如下:

struct gendisk *alloc_disk(int minors)

函数参数和返回值含义如下:

minors:次设备号数量,也就是 gendisk 对应的分区数量。

返回值:成功:返回申请到的 gendisk,失败:NULL。

2、删除 gendisk

如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:

void del_gendisk(struct gendisk *gp)

函数参数和返回值含义如下:

gp:要删除的 gendisk。

返回值:无。

3、将 gendisk 添加到内核

使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中,add_disk 函数原型如下:

void add_disk(struct gendisk *disk)

函数参数和返回值含义如下:

disk:要添加到内核的 gendisk。

返回值:无。

4、设置 gendisk 容量

每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:

void set_capacity(struct gendisk *disk, sector_t size)

函数参数和返回值含义如下:

disk:要设置容量的 gendisk。

size:磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(2*1024*1024)/512=4096。

返回值:无。

5、调整 gendisk 引用计数

内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:

truct kobject *get_disk(struct gendisk *disk)

void put_disk(struct gendisk *disk)

block_device_operations 结构体

和字符设备的 file _operations 一样,块设备也有操作集,为结构体block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:

待补充。

可以看出,block_device_operations 结构体里面的操作集函数和字符设备的 file_operations操作集基本类似,但是块设备的操作集函数比较少,我们来看一下其中比较重要的几个成员函数:

第 2 行,open 函数用于打开指定的块设备。

第 3 行,release 函数用于关闭(释放)指定的块设备。

第 4 行,rw_page 函数用于读写指定的页。

第 5 行,ioctl 函数用于块设备的 I/O 控制。

第 6 行,compat_ioctl 函数和 ioctl 函数一样,都是用于块设备的 I/O 控制。区别在于在 64位系统上,32 位应用程序的 ioctl 会调用 compat_iotl 函数。在 32 位系统上运行的 32 位应用程序调用的就是 ioctl 函数。

第 15 行,getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。

第 18 行,owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。

块设备 I/O 请求过程

大家如果仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出了块设备驱动中非常重要的 request_queue、request 和 bio。

1、请求队列 request_queue

内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们先来看一下 request_queue,这是一个结构体,定义在文件 include/linux/blkdev.h 中,由于request_queue 结构体比较长,这里就不列出来了。大家回过头看一下示例代码 68.2.2.1 的 gendisk结构体就会发现里面有一个 request_queue 结构体指针类型成员变量 queue,也就说在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue。

①、初始化请求队列

我们首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个request_queue 地址赋值给 gendisk 的 queue 成员变量。使用 blk_init_queue 函数来完成request_queue 的申请与初始化,函数原型如下:

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

函数参数和返回值含义如下:

rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数request_fn_proc 原型如下:

void (request_fn_proc) (struct request_queue *q)

请求处理函数需要驱动编写人员自行实现。

lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。

返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。

②、删除请求队列

当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,删除请求队列使用函数 blk_cleanup_queue,函数原型如下:

void blk_cleanup_queue(struct request_queue *q)

函数参数和返回值含义如下:

q:需要删除的请求队列。

返回值:无。

③、分配请求队列并绑定制造请求函数

blk_init_queue 函数完成了请求队列的申请以及请求处理函数的绑定,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备我们可以先申请 request_queue,然后将申请到的request_queue 与“制造请求”函数绑定在一起。

先来看一下 request_queue 申请函数 blk_alloc_queue,函数原型如下:

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

函数参数和返回值含义如下:

gfp_mask:内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义,一般为 GFP_KERNEL。

返回值:申请到的无 I/O 调度的 request_queue。

我们需要为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数(其他参考资料将其直接翻译为“制造请求”函数)。这里我们需要用到函数 blk_queue_make_request,函数原型如下:

void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)

函数参数和返回值含义如下:

q:需要绑定的请求队列,也就是 blk_alloc_queue 申请到的请求队列。

mfn:需要绑定的“制造”请求函数,函数原型如下:

void (make_request_fn) (struct request_queue *q, struct bio *bio)

“制造请求”函数需要驱动编写人员实现。

返回值:无。

一般 blk_alloc_queue 和 blk_queue_make_request 是搭配在一起使用的,用于那么非机械的存储设备、无需 I/O 调度器,比如 EMMC、SD 卡等。blk_init_queue 函数会给请求队列分配一个 I/O 调度器,用于机械存储设备,比如机械硬盘等。

2、请求 request

请求队列(request_queue)里面包含的就是一系列的请求(request),request 是一个结构体,定义在 include/linux/blkdev.h 里面,这里就不展开 request 结构体了,太长了。request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。前面说了,真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

①、获取请求

我们需要从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:

request *blk_peek_request(struct request_queue *q)

函数参数和返回值含义如下:

q:指定 request_queue。

返回值:request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。

②、开启请求

使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,这里要用到 blk_start_request 函数,函数原型如下:

void blk_start_request(struct request *req)

函数参数和返回值含义如下:

req:要开始处理的请求。

返回值:无。

③、一步到位处理请求

我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启,blk_fetch_request函数很简单,内容如下:

可以看出,blk_fetch_request 就是直接调用了 blk_peek_request 和 blk_start_request 这两个函数。

④、其他和请求有关的函数

关于请求的 API 还有很多,常见的见表 68.2.4.1:

3、bio 结构

每个 request 里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构,bio 结构描述了要读写的起始扇区、要读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。上层会将 bio 提交给 I/O 调度器,I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue,request_queue 里面顺序存放着一系列的 request。新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,然后插入到 request_queue 中合适的位置,这一切都是由 I/O 调度器来完成的。request_queue、request 和 bio 之间的关系如图 68.2.4.1 所示:

bio 是个结构体,定义在 include/linux/blk_types.h 中,结构体内容如下:

待补充。

重点来看一下第 6 行和第 30 行,第 6 行为 bvec_iter 结构体类型的成员变量,第 30 行为bio_vec 结构体指针类型的成员变量。

bvec_iter 结构体描述了要操作的设备扇区等信息,结构体内容如下:

bio_vec 结构体描述了内容如下:

可以看出 bio_vec 就是“page,offset,len”组合,page 指定了所在的物理页,offset 表示所处页的偏移地址,len 就是数据长度。

我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度,“页地址”是 linux 内核里面内存管理相关的概念,这里我们不深究 linux 内存管理,我们只需要知道对于 RAM 的操作最终会转换为页相关操作。

bio、bvec_iter 以及 bio_vec 这三个结构体之间的关系如图 68.4.2.2 所示:

①、遍历请求中的 bio

前面说了,请求中包含有大量的 bio,因此就涉及到遍历请求中所有 bio 并进行处理。遍历请求中的 bio 使用函数__rq_for_each_bio,这是一个宏,内容如下:

_bio 就是遍历出来的每个 bio,rq 是要进行遍历操作的请求,_bio 参数为 bio 结构体指针类型,rq 参数为 request 结构体指针类型。

②、遍历 bio 中的所有段

bio 包含了最终要操作的数据,因此还需要遍历 bio 中的所有段,这里要用到bio_for_each_segment 函数,此函数也是一个宏,内容如下:

第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为bio 结构体指针,第三个 iter 参数保存要遍历的 bio 中 bi_iter 成员变量。

③、通知 bio 处理结束

如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通知内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:

bvoid bio_endio(struct bio *bio, int error)

函数参数和返回值含义如下:

bio:要结束的 bio。

error:如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。

返回值:

使用请求队列实验

关于块设备架构就讲解这些,接下来我们使用开发板上的 RAM 模拟一段块设备,也就是ramdisk,然后编写块设备驱动。

实验程序编写

本实验对应的例程路径为:开发板光盘-> 2、Linux 驱动例程-> 24_ramdisk_withrequest。

首先是传统的使用请求队列的时候,也就是针对机械硬盘的时候如何编写驱动。由于实验程序稍微有点长,因此我们就分步骤来讲解一下,本实验参考自 linux 内核 drivers/block/z2ram.c。

打开实验源码,我们先来一下一相关的宏定义和结构体,代码如下:

待补充。

第 34~36行,实验相关宏定义,RAMDISK_SIZE 就是模拟块设备的大小,这里设置为 2MB,也就是说本实验中的虚拟块设备大小为 2MB 。 RAMDISK_NAME 为本实验名字,RADMISK_MINOR 是本实验此设备号数量,注意不是次设备号!此设备号数量决定了本块设备的磁盘分区数量。

第 39~45 行,ramdisk 的设备结构体。

第 47 行,定义一个 ramdisk 示例。

接下来看一下驱动模块的加载与卸载,内容如下:

待补充

ramdisk_init 和 ramdisk_exit 这两个函数分别为驱动入口以及出口函数,我们依次来看一下这两个函数。

第 11 行,因为本实验是使用一块内存模拟真实的块设备,因此这里先使用 kzalloc 函数申请用于 ramdisk 实验的内存,大小为 2MB。

第 18 行,初始化一个自旋锁,blk_init_queue 函数在分配并初始化请求队列的时候需要用到一次自旋锁。

第 21 行,使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里我们让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为0。

第 28 行,使用 alloc_disk 分配一个 gendisk。

第 35 行,使用 blk_init_queue 函数分配并初始化一个请求队列,请求处理函数为ramdisk_request_fn,具体的块设备读写操作就在此函数中完成,这个需要驱动开发人员去编写,稍后讲解。

第 42~47 行,初始化第 28 行申请到的 gendisk,重点是第 44 行设置 gendisk 的 fops 成员变量,也就是设置块设备的操作集。这里设置为 ramdisk_fops,需要驱动开发人员自行编写实现,稍后讲解。

第 48 行,使用 set_capacity 函数设置本块设备容量大小,注意这里的大小是扇区数,不是字节数,一个扇区是 512 字节。

第 49 行,gendisk 初始化完成以后就可以使用 add_disk 函数将 gendisk 添加到内核中,也就是向内核添加一个磁盘设备。

ramdisk_exit 函数就比较简单了,在卸载块设备驱动的时候需要将前面申请的内容都释放掉。第 71 和 72 行使用 put_disk 和 del_gendis 函数释放前面申请的 gendisk,第 75 行使用blk_cleanup_queue 函数消除前面申请的请求队列,第 78 行使用 unregister_blkdev 函数注销前面注册的块设备,最后调用 kfree 来释放掉申请的内存。

在 ramdisk_init 函数中设置了 gendisk 的 fops 成员变量,也就是块设备的操作集,具体内容如下:

待补充。

第 42~48 行就是块设备的操作集 block_device_operations,本例程实现的比较简单,仅仅实现了 open、release 和 getgeo,其中 open 和 release 函数都是空函数。重点是 getgeo 函数,第30~37 行就是 getgeo 的具体实现,此函数用户获取磁盘信息,信息保存在参数 geo 中,为结构体 hd_geometry 类型,如下:

本例程中设置 ramdisk 有 2 个磁头(head)、一共有 32 个柱面(cylinderr)。知道磁盘总容量、磁头数、柱面数以后我们就可以计算出一个磁道上有多少个扇区了,也就是 hd_geometry 中的sectors 成员变量。

最后就是非常重要的请求处理函数,使用 blk_init_queue 函数初始化队列的时候需要指定一个请求处理函数,本例程中注册的请求处理函数如下所示:

待补充。

请求处理函数的重要内容就是完成从块设备中读取数据,或者向块设备中写入数据。首先来看一下 29~47 行的 ramdisk_request_fn 函数,这个就是请求处理函数。此函数只要一个参数q,为 request_queue 结构体指针类型,也就是要处理的请求队列,因此 ramdisk_request_fn 函数的主要工作就是依次处理请求队列中的所有请求。第 35 行,首先使用 blk_fetch_request 函数获取请求队列中第一个请求,如果请求不为空的话就调用 ramdisk_transfer 函数进行对请求做进一步的处理,然后就是 while 循环依次处理完请求队列中的每个请求。第 44 行使用__blk_end_request_cur 函数检查是否为最后一个请求,如果不是的话就继续获取下一个,直至整个请求队列处理完成。

ramdisk_transfer 函数完成清楚中的数据处理,第 8 行调用 blk_rq_pos 函数从请求中获取要操作的块设备扇区地址,第 9 行使用 blk_rq_cur_bytes 函数获取请求要操作的数据长度,第 15行使用 bio_data 函数获取请求中 bio 保存的数据。第 17~20 行调用 rq_data_dir 函数判断当前是读还是写,如果是写的话就将 bio 中的数据拷贝到 ramdisk 指定地址(扇区),如果是读的话就从ramdisk 中的指定地址(扇区)读取数据放到 bio 中。

运行测试

编译上一小节的驱动,得到 ramdisk.ko 驱动模块,然后拷贝到 rootfs/lib/modules/4.1.15 目录中,重启开发板,进入到目录 lib/modules/4.1.15 中。输入如下命令加载 ramdisk.ko 这个驱动模块。

depmod //第一次加载驱动的时候需要运行此命令

modprobe ramdisk.ko //加载驱动模块

1、查看 ramdisk 磁盘

驱动加载成功以后就会在/dev/目录下生成一个名为“ramdisk”的设备, 输入如下命令查看ramdisk 磁盘信息:

fdisk -l //查看磁盘信息

上述命令会将当前系统中所有的磁盘信息都打印出来,其中就包括了 ramdisk 设备,如图68.3.2.1 所示:

2、格式化/dev/ramdisk

使用 mkfs.vfat 命令格式化/dev/ramdisk,将其格式化成 vfat 格式,输入如下命令:

mkfs.vfat /dev/ramdisk

格式化完成以后就可以挂载/dev/ramdisk 来访问了,挂载点可以自定义,这里笔者就将其挂载到/tmp 目录下,输入如下命令:

mount /dev/ramdisk /tmp

挂载成功以后就可以通过/tmp 来访问 ramdisk 这个磁盘了,进入到/tmp 目录中,可以通过vi 命令新建一个 txt 文件来测试磁盘访问是否正常。

不使用请求队列实验

对于 EMMC、SD、ramdisk 这样没有机械结构的存储设备,我们可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了。 

实验程序编写

本实验对应的例程路径为:开发板光盘-> 2、Linux 驱动例程-> 25_ramdisk_norequest。

前面我们学习了如何使用请求队列,请求队列会用到 I/O 调度器,适合机械硬盘这种存储设备。对于 EMMC、SD、ramdisk 这样没有机械结构的存储设备,我们可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了,这个我们前面已经说过了。本实验就来学习一下如何使用“制造请求”方法,本实验在上一个实验的基础上修改而来,参考了linux 内核 drivers/block/zram/zram_drv.c。重点来看一下与上一个实验不同的地方,首先是驱动入口函数 ramdisk_init,ramdisk_init 函数大部分和上一个实验相同,只是本实验中改为使用 blk_queue_make_request 函数设置“制造请求”函数,修改后的 ramdisk_init 函数内容如下(有省略):

待补充

ramdisk_init 函数中第 31~38 行就是与上一个实验不同的地方,这里使用 blk_alloc_queue和 blk_queue_make_request 这两个函数取代了上一个实验的 blk_init_queue 函数。

第 31 行,使用 blk_alloc_queue 函数申请一个请求队列。

第 38 行,使用 blk_queue_make_request 函数设置“制造请求”函数,这里设置的制造请求函数为 ramdisk_make_request_fn,这个需要驱动编写人员去实现,稍后讲解。

第 43 行,设置块设备操作集为 ramdisk_fops,和上一个实验一模一样,这里就不讲解了。

接下来重点看一下“制造请求”函数 ramdisk_make_request_fn,函数内容如下:

待补充。

虽然 ramdisk_make_request_fn 函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个 bio 参数里面,所以 ramdisk_make_request_fn函数里面是全部是对 bio 的操作。

第 13 行,直接读取 bio 的 bi_iter 成员变量的 bi_sector 来获取要操作的设备地址(扇区)。

第 16~25 行,使用 bio_for_each_segment 函数循环获取 bio 中的每个段,然后对其每个段进行处理。

第 17 行,根据 bio_vec 中页地址以及偏移地址转换为真正的数据起始地址。

第 18 行,获取要处理的数据长度,也就是 bio_vec 的 bv_len 成员变量。

第 20~23 行,和上一个实验一样,要操作的块设备起始地址知道了,数据的存放地址以及长度也知道,接下来就是根据读写操作将数据从块设备中读出来,或者将数据写入到块设备中。

第 27 行,调用 bio_endio 函数,结束 bio。

运行测试

测试方法和上一个实验一样,参考上小节即可。

补充:

上层应用对块设备的操作,就是常规的对文件进行读写的操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1559276.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Fiddler配合wireshark解密ssl

环境: win11(wireshark)--虚拟机win7(Fiddler)---虚拟机win7(HTTPS站点) 软件安装问题: 需要.net环境,NDP461-KB3102436-x86-x64-AllOS-ENU.exe。 安装fiddler后安装下…

vite项目打包md5报‘default‘ is not exported错误的解决方法

报错如下: 某一个包中用es方式引入md5模块,导致打包报错,经过一番探究测试,发现我的项目中用了“vite-plugin-require-transform”这个插件,是这个插件在做转换的时候报错了,如果你也是这个原因可按我的方式…

代码随想录day24:贪心part2

121. 买卖股票的最佳时机 class Solution {public int maxProfit(int[] prices) {int ans 0;int minPrice prices[0];for(int p : prices){ans Math.max(ans, p - minPrice);minPrice Math.min(p, minPrice);}return ans;} } 运用前缀和思想, 从左到右枚举卖…

Spring Boot教学资源库:构建微服务的基石

2 相关技术简介 2.1Java技术 Java是一种非常常用的编程语言,在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中,Java的身影无处不在,并且拥有旺盛的生命力。Java的跨平台能力十分强大,只需一次编译,任…

dart-sass和node-sass的区别,使用dart-sass后可能会出现的问题

前言: 2020 年 10 月 27 日,Sass 官方团队正式宣布 Libsass 将弃用,以及基于它的 Node Sass 和 SassC,并且建议用户使用 Dart Sass。如果在 vue 脚手架搭建的项目中需要使用 sass,建议初始化时勾选 sass 配置&#xff…

前端优化之路:git commit 校验拦截

但是想要做到高效落地执行,就需要做些别的功课,先展示下成果图 需要了解git hooks,它是git的钩子,就像vue拥有自己的钩子一样。 官方文档:https://git-scm.com/docs/githooks 项目安装 husky,建议稳定版本…

patch函数前两个参数位

如我们多了解的,patch函数前两个参数位为 oldVnode 和 Vnode ,分别代表旧节点和新节点,主要做了四个判断: patchVnode主要做了两个判断:

java中Math类和Random类的api

目录 Math 类 1)abs(x) 2)ceil(x) 3)floor(x) 4)round(x) 5)max(x, y) 6)min(x, y) 7)sqrt(x) 8)pow(x, y) 9)random() 10)sin(x), cos(x), tan(x) 11&#x…

购物网站毕业设计-电子电器商城管理系统SpringBootSSM框架开发

目录 1. 系统概述 1.1背景介绍 1.2 课题意义 1.3课题目标 2. 主要功能模块 2.1 前端用户模块 2.2 后端管理模块 2.3 功能图展示 3. 技术选型 3.1 VUE介绍 3.2 JAVA介绍 3.3 MySQL介绍 4. 系统设计 4‌.1数据库设计 5 详细设计 5.1 界面展示 设计一个电子电…

通过Keil5编译软件获取函数最深堆栈

文章目录 1.问题提出2.问题分析3.环境搭建4.解决方案5.经验总结6.疑问 1.问题提出 在编写新软件时,由于某功能需要使用RAM约24KB,而新模块的总RAM是96KB,该某功能一旦开启则需要占用四分之一的RAM空间。为了保证在开启某功能后,新…

WPF中的Window类

控件分类 在第一篇文章.Net Core和WPF介绍中的WPF的功能和特性部分根据功能性介绍了WPF的控件 名称。 在接下来的文章中,将会详细的介绍各个控件的概念及使用。 主要包括: 内容控件:Label、Button、CheckBox、ToggleButton、RadioButton、…

高性能缓存方案 —— Caffeine

一、简介 Caffeine是一个高性能的Java缓存库,它提供了本地缓存的功能。 Caffeine和Redis都是内存级别的缓存,为什么要使用在这两缓存作为二级缓存,它们两有什么区别呢? 虽然它们都是内存级别的缓存,但是Redis是需要单独部署的&…

【开源风云】从若依系列脚手架汲取编程之道(五)

📕开源风云系列 🍊本系列将从开源名将若依出发,探究优质开源项目脚手架汲取编程之道。 🍉从不分离版本开写到前后端分离版,再到微服务版本,乃至其中好玩的一系列增强Plus操作。 🍈希望你具备如下…

鸿蒙OS投票机制

(基于openharmony5.0) 投票机制 param get | grep ohos.boot.time 图 投票机制参数图 只有当所有的投票完成,开机动画才会退出,整理需要投票的系统应用(三方应用不参与投票)如下图所示: 以进程foundation为例&…

Python案例--copy复制

在Python编程中,数据的复制是一个常见且重要的操作,它涉及到赋值、浅拷贝和深拷贝三种不同的概念。正确理解这三种操作对于编写高效且正确的程序至关重要。本文将通过一个简单的Python示例,探讨这三种数据复制方式的区别及其应用场景&#xf…

计算机视觉之OpenCV vs YOLO

好多开发者希望搞明白OpenCV 和YOLO区别,实际上,二者在计算机视觉领域都有广泛应用,但它们有很大的不同。 一、OpenCV 概述 OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库。它…

软考攻略/超详细/系统集成项目管理工程师/基础知识分享12

5.1 软件工程定义(了解) 软件工程是指应用计算机科学、数学及管理科学等原理,其目的是提高软件生产率、提高软件质量、降低软件成本。 5.2 软件需求(掌握) 5.2.1 需求的层次(掌握) 软件需求是指…

打造直播美颜平台的关键技术:视频美颜SDK的深度解析

本篇文章,小编将深入解析视频美颜SDK的关键技术,探讨其在打造直播美颜平台中的作用。 一、视频美颜SDK的定义与功能 视频美颜SDK是一套专门为实时视频处理而设计的软件开发工具包。其主要功能包括人脸检测、肤色美化、瑕疵修复、虚化背景、实时滤镜等。…

chaos官方给的V-Ray材质优化器怎么样?

V-Ray材质优化器是一个为3ds Max设计的MAX脚本,它通过以下方式优化场景,提高渲染速度! V-Ray材质优化器安装包可找【成都渲染101云渲染,云渲码6666】提供! ​ 通过创建一个新的UV通道并使用平面映射算法展开场景对象。…

vue3 vue2

vue3.0是如何变快的? diff算法优化 vue2的虚拟dom是进行全局的对比。vue3 新增了静态标记(patchFlag) 在与上次虚拟节点进行比较的时候,只对比带有patch Flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内…