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