一、一些概念
1.Linux内核的块子系统(Block Subsystem) 是连接文件系统层和块设备驱动层的核心中间层,也常被称为 "块 I/O 子系统"。它的核心使命是:标准化、调度、优化所有块设备的 I/O 请求,向上为文件系统提供统一的扇区读写接口,向下为驱动层提供标准化的请求封装和分发机制。
2.调度层级:
用户层 read/write → 块设备层(VFS + 通用块层) → 调度合并 → 驱动程序 → 硬件(磁盘/Flash/模拟内存)。
1)用户层
sudo dd if=/dev/mybd of=/dev/null count=1 # 读
sudo dd if=/dev/zero of=/dev/mybd count=1 # 写
2)VFS虚拟文件系统
用户层调用open,内核找到并识别对应的块设备节点(例如/dev/mybd),然后进入通用块层。
3)通用块层(内核核心)
--把read/write转换成BIO;
--把BIO合并成Request(request);
--把Request放进调度队列;
--把Request派发给驱动的queue_rq。
4)blk-mq多队列层(Linux5.x/6.x标准,不同内核版本可能API接口不太一样)
内核会管理队列、分配请求最终调用驱动中的my_queue_rq()。
5)在my_queue_rq()中编写操作驱动代码
所有读写最终都会进入这个函数,这也是块驱动的唯一I/O入口。
6)返回给用户层(形成闭环)
驱动完成 → 通知块层 → 通知 VFS → 用户层完成退出。
二、关键结构体及函数接口
1.关键结构体:
1)bio:块I/O的最小单元,描述 "一次扇区读写请求"(包含起始扇区、长度、读写方向、数据缓冲区);2)request:一个读写请求,由多个连续的bio合并而成的 ”批量请求"(减少驱动操作次数);3)request_queue:驱动的 "请求队列",块子系统将bio/request放入队列,驱动从队列取请求处理;4)gendisk:块设备的 "身份标识",描述设备的主/次设备号、扇区数、请求队列等信息;5)blk_mq_ops:驱动操作集。
2.关键函数接口:
1)blk_mq_alloc_tag_set():分配blk-mq多队列标签集,必须调用,块驱动的起点;2)blk_mq_alloc_disk():分配gendisk磁盘结构 + 绑定队列;3)add_disk():注册磁盘到内核,自动生成 /dev/xxx;4)del_gendisk/put_disk():卸载磁盘;5)blk_mq_free_tag_set():释放队列资源。
三、测试
1.源码
my_blockdev.c
#include<linux/module.h>#include<linux/blkdev.h>#include<linux/blk-mq.h>#include<linux/hdreg.h>#define MYBD_SECTORS 1024static struct gendisk *mybd_disk;static struct blk_mq_tag_set *mybd_tagset;/* 读写请求处理函数 */staticblk_status_tmybd_queue_rq(struct blk_mq_hw_ctx *hctx, conststruct blk_mq_queue_data *bd){ struct request *rq = bd->rq; pr_info("mybd_queue_rq\n"); blk_mq_start_request(rq); blk_mq_end_request(rq, BLK_STS_OK); return BLK_STS_OK;}static const struct blk_mq_ops mybd_mq_ops = { .queue_rq = mybd_queue_rq,};/* 打开设备 */staticintmybd_open(struct gendisk *disk, blk_mode_t mode){ pr_info("mybd: open\n"); return 0;}/* 关闭设备 */staticvoidmybd_release(struct gendisk *disk){ pr_info("mybd: release\n");}/* 磁盘几何信息(必须实现,否则内核不认) */staticintmybd_getgeo(struct block_device *bdev, struct hd_geometry *geo){ geo->heads = 1; geo->sectors = 32; geo->cylinders = MYBD_SECTORS / (1 * 32); geo->start = 0; return 0;}static const struct block_device_operations mybd_fops = { .owner = THIS_MODULE, .open = mybd_open, .release = mybd_release, .getgeo = mybd_getgeo, // 关键!必须有};staticint __init mybd_init(void){ int ret; /* 分配 tag set */ mybd_tagset = kzalloc(sizeof(*mybd_tagset), GFP_KERNEL); if (!mybd_tagset) return -ENOMEM; mybd_tagset->ops = &mybd_mq_ops; mybd_tagset->nr_hw_queues = 1; mybd_tagset->queue_depth = 64; mybd_tagset->numa_node = NUMA_NO_NODE; ret = blk_mq_alloc_tag_set(mybd_tagset); if (ret) goto free_tagset; /* 分配磁盘 */ mybd_disk = blk_mq_alloc_disk(mybd_tagset, NULL); if (!mybd_disk) { ret = -ENOMEM; goto cleanup_tags; } /* 设置磁盘信息 */ mybd_disk->fops = &mybd_fops; mybd_disk->flags = GENHD_FL_NO_PART; // 无分区 snprintf(mybd_disk->disk_name, DISK_NAME_LEN, "mybd"); set_capacity(mybd_disk, MYBD_SECTORS); /* 注册磁盘 → 自动生成 /dev/mybd */ add_disk(mybd_disk); pr_info("mybd: loaded\n"); return 0;cleanup_tags: blk_mq_free_tag_set(mybd_tagset);free_tagset: kfree(mybd_tagset); return ret;}staticvoid __exit mybd_exit(void){ del_gendisk(mybd_disk); put_disk(mybd_disk); blk_mq_free_tag_set(mybd_tagset); kfree(mybd_tagset); pr_info("mybd: unloaded\n");}module_init(mybd_init);module_exit(mybd_exit);MODULE_AUTHOR("AXUN");MODULE_LICENSE("GPL");
2.Makefile
CC = gcc-12 # 新增:指定编译器为gcc-12obj-m += my_blockdev.o # 要编译的模块名(对应 my_blockdev.c)KERNELDIR ?= /lib/modules/$(shell uname -r)/build #内核源码路径PWD := $(shell pwd) #当前目录all:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules #编译模块clean:$(MAKE) -C $(KERNELDIR) M=$(PWD) clean #清理编译产物
3.编译与加载
#编译驱动
make
#加载模块
sudo insmod my_blockdev.ko
#查看设备号
ls /dev/mybd -l
#读写磁盘
sudo dd if=/dev/mybd of=/dev/null count=1 # 读
sudo dd if=/dev/zero of=/dev/mybd count=1 # 写
四、测试平台及使用的Linux版本