欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

PMDK之libpmemobj库的使用

程序员文章站 2022-07-05 23:21:31
...

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