《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_request;bio_for_each_segment遍历;bio_endio完成通知;完整驱动代码 |
blk_alloc_queue()、blk_queue_make_request()、bio_endio() |
| 13.4 请求队列块设备驱动 | 请求队列工作流程;blk_init_queue;blk_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年