PMDK based Persistent Memory Pro

2018-08-16  本文已影响0人  Glitter试做一号机

基于libpmemobj库的C-style编程

本文主要用于总结对PMDK库中libpmemobj中相关内容的学习,通过简单学习libpmemobj可以对基于PMDK的持久内存编程有个大概的了解。PMDK包含了libpmemobj在内的多个库,其中libpmemobj是上层的封装,一般用户可以直接调用这个库提供的API进行持久内存编程。不过libpmemobj主要还是C的实现,相对而言也是偏向底层的,所以如果有更高抽象层次的需求,PMDK提供了对应的C++绑定,实现了C++ style的一些封装,更加有高级语言风格。目前先介绍libpmemobj,这样也有助于后面学习C++ binding。

开始编程之前的准备

Layout

在进行持久编程之前首先需要定义一个layout名,因为PMDK在打开一个持久内存资源池的时候需要制定一个layout,通过layout来标识一个内存池,这是一个字符串。

定义的方式有两种,一种就是直接定义:(其实感觉类似文件名的作用,但是不是文件名)

#define LAYOUT_NAME "my_layout"

或者直接在参数中传入一个字符串就行

另一种通过宏的方式

这种方式下提供了一系列的宏,除了定义layout外还涉及其他的定义主要有

POBJ_LAYOUT_BEGIN(layout_name)
POBJ_LAYOUT_TOID(layout_name, type)
POBJ_LAYOUT_ROOT(layout_name, root)
POBJ_LAYOUT_END(layout_name)

其中TIOD宏用于定义程序中所需要的类型,而ROOT宏用于定义根对象,关于根对象后面会有解释,这两个定义一定要在BEGIN与END之间

除此之外,使用这种方法定义了layout之后,在使用layout_name的时候就需要通过POBJ_LAYOUT_NAME宏来获取layout name

Persistent Pointer

Raw type

对于易失性内存上的编程,一个指针本质上是一个指向对象在虚拟内存地址空间的起始地址,而持久内存中的指针作用也是类似,同样是相当于一个记录了当前对象在内存池中的位置的结构,也有不一样的地方。

最明显的不同就是,持久内存指针包含了两个值。持久内存指针通过一个结构体PMEMoid来定义,其原始定义为:

typedef struct PMEMoid{
   uint64_t pool_uuid_lo;
   uint64_t off;
}

两个值分别为资源池pool的id,以及该对象在pool中offset,由于一个应用可能有多个资源池,所以通过这两个值就能定位一个持久对象的位置。

当我们需要对一个持久对象进行操作的时候,我们可以需要其在虚拟地址空间中的地址,这个时候可以通过pmembj_direct函数将一个PMEMoid指针转换为我们所需要的结构的指针,比如:

struct root = pmemobj_direct(my_root);//my_root is a PMEMoid

typed persistent pointer

有了PMEMoid我们可以对持久类型对象进行操作了,但是有一个问题,PMEMoid相当于直接通过一个地址去访问数据,但是这个数据的含义是不知道的,就相当于使用一个void*来对数据进行操作(这个时候就可能会出现不同类型的指针之间相互赋值这种情况,但是编译器并不会认为他是错的)。所以我们需要引入类型系统。PMDK中使用较多的类型系统是基于Named Union实现的TIOD类型。

TOID类型的实现定义为:

#define TOID(type)\
union _toid_##type##_toid

#define TOID_DECLARE(type)\
TOID(type)\
{\
    PMEMoid oid;\
    type *_type;\
}

可以看到这里定义了一个\_toid\_##type##\_tiod类型的union,好吧这不是重点,背后的实现机理大致就是这样,所以当我们需要一个类型化的持久指针的时候就可以这样声明:

struct my_root{
    int a;
    char name[10];
}
TOID(struct my_root) root;

这里我们就获得了一个my_root类型的持久指针root,在这种情况下,不同类型的TOID之间的赋值则会被编译器拒绝。同时由于我们有了类型,所以当需要访问类型的对象的时候,不再需要pmemobj_direct的转换而是通过两个API:D_RO以及D_RW,分别实现读以及写操作

printf("%d\n", D_RO(root)->a);

D_RW(root)->a = 10;

而且两个API内部是自动持久化的,所以我们不用显式地再去调用persist,同时更加符合我们的编程习惯。

根对象

其实最开始一直没有弄懂根对象是干什么用的以及怎么用的,直到看到了一个实际的例子之后才大致理解了根对象的作用。(这个例子是PMDK包中提供的一个使用PMDK编程的用例,是一个基于持久内存的小游戏,https://github.com/pmem/pmdk/blob/master/src/examples/libpmemobj/pminvaders/pminvaders2.c,非常有意思,完美地体现了持久内存的魅力)

首先是根对象(root object)是干什么的。根据官方的说法,根对象的作用就是一个访问持久内存对象的入口点,是一个锚的作用。设想一下程序的所有对象都放到你定义的一个内存池中,那么当我们再次打开这个内存池的时候,如何去访问你之前存放在你内存池中的对象?这就需要根对象,因为根对象是你的内存池中唯一可寻址的对象,其他所有对象都需要从根对象开始访问,所以从根对象开始,就可以获取你在这个内存池中存放的所有持久对象了。

根对象示意

那么有了根对象怎么去使用,根据我的理解以及根对象的定义,当你定义一个新的持久对象的时候就需要将其添加到根对象中,比如:

struct root{
    int a;
    char buf[10];
    struct node n;
};

这个根对象表示我们会用到一个整数a、一个字符数组buf和一个node结构体,我们把它们全部放到根对象里面,那么下次需要访问这些对象的时候就可以从根对象开始访问。

根对象的创建

根对象的创建API有两个,主要区别在于是否是类型化的

首先是非类型化的原始API:pmemobj_root(PMEMobjpool* pop, size_t size)

涉及到的参数主要是内存池指针以及所需要的大小。pmemobj_root函数的主要作用是create或者resize根对象,根据官方文档的描述,当你初次调用这个函数的时候,如果size大于0并且没有根对象存在,则会分配空间并创建一个根对象。当size大于当前根对象的size的时候会进行重分配并resize。

另一个接口是类型化的API:POBJ_ROOT(PMEMobjpool* pop, TYPE)

这是一个宏,传入的TYPE是根对象的类型,并且最后返回值类型是一个TOID指针,其余的用法与pmemobj_root一致

需要注意的是一个资源池的根对象是唯一的

如何通过PMDK访问Persistent Memory资源

PMDK是通过将持久内存抽象成资源池的方式进行访问,对应的API主要有三个分别是create、open以及close

PMEMobjpool *pmemobj_create(const char *path, const char *layout, size_t poolsize, mode_t mode)

create函数用于创建一个资源池,通过传入的路径、指定的layout以及大小创建一个持久内存资源池,返回一个PMEMobjpool类型的指针

PMEMobjpool *pmemobj_open(const char *path, const char *layout)

open函数用于给定路径以及layout然后打开一个资源池(二次使用的时候)

void pmemobj_close(PMEMobjpool *pop)

close用于关闭,每次程序结束之前一定要调用close关闭资源池

事务

在没有事务机制的情况下,当我们想要写一段数据的时候,需要先写数据的长度,然后在写数据,通过这样的方式保证数据在崩溃的时候的一致性,比如:

rootp->len = strlen(buf);
pmemobj_persist(pop, &rootp->len, sizeof (rootp->len));
pmemobj_memcpy_persist(pop, rootp->buf, my_buf, rootp->len);

为了简化这一过程引入了事务机制,首先是最基础的事务机制:

TX_BEGIN(pop){

}TX_ONCOMMIT{

}TX_ONABORT{

}TX_FINALY{

}TX_END

整个事务的流程可以通过这几个宏以及代码块来定义,并且将事务分成了多个阶段,中间的三个阶段为可选的,最基本的一个事务流程是TX_BEGIN-TX_END,这也是最常用的部分,其他的几个部分在嵌套事务中使用较多。

除了基本的事务代码块,libpmemobj还提供了相应的事务操作API。

一个是事务性数据写入API:pmemobj_tx_add_range&pmemobj_tx_add_range_direct,add_range函数主要有三个参数:root object、offset以及size,该函数表示我们将会操作[offset, offset+size)这段内存空间,PMDK将会自动在undo log中分配一个新的对象,然后将这段空间的内容记录到undo log中,这样我们就能随机去修改这段空间的内容并且保证一致性。带上direct标志的函数用法一致,区别在于direct函数直接操作的是一段虚拟地址空间。

TX_BEGIN(pop){
    pmemobj_tx_add_range(root, 0, sizeof(my_root));
    memcpy(rootp->buf, buf, strlen(buf));
}

注意事务性的API一定要在事务内执行

除了这两个函数还有一个TX_MEMCPY,用法与memcpy类似,只不过是针对事务性持久内存。

另一个是事务性的数据分配与回收,分别是TX_NEW以及TX_FREE,TX_NEW以待分配的结构作为参数,返回一个对应类型的TOID指针。需要注意的是NEW和FREE使用之前都需要调用TX_ADD函数,其作用与pmemobj_tx_add_range类似,都是表示我们将会修改这段内存空间。

TOID(my_root) root = POBJ_ROOT(pop);
TX_BEGIN(pop){
    TX_ADD(root);
    TOID(my_class) classp = TX_NEW(my_class);
    D_RO(root)->class = classp;
}TX_END

TX_BEGIN(pop){
    TX_ADD(root);
    TX_FREE(D_RO(root)->class);
    D_RO(root)->class = TOID_NULL(my_class);
}TX_END

这里FREE之后还要给class赋值一个NULL指针,防止访问到无效值

数据持久化

数据持久化的事务性API前面已经提到,以及两个非事务的基本API:

void pmemobj_persist(PMEMobjpool *pop, const void *addr, size_t len);:用于将[addr, add+size)区间内的数据持久化

void *pmemobj_memcpy_persist(PMEMobjpool *pop, void *dest, const void *src, size_t len);:作用类似memcpy

上一篇 下一篇

猜你喜欢

热点阅读