Skip to content

Latest commit

 

History

History
2768 lines (2194 loc) · 103 KB

File metadata and controls

2768 lines (2194 loc) · 103 KB

Linux 内核 NVDIMM / 持久内存(Persistent Memory)子系统深度分析

基于 Linux 内核源码(master 分支,commit 8a30aeb0d)撰写。 主要分析文件:drivers/nvdimm/drivers/acpi/nfit/drivers/dax/fs/dax.c


目录

  1. 背景与硬件模型
  2. ACPI NFIT 表解析
  3. 软件分层架构
  4. 核心数据结构详解
  5. Namespace 与操作模式
  6. PMEM 块设备驱动
  7. DAX 框架
  8. mmap + DAX:缺页故障到直接映射
  9. BTT:Block Translation Table
  10. 持久内存写顺序与缓存冲刷
  11. NUMA 拓扑与 SPA Range
  12. 坏块(Bad Blocks)与错误恢复
  13. 初始化流程全景
  14. 关键路径性能分析
  15. Namespace Label 存储格式
  16. PFN 超级块与 ZONE_DEVICE 页面
  17. ndctl 工具与内核接口
  18. NVDIMM 安全功能
  19. virtio-pmem:虚拟化场景下的持久内存
  20. CXL 与持久内存的关系
  21. 持久内存的调试与可观测性
  22. 固件激活(Firmware Activate)机制
  23. 内核配置与编译选项
  24. 附录:关键源码文件索引

1. 背景与硬件模型

NVDIMM(Non-Volatile Dual Inline Memory Module)是一类插在 DDR 插槽上、掉电后数据仍然保持的存储设备。与传统 SSD 相比,它可以被 CPU 以字节粒度直接寻址,延迟在百纳秒量级。Intel 的 Optane DIMM(Apache Pass)是最具代表性的商用产品。

1.1 物理连接方式

  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 缓存冲刷到内存控制器。

1.2 NVDIMM 类型分类

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)。

1.3 App Direct 模式 vs Memory 模式

Intel Optane DIMM 可配置为两种主要模式:

  • App Direct 模式(持久内存模式):操作系统以持久内存方式管理,对应内核的 PMEM 路径。BIOS 通过 NFIT 表将地址空间描述给操作系统。
  • Memory 模式(易失内存模式):DIMM 作为普通 DRAM 的二级缓存(DRAM 为一级),操作系统无感知,不进入 NVDIMM 子系统。

两种模式的混合配置也被支持,系统可以将部分 DIMM 容量用于 App Direct,部分用于 Memory 模式。


2. ACPI NFIT 表解析

2.1 NFIT 概述

NFIT(NVDIMM Firmware Interface Table)是 ACPI 规范定义的一张固件表,描述系统中所有 NVDIMM 的物理地址范围、DIMM 信息、交错集以及刷新地址。内核通过 drivers/acpi/nfit/core.c 解析这张表。

2.2 NFIT 子表类型

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 链表。

2.3 acpi_nfit_desc:顶层描述符

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 秒)
    ...
};

2.4 SPA UUID 类型

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,       // 持久 CD

NFIT_SPA_PM 对应的 SPA Range 就是软件可以直接访问的持久内存地址空间,最终会被注册为 nd_region

2.5 DSM(Device Specific Method)控制

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.hnvdimm_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 = 31nfit.h 第 37 行)。

2.6 nfit_mem:每个 DIMM 的汇聚视图

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 自动识别。

2.7 NFIT 表解析流程

  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

3. 软件分层架构

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 方法                           |
  +------------------------------------------------------+

3.1 libnvdimm 总线模型

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_descriptorinclude/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 *);
};

4. 核心数据结构详解

4.1 nd_region

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 子系统创建。

4.2 nd_mapping:DIMM 与 nd_region 的映射关系

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

4.3 nd_namespace_pmem

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) 判断)。

4.4 nd_btt

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;
};

4.5 nd_pfn / nd_dax

用于管理 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,但占用持久内存空间)。

4.6 pmem_device

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 页面管理
};

5. Namespace 与操作模式

一个 nd_region 可以划分为若干 namespace,每个 namespace 支持以下操作模式:

  nd_region(物理 NVDIMM 区域)
  +--------------------------------------------------+
  |  namespace0 (pmem0)    | namespace1 (pmem0.1)    |
  |  +------------------+  | +------------------+    |
  |  | 操作模式:         |  | | 操作模式:         |    |
  |  | fsdax / devdax   |  | | sector / raw      |    |
  |  | / sector / raw   |  | |                  |    |
  |  +------------------+  | +------------------+    |
  +--------------------------------------------------+

5.1 fsdax 模式(文件系统 DAX)

文件系统(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,允许文件系统利用反向映射进行内存错误处理。

5.2 devdax 模式(设备 DAX)

暴露为字符设备 /dev/daxN.M,用户程序通过 mmap() 直接映射持久内存。不经过文件系统和页缓存。适合数据库等需要自行管理存储布局的程序(如 PMDK 库的场景)。

nd_dax 结构(复用 nd_pfn)通过 drivers/dax/device.c 实现字符设备接口。

5.3 sector 模式(块设备仿真)

使用 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";

5.4 raw 模式

不加任何翻译层,直接暴露原始的持久内存区域,一般用于调试或格式化操作。nd_namespace_common.force_raw 标志强制使用此模式(namespace_devs.c 第 105 行)。

5.5 sector_size 的决策

pmem_sector_size() 函数(namespace_devs.c 第 118 行)根据 namespace label 中的 lbasize 字段决定扇区大小:512 字节(默认)或 4096 字节。

5.6 abstraction_guid:模式标记

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())。


6. PMEM 块设备驱动

drivers/nvdimm/pmem.c 实现了 PMEM 块设备驱动,是用户态 I/O 请求进入持久内存的最后一公里。

6.1 地址转换辅助函数

驱动提供了一组地址转换函数(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 数组的元数据)。

6.2 写路径:write_pmem 与 memcpy_flushcache

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 缓存,保证持久性且不污染缓存。

6.3 读路径:copy_mc_to_kernel

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;
}

6.4 pmem_submit_bio:bio 处理入口

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()。这与传统磁盘驱动的异步模型有本质区别。

6.5 pmem_dax_ops:DAX 操作集

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,供上层直接操作。

6.6 __pmem_direct_access:坏块感知的 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/)提供强替换版本进行单元测试。

6.7 设备初始化:pmem_attach_disk

pmem_attach_disk() 函数(pmem.c 第 448 行)完成块设备的完整初始化,关键步骤:

  1. 分配 pmem_device 结构体。
  2. 根据是否有 PFN 超级块,选择 devm_memremap_pages()devm_memremap() 映射持久内存。
  3. 分配块设备 gendisk,设置容量。
  4. 初始化坏块表(nvdimm_badblocks_populate())。
  5. 创建 dax_devicealloc_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:需要 CLWB

6.8 nd_pmem_probe:驱动探测入口

nd_pmem_probe()pmem.c 第 596 行)是 nd_pmem_driverprobe 回调,整个探测逻辑:

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 的约定:表示"探测成功但需要改变设备类型重新绑定"。

6.9 nd_pmem_notify:热插拔事件处理

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() 更新坏块表。


7. DAX 框架

7.1 dax_device 结构

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。

7.2 dax_operations 接口

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 *);
};

7.3 dax_direct_access:翻译 pgoff 到 PFN

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。

7.4 dax_flush:缓存写回

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 循环
}

7.5 SRCU 保护

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);
}

7.6 dax_hosts:块设备到 DAX 设备的查找表

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;
}

7.7 dax_access_mode:访问模式控制

dax_access_mode 枚举(include/linux/dax.h)控制 direct_access() 的行为:

enum dax_access_mode {
    DAX_ACCESS,         // 普通读写访问
    DAX_RECOVERY_WRITE, // 恢复写模式(处理含毒页)
};

DAX_RECOVERY_WRITE 模式下,驱动必须在返回 PFN 之前先清除介质毒素,否则写操作可能失败。


8. mmap + DAX:缺页故障到直接映射

这是 NVDIMM 最核心的用户态访问路径。应用程序 mmap() 一个挂载在 DAX 文件系统上的文件后,首次访问会触发缺页故障,内核在故障处理中直接把持久内存的物理页映射进进程页表,后续访问不再经过内核

8.1 整体调用链

  用户程序访问 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()
        |
  进程页表直接指向持久内存物理页

8.2 DAX 条目管理(XArray)

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())。

8.3 PTE 故障处理

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);

8.4 PMD 故障:2MB 大页直接映射

当满足条件(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 映射的正确性。

8.5 MAP_SYNC 语义

当 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,推迟页表项的安装,强制文件系统先完成元数据的持久化。

8.6 fsync 的 DAX 写回路径

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 缓存中的脏数据刷写到持久内存。

8.7 写时复制(COW)处理

DAX 模式下写时复制需要特殊处理。当发生 COW 写故障时(私有 mmap 写),dax_iomap_pte_fault() 检测到 IOMAP_F_SHARED 标志,会调用 dax_iomap_copy_around() 将数据复制到新页面,而不能直接映射到共享的持久内存页。这是 DAX 与普通文件页缓存的重要区别之一。


9. BTT:Block Translation Table

BTT 为持久内存提供原子写语义,解决"撕裂写"(torn write)问题——在没有 BTT 的情况下,掉电可能导致一个扇区只有部分数据被写入。

9.1 BTT 的必要性

持久内存的写操作虽然快速,但并非原子的。假设写入一个 512 字节的扇区需要分多个缓存行写入,如果中途掉电,该扇区数据将处于不一致状态。BTT 通过写时复制预写日志机制解决这个问题。

9.2 Arena 布局

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;

9.3 btt_sb 超级块

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 行),这是为了确保每个数据块的读写对缓存行友好。

9.4 arena_info:运行时 Arena 状态

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 中两个有效条目的索引
};

9.5 Map 条目格式

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);
}

9.6 Log 条目与原子写协议

每个并发写"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_mapnew_map 与实际 map 的一致性来判断写操作是否完成,必要时回滚。

9.7 自由块池(freelist)与 RTT

Arena 维护一个 freelist(每个 lane 一个 free_entry)和一个 RTT(Read Tracking Table,u32 *rttbtt.h 第 188 行)。RTT 用于防止一个 lane 在另一个 lane 正在写同一自由块时发生读后写覆盖。

并发控制由 map_locks(每 lane 一个 aligned_lockbtt.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;   // 该块是否有错误标记
};

9.8 log_index 兼容性

内核 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] 存储检测到的两个有效条目索引。

9.9 BTT 初始化状态机

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 时触发懒初始化,避免在探测阶段浪费时间。


10. 持久内存写顺序与缓存冲刷

10.1 x86 CLWB 指令实现

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));
}

10.2 三种缓存冲刷指令对比

指令 功能 缓存行状态 性能
CLFLUSH 写回并驱逐 变为 Invalid 最慢(驱逐后再访问需重新加载)
CLFLUSHOPT 写回并驱逐(弱排序) 变为 Invalid 中等(可并行多条)
CLWB 写回但保留(弱排序) 保持 Clean 最快(缓存行仍可用)

NVDIMM 优先使用 CLWB,因为它不会驱逐缓存行,避免后续读取的 cache miss。

10.3 NT Store 指令

除了 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 保证写入顺序。

10.4 写顺序保证

持久内存写的完整顺序(以无 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 指令。

10.5 写缓存控制决策

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

10.6 nvdimm_flush:刷新提示地址写入

部分 NVDIMM 硬件支持通过写特定的物理地址来触发 DIMM 内部的刷新操作,这些地址称为"Flush Hint Address"(在 NFIT 的 Flush Hint Address 子表中记录)。nvdimm_flush() 函数(core.c)在 REQ_FUAREQ_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();  // 确保写入可见

11. NUMA 拓扑与 SPA Range

11.1 NUMA 节点关联

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 节点分配资源

11.2 nd_region 与 SPA 的对应关系

每个 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_basespa_length 字段:

// nd_region 中记录的物理地址信息
u64 ndr_size;   // = spa_length
u64 ndr_start;  // = spa_base

11.3 交错集(Interleave Set)

多 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())。

11.4 ARS(Address Range Scrub)

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.bbbadblocks 结构),后续 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 状态
};

11.5 nd_region 的 lane 并发控制

为了最大化并发 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。


12. 坏块(Bad Blocks)与错误恢复

12.1 坏块检测

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() 进行区间查找。

12.2 写时清毒(Poison Clear on Write)

写路径中,如果目标扇区有坏块记录,驱动先尝试清除介质毒素(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 使受影响的缓存行无效,确保后续读取不会从缓存返回旧的(含毒的)数据。

12.3 DAX 路径的毒素恢复写

DAX 路径下(pmem_recovery_write()pmem.c 第 325 行),当检测到坏块时,驱动:

  1. 先清除介质毒素(__pmem_clear_poison())。
  2. 清除页面的 HWPoison 标志(pmem_mkpage_present(),第 63 行)。
  3. 重新执行写操作(_copy_from_iter_flushcache())。
  4. 清除坏块记录(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 标记
    }
}

12.4 内存错误通知

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 为损坏,防止后续访问。

12.5 坏块的 sysfs 接口

每个 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)。


13. 初始化流程全景

13.1 从 ACPI 表到 nd_region

  系统启动
      |
  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

13.2 从 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

13.3 nd_region_probe 详解

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 写入种子设备的 uuidmode 属性时,内核会在 region 下创建对应类型的新 namespace/BTT/PFN/DAX 设备。

13.4 namespace label 的读取与验证

nvdimm_drvdatand.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 行),统一处理这两种格式的字段访问。


14. 关键路径性能分析

14.1 DAX 读写路径延迟分解

以 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 的十到百微秒)。

14.2 mmap + DAX 首次访问

  首次访问 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 频率

14.3 BTT 写路径开销

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 的读写),但换来了原子写语义。

14.4 并发设计:per-CPU lane

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 竞争,实现高并发写入。

14.5 队列深度与 IO 统计

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 统计(延迟分布、读写次数、字节数等)。


15. Namespace Label 存储格式

15.1 Label 存储区域(LSA)布局

每个 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   |
  +------------------+

15.2 nd_namespace_index:Label 目录

nd_namespace_indexlabel.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 交替
}

15.3 nvdimm_efi_label:EFI 格式 label

传统 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 校验和
};

15.4 nvdimm_cxl_label:CXL 格式 label

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);
}

15.5 Label 校验:Fletcher-64

Label 使用 Fletcher-64 算法进行完整性校验。nd_fletcher64() 计算校验和,nsl_validate_checksum() 验证 label 的完整性。Fletcher-64 相比 CRC 计算更快,且对单比特错误有良好的检测能力。


16. PFN 超级块与 ZONE_DEVICE 页面

16.1 nd_pfn_sb:PFN 超级块格式

当 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,但不进入文件系统)

16.2 ZONE_DEVICE 页面初始化

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 使用这里存坏块信息)

16.3 devm_memremap_pages 的工作原理

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. 返回内核虚拟地址

16.4 KMSAN 支持:page_struct_override

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 追踪持久内存的未初始化读取。


17. ndctl 工具与内核接口

17.1 ndctl 工具概述

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)         |
  +----------+--------------------------------------+

17.2 ndctl 字符设备

内核为每条 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);

17.3 IOCTL 命令路由

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 行)。

17.4 nd_cmd_dimm_descs:DIMM 命令描述符

内核维护每条 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, },             // 状态码
    },
    ...
};

17.5 常用 ndctl 操作对应的内核路径

ndctl 命令 触发的内核操作 关键代码路径
ndctl list 读取 sysfs 属性 /sys/bus/nd/devices/*/
ndctl create-namespace 写 sysfs uuidmode 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()

17.6 sysfs 属性举例

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 节点

18. NVDIMM 安全功能

18.1 安全状态机

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(无法改密码)              |
  +-----------------------------------------------------------+

18.2 密钥管理

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(内核加密密钥),确保密钥材料不以明文形式出现在用户空间。

18.3 安全命令

支持的安全 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_CMDMASKnfit.h 第 100 行)保护,只允许经过授权的进程(通常需要 CAP_SYS_ADMIN)执行。


19. virtio-pmem:虚拟化场景下的持久内存

19.1 virtio-pmem 概述

在虚拟机中,持久内存通过 virtio-pmem 协议向客户机暴露。drivers/nvdimm/virtio_pmem.c 实现了虚拟持久内存的 flush 接口。

19.2 virtio_pmem_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 后唤醒等待的请求。

19.3 virtio-pmem 与 ACPI NFIT 的关系

在 QEMU/KVM 环境中,宿主机可以通过两种方式向客户机提供持久内存:

  1. ACPI NVDIMM(传统方式):通过 NFIT 表,客户机使用标准的 drivers/acpi/nfit/ 驱动。
  2. virtio-pmem(新方式):通过 virtio 协议,提供更好的迁移支持和 flush 语义,客户机使用 drivers/nvdimm/virtio_pmem.c

两种方式都最终注册为 nd_region,上层(文件系统、用户程序)感知不到区别。


20. CXL 与持久内存的关系

20.1 CXL 协议概述

CXL(Compute Express Link)是基于 PCIe 物理层的高速互连协议,版本 2.0 开始支持持久内存扩展。与 DDR 通道的 NVDIMM 不同,CXL 持久内存通过 PCIe 总线连接,支持更大容量和更灵活的拓扑。

CXL 内存类型:

  • CXL.mem(Type 2/3):字节寻址的 CXL 内存设备,可以是易失的也可以是持久的
  • CXL PMEM:持久类型的 CXL.mem 设备

20.2 cxl_nvdimm:CXL 设备在 libnvdimm 框架中的表示

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);
    ...
}

20.3 CXL Label 格式

CXL 设备使用 CXL 2.0 规范定义的 label 格式(label.h 中的 cxl_region_labelnvdimm_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;
};

20.4 CXL LSA 访问:Mailbox 命令

与 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);
}

20.5 CXL 与 ACPI NFIT 的共存

在一个系统中,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_CXLinclude/linux/libnvdimm.h 中的 region 标志位)标记该区域来自 CXL 子系统,某些操作(如 ARS)对 CXL 设备有不同的处理路径。

20.6 CXL PMEM 的 dirty_shutdown 追踪

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 行)。


21. 持久内存的调试与可观测性

21.1 debugfs 接口

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]);
}

21.2 nd_perf:性能计数器

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 侧操作计数

21.3 sysfs 监控接口

关键的 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     → 使用寿命百分比

21.4 MCE(Machine Check Exception)处理

当 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 错误被正确关联到持久内存地址范围。

21.5 NFIT 更新通知

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 短扫描,快速定位新发现的坏块。


22. 固件激活(Firmware Activate)机制

22.1 概述

Intel Optane DIMM 支持在系统运行时激活新固件(Firmware Activate,FWA)。内核实现了一套完整的固件激活框架(nfit.h 中的 nvdimm_fwa_* 枚举)。

22.2 固件激活状态机

  FWA 状态
  +----------------------------------------------------------------+
  |  idle  →  (arm)  →  armed  →  (activate)  →  activating       |
  |    ↑                                              |             |
  |    +-------------- (complete) --------------------+             |
  |    ↑                                                           |
  |    +-------------- (error) → error_busy                        |
  +----------------------------------------------------------------+

enum nvdimm_fwa_stateinclude/linux/libnvdimm.h):

  • NVDIMM_FWA_INVALID:无效状态
  • NVDIMM_FWA_IDLE:空闲(可以启动固件激活)
  • NVDIMM_FWA_ARMED:已武装(发送了 ARM 命令,等待激活)
  • NVDIMM_FWA_ARM_OVERFLOW:ARM 超时(需要重新武装)
  • NVDIMM_FWA_BUSY:激活中

22.3 固件激活命令序列

通过 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 行)控制是否允许在不挂起系统的情况下激活。


23. 内核配置与编译选项

23.1 关键 Kconfig 选项

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 内存错误检测(调试用)

23.2 持久内存相关的架构选项

  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 热插需要)

23.3 推荐的生产环境配置

  # 完整 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 加密支持

24. 附录:关键源码文件索引

文件路径 内容描述
drivers/nvdimm/pmem.c PMEM 块设备驱动,bio 处理,DAX 操作集
drivers/nvdimm/pmem.h pmem_device 结构体定义
drivers/nvdimm/nd.h nd_regionnd_bttnd_pfnnd_mapping 等核心结构
drivers/nvdimm/namespace_devs.c namespace 设备管理,label 解析
drivers/nvdimm/btt.c BTT 实现(arena 发现、原子写协议)
drivers/nvdimm/btt.h BTT 数据结构(btt_sbarena_infolog_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_devicedax_direct_accessdax_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_spanfit_memacpi_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 初始化

附录 B:关键常量与限制

常量 定义位置 说明
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 超时(秒)

附录 C:NVDIMM 与其他存储技术的比较

  各类存储技术延迟比较(典型值)
  +------------------------------------------------------------------+
  | 技术           | 读延迟    | 写延迟    | 接口      | 字节寻址 |
  +----------------+-----------+-----------+-----------+--------+
  | 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 分析生成