基于 Linux 内核源码(master 分支,commit 8a30aeb0d)撰写。 主要分析文件:
drivers/nvdimm/、drivers/acpi/nfit/、drivers/dax/、fs/dax.c
- 背景与硬件模型
- ACPI NFIT 表解析
- 软件分层架构
- 核心数据结构详解
- Namespace 与操作模式
- PMEM 块设备驱动
- DAX 框架
- mmap + DAX:缺页故障到直接映射
- BTT:Block Translation Table
- 持久内存写顺序与缓存冲刷
- NUMA 拓扑与 SPA Range
- 坏块(Bad Blocks)与错误恢复
- 初始化流程全景
- 关键路径性能分析
- Namespace Label 存储格式
- PFN 超级块与 ZONE_DEVICE 页面
- ndctl 工具与内核接口
- NVDIMM 安全功能
- virtio-pmem:虚拟化场景下的持久内存
- CXL 与持久内存的关系
- 持久内存的调试与可观测性
- 固件激活(Firmware Activate)机制
- 内核配置与编译选项
- 附录:关键源码文件索引
NVDIMM(Non-Volatile Dual Inline Memory Module)是一类插在 DDR 插槽上、掉电后数据仍然保持的存储设备。与传统 SSD 相比,它可以被 CPU 以字节粒度直接寻址,延迟在百纳秒量级。Intel 的 Optane DIMM(Apache Pass)是最具代表性的商用产品。
CPU Package
+--------------------------------------------+
| Core Core Core ... |
| +------+ |
| | iMC | <--- DDR4 Channel |
| +--+---+ | |
| | | |
+-------+ +----------+----------+ |
| DRAM DIMM | NVDIMM (PMem DIMM) | |
| (volatile)| (persistent) | |
+------------+---------------------+ |
NVDIMM 的持久性由两种机制之一保证:
- ADR(Asynchronous DRAM Refresh):平台掉电时自动把内存控制器写缓冲区的数据刷到 DIMM,对软件透明。对应内核标志
ND_REGION_PERSIST_MEMCTRL(定义于include/linux/libnvdimm.h第 65 行)。 - eADR(Extended ADR):ADR 的扩展版本,覆盖整条 CPU 存储路径(包括 CPU 缓存),对应
ND_REGION_PERSIST_CACHE标志(第 59 行)。
有了 ADR/eADR,软件只需要确保数据写入内存控制器即可;无 eADR 时,还需要显式发出 CLWB/CLFLUSHOPT 指令把数据从 CPU 缓存冲刷到内存控制器。
JEDEC 标准定义了几种 NVDIMM 类型,各有不同的软件访问模型:
NVDIMM 类型
+----------------------------------------------------------+
| NVDIMM-N | 独立 DRAM + NAND flash + 超级电容 |
| | DRAM 作缓存,掉电时 DRAM → Flash 转储 |
| | 延迟:DRAM 级别(~10ns),掉电保护成本高 |
+------------+----------------------------------------------+
| NVDIMM-F | 纯 NAND Flash 介质,无 DRAM 缓冲 |
| | 延迟较高,主要用于容量扩展 |
+------------+----------------------------------------------+
| NVDIMM-P | 字节寻址持久内存介质(如 3D XPoint/PCM) |
| | Intel Optane DIMM 属于此类 |
| | 延迟:~300ns 读,~100ns 写(vs DRAM ~100ns) |
+----------------------------------------------------------+
Intel Optane DIMM 属于 NVDIMM-P 类型,使用 3D XPoint(交叉点存储)介质,可在字节级直接读写,支持完整的 PMEM 和内存模式(App Direct / Memory Mode)。
Intel Optane DIMM 可配置为两种主要模式:
- App Direct 模式(持久内存模式):操作系统以持久内存方式管理,对应内核的 PMEM 路径。BIOS 通过 NFIT 表将地址空间描述给操作系统。
- Memory 模式(易失内存模式):DIMM 作为普通 DRAM 的二级缓存(DRAM 为一级),操作系统无感知,不进入 NVDIMM 子系统。
两种模式的混合配置也被支持,系统可以将部分 DIMM 容量用于 App Direct,部分用于 Memory 模式。
NFIT(NVDIMM Firmware Interface Table)是 ACPI 规范定义的一张固件表,描述系统中所有 NVDIMM 的物理地址范围、DIMM 信息、交错集以及刷新地址。内核通过 drivers/acpi/nfit/core.c 解析这张表。
NFIT 由若干子表组成,内核用不同结构体对应每种子表:
| 子表类型 | 内核结构体 | 说明 |
|---|---|---|
| System Physical Address Range (SPA) | nfit_spa |
描述一段系统物理地址范围 |
| Memory Device to SPA Range Map | nfit_memdev |
把 DIMM 映射到 SPA |
| Interleave Descriptor | nfit_idt |
描述多 DIMM 交错方式 |
| Control Region Descriptor | nfit_dcr |
DIMM 控制寄存器区域 |
| Block Data Window | nfit_bdw |
块访问窗口 |
| Flush Hint Address | nfit_flush |
刷新提示地址 |
这些结构体定义于 drivers/acpi/nfit/nfit.h,例如:
// drivers/acpi/nfit/nfit.h 第 163 行
struct nfit_spa {
struct list_head list;
struct nd_region *nd_region;
unsigned long ars_state;
u32 clear_err_unit;
u32 max_ars;
struct acpi_nfit_system_address spa[]; // 柔性数组,紧跟 ACPI 原始数据
};所有已解析的 SPA 子表被挂入 acpi_nfit_desc.spas 链表,DIMM 子表挂入 memdevs 链表。
drivers/acpi/nfit/nfit.h 第 237 行定义了整个 NFIT 解析状态的顶层结构:
struct acpi_nfit_desc {
struct nvdimm_bus_descriptor nd_desc; // 向上层 libnvdimm 暴露的接口
struct acpi_table_header acpi_header;
struct mutex init_mutex;
struct list_head memdevs; // nfit_memdev 链表
struct list_head flushes; // nfit_flush 链表
struct list_head dimms; // DIMM 列表
struct list_head spas; // nfit_spa 链表
struct list_head dcrs; // nfit_dcr 链表
struct list_head bdws; // nfit_bdw 链表
struct list_head idts; // nfit_idt 链表
struct nvdimm_bus *nvdimm_bus;
struct device *dev;
struct nd_cmd_ars_status *ars_status; // ARS(Address Range Scrub)状态
struct nfit_spa *scrub_spa; // 正在扫描的 SPA
struct delayed_work dwork; // ARS 延迟工作队列
unsigned long scrub_flags; // ARS_BUSY / ARS_CANCEL 等
unsigned int scrub_count; // 已完成扫描次数
unsigned int scrub_tmo; // 扫描超时(默认 90 秒)
...
};SPA Range 通过 UUID 区分其用途,定义于 drivers/acpi/nfit/nfit.h 第 115 行的 nfit_uuids 枚举:
NFIT_SPA_VOLATILE, // 易失性内存区域
NFIT_SPA_PM, // 持久内存(Persistent Memory)区域
NFIT_SPA_DCR, // 控制寄存器区域
NFIT_SPA_BDW, // 块数据窗口
NFIT_SPA_VDISK, // 虚拟磁盘
NFIT_SPA_VCD, // 虚拟 CD
NFIT_SPA_PDISK, // 持久磁盘
NFIT_SPA_PCD, // 持久 CDNFIT_SPA_PM 对应的 SPA Range 就是软件可以直接访问的持久内存地址空间,最终会被注册为 nd_region。
ACPI 通过 _DSM 方法向 NVDIMM 发送控制命令。acpi_nfit_ctl() 函数(core.c 第 445 行)是所有 NVDIMM 控制命令的统一入口:
int acpi_nfit_ctl(struct nvdimm_bus_descriptor *nd_desc, struct nvdimm *nvdimm,
unsigned int cmd, void *buf, unsigned int buf_len, int *cmd_rc)
{
struct acpi_nfit_desc *acpi_desc = to_acpi_desc(nd_desc);
struct nfit_mem *nfit_mem = nvdimm_provider_data(nvdimm);
...
func = cmd_to_func(nfit_mem, cmd, call_pkg, &family);
...
// 构造 ACPI _DSM 输入参数
in_obj.type = ACPI_TYPE_BUFFER;
in_obj.buffer.length = buf_len;
in_obj.buffer.pointer = buf;
...
// 调用 ACPI _DSM 方法
out_obj = acpi_evaluate_dsm(handle, guid, revision, func, &in_buf);
}DSM 命令包括健康状态查询、固件升级、ARS 扫描、错误清除等。Intel DIMM 的命令集定义于 nfit.h 的 nvdimm_family_cmds 枚举(第 49 行):
NVDIMM_INTEL_LATCH_SHUTDOWN = 10 // 锁定关机计数
NVDIMM_INTEL_GET_MODES = 11 // 查询工作模式
NVDIMM_INTEL_GET_FWINFO = 12 // 获取固件信息
NVDIMM_INTEL_START_FWUPDATE = 13 // 开始固件升级
NVDIMM_INTEL_SEND_FWUPDATE = 14 // 发送固件数据
NVDIMM_INTEL_FINISH_FWUPDATE = 15 // 完成固件升级
NVDIMM_INTEL_QUERY_FWUPDATE = 16 // 查询升级状态
NVDIMM_INTEL_SET_THRESHOLD = 17 // 设置健康阈值
NVDIMM_INTEL_INJECT_ERROR = 18 // 注入错误(测试用)
NVDIMM_INTEL_GET_SECURITY_STATE = 19 // 获取安全状态
NVDIMM_INTEL_SET_PASSPHRASE = 20 // 设置密码
...
NVDIMM_INTEL_FW_ACTIVATE_ARM = 30 // 固件激活预备
最多支持 31 条命令(NVDIMM_CMD_MAX = 31,nfit.h 第 37 行)。
nfit_mem 结构体(nfit.h 第 207 行)将属于同一 DIMM 的多个 NFIT 子表聚合在一起:
struct nfit_mem {
struct nvdimm *nvdimm;
struct acpi_nfit_memory_map *memdev_dcr; // DCR 区域的 memory map
struct acpi_nfit_memory_map *memdev_pmem; // PMEM 区域的 memory map
struct acpi_nfit_control_region *dcr; // 控制区域描述符
struct acpi_nfit_system_address *spa_dcr; // DCR 的 SPA
struct acpi_nfit_interleave *idt_dcr; // DCR 的交错描述符
struct nfit_flush *nfit_flush; // 刷新提示
char id[NFIT_DIMM_ID_LEN+1]; // DIMM 标识字符串(22 字节)
unsigned long dsm_mask; // 支持的 DSM 函数位图
unsigned long flags; // 状态标志
u32 dirty_shutdown; // 脏关机计数
int family; // DIMM 厂商类型
...
};nfit_mem.family 区分不同厂商的 DSM 接口(Intel / HPE1 / HPE2 / MSFT / Hyperv),由内核根据 DIMM 上报的 UUID 自动识别。
acpi_nfit_probe()
|
acpi_nfit_init() → 获取 NFIT 表(acpi_get_table("NFIT", ...))
|
acpi_nfit_blk_region_do_io / acpi_nfit_add → 驱动入口
|
acpi_nfit_register_dimms()
|
├── 遍历 acpi_nfit_desc.dimms(nfit_mem 链表)
├── 为每个 nfit_mem 构建 nvdimm_desc
│ ├── 根据 family 选择对应的 dsm_mask
│ ├── 设置 NDD_LABELING 标志(支持 namespace label)
│ └── nvdimm_create() → 注册到 nvdimm_bus
|
acpi_nfit_register_regions()
|
├── 遍历 acpi_nfit_desc.spas(nfit_spa 链表)
├── 过滤 NFIT_SPA_PM 类型
├── 收集参与交错的 nfit_mem 列表
├── 构建 nd_region_desc(填入 ndr_start/ndr_size/mappings)
└── nvdimm_pmem_region_create() → 注册 nd_region
Linux NVDIMM 子系统采用清晰的分层架构:
用户空间
+------------------------------------------------------------------+
| 应用程序(数据库、文件系统、mmap 用户) |
+------------------------------------------------------------------+
| | |
文件系统 字符设备 块设备 I/O
(ext4/xfs /dev/dax /dev/pmemN
+DAX 挂载) devdax) (sector 模式)
| | |
+------+--------------------+--------------------+------+
| VFS / Block Layer |
+------+--------------------+--------------------+------+
| | |
+------+------+ +---------+-------+ +--------+------+
| fs/dax.c | | drivers/dax/ | | BTT 层 |
| DAX 文件 | | device_dax.c | | btt.c |
| 操作 | | (devdax) | | (原子写语义) |
+------+------+ +---------+-------+ +--------+------+
| | |
+------+--------------------+--------------------+------+
| DAX 框架 (drivers/dax/super.c) |
| dax_device + dax_operations |
+------+--------------------+--------------------+------+
|
+------+----------------------------------------------+
| PMEM 块设备驱动 |
| drivers/nvdimm/pmem.c |
| pmem_submit_bio / pmem_dax_direct_access |
+------+----------------------------------------------+
|
+------+----------------------------------------------+
| libnvdimm 核心层 |
| nd_region / nd_namespace / nd_btt / nd_pfn |
| drivers/nvdimm/nd-core.c, region_devs.c |
+------+----------------------------------------------+
|
+------+----------------------------------------------+
| ACPI NFIT 驱动 (drivers/acpi/nfit/) |
| acpi_nfit_desc / nfit_spa / nfit_mem |
| NFIT 表解析 + DSM 命令路由 |
+------+----------------------------------------------+
|
+------+----------------------------------------------+
| ACPI 固件 / BIOS |
| NFIT 表 + _DSM 方法 |
+------------------------------------------------------+
libnvdimm 采用 Linux 设备模型实现了一套独立的总线(nvdimm bus),所有 NVDIMM 设备都挂在这条总线上:
/sys/bus/nd/
+-- devices/
| +-- ndbus0/ ← nvdimm_bus(一个平台一个)
| | +-- nmem0/ ← nvdimm(每个 DIMM 一个)
| | +-- nmem1/
| | +-- region0/ ← nd_region(每个 SPA Range 一个)
| | | +-- namespace0.0/ ← nd_namespace_pmem
| | | +-- pfn0.0/ ← nd_pfn(fsdax 模式)
| | | +-- dax0.0/ ← nd_dax(devdax 模式)
| | | +-- btt0.0/ ← nd_btt(sector 模式)
| | +-- region1/
+-- drivers/
| +-- nd_pmem/ ← nd_pmem_driver
| +-- nd_btt/ ← nd_btt_driver
| +-- nd_dax/ ← nd_dax_driver
nvdimm_bus_descriptor(include/linux/libnvdimm.h)是底层驱动(如 ACPI NFIT)向 libnvdimm 注册的接口描述符:
struct nvdimm_bus_descriptor {
const struct attribute_group **attr_groups;
unsigned long bus_dsm_mask;
unsigned long cmd_mask; // 总线级别支持的命令位图
unsigned long bus_family_mask; // 支持的厂商命令族位图
struct nvdimm_bus_fw_activate_ops *fw_activate_ops;
char *provider_name;
struct module *module;
int (*ndctl)(struct nvdimm_bus_descriptor *, struct nvdimm *,
unsigned int, void *, unsigned int, int *); // 命令入口
int (*flush_probe)(struct nvdimm_bus_descriptor *);
int (*clear_to_send)(struct nvdimm_bus_descriptor *,
struct nvdimm *, unsigned int, void *);
};nd_region 代表一段连续的 NVDIMM 地址空间(对应一个或多个 DIMM 的交错区域),是 libnvdimm 层的核心对象,定义于 drivers/nvdimm/nd.h 第 403 行:
struct nd_region {
struct device dev;
struct ida ns_ida; // namespace ID 分配器
struct ida btt_ida; // BTT ID 分配器
struct ida pfn_ida; // PFN(Page Frame Number)设备 ID 分配器
struct ida dax_ida; // DAX 设备 ID 分配器
unsigned long flags; // ND_REGION_PAGEMAP、ND_REGION_PERSIST_CACHE 等
struct device *ns_seed; // 默认 namespace 种子设备
struct device *btt_seed; // 默认 BTT 种子设备
struct device *pfn_seed; // 默认 PFN 种子设备
struct device *dax_seed; // 默认 DAX 种子设备
unsigned long align; // 对齐要求
u16 ndr_mappings; // 参与交错的 DIMM 数量
u64 ndr_size; // 区域总大小
u64 ndr_start; // 区域起始物理地址
int id, num_lanes, ro, numa_node, target_node;
void *provider_data;
struct badblocks bb; // 坏块追踪
struct nd_interleave_set *nd_set;
struct nd_percpu_lane __percpu *lane; // per-CPU 通道
int (*flush)(struct nd_region *nd_region, struct bio *bio);
struct nd_mapping mapping[] __counted_by(ndr_mappings); // 柔性数组
};nd_region.flags 中的关键位(定义于 include/linux/libnvdimm.h):
ND_REGION_PAGEMAP(bit 0):允许把持久内存作为系统页面映射(ZONE_DEVICE)。ND_REGION_PERSIST_CACHE(bit 1):平台保证 CPU 缓存内容在掉电时被持久化(eADR)。ND_REGION_PERSIST_MEMCTRL(bit 2):平台保证内存控制器写缓冲区在掉电时被持久化(ADR)。ND_REGION_ASYNC(bit 3):平台提供异步刷新机制。ND_REGION_CXL(bit 4):该区域由 CXL 子系统创建。
nd_mapping 描述一个 DIMM 对 nd_region 的贡献,定义于 include/linux/libnvdimm.h:
struct nd_mapping {
struct nvdimm *nvdimm; // 对应的 DIMM 设备
u64 start; // 在 DIMM DPA(Device Physical Address)上的起始地址
u64 size; // 贡献的字节数
int position; // 在交错集中的位置(0-based)
struct list_head labels; // 所有关联的 namespace label
struct nd_label_ent *ndd; // 与该映射关联的 label 条目
};对于多 DIMM 交错配置,一个 nd_region 包含多个 nd_mapping,每个对应一个 DIMM 的贡献:
nd_region(4-way 交错,总大小 4 * 256GB = 1TB)
+--------------------------------------------------+
| nd_mapping[0]: nmem0, start=0, size=256GB |
| nd_mapping[1]: nmem1, start=0, size=256GB |
| nd_mapping[2]: nmem2, start=0, size=256GB |
| nd_mapping[3]: nmem3, start=0, size=256GB |
+--------------------------------------------------+
访问 SPA 0x100000000 时:
交错粒度 256B → DIMM 选择 = (SPA / 256) % 4
DIMM DPA = (SPA / 256 / 4) * 256 + SPA % 256
nd_namespace_pmem 是带有标签(namespace label)的持久内存命名空间,定义于 include/linux/nd.h,通过 namespace_devs.c 管理:
// 关键字段(来自 include/linux/nd.h,nd_namespace_common 是基类)
struct nd_namespace_pmem {
struct nd_namespace_io nsio; // 包含 nd_namespace_common + resource
unsigned long lbasize; // 逻辑块大小(512 或 4096)
char *alt_name; // 可选的备用名称
uuid_t *uuid; // 全局唯一标识符
int id; // namespace 序号
};nd_namespace_io 进一步嵌套 nd_namespace_common:
struct nd_namespace_io {
struct nd_namespace_common common;
struct resource res; // 物理地址资源(start / end)
sector_t size;
void *addr;
struct badblocks bb;
};nd_namespace_common.claim 指针用于标记该 namespace 是否已被 BTT 或 PFN 设备"认领"(namespace_devs.c 第 47 行 is_nd_btt(ndns->claim) 判断)。
BTT 设备对象,定义于 drivers/nvdimm/nd.h 第 448 行:
struct nd_btt {
struct device dev;
struct nd_namespace_common *ndns; // 所依赖的 namespace
struct btt *btt; // BTT 运行时状态(btt.c 内部)
unsigned long lbasize; // 对外暴露的逻辑块大小
u64 size;
uuid_t *uuid;
int id;
int initial_offset; // 在 namespace 中的起始偏移
u16 version_major;
u16 version_minor;
};用于管理 PFN(Page Frame Number)映射和 DAX 设备的结构体,定义于 nd.h 第 467 行:
struct nd_pfn {
int id;
uuid_t *uuid;
struct device dev;
unsigned long align;
unsigned long npfns; // 覆盖的页面数
enum nd_pfn_mode mode; // PFN_MODE_RAM 或 PFN_MODE_PMEM
struct nd_pfn_sb *pfn_sb; // 超级块(存储在持久内存开头)
struct nd_namespace_common *ndns;
};
struct nd_dax {
struct nd_pfn nd_pfn; // DAX 设备直接复用 nd_pfn
};nd_pfn_mode 控制 struct page 数组存放在哪里:
PFN_MODE_RAM:struct page 数组分配在普通 RAM 中。PFN_MODE_PMEM:struct page 数组存放在持久内存自身的开头区域(节省 RAM,但占用持久内存空间)。
pmem_device 是 PMEM 块设备驱动(pmem.c)的核心私有数据,定义于 drivers/nvdimm/pmem.h 第 13 行:
struct pmem_device {
phys_addr_t phys_addr; // 持久内存的物理起始地址
phys_addr_t data_offset; // 数据区相对于 phys_addr 的偏移
// (PFN 超级块之后才是数据)
void *virt_addr; // 持久内存的内核虚拟地址
size_t size; // namespace 总字节数
u32 pfn_pad; // section 对齐造成的尾部填充
struct kernfs_node *bb_state;
struct badblocks bb; // 坏块表
struct dax_device *dax_dev; // 对应的 DAX 设备
struct gendisk *disk; // 对应的块设备
struct dev_pagemap pgmap; // 用于 ZONE_DEVICE 页面管理
};一个 nd_region 可以划分为若干 namespace,每个 namespace 支持以下操作模式:
nd_region(物理 NVDIMM 区域)
+--------------------------------------------------+
| namespace0 (pmem0) | namespace1 (pmem0.1) |
| +------------------+ | +------------------+ |
| | 操作模式: | | | 操作模式: | |
| | fsdax / devdax | | | sector / raw | |
| | / sector / raw | | | | |
| +------------------+ | +------------------+ |
+--------------------------------------------------+
文件系统(ext4、XFS 等)以 -o dax 挂载时使用此模式。块设备 /dev/pmemN 被格式化成文件系统,文件 I/O 通过 DAX 路径直接读写持久内存,完全绕过页缓存。
内核在 pmem_attach_disk() 中通过 devm_memremap_pages() 将持久内存注册为 MEMORY_DEVICE_FS_DAX 类型的 ZONE_DEVICE 页面(pmem.c 第 516 行):
pmem->pgmap.type = MEMORY_DEVICE_FS_DAX;
pmem->pgmap.ops = &fsdax_pagemap_ops;
addr = devm_memremap_pages(dev, &pmem->pgmap);注册后,每个持久内存页面都有对应的 struct page,允许文件系统利用反向映射进行内存错误处理。
暴露为字符设备 /dev/daxN.M,用户程序通过 mmap() 直接映射持久内存。不经过文件系统和页缓存。适合数据库等需要自行管理存储布局的程序(如 PMDK 库的场景)。
nd_dax 结构(复用 nd_pfn)通过 drivers/dax/device.c 实现字符设备接口。
使用 BTT(Block Translation Table)提供原子写语义,以标准块设备(/dev/pmemNs)的形式暴露。适合不感知 NVDIMM 的传统应用。nvdimm_namespace_disk_name() 在 namespace_devs.c 第 147 行通过判断 is_nd_btt(ndns->claim) 来决定是否添加 "s" 后缀:
if (ndns->claim && is_nd_btt(ndns->claim))
suffix = "s";不加任何翻译层,直接暴露原始的持久内存区域,一般用于调试或格式化操作。nd_namespace_common.force_raw 标志强制使用此模式(namespace_devs.c 第 105 行)。
pmem_sector_size() 函数(namespace_devs.c 第 118 行)根据 namespace label 中的 lbasize 字段决定扇区大小:512 字节(默认)或 4096 字节。
namespace label 中的 abstraction_guid 字段记录 namespace 当前绑定的抽象层类型,定义于 drivers/nvdimm/label.h 第 186 行:
#define NVDIMM_BTT_GUID "8aed63a2-29a2-4c66-8b12-f05d15d3922a" // BTT v1
#define NVDIMM_BTT2_GUID "18633bfc-1735-4217-8ac9-17239282d3f8" // BTT v2
#define NVDIMM_PFN_GUID "266400ba-fb9f-4677-bcb0-968f11d0d225" // fsdax(pfn 模式)
#define NVDIMM_DAX_GUID "97a86d9c-3cdd-4eda-986f-5068b4f80088" // devdax内核在发现 namespace 时,通过比对 abstraction_guid 来决定使用哪种驱动路径(nd_btt_probe() / nd_pfn_probe() / nd_dax_probe())。
drivers/nvdimm/pmem.c 实现了 PMEM 块设备驱动,是用户态 I/O 请求进入持久内存的最后一公里。
驱动提供了一组地址转换函数(pmem.c 第 48~61 行):
// 物理地址 = phys_addr(namespace 起始) + offset
static phys_addr_t pmem_to_phys(struct pmem_device *pmem, phys_addr_t offset)
{
return pmem->phys_addr + offset;
}
// 扇区号 → 在 namespace 中的字节偏移(跳过 data_offset)
static phys_addr_t to_offset(struct pmem_device *pmem, sector_t sector)
{
return (sector << SECTOR_SHIFT) + pmem->data_offset;
}
// 字节偏移 → 扇区号(反向转换,用于坏块报告)
static sector_t to_sect(struct pmem_device *pmem, phys_addr_t offset)
{
return (offset - pmem->data_offset) >> SECTOR_SHIFT;
}data_offset 非零时说明 namespace 开头有 PFN 超级块(PFN 模式下存储 struct page 数组的元数据)。
write_pmem() 函数(pmem.c 第 124 行)处理 bio 写请求:
static void write_pmem(void *pmem_addr, struct page *page,
unsigned int off, unsigned int len)
{
unsigned int chunk;
void *mem;
while (len) {
mem = kmap_local_page(page);
chunk = min_t(unsigned int, len, PAGE_SIZE - off);
memcpy_flushcache(pmem_addr, mem + off, chunk); // 使用 NT store 指令
kunmap_local(mem);
len -= chunk;
off = 0;
page++;
pmem_addr += chunk;
}
}memcpy_flushcache() 在 x86 上使用非临时存储(NT store)指令(movntdq 等),数据直接写入内存控制器而绕过 CPU 缓存,保证持久性且不污染缓存。
read_pmem() 函数(pmem.c 第 142 行)使用 copy_mc_to_kernel() 读取持久内存,该函数能处理读取时发生的机器检查异常(MCE),对应持久内存的介质错误:
static blk_status_t read_pmem(struct page *page, unsigned int off,
void *pmem_addr, unsigned int len)
{
...
while (len) {
mem = kmap_local_page(page);
chunk = min_t(unsigned int, len, PAGE_SIZE - off);
rem = copy_mc_to_kernel(mem + off, pmem_addr, chunk);
kunmap_local(mem);
if (rem)
return BLK_STS_IOERR; // MCE 发生,返回 I/O 错误
...
}
return BLK_STS_OK;
}pmem_submit_bio() 是块设备的 submit_bio 实现(pmem.c 第 200 行),整个处理流程是同步的:
static void pmem_submit_bio(struct bio *bio)
{
...
if (bio->bi_opf & REQ_PREFLUSH)
ret = nvdimm_flush(nd_region, bio); // 处理 flush 请求
bio_for_each_segment(bvec, bio, iter) {
if (op_is_write(bio_op(bio)))
rc = pmem_do_write(pmem, bvec.bv_page, bvec.bv_offset,
iter.bi_sector, bvec.bv_len);
else
rc = pmem_do_read(pmem, ...);
}
if (bio->bi_opf & REQ_FUA)
ret = nvdimm_flush(nd_region, bio); // FUA 写后再 flush
bio_endio(bio);
}注意:PMEM 驱动不使用 blk-mq 队列,bio 在提交时直接同步执行并立即回调 bio_endio()。这与传统磁盘驱动的异步模型有本质区别。
pmem.c 第 367 行定义了 PMEM 设备的 DAX 操作集:
static const struct dax_operations pmem_dax_ops = {
.direct_access = pmem_dax_direct_access, // 返回物理页帧号
.zero_page_range = pmem_dax_zero_page_range, // 清零
.recovery_write = pmem_recovery_write, // 含毒页恢复写
};pmem_dax_direct_access() 是 DAX 路径的核心:给定页偏移,返回对应的内核虚拟地址和 PFN,供上层直接操作。
__pmem_direct_access()(pmem.c 第 242 行)在返回 PFN 前会检查坏块表:
__weak long __pmem_direct_access(struct pmem_device *pmem, pgoff_t pgoff,
long nr_pages, enum dax_access_mode mode, void **kaddr,
unsigned long *pfn)
{
resource_size_t offset = PFN_PHYS(pgoff) + pmem->data_offset;
if (kaddr)
*kaddr = pmem->virt_addr + offset;
if (pfn)
*pfn = PHYS_PFN(pmem->phys_addr + offset);
if (bb->count &&
badblocks_check(bb, sector, num, &first_bad, &num_bad)) {
if (mode != DAX_RECOVERY_WRITE)
return -EHWPOISON; // 遇到坏块,拒绝非恢复写
// 恢复写路径:只返回到第一个坏块之前的页数
actual_nr = PHYS_PFN(PAGE_ALIGN((first_bad - sector) << SECTOR_SHIFT));
if (actual_nr)
return actual_nr;
return 1; // 至少允许一页的恢复写
}
...
}该函数用 __weak 修饰,允许测试框架(tools/testing/nvdimm/)提供强替换版本进行单元测试。
pmem_attach_disk() 函数(pmem.c 第 448 行)完成块设备的完整初始化,关键步骤:
- 分配
pmem_device结构体。 - 根据是否有 PFN 超级块,选择
devm_memremap_pages()或devm_memremap()映射持久内存。 - 分配块设备
gendisk,设置容量。 - 初始化坏块表(
nvdimm_badblocks_populate())。 - 创建
dax_device(alloc_dax(pmem, &pmem_dax_ops)),并根据 region 是否有 ADR/eADR 配置写缓存和同步标志:
dax_dev = alloc_dax(pmem, &pmem_dax_ops);
set_dax_nocache(dax_dev); // 不在 CPU 缓存中留存数据
set_dax_nomc(dax_dev); // 读取时使用 copy_mc 处理 MCE
if (is_nvdimm_sync(nd_region))
set_dax_synchronous(dax_dev); // eADR:写即持久
dax_write_cache(dax_dev, nvdimm_has_cache(nd_region)); // ADR:需要 CLWBnd_pmem_probe()(pmem.c 第 596 行)是 nd_pmem_driver 的 probe 回调,整个探测逻辑:
static int nd_pmem_probe(struct device *dev)
{
ndns = nvdimm_namespace_common_probe(dev);
if (is_nd_btt(dev))
return nvdimm_namespace_attach_btt(ndns); // BTT 模式
if (is_nd_pfn(dev))
return pmem_attach_disk(dev, ndns); // PFN/fsdax 模式
// 探测是否有已存在的 BTT 超级块
ret = nd_btt_probe(dev, ndns);
if (ret == 0)
return -ENXIO; // 找到 BTT,重新 probe 为 BTT 设备
// 探测是否有已存在的 PFN 超级块
ret = nd_pfn_probe(dev, ndns);
if (ret == 0)
return -ENXIO; // 找到 PFN,重新 probe 为 PFN 设备
// 探测是否为 devdax(DAX 模式)
ret = nd_dax_probe(dev, ndns);
if (ret == 0)
return -ENXIO;
// 无超级块,以原始/块设备模式 attach
return pmem_attach_disk(dev, ndns);
}-ENXIO 的返回是 libnvdimm 的约定:表示"探测成功但需要改变设备类型重新绑定"。
nd_pmem_notify() 处理 NVDIMM 事件通知(pmem.c 第 733 行):
static void nd_pmem_notify(struct device *dev, enum nvdimm_event event)
{
switch (event) {
case NVDIMM_REVALIDATE_POISON:
pmem_revalidate_poison(dev); // 重新扫描坏块表
break;
case NVDIMM_REVALIDATE_REGION:
pmem_revalidate_region(dev); // 重新检查只读状态
break;
}
}NVDIMM_REVALIDATE_POISON 事件由 ARS(Address Range Scrub)完成后触发,驱动重新用 nvdimm_badblocks_populate() 更新坏块表。
dax_device 是整个 DAX 框架的锚点,定义于 drivers/dax/super.c 第 28 行:
struct dax_device {
struct inode inode; // 伪文件系统 inode(用于引用计数)
struct cdev cdev; // 可选:devdax 模式的字符设备
void *private; // 驱动私有数据(对 pmem 而言是 pmem_device)
unsigned long flags; // DAXDEV_ALIVE / DAXDEV_WRITE_CACHE 等
const struct dax_operations *ops; // 操作集
void *holder_data; // 持有者(文件系统或 mapped device)
const struct dax_holder_operations *holder_ops;
};dax_device.flags 的关键位(super.c 第 124 行 dax_device_flags 枚举):
DAXDEV_ALIVE:设备存活,未被销毁。DAXDEV_WRITE_CACHE:需要arch_wb_cache_pmem()刷缓存(无 eADR 时置位)。DAXDEV_SYNC:同步标志(有 eADR 时置位,写后立即持久)。DAXDEV_NOCACHE:写时不留在缓存(使用 NT store)。DAXDEV_NOMC:读时用copy_mc处理 MCE。
dax_operations 是 DAX 驱动与上层框架的接口契约(定义于 include/linux/dax.h):
struct dax_operations {
// 核心接口:把设备 pgoff 转换为内核虚地址和 PFN
long (*direct_access)(struct dax_device *, pgoff_t, long,
enum dax_access_mode, void **, unsigned long *);
// 清零指定范围
int (*zero_page_range)(struct dax_device *, pgoff_t, size_t);
// 从 iov_iter 复制数据到持久内存(用于 DAX 写)
size_t (*copy_from_iter)(struct dax_device *, pgoff_t, void *,
size_t, struct iov_iter *);
// 从持久内存复制数据到 iov_iter(用于 DAX 读)
size_t (*copy_to_iter)(struct dax_device *, pgoff_t, void *,
size_t, struct iov_iter *);
// 含毒页恢复写
size_t (*recovery_write)(struct dax_device *, pgoff_t, void *,
size_t, struct iov_iter *);
};dax_direct_access() 是上层(文件系统、缺页处理器)调用 DAX 设备的统一接口,实现于 super.c 第 149 行:
long dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff, long nr_pages,
enum dax_access_mode mode, void **kaddr, unsigned long *pfn)
{
long avail;
if (!dax_alive(dax_dev))
return -ENXIO;
avail = dax_dev->ops->direct_access(dax_dev, pgoff, nr_pages,
mode, kaddr, pfn);
if (!avail)
return -ERANGE;
return min(avail, nr_pages);
}对 PMEM 设备,最终调用 __pmem_direct_access()(pmem.c 第 242 行),通过 pmem->virt_addr + offset 计算内核虚地址,通过 PHYS_PFN(pmem->phys_addr + offset) 计算 PFN。
dax_flush() 函数(super.c 第 257 行)在需要时调用 arch_wb_cache_pmem() 将 CPU 缓存中的数据写回持久内存:
void dax_flush(struct dax_device *dax_dev, void *addr, size_t size)
{
if (unlikely(!dax_write_cache_enabled(dax_dev)))
return;
arch_wb_cache_pmem(addr, size); // x86: 调用 CLWB 循环
}DAX 设备的存活状态用 SRCU(Sleepable RCU)保护。dax_read_lock() / dax_read_unlock() 确保在设备销毁期间没有并发的 DAX 操作:
// super.c 第 45~55 行
DEFINE_STATIC_SRCU(dax_srcu);
int dax_read_lock(void)
{
return srcu_read_lock(&dax_srcu);
}
void dax_read_unlock(int id)
{
srcu_read_unlock(&dax_srcu, id);
}drivers/dax/super.c 使用 XArray 维护从块设备(gendisk)到 dax_device 的映射(第 60~72 行):
static DEFINE_XARRAY(dax_hosts);
int dax_add_host(struct dax_device *dax_dev, struct gendisk *disk)
{
return xa_insert(&dax_hosts, (unsigned long)disk, dax_dev, GFP_KERNEL);
}
void dax_remove_host(struct gendisk *disk)
{
xa_erase(&dax_hosts, (unsigned long)disk);
}文件系统挂载时通过 fs_dax_get_by_bdev() 查找对应的 dax_device,建立 holder 关系:
struct dax_device *fs_dax_get_by_bdev(struct block_device *bdev, u64 *start_off,
void *holder, const struct dax_holder_operations *ops)
{
*start_off = get_start_sect(bdev) * SECTOR_SIZE;
dax_dev = xa_load(&dax_hosts, (unsigned long)bdev->bd_disk);
...
dax_dev->holder_data = holder; // 文件系统的 super_block
dax_dev->holder_ops = ops; // dax_holder_operations(内存错误回调)
return dax_dev;
}dax_access_mode 枚举(include/linux/dax.h)控制 direct_access() 的行为:
enum dax_access_mode {
DAX_ACCESS, // 普通读写访问
DAX_RECOVERY_WRITE, // 恢复写模式(处理含毒页)
};DAX_RECOVERY_WRITE 模式下,驱动必须在返回 PFN 之前先清除介质毒素,否则写操作可能失败。
这是 NVDIMM 最核心的用户态访问路径。应用程序 mmap() 一个挂载在 DAX 文件系统上的文件后,首次访问会触发缺页故障,内核在故障处理中直接把持久内存的物理页映射进进程页表,后续访问不再经过内核。
用户程序访问 mmap 地址
|
缺页异常(#PF)
|
do_fault() [mm/memory.c]
|
vma->vm_ops->fault()
|
ext4_dax_fault() / xfs_dax_fault() [文件系统]
|
dax_iomap_fault() [fs/dax.c]
|
dax_iomap_pte_fault() (4KB PTE 故障)
dax_iomap_pmd_fault() (2MB PMD 故障,CONFIG_FS_DAX_PMD)
|
iomap_iter() + dax_fault_iter()
|
dax_iomap_direct_access()
|
dax_direct_access() [drivers/dax/super.c]
|
pmem_dax_direct_access() → __pmem_direct_access()
|
返回 PFN(持久内存页帧号)
|
vmf_insert_page_mkwrite() / vmf_insert_folio_pmd()
|
进程页表直接指向持久内存物理页
fs/dax.c 使用文件的 address_space.i_pages(一个 XArray)跟踪哪些文件偏移已经被 DAX 映射。与普通文件不同,这里存储的不是 struct page *,而是 XArray value entry(xa_is_value() 为真),编码了 PFN 和标志位(fs/dax.c 第 62~66 行):
#define DAX_SHIFT (4)
#define DAX_LOCKED (1UL << 0) // 条目被锁定(正在处理 fault)
#define DAX_PMD (1UL << 1) // PMD 级别映射(2MB)
#define DAX_ZERO_PAGE (1UL << 2) // 零页(空洞)
#define DAX_EMPTY (1UL << 3) // 空条目(仅用于锁定)高位存储 PFN:pfn = xa_to_value(entry) >> DAX_SHIFT(第 69 行 dax_to_pfn())。
dax_iomap_pte_fault()(fs/dax.c 第 1862 行)处理 4KB 页级别的 DAX 缺页故障:
static vm_fault_t dax_iomap_pte_fault(struct vm_fault *vmf, ...)
{
XA_STATE(xas, &mapping->i_pages, vmf->pgoff);
struct iomap_iter iter = {
.inode = mapping->host,
.pos = (loff_t)vmf->pgoff << PAGE_SHIFT,
.len = PAGE_SIZE,
.flags = IOMAP_DAX | IOMAP_FAULT,
};
...
entry = grab_mapping_entry(&xas, mapping, 0); // 获取/创建 XArray 条目
...
while ((error = iomap_iter(&iter, ops)) > 0) {
ret = dax_fault_iter(vmf, &iter, pfnp, &xas, &entry, false);
...
}
dax_unlock_entry(&xas, entry);
return ret;
}dax_fault_iter() 内部(第 1805 行)调用 dax_iomap_direct_access() 获取 PFN,然后调用 vmf_insert_page_mkwrite() 将持久内存物理页插入进程页表:
err = dax_iomap_direct_access(iomap, pos, size, &kaddr, &pfn);
...
*entry = dax_insert_entry(xas, vmf, iter, *entry, pfn, entry_flags);
...
ret = vmf_insert_page_mkwrite(vmf, pfn_to_page(pfn), write);当满足条件(VMA 对齐、文件偏移对齐、非 COW 写)时,内核尝试 PMD 级别的 2MB 映射(dax_iomap_pmd_fault(),fs/dax.c 第 1972 行):
XA_STATE_ORDER(xas, &mapping->i_pages, vmf->pgoff, PMD_ORDER);
struct iomap_iter iter = {
.len = PMD_SIZE, // 2MB
.flags = IOMAP_DAX | IOMAP_FAULT,
};PMD 映射的好处是减少 TLB miss:每次 TLB 缺失可以覆盖 2MB 而非 4KB,对大数据集访问性能提升显著。最终通过 vmf_insert_folio_pmd() 完成 PMD 级别的页表项填充。
dax_fault_check_fallback() 函数(第 1939 行)会检查多个回退条件(地址对齐、COW、VMA 边界等),确保 PMD 映射的正确性。
当 VMA 带有 VM_SYNC 标志(mmap(MAP_SYNC))且写操作发生时,dax_fault_is_synchronous() 检查(第 1028 行):
static bool dax_fault_is_synchronous(const struct iomap_iter *iter,
struct vm_area_struct *vma)
{
return (iter->flags & IOMAP_WRITE) && (vma->vm_flags & VM_SYNC) &&
(iter->iomap.flags & IOMAP_F_DIRTY);
}同步模式下,dax_fault_synchronous_pfnp() 返回 VM_FAULT_NEEDDSYNC,推迟页表项的安装,强制文件系统先完成元数据的持久化。
dax_writeback_mapping_range() 函数(fs/dax.c 第 1199 行)处理 fsync() 调用:
int dax_writeback_mapping_range(struct address_space *mapping,
struct dax_device *dax_dev, struct writeback_control *wbc)
{
...
xas_for_each_marked(&xas, entry, end_index, PAGECACHE_TAG_TOWRITE) {
ret = dax_writeback_one(&xas, dax_dev, mapping, entry);
}
...
}dax_writeback_one() 中(第 1173 行)调用 dax_flush(),最终触发 arch_wb_cache_pmem(),将 CPU 缓存中的脏数据刷写到持久内存。
DAX 模式下写时复制需要特殊处理。当发生 COW 写故障时(私有 mmap 写),dax_iomap_pte_fault() 检测到 IOMAP_F_SHARED 标志,会调用 dax_iomap_copy_around() 将数据复制到新页面,而不能直接映射到共享的持久内存页。这是 DAX 与普通文件页缓存的重要区别之一。
BTT 为持久内存提供原子写语义,解决"撕裂写"(torn write)问题——在没有 BTT 的情况下,掉电可能导致一个扇区只有部分数据被写入。
持久内存的写操作虽然快速,但并非原子的。假设写入一个 512 字节的扇区需要分多个缓存行写入,如果中途掉电,该扇区数据将处于不一致状态。BTT 通过写时复制和预写日志机制解决这个问题。
BTT 将持久内存划分为若干 Arena(竞技场),每个 Arena 最大 512 GB(ARENA_MAX_SIZE = 1ULL << 39),最小 16 MB(ARENA_MIN_SIZE = 1UL << 24),定义于 drivers/nvdimm/btt.h 第 23 行。
每个 Arena 的内部布局如下:
Arena 内部布局(从 arena_off 开始)
+------------------+ <- infooff(BTT 超级块,4KB)
| Info Block | btt_sb 结构体,含签名、UUID、版本号
+------------------+ <- dataoff
| |
| Data Area | 实际数据块存储区
| (internal_nlba | internal_nlba = external_nlba + nfree
| * lbasize) | nfree 块是"自由块"缓冲池
| |
+------------------+ <- mapoff
| Map Area | 逻辑块号 → 内部块号的映射表
| (外部 LBA * | 每个条目 4 字节(MAP_ENT_SIZE)
| MAP_ENT_SIZE) | 带 Z/E 标志位
+------------------+ <- logoff
| Log Area | 写操作的预写日志
| (nfree 个 | 每个 lane 一个 log_group(64 字节)
| log_group) |
+------------------+ <- info2off
| Info Block 2 | Info Block 的备份
+------------------+ <- nextoff(0 = 最后一个 Arena)
这些偏移的计算逻辑在 alloc_arena() 函数中(btt.c 第 745 行):
arena->infooff = arena_off;
arena->dataoff = arena->infooff + BTT_PG_SIZE; // info block 之后
arena->mapoff = arena->dataoff + datasize;
arena->logoff = arena->mapoff + mapsize;
arena->info2off = arena->logoff + logsize;BTT 超级块(Info Block)定义于 btt.h 第 95 行:
struct btt_sb {
u8 signature[BTT_SIG_LEN]; // "BTT_ARENA_INFO\0"(16 字节)
u8 uuid[16];
u8 parent_uuid[16];
__le32 flags;
__le16 version_major;
__le16 version_minor;
__le32 external_lbasize; // 对外暴露的扇区大小
__le32 external_nlba; // 对外暴露的块数
__le32 internal_lbasize; // 内部存储的块大小(对齐到 64 字节)
__le32 internal_nlba; // 内部存储的块数(含 nfree)
__le32 nfree; // 自由块数(并发写并行度 = ND_MAX_LANES = 256)
__le32 infosize;
__le64 nextoff; // 下一个 Arena 的偏移(0 = 最后)
__le64 dataoff;
__le64 mapoff;
__le64 logoff;
__le64 info2off; // 备份 Info Block 的偏移
u8 padding[3968];
__le64 checksum; // 整个超级块的校验和
};internal_lbasize 会将外部块大小向上对齐到 INT_LBASIZE_ALIGNMENT = 64 字节(nd.h 第 24 行),这是为了确保每个数据块的读写对缓存行友好。
arena_info 是 Arena 的完整运行时描述符(btt.h 第 168 行):
struct arena_info {
u64 size; // Arena 总字节数(含元数据)
u64 external_lba_start; // 本 Arena 在整个 BTT 中的起始 LBA
u32 internal_nlba; // 内部块数(= external_nlba + nfree)
u32 internal_lbasize; // 内部块大小
u32 external_nlba; // 对外暴露的块数
u32 external_lbasize; // 对外暴露的块大小
u32 nfree; // 自由块数
u16 version_major;
u16 version_minor;
u32 sector_size;
// 各区域的字节偏移
u64 nextoff, infooff, dataoff, mapoff, logoff, info2off;
// 运行时数据结构
struct free_entry *freelist; // 自由块链表(nfree 个条目)
u32 *rtt; // Read Tracking Table(nfree 个条目)
struct aligned_lock *map_locks; // map 写锁(nfree 个,每个填充到 cache line)
struct nd_btt *nd_btt;
struct list_head list;
struct dentry *debugfs_dir;
u32 flags;
struct mutex err_lock;
int log_index[2]; // log_group 中两个有效条目的索引
};Map 区域中每个 4 字节条目编码一个逻辑块号到内部块号的映射(btt.h 第 14~39 行):
位 [29:0] = 内部块号(LBA)
位 [30] = E flag(Error,介质错误)
位 [31] = Z flag(Zero/Trim,已丢弃)
E=1, Z=1(MAP_ENT_NORMAL = 0xC0000000)= 正常映射
E=1, Z=0 = 错误
E=0, Z=1 = 已 trim
E=0, Z=0 = 初始状态(身份映射,不存储在 map 中)
btt_map_read() 和 btt_map_write() 函数(btt.c 第 152、108 行)负责解析和更新这些条目。
btt_map_write() 对不同 Z/E 组合的处理(btt.c 第 108 行):
static int btt_map_write(struct arena_info *arena, u32 lba, u32 mapping,
u32 z_flag, u32 e_flag, unsigned long rwb_flags)
{
ze = (z_flag << 1) + e_flag;
switch (ze) {
case 0:
mapping |= MAP_ENT_NORMAL; // 正常,设置 E=1, Z=1
break;
case 1:
mapping |= (1 << MAP_ERR_SHIFT); // 错误状态
break;
case 2:
mapping |= (1 << MAP_TRIM_SHIFT); // Trim/Zero 状态
break;
}
mapping_le = cpu_to_le32(mapping);
return __btt_map_write(arena, lba, mapping_le, rwb_flags);
}每个并发写"lane"对应一个 log_group,包含 4 个 log_entry,其中两个有效、两个是填充(btt.h 第 84 行):
struct log_entry {
__le32 lba; // 正在写的逻辑块号
__le32 old_map; // 写之前该 LBA 对应的内部块号
__le32 new_map; // 写之后该 LBA 对应的新内部块号
__le32 seq; // 单调递增序列号(用于判断新旧)
};
struct log_group {
struct log_entry ent[4]; // 4 个条目,2 有效 2 填充
};原子写协议(btt_write_pg() 调用链,btt.c):
1. 从 freelist 取一个空闲内部块(old_block)
2. 将新数据写入 old_block(此时 old_block 尚未出现在 map 中)
3. 写 log 条目:log.new_map = old_block
log.old_map = 当前 map[lba](即将被替换的块)
log.seq = 递增序列号
4. 写 map[lba] = new_block(原子化:整个 map 条目是 4 字节对齐写)
5. 把 old_block(即被替换掉的那个块)放回 freelist
关键:步骤 3 的 log 写入分为两个 8 字节写,通过 NVDIMM_IO_ATOMIC 标志确保每半写的原子性(btt.c 第 374 行):
// split the 16B write into atomic, durable halves
ret = arena_write_bytes(arena, ns_off, src, log_half, flags); // 前 8 字节
ns_off += log_half;
src += log_half;
return arena_write_bytes(arena, ns_off, src, log_half, flags); // 后 8 字节掉电恢复时,通过比较 log 中 old_map 和 new_map 与实际 map 的一致性来判断写操作是否完成,必要时回滚。
Arena 维护一个 freelist(每个 lane 一个 free_entry)和一个 RTT(Read Tracking Table,u32 *rtt,btt.h 第 188 行)。RTT 用于防止一个 lane 在另一个 lane 正在写同一自由块时发生读后写覆盖。
并发控制由 map_locks(每 lane 一个 aligned_lock,btt.h 第 124 行)提供,aligned_lock 将 spinlock 填充到整个 cache line,避免伪共享:
struct aligned_lock {
union {
spinlock_t lock;
u8 cacheline_padding[L1_CACHE_BYTES]; // 避免 false sharing
};
};free_entry 结构记录自由块的详细信息(btt.h 第 117 行):
struct free_entry {
u32 block; // 空闲块的内部块号
u8 sub; // 所属的 log 条目索引(old 或 new)
u8 seq; // 对应的序列号
u8 has_err; // 该块是否有错误标记
};内核 4.15 之前存在一个 bug,log 条目的位置不同。log_set_indices() 函数(btt.c 第 624 行)在挂载时自动检测 log 格式(旧格式条目在索引 0、2,新格式在索引 0、1),确保向后兼容。
旧格式(pre-4.15):
[ent[0]: 有效] [ent[1]: 填充] [ent[2]: 有效] [ent[3]: 填充]
新格式(4.15+):
[ent[0]: 有效] [ent[1]: 有效] [ent[2]: 填充] [ent[3]: 填充]
arena->log_index[0] 和 arena->log_index[1] 存储检测到的两个有效条目索引。
btt.init_state 跟踪 BTT 的初始化阶段(btt.h 第 41 行):
enum btt_init_state {
INIT_UNCHECKED = 0, // 尚未检查(默认状态)
INIT_NOTFOUND, // 未找到 BTT 超级块(不是 BTT 设备)
INIT_READY // BTT 初始化完成,可以 I/O
};btt_rw_page() 在 INIT_UNCHECKED 时触发懒初始化,避免在探测阶段浪费时间。
arch_wb_cache_pmem() 是 x86 架构的缓存写回函数,实现于 arch/x86/lib/usercopy_64.c 第 40 行:
void arch_wb_cache_pmem(void *addr, size_t size)
{
clean_cache_range(addr, size);
}
static void clean_cache_range(void *addr, size_t size)
{
u16 x86_clflush_size = boot_cpu_data.x86_clflush_size; // 通常 64 字节
unsigned long clflush_mask = x86_clflush_size - 1;
void *vend = addr + size;
void *p;
// 对每个 cache line 发出 CLWB 指令
for (p = (void *)((unsigned long)addr & ~clflush_mask);
p < vend; p += x86_clflush_size)
clwb(p); // Cache Line Write Back(不驱逐缓存行)
}clwb() 的汇编实现(arch/x86/include/asm/special_insns.h 第 197 行)使用编译时特性检测:
static inline void clwb(volatile void *__p)
{
// 优先使用 CLWB,回退到 CLFLUSHOPT,最后回退到 CLFLUSH
asm volatile(ALTERNATIVE_2(
"clflush %0", // 最老的处理器:驱逐 + 写回
"clflushopt %0", X86_FEATURE_CLFLUSHOPT, // 优化版:不保证顺序
"clwb %0", X86_FEATURE_CLWB) // 最新:写回但保留缓存
: "+m"(*(volatile char __force *)__p));
}| 指令 | 功能 | 缓存行状态 | 性能 |
|---|---|---|---|
CLFLUSH |
写回并驱逐 | 变为 Invalid | 最慢(驱逐后再访问需重新加载) |
CLFLUSHOPT |
写回并驱逐(弱排序) | 变为 Invalid | 中等(可并行多条) |
CLWB |
写回但保留(弱排序) | 保持 Clean | 最快(缓存行仍可用) |
NVDIMM 优先使用 CLWB,因为它不会驱逐缓存行,避免后续读取的 cache miss。
除了 CLWB 写回路径,内核还使用 NT(Non-Temporal)Store 指令直接写入内存,绕过 CPU 缓存:
普通 Store 路径:
CPU Core → L1D Cache → L2 Cache → L3 Cache → 内存控制器 → DIMM
^缓存行变 Modified
NT Store 路径:
CPU Core → Write Combine Buffer(WCB) → 内存控制器 → DIMM
^不进入缓存层次
memcpy_flushcache() 的 x86 实现(arch/x86/lib/copy_user_uncached_64.S)使用 movntdq(128 位 NT Store)批量写入数据,最后发出 sfence 保证写入顺序。
持久内存写的完整顺序(以无 eADR 的场景为例):
1. 应用程序写数据到持久内存(通过 store 指令或 memcpy)
↓
数据在 CPU 缓存中(处于 Modified 状态)
2. CLWB addr(每个 cache line)
↓
数据写入内存控制器写缓冲区,缓存行变为 Clean
3. SFENCE(或 MFENCE)
↓
确保所有 CLWB 已完成(CLWB 是弱排序指令)
4. (如果有 ADR/eADR)掉电时内存控制器自动把缓冲区内容刷到 DIMM
或
(如果没有 ADR)平台不保证,可能数据丢失
内核通过 memcpy_flushcache() 在写路径上结合 NT store 和 SFENCE 实现这一顺序,具体见 arch/x86/lib/copy_user_uncached_64.S 第 129 行的 sfence 指令。
pmem_attach_disk() 中的决策逻辑(pmem.c 第 493 行):
fua = nvdimm_has_flush(nd_region); // 检查 flush 机制是否可用
if (!IS_ENABLED(CONFIG_ARCH_HAS_UACCESS_FLUSHCACHE) || fua < 0) {
dev_warn(dev, "unable to guarantee persistence of writes\n");
fua = 0;
}
if (fua)
lim.features |= BLK_FEAT_FUA; // 支持强制单元访问
...
dax_write_cache(dax_dev, nvdimm_has_cache(nd_region));
// nvdimm_has_cache() 检查 ND_REGION_PERSIST_CACHE 标志(eADR)
// 如果有 eADR,DAXDEV_WRITE_CACHE=0,不需要 CLWB
// 如果没有 eADR,DAXDEV_WRITE_CACHE=1,fsync 时需要 CLWB部分 NVDIMM 硬件支持通过写特定的物理地址来触发 DIMM 内部的刷新操作,这些地址称为"Flush Hint Address"(在 NFIT 的 Flush Hint Address 子表中记录)。nvdimm_flush() 函数(core.c)在 REQ_FUA 或 REQ_PREFLUSH 请求时使用这些地址:
// nd_region 的 flush 回调(NFIT 驱动注册)
// 向每个 DIMM 的刷新提示地址写入 0,触发硬件刷新
for (i = 0; i < nd_region->ndr_mappings; i++) {
struct nd_mapping *nd_mapping = &nd_region->mapping[i];
struct nfit_mem *nfit_mem = nvdimm_provider_data(nd_mapping->nvdimm);
for (j = 0; j < nfit_mem->nfit_flush->flush->hint_count; j++)
writeq(0, flush_wpq[j]); // 写刷新提示地址
}
wmb(); // 确保写入可见NVDIMM 的 NUMA 亲和性由 NFIT 表中的 proximity domain 字段决定,传递到 nd_region:
// nd.h 第 418 行
int id, num_lanes, ro, numa_node, target_node;numa_node:持久内存物理所在的 NUMA 节点。target_node:访问此持久内存的最优 CPU NUMA 节点(某些平台上二者可能不同)。
pmem_attach_disk() 中将 NUMA 节点信息传给 blk_alloc_disk():
int nid = dev_to_node(dev), fua;
...
disk = blk_alloc_disk(&lim, nid); // 在对应 NUMA 节点分配资源每个 NFIT SPA Range 对应一个 nd_region。在 ACPI NFIT 驱动中(core.c),解析 NFIT 表时会为每个 NFIT_SPA_PM 类型的 SPA 创建一个 nd_region_desc,通过 nvdimm_pmem_region_create() 注册到 libnvdimm 总线。
nd_region 的起始地址和大小直接来自 SPA 的 spa_base 和 spa_length 字段:
// nd_region 中记录的物理地址信息
u64 ndr_size; // = spa_length
u64 ndr_start; // = spa_base多 DIMM 配置下,NFIT 的 Interleave Descriptor 描述如何在多个 DIMM 之间交错数据,以提高带宽。内核用 nd_interleave_set 追踪交错集:
// include/linux/libnvdimm.h 第 108 行
struct nd_interleave_set {
u64 cookie1; // ACPI 6.1 定义的 interleave set cookie
u64 cookie2; // ACPI 6.2 定义的算法(修正了 bug)
u64 altcookie; // 与早期 Linux 实现兼容的备用 cookie
guid_t type_guid;
};nd_region.nd_set 指向对应的交错集,用于验证 namespace label 的 isetcookie 字段,确保 namespace 确实属于这个交错集(nd.h 第 170 行 nsl_validate_isetcookie())。
ARS 是 NFIT 定义的一种地址范围扫描机制,用于主动检测持久内存的介质错误。内核在 core.c 中实现了三种 ARS 状态(nfit.h 第 157 行):
enum nfit_ars_state {
ARS_REQ_SHORT, // 快速扫描请求(启动时)
ARS_REQ_LONG, // 完整扫描请求
ARS_FAILED, // 扫描失败
};nfit_spa.ars_state 记录每个 SPA Range 的当前 ARS 状态。ARS 发现的坏块会被加入 pmem_device.bb(badblocks 结构),后续 I/O 会自动检查坏块表。
ARS 的工作流程:
系统启动 / NFIT 更新通知
|
acpi_nfit_register_regions() → 对每个 SPA 发送 ARS_REQ_SHORT
|
ars_start() → ND_CMD_ARS_START(DSM 命令)
|
DIMM 固件扫描介质 ...(最多 90 秒)
|
ars_complete() → ND_CMD_ARS_STATUS(轮询直到完成)
|
acpi_nfit_blk_region_do_io → 解析结果,更新坏块
|
nvdimm_account_cleared_poison() / nvdimm_badblocks_populate()
|
通知 nd_pmem 驱动(NVDIMM_REVALIDATE_POISON 事件)
scrub_flags 使用原子位操作管理 ARS 并发(nfit.h 第 230~235 行):
enum scrub_flags {
ARS_BUSY, // 正在执行 ARS
ARS_CANCEL, // 请求取消 ARS
ARS_VALID, // 上次 ARS 结果有效
ARS_POLL, // 正在轮询 ARS 状态
};为了最大化并发 I/O 性能,libnvdimm 使用 per-CPU 的 lane 机制。nd_region.lane 是一个 per-CPU 指针,每个 CPU 使用独立的 lane 避免锁竞争(nd.h 第 368 行):
struct nd_percpu_lane {
int count; // 当前持有该 lane 的嵌套深度
spinlock_t lock;
};nd_region_acquire_lane() / nd_region_release_lane() 管理 lane 的获取与释放。最大 lane 数等于 ND_MAX_LANES = 256,系统上 CPU 数量超过 256 时多个 CPU 共享同一 lane。
pmem_do_read() 和 pmem_do_write() 在每次 I/O 前调用 is_bad_pmem():
// pmem.c 第 172 行
if (unlikely(is_bad_pmem(&pmem->bb, sector, len)))
return BLK_STS_IOERR;badblocks 结构维护一个有序的坏块区间列表,is_bad_pmem() 用 badblocks_check() 进行区间查找。
写路径中,如果目标扇区有坏块记录,驱动先尝试清除介质毒素(pmem.c 第 187 行):
if (unlikely(is_bad_pmem(&pmem->bb, sector, len))) {
blk_status_t rc = pmem_clear_poison(pmem, pmem_off, len);
if (rc != BLK_STS_OK)
return rc;
}pmem_clear_poison() → __pmem_clear_poison() → nvdimm_clear_poison() 通过 ACPI DSM 命令(ND_CMD_CLEAR_ERROR)通知固件清除介质错误,然后调用 arch_invalidate_pmem() 使对应的内核映射无效。
arch_invalidate_pmem() 的 x86 实现(arch/x86/mm/iomap_32.c)通过 clflushopt 使受影响的缓存行无效,确保后续读取不会从缓存返回旧的(含毒的)数据。
DAX 路径下(pmem_recovery_write(),pmem.c 第 325 行),当检测到坏块时,驱动:
- 先清除介质毒素(
__pmem_clear_poison())。 - 清除页面的
HWPoison标志(pmem_mkpage_present(),第 63 行)。 - 重新执行写操作(
_copy_from_iter_flushcache())。 - 清除坏块记录(
pmem_clear_bb())。
pmem_mkpage_present() 函数(pmem.c 第 63 行)的详细逻辑:
static void pmem_mkpage_present(struct pmem_device *pmem, phys_addr_t offset,
unsigned int len)
{
phys_addr_t phys = pmem_to_phys(pmem, offset);
// 只有 linear map 中的 pmem 才支持 HWPoison 标记
if (is_vmalloc_addr(pmem->virt_addr))
return;
pfn_start = PHYS_PFN(phys);
pfn_end = pfn_start + PHYS_PFN(len);
for (pfn = pfn_start; pfn < pfn_end; pfn++) {
struct page *page = pfn_to_page(pfn);
if (test_and_clear_pmem_poison(page))
clear_mce_nospec(pfn); // 清除 MCE no-spec 标记
}
}pmem_pagemap_memory_failure() 回调(pmem.c 第 433 行)在 ZONE_DEVICE 页面发生硬件错误时被调用,通过 dax_holder_notify_failure() 通知上层(文件系统或数据库)进行错误处理:
static int pmem_pagemap_memory_failure(struct dev_pagemap *pgmap,
unsigned long pfn, unsigned long nr_pages, int mf_flags)
{
struct pmem_device *pmem = container_of(pgmap, struct pmem_device, pgmap);
u64 offset = PFN_PHYS(pfn) - pmem->phys_addr - pmem->data_offset;
u64 len = nr_pages << PAGE_SHIFT;
return dax_holder_notify_failure(pmem->dax_dev, offset, len, mf_flags);
}文件系统(如 ext4/XFS)通过 dax_holder_operations.notify_failure 回调接收通知,标记相关 inode 为损坏,防止后续访问。
每个 PMEM 块设备通过 sysfs 暴露坏块信息:
/sys/block/pmem0/badblocks
→ 格式:"起始扇区 长度\n"(每行一个区间)
驱动在 pmem_attach_disk() 中注册 sysfs 通知节点(pmem.c 第 580 行):
pmem->bb_state = sysfs_get_dirent(disk_to_dev(disk)->kobj.sd, "badblocks");
if (!pmem->bb_state)
dev_warn(dev, "'badblocks' notification disabled\n");当坏块表更新时,pmem_clear_bb() 调用 sysfs_notify_dirent(pmem->bb_state) 通知用户空间监听器(如 ndctl monitor)。
系统启动
|
acpi_nfit_probe() [drivers/acpi/nfit/core.c]
acpi_nfit_init()
|
解析 NFIT 子表 → 填充 acpi_nfit_desc 的各个链表
|
acpi_nfit_register_dimms() → 为每个 DIMM 创建 nvdimm 设备
acpi_nfit_register_regions() → 为每个 SPA_PM 范围:
|
├── 创建 nd_region_desc
├── 计算 NUMA 节点
├── 收集交错信息 → nd_interleave_set
└── nvdimm_pmem_region_create() → 注册 nd_region
nd_region 设备注册到 nvdimm bus
|
nd_region 驱动 probe(drivers/nvdimm/region_devs.c)
|
nd_region_register_namespaces()
|
为每个 namespace label 解析 → 创建 nd_namespace_pmem 设备
|
nd_namespace_pmem 驱动 probe(drivers/nvdimm/pmem.c)
|
nd_pmem_probe()
|
├── (BTT 模式) nd_btt_probe() → 创建 nd_btt 设备
├── (PFN 模式) nd_pfn_probe() → 创建 nd_pfn 设备
└── (普通模式) pmem_attach_disk()
|
├── devm_memremap_pages() / devm_memremap() → 映射持久内存
├── blk_alloc_disk() → 分配 gendisk
├── alloc_dax() → 创建 dax_device
├── dax_add_host() → 注册到 DAX XArray
├── device_add_disk() → 注册块设备
└── 暴露 /dev/pmemN
nd_region_probe()(drivers/nvdimm/region.c 第 13 行)是 nd_region 设备的探测函数:
static int nd_region_probe(struct device *dev)
{
struct nd_region *nd_region = to_nd_region(dev);
// 警告 CPU 数量与 lane 数量不匹配
if (nd_region->num_lanes > num_online_cpus() ...)
dev_dbg(dev, "setting nr_cpus=%d may yield better performance\n",
nd_region->num_lanes);
rc = nd_region_activate(nd_region); // 分配 per-CPU lane 资源
if (devm_init_badblocks(dev, &nd_region->bb))
return -ENODEV;
// 初始化 region 级别的坏块表
nvdimm_badblocks_populate(nd_region, &nd_region->bb, &range);
// 注册所有 namespace
rc = nd_region_register_namespaces(nd_region, &err);
// 创建种子设备(用于创建新 namespace 的模板)
nd_region->btt_seed = nd_btt_create(nd_region);
nd_region->pfn_seed = nd_pfn_create(nd_region);
nd_region->dax_seed = nd_dax_create(nd_region);
return 0;
}种子设备(seed device)是 libnvdimm 特有的概念:当用户通过 sysfs 写入种子设备的 uuid 和 mode 属性时,内核会在 region 下创建对应类型的新 namespace/BTT/PFN/DAX 设备。
nvdimm_drvdata(nd.h 第 28 行)存储从 DIMM NVRAM(配置存储区)读取的 namespace label 数据:
struct nvdimm_drvdata {
struct device *dev;
int nslabel_size; // 每个 label 的字节数
struct nd_cmd_get_config_size nsarea;
void *data; // 原始 label 数据缓冲区
bool cxl; // 是否为 CXL 设备(不同 label 格式)
int ns_current, ns_next; // 当前/下一个 namespace index 槽位
struct resource dpa; // DIMM 物理地址(DPA)资源树
struct kref kref;
};内核支持两种 label 格式(通过 ndd->cxl 区分):
- EFI 格式(传统 NVDIMM):
nd_label->efi.* - CXL 格式(新型 CXL 设备):
nd_label->cxl.*
nd.h 中定义了大量 nsl_get_*() / nsl_set_*() 内联函数(第 39~300 行),统一处理这两种格式的字段访问。
以 fsdax 模式下的 pwrite() 为例:
系统调用 pwrite()
|
vfs_write() → ext4_file_write_iter()
|
dax_iomap_rw() [fs/dax.c:1707]
|
iomap_iter() → ext4_iomap_begin() [ext4 分配/查找块]
|
dax_iomap_iter() [fs/dax.c:1580]
|
dax_direct_access() [super.c:149] → 获取内核虚地址
|
dax_copy_from_iter() [super.c:171]
→ _copy_from_iter_flushcache() [使用 NT store 指令]
|
返回
DAX 写的关键性能优势:完全绕过页缓存,数据直接通过 CPU 存储路径写入持久内存,延迟在微秒级(对比 NVMe SSD 的十到百微秒)。
首次访问 mmap 地址(缺页故障)
|
fault 处理 ~1-5 μs(内核态)
|
dax_iomap_pte_fault()
|
dax_direct_access() ~100-200 ns
|
vmf_insert_page() ~100 ns(修改页表)
|
返回用户态
|
后续访问(TLB 命中) ~200-300 ns(Optane DIMM 延迟)
PMD 映射后 TLB 覆盖 2MB,减少 TLB miss 频率
BTT 为每次写引入额外的元数据操作:
btt_write_pg()
|
btt_map_read() → 读 4 字节 map 条目
__btt_log_write() → 写 16 字节 log(分两次 8 字节)
memcpy 数据到自由块 → 实际数据写入
btt_map_write() → 写 4 字节 map 条目(原子)
btt_flog_write() → 更新 freelist log
BTT 的写放大约为 1.5~2x(多了 log 和 map 的读写),但换来了原子写语义。
nd_region.lane 是 per-CPU 的:
// nd.h 第 368 行
struct nd_percpu_lane {
int count;
spinlock_t lock;
};BTT 的 nfree(通常等于 ND_MAX_LANES = 256)决定了最大并发写数量。每个 CPU 对应一个 lane,lane 内有自己的 freelist 和 log 槽,完全避免跨 CPU 竞争,实现高并发写入。
PMEM 驱动不使用 blk-mq 队列,但仍然支持 IO 统计(pmem.c 第 214 行):
do_acct = blk_queue_io_stat(bio->bi_bdev->bd_disk->queue);
if (do_acct)
start = bio_start_io_acct(bio);
// ... 执行 I/O ...
if (do_acct)
bio_end_io_acct(bio, start);通过 /sys/block/pmem0/stat 可以查看 PMEM 的 I/O 统计(延迟分布、读写次数、字节数等)。
每个 NVDIMM 在持久存储中有一个专用区域(Label Storage Area,LSA)用于存储 namespace label。LSA 由 DIMM 固件管理,软件通过 ND_CMD_GET_CONFIG_DATA / ND_CMD_SET_CONFIG_DATA DSM 命令读写。
LSA 的逻辑布局:
DIMM LSA 布局
+------------------+ ← 偏移 0
| Index Block 0 | nd_namespace_index(256 字节对齐)
| (主 namespace | 包含签名、序列号、label 槽位图
| 目录) |
+------------------+ ← 偏移 sizeof_namespace_index()
| Index Block 1 | Index Block 的备份(轮流更新)
+------------------+ ← 偏移 label_offset
| Label Slot 0 | nvdimm_efi_label 或 nvdimm_cxl_label
+------------------+
| Label Slot 1 |
+------------------+
| ... |
+------------------+
| Label Slot N-1 |
+------------------+
nd_namespace_index(label.h 第 53 行)是 LSA 的超级块,记录哪些 label 槽是有效的:
struct nd_namespace_index {
u8 sig[NSINDEX_SIG_LEN]; // "NAMESPACE_INDEX\0"(16 字节)
u8 flags[3];
u8 labelsize; // log2(label 大小):v1=7(128B),v2=8(256B)
__le32 seq; // 序列号(单调递增,3 bit 有效)
__le64 myoff; // 本 index 在 LSA 中的偏移
__le64 mysize; // 本 index 的大小
__le64 otheroff; // 另一个 index 的偏移
__le64 labeloff; // label 槽区域的起始偏移
__le32 nslot; // 总 label 槽数
__le16 major; // 版本主号
__le16 minor; // 版本次号
__le64 checksum; // fletcher64 校验和
u8 free[]; // 位图:每位对应一个 label 槽(1=空闲)
};两个 Index Block 轮流更新(类似日志),通过序列号(seq)判断哪个是最新版本:
// label.h 第 206 行
static inline int nd_label_next_nsindex(int index)
{
if (index < 0)
return -1;
return (index + 1) % 2; // 0 ↔ 1 交替
}传统 NVDIMM 使用 EFI 风格的 namespace label(label.h 第 120 行),关键字段:
struct nvdimm_efi_label {
u8 uuid[NSLABEL_UUID_LEN]; // namespace 的 UUID
u8 name[NSLABEL_NAME_LEN]; // 可选名称(64 字节)
__le32 flags; // NSLABEL_FLAG_BTT 等
__le16 nlabel; // 描述同一 namespace 的 label 总数
__le16 position; // 本 label 在集合中的位置
__le64 isetcookie; // 交错集 cookie(验证用)
__le64 lbasize; // 扇区大小(0 = pmem)
__le64 dpa; // 在本 DIMM DPA 空间中的起始地址
__le64 rawsize; // 本 label 贡献的字节数
__le32 slot; // 本 label 在 LSA 中的槽号(自引用)
u8 align; // 对齐参数
guid_t type_guid; // SPA Range 类型 GUID
guid_t abstraction_guid; // BTT/PFN/DAX GUID
__le64 checksum; // fletcher64 校验和
};CXL 设备使用不同的 label 格式(label.h 第 161 行),主要差异在于支持不连续(discontiguous)namespace 和区域 UUID:
struct nvdimm_cxl_label {
u8 type[NSLABEL_UUID_LEN]; // 标识 label 类型的 UUID
u8 uuid[NSLABEL_UUID_LEN]; // namespace UUID
u8 name[NSLABEL_NAME_LEN]; // 名称
__le32 flags;
__le16 nrange; // 不连续区间数
__le16 position;
__le64 dpa; // DPA 起始地址
__le64 rawsize; // 字节数
__le32 slot;
__le32 align; // 256MB 块对齐
u8 region_uuid[16]; // 宿主交错集标识
u8 abstraction_uuid[16]; // 抽象层类型
__le16 lbasize; // 扇区大小
__le64 checksum;
};内核在 nd.h 中定义了统一的访问函数族(第 39~300 行),屏蔽两种格式的差异:
// 示例:获取 DPA 字段
static inline u64 nsl_get_dpa(struct nvdimm_drvdata *ndd,
struct nd_namespace_label *nd_label)
{
if (ndd->cxl)
return __le64_to_cpu(nd_label->cxl.dpa);
return __le64_to_cpu(nd_label->efi.dpa);
}Label 使用 Fletcher-64 算法进行完整性校验。nd_fletcher64() 计算校验和,nsl_validate_checksum() 验证 label 的完整性。Fletcher-64 相比 CRC 计算更快,且对单比特错误有良好的检测能力。
当 namespace 以 fsdax 或 devdax 模式使用时,持久内存开头存储一个 PFN 超级块,记录 struct page 数组的位置和大小(drivers/nvdimm/pfn.h 第 16 行):
struct nd_pfn_sb {
u8 signature[PFN_SIG_LEN]; // "NVDIMM_PFN_INFO\0" 或 "NVDIMM_DAX_INFO\0"
u8 uuid[16]; // 与 namespace uuid 对应
u8 parent_uuid[16]; // 所属 namespace 的 uuid
__le32 flags;
__le16 version_major;
__le16 version_minor;
__le64 dataoff; // 数据区起始偏移(相对 namespace 起始 + start_pad)
__le64 npfns; // struct page 数组覆盖的页面数
__le32 mode; // PFN_MODE_RAM(0)或 PFN_MODE_PMEM(1)
__le32 start_pad; // 起始对齐填充(已废弃,但保留兼容)
__le32 end_trunc; // 末尾对齐截断
__le32 align; // 映射对齐(通常 2MB)
__le32 page_size; // 系统页大小
__le16 page_struct_size; // sizeof(struct page)
u8 padding[3994];
__le64 checksum;
};两种签名(pfn.h 第 13~14 行):
PFN_SIG = "NVDIMM_PFN_INFO\0":fsdax 模式(有 struct page,映射到 ZONE_DEVICE)DAX_SIG = "NVDIMM_DAX_INFO\0":devdax 模式(有 struct page,但不进入文件系统)
在 nvdimm_setup_pfn()(drivers/nvdimm/pfn_devs.c)中,内核根据 PFN 超级块配置 dev_pagemap:
PFN_MODE_RAM:
├── struct page 数组在普通 DRAM 中(通过 vmalloc 或 alloc_pages)
├── 持久内存全部用于数据
└── devm_memremap_pages() → ZONE_DEVICE 类型 MEMORY_DEVICE_FS_DAX
PFN_MODE_PMEM:
├── struct page 数组存储在持久内存开头(data_offset 之前)
├── 节省 DRAM,但 struct page 本身不是持久的(元数据)
└── devm_memremap_pages() → ZONE_DEVICE 类型 MEMORY_DEVICE_FS_DAX
每个 ZONE_DEVICE 页面的 struct page 中:
page->pgmap:指向dev_pagemap(通过page_pgmap()访问)page->zone_device_data:驱动私有数据(PMEM 使用这里存坏块信息)
devm_memremap_pages()(mm/memremap.c)为持久内存完成以下工作:
1. 检查物理地址范围是否已被请求(devm_request_mem_region)
2. 调用 arch_add_memory() 将物理地址范围加入内核地址空间
3. 初始化 struct page 数组(设置 page->pgmap)
4. 通知 mm 子系统新增了 ZONE_DEVICE 内存
5. 返回内核虚拟地址
pfn_devs.c 第 16 行:
static const bool page_struct_override = IS_ENABLED(CONFIG_NVDIMM_KMSAN);当启用 KMSAN(Kernel Memory SANitizer)时,page_struct_override 为真,强制所有 PFN 模式使用 PFN_MODE_RAM(struct page 在 DRAM 中),以便 KMSAN 追踪持久内存的未初始化读取。
ndctl(NVDIMM Control)是 Intel 开发的用户空间工具,用于管理 NVDIMM 设备和 namespace。它通过以下内核接口与内核交互:
ndctl 工具
|
+---+-------------------------------------------+
| 接口类型 | 内核路径 |
+----------+--------------------------------------+
| sysfs | /sys/bus/nd/ |
| | /sys/class/nd/ |
| | /sys/block/pmem*/ |
+----------+--------------------------------------+
| ioctl | /dev/ndctl0 → IOCTL_ND_* |
| | /dev/nmem0 → DIMM 命令 |
+----------+--------------------------------------+
| char dev | /dev/dax0.0 → mmap(devdax) |
+----------+--------------------------------------+
内核为每条 nvdimm_bus 创建一个 ndctl 字符设备(bus.c 第 734 行):
int nvdimm_bus_create_ndctl(struct nvdimm_bus *nvdimm_bus)
{
dev_t devt = MKDEV(nvdimm_bus_major, nvdimm_bus->id);
struct device *dev;
...
dev->class = &nd_class;
dev->devt = devt;
rc = dev_set_name(dev, "ndctl%d", nvdimm_bus->id);
rc = device_add(dev);
...
}nvdimm_bus_major 在模块初始化时通过 register_chrdev() 动态分配(bus.c 第 1293 行):
rc = register_chrdev(0, "ndctl", &nvdimm_bus_fops);ndctl 工具通过 ioctl() 系统调用向内核发送命令。内核侧的处理路径(bus.c 第 1080~1180 行):
ioctl(/dev/ndctl0, ND_IOCTL_CALL, buf)
|
nd_ioctl() [bus.c]
|
nd_cmd_clear_to_send() [安全检查]
|
nd_desc->ndctl(nd_desc, nvdimm, cmd, buf, buf_len, &cmd_rc)
|
acpi_nfit_ctl() [drivers/acpi/nfit/core.c]
|
acpi_evaluate_dsm() [ACPI 核心:调用固件 _DSM 方法]
ND_IOCTL_MAX_BUFLEN = 65536 字节限制防止用户传入过大缓冲区(bus.c 第 1151 行)。
内核维护每条 DIMM 命令的输入/输出大小描述(bus.c 第 772 行),用于安全地从用户空间复制参数:
static const struct nd_cmd_desc __nd_cmd_dimm_descs[] = {
[ND_CMD_SMART] = {
.out_num = 2,
.out_sizes = { 4, 128, }, // 状态码 4B + 健康数据 128B
},
[ND_CMD_GET_CONFIG_SIZE] = {
.out_num = 3,
.out_sizes = { 4, 4, 4, }, // 状态码 + config_size + max_xfer
},
[ND_CMD_GET_CONFIG_DATA] = {
.in_num = 2,
.in_sizes = { 4, 4, }, // offset + length
.out_num = 2,
.out_sizes = { 4, UINT_MAX, }, // 状态码 + 数据(变长)
},
[ND_CMD_SET_CONFIG_DATA] = {
.in_num = 3,
.in_sizes = { 4, 4, UINT_MAX, }, // offset + length + 数据
.out_num = 1,
.out_sizes = { 4, }, // 状态码
},
...
};| ndctl 命令 | 触发的内核操作 | 关键代码路径 |
|---|---|---|
ndctl list |
读取 sysfs 属性 | /sys/bus/nd/devices/*/ |
ndctl create-namespace |
写 sysfs uuid、mode |
namespace_devs.c 中的 store 函数 |
ndctl destroy-namespace |
写 sysfs holder_class |
nd_namespace_label_update() |
ndctl check-labels |
ioctl ND_CMD_GET_CONFIG_DATA |
nvdimm_init_nsarea() |
ndctl update-firmware |
ioctl ND_CMD_CALL + DSM |
acpi_nfit_ctl() + Intel DSM |
ndctl start-scrub |
write /sys/.../scrub |
acpi_nfit_scrub() |
ndctl inject-error |
ioctl NVDIMM_INTEL_INJECT_ERROR |
acpi_nfit_ctl() |
namespace 的关键 sysfs 属性(/sys/bus/nd/devices/namespaceX.Y/):
uuid → namespace 的 UUID(读写,创建时设置)
size → namespace 的字节大小
mode → 当前模式(fsdax/devdax/sector/raw)
holder_class → 持有者类型(btt/pfn/dax 或空)
resource → 物理地址范围(起始地址:大小)
align → 对齐要求
name → 可选名称
force_raw → 强制 raw 模式(调试用)
numa_node → NUMA 节点
Intel Optane DIMM 支持加密(AES-256-XTS)和密码保护。内核通过 Linux Key Ring 机制管理 DIMM 密钥(drivers/nvdimm/security.c):
NVDIMM 安全状态
+-----------------------------------------------------------+
| Unsecured → (set_passphrase) → Locked |
| ↓ |
| (unlock_unit) |
| ↓ |
| Unlocked |
| ↓ ↓ |
| (freeze_lock) (secure_erase) |
| ↓ |
| Frozen(无法改密码) |
+-----------------------------------------------------------+
nvdimm_request_key()(security.c 第 50 行)通过内核 Key Ring 机制获取 DIMM 密钥:
static struct key *nvdimm_request_key(struct nvdimm *nvdimm)
{
char desc[NVDIMM_KEY_DESC_LEN + sizeof(NVDIMM_PREFIX)];
// 密钥描述符格式:"nvdimm:<dimm_id>"
sprintf(desc, "%s%s", NVDIMM_PREFIX, nvdimm->dimm_id);
key = request_key(&key_type_encrypted, desc, "");
// 如果用户空间没有提供密钥,request_key 会 upcall 到 key.d 服务
...
}密钥类型使用 key_type_encrypted(内核加密密钥),确保密钥材料不以明文形式出现在用户空间。
支持的安全 DSM 命令(来自 nfit.h 第 78 行 NVDIMM_INTEL_SECURITY_CMDMASK):
NVDIMM_INTEL_GET_SECURITY_STATE = 19 // 查询当前安全状态
NVDIMM_INTEL_SET_PASSPHRASE = 20 // 设置/更改密码
NVDIMM_INTEL_DISABLE_PASSPHRASE = 21 // 禁用密码(擦除加密)
NVDIMM_INTEL_UNLOCK_UNIT = 22 // 解锁(输入密码)
NVDIMM_INTEL_FREEZE_LOCK = 23 // 冻结(锁定配置)
NVDIMM_INTEL_SECURE_ERASE = 24 // 安全擦除(删除所有数据)
NVDIMM_INTEL_OVERWRITE = 25 // 覆写(用随机数填充)
NVDIMM_INTEL_SET_MASTER_PASSPHRASE = 27 // 设置主密码
NVDIMM_INTEL_MASTER_SECURE_ERASE = 28 // 主密码安全擦除
安全命令通过 NVDIMM_INTEL_DENY_CMDMASK(nfit.h 第 100 行)保护,只允许经过授权的进程(通常需要 CAP_SYS_ADMIN)执行。
在虚拟机中,持久内存通过 virtio-pmem 协议向客户机暴露。drivers/nvdimm/virtio_pmem.c 实现了虚拟持久内存的 flush 接口。
virtio_pmem_flush()(nd_virtio.c 第 38 行)通过 virtio 队列向宿主机发送 flush 请求:
static int virtio_pmem_flush(struct nd_region *nd_region)
{
struct virtio_device *vdev = nd_region->provider_data;
struct virtio_pmem *vpmem = vdev->priv;
guard(mutex)(&vpmem->flush_lock);
// 构造请求
req_data->req.type = cpu_to_le32(VIRTIO_PMEM_REQ_TYPE_FLUSH);
// 通过 virtio 队列发送,如果队列满则等待
while ((err = virtqueue_add_sgs(...)) == -ENOSPC) {
spin_unlock_irqrestore(&vpmem->pmem_lock, flags);
wait_event(req_buf->wq_buf, req_buf->wq_buf_avail);
...
}
virtqueue_kick(vpmem->req_vq);
// 等待宿主机 ACK
wait_event(req_data->host_acked, req_data->done);
...
}virtio_pmem_host_ack()(nd_virtio.c 第 13 行)是中断处理函数,在宿主机完成 flush 后唤醒等待的请求。
在 QEMU/KVM 环境中,宿主机可以通过两种方式向客户机提供持久内存:
- ACPI NVDIMM(传统方式):通过 NFIT 表,客户机使用标准的
drivers/acpi/nfit/驱动。 - virtio-pmem(新方式):通过 virtio 协议,提供更好的迁移支持和 flush 语义,客户机使用
drivers/nvdimm/virtio_pmem.c。
两种方式都最终注册为 nd_region,上层(文件系统、用户程序)感知不到区别。
CXL(Compute Express Link)是基于 PCIe 物理层的高速互连协议,版本 2.0 开始支持持久内存扩展。与 DDR 通道的 NVDIMM 不同,CXL 持久内存通过 PCIe 总线连接,支持更大容量和更灵活的拓扑。
CXL 内存类型:
- CXL.mem(Type 2/3):字节寻址的 CXL 内存设备,可以是易失的也可以是持久的
- CXL PMEM:持久类型的 CXL.mem 设备
drivers/cxl/pmem.c 实现了 CXL 持久内存设备到 libnvdimm 框架的适配层。cxl_nvdimm_probe()(pmem.c 第 136 行):
static int cxl_nvdimm_probe(struct device *dev)
{
struct cxl_nvdimm *cxl_nvd = to_cxl_nvdimm(dev);
struct cxl_memdev *cxlmd = cxl_nvd->cxlmd;
unsigned long flags = 0, cmd_mask = 0;
// 设置 labeling 能力(与 NFIT DIMM 相同)
set_bit(NDD_LABELING, &flags);
set_bit(NDD_REGISTER_SYNC, &flags);
// CXL 设备支持 label 相关命令
set_bit(ND_CMD_GET_CONFIG_SIZE, &cmd_mask);
set_bit(ND_CMD_GET_CONFIG_DATA, &cmd_mask);
set_bit(ND_CMD_SET_CONFIG_DATA, &cmd_mask);
// 注册到 libnvdimm 框架
nvdimm = __nvdimm_create(cxl_nvb->nvdimm_bus, cxl_nvd,
cxl_dimm_attribute_groups, flags,
cmd_mask, 0, NULL, cxl_nvd->dev_id,
cxl_security_ops, NULL);
...
}CXL 设备使用 CXL 2.0 规范定义的 label 格式(label.h 中的 cxl_region_label 和 nvdimm_cxl_label),与传统 EFI label 格式不同。
CXL Region Label(label.h 第 85 行):
struct cxl_region_label {
u8 type[NSLABEL_UUID_LEN]; // UUID: CXL_REGION_UUID
u8 uuid[NSLABEL_UUID_LEN]; // 区域 UUID
__le32 flags;
__le16 nlabel; // 交错路数
__le16 position; // 本设备在交错中的位置
__le64 dpa; // 设备本地物理地址起始
__le64 rawsize; // 贡献的字节数
__le64 hpa; // 强制要求的系统物理地址(HPA)
__le32 slot; // label 槽号
__le32 ig; // 交错粒度:(1 << ig) * 256 字节
__le32 align; // 256MB 对齐
__le64 checksum;
};与 NFIT DIMM 通过 ACPI DSM 访问 LSA 不同,CXL 设备通过 mailbox 命令(CXL_MBOX_OP_GET_LSA / CXL_MBOX_OP_SET_LSA)读写 LSA(drivers/cxl/pmem.c 第 205~278 行):
static int cxl_pmem_get_config_data(struct cxl_memdev_state *mds,
struct nd_cmd_get_config_data_hdr *cmd, unsigned int buf_len)
{
struct cxl_mbox_get_lsa get_lsa = {
.offset = cpu_to_le32(cmd->in_offset),
.length = cpu_to_le32(cmd->in_length),
};
mbox_cmd = (struct cxl_mbox_cmd) {
.opcode = CXL_MBOX_OP_GET_LSA, // Get Label Storage Area
.payload_in = &get_lsa,
.size_out = cmd->in_length,
.payload_out = cmd->out_buf,
};
return cxl_internal_send_cmd(cxl_mbox, &mbox_cmd);
}在一个系统中,NVDIMM(通过 DDR 通道)和 CXL 持久内存可以共存。它们各自注册独立的 nvdimm_bus,但共享上层的 libnvdimm、DAX 框架和文件系统接口:
系统中多种持久内存共存
+----------------------------------+
| ndbus0 (NFIT/DDR) |
| nmem0, nmem1, region0 |
+----------------------------------+
| ndbus1 (CXL PCIe) |
| nmem2, region1 |
+----------------------------------+
|
+----------------------------------+
| libnvdimm 核心(共享) |
| DAX 框架(共享) |
| ext4/XFS with DAX(共享) |
+----------------------------------+
ND_REGION_CXL(include/linux/libnvdimm.h 中的 region 标志位)标记该区域来自 CXL 子系统,某些操作(如 ARS)对 CXL 设备有不同的处理路径。
CXL 设备引入了"脏关机计数"(dirty_shutdown_count)机制,记录设备在未正确持久化的情况下断电的次数(pmem.c 第 59~66 行):
static ssize_t dirty_shutdown_show(struct device *dev, ...)
{
struct cxl_nvdimm *cxl_nvd = nvdimm_provider_data(nvdimm);
return sysfs_emit(buf, "%llu\n", cxl_nvd->dirty_shutdowns);
}cxl_nvdimm_arm_dirty_shutdown_tracking() 在 probe 时通过 GPF(Global Persistent Flush)DVSEC 和 mailbox 命令设置追踪(pmem.c 第 104 行)。
BTT 驱动在 /sys/kernel/debug/btt/ 下暴露每个 Arena 的详细状态(btt.c 第 218~255 行):
/sys/kernel/debug/btt/
└── btt0.0/
├── arena0/
│ ├── size → Arena 总字节数
│ ├── external_lba_start → 起始 LBA
│ ├── internal_nlba → 内部块数
│ ├── external_nlba → 对外块数
│ ├── nfree → 自由块数
│ ├── dataoff/mapoff/logoff → 各区域偏移
│ ├── flags → Arena 状态标志
│ └── log_index_0/1 → log 条目索引
└── arena1/
└── ...
// btt.c 第 228 行
static void arena_debugfs_init(struct arena_info *a, struct dentry *parent, int idx)
{
debugfs_create_x64("size", S_IRUGO, d, &a->size);
debugfs_create_x32("internal_nlba", S_IRUGO, d, &a->internal_nlba);
debugfs_create_x32("nfree", S_IRUGO, d, &a->nfree);
debugfs_create_x64("dataoff", S_IRUGO, d, &a->dataoff);
debugfs_create_x64("mapoff", S_IRUGO, d, &a->mapoff);
debugfs_create_x64("logoff", S_IRUGO, d, &a->logoff);
debugfs_create_u32("log_index_0", S_IRUGO, d, &a->log_index[0]);
debugfs_create_u32("log_index_1", S_IRUGO, d, &a->log_index[1]);
}drivers/nvdimm/nd_perf.c 实现了 NVDIMM 性能监控单元(PMU)的 perf 接口,允许通过 perf stat 查看 DIMM 级别的访问统计:
perf stat -e nvdimm0/media_reads/ ./workload
支持的性能事件(取决于 DIMM 固件):
media_reads/media_writes:介质读写字节数read_64b_ops/write_64b_ops:64 字节操作计数cpu_read_ops/cpu_write_ops:CPU 侧操作计数
关键的 sysfs 监控节点:
/sys/block/pmem0/
├── stat → I/O 统计(延迟、次数、字节数)
├── queue/
│ ├── logical_block_size → 逻辑块大小
│ ├── physical_block_size → 物理块大小
│ ├── dax → 是否支持 DAX(1/0)
│ └── write_cache → 写缓存状态
└── dax/
└── write_cache → DAX 写缓存是否需要 CLWB
/sys/bus/nd/devices/nmem0/
├── available_slots → 可用 label 槽数
├── dirty_shutdown → 脏关机计数(Intel 特有)
├── flags → DIMM 状态标志
│ ├── save_fail → 保存失败
│ ├── restore_fail → 恢复失败
│ ├── flush_fail → 刷新失败
│ └── not_armed → 未武装(ADR 未就绪)
└── health/
├── alarm_temp → 温度告警
└── life_used → 使用寿命百分比
当 CPU 在读取持久内存时遇到不可纠正的错误(UCE),会产生 MCE。drivers/acpi/nfit/mce.c 实现了 NFIT MCE 处理器:
// drivers/acpi/nfit/mce.c
static int nfit_handle_mce(struct notifier_block *nb, unsigned long val,
void *data)
{
struct mce *mce = data;
...
// 如果错误地址在某个 SPA Range 内,记录为坏块
nfit_mem_find_spa(acpi_desc, mce->addr);
// 设置 ARS_REQ_SHORT,触发下次扫描时重新检测该区域
set_bit(ARS_REQ_SHORT, &nfit_spa->ars_state);
...
}MCE 处理程序通过 mce_register_decode_chain() 注册,优先级高于通用 MCE 处理器,确保 NVDIMM 错误被正确关联到持久内存地址范围。
BIOS 可以通过 ACPI 通知(NFIT_NOTIFY_UPDATE = 0x80)通知内核 NFIT 表发生变化(如热插 DIMM)。内核的处理路径:
// nfit.h 第 148 行
enum nfit_root_notifiers {
NFIT_NOTIFY_UPDATE = 0x80, // NFIT 表更新(热插 DIMM)
NFIT_NOTIFY_UC_MEMORY_ERROR = 0x81, // 不可纠正内存错误
};NFIT_NOTIFY_UC_MEMORY_ERROR 通知触发 ARS 短扫描,快速定位新发现的坏块。
Intel Optane DIMM 支持在系统运行时激活新固件(Firmware Activate,FWA)。内核实现了一套完整的固件激活框架(nfit.h 中的 nvdimm_fwa_* 枚举)。
FWA 状态
+----------------------------------------------------------------+
| idle → (arm) → armed → (activate) → activating |
| ↑ | |
| +-------------- (complete) --------------------+ |
| ↑ |
| +-------------- (error) → error_busy |
+----------------------------------------------------------------+
enum nvdimm_fwa_state(include/linux/libnvdimm.h):
NVDIMM_FWA_INVALID:无效状态NVDIMM_FWA_IDLE:空闲(可以启动固件激活)NVDIMM_FWA_ARMED:已武装(发送了 ARM 命令,等待激活)NVDIMM_FWA_ARM_OVERFLOW:ARM 超时(需要重新武装)NVDIMM_FWA_BUSY:激活中
通过 ndctl 完成固件激活:
1. ndctl update-firmware --force → 上传固件(NVDIMM_INTEL_SEND_FWUPDATE)
2. ndctl activate-firmware arm → 武装(NVDIMM_INTEL_FW_ACTIVATE_ARM)
3. ndctl activate-firmware activate → 激活(NVDIMM_BUS_INTEL_FW_ACTIVATE)
激活过程中,系统需要停机(quiesce)或在线激活(取决于固件支持)。fwa_nosuspend 标志(acpi_nfit_desc 第 268 行)控制是否允许在不挂起系统的情况下激活。
NVDIMM 子系统的关键编译配置(drivers/nvdimm/Kconfig):
CONFIG_LIBNVDIMM 必选:libnvdimm 核心框架
CONFIG_BLK_DEV_PMEM PMEM 块设备驱动(/dev/pmemN)
CONFIG_ND_BTT BTT 原子写支持
CONFIG_ND_PFN PFN/fsdax 支持(ZONE_DEVICE)
CONFIG_NVDIMM_PFN 同上(别名)
CONFIG_NVDIMM_DAX devdax 支持(/dev/daxN.M)
CONFIG_ACPI_NFIT ACPI NFIT 驱动(需要 CONFIG_ACPI)
CONFIG_OF_PMEM 设备树方式描述的 PMEM
CONFIG_VIRTIO_PMEM virtio-pmem 驱动
CONFIG_NVDIMM_SECURITY NVDIMM 加密和密码保护
CONFIG_FS_DAX 文件系统 DAX 支持(ext4/XFS 需要)
CONFIG_FS_DAX_PMD 2MB PMD 级 DAX 映射支持
CONFIG_DEV_DAX device DAX 支持
CONFIG_DEV_DAX_PMEM device DAX pmem 设备
CONFIG_NVDIMM_KMSAN KMSAN 内存错误检测(调试用)
CONFIG_ARCH_HAS_PMEM_API 平台支持 PMEM 持久性操作
(CLWB/CLFLUSHOPT + SFENCE)
CONFIG_ARCH_HAS_UACCESS_FLUSHCACHE
平台支持带刷缓存的用户空间复制
CONFIG_X86_PMEM_LEGACY 支持 e820 报告的 PMEM 区域(无 NFIT)
CONFIG_ZONE_DEVICE ZONE_DEVICE 内存支持(struct page for PMEM)
CONFIG_MEMORY_HOTPLUG 内存热插拔支持(NVDIMM 热插需要)
# 完整 NVDIMM 支持(生产环境推荐)
CONFIG_LIBNVDIMM=y
CONFIG_BLK_DEV_PMEM=m
CONFIG_ND_BTT=m
CONFIG_ND_PFN=m
CONFIG_NVDIMM_DAX=m
CONFIG_ACPI_NFIT=m
CONFIG_FS_DAX=y
CONFIG_FS_DAX_PMD=y
CONFIG_ZONE_DEVICE=y
CONFIG_DEV_DAX=m
CONFIG_DEV_DAX_PMEM=m
CONFIG_NVDIMM_SECURITY=y # Intel 加密支持
| 文件路径 | 内容描述 |
|---|---|
drivers/nvdimm/pmem.c |
PMEM 块设备驱动,bio 处理,DAX 操作集 |
drivers/nvdimm/pmem.h |
pmem_device 结构体定义 |
drivers/nvdimm/nd.h |
nd_region、nd_btt、nd_pfn、nd_mapping 等核心结构 |
drivers/nvdimm/namespace_devs.c |
namespace 设备管理,label 解析 |
drivers/nvdimm/btt.c |
BTT 实现(arena 发现、原子写协议) |
drivers/nvdimm/btt.h |
BTT 数据结构(btt_sb、arena_info、log_entry) |
drivers/nvdimm/label.c |
namespace label 读写操作 |
drivers/nvdimm/label.h |
label 数据结构(EFI/CXL 格式、nd_namespace_index) |
drivers/nvdimm/pfn.h |
PFN 超级块格式(nd_pfn_sb) |
drivers/nvdimm/pfn_devs.c |
PFN/DAX 设备管理,nd_pfn_mode |
drivers/nvdimm/region.c |
nd_region_probe,namespace 注册 |
drivers/nvdimm/bus.c |
nvdimm_bus 管理,ioctl 路由,ndctl 字符设备 |
drivers/nvdimm/security.c |
NVDIMM 加密/密码功能,Key Ring 集成 |
drivers/nvdimm/nd_perf.c |
NVDIMM PMU perf 接口 |
drivers/nvdimm/virtio_pmem.c |
virtio-pmem flush 接口 |
drivers/nvdimm/nd_virtio.c |
virtio-pmem nd_region flush 回调 |
drivers/dax/super.c |
DAX 框架(dax_device、dax_direct_access、dax_flush) |
drivers/dax/device.c |
devdax 字符设备(/dev/daxN.M) |
fs/dax.c |
DAX 文件操作(缺页处理、写回、IO 路径) |
drivers/acpi/nfit/core.c |
NFIT 表解析,DSM 命令,ARS |
drivers/acpi/nfit/nfit.h |
NFIT 数据结构(nfit_spa、nfit_mem、acpi_nfit_desc) |
drivers/acpi/nfit/mce.c |
NFIT MCE(机器检查异常)处理 |
drivers/acpi/nfit/intel.c |
Intel 特有 DSM 命令实现 |
drivers/cxl/pmem.c |
CXL 持久内存到 libnvdimm 的适配层 |
include/linux/libnvdimm.h |
libnvdimm 公共接口,region 标志位,bus descriptor |
include/linux/nd.h |
namespace、nd_region 的公共头文件 |
include/uapi/linux/ndctl.h |
ndctl 用户空间接口(ioctl、命令号、结构体) |
arch/x86/lib/usercopy_64.c |
arch_wb_cache_pmem()、clean_cache_range()(CLWB 循环) |
arch/x86/include/asm/special_insns.h |
clwb()、clflushopt() 汇编封装 |
mm/memremap.c |
devm_memremap_pages(),ZONE_DEVICE 初始化 |
| 常量 | 值 | 定义位置 | 说明 |
|---|---|---|---|
ND_MAX_LANES |
256 | nd.h:23 |
最大并发 lane 数(= BTT nfree) |
INT_LBASIZE_ALIGNMENT |
64 | nd.h:24 |
BTT 内部块大小对齐(字节) |
NVDIMM_IO_ATOMIC |
1 | nd.h:25 |
标志:要求原子 I/O |
ARENA_MIN_SIZE |
16MB | btt.h:23 |
Arena 最小大小 |
ARENA_MAX_SIZE |
512GB | btt.h:24 |
Arena 最大大小 |
BTT_PG_SIZE |
4096 | btt.h:27 |
BTT 信息块大小 |
BTT_DEFAULT_NFREE |
256 | btt.h:28 |
默认自由块数(= ND_MAX_LANES) |
MAP_ENT_SIZE |
4 | btt.h:14 |
Map 条目大小(字节) |
MAP_ENT_NORMAL |
0xC0000000 |
btt.h:20 |
正常 map 条目标志(E=1, Z=1) |
NVDIMM_CMD_MAX |
31 | nfit.h:37 |
最大 DSM 命令数 |
ND_IOCTL_MAX_BUFLEN |
65536 | include/uapi/linux/ndctl.h |
ioctl 最大缓冲长度 |
NSLABEL_NAME_LEN |
64 | label.h:18 |
namespace label 名称长度 |
NSLABEL_UUID_LEN |
16 | label.h:17 |
UUID 长度 |
NSINDEX_ALIGN |
256 | label.h:15 |
Index Block 对齐(字节) |
PFN_SIG_LEN |
16 | pfn.h:12 |
PFN 签名长度 |
NFIT_ARS_TIMEOUT |
90 | nfit.h:145 |
ARS 超时(秒) |
各类存储技术延迟比较(典型值)
+------------------------------------------------------------------+
| 技术 | 读延迟 | 写延迟 | 接口 | 字节寻址 |
+----------------+-----------+-----------+-----------+--------+
| DRAM | ~60 ns | ~60 ns | DDR4 | 是 |
| Optane DIMM | ~300 ns | ~100 ns | DDR4 | 是 |
| Optane SSD | ~10 μs | ~10 μs | PCIe NVMe | 否 |
| NVMe SSD(3D NAND)| ~100 μs | ~20 μs | PCIe NVMe | 否 |
| SATA SSD | ~100 μs | ~50 μs | SATA | 否 |
| HDD | ~5 ms | ~5 ms | SATA/SAS | 否 |
| CXL PMEM | ~200 ns | ~150 ns | PCIe CXL | 是 |
+----------------+-----------+-----------+-----------+--------+
访问路径延迟(软件开销)
+------------------------------------------------------------------+
| 路径 | 延迟 | 绕过 page cache |
+----------------+------------+----------------+
| 直接 load/store | 0 | 是 |
| mmap DAX | ~1 μs (首次) | 是 |
| DAX write() | ~2-5 μs | 是 |
| 块 I/O (bio) | ~5-10 μs | 否 |
| BTT 块 I/O | ~10-20 μs | 否 |
+----------------+------------+----------------+
由 Claude Code 分析生成