FEMU—NVMe ZNS 核心实现的学习
底层逻辑
一、核心数据结构
NvmeNamespace、FemuCtrl、NvmeZone
在 FEMU 的源代码(通常在 hw/block/femu/ 路径下)中,NvmeNamespace、FemuCtrl、NvmeZone 是构成整个 NVMe 模拟栈的三个核心结构,分别对应 命名空间(Namespace)、控制器(Controller) 和 ZNS 区域(Zone Namespace) 的不同层次。
一、三者关系总览
| 元素 | 所在文件 | 主要作用 | 上下级关系 | 关键字段 / 指针关系 |
|---|---|---|---|---|
| FemuCtrl | femu/nvme.c、femu/femu.h | 整个 NVMe 控制器的核心结构,管理所有命名空间、队列、寄存器、模拟设备状态 | 顶层控制模块,包含多个 NvmeNamespace | FemuCtrl -> namespaces[NvmeNamespace];FemuCtrl -> zns_ctrl(若启用 ZNS) |
| NvmeNamespace | femu/nvme-ns.c、femu/nvme.h | 表示 NVMe 命名空间(即逻辑存储卷),负责 LBA 映射、数据管理、读写操作 | 由 FemuCtrl 创建和管理;若为 ZNS 类型,则含有多个 NvmeZone | NvmeNamespace -> FemuCtrl *ctrl(反向指针);NvmeNamespace -> NvmeZone *zones(若启用 ZNS) |
| NvmeZone | femu/nvme-zns.c、femu/nvme.h | 表示 Zoned Namespace 模式下的单个 Zone 区域;管理其写指针、状态、容量等 | 隶属于某个 NvmeNamespace,被统一调度 | NvmeZone -> NvmeNamespace *ns;NvmeZone -> 状态、写指针、剩余容量等 |
二、结构层次示意图(逻辑关系)
+---------------------------------------------------+
| FemuCtrl |
|---------------------------------------------------|
| 控制寄存器 | 队列管理 | IO处理 | ZNS支持 | |
| ... |
| +-------------------------------------------+ |
| | NvmeNamespace (nsid=1) | |
| |-------------------------------------------| |
| | LBA->PPA映射表 | 媒体模拟 | ZNS结构体指针 | |
| | +------------------------------+ | |
| | | NvmeZone[0] ... NvmeZone[N] |<----------+ |
| | +------------------------------+ |
| +-------------------------------------------+ |
| | NvmeNamespace (nsid=2) ... | |
+---------------------------------------------------+
三、关键交互机制
| 交互类型 | 说明 | FEMU源码中的体现 |
|---|---|---|
| 初始化阶段 | FemuCtrl 在初始化时调用 nvme_init_namespaces() 来创建并初始化 NvmeNamespace;若是 ZNS 模式,则进一步调用 nvme_zns_init_zones() 初始化 NvmeZone | hw/block/femu/nvme.c 中的 nvme_realize() |
| I/O命令处理 | 控制器接收 NVMe 命令后,由 nvme_io_cmd() 分发到对应命名空间(通过 nsid);若为 ZNS 命令,则由 nvme_zns_* 系列函数进一步处理 Zone 操作 | nvme_cmd.c、nvme-zns.c |
| ZNS写入限制 | NvmeZone 通过写指针(wp)和状态(state)限制顺序写入和重置操作 | nvme_zns_write()、nvme_zns_reset() |
| 元数据管理 | NvmeNamespace 持有逻辑块信息、媒体模拟层接口和 Zone 信息,协调读写与元数据一致性 | nvme_ns.c |
四、结构体核心字段对比(简化版)
| 结构体 | 关键字段 | 含义 |
|---|---|---|
FemuCtrl | NvmeNamespace *namespaces; | 命名空间数组 |
uint32_t num_namespaces; | 命名空间数量 | |
NvmeZnsCtrl *zns_ctrl; | 若为ZNS控制器,则管理Zone命令逻辑 | |
NvmeNamespace | FemuCtrl *ctrl; | 指向控制器的反向指针 |
NvmeZone *zone_array; | Zone信息数组 | |
uint64_t nsze; | 命名空间大小(以LBA计) | |
NvmeZone | uint64_t zslba, zcap, wp; | Zone起始地址、容量、写指针 |
uint8_t state; | Zone当前状态(Empty/Open/Closed/Full) | |
NvmeNamespace *ns; | 所属命名空间指针 |
五、总结一句话关系:
FemuCtrl 是整个 NVMe 控制器的“大脑”;
NvmeNamespace 是它所管理的“逻辑卷”;
NvmeZone 是 ZNS 模式下命名空间的“物理分区”。
三者形成了自上而下的 控制器 → 命名空间 → 区域(Zone) 层级关系。
ZNS 的核心逻辑围绕 “区域(Zone)” 和 “命名空间(Namespace)” 的管理展开,关键数据结构如下:
1. 区域(Zone)的定义
zns.h中定义的NvmeZone结构体是单个 ZNS 区域的核心描述,包含区域状态、容量、位置等关键信息:
typedef struct NvmeZone {NvmeZoneDescr d; // 区域描述符(包含类型、状态、容量等元数据)uint64_t w_ptr; // 实际写指针(记录当前写入位置,单位LBA)int mode; // 存储模式(0:SLC,1:QLC)int accessed; // 访问标记(用于定期清理)QTAILQ_ENTRY(NvmeZone) entry; // 链表节点(用于状态链表管理)int pos; // 区域在数组中的索引
} NvmeZone;
typedef struct QTailQLink {void *tql_next;struct QTailQLink *tql_prev;
} QTailQLink;#define QTAILQ_ENTRY(type) \
union { \struct type *tqe_next; /* next element */ \QTailQLink tqe_circ; /* link for circular backwards list */ \
}
QTAILQ_ENTRY(NvmeZone) entry;是一个与 QTAILQ 队列数据结构相关的声明。
QTAILQ 是 FreeBSD 内核中的一种队列数据结构,主要用于实现双向有尾链表6。QTAILQ_ENTRY是一个宏,用于定义队列中的元素结构5。
根据QTAILQ_ENTRY的定义,QTAILQ_ENTRY(NvmeZone)展开后会得到一个结构体,该结构体包含两个成员变量4:
tqe_next:这是一个指针,指向队列中的下一个元素。tqe_prev:这是一个二级指针,指向队列中前一个元素的tqe_next指针的地址。
在NvmeZone结构体中声明QTAILQ_ENTRY(NvmeZone) entry;,意味着NvmeZone结构体可以作为 QTAILQ 队列中的一个元素,通过entry这个成员变量来链接到队列中的其他元素,从而实现链表的功能。
其中,NvmeZoneDescr(区域描述符)是 NVMe 协议定义的标准结构,包含:
NvmeZoneDescr
typedef struct QEMU_PACKED NvmeZoneDescr {uint8_t zt; // 区域类型(如NVME_ZONE_TYPE_SEQ_WRITE:顺序写入区域)uint8_t zs; // 区域状态(高4位有效,如EMPTY、OPEN、CLOSED等)uint8_t za; // 区域属性(如是否需要重置、扩展信息是否有效等)uint64_t zcap; // 区域容量(单位LBA)uint64_t zslba; // 区域起始LBA(逻辑块地址)uint64_t wp; // 协议层写指针(与w_ptr同步,用于对外暴露)
} NvmeZoneDescr;
NvmeZoneState
区域状态通过NvmeZoneState枚举定义,符合 NVMe 协议:
enum NvmeZoneState {NVME_ZONE_STATE_EMPTY = 0x01, // 空(未写入数据)NVME_ZONE_STATE_IMPLICITLY_OPEN = 0x02, // 隐式打开(由写入操作触发)NVME_ZONE_STATE_EXPLICITLY_OPEN = 0x03, // 显式打开(由Open命令触发)NVME_ZONE_STATE_CLOSED = 0x04, // 关闭(可重新打开)NVME_ZONE_STATE_FULL = 0x0E, // 满(无法再写入)NVME_ZONE_STATE_READ_ONLY = 0x0D, // 只读NVME_ZONE_STATE_OFFLINE = 0x0F // 离线
};
2. ZNS 命名空间的管理结构
ZNS 命名空间是 NVMe 设备上的一个分区,专门用于 ZNS 特性,其管理依赖以下结构:
NvmeNamespaceParams(zns.h):存储 ZNS 命名空间的配置参数:
typedef struct NvmeNamespaceParams {bool zoned; // 是否启用ZNS特性bool cross_zone_read; // 是否允许跨区域读uint64_t zone_size_bs; // 区域大小(字节)uint64_t zone_cap_bs; // 区域容量(字节)uint32_t max_active_zones; // 最大活动区域数uint32_t max_open_zones; // 最大打开区域数
} NvmeNamespaceParams;
命名空间是存储设备的逻辑分区,而Zone是ZNS命名空间内的顺序写入区域。一个ZNS命名空间可以包含成百上千个Zone。
FemuCtrl
-
FemuCtrl(隐含在代码中):设备控制结构体,包含 ZNS 管理的核心数据: -
FemuCtrl是 Femu 模拟器中 NVMe 设备的 “大脑”,它整合了 ZNS 区域管理、命名空间配置、命令队列、中断处理、性能模拟等所有核心逻辑。通过该结构体,Femu 能够模拟 NVMe ZNS 设备的完整行为,包括区域状态转换、顺序写入控制、与主机的命令交互等,是理解 Femu 中 ZNS 底层实现的关键数据结构。// 从代码片段推断的关键字段 struct FemuCtrl {NvmeZone *zone_array; // 区域数组(所有区域的集合)uint32_t num_zones; // 区域总数uint64_t zone_size; // 区域大小(单位LBA)uint64_t zone_capacity; // 区域容量(单位LBA)uint32_t max_open_zones; // 最大打开区域数限制uint32_t max_active_zones; // 最大活动区域数限制// 状态链表(按区域状态分类管理)QTAILQ_HEAD(, NvmeZone) exp_open_zones; // 显式打开的区域QTAILQ_HEAD(, NvmeZone) imp_open_zones; // 隐式打开的区域QTAILQ_HEAD(, NvmeZone) closed_zones; // 关闭的区域QTAILQ_HEAD(, NvmeZone) full_zones; // 满的区域 };
typedef struct FemuCtrl { ... } 是 Femu(一个 NVMe 设备模拟器)中定义的核心控制器结构体,用于整合管理 NVMe 设备的所有硬件配置、运行状态、资源(如命名空间、队列、区域等)及功能逻辑。它是 Femu 模拟 NVMe 设备的 “总控中心”,几乎包含了设备运行所需的全部关键信息。
以下是其核心字段的分类解析(结合 NVMe 协议和 ZNS 特性):
1. 基础硬件与 PCI 设备属性
PCIDevice parent_obj:继承 QEMU 的PCIDevice结构体,表明该控制器是一个 PCI 设备,用于兼容 PCIe 总线规范(NVMe 设备基于 PCIe 接口)。MemoryRegion iomem和ctrl_mem:内存区域对象,模拟 NVMe 设备的 I/O 映射空间(如控制器寄存器、BAR 空间等),供主机通过内存映射访问设备。NvmeBar bar:NVMe 基地址寄存器(Base Address Register)配置,用于管理设备的内存映射地址范围。
2. ZNS 特性核心管理字段
(这部分与之前分析的 ZNS 逻辑直接关联)
struct zonessd *znsssd:指向 ZNS SSD 的底层存储控制结构,关联 ZNS 区域与物理存储介质的映射(FTL 层)。bool zoned:标识该设备是否启用 ZNS 特性。uint64_t zone_size_bs/zone_cap_bs:区域大小(字节)和区域容量(字节,ZNS 中容量可能小于大小,因存在元数据开销)。uint32_t max_active_zones/max_open_zones:ZNS 协议限制的最大活动区域数和最大打开区域数(符合 NVMe ZNS 规范的 AOR 机制)。NvmeZone *zone_array:所有 ZNS 区域的数组,每个元素对应一个区域的元数据(状态、写指针等)。- 状态链表头:
exp_open_zones(显式打开的区域)、imp_open_zones(隐式打开的区域)、closed_zones(关闭的区域)、full_zones(满的区域),用于按状态高效管理区域。 uint32_t num_zones:总区域数量;uint64_t zone_size/zone_capacity:区域大小 / 容量(单位 LBA)。
3. 命名空间与存储配置
NvmeNamespace *namespaces:指向命名空间数组,NVMe 设备通过命名空间划分存储区域(类似磁盘分区),ZNS 特性通常绑定在特定命名空间上。uint32_t num_namespaces:命名空间总数。uint64_t ns_size:设备总存储大小;uint8_t flash_type:模拟的闪存类型(如 SLC/QLC 等,影响 ZNS 区域的读写性能)。
4. 队列管理(NVMe 核心机制)
NVMe 通过提交队列(SQ)和完成队列(CQ)处理命令,相关字段包括:
NvmeSQueue **sq/NvmeCQueue **cq:I/O 提交队列和完成队列数组(管理多个 I/O 队列)。admin_sq/admin_cq:管理命令的提交队列和完成队列(用于处理 NVMe 管理命令,如创建命名空间、查询区域状态等)。uint32_t max_q_ents:队列最大条目数;uint16_t sqid/cqid:队列 ID 标识。
5. 中断与事件处理
int32_t virq:虚拟中断号,用于向主机报告设备事件(如命令完成、错误等)。EventNotifier guest_notifier:通知机制,用于在命令完成时唤醒主机处理。QSIMPLEQ_HEAD(aer_queue, NvmeAsyncEvent) aer_queue:异步事件队列(如区域状态变化、错误通知等,符合 NVMe AER 机制)。
6. 性能与调试
- 延迟字段:
upg_rd_lat_ns(上层页读延迟)、blk_er_lat_ns(块擦除延迟)等,模拟不同闪存页 / 块的操作耗时。 - 统计字段:
nr_tt_ios(总 I/O 数)、nr_tt_late_ios(延迟 I/O 数),用于性能分析。 femu_mode:模拟器运行模式(如FEMU_ZNSSD_MODE表示 ZNS 模式)。
3. FTL 层辅助结构(znsftl.h)
zonessd结构体用于 ZNS 的闪存转换层(FTL)管理,关联物理存储与逻辑区域:
struct zonessd {struct zonessd_params sp; // SSD硬件参数(如页大小、块大小等)struct ssd_channel *ch; // 存储通道(物理存储介质)struct zone_block *maptbl; // 区域-物理块映射表(逻辑区域到物理块的映射)zone *zone_array; // FTL层的区域数组(与NvmeZone对应)uint64_t zone_size_bs; // 区域大小(字节)int zone_num; // 区域总数// 容量统计uint64_t total_cap; // 总容量uint64_t used_cap; // 已用容量uint64_t used_slc_cap; // SLC模式已用容量uint64_t used_qlc_cap; // QLC模式已用容量
};
struct zonessd 是FEMU中用于模拟Zoned Namespace SSD的核心数据结构,它代表了整个ZNS SSD设备的内部状态和组织结构。
主要成员变量说明
-
sp:zonessd_params结构体,存储SSD的基本参数配置,如页面大小、块大小、通道数量等物理特性 -
ch:ssd_channel结构体指针,指向SSD的通道数组,每个通道包含多个LUN(Logic Unit Number) -
maptbl:zone_block结构体指针,作为zone到物理block的映射表,用于跟踪每个zone在SSD中的物理位置 -
zone_array:zone结构体指针,指向zone数组,管理所有zone的状态和写指针 -
zone_size_bs: 每个zone的大小(以字节为单位) -
zone_num: SSD中zone的总数量 -
zone_size: 每个zone包含的页面数量 -
zone_size_log2: zone大小的对数表示,用于快速计算,如果zone大小不是2的幂则为0
HZNS相关字段
total_cap: 总容量used_cap: 已使用容量used_slc_cap: 已使用的SLC模式容量used_qlc_cap: 已使用的QLC模式容量
这个结构体连接了逻辑的ZNS接口和物理的SSD存储结构,是实现ZNS功能的关键数据结构。
二、核心函数(zns.c)
ZNS 的核心逻辑通过区域的初始化、状态管理、IO 操作控制等函数实现:
1. 区域初始化与几何参数计算
zns_init_zone_geometry
zns_init_zone_geometry:初始化区域的几何参数(大小、容量、数量),并校验合法性:
static int zns_init_zone_geometry(NvmeNamespace *ns, Error **errp) {// 1. 确定区域大小(优先使用用户配置,否则用默认值NVME_DEFAULT_ZONE_SIZE)// 2. 确定区域容量(优先用户配置,否则等于区域大小)// 3. 校验:容量≤大小、区域大小≥LBA大小、容量≥LBA大小// 4. 计算区域数量(命名空间总LBA数 / 每个区域的LBA数)// 5. 校验最大打开/活动区域数不超过区域总数
}
具体解释:
zns_ns_lbads(ns) 获取命名空间的逻辑块地址移位值
1 << zns_ns_lbads(ns) 将1左移相应的位数
结果存储在 lbasz 变量中,表示逻辑块的实际大小(以字节为单位)
例如:如果 zns_ns_lbads(ns) 返回9,则 lbasz = 1 << 9 = 512 字节。
这是一个位运算操作,让我来解释一下计算过程:
位运算左移操作
1 << 9 表示将数字1向左移动9位,计算过程如下:
- 初始值:
1(二进制:00000001) - 左移1位:
1 << 1 = 2(二进制:00000010) - 左移2位:
1 << 2 = 4(二进制:00000100) - 左移3位:
1 << 3 = 8(二进制:00001000) - …
- 左移9位:
1 << 9 = 512(二进制:1000000000)
数学原理
左移操作相当于乘以2的幂次:
1 << n = 1 × 2^n = 2^n
因此:
1 << 9 = 2^9 = 512
在代码中的意义
在 zns.c 中,这个计算用于确定逻辑块大小(LBA Size):
zns_ns_lbads(ns)返回的是LBADS(Logical Block Address Shift)值- LBADS表示逻辑块地址移位的位数
- 通过
1 << lbads计算出实际的逻辑块大小(以字节为单位)
例如LBADS为9时,表示每个逻辑块大小为512字节,这是NVMe设备中常见的扇区大小。
zns_init_zoned_state
-
zns_init_zoned_state:初始化所有区域的状态和元数据:static void zns_init_zoned_state(NvmeNamespace *ns) {// 1. 分配区域数组(zone_array)和扩展信息内存// 2. 初始化状态链表(exp_open_zones等)// 3. 为每个区域设置初始状态:// - 类型:NVME_ZONE_TYPE_SEQ_WRITE(顺序写入)// - 状态:NVME_ZONE_STATE_EMPTY(空)// - 起始LBA(zslba)、写指针(wp=zslba)、容量(zcap) }zns_init_zoned_state函数是 Femu 模拟器中 ZNS(Zoned Namespace)特性的核心初始化函数之一,主要作用是为 ZNS 命名空间初始化所有区域(Zone)的元数据和管理结构,确保区域符合 NVMe ZNS 协议规范并能被正确管理。
函数核心功能解析:
-
分配区域数据结构
- 为控制器(
FemuCtrl)的zone_array分配内存,用于存储所有区域的元数据(NvmeZone结构体),数量为n->num_zones(总区域数)。 - 若配置了区域描述符扩展(
zd_extension_size),则分配对应大小的内存(zd_extensions),用于存储区域的扩展信息。
- 为控制器(
-
初始化区域状态管理链表
- 初始化 4 个双向链表(
exp_open_zones、imp_open_zones、closed_zones、full_zones),这些链表用于按状态分类管理区域:- 显式打开的区域(
exp_open_zones) - 隐式打开的区域(
imp_open_zones) - 关闭的区域(
closed_zones) - 满的区域(
full_zones)
- 显式打开的区域(
- 这种分类管理符合 NVMe ZNS 协议对区域状态转换的要求,便于高效查询和操作特定状态的区域。
- 初始化 4 个双向链表(
-
初始化每个区域的元数据
遍历所有区域(
n->num_zones),为每个区域设置初始属性:pos:区域索引(唯一标识)。d.zt:区域类型,固定为NVME_ZONE_TYPE_SEQ_WRITE(顺序写入区域,ZNS 的核心特性)。- 初始状态:通过
zns_set_zone_state设置为NVME_ZONE_STATE_EMPTY(空状态,区域创建时的初始状态)。 d.zcap:区域容量(LBA 数),取自控制器配置的zone_capacity。d.zslba:区域起始 LBA(逻辑块地址),从 0 开始按区域大小递增。- 写指针(
d.wp和w_ptr):初始化为区域起始 LBA(空区域的写指针未移动)。 mode:存储介质模式(SLC/QLC),由hzns_mode(混合 ZNS 模式)决定,纯 QLC 模式下为 QLC,否则为 SLC。
-
计算区域大小的对数(优化索引计算)
若区域大小(
n->zone_size)是 2 的幂,计算其对数(zone_size_log2),用于后续通过位移操作快速计算区域索引(如zns_zone_idx函数中通过slba >> zone_size_log2定位区域),提升效率。
2. 区域状态管理
ZNS 通过状态链表(显式打开、隐式打开、关闭、满)管理区域,核心函数:
zns_assign_zone_state
-
zns_assign_zone_state:修改区域状态(核心状态转换逻辑):static void zns_assign_zone_state(NvmeNamespace *ns, NvmeZone *zone, NvmeZoneState state) {// 1. 从当前状态的链表中移除区域(如从closed_zones移除)// 2. 更新区域状态(zs字段)// 3. 将区域加入新状态的链表(如加入exp_open_zones) }
zns_assign_zone_state 函数的核心作用是 更新 ZNS 设备中区域(zone)的状态,并维护区域在对应状态队列中的归属关系。它会先将区域从当前所在的状态队列中移除,再将其添加到新状态对应的队列中,确保控制器能准确跟踪所有区域的状态(如空、打开、关闭、满等),符合 NVMe ZNS 协议对区域状态管理的要求。
使用场景
当需要对区域执行状态转换操作时(如打开、关闭、完成、重置等),都会调用该函数。例如:
- 打开区域时,将状态从 “空” 或 “关闭” 转换为 “显式打开”;
- 关闭区域时,将状态从 “显式打开” 或 “隐式打开” 转换为 “关闭”;
- 区域写满时,将状态转换为 “满”;
- 重置区域时,将状态从 “满” 或 “关闭” 转换为 “空”。
代码中实际使用的例子:打开区域(zns_open_zone 函数)
在处理 “打开区域” 命令时,会通过 zns_assign_zone_state 将区域状态更新为 “显式打开”,并加入对应的队列。
static uint16_t zns_open_zone(NvmeNamespace *ns, NvmeZone *zone,NvmeZoneState state, NvmeRequest *req)
{uint16_t status;// ... 省略参数检查和模式设置逻辑 ...switch (state) {case NVME_ZONE_STATE_EMPTY:// ... 省略资源检查逻辑 ...zns_aor_inc_active(ns);/* 继续处理下一个状态 */case NVME_ZONE_STATE_CLOSED:// ... 省略资源检查逻辑 ...zns_aor_inc_open(ns);/* 继续处理下一个状态 */case NVME_ZONE_STATE_IMPLICITLY_OPEN:// 关键调用:将区域状态更新为“显式打开”,并加入exp_open_zones队列zns_assign_zone_state(ns, zone, NVME_ZONE_STATE_EXPLICITLY_OPEN);/* 继续处理下一个状态 */case NVME_ZONE_STATE_EXPLICITLY_OPEN:return NVME_SUCCESS;default:return NVME_ZONE_INVAL_TRANSITION;}
}
场景说明:
当区域当前状态为 “空”“关闭” 或 “隐式打开” 时,执行 “打开区域” 命令后,需要将其状态转换为 “显式打开”。此时:
zns_assign_zone_state会先检查区域当前所在的队列(如 “空” 队列、“关闭” 队列或 “隐式打开” 队列),并将其从原队列中移除;- 然后将区域状态设置为
NVME_ZONE_STATE_EXPLICITLY_OPEN; - 最后将区域加入
exp_open_zones队列(显式打开区域队列),便于控制器跟踪和管理所有显式打开的区域。
通过这个函数,控制器能准确维护区域状态的一致性,确保符合 NVMe ZNS 协议对区域操作的约束(如最大打开 / 活动区域数限制)。
zns_clear_zone
-
zns_clear_zone:清空区域(重置写指针,调整状态,重置 ZNS 区域(zone)的状态并调整其在状态队列中的归属):static void zns_clear_zone(NvmeNamespace *ns, NvmeZone *zone) {// 1. 重置写指针(w_ptr = zslba)// 2. 根据是否有数据,设置状态为EMPTY或CLOSED// 3. 加入对应状态的链表 }关键逻辑拆解:
static void zns_clear_zone(NvmeNamespace *ns, NvmeZone *zone) {FemuCtrl *n = ns->ctrl; // 获取NVMe控制器实例uint8_t state;// 重置区域的写指针(w_ptr)到描述符中记录的写指针位置(d.wp)zone->w_ptr = zone->d.wp;state = zns_get_zone_state(zone); // 获取当前区域状态// 条件判断:区域是否有数据写入或包含有效扩展信息if (zone->d.wp != zone->d.zslba || // d.wp != 区域起始LBA(说明有数据写入)(zone->d.za & NVME_ZA_ZD_EXT_VALID)) { // 区域描述符扩展有效// 若当前状态不是“关闭”,则强制设为“关闭”if (state != NVME_ZONE_STATE_CLOSED) {zns_set_zone_state(zone, NVME_ZONE_STATE_CLOSED);}// 增加活跃区域计数(活跃区域包括打开、关闭状态的区域)zns_aor_inc_active(ns);// 将区域加入“关闭区域队列”(closed_zones),便于管理QTAILQ_INSERT_HEAD(&n->closed_zones, zone, entry);} else {// 若区域无数据且无扩展信息,直接设为“空”状态zns_set_zone_state(zone, NVME_ZONE_STATE_EMPTY);} }重点说明:
- 写指针重置:
zone->w_ptr = zone->d.wp确保区域的实际写指针与描述符中记录的一致,避免状态不一致。 - 状态转换条件:
- 当区域有数据(
d.wp != zslba)或有有效扩展信息时,无论之前状态如何,最终都会转为 “关闭” 状态并加入关闭队列。 - 当区域无数据且无扩展信息时,直接转为 “空” 状态(初始状态)。
- 当区域有数据(
- 队列管理:通过
QTAILQ_INSERT_HEAD将关闭的区域加入closed_zones队列,方便控制器跟踪所有关闭状态的区域。
- 写指针重置:
zns_aor_inc_active 函数解析
该函数用于 安全地增加 “活跃区域”(Active Zones)的计数,是 ZNS 协议中 “活跃与打开资源管理(AOR, Active and Open Resources)” 的关键逻辑,确保区域数量不超过控制器配置的上限。
函数内容与解析:
static inline void zns_aor_inc_active(NvmeNamespace *ns)
{FemuCtrl *n = ns->ctrl; // 获取控制器实例// 断言:当前活跃区域数必须非负(调试阶段检查逻辑错误)assert(n->nr_active_zones >= 0);// 若配置了最大活跃区域数(max_active_zones > 0)if (n->max_active_zones) {n->nr_active_zones++; // 增加活跃区域计数// 断言:活跃区域数不能超过最大值(防止超出配置限制)assert(n->nr_active_zones <= n->max_active_zones);}
}
重点说明:
- AOR 概念:ZNS 协议限制了 “活跃区域”(包括打开、关闭状态的区域)的最大数量(
max_active_zones),用于控制设备资源占用。zns_aor_inc_active是维护这一限制的核心函数。 assert的作用:assert(n->nr_active_zones >= 0):确保活跃区域计数不会出现负数(逻辑错误检查,如异常的减操作)。assert(n->nr_active_zones <= n->max_active_zones):确保活跃区域数不超过配置的最大值(防止超出设备能力)。- 注意:
assert仅在调试阶段生效,用于捕获程序逻辑错误;发布版本中通常会被禁用,不会影响性能。
- 条件执行:仅当
max_active_zones配置为非 0 时(即启用限制),才会执行计数增加操作,否则不限制活跃区域数量。
总结
zns_clear_zone负责重置区域状态并维护状态队列,确保区域状态与数据状态一致。zns_aor_inc_active负责安全地增加活跃区域计数,通过assert确保计数在有效范围内,符合 ZNS 协议对资源管理的要求。
zns_aor_check
-
zns_aor_check:检查打开 / 激活区域是否超过限制(符合 NVMe AOR 机制),确保在执行区域状态转换(如打开、激活区域)时,不超过设备配置的最大活跃区域数(max_active_zones)和最大打开区域数(max_open_zones)。:static int zns_aor_check(NvmeNamespace *ns, uint32_t act, uint32_t opn) {// 检查活动区域数(nr_active_zones + act)是否超过max_active_zones// 检查打开区域数(nr_open_zones + opn)是否超过max_open_zones } -
参数
act:本次操作将新增的活跃区域数(活跃区域包括 “打开”“关闭” 状态的区域)。 -
参数
opn:本次操作将新增的打开区域数(打开区域包括 “显式打开”“隐式打开” 状态的区域)。 -
返回值:成功返回
NVME_SUCCESS;若活跃区域超限返回NVME_ZONE_TOO_MANY_ACTIVE;若打开区域超限返回NVME_ZONE_TOO_MANY_OPEN。
实际应用例子:打开区域(zns_open_zone 函数)
在打开一个 ZNS 区域时,zns_aor_check 会被调用以确保操作不违反设备的资源限制。以下是具体场景:
假设设备配置:
- 最大活跃区域数
max_active_zones = 5,当前活跃区域数nr_active_zones = 4。 - 最大打开区域数
max_open_zones = 3,当前打开区域数nr_open_zones = 2。
现在要打开一个状态为 “关闭(CLOSED)” 的区域(从关闭到打开的转换)。
代码执行流程
static uint16_t zns_open_zone(NvmeNamespace *ns, NvmeZone *zone,NvmeZoneState state, NvmeRequest *req)
{uint16_t status;// ... 省略其他逻辑 ...switch (state) {case NVME_ZONE_STATE_EMPTY:// 从“空”状态打开:新增1个活跃区域(act=1),暂时不新增打开区域(opn=0)status = zns_aor_check(ns, 1, 0);if (status != NVME_SUCCESS) {return status; // 若活跃区域超限,返回错误}zns_aor_inc_active(ns); // 增加活跃区域计数/* 继续处理 */case NVME_ZONE_STATE_CLOSED:// 从“关闭”状态打开:不新增活跃区域(已算入活跃),新增1个打开区域(opn=1)status = zns_aor_check(ns, 0, 1);if (status != NVME_SUCCESS) {if (state == NVME_ZONE_STATE_EMPTY) {zns_aor_dec_active(ns); // 回滚活跃计数}return status; // 若打开区域超限,返回错误}zns_aor_inc_open(ns); // 增加打开区域计数/* 继续处理 */case NVME_ZONE_STATE_IMPLICITLY_OPEN:// 转换为“显式打开”,无需新增计数zns_assign_zone_state(ns, zone, NVME_ZONE_STATE_EXPLICITLY_OPEN);/* 继续处理 */case NVME_ZONE_STATE_EXPLICITLY_OPEN:return NVME_SUCCESS;default:return NVME_ZONE_INVAL_TRANSITION;}
}
具体分析
-
参数选择:
由于区域当前状态是 “关闭(CLOSED)”,属于 “活跃区域”(活跃区域包括打开、关闭状态),因此打开操作不会新增活跃区域(
act=0),但会将其从 “关闭” 转为 “打开”,因此新增 1 个打开区域(opn=1)。 -
检查过程:
调用
zns_aor_check(ns, 0, 1)时:-
活跃区域检查:
nr_active_zones + 0 = 4 ≤ 5(未超限)。 -
打开区域检查:
nr_open_zones + 1 = 3 ≤ 3(未超限)。因此返回
NVME_SUCCESS,允许打开操作。
-
-
后续操作:
检查通过后,调用
zns_aor_inc_open(ns)将打开区域数更新为 3,并通过zns_assign_zone_state将区域状态转为 “显式打开”,加入打开区域队列。
超限场景示例
若当前打开区域数已达 3(等于 max_open_zones),再调用 zns_aor_check(ns, 0, 1) 时:
- 打开区域检查:
3 + 1 = 4 > 3(超限),返回NVME_ZONE_TOO_MANY_OPEN。 - 此时
zns_open_zone会终止操作,返回错误码,避免违反设备限制。
总结
zns_aor_check 是 ZNS 设备资源管理的核心检查点,在区域状态转换(如打开、激活)前确保不超过配置的最大限制。上述打开区域的例子中,它通过验证 “新增活跃 / 打开区域数” 与 “当前计数 + 最大值” 的关系,防止资源滥用,符合 NVMe ZNS 协议对设备稳定性的要求。
3. IO 操作控制(写入 / 追加)
zns_check_zone_state_for_writ
ZNS 的写入操作严格限制为顺序写入,核心校验函数:
-
zns_check_zone_state_for_write:检查区域状态是否允许写入:static uint16_t zns_check_zone_state_for_write(NvmeZone *zone) {// 允许写入的状态:EMPTY、IMPLICITLY_OPEN、EXPLICITLY_OPEN、CLOSED// 拒绝写入的状态:FULL(返回NVME_ZONE_FULL)、READ_ONLY、OFFLINE等 }
zns_check_zone_write
-
zns_check_zone_write函数的核心作用是 验证对 ZNS 区域的写操作是否合法,确保符合 ZNS 协议的写入规则(如顺序写入、不超边界、状态允许等):static uint16_t zns_check_zone_write(FemuCtrl *n, NvmeNamespace *ns,NvmeZone *zone, uint64_t slba,uint32_t nlb, bool append) {// 1. 检查写入范围(slba + nlb)是否超过区域写边界(zslba + zcap)// 2. 检查区域状态是否允许写入 }
重点检查逻辑(来自代码):
- 写边界检查:写入范围(
slba + nlb)不能超过区域的最大可写边界(zns_zone_wr_boundary),否则返回NVME_ZONE_BOUNDARY_ERROR。 - 区域状态检查:仅允许对 “空(EMPTY)”“隐式打开”“显式打开”“关闭(CLOSED)” 状态的区域写入;若区域是 “满(FULL)”“离线(OFFLINE)”“只读(READ_ONLY)”,则返回对应错误。
- 写入顺序检查:
- 普通写入:起始 LBA(
slba)必须等于区域当前写指针(w_ptr),否则返回NVME_ZONE_INVALID_WRITE(保证顺序写入)。 - 追加写入(
append):起始 LBA 必须是区域起始 LBA(zslba),且写入大小符合限制。
- 普通写入:起始 LBA(
实际应用例子:
假设场景:
- 区域 A 的状态为 “显式打开(EXPLICITLY_OPEN)”,允许写入。
- 区域起始 LBA(
zslba)= 0,写指针(w_ptr)= 100,最大可写边界(zcap)= 500(即最多写到 LBA 499)。
例 1:合法写入
写入请求:slba=100,长度nlb=50(写到 LBA 149)。
- 边界检查:
100+50=150 ≤ 500→ 合法。 - 状态检查:“显式打开” 允许写入 → 合法。
- 顺序检查:
slba=100等于当前w_ptr=100→ 合法。 - 结果:返回
NVME_SUCCESS,允许写入。
例 2:越界写入
写入请求:slba=480,长度nlb=30(写到 LBA 509)。
- 边界检查:
480+30=509 > 500→ 触发NVME_ZONE_BOUNDARY_ERROR。 - 结果:拒绝写入,返回错误。
例 3:无序写入
写入请求:slba=120,长度nlb=20(当前w_ptr=100)。
- 顺序检查:
slba=120 ≠ w_ptr=100→ 触发NVME_ZONE_INVALID_WRITE。 - 结果:拒绝写入,返回错误(ZNS 要求严格顺序写入)。
例 4:区域已满
区域 A 的状态变为 “满(FULL)”,写入请求:slba=100,长度nlb=10。
- 状态检查:“满” 状态不允许写入 → 触发
NVME_ZONE_FULL。 - 结果:拒绝写入,返回错误。
总结
zns_check_zone_write 是 ZNS 设备防止非法写入的 “守门人”,通过验证边界、状态和顺序,确保写入操作符合 ZNS 协议规范,避免数据混乱或设备异常。
4. 命名空间生命周期管理
-
zns_zoned_ns_shutdown:关闭命名空间时清理所有区域:static void zns_zoned_ns_shutdown(NvmeNamespace *ns) {// 遍历所有状态的区域,重置状态为EMPTY,释放资源 } -
zns_ns_cleanup:释放 ZNS 相关内存(区域数组、描述符等):void zns_ns_cleanup(NvmeNamespace *ns) {g_free(n->id_ns_zoned); // 释放ZNS标识信息g_free(n->zone_array); // 释放区域数组g_free(n->zd_extensions); // 释放扩展信息 }
三、与 NVMe 协议的衔接
ZNS 作为 NVMe 协议的扩展特性,其逻辑通过以下方式嵌入 NVMe 命名空间管理流程:
1. NVMe 标识信息初始化
zns_init_zone_identify函数初始化 ZNS 相关的 NVMe 标识结构(符合 NVMe spec):
static void zns_init_zone_identify(FemuCtrl *n, NvmeNamespace *ns, int lba_index) {// 1. 初始化NvmeIdNsZoned结构(ZNS命名空间标识):// - mar:最大活动区域数(减1,因协议为0-based)// - mor:最大打开区域数(减1)// - ozcs:是否支持跨区域读(0x01表示支持)// - zsze:区域大小(单位LBA)// 2. 设置命名空间大小(nsze = 区域数 * 区域大小)// 3. 禁用不支持的特性(如DULBE,若区域大小不符合粒度要求)
}
NvmeIdNsZoned结构和NvmeIdNs的联系和区别
联系
两者均是 NVMe 协议中用于描述命名空间(Namespace)属性的结构体,用于向主机报告命名空间的配置、能力和限制。
- 在 ZNS 设备中,
NvmeIdNsZoned是NvmeIdNs的扩展补充,前者专注于 ZNS 特有的区域(Zone)管理特性,后者提供通用的命名空间基础信息。 - 两者共同配合,完整描述一个 ZNS 命名空间的属性(基础信息 + 区域管理特性)。
区别
| 维度 | NvmeIdNs(通用命名空间标识) | NvmeIdNsZoned(ZNS 专用命名空间标识) |
|---|---|---|
| 适用场景 | 所有 NVMe 命名空间(包括普通 NVM 命名空间和 ZNS 命名空间)。 | 仅适用于 ZNS(Zoned Namespace)类型的命名空间,描述区域相关特性。 |
| 核心作用 | 提供命名空间的基础信息,如 LBA 格式、容量、支持的操作等通用属性。 | 提供 ZNS 特有的区域管理参数,如区域操作能力、最大活跃 / 打开区域数、区域大小等。 |
| 关键字段 | 包含 nsze(命名空间总 LBA 数)、ncap(容量 LBA 数)、lbaf(LBA 格式数组)、flbas(当前 LBA 格式)等通用字段。 | 包含:- zoc(区域操作能力,如支持的关闭 / 重置等操作);- mar(最大活跃区域数)、mor(最大打开区域数);- lbafe(ZNS 专用 LBA 格式,含区域大小 zsze)等 ZNS 特有字段。 |
| 代码定义 | 未在提供的代码中完整展示,但通过引用(如 ns->id_ns)可知是命名空间的基础标识结构。 | 在 zns.h 中明确定义,专门用于 ZNS 特性描述。 |
总结
NvmeIdNs是 NVMe 命名空间的 “基础身份证”,描述通用属性;NvmeIdNsZoned是 ZNS 命名空间的 “扩展身份证”,仅在 ZNS 场景下存在,补充区域管理相关的特有属性,两者结合实现对 ZNS 命名空间的完整描述。
2. 命名空间与 ZNS 特性绑定
- NVMe 命名空间的
zoned标志(NvmeNamespaceParams.zoned)用于区分是否为 ZNS 命名空间。 - 命名空间的生命周期(创建、关闭、清理)与 ZNS 区域管理绑定:
- 命名空间初始化时,调用
zns_init_zone_geometry和zns_init_zoned_state初始化区域。 - 命名空间关闭时,调用
zns_zoned_ns_shutdown清理区域。 - 命名空间销毁时,调用
zns_ns_cleanup释放 ZNS 资源。
- 命名空间初始化时,调用
3. 协议命令处理
ZNS 的核心命令(如 Open、Close、Finish、Reset、Append)通过 NVMe 命令集触发,底层通过上述状态管理和 IO 校验函数实现:
- 打开区域(Open):调用
zns_aor_check检查限制,通过zns_assign_zone_state设置为 EXPLICITLY_OPEN。 - 写入 / 追加(Write/Append):通过
zns_check_zone_write校验,成功后更新写指针(w_ptr)。 - 关闭区域(Close):通过
zns_assign_zone_state设置为 CLOSED。 - 重置区域(Reset):调用
zns_clear_zone清空数据,恢复为 EMPTY 或 CLOSED。
总结
Femu 的 ZNS 实现核心逻辑是:通过区域数组管理所有 ZNS 区域,按状态分类维护链表,结合 NVMe 协议的标识和命令集,实现区域的创建、状态转换、顺序写入控制等功能。其底层通过几何参数校验确保区域配置合法,通过状态链表和写指针管理确保顺序写入特性,最终通过 FTL 层(znsftl.c)关联物理存储,完成从逻辑区域到物理介质的映射。
代码
nvme_register_znssd
int nvme_register_znssd(FemuCtrl *n)
{n->ext_ops = (FemuExtCtrlOps) {.state = NULL,.init = zns_init,.exit = zns_exit,.rw_check_req = NULL,.start_ctrl = zns_start_ctrl,.admin_cmd = zns_admin_cmd,.io_cmd = zns_io_cmd,.get_log = NULL,};return 0;
}
nvme_register_znssd 函数的核心作用是 向 NVMe 控制器注册 ZNS(Zoned Namespace)模式的操作回调,使其能够处理 ZNS 设备特有的初始化、命令执行等逻辑。它不是程序的主入口,而是 ZNS 功能的注册入口。
具体作用:
| 注册的函数 | 执行时机 |
|---|---|
init = zns_init | 控制器初始化阶段:当 ZNS 模式被启用时,由上层初始化逻辑调用。作用:完成 ZNS 设备的核心初始化,如设置控制器名称、初始化区域几何结构(zone 大小、数量)、分配区域数组等。 |
exit = zns_exit | 控制器销毁 / 关闭阶段:当设备停止或退出 ZNS 模式时调用。作用:释放 ZNS 模式下分配的资源(如区域管理相关的内存、数据结构等)。 |
start_ctrl = zns_start_ctrl | 控制器启动阶段:在控制器完成基础初始化后、开始处理命令前调用。作用:做启动前的校验和配置,例如代码中检查页大小是否为 4096、计算并设置 ZASL(Zone Attribute Size Limit)参数,确保设备启动条件合法。 |
admin_cmd = zns_admin_cmd | 处理管理员命令时:当控制器收到 ZNS 相关的管理员命令(如命名空间管理、控制器属性配置等)时调用。作用:目前代码中默认返回 “无效操作码”,实际可扩展用于处理 ZNS 特有的管理员命令。 |
io_cmd = zns_io_cmd | 处理 IO 命令时:当控制器收到 ZNS 相关的 IO 命令(如读、写、区域追加、区域管理等)时调用。作用:分发具体命令到对应处理函数,例如:- NVME_CMD_READ 对应 zns_read- NVME_CMD_WRITE 对应 zns_write- NVME_CMD_ZONE_APPEND 对应 zns_zone_append |
rw_check_req = NULL | 未使用(ZNS 模式下不需要额外的读写请求检查)。 |
get_log = NULL | 未使用(ZNS 模式下暂不支持日志获取功能)。 |
简单说,这些函数是 ZNS 模式的 “事件响应器”:初始化时 setup 环境,启动时做校验,运行时处理具体命令,关闭时清理资源,各司其职完成 ZNS 设备的完整生命周期管理。
