LIBNVDIMM:非易失性设备¶
libnvdimm - 内核 / libndctl - 用户空间辅助库
版本 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 中描述的块窗口孔径功能,但该支持此后已被放弃,从未在产品中发布。
支持文档¶
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 的区域设备标识。
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”总线上有两个 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);
}
为何使用术语“命名空间”?¶
例如,为什么不是“卷”(volume)?“卷”有使 ND (libnvdimm 子系统) 与 device-mapper 等卷管理器混淆的风险。
该术语最初用于描述可在 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 <-+
+-------+