《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 13 章 Linux 块设备驱动

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 13 章 Linux 块设备驱动

参考:宋宝华 著,机械工业出版社,2015年版


13.1 块设备的 I/O 操作特点

13.1.1 块设备与字符设备的本质区别

块设备(Block Device)和字符设备(Character Device)是 Linux 中两种最基本的设备类型,它们在 I/O 操作上有本质区别:

复制代码
块设备 vs 字符设备:

字符设备:
  - 数据以字节流方式传输
  - 顺序访问,不支持随机定位(少数例外)
  - 无内核缓冲(直接 I/O)
  - 典型:串口、键盘、鼠标、LED

块设备:
  - 数据以固定大小的块(Block)为单位传输
  - 支持随机访问(可指定块号读写)
  - 有内核页缓存(Page Cache)
  - 有 I/O 调度器(合并、排序请求)
  - 典型:硬盘、SSD、U盘、eMMC、SD卡

┌─────────────────────────────────────────────────────────┐
│              块设备 I/O 路径                              │
│                                                         │
│  应用程序 read()/write()                                 │
│       ↓                                                 │
│  VFS(虚拟文件系统)                                     │
│       ↓                                                 │
│  文件系统(ext4/xfs/fat32)                              │
│       ↓                                                 │
│  页缓存(Page Cache)← 命中则直接返回,不访问硬件        │
│       ↓ 缓存未命中                                       │
│  通用块层(Generic Block Layer)                         │
│       ↓                                                 │
│  I/O 调度器(合并相邻请求,排序减少磁头移动)             │
│       ↓                                                 │
│  块设备驱动(处理请求队列)                              │
│       ↓                                                 │
│  物理存储介质(磁盘/Flash)                              │
└─────────────────────────────────────────────────────────┘

13.1.2 块设备的 I/O 特点

(1)以块为单位传输
复制代码
块(Block)的概念:

扇区(Sector):硬件层面的最小寻址单位
  - 传统硬盘:512 字节
  - 现代硬盘(4K 扇区):4096 字节
  - 内核中用 sector_t 表示扇区号

块(Block):文件系统层面的最小操作单位
  - 通常是扇区大小的整数倍(512B、1KB、2KB、4KB)
  - 由文件系统在格式化时决定
  - 内核中用 block_t 表示块号

页(Page):内存管理的基本单位
  - 通常 4096 字节(4KB)
  - 页缓存以页为单位缓存块设备数据
(2)随机访问
c 复制代码
/* 块设备支持随机访问,可以直接定位到任意扇区 */
/* 这是块设备与字符设备的最重要区别 */

/* 文件系统通过块号直接访问任意数据块 */
/* 例如:读取 inode 表中的第 1000 个 inode */
sector_t sector = inode_table_start + 1000 * INODE_SIZE / SECTOR_SIZE;
submit_bio(READ, bio);  /* 直接读取指定扇区 */
(3)I/O 调度
复制代码
I/O 调度器的作用:

问题:应用程序的 I/O 请求是随机的,磁头需要频繁移动
解决:I/O 调度器对请求进行合并和排序

合并(Merge):
  请求A:读取扇区 100~199
  请求B:读取扇区 200~299
  → 合并为:读取扇区 100~299(一次 I/O 完成)

排序(Sort):
  请求A:读取扇区 500
  请求B:读取扇区 100
  请求C:读取扇区 300
  → 排序为:100 → 300 → 500(减少磁头移动距离)

Linux 4.0 支持的 I/O 调度器:
  CFQ(Completely Fair Queuing):默认,公平调度
  Deadline:保证请求在截止时间前完成
  NOOP:不排序,适合 SSD(SSD 无磁头,不需要排序)

查看/修改 I/O 调度器:
  cat /sys/block/sda/queue/scheduler
  # noop deadline [cfq]  ← 当前使用 cfq
  echo deadline > /sys/block/sda/queue/scheduler
(4)页缓存
复制代码
页缓存(Page Cache)的作用:

读操作:
  第一次读:从磁盘读取 → 存入页缓存 → 返回给应用程序
  第二次读:直接从页缓存返回(不访问磁盘)→ 速度极快

写操作(写回模式,默认):
  应用程序写入 → 写入页缓存(标记为脏页)→ 立即返回
  后台 pdflush/kworker 线程 → 将脏页写回磁盘

写操作(直写模式,O_SYNC):
  应用程序写入 → 写入页缓存 → 同步写回磁盘 → 返回

绕过页缓存(O_DIRECT):
  应用程序直接与磁盘交互,不经过页缓存
  适用于数据库等自己管理缓存的应用

13.2 Linux 块设备驱动结构

13.2.1 块设备驱动的核心数据结构

Linux 块设备驱动围绕以下核心数据结构展开:

(1)gendisk ------ 通用磁盘结构
c 复制代码
#include <linux/genhd.h>

/*
 * gendisk:描述一个块设备(磁盘)
 * 每个物理磁盘或逻辑磁盘对应一个 gendisk
 */
struct gendisk {
    int             major;          /* 主设备号 */
    int             first_minor;    /* 第一个次设备号 */
    int             minors;         /* 次设备号数量(分区数+1)*/
    char            disk_name[DISK_NAME_LEN]; /* 磁盘名称(如 sda)*/

    const struct block_device_operations *fops; /* 块设备操作函数集 */
    struct request_queue *queue;    /* 请求队列 */
    void            *private_data;  /* 驱动私有数据 */

    sector_t        capacity;       /* 磁盘容量(扇区数)*/
    /* ... */
};

/* gendisk 操作函数 */
struct gendisk *alloc_disk(int minors);    /* 分配 gendisk */
void add_disk(struct gendisk *disk);       /* 注册到内核 */
void del_gendisk(struct gendisk *disk);    /* 从内核注销 */
void put_disk(struct gendisk *disk);       /* 释放 gendisk */

/* 设置磁盘容量(扇区数)*/
void set_capacity(struct gendisk *disk, sector_t size);
(2)block_device_operations ------ 块设备操作函数集
c 复制代码
#include <linux/blkdev.h>

/*
 * block_device_operations:块设备操作函数集
 * 类似字符设备的 file_operations
 */
struct block_device_operations {
    /* 打开块设备 */
    int (*open)(struct block_device *, fmode_t);

    /* 关闭块设备 */
    void (*release)(struct gendisk *, fmode_t);

    /* ioctl 控制 */
    int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);

    /* 检查介质是否改变(可移动介质,如 U 盘)*/
    int (*media_changed)(struct gendisk *);

    /* 重新验证磁盘(介质改变后重新读取分区表)*/
    int (*revalidate_disk)(struct gendisk *);

    /* 获取磁盘几何信息 */
    int (*getgeo)(struct block_device *, struct hd_geometry *);

    struct module *owner;
};
(3)request_queue ------ 请求队列
c 复制代码
/*
 * request_queue:I/O 请求队列
 * 内核将 I/O 请求放入队列,驱动从队列中取出并处理
 */
struct request_queue {
    /* 请求处理函数(有请求时调用)*/
    request_fn_proc     *request_fn;

    /* 制造请求函数(直接处理 bio,不经过队列)*/
    make_request_fn     *make_request_fn;

    /* 队列参数 */
    unsigned int        nr_requests;    /* 队列中最大请求数 */
    unsigned int        max_sectors;    /* 每个请求最大扇区数 */
    /* ... */
};

/* 创建请求队列 */
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);

/* 创建无队列的块设备(直接处理 bio)*/
struct request_queue *blk_alloc_queue(gfp_t gfp_mask);
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn);

/* 释放请求队列 */
void blk_cleanup_queue(struct request_queue *q);
(4)request ------ I/O 请求
c 复制代码
/*
 * request:一个 I/O 请求
 * 由 I/O 调度器合并多个 bio 后生成
 */
struct request {
    struct request_queue *q;        /* 所属请求队列 */
    int             cmd_type;       /* 请求类型 */
    unsigned long   cmd_flags;      /* 请求标志(READ/WRITE 等)*/

    sector_t        __sector;       /* 起始扇区号 */
    unsigned int    __data_len;     /* 数据长度(字节)*/

    struct bio      *bio;           /* 关联的 bio 链表 */
    struct bio      *biotail;       /* bio 链表尾 */

    /* ... */
};

/* 从请求中获取信息 */
sector_t blk_rq_pos(const struct request *rq);    /* 起始扇区 */
unsigned int blk_rq_sectors(const struct request *rq); /* 扇区数 */
unsigned int blk_rq_bytes(const struct request *rq);   /* 字节数 */
int rq_data_dir(const struct request *rq);         /* 方向(READ/WRITE)*/
(5)bio ------ 块 I/O 描述符
c 复制代码
/*
 * bio(Block I/O):描述一次块 I/O 操作
 * 是块设备层的基本 I/O 单元
 */
struct bio {
    sector_t        bi_sector;      /* 起始扇区号 */
    struct bio      *bi_next;       /* 链表指针 */
    struct block_device *bi_bdev;   /* 目标块设备 */
    unsigned long   bi_flags;       /* 标志 */
    unsigned short  bi_vcnt;        /* bio_vec 数量 */
    unsigned short  bi_idx;         /* 当前 bio_vec 索引 */
    unsigned int    bi_size;        /* 剩余 I/O 大小(字节)*/

    struct bio_vec  *bi_io_vec;     /* bio_vec 数组(描述内存缓冲区)*/
    bio_end_io_t    *bi_end_io;     /* I/O 完成回调函数 */
    void            *bi_private;    /* 私有数据 */
};

/*
 * bio_vec:描述一个内存段(物理页 + 偏移 + 长度)
 */
struct bio_vec {
    struct page *bv_page;    /* 物理页 */
    unsigned int bv_len;     /* 数据长度 */
    unsigned int bv_offset;  /* 页内偏移 */
};

/* 遍历 bio 中的所有 bio_vec */
struct bio_vec bvec;
struct bvec_iter iter;
bio_for_each_segment(bvec, bio, iter) {
    void *data = kmap(bvec.bv_page) + bvec.bv_offset;
    /* 处理 data,长度为 bvec.bv_len */
    kunmap(bvec.bv_page);
}

13.2.2 块设备驱动的注册流程

复制代码
块设备驱动的完整注册流程:

module_init()
    ↓
1. register_blkdev(major, name)    ← 注册块设备,获取主设备号
    ↓
2. blk_init_queue(request_fn, &lock) ← 创建请求队列
   或 blk_alloc_queue() + blk_queue_make_request()
    ↓
3. alloc_disk(minors)              ← 分配 gendisk
    ↓
4. 设置 gendisk 参数:
   disk->major = major
   disk->first_minor = 0
   disk->fops = &my_fops
   disk->queue = queue
   set_capacity(disk, capacity)
    ↓
5. add_disk(disk)                  ← 注册到内核(此后设备可用)

module_exit()
    ↓
1. del_gendisk(disk)               ← 注销 gendisk
2. put_disk(disk)                  ← 释放 gendisk
3. blk_cleanup_queue(queue)        ← 释放请求队列
4. unregister_blkdev(major, name)  ← 注销块设备

13.3 一个简单的块设备驱动

13.3.1 ramdisk 设备描述

宋宝华在书中以 **ramdisk(内存磁盘)**作为块设备驱动的入门案例。ramdisk 使用一块内核内存模拟磁盘,是理解块设备驱动的最佳起点:

复制代码
ramdisk 设备规格:

设备名称:ramdisk
设备文件:/dev/ramdisk(主设备号 253,次设备号 0)
容量:16MB(可配置)
扇区大小:512 字节
实现方式:使用 vmalloc 分配的内核内存模拟磁盘
不使用请求队列:直接处理 bio(make_request 方式)

ramdisk 内存布局:
┌─────────────────────────────────────────────────────────┐
│  ramdisk_data(vmalloc 分配,16MB)                      │
│  ┌──────────────────────────────────────────────────┐   │
│  │  扇区0(512B)│ 扇区1(512B)│ ... │ 扇区32767   │   │
│  └──────────────────────────────────────────────────┘   │
│  ↑ 物理上不连续,但虚拟地址连续                          │
└─────────────────────────────────────────────────────────┘

13.3.2 简单 ramdisk 驱动(不使用请求队列)

c 复制代码
/*
 * ramdisk.c ------ 简单的 ramdisk 块设备驱动
 *
 * 使用 make_request 方式直接处理 bio,不经过 I/O 调度器
 * 适合内存设备(无需排序优化)
 *
 * 参考:宋宝华《Linux设备驱动开发详解》第13章
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/errno.h>
#include <linux/vmalloc.h>
#include <linux/bio.h>

/* ── 宏定义 ──────────────────────────────────────────────── */
#define RAMDISK_NAME     "ramdisk"
#define RAMDISK_MAJOR    253          /* 主设备号(0 = 动态分配)*/
#define RAMDISK_MINORS   1            /* 次设备号数量(不分区)*/
#define SECTOR_SIZE      512          /* 扇区大小(字节)*/
#define RAMDISK_SIZE     (16 * 1024 * 1024)  /* 磁盘大小:16MB */
#define RAMDISK_SECTORS  (RAMDISK_SIZE / SECTOR_SIZE)  /* 扇区总数 */

/* ── 设备结构体 ──────────────────────────────────────────── */
struct ramdisk_dev {
    int             major;          /* 主设备号 */
    u8             *data;           /* 磁盘数据(vmalloc 分配)*/
    struct gendisk *gd;             /* gendisk 结构体 */
    struct request_queue *queue;    /* 请求队列 */
    spinlock_t      lock;           /* 自旋锁 */
};

static struct ramdisk_dev ramdisk;

/* ── 核心 I/O 处理函数 ───────────────────────────────────── */

/*
 * ramdisk_transfer:执行实际的数据传输
 *
 * dev:设备结构体
 * sector:起始扇区号
 * nsect:扇区数量
 * buffer:数据缓冲区(内核虚拟地址)
 * write:1(写操作),0(读操作)
 */
static void ramdisk_transfer(struct ramdisk_dev *dev,
                              sector_t sector,
                              unsigned long nsect,
                              char *buffer,
                              int write)
{
    /* 计算字节偏移和长度 */
    unsigned long offset = sector * SECTOR_SIZE;
    unsigned long nbytes = nsect * SECTOR_SIZE;

    /* 边界检查 */
    if ((offset + nbytes) > RAMDISK_SIZE) {
        pr_err("ramdisk: 超出磁盘范围(offset=%lu, nbytes=%lu)\n",
               offset, nbytes);
        return;
    }

    if (write) {
        /* 写操作:从 buffer 复制到 ramdisk */
        memcpy(dev->data + offset, buffer, nbytes);
    } else {
        /* 读操作:从 ramdisk 复制到 buffer */
        memcpy(buffer, dev->data + offset, nbytes);
    }
}

/*
 * ramdisk_make_request:直接处理 bio 的函数
 *
 * 当使用 blk_queue_make_request 时,内核直接调用此函数
 * 而不是将请求放入队列
 *
 * q:请求队列
 * bio:要处理的 bio
 */
static void ramdisk_make_request(struct request_queue *q, struct bio *bio)
{
    struct ramdisk_dev *dev = q->queuedata;
    struct bio_vec bvec;
    struct bvec_iter iter;
    sector_t sector = bio->bi_iter.bi_sector;

    /*
     * 遍历 bio 中的所有 bio_vec(内存段)
     * 每个 bio_vec 描述一个物理页中的一段数据
     */
    bio_for_each_segment(bvec, bio, iter) {
        char *buffer;
        unsigned int len = bvec.bv_len;

        /* 将物理页映射到内核虚拟地址 */
        buffer = kmap_atomic(bvec.bv_page) + bvec.bv_offset;

        /* 执行数据传输 */
        ramdisk_transfer(dev,
                          sector,
                          len / SECTOR_SIZE,
                          buffer,
                          bio_data_dir(bio) == WRITE);

        /* 解除映射 */
        kunmap_atomic(buffer - bvec.bv_offset);

        /* 更新扇区号 */
        sector += len / SECTOR_SIZE;
    }

    /* 通知内核 bio 处理完成 */
    bio_endio(bio, 0);
}

/* ── 块设备操作函数集 ────────────────────────────────────── */

static int ramdisk_open(struct block_device *bdev, fmode_t mode)
{
    pr_info("ramdisk: 设备打开\n");
    return 0;
}

static void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
    pr_info("ramdisk: 设备关闭\n");
}

static int ramdisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    /*
     * 返回磁盘几何信息(柱面/磁头/扇区)
     * 对于内存磁盘,这些值是虚构的,但某些工具需要
     */
    geo->heads     = 4;
    geo->sectors   = 16;
    geo->cylinders = RAMDISK_SECTORS / (4 * 16);
    return 0;
}

static const struct block_device_operations ramdisk_fops = {
    .owner      = THIS_MODULE,
    .open       = ramdisk_open,
    .release    = ramdisk_release,
    .getgeo     = ramdisk_getgeo,
};

/* ── 模块加载函数 ─────────────────────────────────────────── */
static int __init ramdisk_init(void)
{
    int ret;

    /* 1. 注册块设备,获取主设备号 */
    ret = register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    if (ret < 0) {
        pr_err("ramdisk: 注册块设备失败\n");
        return ret;
    }
    ramdisk.major = RAMDISK_MAJOR;
    pr_info("ramdisk: 注册成功,主设备号 %d\n", ramdisk.major);

    /* 2. 分配磁盘数据内存(vmalloc,16MB)*/
    ramdisk.data = vmalloc(RAMDISK_SIZE);
    if (!ramdisk.data) {
        pr_err("ramdisk: 内存分配失败\n");
        ret = -ENOMEM;
        goto err_vmalloc;
    }
    memset(ramdisk.data, 0, RAMDISK_SIZE);
    pr_info("ramdisk: 分配 %d MB 内存\n", RAMDISK_SIZE / 1024 / 1024);

    /* 3. 初始化自旋锁 */
    spin_lock_init(&ramdisk.lock);

    /*
     * 4. 创建请求队列(make_request 方式,不使用 I/O 调度器)
     * blk_alloc_queue:分配队列
     * blk_queue_make_request:设置直接处理 bio 的函数
     */
    ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
    if (!ramdisk.queue) {
        pr_err("ramdisk: 创建请求队列失败\n");
        ret = -ENOMEM;
        goto err_queue;
    }
    blk_queue_make_request(ramdisk.queue, ramdisk_make_request);
    ramdisk.queue->queuedata = &ramdisk;

    /* 5. 分配 gendisk */
    ramdisk.gd = alloc_disk(RAMDISK_MINORS);
    if (!ramdisk.gd) {
        pr_err("ramdisk: 分配 gendisk 失败\n");
        ret = -ENOMEM;
        goto err_disk;
    }

    /* 6. 设置 gendisk 参数 */
    ramdisk.gd->major       = ramdisk.major;
    ramdisk.gd->first_minor = 0;
    ramdisk.gd->fops        = &ramdisk_fops;
    ramdisk.gd->queue       = ramdisk.queue;
    ramdisk.gd->private_data = &ramdisk;
    snprintf(ramdisk.gd->disk_name, DISK_NAME_LEN, RAMDISK_NAME);

    /* 设置磁盘容量(扇区数)*/
    set_capacity(ramdisk.gd, RAMDISK_SECTORS);

    /* 7. 注册 gendisk(此后 /dev/ramdisk 可用)*/
    add_disk(ramdisk.gd);

    pr_info("ramdisk: 驱动加载成功\n");
    pr_info("ramdisk: 容量 %d MB,扇区数 %d\n",
            RAMDISK_SIZE / 1024 / 1024, RAMDISK_SECTORS);
    return 0;

err_disk:
    blk_cleanup_queue(ramdisk.queue);
err_queue:
    vfree(ramdisk.data);
err_vmalloc:
    unregister_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    return ret;
}

/* ── 模块卸载函数 ─────────────────────────────────────────── */
static void __exit ramdisk_exit(void)
{
    del_gendisk(ramdisk.gd);          /* 注销 gendisk */
    put_disk(ramdisk.gd);             /* 释放 gendisk */
    blk_cleanup_queue(ramdisk.queue); /* 释放请求队列 */
    vfree(ramdisk.data);              /* 释放磁盘数据内存 */
    unregister_blkdev(RAMDISK_MAJOR, RAMDISK_NAME); /* 注销块设备 */
    pr_info("ramdisk: 驱动已卸载\n");
}

module_init(ramdisk_init);
module_exit(ramdisk_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("参考宋宝华《Linux设备驱动开发详解》");
MODULE_DESCRIPTION("简单的 ramdisk 块设备驱动(make_request 方式)");

13.3.3 测试简单 ramdisk 驱动

bash 复制代码
# 编译并加载驱动
make
sudo insmod ramdisk.ko

# 查看设备
ls -l /dev/ramdisk
# brw-rw---- 1 root disk 253, 0 Jun 21 10:00 /dev/ramdisk
# b 表示块设备(block device)

# 查看磁盘信息
sudo fdisk -l /dev/ramdisk
# Disk /dev/ramdisk: 16 MiB, 16777216 bytes, 32768 sectors
# Units: sectors of 1 * 512 = 512 bytes
# Sector size (logical/physical): 512 bytes / 512 bytes

# 格式化为 ext4 文件系统
sudo mkfs.ext4 /dev/ramdisk
# mke2fs 1.44.5 (15-Dec-2018)
# Creating filesystem with 4096 4k blocks and 4096 inodes
# ...

# 挂载并使用
sudo mkdir -p /mnt/ramdisk
sudo mount /dev/ramdisk /mnt/ramdisk

# 写入测试文件
echo "Hello, ramdisk!" | sudo tee /mnt/ramdisk/test.txt
sudo ls -la /mnt/ramdisk/

# 读取测试
cat /mnt/ramdisk/test.txt
# Hello, ramdisk!

# 卸载
sudo umount /mnt/ramdisk

# 性能测试
sudo dd if=/dev/zero of=/dev/ramdisk bs=4096 count=4096
# 4096+0 records in
# 4096+0 records out
# 16777216 bytes (17 MB, 16 MiB) copied, 0.0234 s, 717 MB/s

sudo dd if=/dev/ramdisk of=/dev/null bs=4096 count=4096
# 4096+0 records in
# 4096+0 records out
# 16777216 bytes (17 MB, 16 MiB) copied, 0.0189 s, 888 MB/s

# 卸载驱动
sudo umount /mnt/ramdisk 2>/dev/null
sudo rmmod ramdisk

13.4 使用请求队列的块设备驱动

13.4.1 请求队列的工作原理

使用请求队列的块设备驱动通过 I/O 调度器对请求进行合并和排序,适合真实的磁盘设备:

复制代码
请求队列的工作流程:

应用程序 I/O 请求
    ↓
bio(块 I/O 描述符)
    ↓
通用块层(Generic Block Layer)
    ↓
I/O 调度器(合并相邻 bio,排序减少寻道)
    ↓
请求队列(request_queue)
    ↓ 调用 request_fn(驱动注册的请求处理函数)
驱动处理请求(从队列取出 request,执行 I/O)
    ↓
硬件设备
    ↓ I/O 完成(中断)
end_request / blk_end_request(通知内核请求完成)

13.4.2 使用请求队列的 ramdisk 驱动

c 复制代码
/*
 * ramdisk_queue.c ------ 使用请求队列的 ramdisk 块设备驱动
 *
 * 使用 blk_init_queue 创建请求队列
 * 驱动实现 request_fn 处理请求队列中的请求
 *
 * 参考:宋宝华《Linux设备驱动开发详解》第13章
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/errno.h>
#include <linux/vmalloc.h>
#include <linux/bio.h>
#include <linux/hdreg.h>

#define RAMDISK_NAME     "ramdisk_q"
#define RAMDISK_MAJOR    254
#define RAMDISK_MINORS   16           /* 支持最多 15 个分区 */
#define SECTOR_SIZE      512
#define RAMDISK_SIZE     (16 * 1024 * 1024)
#define RAMDISK_SECTORS  (RAMDISK_SIZE / SECTOR_SIZE)

struct ramdisk_dev {
    int              major;
    u8              *data;
    struct gendisk  *gd;
    struct request_queue *queue;
    spinlock_t       lock;           /* 保护请求队列 */
};

static struct ramdisk_dev ramdisk_q;

/* ── 数据传输函数 ────────────────────────────────────────── */
static void ramdisk_transfer(struct ramdisk_dev *dev,
                              unsigned long sector,
                              unsigned long nsect,
                              char *buffer,
                              int write)
{
    unsigned long offset = sector * SECTOR_SIZE;
    unsigned long nbytes = nsect * SECTOR_SIZE;

    if ((offset + nbytes) > RAMDISK_SIZE) {
        pr_err("ramdisk_q: 超出磁盘范围\n");
        return;
    }

    if (write)
        memcpy(dev->data + offset, buffer, nbytes);
    else
        memcpy(buffer, dev->data + offset, nbytes);
}

/*
 * ── 请求处理函数(核心)────────────────────────────────────
 *
 * 当请求队列中有请求时,内核调用此函数
 * 驱动必须处理队列中的所有请求
 *
 * 注意:调用此函数时,队列自旋锁已被持有
 */
static void ramdisk_request(struct request_queue *q)
{
    struct request *req;
    struct ramdisk_dev *dev = q->queuedata;

    /*
     * blk_fetch_request:从队列中取出下一个请求
     * 返回 NULL 表示队列为空
     */
    while ((req = blk_fetch_request(q)) != NULL) {
        /*
         * 检查请求类型
         * 只处理文件系统请求(REQ_TYPE_FS)
         * 忽略特殊请求(如 FLUSH、DISCARD 等)
         */
        if (req->cmd_type != REQ_TYPE_FS) {
            pr_notice("ramdisk_q: 跳过非文件系统请求\n");
            /* 结束请求,返回错误 */
            __blk_end_request_all(req, -EIO);
            continue;
        }

        /*
         * 处理请求中的所有 segment
         * 一个 request 可能包含多个 bio,每个 bio 包含多个 segment
         */
        do {
            /* 获取当前 segment 的信息 */
            sector_t sector = blk_rq_pos(req);      /* 起始扇区 */
            unsigned int nsect = blk_rq_cur_sectors(req); /* 当前扇区数 */
            char *buffer = bio_data(req->bio);       /* 数据缓冲区 */
            int write = rq_data_dir(req);            /* 读(0)或写(1) */

            /* 执行数据传输 */
            ramdisk_transfer(dev, sector, nsect, buffer, write);

            /*
             * blk_end_request:通知内核当前 segment 处理完成
             * 返回 false:请求中所有 segment 都已处理完
             * 返回 true:请求中还有未处理的 segment
             */
        } while (blk_end_request(req, 0, blk_rq_cur_bytes(req)));
    }
}

/* ── 块设备操作函数集 ────────────────────────────────────── */

static int ramdisk_q_open(struct block_device *bdev, fmode_t mode)
{
    pr_info("ramdisk_q: 设备打开\n");
    return 0;
}

static void ramdisk_q_release(struct gendisk *disk, fmode_t mode)
{
    pr_info("ramdisk_q: 设备关闭\n");
}

static int ramdisk_q_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    geo->heads     = 4;
    geo->sectors   = 16;
    geo->cylinders = RAMDISK_SECTORS / (4 * 16);
    geo->start     = 0;
    return 0;
}

static const struct block_device_operations ramdisk_q_fops = {
    .owner   = THIS_MODULE,
    .open    = ramdisk_q_open,
    .release = ramdisk_q_release,
    .getgeo  = ramdisk_q_getgeo,
};

/* ── 模块加载函数 ─────────────────────────────────────────── */
static int __init ramdisk_q_init(void)
{
    int ret;

    /* 1. 注册块设备 */
    ret = register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    if (ret < 0) {
        pr_err("ramdisk_q: 注册块设备失败\n");
        return ret;
    }
    ramdisk_q.major = RAMDISK_MAJOR;

    /* 2. 分配磁盘数据内存 */
    ramdisk_q.data = vmalloc(RAMDISK_SIZE);
    if (!ramdisk_q.data) {
        ret = -ENOMEM;
        goto err_vmalloc;
    }
    memset(ramdisk_q.data, 0, RAMDISK_SIZE);

    /* 3. 初始化自旋锁 */
    spin_lock_init(&ramdisk_q.lock);

    /*
     * 4. 创建请求队列(使用 I/O 调度器)
     *
     * blk_init_queue(request_fn, lock):
     * - request_fn:请求处理函数(有请求时调用)
     * - lock:保护请求队列的自旋锁
     *
     * 与 blk_alloc_queue + blk_queue_make_request 的区别:
     * - blk_init_queue:使用 I/O 调度器,适合真实磁盘
     * - blk_alloc_queue:不使用调度器,适合内存设备
     */
    ramdisk_q.queue = blk_init_queue(ramdisk_request, &ramdisk_q.lock);
    if (!ramdisk_q.queue) {
        ret = -ENOMEM;
        goto err_queue;
    }
    ramdisk_q.queue->queuedata = &ramdisk_q;

    /* 设置队列参数 */
    blk_queue_logical_block_size(ramdisk_q.queue, SECTOR_SIZE);

    /* 5. 分配并设置 gendisk */
    ramdisk_q.gd = alloc_disk(RAMDISK_MINORS);
    if (!ramdisk_q.gd) {
        ret = -ENOMEM;
        goto err_disk;
    }

    ramdisk_q.gd->major        = ramdisk_q.major;
    ramdisk_q.gd->first_minor  = 0;
    ramdisk_q.gd->fops         = &ramdisk_q_fops;
    ramdisk_q.gd->queue        = ramdisk_q.queue;
    ramdisk_q.gd->private_data = &ramdisk_q;
    snprintf(ramdisk_q.gd->disk_name, DISK_NAME_LEN, RAMDISK_NAME);
    set_capacity(ramdisk_q.gd, RAMDISK_SECTORS);

    /* 6. 注册 gendisk */
    add_disk(ramdisk_q.gd);

    pr_info("ramdisk_q: 驱动加载成功(使用请求队列)\n");
    return 0;

err_disk:
    blk_cleanup_queue(ramdisk_q.queue);
err_queue:
    vfree(ramdisk_q.data);
err_vmalloc:
    unregister_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    return ret;
}

/* ── 模块卸载函数 ─────────────────────────────────────────── */
static void __exit ramdisk_q_exit(void)
{
    del_gendisk(ramdisk_q.gd);
    put_disk(ramdisk_q.gd);
    blk_cleanup_queue(ramdisk_q.queue);
    vfree(ramdisk_q.data);
    unregister_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    pr_info("ramdisk_q: 驱动已卸载\n");
}

module_init(ramdisk_q_init);
module_exit(ramdisk_q_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("使用请求队列的 ramdisk 块设备驱动");

13.4.3 请求处理函数详解

c 复制代码
/*
 * 请求处理函数的详细分析
 *
 * 关键函数说明:
 */

/* blk_fetch_request:从队列取出请求 */
struct request *req = blk_fetch_request(q);
/*
 * 等价于:
 * req = blk_peek_request(q);  // 查看队列头部请求
 * if (req) blk_start_request(req);  // 标记请求开始处理
 */

/* 获取请求信息 */
sector_t sector    = blk_rq_pos(req);         /* 起始扇区号 */
unsigned int nsect = blk_rq_sectors(req);     /* 总扇区数 */
unsigned int nbytes = blk_rq_bytes(req);      /* 总字节数 */
int write          = rq_data_dir(req);        /* READ(0) 或 WRITE(1) */

/* 当前 segment 的信息 */
unsigned int cur_sectors = blk_rq_cur_sectors(req); /* 当前 segment 扇区数 */
unsigned int cur_bytes   = blk_rq_cur_bytes(req);   /* 当前 segment 字节数 */

/* 结束请求处理 */
/*
 * blk_end_request(req, error, nr_bytes):
 * - error:0(成功),负值(错误码)
 * - nr_bytes:本次处理的字节数
 * - 返回 true:请求未完成,还有更多 segment
 * - 返回 false:请求已完成
 */
bool more = blk_end_request(req, 0, blk_rq_cur_bytes(req));

/* 结束整个请求(不管是否完成)*/
__blk_end_request_all(req, error);

/* 完整的请求处理循环 */
static void my_request(struct request_queue *q)
{
    struct request *req;

    while ((req = blk_fetch_request(q)) != NULL) {
        /* 检查请求类型 */
        if (req->cmd_type != REQ_TYPE_FS) {
            __blk_end_request_all(req, -EIO);
            continue;
        }

        /* 处理请求的每个 segment */
        do {
            sector_t sector = blk_rq_pos(req);
            unsigned int nsect = blk_rq_cur_sectors(req);
            char *buf = bio_data(req->bio);
            int write = rq_data_dir(req);

            /* 执行 I/O */
            do_io(sector, nsect, buf, write);

        } while (blk_end_request(req, 0, blk_rq_cur_bytes(req)));
    }
}

13.4.4 两种块设备驱动方式的对比

复制代码
make_request 方式 vs 请求队列方式:

┌─────────────────────────────────────────────────────────────┐
│              make_request 方式                               │
│  blk_alloc_queue() + blk_queue_make_request()               │
│                                                             │
│  优点:                                                     │
│    ✓ 简单,直接处理 bio                                     │
│    ✓ 无 I/O 调度开销                                        │
│    ✓ 适合内存设备(无需排序)                               │
│                                                             │
│  缺点:                                                     │
│    ✗ 无 I/O 合并和排序                                      │
│    ✗ 不适合真实磁盘(性能差)                               │
│                                                             │
│  适用场景:ramdisk、内存文件系统、虚拟块设备                 │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│              请求队列方式                                    │
│  blk_init_queue(request_fn, lock)                           │
│                                                             │
│  优点:                                                     │
│    ✓ 有 I/O 调度器(合并、排序)                            │
│    ✓ 适合真实磁盘(减少寻道时间)                           │
│    ✓ 支持 I/O 优先级                                        │
│                                                             │
│  缺点:                                                     │
│    ✗ 实现复杂                                               │
│    ✗ 有调度开销                                             │
│    ✗ 对 SSD 等无寻道设备无益                                │
│                                                             │
│  适用场景:机械硬盘、eMMC、SD 卡等真实存储设备              │
└─────────────────────────────────────────────────────────────┘

13.4.5 块设备驱动的分区支持

c 复制代码
/*
 * 支持分区的块设备驱动
 * 通过设置 minors > 1 来支持分区
 */

/* 分配支持 16 个分区的 gendisk(1个主设备 + 15个分区)*/
disk = alloc_disk(16);

/* 设置主设备号和次设备号 */
disk->major       = MY_MAJOR;
disk->first_minor = 0;   /* 主设备:次设备号 0 */
                         /* 分区1:次设备号 1 */
                         /* 分区2:次设备号 2 */
                         /* ... */

/* 分区操作 */
/* 用户空间使用 fdisk 创建分区 */
/* sudo fdisk /dev/ramdisk_q */
/* n → 新建分区 → p → 主分区 → 1 → 起始扇区 → 结束扇区 → w */

/* 分区后的设备文件 */
/* /dev/ramdisk_q    ← 整个磁盘 */
/* /dev/ramdisk_q1   ← 第1个分区 */
/* /dev/ramdisk_q2   ← 第2个分区 */

/* 格式化分区 */
/* sudo mkfs.ext4 /dev/ramdisk_q1 */
/* sudo mount /dev/ramdisk_q1 /mnt/part1 */

13.4.6 完整测试流程

bash 复制代码
# 编译并加载使用请求队列的驱动
make
sudo insmod ramdisk_queue.ko

# 查看设备
ls -l /dev/ramdisk_q
# brw-rw---- 1 root disk 254, 0 Jun 21 10:00 /dev/ramdisk_q

# 查看 I/O 调度器
cat /sys/block/ramdisk_q/queue/scheduler
# noop deadline [cfq]

# 切换 I/O 调度器
echo noop > /sys/block/ramdisk_q/queue/scheduler

# 创建分区
sudo fdisk /dev/ramdisk_q << 'EOF'
n
p
1


w
EOF

# 查看分区
ls /dev/ramdisk_q*
# /dev/ramdisk_q  /dev/ramdisk_q1

# 格式化并挂载
sudo mkfs.ext4 /dev/ramdisk_q1
sudo mkdir -p /mnt/ramdisk_q
sudo mount /dev/ramdisk_q1 /mnt/ramdisk_q

# 读写测试
echo "Hello, ramdisk with queue!" | sudo tee /mnt/ramdisk_q/test.txt
cat /mnt/ramdisk_q/test.txt

# 性能测试(对比两种方式)
echo "=== 请求队列方式 ==="
sudo dd if=/dev/zero of=/dev/ramdisk_q bs=4096 count=4096 2>&1
sudo dd if=/dev/ramdisk_q of=/dev/null bs=4096 count=4096 2>&1

# 查看 I/O 统计
cat /sys/block/ramdisk_q/stat
# 读请求数 读合并数 读扇区数 读耗时 写请求数 写合并数 写扇区数 写耗时 ...

# 卸载
sudo umount /mnt/ramdisk_q
sudo rmmod ramdisk_queue

本章小结

章节 核心知识点 关键 API
13.1 块设备I/O特点 块设备vs字符设备;扇区/块/页的概念;I/O调度器(合并/排序);页缓存(读缓存/写回/直写/O_DIRECT) /sys/block/sda/queue/scheduler
13.2 块设备驱动结构 gendisk(磁盘描述);block_device_operations;request_queue;request;bio/bio_vec;注册流程 alloc_disk()add_disk()blk_init_queue()
13.3 简单块设备驱动 make_request方式;ramdisk设计;blk_alloc_queue+blk_queue_make_requestbio_for_each_segment遍历;bio_endio完成通知;完整驱动代码 blk_alloc_queue()blk_queue_make_request()bio_endio()
13.4 请求队列块设备驱动 请求队列工作流程;blk_init_queueblk_fetch_request取请求;blk_end_request完成请求;两种方式对比;分区支持 blk_init_queue()blk_fetch_request()blk_end_request()

块设备驱动开发要点

复制代码
1. 选择合适的驱动方式
   内存设备(ramdisk)→ make_request 方式(无调度开销)
   真实磁盘设备       → 请求队列方式(I/O 调度优化)
   SSD/NVMe          → 多队列(blk-mq,Linux 3.13+)

2. 正确处理请求
   检查请求类型(REQ_TYPE_FS)
   使用 blk_fetch_request 取请求
   使用 blk_end_request 通知完成
   处理所有 segment(do-while 循环)

3. 设置正确的磁盘容量
   set_capacity(disk, sectors)  ← 以扇区为单位
   扇区大小通常为 512 字节

4. 支持分区
   alloc_disk(minors) 中 minors > 1
   minors = 最大分区数 + 1

5. 资源管理(逆序释放)
   del_gendisk → put_disk → blk_cleanup_queue
   → vfree → unregister_blkdev

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年