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
上一篇: instanceof详细介绍和使用,实际项目中的使用案例
下一篇: ASP.NET获取上传图片的大小
推荐阅读
-
oracle数据库删除数据Delete语句和Truncate语句的使用比较
-
Python的Django框架中使用SQLAlchemy操作数据库的教程
-
python三方库之requests的快速上手
-
django配置连接数据库及原生sql语句的使用方法
-
纯Python开发的nosql数据库CodernityDB介绍和使用实例
-
[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用
-
使用Python的SymPy库解决数学运算问题的方法
-
Python使用win32com模块实现数据库表结构自动生成word表格的方法
-
python模块之subprocess模块级方法的使用
-
DRF跨域后端解决之django-cors-headers的使用