当前位置: 首页 > ai >正文

[Linux] Linux标准块设备驱动详解:从原理到实现

Linux标准块设备驱动详解:从原理到实现

在Linux系统中,块设备是存储系统的核心组成部分,涵盖了硬盘、固态硬盘(SSD)、U盘、SD卡等各类持久化存储介质。与字符设备不同,块设备以固定大小的“块”为单位进行数据读写,支持随机访问,并通过复杂的I/O调度机制提升性能和设备寿命。本文将深入剖析Linux块设备驱动的架构、核心数据结构、注册流程及请求处理机制,并通过一个完整的基于内存的RAM磁盘驱动示例,帮助开发者掌握块设备驱动开发的关键技术。

文章目录

  • Linux标准块设备驱动详解:从原理到实现
    • 一、块设备概述:理解I/O模型的本质差异
    • 二、核心数据结构解析
      • 1. `block_device_operations`:设备操作接口
      • 2. `gendisk`:磁盘设备的抽象
    • 三、驱动注册与注销流程详解
      • 1. 注册流程
      • 2. 注销流程
    • 四、I/O请求处理机制
      • 1. 核心组件
      • 2. 多队列(blk-mq)处理模式
        • 定义多队列操作集
        • 请求处理函数示例
    • 五、完整示例:基于内存的RAM磁盘驱动
      • 编译与测试
    • 六、关键要点总结
    • 结语


一、块设备概述:理解I/O模型的本质差异

在Linux设备模型中,设备主要分为字符设备、块设备和网络设备三类。其中,块设备(Block Device) 的显著特征是:

  • 以块为单位传输数据:通常以512字节或4KB为基本单位(扇区),即使应用层请求非对齐数据,内核也会自动进行填充和裁剪。
  • 支持随机访问:可以任意读写任意扇区,无需按顺序操作。
  • 使用缓冲区缓存(Buffer Cache):内核通过Page Cache和Buffer Head机制缓存频繁访问的数据,减少对物理设备的直接访问,提升性能并延长设备寿命(尤其是SSD)。
  • 依赖I/O调度器:内核提供多种I/O调度算法(如CFQ、Deadline、NOOP、BFQ),用于合并相邻请求、优化请求顺序,降低磁头寻道时间或提升SSD的并行性。

与之对比,字符设备(如串口、键盘)通常以字节流方式工作,不经过块层调度,也不支持随机访问。因此,块设备驱动需要更复杂的软件栈来处理请求的排队、合并、调度和完成通知。


二、核心数据结构解析

Linux内核通过一组关键数据结构来抽象和管理块设备。掌握这些结构是编写块设备驱动的基础。

1. block_device_operations:设备操作接口

该结构体定义了用户空间与块设备交互的操作接口,类似于字符设备中的file_operations

struct block_device_operations {int (*open)(struct block_device *bdev, fmode_t mode);void (*release)(struct gendisk *disk, fmode_t mode);int (*ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);int (*compat_ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);int (*revalidate_disk)(struct gendisk *disk);int (*getgeo)(struct block_device *bdev, struct hd_geometry *geo);void (*swap_slot_free_notify)(struct block_device *, unsigned long);struct module *owner;
};
  • open / release:设备打开和关闭时的回调,用于初始化硬件或释放资源。
  • ioctl:处理设备特定的控制命令,例如获取磁盘几何信息(CHS)、执行设备诊断等。
  • getgeo:返回磁盘的物理几何参数(柱面、磁头、扇区),主要用于兼容旧系统。
  • owner:指向所属模块,防止模块在使用中被卸载。

注意:现代驱动中,openrelease通常为空,因为块设备的打开由内核自动管理。


2. gendisk:磁盘设备的抽象

gendisk结构体代表一个完整的磁盘设备,包括主设备和所有分区。

struct gendisk {int major;                  // 主设备号int first_minor;            // 起始次设备号int minors;                 // 支持的分区数量(1表示无分区)char disk_name[32];         // 设备名称,如 "myblk"struct block_device_operations *fops;  // 操作函数集struct request_queue *queue;          // 请求队列sector_t capacity;          // 容量(以512字节扇区为单位)struct disk_part_tbl *part_tbl;       // 分区表struct hd_struct part0;     // 主设备信息// 其他成员...
};

关键操作流程

  1. 分配:使用 alloc_disk(minors) 动态分配一个gendisk对象。
  2. 初始化:设置设备号、名称、操作函数、请求队列和容量。
  3. 设置容量:通过 set_capacity(disk, sectors) 指定设备总扇区数。例如,1MB内存磁盘对应:
    set_capacity(disk, (1 * 1024 * 1024) / 512); // = 2048 扇区
    
  4. 注册:调用 add_disk(disk) 将设备注册到内核,此后设备节点(如 /dev/myblk)将自动出现在/dev目录下。

重要提示:一旦调用add_disk(),驱动必须确保设备可正常响应I/O请求,否则可能导致系统挂起。


三、驱动注册与注销流程详解

块设备驱动的生命周期管理涉及设备号分配、磁盘对象初始化和内核注册。

1. 注册流程

static dev_t dev_num;  // 设备号
static struct gendisk *disk;
static struct request_queue *queue;static int __init myblk_init(void)
{int ret;// 1. 动态分配设备号ret = register_blkdev(0, "myblk");if (ret <= 0) {printk(KERN_ERR "Failed to register block device\n");return -EIO;}dev_num = MKDEV(ret, 0);  // 主设备号由内核返回// 2. 分配并初始化gendiskdisk = alloc_disk(1);  // 支持1个分区if (!disk) {unregister_blkdev(MAJOR(dev_num), "myblk");return -ENOMEM;}disk->major = MAJOR(dev_num);disk->first_minor = 0;strcpy(disk->disk_name, "myblk");disk->fops = &my_blk_fops;           // 操作函数disk->queue = queue;                 // 请求队列set_capacity(disk, 2048);            // 1MB容量// 3. 注册到内核add_disk(disk);printk(KERN_INFO "myblk: Registered block device with major %d\n", MAJOR(dev_num));return 0;
}

2. 注销流程

static void __exit myblk_exit(void)
{if (disk) {del_gendisk(disk);           // 从内核移除设备put_disk(disk);              // 释放gendisk}if (queue) {blk_cleanup_queue(queue);    // 清理请求队列}unregister_blkdev(MAJOR(dev_num), "myblk");  // 释放设备号
}

注意del_gendisk()会阻止新的I/O请求进入,但不会等待正在进行的请求完成。因此,驱动应确保在调用此函数前所有请求已处理完毕。


四、I/O请求处理机制

块设备驱动的核心任务是处理来自文件系统的I/O请求。现代Linux内核采用多队列(Multi-Queue, blk-mq) 架构以提升多核系统的并发性能。

1. 核心组件

  • request_queue:请求队列,由blk_mq_init_queue()创建,管理所有待处理的I/O请求。
  • bio(Block I/O)结构体:描述一个I/O操作的基本单元,包含:
    • bi_sector:起始逻辑扇区号
    • bi_size:数据长度(字节)
    • bi_io_vec:指向bio_vec数组,描述分散/聚集(scatter-gather)的内存页
    • bi_end_io:完成回调函数

2. 多队列(blk-mq)处理模式

传统请求队列使用request_fn处理合并后的请求,而blk-mq直接处理bio,简化了驱动逻辑。

定义多队列操作集
static struct blk_mq_ops my_mq_ops = {.queue_rq = my_queue_rq,      // 核心请求处理函数.complete = my_complete_rq,   // 可选:完成回调
};
请求处理函数示例
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{struct request *req = bd->rq;struct bio *bio;sector_t sector = blk_rq_pos(req);unsigned int nr_bytes = blk_rq_bytes(req);// 遍历所有bio(支持合并请求)__rq_for_each_bio(bio, req) {void *data = bio_data(bio);unsigned int len = bio->bi_iter.bi_size;if (bio_data_dir(bio) == READ) {// 模拟读操作:从模拟存储区复制数据memcpy(data, disk_data + sector * 512, len);} else {// 模拟写操作memcpy(disk_data + sector * 512, data, len);}sector += len >> 9;  // 转换为扇区数(512B/sector)}// 标记请求完成blk_mq_end_request(req, BLK_STS_OK);return BLK_STS_OK;
}

说明blk_mq_end_request()会自动调用bio的完成回调并释放资源。


五、完整示例:基于内存的RAM磁盘驱动

以下是一个可编译加载的完整RAM磁盘驱动,模拟一个1MB的块设备。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/vmalloc.h>#define DEV_NAME        "myramdisk"
#define DISK_SIZE       (1 * 1024 * 1024)  // 1MBstatic dev_t dev_num;
static struct request_queue *queue;
static struct gendisk *disk;
static unsigned char *disk_data;// 请求处理函数
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{struct request *req = bd->rq;struct bio *bio;sector_t sector = blk_rq_pos(req);__rq_for_each_bio(bio, req) {void *data = bio_data(bio);unsigned int len = bio->bi_iter.bi_size;if (sector + (len >> 9) > DISK_SIZE / 512) {return BLK_STS_IOERR;  // 越界检查}if (bio_data_dir(bio) == READ) {memcpy(data, disk_data + sector * 512, len);} else {memcpy(disk_data + sector * 512, data, len);}sector += len >> 9;}blk_mq_end_request(req, BLK_STS_OK);return BLK_STS_OK;
}// 多队列操作集
static struct blk_mq_ops my_mq_ops = {.queue_rq = my_queue_rq,
};// 模块初始化
static int __init myramdisk_init(void)
{int ret;// 1. 分配设备号ret = register_blkdev(0, DEV_NAME);if (ret < 0) return ret;dev_num = MKDEV(ret, 0);// 2. 分配模拟存储空间disk_data = vmalloc(DISK_SIZE);if (!disk_data) {unregister_blkdev(MAJOR(dev_num), DEV_NAME);return -ENOMEM;}memset(disk_data, 0, DISK_SIZE);// 3. 初始化请求队列queue = blk_mq_init_sq_queue(&tag_set, &my_mq_ops, 0, BLK_MQ_F_SHOULD_MERGE);if (IS_ERR(queue)) {vfree(disk_data);unregister_blkdev(MAJOR(dev_num), DEV_NAME);return PTR_ERR(queue);}// 4. 分配并初始化gendiskdisk = alloc_disk(1);if (!disk) {blk_cleanup_queue(queue);vfree(disk_data);unregister_blkdev(MAJOR(dev_num), DEV_NAME);return -ENOMEM;}disk->major = MAJOR(dev_num);disk->first_minor = 0;strcpy(disk->disk_name, DEV_NAME);disk->fops = &my_fops;disk->queue = queue;set_capacity(disk, DISK_SIZE / 512);disk->private_data = NULL;// 5. 注册设备add_disk(disk);printk(KERN_INFO "%s: RAM disk initialized (%d MB)\n", DEV_NAME, DISK_SIZE >> 20);return 0;
}// 模块退出
static void __exit myramdisk_exit(void)
{if (disk) {del_gendisk(disk);put_disk(disk);}if (queue) {blk_cleanup_queue(queue);}if (disk_data) {vfree(disk_data);}unregister_blkdev(MAJOR(dev_num), DEV_NAME);printk(KERN_INFO "%s: unloaded\n", DEV_NAME);
}module_init(myramdisk_init);
module_exit(myramdisk_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple RAM block device driver");

编译与测试

  1. 编译模块

    make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
    
  2. 加载模块

    sudo insmod myramdisk.ko
    
  3. 验证设备

    ls /dev/myramdisk
    dmesg | tail
    
  4. 格式化并挂载

    sudo mkfs.ext4 /dev/myramdisk
    sudo mkdir /mnt/ramdisk
    sudo mount /dev/myramdisk /mnt/ramdisk
    

六、关键要点总结

  1. 设备号管理:使用register_blkdev(0, ...)实现主设备号动态分配,避免冲突。
  2. 多队列优先:现代驱动应使用blk-mq架构,直接处理bio,提高并发性能。
  3. 内存分配:大容量设备应使用vmalloc而非kmalloc,避免内存碎片。
  4. 错误处理:在queue_rq中进行边界检查,返回适当的blk_status_t
  5. 生命周期同步:确保del_gendisk()调用前无活跃I/O,防止内存访问错误。
  6. 性能优化:合理配置队列深度、硬件上下文数,启用I/O调度器(如Deadline用于SSD)。

结语

Linux块设备驱动是连接上层文件系统与底层存储硬件的桥梁。通过理解gendiskrequest_queuebio等核心结构,掌握blk-mq请求处理机制,开发者可以构建高效、稳定的存储驱动。本文的RAM磁盘示例为学习和调试提供了基础框架,实际开发中可将其扩展为支持真实硬件(如PCIe SSD、NAND控制器)的复杂驱动。

更多细节可参考内核源码树中的drivers/block/目录,如brd.c(RAM磁盘)、null_blk.c(空设备)等经典实现。


研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)


http://www.xdnf.cn/news/19979.html

相关文章:

  • 2025年数学建模国赛E题超详细解题思路
  • 【读书笔记】《好奇心》
  • Spring Cloud LoadBalancer 核心原理
  • 开关电源——只需这三个阶段,从电源小白到维修大神
  • 什么是基于AI的智能RPA?
  • 传统装修行业数字化转型:如何通过GEO工具实现300%业绩增长?
  • QT面经(含相关知识)
  • 【面试题】如何构造排序模型训练数据?解决正负样本不均?
  • 机器学习中决策树
  • LeetCode 48 - 旋转图像算法详解(全网最优雅的Java算法
  • 安全与效率兼得:工业控制系统如何借力数字孪生实现双赢?
  • CPTS-Manager ADCS ESC7利用
  • HTML图片标签及路径详解
  • 代码随想录训练营第三十一天|LeetCode56.合并区间、LeetCode738.单调递增的数字
  • freertos下printf(“hello\r\n“)和printf(“hello %d\r\n“,i)任务堆栈消耗有何区别
  • 金贝 KA Box 1.18T:一款高效能矿机的深度解析
  • Python 第三方自定义库开发与使用教程
  • Redis是单线程的,为啥那么快呢?经典问题
  • 第六章 Cesium 实现简易河流效果
  • 热计量表通过M-Bus接口实现无线集抄系统的几种解决方
  • 2025国赛C题题目及最新思路公布!
  • ubuntu20.04配置运行ODM2.9.2教程,三维重建,OpenDroneMap/ODM2.9.2
  • 智能家居芯片:技术核心与创新突破
  • Spring Cloud Ribbon 核心原理
  • 数字化转型:从锦上添花到生存必需——2025年零售行业生存之道
  • Function Call实战:用GPT-4调用天气API,实现实时信息查询
  • Matlab中的积分——函数int()和quadl()
  • PDF24 Creator:免费的多功能PDF工具
  • OPC UA双层安全认证模型解析
  • 【蓝桥杯选拔赛真题64】C++最大空白区 第十四届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解