技术与生活 性能之重 持久内存编程指南—乱译连载 (7. libpmemobj: 本地事务对象存储)

持久内存编程指南—乱译连载 (7. libpmemobj: 本地事务对象存储)

本章描述了libpmemobj库,设计用于简化持久内存编程。通过提供那些支持原子操作、事务和预订/发布特性的API,使得创建保证数据完整性的应用,更不易于出错。

7  libpmemobj: 本地事务对象存储

前面的章节,我们介绍了libpmem,低层持久内存库提供了简单易用的方法来直接访问持久内存。Libpmem很小、很轻量,特性当然也有限,它是为那些追踪每个存储到pmem和需要flush这些变更持久化的软件而设计。它擅长它所做的事情。然而,大部分开发者更愿意使用PMDK中更高层的库,如:libpmemobj,这更加便捷。

本章介绍libpmemobj,构建于libpmem之上,并且将持久内存映射文件变成一个灵活的对象存储库。它支持事务、内存管理、锁、列表,以及几个其他特性。

7.1  Libpmemobj是什么

Libpmemobj是什么?

Libpmemobj库为应用提供了一个持久内存上的事务对象存储库,它使用DAX直接访问内存。简要回顾下第3章讨论的DAX,DAX允许应用使用持久内存上的内存映射文件,并提供直接的load/store操作,而非块存储设备上的页块式操作。它绕过了kernel,避免了上下文切换和中断,允许应用直接读写字节寻址方式的持久化存储。

7.2  为什么不是malloc()

使用libpmem似乎很简单。您需要刷新已写的任何内容,并在排序时使用规程,以便在指向数据的任何指针生效之前,数据已被持久化。

如果持久内存编程这么简单就好了。除了一些特定模式很简单(如 libpmemlog高效提供的只读记录)之外,任何新数据都需要分配内存。分配器应在何时以及如何将内存标记为正在使用?分配器应该在写入数据之前还是之后将内存标记为已分配?在数据写之前或者写之后标记内存已分配,都不能正常工作,原因如下:

  1. 如果分配器在数据写之前标记内存已分配,那么在写期间断电会引起更新被撕裂,也称为持久泄露(persistent leak);
  2. 如果分配器在数据写之后标记为已分配,在写完成持久化和分配器标记它分配完成之间断电,在应用重启时可能会覆盖该数据,因为分配器认为该块是可用的。

另外一个问题是大量的数据结构包含循环引用,因而不能形成一个树。他们应当被实现为一个树,但此方法通常难于实现。

字节可寻址内存只保障了一次写入的原子性。对于当前的处理器,通常是64位字(8字节),应该对齐,但实际上不需要。

如果多写可以同时发生,那么上述所有问题都可以解决。当面临电源失效时,任何未完成的写应当被重放,就好像电源失效没发生过一样;或者被放弃就像写从来没发生过一样。应用程序使用原子操作、事务、重做/撤消日志等以不同的方式解决这些问题。使用libpmemobj也能解决这些问题,因为它也使用了原子事务和重做/撤消日志。

【为什么不是malloc ? 因为其不能保障断电安全】

7.3  分组操作

除了修改符合处理器字的单个标量值外,一系列数据修改必须组合在一起,并附带在完成之前检测中断的方法。

7.4  内存池

内存映射文件是持久内存编程模型的核心。libpmemobj库提供了一个方便的API来轻松管理池的创建和访问,避免了直接映射和同步数据的复杂性。PMDK还提供了pmempool实用程序,用于从命令行管理内存池。内存池位于DAX装载的文件系统上。【DAX-mounted,指内存映射文件,以DAX直接访问方式mount到文件系统中】

7.4.1 创建内存池

使用pmempool实用程序创建用于应用程序的持久内存池。可以创建几种池类型,包括pmemblk、pmemlog和pmemobj。在应用程序中使用libpmemobj时,需要创建obj(pmemobj)类型的池。有关所有可用的命令和选项,请参阅pmempool create(1)手册页。以下示例仅供参考:

使用pmempool工具可以创建用于应用程序的持久内存池。可以创建几种池类型,包括pmemblk、pmemlog和pmemobj。当在应用中使用libpmemobj时,要创建obj(pmemobj)类型的内存池。有关所有可用的命令和选项,请参阅:pmempool-create(1) 手册页。以下示例仅供参考:

例1. 创建一个允许的最小尺寸的libpmemobj (obj)类型的池子,其布局取名为my_layout,位置在已挂载的文件系统:/mnt/pmemfs0/,命令如下:

$ pmempool create --layout my_layout obj /mnt/pmemfs0/pool.obj

例2. 创建一个20GiB的libpmemobj (obj)类型的池子,其布局取名为my_layout,位置在已挂载的文件系统:/mnt/pmemfs0/,命令如下:

$ pmempool create --layout my_layout –-size 20G obj \ /mnt/pmemfs0/pool.obj

例3. 创建一个使用所有可用空间的libpmemobj (obj)类型的池子,其布局取名为my_layout,位置在已挂载的文件系统:/mnt/pmemfs0/,命令如下:

$ pmempool create --layout my_layout –-max-size obj \ /mnt/pmemfs0/pool.obj

应用也可以在启动时通过使用pmemobj_create()函数创建池子。 pmemobj_create() 参数如下:

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

  1. Path,指定被创建的内存池文件名称,可以是全路径,也可以是相对路径;
  2. Layout,指定应用的布局类型,使用字符串来标识池子;
  3. Poolsize,指定池子所需大小。池子被使用posix_fallocate(3)来分配指定的全尺寸。最小尺寸是由<libpmemobj.h>的PMEMOBJ_MIN_POOL定义的。如果池子已经存在,pmemobj_create() 将返回 EEXISTS 错误。指定poolsize为0,将从文件大小来取pool size,并将通过搜索在文件开始位置的池子头中的任何非0数据来验证是文件否为空;
  4. Mode,指定创建的文件的ACL访问权限,具体描述见create(2)。

Listing 7-1 展示了如何使用pmemobj_create()创建一个池子

Listing 7-1. pwriter.c – An example showing how to create a pool using pmemobj_create()
33  /*    
34   * pwriter.c -  Write a string to a    
35   *              persistent memory pool    
36   */    
37
38  #include <stdio.h>    
39  #include <string.h>    
40  #include <libpmemobj.h>    
41    
42  #define LAYOUT_NAME "rweg"    
43  #define MAX_BUF_LEN 31    
44    
45  struct my_root {    
46      size_t len;    
47      char buf[MAX_BUF_LEN];    
48  };    
49    
50  int    
51  main(int argc, char *argv[])    
52  {    
53      if (argc != 2) {    
54          printf("usage: %s file-name\n", argv[0]);    
55          return 1;    
56      }    
57    
58      PMEMobjpool *pop = pmemobj_create(argv[1],    
59          LAYOUT_NAME, PMEMOBJ_MIN_POOL, 0666);    
60    
61      if (pop == NULL) {    
62          perror("pmemobj_create");    
63          return 1;    
64      }    
65    
66      PMEMoid root = pmemobj_root(pop,    
67          sizeof(struct my_root));    
68    
69      struct my_root *rootp = pmemobj_direct(root);    
70    
71      char buf[MAX_BUF_LEN] = "Hello PMEM World";    
72
73      rootp->len = strlen(buf);    
74      pmemobj_persist(pop, &rootp->len,    
75          sizeof(rootp->len));    
76    
77      pmemobj_memcpy_persist(pop, rootp->buf, buf,    
78          rootp->len);    
79    
80      pmemobj_close(pop);    
81    
82      return 0;    
83  }
  • 第42行: 定义了我们的池子的名字为“rweg” (read- write example)。这只是个名字,可以是任何字符串,但要唯一。 NULL 值是有效的。当多个内存池被打开时,依靠此名字来识别;
  • 第43行: 定义写buffer的最大长度;
  • 第45-47行: 定义root对象数据结构,其成员有len和buffer;buf将容纳我们想写的字符串,len是buffer的长度;
  • 第53- 56行: pwriter命令接受一个参数:要写入的path和pool的名称。例如, /mnt/pmemfs0/helloworld_ obj.pool。文件扩展名可任意或者不选;
  • 第58-59行: 调用pmemobj_create() 来创建内存池,把来自命令行参数文件名、布局名称、object pool类型的最小大小,以及权限0666当作参数。无法创建比PMEMOBJ_MIN_POOL更小或者比文件系统可用空间更大的池子。因为我们例子中的字符串非常小,所以只需要最小的尺寸就可以。如果成功, pmemobj_create() 返回PMEMobjpool 类型的pool object pointer (POP) 针对,我们可以用来获取指向root对象的指针;
  • 第61-64行: 如果pmemobj_create() 失败,我们将退出程序并返回一个错误;
  • 第66行: 使用58行返回的指针,调用pmemobj_ root() 定位到root对象;
  • 第69行: 使用pmemobj_direct()获得root对象的指针;
  • 第71行: 使用 “Hello PMEM World.”设置string/buffer;
  • 第73-78行. 确定buffer长度后,首先root对象的成员len,然后写成员buf,到持久内存;
  • 第80行: 最后关闭持久内存内存池(通过unmapping它)。

7.4.2 池对象指针POP和Root对象

因为大多数操作系统address space layout randomization (ASLR)地址空间布局随机化,所以内存池的位置,在多次执行或系统重启后,再次内存映射到应用地址空间时会不同。没有方法可以访问内存池中的数据,你会发现在内存池中定位数据是一个很大的挑战。基于PMDK的内存池通过加入少量的元数据来解决此问题。每一个pmemobj (obj)类型的内存池都有一个Root对象。Root对象是必需的,因为它是查找此内存池中创建的所有其他对象的入口点,所有其他对象就是数据。应用使用特别的对象称为pool object pointer (POP),来定位Root对象。POP对象存放在易失性内存中,通过程序调用来创建。它记录内存池相关的元数据,如:Root对象在内存池中的偏移量。图Figure 7-1描绘了POP和内存池布局:

图7-1 持久内存池高层视图—池对象指针(POP)指向root对象

可以调用pmemobj_root()来获得一个有效的指向Root对象的指针。在内部,该函数通过将当前映射内存池的内存地址加上Root的内部偏移创建一个有效地指针。

7.4.3 打开并读取内存池

使用pmemobj_create()创建内存池,使用pmemobj_open()打开已存在的内存池。这两个函数都返回一个 PMEMobjpool *pop 指针。Listing 7-1中的pwriter 例子展示了如何创建内存池并写一个字符串进去。清单Listing 7-2 展示了如何打开这个内存池并读取、显示该串。

Listing 7-2. preader.c – An example showing how to open a pool and access the root object and data
33  /*    
34   * preader.c -  Read a string from a    
35   *              persistent memory pool    
36   */    
37    
38  #include <stdio.h>    
39  #include <string.h>    
40  #include <libpmemobj.h>    
41
42  #define LAYOUT_NAME "rweg"    
43  #define MAX_BUF_LEN 31    
44    
45  struct my_root {    
46      size_t len;    
47      char buf[MAX_BUF_LEN];    
48  };    
49    
50  int    
51  main(int argc, char *argv[])    
52  {    
53      if (argc != 2) {    
54          printf("usage: %s file-name\n", argv[0]);    
55          return 1;    
56      }    
57    
58      PMEMobjpool *pop = pmemobj_open(argv[1],    
59          LAYOUT_NAME);    
60    
61      if (pop == NULL) {    
62          perror("pmemobj_open");    
63          return 1;    
64      }    
65    
66      PMEMoid root = pmemobj_root(pop,    
67          sizeof(struct my_root));    
68      struct my_root *rootp = pmemobj_direct(root);    
69    
70      if (rootp->len == strlen(rootp->buf))    
71          printf("%s\n", rootp->buf);    
72    
73      pmemobj_close(pop);    
74    
75      return 0;    
76  }
  • 第42-48行: 使用pwriter.c中声明的相同的数据结构。实践上,应该在一个头文件中声明,以保障一致性;
  • 第58行: 打开内存池并返回一个POP指针;
  • 第66行: pmemobj_root() 返回一个root对象句柄;
  • 第68行: pmemobj_direct() 返回root对象的指针;
  • 第70-71行: 确定rootp->buf 的长度;如果匹配我们要写的buffer的长度,buffer内存打印到STDOUT。

7.5  内存池集

多个内存池的容量可以组合成一个池集。除了能够提升可用空间,池集还用于跨多个内存池设备,以及提供本地和远程复制。

可以像使用单池一样使用pmemobj_open()打开一个池集。(在发布时,不能使用pmemobj_create() 和 pmempool工具创建池集)尽管创建池集需要手工操作,但池集可以通过libpmempool或者pmempool工具自动管理,更详细信息见poolset(5)手册。

7.5.1 连接池集

在单一或者多个文件系统上的独立内存池可以连接在一起。但这些内存池的类型必须一致,同为block、object、或者log 类型。清单Listing 7- 3 展示了一个“myconcatpool.set” 池集文件,其连接了三个小内存池,形成一个大内存池。为了演示的目的,每个内存池采用了不同的大小,并且位于不同的文件系统。使用池集的应用将看到一个单个的700GiB的内存池。

Listing 7-3. myconcatpool.set – An example of a concatenated poolset created from three individual pools on three different file systems
PMEMPOOLSET OPTION NOHDRS 
100G /mountpoint0/myfile.part0 
200G /mountpoint1/myfile.part1 
400G /mountpoint2/myfile.part2

注意:part0中的数据会保留,而part1、part2中的数据会丢失。所以建议只添加新的或者空的内存池到池集。

7.5.2 复制池集

除了绑定多个内存池来提供更多空间,池集也可以维护相同数据的多个副本以提升弹性。数据可以复制到本机的其他池集上或者远程主机上的池集上。

清单Listing 7-4展示了一个叫“myreplicatedpool.set”的池集,把数据从/mnt/pmem0/pool1 池复制到本主机上的在不同的文件系统下的/mnt/pmem1/pool1,以及在远程主机example.com 上的池集remote-objpool.set。

Listing 7-4. myreplicatedpool.set – An example demonstrating how to replicate local data locally and remote host

PMEMPOOLSET 
256G /mnt/pmem0/pool1

REPLICA 
256G /mnt/pmem1/pool1

REPLICA 
user@example.com remote-objpool.set

Librpmem库,一个远程持久内存支持库,是本特性的基础。第18章讨论了librpmem和复制池子的更多细节。

7.6  管理内存池和池集

Pmempool 工具有几个特性对于开发者和系统管理员很有用。由于每个命令都在手册中有详细描述,这里就不详细描述了:

  1. pmempool info 以人类可读格式打印指定内存池的信息和统计信息;
  2. pmempool check 检查内存池的一致性,并且当不一致时修复它;
  3. pmempool create 创建指定类型的内存池,并可指定该内存池的属性;
  4. pmempool dump 以16进制、二进制格式从内存池中导出可用数据;
  5. pmempool rm 移除内存池文件或者池集配置文件中的所有池文件;
  6. pmempool convert 更新内存池到最近的可用布局版本;
  7. pmempool sync 在池集内同步复制;
  8. pmempool transform 修改池集的内部结构;
  9. pmempool feature 切换或者查询池集特性。

7.7  类型对象标识(TOIDs)

当我们向持久内存池或者设备写入数据时,我们以物理地址提交。由于操作系统的ASLR特性,当应用打开一个内存池并映射到地址空间时,虚拟地址每次都会变化。由于此原因,句柄(指针)类型不能改变;该句柄称作OID(object identifier对象标识)。在内部,它是一对值:内存池或者池集的唯一标识UUID(unique identifier)和在内存池或者池集中的偏移量。OID可以在持久化形式和指针间来回转换,指针非常适合在你的程序中直接使用。

在低层,通过函数手动转换,如:清单Listing 7-2 preader.c例子中使用的pmemobj_direct()函数。因为手动转换需要显式类型转换,并且容易出错,我们建议为每个对象加上类型标识。这会使一些类型形式更安全,可以在编译时期进行检查,这要感谢宏。

例如:一个通过TOID(struct foo) x 声明的持久变量,可以通过 D_RO(x)->field来读取。在如下布局的内存池中:

POBJ_LAYOUT_BEGIN(cathouse); 
POBJ_LAYOUT_TOID(cathouse, struct canaries); 
POBJ_LAYOUT_TOID(cathouse, int); 
POBJ_LAYOUT_END(cathouse);

第一行中声明的属性val,可以通过下面三个操作的任意一个访问:

TOID(int) val; 
TOID_ASSIGN(val, oid_of_val); // Assigns 'oid_of_val' to typed OID 'val' 
D_RW(val) = 42; // Returns a typed write pointer to 'val' and writes 42 
return D_RO(val); // Returns a typed read-only (const) pointer to 'val'

7.8  分配内存

C开发者通常使用malloc()来分配内存,不能自动管理内存分配与释放。对于持久内存,你可以使用pmemobj_alloc()、pmemobj_reserve()、或者 pmemobj_ xreserve()来为临时对象保留内存,使用方式与malloc()相同。当应用不再需要时,为了避免运行时内存泄露,我们建议你使用pmemobj_free() 或者POBJ_FREE()来释放分配的内存。因为这些是临时内存分配,在crash后应用优雅的退出后,他们不会引起持久内存泄露。

7.9  持久化数据

使用持久内存的典型目的是永久保存数据。因此,你需要使用libpmemobj提供的下面三个API之一:

  1. 原子操作;
  2. 预订/发布;
  3. 事务。

7.9.1 原子操作

下面所展示的pmemobj_alloc()及其变种非常易于使用,但功能有限,需要开发者自己实现额外的功能。

1)	int pmemobj_alloc(PMEMobjpool *pop, PMEMoid *oidp,    size_t size, uint64_t type_num, pmemobj_constr    constructor, void *arg); 
2)	int pmemobj_zalloc(PMEMobjpool *pop, PMEMoid *oidp,    size_t size, uint64_t type_num); 
3)	void pmemobj_free(PMEMoid *oidp);
4)	int pmemobj_realloc(PMEMobjpool *pop, PMEMoid *oidp,    size_t size, uint64_t type_num); 
5)	int pmemobj_zrealloc(PMEMobjpool *pop, PMEMoid *oidp,    size_t size, uint64_t type_num); 
6)	int pmemobj_strdup(PMEMobjpool *pop, PMEMoid *oidp,    const char *s, uint64_t type_num); 
7)	int pmemobj_wcsdup(PMEMobjpool *pop, PMEMoid *oidp,    const wchar_t *s, uint64_t type_num);

基于TOID的包装器包含了这些函数:

1)	POBJ_NEW(PMEMobjpool *pop, TOID *oidp, TYPE,    pmemobj_constr constructor, void *arg) 
2)	POBJ_ALLOC(PMEMobjpool *pop, TOID *oidp, TYPE, size_t size,    pmemobj_constr constructor, void *arg) 
3)	POBJ_ZNEW(PMEMobjpool *pop, TOID *oidp, TYPE) POBJ_ZALLOC(PMEMobjpool *pop, TOID *oidp, TYPE, size_t size)
4)	POBJ_REALLOC(PMEMobjpool *pop, TOID *oidp, TYPE, size_t size) POBJ_ZREALLOC(PMEMobjpool *pop, TOID *oidp, TYPE, size_t size) POBJ_FREE(TOID *oidp)

这些函数将对象保留在临时状态,调用您提供的构造函数,然后在一个原子操作中,将分配标记为持久性。它们将把指向新初始化对象的指针插入到您选择的变量中。

如果新对象仅仅需要被调零,pmemobj_zalloc()就可以了,不需要提供构造方法。

因为复制NULL结尾的字符串是一个常用操作,libpmemobj提供了pmemobj_strdup()函数和宽字符版本pmemobj_wcsdup()来处理此问题。除了操作是在内存池的持久内存堆上外,pmemobj_strdup() 在语义上与strdup(3)相同。

一旦用完了一个对象,清零它所存指针指向的变量,调用pmemobj_free()释放该对象。pmemobj_free()函数释放oidp代表的内存空间,该内存空间必须是先前调用pmemobj_alloc()、pmemobj_xalloc()、pmemobj_zalloc()、pmemobj_realloc(),或者 pmemobj_zrealloc()分配的。pmemobj_free()函数提供与free(3)相同的语义,除了以下不同:free(3)在系统提供的进程堆上执行,而该函数是在持久内存堆上执行。

清单 Listing 7-5 展示了一个使用libpmemobj API分配和释放内存的小例子。

Listing 7-5. Using pmemobj_alloc() to allocate memory and using pmemobj_ free() to free it
33  /*    
34   * pmemobj_alloc.c - An example to show how to use    
35   *                   pmemobj_alloc()    
36   */    ..    
47  typedef uint32_t color;    
48    
49  static int paintball_init(PMEMobjpool *pop,    
50          void *ptr, void *arg)    
51  {    
52      *(color *)ptr = time(0) & 0xffffff;
53      pmemobj_persist(pop, ptr, sizeof(color));    
54      return 0;    
55  }    
56    
57  int main()    
58  {    
59      PMEMobjpool *pool = pmemobj_open(POOL, LAYOUT);    
60      if (!pool) {    
61          pool = pmemobj_create(POOL, LAYOUT,    
62          PMEMOBJ_MIN_POOL, 0666);    
63          if (!pool)    
64              die("Couldn't open pool: %m\n");    
65    
66      }    
67      PMEMoid root = pmemobj_root(pool,    
68              sizeof(PMEMoid) * 6);    
69      if (OID_IS_NULL(root))    
70          die("Couldn't access root object.\n");    
71    
72      PMEMoid *chamber = (PMEMoid *)pmemobj_direct(root)    
73          + (getpid() % 6);    
74      if (OID_IS_NULL(*chamber)) {    
75          printf("Reloading.\n");    
76          if (pmemobj_alloc(pool, chamber, sizeof(color)    
77              , 0, paintball_init, 0))    
78              die("Failed to alloc: %m\n");    
79      } else {    
80          printf("Shooting %06x colored bullet.\n",    
81          *(color *)pmemobj_direct(*chamber));    
82          pmemobj_free(chamber);    
83      }    
84    
85      pmemobj_close(pool);    
86      return 0;    
87  }
  • 第47行: 定义了一个color,将要存储在池子中;
  • 第49-54行: paintball_init() 函数当我们分配内存时(76行)被调用。该函数获取池子和对象的指针,为彩弹color计算出一个随机16进制值,并持久化写入池中。写完后程序退出;
  • 第59-70行: 打开或者创建一个池子,并获取池中roo对象的指针;
  • 第72行: 获取池中指向偏移量的指针;
  • 第74-78行: 如果72行的指针是一个无效对象,我们调用paintball_init()分配空间;
  • 第79-80行: 如果72行的指针是一个有效对象,我们读取color值,打印字符串,然后释放该对象。

7.9.2 预订/发布API

原子分配API在下面情况下没有什么帮助:

  1. 需要被更新的对象有不只一个引用;
  2. 有多个标量需要被更新。

例如:你的程序需要从A帐户减钱加到帐户B,这些操作必须一起完成。这可以通过预订/发布 API来完成。为了使用它,你需要给出所有要完成的操作列表。这些操作使用pmemobj_set_value()设定一个64位的标量值,使用pmemobj_ defer_free()释放,或者使用pmemobj_reserve()为它分配。这些操作只有分配是立即发生,让你为新的预订对象做任何初始化。在pmemobj_publish()调用之前,修改不会被持久化。Libpmemobj提供的与预订/发布有关的函数是:

1)	PMEMoid pmemobj_reserve(PMEMobjpool *pop,    struct pobj_action *act, size_t size, uint64_t type_num); 
2)	void pmemobj_defer_free(PMEMobjpool *pop, PMEMoid oid, struct pobj_action *act); 
3)	void pmemobj_set_value(PMEMobjpool *pop,    struct pobj_action *act, uint64_t *ptr, uint64_t value); 
4)	int pmemobj_publish(PMEMobjpool *pop,    struct pobj_action *actv, size_t actvcnt); 
5)	void pmemobj_cancel(PMEMobjpool *pop,    struct pobj_action *actv, size_t actvcnt);

清单Listing 7-6 是一个简单的银行例子,演示了如何在发布这些更新到内存池前,改变多个标量(帐户余额)。

Listing 7-6. Using the reserve/publish API to modify bank account balances
32    
33  /*    
34   * reserve_publish.c – An example using the    
35   *                     reserve/publish libpmemobj API    
36   */    
37    ..    
44  #define POOL "/mnt/pmem/balance"    
45    
46  static PMEMobjpool *pool;    
47    
48  struct account {    
49      PMEMoid name;    
50      uint64_t balance;    
51  };    
52  TOID_DECLARE(struct account, 0);    
53    ..    
60  static PMEMoid new_account(const char *name,    
61                  int deposit)    
62  {    
63      int len = strlen(name) + 1;    
64    
65      struct pobj_action act[2];
66      PMEMoid str = pmemobj_reserve(pool, act + 0,    
67                      len, 0);    
68      if (OID_IS_NULL(str))    
69          die("Can't allocate string: %m\n");    ..    
75      pmemobj_memcpy(pool, pmemobj_direct(str), name,    
76                      len, PMEMOBJ_F_MEM_NODRAIN);    
77      TOID(struct account) acc;    
78      PMEMoid acc_oid = pmemobj_reserve(pool, act + 1,    
79                      sizeof(struct account), 1);    
80      TOID_ASSIGN(acc, acc_oid);    
81      if (TOID_IS_NULL(acc))    
82          die("Can't allocate account: %m\n");    
83      D_RW(acc)->name = str;    
84      D_RW(acc)->balance = deposit;    
85      pmemobj_persist(pool, D_RW(acc),    
86                      sizeof(struct account));    
87      pmemobj_publish(pool, act, 2);    
88      return acc_oid;    
89  }    
90    
91  int main()    
92  {    
93      if (!(pool = pmemobj_create(POOL, " ",    
94                             PMEMOBJ_MIN_POOL, 0600)))    
95          die("Can't create pool "%s": %m\n", POOL);    
96    
97      TOID(struct account) account_a, account_b;    
98      TOID_ASSIGN(account_a,    
99                    new_account("Julius Caesar", 100));   
100      TOID_ASSIGN(account_b,   
101                    new_account("Mark Anthony", 50));   
102   
103      int price = 42;   
104      struct pobj_action act[2];
105      pmemobj_set_value(pool, &act[0],   
106                      &D_RW(account_a)->balance,   
107                      D_RW(account_a)->balance – price);   
108      pmemobj_set_value(pool, &act[1],   
109                      &D_RW(account_b)->balance,   
110                      D_RW(account_b)->balance + price);   
111      pmemobj_publish(pool, act, 2);   
112   
113      pmemobj_close(pool);   
114      return 0;   
115  }
  • 第44行: 定义了内存池的位置;
  • 第48-52行: 声明了一个帐户数据结构,有name 和 balance 两个成员;
  • 第60-89行: 函数new_account()预订了内存(66行和78行),更新name和balance(83行和84行),持久化这些变化(85行),然后发布这些更新(87行);
  • 第93-95行: 创建一个新池,失败时退出;
  • 第97行: 声明两个account 实例;
  • 第98-101行: 用初始余额为每个用户创建一个新帐户;
  • 第103-111行: 从Julius Caesar’s 的帐户中减42,把Mark Anthony’s 帐户加42。修改在111行发布。

7.9.3 事务API

预订/发布API速度很快,但在写时不允许读数据。要想解决此问题,你需要使用事务API。

变量第一次被写时,必须显式地添加进事务。这通过pmemobj_tx_add_range()或者它的变种(xadd, _direct)来完成。方便的宏,如:TX_ADD() or TX_SET(),也可以执行相同的操作。Libpmemobj提供的基于事务的函数和宏有:

1)	int pmemobj_tx_add_range(PMEMoid oid, uint64_t off,    size_t size); 
2)	int pmemobj_tx_add_range_direct(const void *ptr, size_t size);
3)	TX_ADD(TOID o) 
4)	TX_ADD_FIELD(TOID o, FIELD) 
5)	TX_ADD_DIRECT(TYPE *p) 
6)	TX_ADD_FIELD_DIRECT(TYPE *p, FIELD)
7)	TX_SET(TOID o, FIELD, VALUE) 
8)	TX_SET_DIRECT(TYPE *p, FIELD, VALUE) 
9)	TX_MEMCPY(void *dest, const void *src, size_t num) 
10)	TX_MEMSET(void *dest, int c, size_t num)

事务可以分配完整的新对象,然后只在事务提交时持久化。这些函数包含:

1)	PMEMoid pmemobj_tx_alloc(size_t size, uint64_t type_num); 
2)	PMEMoid pmemobj_tx_zalloc(size_t size, uint64_t type_num); 
3)	PMEMoid pmemobj_tx_realloc(PMEMoid oid, size_t size,    uint64_t type_num); 
4)	PMEMoid pmemobj_tx_zrealloc(PMEMoid oid, size_t size,    uint64_t type_num); 
5)	PMEMoid pmemobj_tx_strdup(const char *s, uint64_t type_num); 
6)	PMEMoid pmemobj_tx_wcsdup(const wchar_t *s,    uint64_t type_num);

我们可以使用事务API重写清单Listing 7-6中的银行例子。大多数代码都保留了,当我们要从余额中添加或者减少的那部分代码除外;我们简要概括了一个事务中的这些更新,如清单Listing 7-7所示。

Listing 7-7. Using the transaction API to modify bank account balances
33  /*    
34   * tx.c - An example using the transaction API    
35   */    
36    ..
94  int main()    
95  {    
96      if (!(pool = pmemobj_create(POOL, " ",    
97                          PMEMOBJ_MIN_POOL, 0600)))    
98          die("Can't create pool "%s": %m\n", POOL);    
99   
100      TOID(struct account) account_a, account_b;   
101      TOID_ASSIGN(account_a,   
102                    new_account("Julius Caesar", 100));   
103      TOID_ASSIGN(account_b,   
104                    new_account("Mark Anthony", 50));   
105   
106      int price = 42;   
107      TX_BEGIN(pool) {   
108          TX_ADD_DIRECT(&D_RW(account_a)->balance);   
109          TX_ADD_DIRECT(&D_RW(account_b)->balance);   
110          D_RW(account_a)->balance -= price;   
111          D_RW(account_b)->balance += price;   
112      } TX_END   
113   
114      pmemobj_close(pool);   
115      return 0;   
116  }
  • 第107行: 启动事务;
  • 第108-111行: 修改多个帐户的余额;
  • 第112行: 完成事务;所有的更新要么全部完成,要么在事务未完成之前应用或者系统crash后回滚。

每个事务都有多个阶段,他们之间互相影响。这些事务阶段包括:

  • TX_STAGE_NONE: 该线程中没有打开的事务;
  • TX_STAGE_WORK: 事务处理过程中;
  • TX_STAGE_ONCOMMIT: 成功提交;
  • TX_STAGE_ONABORT: 事务开始提交失败或者放弃;
  • TX_STAGE_FINALLY: 准备清环境。

清单Listing 7-7 中的例子使用两个强制的阶段:TX_BEGIN 和 TX_END。然而,我们也可以很容易地添加其他阶段到执行的操作中,例如:

TX_BEGIN(Pop) {        
/* the actual transaction code goes here... */ 
} 
TX_ONCOMMIT {        
/*         
* optional - executed only if the above block         
* successfully completes         
*/ 
} 
TX_ONABORT {        
/*         
* optional - executed only if starting the transaction         
* fails, or if transaction is aborted by an error or a         
* call to pmemobj_tx_abort()         
*/ 
} 
TX_FINALLY {        
/*         
* optional - if exists, it is executed after         
* TX_ONCOMMIT or TX_ONABORT block         
*/
} TX_END /* mandatory */

作为可选项,你可以提供事务参数列表。每个参数包含一个类型,该类型是下面的类型值之一:

  • TX_PARAM_NONE 作为终结符标记使用,无需后跟值;
  • TX_PARAM_MUTEX 后跟一个值,pmem-resident PMEMmutex;
  • TX_PARAM_RWLOCK 后跟一个值, a pmem-resident PMEMrwlock;
  • TX_PARAM_CB 后跟两个值:pmemobj_tx_callback类型的回调函数和一个void指针。

使用TX_PARAM_MUTEX 或者 TX_PARAM_RWLOCK会引致在事务的开始获取一个特定的锁。TX_PARAM_RWLOCK 获取写锁。这是有保障的:pmemobj_tx_begin()在成功完成前获取到所有锁,并且被当前线程保持,直到最远的事务完成。锁以从左到右的顺序获取。为了避免死锁,你要保障合适的锁顺序。TX_PARAM_CB 注册了每个事务阶段被执行的指定的回调函数。对于TX_STAGE_WORK,在提交前回调函数被执行。对于所有其他阶段,回调函数在阶段变化后作为第一个操作被执行。

7.9.4 可选项标记

上面讨论的原子性、预订/发布、事务API,许多函数都有一个flags参数或变种,该参数取值:

  1. POBJ_XALLOC_ZERO 调零分配的对象;
  2. POBJ_XALLOC_NO_FLUSH 控制自动flush。你可以按预期以某种方式flush数据;否则,它可能在非预期断电情况下不能持久化。

7.9.5 持久化数据小结

原子性、预订/发布,以及事务API有不同的强度:

  1. 原子分配是最简单的、最快的,但只能用于分配和初始化全新的块;
  2. 预订/发布API,只有当所有涉及的操作是分配、释放整个对象,或者是修改标量值时,才能与原子分配一样快。其他情况下就会慢一些,但为了能达到写时可读,还是值得的;
  3. 事务API在变量加到事务时,都需要同步,所以慢。如果变量在事务中多次改变,后来的操作没有额外花费。它也可以方便的转换比单机器字更大的数据片。

7.10  libpmemobj’s APIs的保证

Libpmemobj提供的事务、原子分配、预订/发布 API 都提供了失效-安全保障的原子性和一致性。

事务API确保了加入到事务的对象的任何内存修改的持久性。一个例外是当使用POBJ_X***_ NO_FLUSH标记时,此种情况下应用要自己负责flush内存范围或者使用来自libpmemobj的类似memcpy的函数。no-flush标记不提供多线程间的任何隔离性,这意味着整个操作的部分写入立即对其他线程可见。

原子分配API需要应用通过对象的构造方法来flush写操作。如果操作成功,就确保了持久性。这是在线程间提供完全隔离的唯一API。

reserve/publish API 需要显示的flush 写到通过调用pmemobj_reserve()分配的内存块,通过 pmemobj_set_value() 完成flush写。线程间没有隔离,尽管直到pmemobj_ publish()开始还没有修改生效,但这期间允许你在发布阶段使用显示锁。

使用数据库的术语,这三种API提供的隔离级别为:

  • 事务API: READ_UNCOMMITTED;
  • 原子操作API: READ_COMMITTED;
  • 预定/发布API: READ_COMMITTED直到publish开始,然后是 READ_UNCOMMITTED。

7.11  管理库行为

pmemobj_set_funcs()允许应用重写libpmemobj内部使用的内存分配函数。当任一函数句柄传递为NULL时,libpmemobj 将使用默认函数。该库虽未大量使用系统的malloc()函数,但它确实为每个内存池分配了大约4-8K的空间。

默认情况下,libpmemobj 支持1024并发事务/分配。为了调试目的,可以通过设置PMEMOBJ_NLANES shell环境变量来降低并发数量到期望的限制。例如,在shell提示符下,执行”export PMEMOBJ_NLANES=512″,然后运行应用:

$ export PMEMOBJ_NLANES=512 
$ ./my_app

设置回默认行为,使用 unset PMEMOBJ_NLANES命令:

$ unset PMEMOBJ_NLANES

7.12  调试和错误处理

如果在调用libpmemobj函数时检测到错误,应用应使用pmemobj_ errormsg()检索描述失败原因的错误消息。该函数返回一个指向记录当前线程最近错误信息的静态缓存的指针。如果errno被设置,错误消息包含一个strerror(3)返回的对应errno的描述。错误消息缓存是线程本地化的;一个线程中遇到的错误不会影响其他线程。缓存永远不会被任何库函数清除;它的内容只有当前面调用的libpmemobj函数指示有错误或者设置了errno时才有意义。应用不能修改或者释放错误消息字符串,但它可以被后续调用的其他库函数所修改。

libpmemobj在开发系统上通常有两可用的版本。非调试版本为性能而优化,当一个程序link时使用了-lpmemobj 选项,则使用的是非调试版本。该库跳过了那些影响性能的检测,从不记录任何trace信息,也不执行任何运行时断言(assertions)。

Libpmemobj提供了调试版本,/usr/lib/pmdk_debug  或者 /usr/local/lib64/pmdk_debug。调试版本包含了运行时断言(assertions)和tracepoints(跟踪点)。通常使用调试版本的做法是设置环境变量LD_ LIBRARY_PATH。一个替代方法是,使用LD_PRELOAD指向/usr/lib/pmdk_debug或/usr/lib64/pmdk_debug,适情况而定。这些库可能在不同的位置,如/usr/local/lib/pmdk_debug和/usr/local/lib64/pmdk_debug,这取决于你的Linux发布版本,或者你是否从源码编译安装并选择/usr/local作为安装路径。下面的例子是一个等价方法,一个叫my_app的应用加载并使用libpmemobj的调试版本:

$ export LD_LIBRARY_PATH=/usr/lib64/pmdk_debug 
$ ./my_app

或者:

$ LD_PRELOAD=/usr/lib64/pmdk_debug ./my_app

调试库输出使用环境变量PMEMOBJ_LOG_LEVEL和PMEMOBJ_LOG_FILE来控制。这些变量在非调试版本上无效。

PMEMOBJ_LOG_LEVEL

PMEMOBJ_LOG_LEVEL启用调试版本的跟踪点(tracepoints),取值如下:

  1. 当PMEMOBJ_LOG_LEVEL 未设置时的默认值,此级别不发布任何日志信息;【理解是不记录日志】
  2. 检测到的任何错误都会被记录,并返回基于errno的错误。相同的信息可以使用pmemobj_errormsg()检索;【理解是error】
  3. 记录基本操作的trace信息;【理解是info】
  4. 开启库中广泛的函数调用trace;【理解是追踪到库的trace】
  5. 开启宽松的和相当模糊的trace信息,可能只对libpmemobj开发者有用。【理解是开发libpmemobj的工程师用的】

调试信息写到STDERR,除非PMEMOBJ_LOG_FILE被设置。设置调试级别,使用:

$ export PMEMOBJ_LOG_LEVEL=2 
$ ./my_app

PMEMOBJ_LOG_FILE

PMEMOBJ_LOG_FILE的值包含全路径和文件名称,所有日志信息将写入此文件中。如果PMEMOBJ_LOG_FILE没有被设置,日志输出到STDERR。下面的例子定义了日志位置/var/tmp/libpmemobj_debug.log,当我们在后台执行my_app时,要确保我们使用的是libpmemobj的调试版本,并设置调试日志级别为2,可使用tail -f实时监控日志。

$ export PMEMOBJ_LOG_FILE=/var/tmp/libpmemobj_debug.log 
$ export PMEMOBJ_LOG_LEVEL=2 $ LD_PRELOAD=/usr/lib64/pmdk_debug ./my_app & 
$ tail -f /var/tmp/libpmemobj_debug.log

如果调试文件名称的最后一个字符是”-“,当日志文件被创建时,当前进程的进程标识PID将被追加到文件名称尾部。当调试多个进程时这很有用。

7.13  本章小结

本章描述了libpmemobj库,设计用于简化持久内存编程。通过提供那些支持原子操作、事务和预订/发布特性的API,使得创建保证数据完整性的应用,更不易于出错。

作者: charlie_chen

编程是一生最爱: >> 架构与设计; >> 软件工程; >> 项目管理; >> 产品研发。
联系我们

联系我们

022-XXXXXXXX

在线咨询: QQ交谈

邮箱: 1549889473@qq.com

欢迎交流。
关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部