LIBNVDIMM:非易失性设备¶
libnvdimm - 内核 / libndctl - 用户空间帮助库
版本 13
术语表¶
- PMEM
写入操作具有持久性的系统物理地址范围。由 PMEM 组成的块设备能够使用 DAX。PMEM 地址范围可以跨越多个 DIMM 的交错。
- DPA
DIMM 物理地址,是 DIMM 相对偏移量。在系统中只有一个 DIMM 的情况下,系统物理地址与 DPA 之间将存在 1:1 的关联。一旦添加更多 DIMM,必须解码内存控制器交错以确定与给定系统物理地址关联的 DPA。
- DAX
文件系统扩展,用于绕过页面缓存和块层,从而将持久内存(来自 PMEM 块设备)直接 mmap 到进程地址空间。
- DSM
设备特定方法:用于控制特定设备的 ACPI 方法 - 在此例中为固件。
- DCR
ACPI 6 第 5.2.25.5 节中定义的 NVDIMM 控制区域结构。它为给定的 DIMM 定义了供应商 ID、设备 ID 和接口格式。
- BTT
块转换表:持久内存是字节寻址的。现有软件可能期望写入操作的电源故障原子性至少为一个扇区,即 512 字节。BTT 是一个具有原子更新语义的间接表,用于为 PMEM 块设备驱动程序提供前端并呈现任意原子扇区大小。
- LABEL
存储在 DIMM 设备上的元数据,用于分区并标识(持久命名)分配给不同 PMEM 命名空间的容量。它还指示是否将诸如 BTT 之类的地址抽象应用于命名空间。请注意,传统的分区表 GPT/MBR 分层在 PMEM 命名空间之上,或者在存在时分层在诸如 BTT 之类的地址抽象之上,但未来将弃用分区支持。
概述¶
LIBNVDIMM 子系统提供对平台固件或设备驱动程序描述的 PMEM 的支持。在基于 ACPI 的系统上,平台固件通过 ACPI 6 中的 ACPI NFIT “NVDIMM 固件接口表”来传递持久内存资源。虽然 LIBNVDIMM 子系统实现是通用的并且支持 pre-NFIT 平台,但它是以支持 ACPI 6 中 NVDIMM 资源定义的超集功能为指导的。最初的实现支持 NFIT 中描述的块窗口孔径功能,但该支持后来被放弃,并且从未在产品中发布。
支持文档¶
Git 树¶
LIBNVDIMM PMEM¶
在 NFIT 出现之前,非易失性内存是以各种临时方式向系统描述的。通常只提供最低限度的信息,即单个系统物理地址范围,在该范围内,写入操作预计在系统掉电后是持久的。现在,NFIT 规范不仅标准化了 PMEM 的描述,还标准化了用于控制和配置的平台消息传递入口点。
PMEM (nd_pmem.ko):驱动系统物理地址范围。此范围在系统内存中是连续的,并且可以跨多个 DIMM 进行交错(硬件内存控制器条带化)。在交错时,平台可以选择提供有关哪些 DIMM 参与交错的详细信息。
值得注意的是,当检测到标记功能时(找到 EFI 命名空间标签索引块),默认情况下不会创建块设备,因为用户空间至少需要将一个 DPA 分配给 PMEM 范围。相反,ND_NAMESPACE_IO 范围一旦注册,就可以立即附加到 nd_pmem。后一种模式称为无标签或“传统”模式。
PMEM-REGION、原子扇区和 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 的区域设备标识。
DIMM0 和 DIMM1 的第一部分被交错为 REGION0。在 REGION0-SPA 范围内创建了一个 PMEM 命名空间,该范围跨越了 DIMM0 和 DIMM1 的大部分,用户指定的名称为“pm0.0”。该交错的系统物理地址范围的某些部分被保留为空闲状态,以便定义另一个 PMEM 命名空间。
在 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” 总线上有 2 个 PMEM 区域。区域的主要作用是作为“映射”的容器。“映射”是一个 <DIMM、DPA 起始偏移量、长度> 的元组。
LIBNVDIMM 为 REGION 设备提供了一个内置驱动程序。此驱动程序负责解析所有 LABEL(如果存在),然后为 nd_pmem 驱动程序生成 NAMESPACE 设备供其使用。
除了“映射”、“交错方式”和“大小”的通用属性外,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 命名空间(请注意,由于不支持 LABEL,因此它没有 “uuid” 属性)。
/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:命名空间创建示例¶
如果给定区域有足够的可用容量来创建新的命名空间,则内核会自动创建空闲命名空间。命名空间实例化涉及查找空闲命名空间并对其进行配置。在大多数情况下,命名空间属性的设置可以以任何顺序进行,唯一的约束是必须在 “size” 之前设置 “uuid”。这使内核可以使用静态标识符在内部跟踪 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), paramaters->id);
ndctl_namespace_set_alt_name(ndns, devname);
/* 'uuid' must be set prior to setting size! */
ndctl_namespace_set_uuid(ndns, paramaters->uuid);
ndctl_namespace_set_size(ndns, paramaters->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);
}
为什么使用术语“命名空间”?¶
为什么不使用 “卷” 之类的术语呢?“卷” 有可能使 ND(libnvdimm 子系统)与像设备映射器这样的卷管理器混淆。
该术语最初用于描述可以在 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 <-+
+-------+