PMDK之libpmemobj庫的使用

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章