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

发布于:2025-09-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

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

在Linux系统中,块设备是存储系统的核心组成部分,涵盖了硬盘、固态硬盘(SSD)、U盘、SD卡等各类持久化存储介质。与字符设备不同,块设备以固定大小的“块”为单位进行数据读写,支持随机访问,并通过复杂的I/O调度机制提升性能和设备寿命。本文将深入剖析Linux块设备驱动的架构、核心数据结构、注册流程及请求处理机制,并通过一个完整的基于内存的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. 分配并初始化gendisk
    disk = 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)  // 1MB

static 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. 分配并初始化gendisk
    disk = 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(空设备)等经典实现。


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



网站公告

今日签到

点亮在社区的每一天
去签到