RT-Thread 物理磁盘驱动编写指南:深入 `struct rt_blk_disk` 与 `rt_blk_disk_ops`
RT-Thread 物理磁盘驱动编写指南:深入 struct rt_blk_disk 与 rt_blk_disk_ops
RT-Thread 的 blk 块设备层将单个物理存储后端(NVMe、SDIO/MMC、SCSI 磁盘等)封装为统一的块设备,并自动解析分区表、生成可供 DFS 与应用使用的卷设备节点。本文以官方驱动编写指南(disk.md)为核心,结合 blk.h 与 blk.c 源码,系统讲解磁盘驱动从结构体填充、ops 实现到注册/注销的完整流程。读完本文,你将掌握如何为自有硬件编写一个可被 RT-Thread 分区探测与文件系统挂载的物理磁盘驱动。
一、blk 层架构:物理磁盘与卷的分层模型
在 RT-Thread 块设备子系统中,struct rt_blk_disk 代表"物理后备存储"——由驱动实现的、连续 LBA 地址空间中的一块存储介质。blk 核心层将其包装成一个 rt_device(块设备类 RT_Device_Class_Block),随后为核心层创建的卷(volume)子设备提供父级支撑。
三层结构如下:
| 层 | 数据结构 | 说明 |
|---|---|---|
| 物理磁盘 | struct rt_blk_disk |
本文主角,驱动通过 rt_blk_disk_ops 访问底层介质 |
| 分区表 | 磁盘上的元数据(GPT / DFS) | 由 blk_partition.c 解析 |
| 卷(分区) | struct rt_blk_device |
面向应用与 DFS 的设备节点,如 sda1、mmcsd0p1 |
数据流方向:驱动(NVMe / SDIO / SCSI SD / 自研) → rt_blk_disk_ops → struct rt_blk_disk(rt_hw_blk_disk_register 注册,内部自动触发 rt_blk_disk_probe_partition)→ struct rt_blk_device × N → DFS / 应用 / POSIX devio。
应用层通常打开卷设备(如 sda1)而非裸磁盘设备,除非探测逻辑创建了 partno == 0 的整盘卷。卷设备的读写会通过 blk_dev.c 转发到父磁盘节点,偏移量自动加上 sector_start。
实现文件:components/drivers/block/blk.c;头文件:components/drivers/include/drivers/blk.h。相关配套文档见 blk.md(应用层 API 与卷层)与 partitions.md(分区探测)。
二、struct rt_blk_disk:注册前的字段填充
在调用 rt_hw_blk_disk_register 之前,驱动必须正确填充 struct rt_blk_disk 各成员。该结构体定义于 blk.h:
struct rt_blk_disk
{
struct rt_device parent;
const struct rt_blk_disk_ops *ops;
#ifdef RT_USING_DM
struct rt_dm_ida *ida;
#endif
rt_uint32_t read_only : 1;
rt_uint32_t parallel_io : 1;
rt_uint32_t removable : 1;
#define RT_BLK_DISK_MAGIC 0xbdaabdaa
rt_uint32_t __magic;
rt_uint32_t partitions;
#define RT_BLK_PARTITION_NONE (-1)
#define RT_BLK_PARTITION_MAX (RT_UINT32_MAX >> 1)
rt_int32_t max_partitions;
rt_list_t part_nodes;
struct rt_spinlock lock;
struct rt_semaphore usr_lock;
};
各成员的所有权归属与含义如下表:
| 成员 | 归属 | 规则 / 含义 |
|---|---|---|
parent |
调用方 | 内嵌的 rt_device。注册前必须设置设备名(parent.parent.name 首字符 ≠ '\0',即非空字符串),否则注册返回 -RT_EINVAL |
ops |
调用方 | 必填,即下文要讲的 struct rt_blk_disk_ops |
ida |
调用方(RT_USING_DM) |
非 NULL,与所有卷子设备共享,用于 rt_dm_ida_alloc 分配设备号;NULL 时注册直接失败 |
read_only |
调用方 / 核心 | 若 ops->write == NULL,核心会在注册时强制置 read_only = RT_TRUE(见 blk.c) |
parallel_io |
调用方 | RT_TRUE 时磁盘读写跳过 usr_lock 互斥(NVMe 即使用此模式) |
removable |
调用方 | 可移动介质提示(如 SDIO 从 host 控制器属性设置) |
max_partitions |
调用方 | 分区解析器的数量上限;RT_BLK_PARTITION_NONE 表示禁用探测;RT_BLK_PARTITION_MAX 表示不限制 |
partitions |
核心 | 已注册的 rt_blk_device 节点数量(探测后由核心维护) |
part_nodes |
核心 | 卷列表,元素类型为 struct rt_blk_device |
lock |
核心 | 自旋锁,保护分区链表、注销与擦除路径 |
usr_lock |
核心 | 信号量(PRIO 优先级继承),parallel_io == 0 时串行化 I/O |
__magic |
核心 | 注册时置为 RT_BLK_DISK_MAGIC(0xbdaabdaa),供核心与 MSH 命令识别合法磁盘节点 |
其中 RT_BLK_PARTITION_NONE、RT_BLK_PARTITION_MAX 的定义与 struct rt_blk_device(卷结构体:partno、partition、sector_start、sector_count、disk 回指等)均在同头文件中。
三、struct rt_blk_disk_ops:驱动能力回调集
驱动通过 struct rt_blk_disk_ops 暴露介质访问能力,定义于 blk.h:
struct rt_blk_disk_ops
{
rt_ssize_t (*read)(struct rt_blk_disk *disk, rt_off_t sector, void *buffer,
rt_size_t sector_count);
rt_ssize_t (*write)(struct rt_blk_disk *disk, rt_off_t sector, const void *buffer,
rt_size_t sector_count);
rt_err_t (*getgeome)(struct rt_blk_disk *disk, struct rt_device_blk_geometry *geometry);
rt_err_t (*sync)(struct rt_blk_disk *disk);
rt_err_t (*erase)(struct rt_blk_disk *disk);
rt_err_t (*autorefresh)(struct rt_blk_disk *disk, rt_bool_t is_auto);
rt_err_t (*control)(struct rt_blk_disk *disk, struct rt_blk_device *blk, int cmd, void *args);
};
各回调的契约如下:
| 回调 | 是否必需 | 契约 |
|---|---|---|
read |
必选(实践中) | sector、sector_count 以 512 字节扇区为单位(除非 getgeome 声明其他扇区大小)。返回实际读出的扇区数,失败返回 < 0 错误码 |
write |
可选 | 置 NULL 表示磁盘只读(核心自动设 read_only)。寻址语义与 read 相同 |
getgeome |
必选(实践中) | 填充 sector_count(总容量扇区数)、bytes_per_sector(每扇区字节数)、block_size(擦除块大小,面向 Flash 类介质)。分区代码与 DFS 均依赖此回调 |
sync |
可选 | 刷写缓存;在注销与 RT_DEVICE_CTRL_BLK_SYNC 控制命令时被调用 |
erase |
可选 | 整盘擦除;磁盘节点的 ERASE IOCTL 会先移除全部分区再调用 |
autorefresh |
可选 | 对应 RT_DEVICE_CTRL_BLK_AUTOREFRESH(如 SD 卡的自动刷新场景) |
control |
可选 | 厂商自定义 IOCTL;blk 参数在裸磁盘路径为 NULL,在卷转发路径为非 NULL |
扇区寻址约定
- 参数是整盘上的 LBA 扇区索引,而不是字节偏移。
getgeome()->bytes_per_sector必须与read/write实际使用的扇区大小一致。getgeome()->sector_count表示整盘容量(扇区数)。- 若
parallel_io为真,read/write可能并发交叠执行——驱动内部必须自带锁或队列深度控制。
struct rt_device_blk_geometry 定义于 components/drivers/include/drivers/classes/block.h,字段为 sector_count(rt_uint64_t)、bytes_per_sector、block_size(均 rt_uint32_t)。
四、注册核心流程:rt_hw_blk_disk_register
rt_hw_blk_disk_register 是驱动接入 blk 层的唯一入口,实现在 blk.c。其执行序列为:
| 步骤 | 动作 |
|---|---|
| 1 | 校验 disk、ops、设备名、ida(DM 模式),任一非法返回 -RT_EINVAL |
| 2 | rt_dm_ida_alloc(disk->ida) 为磁盘分配设备号,失败返回 -RT_EFULL |
| 3 | 初始化 usr_lock(信号量,初值 1,RT_IPC_FLAG_PRIO)、part_nodes 链表、lock 自旋锁 |
| 4 | 在 parent 上挂接 blk_ops 或 blk_parallel_ops(取决于 parallel_io;RT_USING_DEVICE_OPS 启用时用 ops 表,否则直接赋值函数指针) |
| 5 | 若 !ops->write,置 read_only = 1 |
| 6 | device_set_blk_fops 挂文件系统操作,rt_device_register 以 RD/WR 标志注册设备(flags 初始为 RT_DEVICE_FLAG_RDONLY,非只读时追加 RT_DEVICE_FLAG_WRONLY) |
| 7 | 调用 rt_blk_disk_probe_partition(disk) 触发分区探测——探测错误被忽略,不影响注册成功与否 |
返回值语义:仅返回注册过程本身的错误;分区探测失败不会导致注册失败。因此注册成功后应主动检查 disk->partitions 或直接尝试打开卷设备,判断介质是否含有可用分区。
内部设备操作(disk 节点)
注册后,磁盘节点的 rt_device 读写/控制按 parallel_io 分支:
| 操作 | parallel_io == 0 |
parallel_io == 1 |
|---|---|---|
| read/write | 先 rt_sem_take(&disk->usr_lock, RT_WAITING_FOREVER) 再调用 ops->read/write,最后释放 |
直接调用 ops->read/write,不加锁 |
| control | blk_control 分发到 ops |
同上 |
blk_control(blk.c)的标准处理包括:RT_DEVICE_CTRL_BLK_GETGEOME(转发 ops->getgeome)、RT_DEVICE_CTRL_BLK_SYNC(在 usr_lock + lock 下调用 ops->sync)、RT_DEVICE_CTRL_BLK_ERASE(要求 ref_count == 1,先 blk_remove_all 清空全部分区再执行 ops->erase,否则返回 -RT_EBUSY)、RT_DEVICE_CTRL_BLK_AUTOREFRESH(转发 ops->autorefresh(disk, !!args));未识别命令落入 default 分支转发 ops->control(disk, RT_NULL, cmd, args)。
特别地:对磁盘节点发送 RT_DEVICE_CTRL_BLK_PARTITION 固定返回 -RT_EINVAL——分区信息只能通过卷设备查询。
此外,在启用 RT_USING_DFS_MNTTABLE 时,核心还会通过 INIT_ENV_EXPORT(blk_dfs_mnt_table) 自动挂载所有块设备及其卷;启用 MSH 时提供 list_blk 命令(blk.c),以 lsblk 风格输出每个磁盘/分区的 MAJ:MIN、可移动标志(RM)、容量(自动换算 B/K/M/G/T/P/E)、只读标志(RO)、类型(disk/part)与挂载点。
五、注销流程:rt_hw_blk_disk_unregister
rt_hw_blk_disk_unregister(blk.c)的注销序列为:
| 步骤 | 动作 |
|---|---|
| 1 | disk->parent.ref_count > 0 → 返回 -RT_EBUSY(设备仍被占用) |
| 2 | 若存在 ops->sync,先刷写(失败则中止注销并 LOG_E) |
| 3 | blk_remove_all——逐个注销所有 rt_blk_device 卷节点 |
| 4 | rt_dm_ida_free 释放磁盘设备号,rt_device_unregister 注销设备节点 |
注销前务必先卸载 DFS 文件系统、关闭所有打开句柄(close 到 ref_count == 0),否则返回 -RT_EBUSY。热插拔场景的标准动作:拔卡 → 注销;插卡 → 重新注册(此时会重新触发分区探测)。
六、辅助 API
| API | 作用 |
|---|---|
rt_blk_disk_probe_partition(disk) |
仅在 disk->partitions == 0 时重新运行分区解析器;详见 partitions.md |
rt_blk_disk_get_capacity(disk) |
调用 getgeome 返回 sector_count(总容量,扇区数) |
rt_blk_disk_get_logical_block_size(disk) |
调用 getgeome 返回 bytes_per_sector(逻辑块大小,字节) |
这两个获取函数实现在 blk.c:内部先执行 ops->getgeome,成功则返回对应几何字段,disk == NULL 返回 -RT_EINVAL,回调失败则透传错误码。
七、分区探测如何工作(驱动侧须知)
注册的最后一步 rt_blk_disk_probe_partition(blk_partition.c)执行如下判定:
disk == NULL→-RT_EINVAL;disk->partitions != 0→ 已探测过,直接成功返回;max_partitions == RT_BLK_PARTITION_NONE→-RT_EEMPTY(禁用分区表解析);- 依次调用
partition_listi中的解析器; - 解析器返回
-RT_ENOMEM→ 中止并返回-RT_ENOMEM; - 解析器返回
0(成功)→ 停止; - 所有解析器均未匹配 → 回退创建整盘卷:
blk_put_partition(disk, NULL, 0, total_sectors, 0)。
因此即使介质为空白盘或未知分区格式,探测仍会生成一个覆盖整盘的卷设备(partno == 0),保证 DFS 可以挂载。解析器顺序由 blk_partition.c 的 partition_list[] 决定:EFI(GPT)在前,DFS 在后,第一个返回 0 的解析器胜出。
Kconfig 开关见 partitions/Kconfig:
| 选项 | 解析器文件 | 作用 |
|---|---|---|
RT_USING_BLK |
components/drivers/block/ 主开关(Kconfig) |
使能整个 blk 层 |
RT_BLK_PARTITION_EFI |
partitions/efi.c |
保护性 MBR + GPT(UEFI 风格),默认 y |
RT_BLK_PARTITION_DFS |
partitions/dfs.c |
DFS 传统分区表(扇区 0 的超块布局),依赖 RT_USING_DFS,默认 y |
卷设备的命名规则在 blk_dev.c 的 disk_add_blk_dev 中:若磁盘名末字符是字母 a–z(如 sda),卷命名为 sdap1(%sp%d);否则(如 mmcsd0、nvme0n1)卷命名为 mmcsd0p1、nvme0n1p2(%s%d)。
八、树内集成实例:四种成熟驱动模板
NVMe(components/drivers/nvme/nvme.c)
NVMe 驱动为**每个命名空间(namespace)**创建独立的 rt_blk_disk(nvme.c):
- 命名:
rt_dm_dev_set_name(&ndev->parent.parent, "%sn%u", nvme->name, nsid)——命名空间从 1 起,得到nvme0n1风格名称; parallel_io = RT_TRUE(NVMe 硬件天然支持并发队列),ops = &nvme_blk_ops,ida = &nvme_ida;max_partitions = RT_BLK_PARTITION_MAX(不限分区数);- Identify 完成后调用
rt_hw_blk_disk_register(&ndev->parent)。
SDIO / MMC(components/drivers/sdio/dev_block.c)
rt_mmcsd_blk_probe(dev_block.c)的要点:
- 几何信息取自卡 CSD:
bytes_per_sector = MMCSD_SECTOR_SIZE(512)、block_size = card->card_blksize、sector_count = card->card_capacity * (1024 / MMCSD_SECTOR_SIZE); - 注册前先
mmcsd_set_blksize(card)(代码注释特别强调"Set blk size before partitions probe"); removable = controller_is_removable(host),max_partitions = RT_MMCSD_MAX_PARTITION(默认 16,见 Kconfig 与 dev_block.c);- 名称取自
host->name,卷设备形如hostp1(mmcsd0p1)。
SCSI 磁盘(components/drivers/scsi/scsi_sd.c)
scsi_sd_probe(scsi_sd.c)流程:
- 先用
rt_scsi_read_capacity10读容量,若last_block == 0xffffffff再升级到rt_scsi_read_capacity16(大容量磁盘); - 几何:
bytes_per_sector = block_size、block_size = block_size、sector_count = last_block + 1; - 命名:
rt_dm_dev_set_name(&ssd->parent.parent, "sd%c%c", letter_name(ssd->sd_id)),得到sda、sdb风格; parallel_io直接取自 SCSI host(sdev->host->parallel_io),ida = &scsi_sd_ida,max_partitions = RT_BLK_PARTITION_MAX;- 注销路径
scsi_sd_remove先释放sd_ida槽位再调rt_hw_blk_disk_unregister。
SCSI CD-ROM(components/drivers/scsi/scsi_cdrom.c)
采用相同的 blk 注册模式;光学介质通常为只读(write 缺省或只读标志),且 max_partitions = RT_BLK_PARTITION_NONE(scsi_cdrom.c)——CD 盘无分区表概念,禁用探测。
另外 virtio-blk.c 也使用了 RT_BLK_PARTITION_MAX,可作为虚拟化场景的参考。
九、最小驱动骨架(可直接照抄)
综合以上模式,一个最小可用的物理磁盘驱动骨架如下(结构参照 blk.md 的 Minimal driver pattern):
static const struct rt_blk_disk_ops my_blk_ops = {
.read = my_read,
.write = my_write,
.getgeome = my_getgeome,
.sync = my_sync, /* 可选:刷写缓存 */
/* .erase / .autorefresh / .control 按需实现 */
};
static struct rt_dm_ida my_ida = RT_DM_IDA_INIT(MY_DISK);
static rt_ssize_t my_read(struct rt_blk_disk *disk, rt_off_t sector,
void *buffer, rt_size_t sector_count)
{
/* 按 LBA 扇区寻址;若 parallel_io,需自带并发保护 */
return my_hw_read(sector, buffer, sector_count);
}
static rt_err_t my_getgeome(struct rt_blk_disk *disk,
struct rt_device_blk_geometry *geometry)
{
geometry->bytes_per_sector = MY_SECTOR_SIZE; /* 如 512 */
geometry->block_size = MY_ERASE_SIZE; /* Flash 擦除块大小 */
geometry->sector_count = MY_TOTAL_SECTORS; /* 整盘容量 */
return RT_EOK;
}
void my_disk_register(struct my_hw *hw)
{
struct rt_blk_disk *disk = &hw->blk;
disk->ops = &my_blk_ops;
disk->ida = &my_ida;
disk->parallel_io = RT_FALSE; /* ops 可重入时才设 RT_TRUE */
disk->removable = RT_FALSE;
disk->max_partitions = RT_BLK_PARTITION_MAX; /* 或按需限定数量 */
rt_dm_dev_set_name(&disk->parent.parent, "sda"); /* 示例命名 */
rt_hw_blk_disk_register(disk); /* 注册并自动触发分区探测 */
}
注意:在启用 RT_USING_DM 的配置下,ida 必须使用 RT_DM_IDA_INIT 初始化并赋给 disk->ida,且磁盘名必须在注册前通过 rt_dm_dev_set_name 设置完成。
十、DM 命名与 IDA 分配清单
| 步骤 | 动作 |
|---|---|
| 1 | 每个磁盘家族(family)定义并初始化一个 IDA,例如 scsi_sd_ida、nvme_ida、sdio_ida |
| 2 | 注册前把 disk->ida 指向对应 IDA |
| 3 | 磁盘名通过 rt_dm_dev_set_name(&disk->parent.parent, ...) 生成 |
| 4 | 卷设备由探测逻辑自动命名(disk_add_blk_dev)——不要自行注册 rt_blk_device,除非你绕开 blk_put_partition 手动管理卷 |
每个卷在注册时还会 rt_dm_ida_alloc(disk->ida) 占用一个 IDA 槽位,且与磁盘共享同一个 master_id。
十一、驱动注意事项速查
| 主题 | 指引 |
|---|---|
| DMA / 缓冲区 | 若调用方传入非 DMA 安全指针,必须在 ops 内部做 bounce 缓冲拷贝 |
| 容量变化 | 先 rt_hw_blk_disk_unregister,更新几何信息,再重新注册 |
| 热插拔 | 拔卡 → rt_hw_blk_disk_unregister;插卡 → 重新注册(自动重跑分区探测) |
| GPT + DFS 并存 | 同时使能两个 Kconfig 解析器;partition_list[] 中 EFI 先执行 |
| 自定义 IOCTL | 实现 control(disk, blk, cmd, args);卷路径未识别命令由 blk_control 的 default 分支转发(此时 blk 非 NULL) |
| 只读介质 | ops 中省略 write,核心自动置只读,写路径返回 -RT_ENOSYS |
| 注销前置条件 | 先卸载 DFS、关闭所有句柄;ref_count > 0 时返回 -RT_EBUSY |
| 擦除语义 | 磁盘 ERASE 会先移除全部分区;卷设备上 ERASE 仅在 disk->partitions <= 1 时允许,否则 -RT_EIO |
延伸阅读
- blk.md — 应用层 API、卷设备与 DFS 集成
- partitions.md — GPT/EFI 与 DFS 分区解析器细节
- ata.md — AHCI → SCSI → blk 链路
- blk.h — 核心数据结构与 API 声明
- blk.c — 注册/注销/控制核心实现
- blk_partition.c — 分区探测实现