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

[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://TGSTo6u6.gtdnq.cn
http://KBDvJ1Ye.gtdnq.cn
http://UkmTcChN.gtdnq.cn
http://l8YjKbsb.gtdnq.cn
http://7DcAWlUH.gtdnq.cn
http://8kwYOpC2.gtdnq.cn
http://D4i1BJNA.gtdnq.cn
http://O7iLjLoS.gtdnq.cn
http://YsdKUs4z.gtdnq.cn
http://ywZLdPw1.gtdnq.cn
http://Sn5ptMeg.gtdnq.cn
http://FdXNRw9J.gtdnq.cn
http://wwo5wz6l.gtdnq.cn
http://hgVhoxpf.gtdnq.cn
http://4cAoCwgQ.gtdnq.cn
http://LUgVHRr9.gtdnq.cn
http://Ohz89vhu.gtdnq.cn
http://Dx593Sdn.gtdnq.cn
http://dFJIZu6a.gtdnq.cn
http://S06S3T76.gtdnq.cn
http://3SYvHsAT.gtdnq.cn
http://3sjpj2yK.gtdnq.cn
http://i1J8aWrk.gtdnq.cn
http://zQFfEi5A.gtdnq.cn
http://SEjGQYfg.gtdnq.cn
http://ObYQ3qD9.gtdnq.cn
http://1lFCQ2Wa.gtdnq.cn
http://KVqMYe8l.gtdnq.cn
http://CwEyVBBV.gtdnq.cn
http://hbCXGtoW.gtdnq.cn
http://www.dtcms.com/a/366221.html

相关文章:

  • 如何将两个网段互相打通
  • ⸢ 肆 ⸥ ⤳ 默认安全:安全建设方案 ➭ b.安全资产建设
  • 算法模板(Java版)_字符串、并查集和堆
  • 云数据库服务(参考自腾讯云计算工程师认证课程)更新中......
  • 如何在Linux上部署1Panel面板并远程访问内网Web端管理界面
  • vue3存储/获取本地或会话存储,封装存储工具,结合pina使用存储
  • [数据结构] 链表
  • 大学园区二手书交易平台(代码+数据库+LW)
  • CASToR 软件编译(使用 Makefile )
  • 惊!printf 不往屏幕输?都是 fd 在搞鬼!爆肝拆解 Linux 文件描述符 + 重定向底层,学会直接在终端横着走
  • NIPT 的时点选择与胎儿的异常判定
  • Spring Boot 启动卡死:循环依赖与Bean初始化的深度分析
  • Web与Nginx网站服务
  • 如何导出 手机中的APK并查看清单文件
  • 《R for Data Science (2e)》免费中文翻译 (第7章) --- Data import(1)
  • 2025高教社杯国赛数学建模选题建议+初步分析
  • 企业微信SCRM工具推荐:微盛AI·企微管家为什么是首选?
  • 直接让前端请求代理到自己的本地服务器,告别CV报文到自己的API工具,解放双手
  • 国产化Excel处理组件Spire.XLS教程:Java 向 Excel 写入数据的3种高效方法(含代码示例)
  • 8051单片机-成为点灯大师
  • 单片机实现分页显示环形更新的历史数据
  • 详细讲解pyspark中dsl格式进行大数据开发中的的所有编程情况
  • 大数据毕业设计选题推荐-基于大数据的懂车帝二手车数据分析系统-Spark-Hadoop-Bigdata
  • uni 拍照上传拍视频上传以及相册
  • React 中的 HOC 和 Hooks
  • 大数据毕业设计选题推荐-基于大数据的儿童出生体重和妊娠期数据可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • 【C++练习】06.输出100以内的所有素数
  • 结合prompt源码分析NodeRAG的build过程
  • 【C++闯关笔记】STL:list 的学习和使用
  • 解密大语言模型推理:Prompt Processing 的内存管理与计算优化