PMDK簡介
PMDK是業界公認的持久性內存庫(NVML),它包含一系列的程序庫和工具,以便管理和訪問持久性內存設備。這些庫基於Linux和Windows上的Direct Access (DAX) 特性,讓應用程序可以通過持久性內存文件系統[1]直接讀寫持久性內存。這種機制繞過了Page Cache,直接將持久性內存映射到用戶進程內存空間,從而使用戶直接以內存讀寫的形式訪問持久性文件,顯著提升持久性內容的訪問性能。
在PMDK的程序庫中,主要有以下幾種:
- libpmem,libpmempool等底層庫,用於構建其他工具庫。
- libpmemobj,libpmemlog,libpmemobj 等,它們將持久新內存當成非易失內存暴露給用戶。
- libvmem,libvmmalloc等,將持久性內存當成易失性內存暴露給用戶。
- 其他librpmem,libvmemcache等其他功能庫。
由於構建一棵持久性內存B+樹索引不可避免需要使用libpmemobj庫[2]來提供持久性保證,事務支持和持久性內存的管理。因此我們以下主要調研libpmemobj庫的使用方法。
Libpmemobj庫:
-
Memory Pools : PMEMobjpool
PMDK利用DAX特性實現對持久性內存的直接訪問,這種訪問通過內存映射文件(memory-mapping file)的方式實現。libpmemobj庫提供了簡便的接口,使得內存文件映射過程能夠自動完成。簡單來講,當我們完成一個文件的內存映射後,就相當於開闢了一個PMEMobjpool,隨後應用程序便可以在該pool中分配對象並執行內存讀寫操作。
//PMEMobjpool接口 PMEMobjpool * pop = pmemobj_create(path, POBJ_LAYOUT_NAME(layout_name) PMEMOBJ_MIN_POOL, 0666);//path爲映射的文件路徑 pmemobj_open(path, POBJ_LAYOUT_NAME(layout_name)); pmemobj_close(pop);
-
Persistence Pointer : PMEMoid
Persistence Pointer是持久性內存對象的邏輯指針,通過它可以在對應pool找到相應的對象起始地址。其物理結構如下:
// PMEMoid結構 typedef struct pmemoid { uint64_t pool_uuid_lo; uint64_t off; } PMEMoid;
如果已知pool的物理起始地址,那麼可以通過
(void *)((uint64_t)pool + oid.off)
獲得其在持久性內存空間中的實際內存地址,從而對內存執行直接讀寫操作。//PMEMoid的相關接口 pmemobj_root(pop, sizeof(T)); //獲得存儲在pool中數據結構的入口,參見下文 pmemobj_persist(pop, mem_address, size_t); //持久化size_t大小的內存空間,其中mem_address爲實際內存地址 pmemobj_memcpy_persist(pop, mem_address, void *, size_t); //持久化內存拷貝操作 pmemobj_direct(PMEMoid data) //獲得data對象的實際內存地址 OID_INSTANCEOF(PMEMoid data, T); //類型檢查,查看是否爲T類型
作爲持久性編程模型,NVM內存管理的一大難點也是防止內存泄漏。數據存儲在PMEMobjpool中,當其他程序需要再次訪問其中的對象時,便需要獲得對象的地址。因此必須將存儲在pool中的地址記錄下來,以便程序生命期後能夠繼續訪問。PMEMobjpool提供了這樣的一個記錄手段——root對象。每個pool都可記錄一個root對象,利用該對象作爲入口地址即可迅速定位存儲在該pool的持久性數據結構。想象一個pool中存儲了一棵B+樹或者一個鏈表,則B+樹的root結點、鏈表的頭結點便可作爲root對象。
//基本使用方法 PMEMoid root = pmemobj_root(pop, sizeof(root_t)); //從池中定位到數據結構入口 struct my_root *rootp = pmemobj_direct(root); //轉換成實際內存地址 rootp->len = strlen(buf); //直接賦值 pmemobj_persist(pop, &root->len, sizeof (root->len)); //賦值後持久化
-
類型 : TOID(T) ,其中T爲任何內置基本類型和自定義類型,當然也可爲PMEMoid類型。
C/C++是有類型語言,在編譯過程中可以執行類型檢查,但是PMEMoid指針類型形式上等價於void,因此在很多情況下並不安全,特別是執行函數傳參時。爲了增加類型安全性,libpemobj引入了類型的概念,底層採用宏實現。其結構如下:
union _toid_T_toid { //在全局註冊時會生成如下類型(宏展開) PMEMoid oid; T *_type; _toid_T_toid_type_num *_type_num; }; //TOID相關接口 POBJ_LAYOUT_TOID(layout_name, T); //使用宏定義爲pool在全局註冊T類型,此處會宏展開定義_toid_T_toid新類型 TOID(root_t) root = POBJ_ROOT(pop, root_t); //找到pool中的入口對象 TOID(T) name;//聲明持久化T對象 D_RW(name); // 類似解引用的指針 D_RO(name); // 類似解引用的指針常量 name.oid; //獲得name對象的PMEMoid指針
TOID宏封裝了對持久性內存對象的訪問細節,使得用戶使用更爲簡便。在PMEMoid指針形式下,需要先獲得對象的實際內存地址,然後執行內存讀寫,並手動完成持久化操作。在TOID類型下,其使用方式如下:
//在pool中註冊該類型 POBJ_LAYOUT_BEGIN(list); POBJ_LAYOUT_ROOT(list, struct node); POBJ_LAYOUT_TOID(list, struct node); POBJ_LAYOUT_END(list); //找到root對象 TOID(struct node) head = POBJ_ROOT(pop, struct node); //這裏root是鏈表頭結點 TOID(struct node) new_node; //聲明一個新結點 POBJ_ZNEW(pop, &new_node, struct node, sizeof(struct node)); //爲新結點分配內存空間 //掛載新結點 D_RW(new_node)->val = D_RO(head)->val; D_RW(new_node)->next = D_RO(head)->next; //更新head結點 D_RW(head)->val = value; D_RW(head)->next = new_node.oid;
-
動態內存分配(事務區塊外的內存分配)
除了上述類似C/C++語言從棧空間分配內存空間的方式使用持久性內存,我們還需要動態分配內存空間的接口,以便更自由地管理持久性內存空間。同樣libpmemobj庫也提供了相應的接口。
//動態持久化內存分配接口 POBJ_NEW(pop, &(TOID(T)), T, construct_fn, arg); //指定構造函數分配對象 POBJ_ALLOC(pop, &(TOID(T)), T, size_t, initialize_fn, arg);//指定初始化函數分配空間 POBJ_ZNEW(pop, &(TOID(T)), T, size_t); //零初始化分配空間 POBJ_FREE(&(TOID(T)));//釋放內存空間
下面給出一個例子,用來介紹持久性動態內存空間分配的使用:
TOID(int) array; //聲明一個array類型。 POBJ_ALLOC(pop, &array, int, sizeof(int) * size, NULL, NULL); //爲array分配內存空間 if (TOID_IS_NULL(array)) { // 分配失敗 fprintf(stderr, "POBJ_ALLOC\n"); return OID_NULL; } for (size_t i = 0; i < size; i++) // 使用array接口 D_RW(array)[i] = (int)i; //將堆array的修改持久化 pmemobj_persist(pop, D_RW(array), size * sizeof(*D_RW(array)));
-
事務功能
爲了方便安全地使用持久性內存,PMDK庫提供了事務功能,這使得應用程序可以執行一段關鍵功能代碼時像事務一樣具有ACID性質,且在代碼失敗時執行相應地恢復操作。通過事務接口隔離出來的區塊如下,注意其只有TX_BEGIN和TX_END是必須存在的:
/* TX_STAGE_NONE: 非事務區塊*/ TX_BEGIN(pop) { /* TX_STAGE_WORK: 事務想要完成的功能區塊*/ } TX_ONCOMMIT { /* TX_STAGE_ONCOMMIT: 事務提交時需要做的額外工作*/ } TX_ONABORT { /* TX_STAGE_ONABORT: 事務回滾時需要做的恢復工作*/ } TX_FINALLY { /* TX_STAGE_FINALLY: 事務commit或者abort都會執行的區塊*/ } TX_END /* TX_STAGE_NONE: 非事務區塊 */
在一個事務中,主要有三種操作,內存分配,內存釋放和內存賦值。在下面的例子中,我們嘗試給出三種操作的使用方法:
TOID(struct root_t) root = POBJ_ROOT(pop); TX_BEGIN(pop) { //事務中分配對象 TX_ADD(root); /* we are going to operate on the root object */ TOID(struct rectangle) rect = TX_NEW(struct rectangle); D_RW(rect)->x = 5; D_RW(rect)->y = 10; D_RW(root)->rect = rect; } TX_END TX_BEGIN(pop) { // 事務中釋放對象 TX_ADD(root); TX_FREE(D_RW(root)->rect); D_RW(root)->rect = TOID_NULL(struct rectangle); } TX_END
-
其他功能
除上述功能之外,libpmemobj還提供了線程,以及其他內置宏:內置對象序列(類似對象構成的鏈表),持久化雙鏈表等功能,具體內容可參考[3]。
參考文獻
[1]. SNIA NVM Programming Model, https://www.snia.org/sites/default/files/technical_work/final/NVMProgrammingModel_v1.2.pdf
[2]. libpmemobj庫, https://pmem.io/pmdk/libpmemobj/
[3]. Persistent Lists, https://pmem.io/2015/06/19/lists.html