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

你可能感兴趣的:(C++,Database,数据库,内存数据库)