title: Linux块设备驱动 tags:
块设备是针对存储设备的,比如SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备驱动其实就是这些存储设备的驱动,块设备驱动相比字符设备驱动的主要区别如下:
| 特性 | 块设备 | 字符设备 |
|---|---|---|
| 访问单位 | 块(VFS 基本传输单位,扇区为512字节) | 字节 |
| 数据传输 | 需要缓冲区 | 不需要缓冲 |
| 访问方式 | 随机访问 | 顺序访问 |
| I/O调度 | 需要I/O调度器优化 | 无需调度 |
| 生命周期 | 有擦写次数限制 | 无限制 |
原书 68.1 指出:块设备以块为单位读写,使用缓冲区暂存数据、满足条件后再一次性写入物理设备,以减少擦除次数、提高寿命;字符设备不需要缓冲区,实时按字节访问。因此块设备寿命受 Flash 擦写次数限制。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十八章内容,为扩展知识。
块设备主要用于数据的持久化存储,典型应用场景包括:
块设备驱动是Linux三大驱动类型之一,其复杂度远高于字符设备。块设备驱动需要处理:
graph TB
subgraph "用户空间"
APP[应用程序]
VFS[虚拟文件系统VFS]
end
subgraph "内核空间"
BIO[BIO层<br/>块I/O请求]
QUEUE[请求队列<br/>request_queue]
SCHEDULER[I/O调度器<br/>电梯算法]
DRIVER[块设备驱动]
end
subgraph "硬件层"
DISK[块设备<br/>HDD/SSD/eMMC]
end
APP --> VFS
VFS --> BIO
BIO --> QUEUE
QUEUE --> SCHEDULER
SCHEDULER --> DRIVER
DRIVER --> DISK
内核将对块设备的读写都发送到请求队列 request_queue 中。request_queue 里是大量的 request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据。
请求队列的工作流程:
bio 结构bio 被合并或创建新的 requestLinux内核使用 block_device 来表示块设备,定义在 include/linux/fs.h 中:
struct block_device {
dev_t bd_dev; /* 设备号 */
int bd_openers; /* 打开次数 */
struct inode *bd_inode; /* inode指针 */
struct super_block *bd_super; /* 超级块 */
struct mutex bd_mutex; /* open/close互斥锁 */
struct list_head bd_inodes; /* inode链表 */
void *bd_claiming;
void *bd_holder;
int bd_holders;
bool bd_write_holder;
struct block_device *bd_contains;
unsigned bd_block_size; /* 块大小 */
struct hd_struct *bd_part; /* 分区信息 */
unsigned bd_part_count;
int bd_invalidated;
struct gendisk *bd_disk; /* 通用磁盘结构 */
struct request_queue *bd_queue; /* 请求队列 */
struct list_head bd_list;
unsigned long bd_private;
};
重点关注 bd_disk 成员变量,此成员变量为 gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象。
Linux内核使用 gendisk 来描述一个磁盘设备,定义在 include/linux/genhd.h 中:
struct gendisk {
int major; /* 主设备号 */
int first_minor; /* 第一个次设备号 */
int minors; /* 次设备号数量,即分区数 */
char disk_name[DISK_NAME_LEN]; /* 磁盘名字 */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
struct disk_part_tbl __rcu *part_tbl; /* 分区表 */
struct hd_struct part0; /* 第一个分区 */
const struct block_device_operations *fops; /* 操作集 */
struct request_queue *queue; /* 请求队列 */
void *private_data; /* 私有数据 */
int flags;
struct device *driverfs_dev;
struct kobject *slave_dir;
};
关键成员说明:
major:主设备号first_minor:起始次设备号minors:次设备号数量,即磁盘分区数量fops:块设备操作集,类似字符设备的 file_operationsqueue:请求队列,所有针对该磁盘的请求都放到此队列中bio 是块设备I/O传输的基本单元,定义在 include/linux/blk_types.h 中:
struct bio {
struct bio *bi_next; /* 请求队列中的下一个bio */
struct block_device *bi_bdev; /* 指向块设备 */
unsigned long bi_flags; /* bio状态等信息 */
unsigned long bi_rw; /* I/O操作,读或写 */
struct bvec_iter bi_iter; /* I/O操作信息 */
unsigned int bi_phys_segments;
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t bi_remaining;
bio_end_io_t *bi_end_io; /* bio完成回调 */
void *bi_private; /* 私有数据 */
unsigned short bi_vcnt; /* bio_vec列表中元素数量 */
unsigned short bi_max_vecs; /* bio_vec列表最大长度 */
atomic_t bi_cnt; /* 引用计数 */
struct bio_vec *bi_io_vec; /* bio_vec列表 */
struct bio_set *bi_pool;
struct bio_vec bi_inline_vecs[0];
};
描述要操作的设备扇区等信息:
struct bvec_iter {
sector_t bi_sector; /* I/O请求的设备起始扇区(512字节) */
unsigned int bi_size; /* 剩余的I/O数量 */
unsigned int bi_idx; /* bvec_vec中当前索引 */
unsigned int bi_bvec_done; /* 当前bvec已经处理完成的字节数 */
};
描述内存中的数据页信息:
struct bio_vec {
struct page *bv_page; /* 页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};
bio_vec 就是 "page, offset, len" 组合,page指定了所在的物理页,offset表示所在页的偏移地址,len就是数据长度。
注册块设备,申请设备号:
int register_blkdev(unsigned int major, const char *name)
参数说明:
major:主设备号name:块设备名字返回值(原书 68.2.1):
major 为 1~255,表示自定义主设备号:返回 0 表示注册成功,负值表示失败major 为 0,表示由系统自动分配主设备号:返回值为系统分配的主设备号(1~255),负值表示失败注销块设备:
void unregister_blkdev(unsigned int major, const char *name)
申请gendisk:
struct gendisk *alloc_disk(int minors)
参数说明:
minors:次设备号数量,即分区数量返回值:成功返回申请到的gendisk,失败返回NULL
删除gendisk:
void del_gendisk(struct gendisk *gp)
将gendisk添加到内核:
void add_disk(struct gendisk *disk)
设置磁盘容量(单位为扇区,每扇区512字节):
void set_capacity(struct gendisk *disk, sector_t size)
示例:设置2MB的磁盘容量
set_capacity(gendisk, (2 * 1024 * 1024) / 512); // 4096个扇区
内核通过这两个函数调整 gendisk 的引用计数:
struct kobject *get_disk(struct gendisk *disk); /* 增加引用计数 */
void put_disk(struct gendisk *disk); /* 减少引用计数 */
和字符设备的 file_operations 类似,块设备操作集定义在 include/linux/blkdev.h(原书示例代码 68.2.3.1):
struct block_device_operations {
int (*open)(struct block_device *, fmode_t);
void (*release)(struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t,
void **, unsigned long *pfn, long size);
unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);
int (*media_changed)(struct gendisk *); /* 已废弃,用 check_events 代替 */
void (*unlock_native_capacity)(struct gendisk *);
int (*revalidate_disk)(struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
void (*swap_slot_free_notify)(struct block_device *, unsigned long);
struct module *owner;
};
重点成员:open/release(打开/关闭块设备)、ioctl/compat_ioctl(I/O 控制,64 位系统上 32 位程序走 compat_ioctl)、getgeo(获取磁盘磁头/柱面/扇区信息)、owner(一般为 THIS_MODULE)。块设备操作集没有 read/write,数据读写通过 request_queue/request/bio 完成。
getgeo 用到的 hd_geometry 结构(原书示例代码 68.3.1.4):
struct hd_geometry {
unsigned char heads; /* 磁头 */
unsigned char sectors; /* 一个磁道上的扇区数量 */
unsigned short cylinders; /* 柱面 */
unsigned long start;
};
分配并初始化请求队列(用于机械硬盘等需要I/O调度的设备):
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
参数说明:
rfn:请求处理函数指针lock:自旋锁指针删除请求队列:
void blk_cleanup_queue(struct request_queue *q)
分配无I/O调度的请求队列(用于eMMC、SD卡等非机械设备):
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
绑定"制造请求"函数:
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
| 函数 | 描述 |
|---|---|
blk_peek_request() |
获取请求队列中下一个请求,无则返回NULL |
blk_start_request() |
开始处理指定请求 |
blk_fetch_request() |
一次性获取并开启请求(=peek+start) |
blk_end_request() |
请求中指定字节数据被处理完成 |
blk_end_request_all() |
请求中所有数据全部处理完成 |
blk_end_request_cur() |
完成当前请求中的 chunk |
blk_end_request_err() |
处理完请求,直到下一个错误产生 |
__blk_end_request() |
同 blk_end_request,但需持有队列锁 |
__blk_end_request_all() |
同 blk_end_request_all,需持有队列锁 |
__blk_end_request_cur() |
同 blk_end_request_cur,需持有队列锁 |
__blk_end_request_err() |
同 blk_end_request_err,需持有队列锁 |
关键函数原型(原书 68.2.4):
request *blk_peek_request(struct request_queue *q);
void blk_start_request(struct request *req);
struct request *blk_fetch_request(struct request_queue *q); /* peek + start */
/* 遍历请求中的所有 bio(原书示例代码 68.2.4.5) */
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
/* 遍历 bio 中的所有段(原书示例代码 68.2.4.6) */
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
void bio_endio(struct bio *bio, int error); /* 通知 bio 处理结束,error=0 表示成功 */
请求处理中还会用到这些辅助函数:
| 函数 | 说明 |
|---|---|
blk_rq_pos(req) |
获取请求要操作的扇区地址(左移9位转换为字节地址) |
blk_rq_cur_bytes(req) |
获取当前请求要处理的数据长度 |
rq_data_dir(req) |
判断请求方向(READ/WRITE) |
bio_data(req->bio) |
获取请求中 bio 的数据缓冲区 |
bio_data_dir(bio) |
判断 bio 方向(READ/WRITE) |
page_address(bvec.bv_page) |
获取 bio_vec 中页的虚拟地址 |
本实例使用RAM模拟2MB的块设备,实现完整的块设备驱动框架。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/blkdev.h>
#include <linux/blk_types.h>
#define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为2MB */
#define RAMDISK_NAME "ramdisk" /* 名字 */
#define RAMDISK_MINOR 3 /* 表示三个磁盘分区!不是次设备号为3! */
/* ramdisk设备结构体 */
struct ramdisk_dev {
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间 */
spinlock_t lock; /* 自旋锁 */
struct gendisk *gendisk; /* gendisk */
struct request_queue *queue; /* 请求队列 */
};
struct ramdisk_dev ramdisk; /* ramdisk设备 */
/* 打开块设备 */
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
/* 释放块设备 */
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}
/* 获取磁盘信息 */
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{
geo->heads = 2; /* 磁头 */
geo->cylinders = 32; /* 柱面 */
geo->sectors = RAMDISK_SIZE / (2 * 32 * 512); /* 每磁道扇区数 */
return 0;
}
/* 块设备操作函数 */
static struct block_device_operations ramdisk_fops = {
.owner = THIS_MODULE,
.open = ramdisk_open,
.release = ramdisk_release,
.getgeo = ramdisk_getgeo,
};
/* 处理传输过程 */
static void ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9; /* 扇区地址转字节地址 */
unsigned long len = blk_rq_cur_bytes(req); /* 数据长度 */
/* bio中的数据缓冲区 */
void *buffer = bio_data(req->bio);
if (rq_data_dir(req) == READ) /* 读数据 */
memcpy(buffer, ramdisk.ramdiskbuf + start, len);
else if (rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + start, buffer, len);
}
/* 请求处理函数 */
void ramdisk_request_fn(struct request_queue *q)
{
int err = 0;
struct request *req;
/* 循环处理请求队列中的每个请求 */
req = blk_fetch_request(q);
while (req != NULL) {
/* 针对请求做具体的传输处理 */
ramdisk_transfer(req);
/* 判断是否为最后一个请求 */
if (!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
/* 驱动入口函数 */
static int __init ramdisk_init(void)
{
int ret = 0;
/* 1、申请用于ramdisk内存 */
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if (ramdisk.ramdiskbuf == NULL) {
ret = -EINVAL;
goto ram_fail;
}
/* 2、初始化自旋锁 */
spin_lock_init(&ramdisk.lock);
/* 3、注册块设备 */
ramdisk.major = register_blkdev(0, RAMDISK_NAME);
if (ramdisk.major < 0) {
goto register_blkdev_fail;
}
printk("ramdisk major = %d\r\n", ramdisk.major);
/* 4、分配并初始化gendisk */
ramdisk.gendisk = alloc_disk(RAMDISK_MINOR);
if (!ramdisk.gendisk) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}
/* 5、分配并初始化请求队列 */
ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
if (!ramdisk.queue) {
ret = -EINVAL;
goto blk_init_fail;
}
/* 6、添加(注册)disk */
ramdisk.gendisk->major = ramdisk.major;
ramdisk.gendisk->first_minor = 0;
ramdisk.gendisk->fops = &ramdisk_fops;
ramdisk.gendisk->private_data = &ramdisk;
ramdisk.gendisk->queue = ramdisk.queue;
sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME);
set_capacity(ramdisk.gendisk, RAMDISK_SIZE / 512);
add_disk(ramdisk.gendisk);
return 0;
blk_init_fail:
put_disk(ramdisk.gendisk);
gendisk_alloc_fail:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf);
ram_fail:
return ret;
}
/* 驱动出口函数 */
static void __exit ramdisk_exit(void)
{
del_gendisk(ramdisk.gendisk);
put_disk(ramdisk.gendisk);
blk_cleanup_queue(ramdisk.queue);
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
kfree(ramdisk.ramdiskbuf);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");
对于eMMC、SD卡等非机械存储设备,可以不使用I/O调度器,直接处理bio。
static int __init ramdisk_init(void)
{
/* ... 省略相同部分 ... */
/* 5、分配请求队列(无I/O调度) */
ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
if (!ramdisk.queue) {
ret = -EINVAL;
goto blk_allo_fail;
}
/* 6、设置"制造请求"函数 */
blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);
/* 7、添加(注册)disk - 与上一个实验相同 */
/* ... */
}
/* "制造请求"函数 */
void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
{
int offset;
struct bio_vec bvec;
struct bvec_iter iter;
unsigned long len = 0;
offset = (bio->bi_iter.bi_sector) << 9; /* 获取设备偏移地址 */
/* 处理bio中的每个段 */
bio_for_each_segment(bvec, bio, iter) {
char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
len = bvec.bv_len;
if (bio_data_dir(bio) == READ) /* 读数据 */
memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
else if (bio_data_dir(bio) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + offset, ptr, len);
offset += len;
}
set_bit(BIO_UPTODATE, &bio->bi_flags);
bio_endio(bio, 0); /* 结束bio */
}
# 编译驱动,得到 ramdisk.ko
make
# 将 ramdisk.ko 拷贝到开发板根文件系统 rootfs/lib/modules/4.1.15 目录中
# 重启开发板后进入该目录:
cd /lib/modules/4.1.15
# 加载驱动
depmod # 首次加载需要
modprobe ramdisk.ko # 加载驱动模块
fdisk -l # 查看所有磁盘信息,其中包括 ramdisk
ls /dev/ramdisk # 查看设备节点
驱动加载成功后会在 /dev/ 下生成名为 ramdisk 的设备,fdisk -l 会显示其大小为 2MB,同时提示 /dev/ramdisk 没有分区表(因为尚未格式化)。
# 格式化为vfat格式
mkfs.vfat /dev/ramdisk
# 挂载到/tmp目录
mount /dev/ramdisk /tmp
# 测试读写
cd /tmp
echo "Hello Block Device" > test.txt
cat test.txt
# 卸载
umount /tmp
rmmod ramdisk # 卸载驱动模块
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
| 特性 | IMX6ULL | STM32MP1 | RK3568 |
|---|---|---|---|
| 主存储接口 | eMMC/SD | eMMC/SD | eMMC/SD/NVMe |
| eMMC控制器 | USDHC | SDMMC | SDHCI |
| 块大小 | 512字节 | 512字节 | 512字节 |
| DMA支持 | 有 | 有 | 有 |
| 最大频率 | 200MHz | 200MHz | 200MHz |
| 分区支持 | 支持 | 支持 | 支持 |
drivers/mmc/host/sdhci-esdhc-imx.cdrivers/mmc/host/stmmci.c所有平台的块设备驱动都遵循统一的Linux块设备框架,主要差异在于:
⚠️ 来源说明:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识。
答:
gendisk 和 request_queue,字符设备使用 cdevblock_device_operations,字符设备使用 file_operations答:
bio 是块设备I/O传输的基本单元,描述了要读写的起始扇区、数据长度、读写方向、页偏移等信息。
主要作用:
bio_vec 数组支持不连续内存的I/O操作bi_end_io 回调函数通知I/O完成bio 结构体是连接用户空间和块设备驱动的桥梁。
答:
requestbio 合并而成关系:request_queue → request → bio
使用 request_queue 的原因:
对于eMMC、SSD等无机械结构的设备,可以直接使用 blk_queue_make_request 绕过I/O调度。
答: 实现虚拟块设备驱动需要以下关键步骤:
register_blkdev() 申请设备号alloc_disk() 分配磁盘结构blk_init_queue() 绑定请求处理函数blk_alloc_queue() + blk_queue_make_request()add_disk() 注册到内核open、release、getgeo 等request_fn_proc 处理函数make_request_fn 处理函数答: Linux内核支持多种I/O调度算法:
NOOP(No Operation)
CFQ(Complete Fair Queuing)
Deadline
Anticipatory
选择建议:
内容来源: 《I.MX6U嵌入式Linux驱动开发指南》第六十八章 Linux 块设备驱动实验