6 libpmem: 低层次持久内存支持
本章介绍libpmem,它是PMDK中最小的库。该C库非常低层,像CPU指令一样处理持久内存相关的事情,以最优方式copy数据到持久内存,以及进行文件映射。只想完全以原始方式访问持久内存的程序员,不需要提供分配器或者事务的库,只使用libpmem作为他们开发的基石。
Libpmem中检测CPU指令有效性的代码,是单调的样板代码,你无需在每个应用中都重写一遍。利用这些来自libpmem中的少量代码可以节省时间,这些代码还是经过充分测试和调优的。
对于大多数程序员来说,libpmem太低层,你可以快速浏览此章(或者跳过),把精力移向更高层,PMDK中更友好的库。处理持久化的所有PMDK库,如:libpmemobj,都构建在满足低层需求的libpmem之上。
像所有PMDK库一样,都提供在线手册。对于libpmem,地址:http://pmem.io/pmdk/libpmem/,该页上有Linux和Windows版本的手册链接。尽管PMDK项目的目的是提供跨操作系统的统一接口,但还是会有些小的不同。本章中的C代码样例可以在Linux和Windows上构建和运行。
本章样例有:
- simple_copy.c 是一个小程序,从源文件中copy 4k的块到持久内存上的文件中;
- full_copy.c 是一个更完整的copy程序,copy整个文件;
- manpage.c 是libpmem手册页中使用的简单示例。
6.1 使用库
为了使用 libpmem, 首先要包括所需的头文件,如列表Listing 6-1所示:
Listing 6-1. Including the libpmem headers
32
33 /*
34 * simple_copy.c
35 *
36 * usage: simple_copy src-file dst-file
37 *
38 * Reads 4KiB from src-file and writes it to dst-file.
39 */
40
41 #include <sys/types.h>
42 #include <sys/stat.h>
43 #include <fcntl.h>
44 #include <stdio.h>
45 #include <errno.h>
46 #include <stdlib.h>
47 #ifndef _WIN32
48 #include <unistd.h>
49 #else
50 #include <io.h>
51 #endif
52 #include <string.h>
53 #include <libpmem.h>
注意53行,使用libpmem,要使用此头文件,然后在linux下构建link 此C程序时还要使用 -lpmem 选项。
6.1 映射文件
Libpmem包含了一些内存文件映射的简便函数。当然,应用可以在linux上直接调用mmap() 或者在windows上直接调用 MapViewOfFile(), 但使用libpmem有以下优势:
- Libpmem知道操作系统调用映射时的正确参数。例如,在Linux上,直接使用CPU指令来将变更flush到持久内存是不安全的,除非映射创建时指定了MAP_SYNC标志;
- libpmem 检测映射是否真的映射到持久内存,以及直接使用CPU指令来flush是否是安全的。
Listing 6-2 展示了在持久内存-感知的文件系统上,如何内存映射文件到应用中
Listing 6-2. Mapping a persistent memory file
80 /* create a pmem file and memory map it */
81 if ((pmemaddr = pmem_map_file(argv[2], BUF_LEN,
82 PMEM_FILE_CREATE|PMEM_FILE_EXCL,
83 0666, &mapped_len, &is_pmem)) == NULL) {
84 perror("pmem_map_file");
85 exit(1);
86 }
对于前面提到的检测是否是映射到持久内存上,可以通过下面的方式来知晓:在调用pmem_map_file后,is_pmem标志会被返回;是否是真的映射到持久内存上,可以通过is_pmem来判断。使用该标志来决定如何flush变更到持久化,就是调用者的责任了。当使一个内存范围持久化时,只有当is_pmem标志被设置时,调用者才可以使用libpmem、pmem_persist提供的优化的flush操作。从手册中摘录的例子Listing 6-3说明了此点。
Listing 6-3. manpage.c: Using the is_pmem flag
74 /* Flush above strcpy to persistence */
75 if (is_pmem)
76 pmem_persist(pmemaddr, mapped_len);
77 else
78 pmem_msync(pmemaddr, mapped_len);
Listing 6-3 展示了pmem_msync()函数的方便性,它只是对msync()的小小包装,Windows也一样。所以你在Linux和Windows上的程序,不需要以不同的逻辑来构建,因为libpmem处理了它们之间的不同。
6.3 copy到持久内存
在libpmem中有几个接口,用来以最优方式copy或者清空一段持久内存。最简单的接口如清单Listing 6-4所示,从源文件copy一个数据块到持久内存中并且flush持久化。
Listing 6-4. simple_copy.c: Copying to persistent memory
88 /* read up to BUF_LEN from srcfd */
89 if ((cc = read(srcfd, buf, BUF_LEN)) < 0) {
90 pmem_unmap(pmemaddr, mapped_len);
91 perror("read");
92 exit(1);
93 }
94
95 /* write it to the pmem */
96 if (is_pmem) {
97 pmem_memcpy_persist(pmemaddr, buf, cc);
98 } else {
99 memcpy(pmemaddr, buf, cc);
100 pmem_msync(pmemaddr, cc);
101 }
注意96行is_pmem如何使用,本应调用pmem_persist(),却调用了pmem_memcpy_persist(),这是因为pmem_memcpy_persist()包含了flush持久化操作。
pmem_memcpy_persist()包含了持久化是因为它确定使用non-temporal存储更优,绕过CPU cache并且不需要为持久化执行后续的cache flush指令。通过使用提供的包括copy和flush的API,libpmem使用最优的方式来执行这些步骤。
6.4 分离Flush步骤
Flush到持久化设备包含两步:
- Flush CPU cache或者向前面例子一样整个绕过他们;
- 等待所有硬件缓存完成flush,确保都已经写到媒体。
当调用pmem_persist()时,这些步骤一起执行,或者第一步调用pmem_flush(),然后第二步调用pmem_drain()。注意这些步骤在所给的平台上不一定都需要,但该库知道如何检测并正确执行。例如,在Intel平台,pmem_drain是一个空函数。
什么时候分成多步?例子清单Listing 6-5 演示了这么做的一个理由。因为例子多次调用memcpy()来copy数据,它使用libpmem copy(pmem_memcpy_nodrain()),该方法只执行flush,将最终的drain抽干步骤延到最后。不像flush步骤,抽干步骤不能获取地址范围,所以它是一个系统范围内的抽干操作,发生在copy各数据块循环结尾处。
Listing 6-5. full_copy.c: Separating the flush steps
58 /*
59 * do_copy_to_pmem
60 */
61 static void
62 do_copy_to_pmem(char *pmemaddr, int srcfd, off_t len)
63 {
64 char buf[BUF_LEN];
65 int cc;
66
67 /*
68 * Copy the file,
69 * saving the last flush & drain step to the end
70 */
71 while ((cc = read(srcfd, buf, BUF_LEN)) > 0) {
72 pmem_memcpy_nodrain(pmemaddr, buf, cc);
73 pmemaddr += cc;
74 }
75
76 if (cc < 0) {
77 perror("read");
78 exit(1);
79 }
80
81 /* Perform final flush step */
82 pmem_drain();
83 }
在清单Listing 6-5中,pmem_memcpy_nodrain()是专门为持久内存设计的。当使用其他库和标准函数如memcpy()时,记住这些函数在持久内存存在之前就被实现完成了,所以不会执行任何flush操作到持久化设备。特别是通过C运行时环境提供的memcpy(),经常会在常规存储(需要flushing)和非临时性存储(不需要flushing)之间选择。这个选择基于性能,而不是持久化。因为你不知道它会选择哪个指令,所以你需要自己使用pmem_persist() 或者msync()来执行flush持久化。
当copy多个范围到持久内存时,在许多应用中,选择所用指令时更看中性能。持久内存范围的置零也是一样的。为了满足这些需求,libpmem 提供了 pmem_memmove()、pmem_memcpy(), 以及pmem_memset(),这些函数提供了一个标志参数,以使调用者可以对所使用的指令有更多的控制。例如,传给PMEM_F_MEM_NONTEMPORAL 标志,将告诉这些函数使用非临时存储代替选择那些基于范围大小的指令。标志完整清单见这些函数的手册。
6.5 本章小结
本章演示了libpmem提供的API的一个很小集。该库不会为你改变,不会提供电源失效-安全事务,不提供分配器。像libpmemobj库(下章描述)提供了所有这些任务,并内部使用libpmem以简单化flush和copy操作。