1. Linux块设备驱动概述
块设备驱动是Linux内核中负责管理块设备的核心组件,它充当了操作系统与物理存储设备之间的桥梁。与字符设备不同,块设备具有随机访问能力,数据以固定大小的块为单位进行读写。典型的块设备包括机械硬盘、固态硬盘(SSD)、U盘等。
块设备驱动的主要职责包括:
- 将文件系统的读写请求转换为硬件设备能理解的指令
- 管理设备的I/O操作队列
- 处理设备的错误和异常情况
- 提供设备的状态和控制接口
在Linux系统中,块设备通常以文件形式出现在/dev目录下,如/dev/sda、/dev/nvme0n1等。用户和应用程序可以通过这些设备文件访问底层存储设备。
注意:块设备与字符设备的关键区别在于数据访问方式。字符设备以字节流形式操作,而块设备必须按固定大小的块(通常为512字节或4KB)进行读写。
2. 存储介质结构与工作原理
2.1 机械硬盘(HDD)结构
机械硬盘由多个高速旋转的盘片和可移动的磁头组成,数据存储在盘片的磁性表面上。主要结构单元包括:
- 扇区(Sector):盘片上最小的可寻址单元,传统硬盘通常为512字节,现代硬盘多为4KB
- 磁道(Track):盘片表面同一半径上的同心圆,由磁头划过形成
- 柱面(Cylinder):所有盘片相同半径的磁道组成的圆柱面
机械硬盘的访问时间主要由三部分组成:
- 寻道时间(Seek Time):磁头移动到目标磁道所需时间
- 旋转延迟(Rotational Latency):盘片旋转到目标扇区所需时间
- 传输时间(Transfer Time):实际读写数据所需时间
2.2 NAND闪存(SSD)结构
NAND闪存是固态硬盘的核心存储介质,其工作原理与机械硬盘完全不同:
- 数据存储在由浮栅晶体管构成的存储单元中
- 通过电荷的有无表示二进制数据(1或0)
- 基本操作单元:
- 页(Page):最小的读写单元,通常4KB
- 块(Block):最小的擦除单元,通常由128-256页组成
NAND闪存的特点:
- 没有机械部件,访问速度快
- 写入前必须先擦除
- 每个存储单元有擦写次数限制(通常3000-10000次)
- 需要专门的闪存转换层(FTL)管理
2.3 eMMC存储结构
eMMC(嵌入式多媒体卡)是一种集成了NAND闪存和控制器的封装存储解决方案,广泛应用于移动设备和嵌入式系统:
- NAND闪存阵列:实际存储数据的介质
- 控制器:管理闪存操作,包括:
- 坏块管理
- 磨损均衡
- 错误校验与纠正
- 垃圾回收
- 标准接口:简化了与主机的连接
eMMC的主要优势在于其高度集成化和简化的设计,使设备制造商可以轻松地将存储集成到产品中。
3. Linux存储子系统架构
3.1 文件系统层
文件系统是操作系统用于管理存储设备上数据的机制,主要功能包括:
- 组织和存储文件与目录
- 管理文件元数据(权限、时间戳等)
- 实现数据的持久化存储
- 提供文件访问接口
Linux支持多种文件系统类型:
- 传统文件系统:ext2/ext3/ext4
- 日志文件系统:XFS、JFS
- 网络文件系统:NFS、CIFS
- 特殊用途文件系统:procfs、sysfs
3.2 虚拟文件系统(VFS)
VFS是Linux内核中的一个抽象层,它为不同的文件系统提供统一的接口:
- 抽象文件操作:提供统一的open、read、write等系统调用接口
- 支持多文件系统:允许不同文件系统共存并透明访问
- 统一路径解析:处理路径名到具体文件的映射
- 性能优化:实现目录项缓存(dcache)和inode缓存
VFS的核心数据结构包括:
- super_block:描述已挂载的文件系统
- inode:描述文件系统对象(文件、目录等)
- dentry:目录项,用于路径名解析
- file:描述进程打开的文件
3.3 块设备驱动层
块设备驱动位于存储栈的最底层,直接与硬件交互:
- 设备文件接口:通过/dev下的设备文件提供用户空间访问
- 请求队列管理:使用I/O调度器优化请求顺序
- DMA传输:实现高效的数据传输
- 错误处理:检测并处理设备错误
Linux块设备驱动的核心数据结构:
- gendisk:描述一个磁盘设备
- request_queue:管理I/O请求队列
- bio:描述块I/O操作的基本单元
- request:包含一个或多个bio的请求
4. 块设备驱动核心实现
4.1 设备注册与注销
块设备驱动需要向内核注册才能被识别和使用:
c复制// 注册块设备
int register_blkdev(unsigned int major, const char *name);
// 注销块设备
int unregister_blkdev(unsigned int major, const char *name);
典型用法:
c复制#define DEVICE_NAME "my_block_device"
static int major_num;
// 模块初始化时注册
major_num = register_blkdev(0, DEVICE_NAME); // 0表示自动分配主设备号
if (major_num < 0) {
// 错误处理
}
// 模块退出时注销
unregister_blkdev(major_num, DEVICE_NAME);
4.2 gendisk结构体操作
gendisk结构体代表内核中的一个磁盘设备,相关操作函数:
c复制// 分配gendisk结构体
struct gendisk *alloc_disk(int minors);
// 释放gendisk
void put_disk(struct gendisk *disk);
// 将gendisk添加到系统
void add_disk(struct gendisk *disk);
// 从系统移除gendisk
void del_gendisk(struct gendisk *disk);
// 设置磁盘容量(以扇区为单位)
void set_capacity(struct gendisk *disk, sector_t size);
4.3 请求队列管理
现代Linux内核使用blk-mq(多队列)框架管理块设备请求:
c复制// 初始化单队列
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
const struct blk_mq_ops *ops,
unsigned int queue_depth,
unsigned int set_flags);
// 清理请求队列
void blk_cleanup_queue(struct request_queue *q);
请求队列操作函数:
c复制// 标记请求开始处理
void blk_mq_start_request(struct request *rq);
// 标记请求处理完成
void blk_mq_end_request(struct request *rq, blk_status_t error);
// 更新请求状态
bool blk_update_request(struct request *rq, blk_status_t error,
unsigned int nr_bytes);
4.4 BIO结构体操作
BIO(Block I/O)是Linux块I/O操作的基本单元:
c复制// 获取BIO数据缓冲区指针
void *bio_data(struct bio *bio);
// 获取请求方向(READ/WRITE)
rq_data_dir(struct request *req);
// 获取请求起始扇区
blk_rq_pos(struct request *req);
// 获取当前请求段数据长度
blk_rq_cur_bytes(struct request *req);
// 获取请求总数据长度
blk_rq_bytes(struct request *req);
5. I/O调度算法详解
Linux内核提供了多种I/O调度算法,针对不同设备特性进行优化:
5.1 Noop调度器
最简单的调度算法,特点:
- 仅进行基本的请求合并
- 不进行排序操作
- 适合无需寻道的设备(如SSD)
- 开销最小,延迟最低
5.2 CFQ(完全公平队列)调度器
设计目标是为所有进程提供公平的I/O带宽:
- 为每个进程维护独立的请求队列
- 使用时间片轮转方式调度队列
- 支持I/O优先级控制
- 适合多用户系统
5.3 Deadline调度器
专注于减少I/O请求的延迟:
- 维护四个队列(读/写各两个)
- 为每个请求设置截止时间
- 优先处理即将超时的请求
- 防止请求饿死
- 适合数据库等低延迟应用
5.4 Kyber调度器
专为快速设备(如NVMe SSD)设计:
- 将I/O分为读、写、丢弃等类别
- 使用令牌桶算法控制各类别I/O比例
- 自动调整队列深度
- 减少延迟波动
5.5 BFQ(预算公平队列)调度器
改进的公平调度器:
- 为每个进程分配I/O预算(字节数)
- 预算耗尽后暂停该进程的I/O
- 提供更精确的带宽控制
- 适合交互式系统和虚拟机
6. 虚拟块设备驱动实现实例
下面是一个完整的内存虚拟块设备驱动实现,模拟2MB大小的磁盘:
6.1 设备结构体定义
c复制#include <linux/blkdev.h>
#include <linux/fs.h>
#include <linux/genhd.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
#define VIRTUAL_DISK_NAME "virtual_disk"
#define VIRTUAL_DISK_SIZE (2*1024*1024) // 2MB
#define VIRTUAL_DISK_MINOR 5
struct virtual_disk_dev {
int major;
unsigned char *data; // 虚拟磁盘数据缓冲区
struct blk_mq_tag_set tag_set; // blk-mq标签集
struct request_queue *queue; // 请求队列
struct gendisk *gendisk; // 通用磁盘结构
spinlock_t lock; // 自旋锁
};
6.2 数据传输函数
c复制static int virtual_disk_transfer(struct request *req)
{
struct virtual_disk_dev *dev = req->rq_disk->private_data;
unsigned long start = blk_rq_pos(req) << 9; // 扇区转字节
unsigned long len = blk_rq_cur_bytes(req);
void *buffer = bio_data(req->bio);
// 边界检查
if ((start + len) > VIRTUAL_DISK_SIZE) {
len = VIRTUAL_DISK_SIZE - start;
}
if (rq_data_dir(req) == READ) {
memcpy(buffer, dev->data + start, len);
} else {
memcpy(dev->data + start, buffer, len);
}
return 0;
}
6.3 请求处理函数
c复制static blk_status_t virtual_disk_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *req = bd->rq;
struct virtual_disk_dev *dev = req->rq_disk->private_data;
int ret = 0;
blk_mq_start_request(req);
spin_lock(&dev->lock);
do {
ret = virtual_disk_transfer(req);
} while (blk_update_request(req, ret, blk_rq_cur_bytes(req)));
blk_mq_end_request(req, BLK_STS_OK);
spin_unlock(&dev->lock);
return BLK_STS_OK;
}
static struct blk_mq_ops mq_ops = {
.queue_rq = virtual_disk_queue_rq,
};
6.4 设备操作函数
c复制static int virtual_disk_open(struct block_device *bdev, fmode_t mode)
{
printk(KERN_INFO "Virtual disk opened\n");
return 0;
}
static void virtual_disk_release(struct gendisk *disk, fmode_t mode)
{
printk(KERN_INFO "Virtual disk closed\n");
}
static int virtual_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
geo->heads = 2;
geo->cylinders = 32;
geo->sectors = VIRTUAL_DISK_SIZE / (2 * 32 * 512);
return 0;
}
static struct block_device_operations virtual_disk_fops = {
.owner = THIS_MODULE,
.open = virtual_disk_open,
.release = virtual_disk_release,
.getgeo = virtual_getgeo,
};
6.5 模块初始化和退出
c复制static struct virtual_disk_dev *virtual_disk;
static int __init virtual_disk_init(void)
{
// 1. 分配设备结构体
virtual_disk = kzalloc(sizeof(*virtual_disk), GFP_KERNEL);
if (!virtual_disk)
return -ENOMEM;
// 2. 分配数据缓冲区
virtual_disk->data = vmalloc(VIRTUAL_DISK_SIZE);
if (!virtual_disk->data) {
kfree(virtual_disk);
return -ENOMEM;
}
// 3. 初始化自旋锁
spin_lock_init(&virtual_disk->lock);
// 4. 注册块设备
virtual_disk->major = register_blkdev(0, VIRTUAL_DISK_NAME);
if (virtual_disk->major < 0) {
vfree(virtual_disk->data);
kfree(virtual_disk);
return virtual_disk->major;
}
// 5. 初始化请求队列
virtual_disk->queue = blk_mq_init_sq_queue(&virtual_disk->tag_set, &mq_ops, 2,
BLK_MQ_F_SHOULD_MERGE);
if (!virtual_disk->queue) {
unregister_blkdev(virtual_disk->major, VIRTUAL_DISK_NAME);
vfree(virtual_disk->data);
kfree(virtual_disk);
return -ENOMEM;
}
// 6. 分配并设置gendisk
virtual_disk->gendisk = alloc_disk(VIRTUAL_DISK_MINOR);
if (!virtual_disk->gendisk) {
blk_cleanup_queue(virtual_disk->queue);
unregister_blkdev(virtual_disk->major, VIRTUAL_DISK_NAME);
vfree(virtual_disk->data);
kfree(virtual_disk);
return -ENOMEM;
}
virtual_disk->gendisk->major = virtual_disk->major;
virtual_disk->gendisk->first_minor = 0;
virtual_disk->gendisk->fops = &virtual_disk_fops;
virtual_disk->gendisk->private_data = virtual_disk;
virtual_disk->gendisk->queue = virtual_disk->queue;
sprintf(virtual_disk->gendisk->disk_name, VIRTUAL_DISK_NAME);
set_capacity(virtual_disk->gendisk, VIRTUAL_DISK_SIZE >> 9);
// 7. 激活磁盘
add_disk(virtual_disk->gendisk);
printk(KERN_INFO "Virtual disk initialized: major=%d\n", virtual_disk->major);
return 0;
}
static void __exit virtual_disk_exit(void)
{
del_gendisk(virtual_disk->gendisk);
put_disk(virtual_disk->gendisk);
blk_cleanup_queue(virtual_disk->queue);
unregister_blkdev(virtual_disk->major, VIRTUAL_DISK_NAME);
vfree(virtual_disk->data);
kfree(virtual_disk);
printk(KERN_INFO "Virtual disk unloaded\n");
}
module_init(virtual_disk_init);
module_exit(virtual_disk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple virtual block device driver");
7. 块设备驱动开发实践技巧
7.1 性能优化策略
-
合理设置队列参数:
- 根据设备特性调整队列深度
- 选择适合的I/O调度器
- 启用请求合并(BLK_MQ_F_SHOULD_MERGE)
-
减少锁竞争:
- 使用细粒度锁保护关键数据结构
- 考虑使用无锁算法
- 避免在持有锁的情况下进行耗时操作
-
DMA优化:
- 使用分散/聚集(scatter-gather)DMA
- 合理使用DMA映射API
- 考虑使用流式DMA映射
7.2 调试与问题排查
-
常用调试工具:
- blktrace:跟踪块层I/O请求
- iostat:监控设备I/O统计
- ftrace:内核函数跟踪
- printk:内核日志输出
-
常见问题及解决:
- I/O性能差:检查调度器设置、队列深度、DMA配置
- 数据损坏:验证DMA同步操作、内存屏障使用
- 设备不响应:检查中断处理、超时机制
- 内存泄漏:确保所有分配的资源都有对应的释放操作
7.3 兼容性考虑
-
处理不同扇区大小:
- 现代设备可能使用4KB扇区
- 检查并设置queue的物理块大小和逻辑块大小
-
支持高级功能:
- 考虑实现discard/TRIM支持
- 支持电源管理功能
- 实现可选的SCSI命令集
-
多队列支持:
- 现代高性能设备通常支持多队列
- 合理设计硬件上下文(hctx)数量
8. 实际开发中的经验分享
在开发实际的块设备驱动时,有几个关键点需要特别注意:
-
并发控制:
块设备驱动需要处理来自多个进程的并发I/O请求。除了使用自旋锁保护关键数据结构外,还需要注意:- 请求处理函数可能在不同CPU核心上并发执行
- 完成回调可能在中断上下文中执行
- 考虑使用引用计数管理资源生命周期
-
错误处理:
完善的错误处理是稳定驱动的基础:- 所有内存分配都需要检查返回值
- I/O操作需要处理超时和错误情况
- 提供合理的错误恢复机制
-
性能统计:
添加性能统计信息有助于优化和调试:- 记录I/O延迟分布
- 统计请求大小分布
- 跟踪队列深度变化
-
模块化设计:
将驱动分解为多个功能模块:- 设备探测和初始化
- I/O请求处理
- 中断处理
- 电源管理
- 错误处理
-
文档与注释:
详细的文档和代码注释对维护至关重要:- 记录硬件特性和限制
- 说明关键算法和设计决策
- 标注所有重要的并发约束
通过遵循这些实践原则,可以开发出高性能、稳定可靠的Linux块设备驱动。在实际项目中,建议参考内核源码中现有的高质量驱动实现,如drivers/block/null_blk.c等,学习其中的设计模式和实现技巧。