LIBNVDIMM:非易失性设备

libnvdimm - 内核 / libndctl - 用户空间辅助库

nvdimm@lists.linux.dev

版本 13

术语表

PMEM

一个系统物理地址范围,其写入是持久的。由 PMEM 组成的块设备能够支持 DAX。一个 PMEM 地址范围可以跨越多个 DIMM 的交错。

DPA

DIMM 物理地址,是相对于 DIMM 的偏移量。当系统中只有一个 DIMM 时,系统物理地址与 DPA 之间存在 1:1 的关联。一旦添加了更多的 DIMM,就必须解码内存控制器交错以确定与给定系统物理地址相关联的 DPA。

DAX

文件系统扩展,用于绕过页缓存和块层,将 PMEM 块设备中的持久内存直接 mmap 到进程地址空间。

DSM

设备特定方法:ACPI 方法,用于控制特定设备——在此情况下是固件。

DCR

NVDIMM 控制区域结构,定义于 ACPI 6 第 5.2.25.5 节。它定义了给定 DIMM 的供应商 ID、设备 ID 和接口格式。

BTT

块转换表:持久内存是字节寻址的。现有软件可能期望写入的掉电原子性至少为一个扇区(512 字节)。BTT 是一个具有原子更新语义的间接表,用于支持 PMEM 块设备驱动程序并提供任意原子扇区大小。

标签

存储在 DIMM 设备上的元数据,用于分区和识别(持久命名)分配给不同 PMEM 命名空间的容量。它还指示是否将 BTT 等地址抽象应用于命名空间。请注意,传统的分区表(GPT/MBR)是叠加在 PMEM 命名空间或 BTT 等地址抽象(如果存在)之上的,但未来将弃用分区支持。

概述

LIBNVDIMM 子系统支持平台固件或设备驱动程序描述的 PMEM。在基于 ACPI 的系统中,平台固件通过 ACPI 6 中的 ACPI NFIT “NVDIMM 固件接口表” 传输持久内存资源。虽然 LIBNVDIMM 子系统实现是通用的并支持预 NFIT 平台,但其设计指导思想是支持 ACPI 6 对 NVDIMM 资源定义的超集能力。最初的实现支持 NFIT 中描述的块窗口孔径功能,但该支持此后已被放弃,从未在产品中发布。

支持文档

ACPI 6

https://www.uefi.org/sites/default/files/resources/ACPI_6.0.pdf

NVDIMM 命名空间

https://pmem.io/documents/NVDIMM_Namespace_Spec.pdf

DSM 接口示例

https://pmem.io/documents/NVDIMM_DSM_Interface_Example.pdf

驱动编写者指南

https://pmem.io/documents/NVDIMM_Driver_Writers_Guide.pdf

Git 仓库

LIBNVDIMM

https://git.kernel.org/cgit/linux/kernel/git/nvdimm/nvdimm.git

LIBNDCTL

https://github.com/pmem/ndctl.git

LIBNVDIMM PMEM

在 NFIT 出现之前,非易失性内存以各种临时方式向系统描述。通常只提供最低限度的信息,即单个系统物理地址范围,其中写入预计在系统断电后仍能持久。现在,NFIT 规范不仅标准化了 PMEM 的描述,还标准化了用于控制和配置的平台消息传递入口点。

PMEM (nd_pmem.ko):驱动一个系统物理地址范围。此范围在系统内存中是连续的,并且可以在多个 DIMM 上交错(由硬件内存控制器条带化)。当交错时,平台可以选择性地提供哪些 DIMM 参与交错的详细信息。

值得注意的是,当检测到标签功能(找到 EFI 命名空间标签索引块)时,默认情况下不会创建块设备,因为用户空间需要至少一次将 DPA 分配给 PMEM 范围。相比之下,ND_NAMESPACE_IO 范围一旦注册,就可以立即附加到 nd_pmem。后一种模式称为无标签或“传统”模式。

PMEM 区域、原子扇区和 DAX

对于应用程序或文件系统仍然需要原子扇区更新保证的情况,它可以在 PMEM 设备或分区上注册一个 BTT。请参阅 LIBNVDIMM/NDCTL:块转换表 “btt”

NVDIMM 平台示例

对于本文的其余部分,任何 sysfs 布局示例都将参考以下图表

                             (a)               (b)           DIMM
          +-------------------+--------+--------+--------+
+------+  |       pm0.0       |  free  | pm1.0  |  free  |    0
| imc0 +--+- - - region0- - - +--------+        +--------+
+--+---+  |       pm0.0       |  free  | pm1.0  |  free  |    1
   |      +-------------------+--------v        v--------+
+--+---+                               |                 |
| cpu0 |                                     region1
+--+---+                               |                 |
   |      +----------------------------^        ^--------+
+--+---+  |           free             | pm1.0  |  free  |    2
| imc1 +--+----------------------------|        +--------+
+------+  |           free             | pm1.0  |  free  |    3
          +----------------------------+--------+--------+

在此平台中,我们有一个插槽中的四个 DIMM 和两个内存控制器。每个 PMEM 交错集都由一个具有动态分配 ID 的区域设备标识。

  1. DIMM0 和 DIMM1 的第一部分以 REGION0 的形式交错。在 REGION0-SPA 范围内创建了一个单个 PMEM 命名空间,该范围跨越了 DIMM0 和 DIMM1 的大部分,用户指定名称为 “pm0.0”。该交错系统物理地址范围的一部分被保留,以供定义另一个 PMEM 命名空间。

  2. 在 DIMM0 和 DIMM1 的最后一部分,我们有一个交错的系统物理地址范围 REGION1,它跨越这两个 DIMM 以及 DIMM2 和 DIMM3。REGION1 的一部分被分配给一个名为 “pm1.0” 的 PMEM 命名空间。

当加载来自 tools/testing/nvdimm 的 nfit_test.ko 模块时,内核在设备 /sys/devices/platform/nfit_test.0 下提供此总线。此模块是 LIBNVDIMM 和 acpi_nfit.ko 驱动程序的单元测试。

LIBNVDIMM 内核设备模型和 LIBNDCTL 用户空间 API

以下是 LIBNVDIMM sysfs 布局的描述以及通过 LIBNDCTL API 查看的相应对象层次结构图。示例 sysfs 路径和图表是相对于 NVDIMM 平台示例的,该示例也是 LIBNDCTL 单元测试中使用的 LIBNVDIMM 总线。

LIBNDCTL:上下文

LIBNDCTL 库中的每个 API 调用都需要一个上下文,该上下文保存日志参数和其他库实例状态。该库基于 libabc 模板

LIBNDCTL:实例化新库上下文示例

struct ndctl_ctx *ctx;

if (ndctl_new(&ctx) == 0)
        return ctx;
else
        return NULL;

LIBNVDIMM/LIBNDCTL:总线

总线与 NFIT 之间存在 1:1 的关系。目前对基于 ACPI 的系统的期望是,只有一个平台全局 NFIT。尽管如此,注册多个 NFIT 是微不足道的,规范并未排除这种情况。基础设施支持多个总线,我们利用此功能在单元测试中测试多种 NFIT 配置。

LIBNVDIMM:/sys/class 中的控制类设备

此字符设备接受 DSM 消息,并将其传递给由其 NFIT 句柄标识的 DIMM

/sys/class/nd/ndctl0
|-- dev
|-- device -> ../../../ndbus0
|-- subsystem -> ../../../../../../../class/nd

LIBNVDIMM:总线

struct nvdimm_bus *nvdimm_bus_register(struct device *parent,
       struct nvdimm_bus_descriptor *nfit_desc);
/sys/devices/platform/nfit_test.0/ndbus0
|-- commands
|-- nd
|-- nfit
|-- nmem0
|-- nmem1
|-- nmem2
|-- nmem3
|-- power
|-- provider
|-- region0
|-- region1
|-- region2
|-- region3
|-- region4
|-- region5
|-- uevent
`-- wait_probe

LIBNDCTL:总线枚举示例

查找描述 NVDIMM 平台示例中总线的总线句柄

static struct ndctl_bus *get_bus_by_provider(struct ndctl_ctx *ctx,
                const char *provider)
{
        struct ndctl_bus *bus;

        ndctl_bus_foreach(ctx, bus)
                if (strcmp(provider, ndctl_bus_get_provider(bus)) == 0)
                        return bus;

        return NULL;
}

bus = get_bus_by_provider(ctx, "nfit_test.0");

LIBNVDIMM/LIBNDCTL:DIMM (NMEM)

DIMM 设备提供了一个字符设备用于向硬件发送命令,并且它是 LABEL 的容器。如果 DIMM 由 NFIT 定义,则可以提供一个可选的“nfit”属性子目录以添加 NFIT 特定的内容。

请注意,内核中“DIMM”的设备名称是“nmemX”。NFIT 通过“内存设备到系统物理地址范围映射结构”描述这些设备,并且没有要求它们必须是物理 DIMM,因此我们使用了一个更通用的名称。

LIBNVDIMM:DIMM (NMEM)

struct nvdimm *nvdimm_create(struct nvdimm_bus *nvdimm_bus, void *provider_data,
                const struct attribute_group **groups, unsigned long flags,
                unsigned long *dsm_mask);
/sys/devices/platform/nfit_test.0/ndbus0
|-- nmem0
|   |-- available_slots
|   |-- commands
|   |-- dev
|   |-- devtype
|   |-- driver -> ../../../../../bus/nd/drivers/nvdimm
|   |-- modalias
|   |-- nfit
|   |   |-- device
|   |   |-- format
|   |   |-- handle
|   |   |-- phys_id
|   |   |-- rev_id
|   |   |-- serial
|   |   `-- vendor
|   |-- state
|   |-- subsystem -> ../../../../../bus/nd
|   `-- uevent
|-- nmem1
[..]

LIBNDCTL:DIMM 枚举示例

请注意,在此示例中,我们假设是 NFIT 定义的 DIMM,它们由“nfit_handle”标识,这是一个 32 位值,其中:

  • 位 3:0 内存通道内的 DIMM 编号

  • 位 7:4 内存通道编号

  • 位 11:8 内存控制器 ID

  • 位 15:12 插槽 ID(如果在场节点控制器范围内)

  • 位 27:16 节点控制器 ID

  • 位 31:28 保留

static struct ndctl_dimm *get_dimm_by_handle(struct ndctl_bus *bus,
       unsigned int handle)
{
        struct ndctl_dimm *dimm;

        ndctl_dimm_foreach(bus, dimm)
                if (ndctl_dimm_get_handle(dimm) == handle)
                        return dimm;

        return NULL;
}

#define DIMM_HANDLE(n, s, i, c, d) \
        (((n & 0xfff) << 16) | ((s & 0xf) << 12) | ((i & 0xf) << 8) \
         | ((c & 0xf) << 4) | (d & 0xf))

dimm = get_dimm_by_handle(bus, DIMM_HANDLE(0, 0, 0, 0, 0));

LIBNVDIMM/LIBNDCTL:区域

每个 PMEM 交错集/范围都注册一个通用 REGION 设备。根据示例,“nfit_test.0”总线上有两个 PMEM 区域。区域的主要作用是充当“映射”的容器。映射是一个 <DIMM, DPA-起始偏移量, 长度> 的元组。

LIBNVDIMM 为 REGION 设备提供了内置驱动程序。此驱动程序负责解析所有 LABEL(如果存在),然后发出 NAMESPACE 设备供 nd_pmem 驱动程序使用。

除了“mapping”、“interleave_ways”和“size”的通用属性外,REGION 设备还导出了一些便利属性。“nstype”指示此区域发出的命名空间设备的整数类型,“devtype”复制 udev 在“add”事件时存储的 DEVTYPE 变量,“modalias”复制 udev 在“add”事件时存储的 MODALIAS 变量,最后,在区域由 SPA 定义的情况下,提供可选的“spa_index”。

LIBNVDIMM:区域

struct nd_region *nvdimm_pmem_region_create(struct nvdimm_bus *nvdimm_bus,
                struct nd_region_desc *ndr_desc);
/sys/devices/platform/nfit_test.0/ndbus0
|-- region0
|   |-- available_size
|   |-- btt0
|   |-- btt_seed
|   |-- devtype
|   |-- driver -> ../../../../../bus/nd/drivers/nd_region
|   |-- init_namespaces
|   |-- mapping0
|   |-- mapping1
|   |-- mappings
|   |-- modalias
|   |-- namespace0.0
|   |-- namespace_seed
|   |-- numa_node
|   |-- nfit
|   |   `-- spa_index
|   |-- nstype
|   |-- set_cookie
|   |-- size
|   |-- subsystem -> ../../../../../bus/nd
|   `-- uevent
|-- region1
[..]

LIBNDCTL:区域枚举示例

基于 NFIT 独有数据(如“spa_index”(交错集 ID))的示例区域检索例程。

static struct ndctl_region *get_pmem_region_by_spa_index(struct ndctl_bus *bus,
                unsigned int spa_index)
{
        struct ndctl_region *region;

        ndctl_region_foreach(bus, region) {
                if (ndctl_region_get_type(region) != ND_DEVICE_REGION_PMEM)
                        continue;
                if (ndctl_region_get_spa_index(region) == spa_index)
                        return region;
        }
        return NULL;
}

LIBNVDIMM/LIBNDCTL:命名空间

一个 REGION,在解决了 DPA 别名和 LABEL 指定的边界之后,会显露一个或多个“命名空间”设备。目前,“命名空间”设备的出现会触发 nd_pmem 驱动程序加载并注册一个磁盘/块设备。

LIBNVDIMM:命名空间

以下是两种主要 NAMESPACE 类型的一个示例布局,其中 namespace0.0 代表由 DIMM 信息支持的 PMEM(请注意它有一个‘uuid’属性),而 namespace1.0 代表一个匿名 PMEM 命名空间(请注意它没有‘uuid’属性,因为不支持 LABEL)

/sys/devices/platform/nfit_test.0/ndbus0/region0/namespace0.0
|-- alt_name
|-- devtype
|-- dpa_extents
|-- force_raw
|-- modalias
|-- numa_node
|-- resource
|-- size
|-- subsystem -> ../../../../../../bus/nd
|-- type
|-- uevent
`-- uuid
/sys/devices/platform/nfit_test.1/ndbus1/region1/namespace1.0
|-- block
|   `-- pmem0
|-- devtype
|-- driver -> ../../../../../../bus/nd/drivers/pmem
|-- force_raw
|-- modalias
|-- numa_node
|-- resource
|-- size
|-- subsystem -> ../../../../../../bus/nd
|-- type
`-- uevent

LIBNDCTL:命名空间枚举示例

命名空间相对于其父区域进行索引,示例如下。这些索引在每次引导时大多是静态的,但子系统对此不作保证。对于静态命名空间标识符,请使用其“uuid”属性。

static struct ndctl_namespace
*get_namespace_by_id(struct ndctl_region *region, unsigned int id)
{
        struct ndctl_namespace *ndns;

        ndctl_namespace_foreach(region, ndns)
                if (ndctl_namespace_get_id(ndns) == id)
                        return ndns;

        return NULL;
}

LIBNDCTL:命名空间创建示例

如果给定区域有足够的可用容量来创建新命名空间,内核会自动创建空闲命名空间。命名空间实例化涉及查找空闲命名空间并对其进行配置。大多数情况下,命名空间属性的设置可以以任何顺序进行,唯一的限制是“uuid”必须在“size”之前设置。这使得内核能够使用静态标识符在内部跟踪 DPA 分配。

static int configure_namespace(struct ndctl_region *region,
                struct ndctl_namespace *ndns,
                struct namespace_parameters *parameters)
{
        char devname[50];

        snprintf(devname, sizeof(devname), "namespace%d.%d",
                        ndctl_region_get_id(region), parameters->id);

        ndctl_namespace_set_alt_name(ndns, devname);
        /* 'uuid' must be set prior to setting size! */
        ndctl_namespace_set_uuid(ndns, parameters->uuid);
        ndctl_namespace_set_size(ndns, parameters->size);
        /* unlike pmem namespaces, blk namespaces have a sector size */
        if (parameters->lbasize)
                ndctl_namespace_set_sector_size(ndns, parameters->lbasize);
        ndctl_namespace_enable(ndns);
}

为何使用术语“命名空间”?

  1. 例如,为什么不是“卷”(volume)?“卷”有使 ND (libnvdimm 子系统) 与 device-mapper 等卷管理器混淆的风险。

  2. 该术语最初用于描述可在 NVME 控制器内创建的子设备(请参阅 nvme 规范:https://www.nvmexpress.org/specifications/),而 NFIT 命名空间旨在与 NVME 命名空间的功能和可配置性并行。

LIBNVDIMM/LIBNDCTL:块转换表 “btt”

BTT(设计文档:https://pmem.io/2014/09/23/btt.html)是命名空间的一个个性化驱动程序,它将整个命名空间呈现为一种“地址抽象”。

LIBNVDIMM:BTT 布局

每个区域最初都将至少有一个 BTT 设备,即种子设备。要激活它,请设置“namespace”、“uuid”和“sector_size”属性,然后根据区域类型将设备绑定到 nd_pmem 或 nd_blk 驱动程序。

/sys/devices/platform/nfit_test.1/ndbus0/region0/btt0/
|-- namespace
|-- delete
|-- devtype
|-- modalias
|-- numa_node
|-- sector_size
|-- subsystem -> ../../../../../bus/nd
|-- uevent
`-- uuid

LIBNDCTL:BTT 创建示例

与命名空间类似,每个区域都会自动创建一个空闲的 BTT 设备。每次配置和启用此“种子”BTT 设备时,都会创建一个新的种子。创建 BTT 配置涉及两个步骤:找到一个空闲的 BTT 并将其分配给一个命名空间。

static struct ndctl_btt *get_idle_btt(struct ndctl_region *region)
{
        struct ndctl_btt *btt;

        ndctl_btt_foreach(region, btt)
                if (!ndctl_btt_is_enabled(btt)
                                && !ndctl_btt_is_configured(btt))
                        return btt;

        return NULL;
}

static int configure_btt(struct ndctl_region *region,
                struct btt_parameters *parameters)
{
        btt = get_idle_btt(region);

        ndctl_btt_set_uuid(btt, parameters->uuid);
        ndctl_btt_set_sector_size(btt, parameters->sector_size);
        ndctl_btt_set_namespace(btt, parameters->ndns);
        /* turn off raw mode device */
        ndctl_namespace_disable(parameters->ndns);
        /* turn on btt access */
        ndctl_btt_enable(btt);
}

一旦实例化,一个新的非活动 BTT 种子设备将出现在该区域下方。

一旦“命名空间”从 BTT 中移除,该 BTT 设备的实例将被删除或重置为默认值。此删除仅在设备模型级别进行。为了销毁 BTT,“信息块”需要被销毁。请注意,要销毁 BTT,介质需要以原始模式写入。默认情况下,内核将自动检测 BTT 的存在并禁用原始模式。可以通过 ndctl_namespace_set_raw_mode() API 为命名空间启用原始模式来抑制此自动检测行为。

LIBNDCTL 概述图

对于上述示例,以下是 LIBNDCTL API 所见对象的视图

            +---+
            |CTX|
            +-+-+
              |
+-------+     |
| DIMM0 <-+   |      +---------+   +--------------+  +---------------+
+-------+ |   |    +-> REGION0 +---> NAMESPACE0.0 +--> PMEM8 "pm0.0" |
| DIMM1 <-+ +-v--+ | +---------+   +--------------+  +---------------+
+-------+ +-+BUS0+-| +---------+   +--------------+  +----------------------+
| DIMM2 <-+ +----+ +-> REGION1 +---> NAMESPACE1.0 +--> PMEM6 "pm1.0" | BTT1 |
+-------+ |        | +---------+   +--------------+  +---------------+------+
| DIMM3 <-+
+-------+