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.pswp.cn/bicheng/95767.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/95767.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/95767.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是压力测试,有哪些方法

压力测试&#xff08;Stress Testing&#xff09;是性能测试的一种&#xff0c;旨在评估系统在极端负载条件下的表现&#xff0c;验证其稳定性、可靠性和容错能力。通过模拟超出正常范围的并发用户、数据量或请求频率&#xff0c;发现系统在高负载下的瓶颈&#xff08;如内存泄…

lua脚本在redis中执行是否是原子性?

lua脚本在redis中执行是否是原子性&#xff1f;以及是否会阻塞其他脚本的执行【客户端的请求】&#xff1f;先解答第二个问题:是的&#xff0c;保持原子执行。这也是redis中支持lua脚本执行的原因。Lua 脚本在 Redis 中是以原子方式执行的&#xff0c;在 Redis 服务器执行EVAL命…

DeepSeek文献太多太杂?一招制胜:学术论文检索的“核心公式”与提问艺术

如果我们想要完成一次学术论文检索&#xff0c;那我们可以把它想象成一次精准的“学术寻宝”。你不是在漫无目的地闲逛&#xff0c;而是一名装备精良的“学术寻宝猎人”&#xff0c;你的目标是找到深藏在浩瀚文献海洋中的“珍宝”&#xff08;高价值论文&#xff09;。1 你的寻…

Linux内存管理章节一:深入浅出Linux内存管理:从物理内存到ARM32的用户与内核空间

引言 如果说操作系统是计算机的心脏&#xff0c;那么内存管理就是它的灵魂脉络。它默默地工作在Linux内核的最底层&#xff0c;却决定着整个系统的稳定性、安全性和性能。今天&#xff0c;我们将拨开迷雾&#xff0c;深入探索Linux内存管理的核心概念&#xff0c;并结合熟悉的A…

ECMAScript (5)ES6前端开发核心:国际化与格式化、内存管理与性能

好的&#xff0c;我将根据【国际化与格式化】和【内存管理与性能】这两个主题&#xff0c;为你生成详细的课件内容&#xff0c;涵盖概念、应用和实例。 &#x1f4d7; 前端开发核心&#xff1a;国际化与格式化、内存管理与性能 1. 国际化与格式化 (Internationalization & …

3D 可视化数字孪生运维管理平台:构建 “虚实协同” 的智慧运维新范式

3D 可视化数字孪生运维管理平台通过 “物理空间数字化建模 实时数据动态映射 智能分析决策”&#xff0c;将建筑、园区、工业设施等物理实体 1:1 复刻为虚拟孪生体&#xff0c;打破传统运维 “信息割裂、依赖经验、响应滞后” 的痛点&#xff0c;实现从 “被动抢修” 到 “主…

DP-观察者模式代码详解

观察者模式&#xff1a; 定义一系列对象之间的一对多关系&#xff1b;当一个对象改变状态&#xff0c;它的依赖都会被通知。 主要由主题&#xff08;Subject&#xff09;和观察者&#xff08;Observer&#xff09;组成。 代码实现 package com.designpatterns.observer;/*** 定…

1983:ARPANET向互联网的转变

一、ARPANET早期1969年诞生的ARPANET最初还算不上互联网&#xff0c;不过在ARPANET构建之初就已经考虑了分组交换&#xff1a;1970年代的ARPANET:其实这个时候我就有疑问&#xff0c;TCP/IP是1983年1月1日更新到ARPANET的&#xff0c;但是1970年代的ARPANET已经连接全美的重要单…

自动化运维-ansible中的变量运用

自动化运维-ansible中的变量运用 一、变量命名规则 组成&#xff1a;字母、数字、下划线。必须以字母开头。 合法: app_port, web_1, varA非法: 2_var (以数字开头), my-var (包含其他字符), _private (以下划线开头) 避免使用内置关键字&#xff1a;例如 hosts, tasks, name…

深入学习并发编程中的volatile

volatile 的作用 保证变量的内存可见性禁止指令重排序1.保证此变量对所有的线程的可见性&#xff0c;当一个线程修改了这个变量的值&#xff0c;volatile 保证了新值能立即同步到主内存&#xff0c;其它线程每次使用前立即从主内存刷新。 但普通变量做不到这点&#xff0c;普通…

使用Java获取本地PDF文件并解析数据

获取本地文件夹下的PDF文件要获取本地文件夹下的PDF文件&#xff0c;可以使用Java的File类和FilenameFilter接口。以下是一个示例代码片段&#xff1a;import java.io.File; import java.io.FilenameFilter;public class PDFFileFinder {public static void main(String[] args…

吴恩达机器学习补充:决策树和随机森林

数据集&#xff1a;通过网盘分享的文件&#xff1a;sonar-all-data.csv 链接: https://pan.baidu.com/s/1D3vbcnd6j424iAwssYzDeQ?pwd12gr 提取码: 12gr 学习来源&#xff1a;https://github.com/cabin-w/MLBeginnerHub 文末有完整代码&#xff0c;由于这里的代码和之前的按…

Shell脚本一键监控平台到期时间并钉钉告警推送指定人

1. 监控需求客户侧有很多平台需要定期授权&#xff0c;授权后管理后台才可正常登录&#xff0c;为避免授权到期&#xff0c;现撰写脚本自动化监控平台授权到期时间&#xff0c;在到期前15天钉钉或其他媒介提醒。2. 监控方案2.1 收集平台信息梳理需要监控的平台地址信息&#xf…

华为HCIE数通含金量所剩无几?考试难度加大?

最近网上很火的一个梗——法拉利老了还是法拉利&#xff0c;这句话套在华为HCIE数通身上同样适用&#xff0c;华为认证中的华为数通和云计算两大巨头充斥着大家的视野里面&#xff0c;也更加广为人知&#xff0c;但随着时代的发展&#xff0c;华为认证体系的调整&#xff0c;大…

#数据结构----2.1线性表

在数据结构的学习中&#xff0c;线性表是最基础、最核心的结构之一 —— 它是后续栈、队列、链表等复杂结构的 “基石”。今天从 “是什么”&#xff08;定义&#xff09;到 “怎么用”&#xff08;基本操作&#xff09;&#xff0c;彻底搞懂线性表的核心逻辑。 一、先明确&…

2508C++,skia动画

gif动画原理 先了解一下gif动画的原理: gif动画由一系列静态图像(或叫帧)组成.这些图像按特定的顺序排列,每一帧都代表动画中的一个瞬间,帧图像是支持透明的. 每两帧之间有指定的时间间隔(一般小于60毫秒),gif播放器每渲染一帧静态图像后,即等待此时间间隔,依此逻辑不断循环渲染…

AI + 机器人:当大语言模型赋予机械 “思考能力”,未来工厂将迎来怎样变革?

一、引言1.1 未来工厂变革背景与趋势在科技飞速发展的当下&#xff0c;全球制造业正站在变革的十字路口。随着消费者需求日益多样化、市场竞争愈发激烈&#xff0c;传统工厂模式的弊端逐渐显现。生产效率低下、难以适应个性化定制需求、设备维护成本高昂且缺乏前瞻性等问题&…

pinia状态管理的作用和意义

1. 什么是状态管理 状态管理就是统一管理应用中的数据&#xff0c;让数据在多个组件之间共享和同步。 // 没有状态管理 - 数据分散在各个组件中 // 组件A const user ref({ name: 张三, age: 25 })// 组件B const user ref({ name: 张三, age: 25 }) // 重复定义// 组件C c…

十四、STM32-----低功耗

一、电源框图VDDA 供电区域&#xff0c;主要是 ADC 电源以及参考电压&#xff0c;STM32 的 ADC 模块配备独立的供电方 式&#xff0c;使用了 VDDA 引脚作为输入&#xff0c;使用 VSSA 引脚作为独立地连接&#xff0c;VREF 引脚为提供给 ADC 的 参考电压。电压调节器是 STM32 的…

一篇文章带你彻底搞懂 JVM 垃圾收集器

垃圾收集器是 JVM 内存管理的执行引擎&#xff0c;负责自动回收无用的对象内存。其设计核心是 权衡&#xff1a;主要是吞吐量和停顿时间之间的权衡。没有“最好”的收集器&#xff0c;只有“最适合”特定场景的收集器。一、核心基础&#xff1a;分代收集模型主流 HotSpot JVM 采…