一、基本概念:
块设备(block device):能随机访问固定大小数据片(chunk)的设备,如硬盘,通常以安装文件系统的方式使用;
扇区(sector):块设备中的最小可寻址单元,一般为512Byte(如果大于512Byte,则底层驱动负责相应的转换),数据在块设备上的位置由块索引和块内偏移决定。
块(blocks):对VFS(虚拟文件系统),块是基本数据传输单元。当内核访问文件数据时,首先从磁盘上读取一个块,这个块有文件的inode,该块对应磁盘上一个或多个扇区。块的大小一般为扇区大小的整数倍,但同时要不能超过一页的长度。
段(segment):
简单DMA操作,只能传输磁盘上相邻的扇区的数据到连续的内存区域中。
但在“分散/聚合”DMA操作模式下,传输可以在多个非连续的内存区域中进行。例如在读操作中,控制器从磁盘相邻扇区上读取数据,然后将数据分散存储在内存的不同区域中。一个段(segment)就是一个内存页面或内存页面的一部分,它包含磁盘相邻扇区上的部分数据。

二、通用块设备层
通用块设备层(generic block layer)处理系统所有对块设备的请求。有了它之后,内核可以:
1)磁盘数据直接拷贝到用户地址空间中,不需要先拷贝到内核地址空间。实际上是,内核进行I/O传输的数据页面被映射到用户进程的地址空间中。
2)管理逻辑卷,如LVM(Logical volume manager)和RAID(磁盘阵列)。
gendisk结构:
gendisk是一个物理磁盘或分区在内核中的描述。
Request_queue结构:
每一个gendisk对象都有一个request_queue对象,表示针对一个gendisk对象的所有请求的队列。
请求request:
每个request结构都代表了一个块设备的I/O请求.
在分散-聚合DMA模式下,读请求操作中,控制器从磁盘相邻扇区上读取数据,然后将数据分散存储在内存的不同区域中;写请求操作中,控制器读取分散在内存不同区域的数据,然后写入磁盘相邻扇区中。
请求被表示为一系列段(segment),每个段对应内存中的一个缓冲区。从本质上讲,一个request结构是作为一个bio结构的链表实现的。
bio结构:
bio用来描述单一的块设备I/O请求。它代表了正在活动的以段(segment)链表形式组织的块I/O操作。一个段是一小块内存缓冲区,这些段代表的小块缓冲区在内存中不一定连续,但通过段链表组织在一起就在逻辑上合并成了一个完整的缓冲区。这样的向量I/O称为分散-聚合I/O。
核心成员:
unsigned short bi_vcnt;// 这个bio包含的bio_vec的数目;
struct bio_vec *bi_io_vec;// 指向一个bio_vec结构体数组,该结构体数组包含了一个特定I/O操作所需要使用的所有段(segment)
注:bio_vec描述指定page中的一块连续的区域,也就是一个page中的一个”段”(segment)。
更多细节参考链接:https://www.cnblogs.com/xiaojiang1025/p/6500557.html
版本1(Linux2.6.32之前的版本)的请求处理函数
static void simp_blkdev_do_request(struct request_queue *q){
struct request *req;
// elv_next_request从请求队列中拿出一条请求
// end_rquest结束一个请求,1:成功,0:失败
while((req = elv_next_request(q)) != NULL){
if((req->setor + req->current_nr_sectors)<<9 > SIMP_BLKDEV_BYTES){
printk(KERN_ERR_SIMP_BLKDEV_DISKNAME
":bad request:block=%llu, count=%u\n",
(unsigned long long)req->sector,
req->current_nr_sectors);
end_request(req, 0);
continue;
}
// rq_data_dir返回请求的方向(读/写)
// void *memcpy(void*dest, const void *src, size_t n);
// 由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。
switch(rq_data_dir(req)){
case READ:
memcpy(req->buffer,
simp_blkdev_disk+(req->sector<<9),
req->current_nr_sectors<<9);
end_request(req, 1);
break;
case WRITE:
memcpy(simp_blkdev_disk+(req->sector<<9),
req->buffer,
req->current_nr_sectors<<9);
end_request(req, 1);
break;
default:
// No default because rq_data_dir(req) is 1 bit
break;
}
}
}
当使用高于Linux2.6.32版本的内核编译以上函数时,会报错:request结构体不存在buffer成员。
版本2(Linux3.x.x版本)的请求处理函数:
相比于Linux2.6版本的内核,Linux2.6.32之后的版本内核有了很大的改动,例如request结构体的buffer成员被抛弃,无法直接利用memcpy函数对块设备的物理内存进行读写操作;在2.6.32内核中:
| 原成员 | 转变 | 新成员 |
|---|---|---|
| request -> sectors | 变为 | blk_rq_pos(request) |
| request -> nr_sectors | 变为 | blk_rq_nr_sectors(request) |
| elv_next_request(request) | 变为 | blk_fetch_request(request) |
| end_request(request, error) | 变为 | __blk_end_request_all(req, err)) |
static void simp_blkdev_do_request(struct request_queue *q)
{
struct request *req;
struct req_iterator ri;
struct bio_vec *bvec;
char *disk_mem;
char *buffer;
//依次从队列中获取request
while ((req = blk_fetch_request(q)) != NULL) {
//判断当前request是否合法
if ((blk_rq_pos(req) << 9) + blk_rq_cur_bytes(req)
> SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)blk_rq_pos(req),
blk_rq_cur_bytes(req));
blk_end_request_all(req, -EIO);
continue;
}
//获取需要操作的内存位置
disk_mem = simp_blkdev_data + (blk_rq_pos(req) << 9);
switch (rq_data_dir(req)) { //判断请求的类型
case READ:
rq_for_each_segment(bvec, req, ri)
{
buffer = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(buffer, disk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
disk_mem += bvec->bv_len;
}
__blk_end_request_all(req, 0);
break;
case WRITE:
rq_for_each_segment(bvec, req, ri)
{
buffer = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(disk_mem, buffer, bvec->bv_len);
kunmap(bvec->bv_page);
disk_mem += bvec->bv_len;
}
__blk_end_request_all(req, 0);
break;
default:
/* No default because rq_data_dir(req) is 1 bit */
break;
}
}
}
使用Linux4.13.0版本进行编译出现的错误:
原因:函数rq_for_each_segment(bvec, req, ri)参数类型出错。
参考链接:https://www.cnblogs.com/chengxuyuancc/p/3550047.html
版本3(Linux4.13.0内核)的请求处理函数:
为了纠正版本2中函数参数类型不兼容问题,不再使用rq_for_each_segment函数遍历req->bio结构中的段(segment)链表。
bio->bi_io_vec指向一个bio_vec结构体数组,该结构体数组包含了一个特定块I/O操作所需要使用的所有段(segment)。每个bio_vec结构体都是一个形式为(page, offset , len)的向量,它描述一个特定的段:段所在的物理页,段在物理页中的偏移量,从给定偏移量开始的段的长度。于是整个bi_io_vec结构体数组就可以表示一个完整的缓冲区。
根据以上数据结构的分析,定义一个while循环体来遍历当前正在处理的request请求的bio结构体链表,在while循环体内部定义一个for循环来遍历bio结构体中的bio_vec结构体数组:
/******************************************************
*
* 磁盘块设备数据请求的处理函数
*
******************************************************/
static void simp_blkdev_do_request(struct request_queue *q){
struct request *req;// 正在处理的请求队列中的请求
struct bio *req_bio;// 当前请求的bio
struct bio_vec *bvec;// 当前请求的bio的段(segment)链表
char *disk_mem; // 需要读/写的磁盘区域
char *buffer; // 磁盘块设备的缓冲区
int i = 0;
while((req = blk_fetch_request(q)) != NULL){
// 判断当前req是否合法
if((blk_rq_pos(req)<<SECTOR_SIZE_SHIFT) + blk_rq_bytes(req) > SIMP_BLKDEV_BYTES){
printk(KERN_ERR SIMP_BLKDEV_DISKNAME":bad request:block=%llu, count=%u\n",(unsigned long long)blk_rq_pos(req),blk_rq_sectors(req));
blk_end_request_all(req, -EIO);
continue;
}
//获取需要操作的内存位置
disk_mem = simp_blkdev_data + (blk_rq_pos(req) << SECTOR_SIZE_SHIFT);
req_bio = req->bio;// 获取当前请求的bio
switch (rq_data_dir(req)) { //判断请求的类型
case READ:
// 遍历req请求的bio链表
while(req_bio != NULL){
// for循环处理bio结构中的bio_vec结构体数组(bio_vec结构体数组代表一个完整的缓冲区)
for(i=0; i<req_bio->bi_vcnt; i++){
bvec = &(req_bio->bi_io_vec[i]);
buffer = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(buffer, disk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
disk_mem += bvec->bv_len;
}
req_bio = req_bio->bi_next;
}
__blk_end_request_all(req, 0);
break;
case WRITE:
while(req_bio != NULL){
for(i=0; i<req_bio->bi_vcnt; i++){
bvec = &(req_bio->bi_io_vec[i]);
buffer = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(disk_mem, buffer, bvec->bv_len);
kunmap(bvec->bv_page);
disk_mem += bvec->bv_len;
}
req_bio = req_bio->bi_next;
}
__blk_end_request_all(req, 0);
break;
default:
/* No default because rq_data_dir(req) is 1 bit */
break;
}
}
}
三、完整代码:
simp_blkdev.c 文件的内容:
#include <linux/module.h>
#include <linux/blkdev.h>
#define SIMP_BLKDEV_DISKNAME "simp_blkdev" //块设备名
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR //主设备号
#define SIMP_BLKDEV_BYTES (50*1024*1024) // 块设备大小为50MB
#define SECTOR_SIZE_SHIFT 9
static struct gendisk *simp_blkdev_disk;// gendisk结构表示一个简单的磁盘设备
static struct block_device_operations simp_blkdev_fops = { //块设备操作,gendisk的一个属性
.owner = THIS_MODULE,
};
static struct request_queue *simp_blkdev_queue;//指向块设备请求队列的指针
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];// 虚拟磁盘块设备的存储空间
/******************************************************
*
* 磁盘块设备数据请求的处理函数
*
******************************************************/
static void simp_blkdev_do_request(struct request_queue *q){
struct request *req;// 正在处理的请求队列中的请求
struct bio *req_bio;// 当前请求的bio
struct bio_vec *bvec;// 当前请求的bio的段(segment)链表
char *disk_mem; // 需要读/写的磁盘区域
char *buffer; // 磁盘块设备的请求在内存中的缓冲区
int i = 0;
while((req = blk_fetch_request(q)) != NULL){
// 判断当前req是否合法
if((blk_rq_pos(req)<<SECTOR_SIZE_SHIFT) + blk_rq_bytes(req) > SIMP_BLKDEV_BYTES){
printk(KERN_ERR SIMP_BLKDEV_DISKNAME":bad request:block=%llu, count=%u\n",(unsigned long long)blk_rq_pos(req),blk_rq_sectors(req));
blk_end_request_all(req, -EIO);
continue;
}
//获取需要操作的内存位置
disk_mem = simp_blkdev_data + (blk_rq_pos(req) << SECTOR_SIZE_SHIFT);
req_bio = req->bio;// 获取当前请求的bio
switch (rq_data_dir(req)) { //判断请求的类型
case READ:
// 遍历req请求的bio链表
while(req_bio != NULL){
// for循环处理bio结构中的bio_vec结构体数组(bio_vec结构体数组代表一个完整的缓冲区)
for(i=0; i<req_bio->bi_vcnt; i++){
bvec = &(req_bio->bi_io_vec[i]);
buffer = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(buffer, disk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
disk_mem += bvec->bv_len;
}
req_bio = req_bio->bi_next;
}
__blk_end_request_all(req, 0);
break;
case WRITE:
while(req_bio != NULL){
for(i=0; i<req_bio->bi_vcnt; i++){
bvec = &(req_bio->bi_io_vec[i]);
buffer = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(disk_mem, buffer, bvec->bv_len);
kunmap(bvec->bv_page);
disk_mem += bvec->bv_len;
}
req_bio = req_bio->bi_next;
}
__blk_end_request_all(req, 0);
break;
default:
/* No default because rq_data_dir(req) is 1 bit */
break;
}
}
}
/******************************************************
*
* 模块的入口函数
*
******************************************************/
static int __init simp_blkdev_init(void){
int ret;
//1.添加设备之前,先申请设备的资源
simp_blkdev_disk = alloc_disk(1);
if(!simp_blkdev_disk){
ret = -ENOMEM;
goto err_alloc_disk;
}
//2.设置设备的有关属性(设备名,设备号,fops指针,请求队列,512B的扇区数)
strcpy(simp_blkdev_disk->disk_name,SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;
// 将块设备请求处理函数的地址传入blk_init_queue函数,初始化一个请求队列
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
if(!simp_blkdev_queue){
ret = -ENOMEM;
goto err_init_queue;
}
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
//3.入口处添加磁盘块设备
add_disk(simp_blkdev_disk);
return 0;
err_alloc_disk:
return ret;
err_init_queue:
return ret;
}
/******************************************************
*
* 模块的出口函数
*
******************************************************/
static void __exit simp_blkdev_exit(void){
del_gendisk(simp_blkdev_disk);// 释放磁盘块设备
put_disk(simp_blkdev_disk); // 释放申请的设备资源
blk_cleanup_queue(simp_blkdev_queue);// 清除请求队列
}
module_init(simp_blkdev_init);// 声明模块的入口
module_exit(simp_blkdev_exit);// 声明模块的出口
Makefile 文件的内容:
ifeq ($(KERNELRELEASE),)
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *.ko .depend *.mod.o *.mod.c Module.* modules.*
.PHONY:modules modules_install clean
else
obj-m := simp_blkdev.o
endif
版本3在Linux4.13.0版本下编译通过:
1. 块设备驱动模块插入成功:
2. 在块设备上创建ext3文件系统:
3. 将该块设备挂载到/mnt/temp1目录下:
4. 向该块设备中存入一些文件:
添加文件之后,查看块设备的存储资源使用情况:
删除块设备中的所有文件,块设备存储资源使用量变小:
5. 取消块设备的挂载之后,对该块设备的引用从1变为0:
6. 移除该块设备驱动模块:


本文深入解析块设备的基本概念,包括块设备、扇区、块和段等,并详细介绍了通用块设备层的工作原理及其数据结构。此外,还展示了不同内核版本下的请求处理函数实现,并提供了一段完整的块设备驱动代码。
612





