12 调试持久内存应用程序
持久内存编程带来了新的机会,使开发人员能够直接持久化数据结构而无需序列化,并在不涉及经典块I/O的情况下就地访问它们。因此,您可以合并数据模型并避免内存中数据之间的典型分割,这是一种易失性、快速和字节可寻址的方法–使用传统存储设备上的数据,这是非易失性的,但速度较慢。
持久内存编程也带来了挑战。回想一下我们在第2章中关于电源故障保护的持久性域的讨论:当进程或系统在启用异步DRAM刷新(ADR)的平台上崩溃时,驻留在CPU缓存中尚未刷新的数据将丢失。这不是易失性内存的问题,因为所有的内存层次结构都是易失性的。但是,对于持久性内存,崩溃可能会导致永久性数据损坏。你必须多久flush一次数据?flush频率过高会产生次优性能,而flush频率不足则可能导致数据丢失或损坏。
第11章描述了设计数据结构和使用诸如写时拷贝、版本控制和事务等方法来维护数据完整性的几种方法。持久内存开发工具包(PMDK)中的许多库提供数据结构和变量的事务性更新。当平台需要时,这些库会在正确的时间提供最佳的CPU缓存flush,因此您可以编程而不必担心硬件的复杂性。
这种编程范式引入了与错误和性能问题相关的新维度,程序员需要注意这些问题。PMDK库减少了持久内存编程中的错误,但它们不能消除这些错误。本章描述了常见的持久内存编程问题和陷阱,以及如何使用可用的工具纠正它们。本章的前半部分介绍了这些工具。下半部分介绍了几个错误的编程场景,并描述了如何使用这些工具在将代码发布到生产环境之前纠正错误。
12.1 pmemcheck for Valgrind
pmemcheck是由Intel开发的Valgrind(http://www.Valgrind.org/)工具。它与memcheck非常相似,memcheck是Valgrind中发现内存相关错误的默认工具,但pmemcheck适合持久内存。Valgrind是一个用于构建动态分析工具的工具框架。一些Valgrind工具可以自动检测许多内存管理和线程错误,并详细分析您的程序。你也可以使用Valgrind来构建新的工具。
要运行pmemcheck,您需要修改Valgrind版本,以支持新的CLFLUSHOPT和CLWB刷新指令。Valgrind的持久内存版本包括pmemcheck工具,可从https://github.com/pmem/Valgrind获得【已下载 valgrind-pmem-3.15.zip.zip】。有关安装说明,请参阅GitHub项目中的README.md。
PMDK中的所有库都已使用pmemcheck进行检测。如果使用PMDK进行持久内存编程,则可以使用pmemcheck轻松检查代码,而无需修改任何代码。
在讨论pmemcheck细节之前,下面两部分将演示它如何在越界和内存泄漏示例中识别错误。
12.1.1 堆栈溢出示例
越界的情况是堆栈/缓冲区溢出错误,其中数据的写入或读取超出了堆栈或数组的容量。考虑清单12-1中的小代码片段。
Listing 12-1. stackoverflow.c: Example of an out-of-bound bug
32 #include <stdlib.h>
33
34 int main() {
35 int *stack = malloc(100 * sizeof(int));
36 stack[100] = 1234;
37 free(stack);
38 return 0;
39 }
在第36行中,我们错误地将值1234分配给位置100,该位置不在0-99的数组范围内。如果我们编译并运行这段代码,它可能不会失败。这是因为,即使我们只为数组分配了400字节(100个整数),操作系统也提供了一个完整的内存页,通常是4KiB。执行Valgrind下的二进制文件会报告一个问题,如清单12-2所示。
Listing 12-2. Running Valgrind with code Listing 12-1
$ valgrind ./stackoverflow
==4188== Memcheck, a memory error detector
...
==4188== Invalid write of size 4
==4188== at 0x400556: main (stackoverflow.c:36)
==4188== Address 0x51f91d0 is 0 bytes after a block of size 400 alloc'd
==4188== at 0x4C2EB37: malloc (vg_replace_malloc.c:299)
==4188== by 0x400547: main (stackoverflow.c:35)
...
==4188== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
因为Valgrind可以生成长报告,所以我们只显示报告中相关的“无效写入”错误部分。使用符号信息(gcc -g)编译代码时,很容易看到代码中检测到错误的确切位置。在本例中,Valgrind突出显示stackoverflow.c文件的第36行。在代码中发现问题后,我们知道在哪里修复它。
12.1.2 内存泄露示例
内存泄漏是另一个常见问题。考虑清单12-3中的代码。
Listing 12-3. leak.c: Example of a memory leak
32 #include <stdlib.h>
33
34 void func(void) {
35 int *stack = malloc(100 * sizeof(int));
36 }
37
38 int main(void) {
39 func();
40 return 0;
41 }
内存分配被移到函数func()中。发生内存泄漏的原因是指向新分配内存的指针是第35行的局部变量,当函数返回时,该变量将丢失。在Valgrind下执行这个程序的结果如清单12-4所示。
Listing 12-4. Running Valgrind with code Listing 12-3
$ valgrind --leak-check=yes ./leak
==4413== Memcheck, a memory error detector
...
==4413== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==4413== at 0x4C2EB37: malloc (vg_replace_malloc.c:299)
==4413== by 0x4004F7: func (leak.c:35)
==4413== by 0x400507: main (leak.c:39)
==4413==
==4413== LEAK SUMMARY:
...
==4413== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Valgrind显示泄漏时分配的内存丢失400字节在leak.c:35行。要了解更多信息,请访问Valgrind官方文档(http://www.Valgrind.org/docs/manual/index.html)。
12.2 Intel检查器 – 持久性检查器
Intel Inspector–Persistence Inspector是一个运行时工具,开发人员使用它来检测持久内存程序中的编程错误。除了遗漏刷新缓存之外,此工具还检测:
- Redundant cache flushes and memory fences 冗余缓存flush和内存fence;
- Out-of-order persistent memory stores 无序的持久内存存储;
- Incorrect undo logging for the PMDK PMDK的撤销日志不正确。
持久性检查器是Intel检查器的一部分,它是一个易于使用的内存和线程错误调试器,适用于C、C++和FORTRAN,适用于Windows和Linux操作系统。它有直观的图形界面和命令行界面,可以与Microsoft Visual Studio集成。Intel Inspector是Intel Parallel Studio XE(https://software.intel.com/en-us/parallel-studio-xe)和Intel System Studio(https://software.intel.com/en-us/system-studio)的一部分。
本节介绍Intel Inspector工具如何处理清单12-1和12-3中相同的越界和内存泄漏。
12.2.1 堆栈溢出示例
清单12-5示例演示了如何使用命令行界面执行分析和收集数据,然后切换到GUI来详细检查结果。为了收集数据,我们使用inspxe-cl实用程序和-c=mi2收集选项来检测内存问题。
Listing 12-5. Running Intel Inspector with code Listing 12-1
$ inspxe-cl -c=mi2 -- ./stackoverflow
1 new problem(s) found
1 Invalid memory access problem(s) detected
Intel Inspector创建一个包含数据和分析结果的新目录,并将结果摘要打印到终端。对于stackoverflow应用程序,它检测到一个无效的内存访问。
在使用inspxe-gui启动GUI之后,我们通过File ➤ Open ➤ Result菜单打开results集合,并导航到inspxe cli创建的目录。如果是第一次运行,目录将命名为r000mi2。目录中有一个名为r000mi2.inspxe的文件。一旦打开并处理,GUI就会显示如图12-1所示的数据。
GUI默认使用Summary选项卡来提供分析的概述。由于我们用符号编译程序,底部的“Code Locations”面板显示了代码中检测到问题的确切位置。Intel Inspector在Valgrind发现的第36行发现了相同的错误。
如果Intel Inspector在程序中检测到多个问题,则这些问题将列在窗口左上角的“Problems”部分。您可以选择每个问题并在窗口的其他部分中查看与之相关的信息。
12.2.2 内存泄露示例
清单12-6的示例使用清单12-2中的leak.c代码运行Intel Inspector,并使用stackoverflow程序中的相同参数来检测内存问题。
Listing 12-6. Running Intel Inspector with code Listing 12-2
$ inspxe-cl -c=mi2 -- ./leak
1 new problem(s) found
1 Memory leak problem(s) detected
Intel Inspector输出如图12-2所示,并说明检测到内存泄漏问题。当我们在GUI中打开r001mi2/r001mi2.inspxe结果文件时,我们得到了类似于图12-2左下部分所示的结果。
与泄漏对象相关的信息显示在代码列表上方:
- Allocation site (source, function name, and module) 分配地点(来源、功能名称和模块);
- Object size (400 bytes)对象大小(400字节);
- The variable name that caused the leak 导致泄漏的变量名。
代码面板的右侧显示了导致错误的调用堆栈(调用堆栈从下到上读取)。我们在第39行的main() 函数中看到对func() 的调用(leak.c:39),然后在第35行的func() 中进行内存分配(leak.c:35)。
Intel Inspector提供的比我们在这里介绍的多得多。要了解更多信息,请访问文档(https://software.intel.com/en-us/intelinsbart-support/documentation)。
12.3 常见的持久内存编程问题
本节回顾您可能遇到的几个编码和性能问题,如何使用pmemcheck和Intel Inspector工具捕捉它们,以及如何解决这些问题。
我们使用的工具强调了代码中故意添加的问题,这些问题可能导致错误、数据损坏或其他问题。对于pmemcheck,我们将演示如何绕过工具不应检查的数据节,并使用宏帮助工具更好地理解我们的意图。
12.3.1 非持久性储存
问题:非持久性存储引用写入持久性内存但未显式flush的数据。可以理解为,如果程序写入持久内存,它希望这些写入是持久的。如果程序结束时没有显式flush写操作,则可能会出现数据损坏。当程序正常退出时,CPU缓存中的所有挂起写操作都会自动flush。但是,如果程序意外崩溃,仍然驻留在CPU缓存中的写操作可能会丢失。
考虑清单12-7中的代码,该代码将数据写入挂载到/mnt/pmem的持久性内存设备,而不flush数据。
Listing 12-7. Example of writing to persistent memory without flushing
32 #include <stdio.h>
33 #include <sys/mman.h>
34 #include <fcntl.h>
35
36 int main(int argc, char *argv[]) {
37 int fd, *data;
38 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
39 posix_fallocate(fd, 0, sizeof(int));
40 data = (int *) mmap(NULL, sizeof(int), PROT_READ |
41 PROT_WRITE, MAP_SHARED_VALIDATE |
42 MAP_SYNC, fd, 0);
43 *data = 1234;
44 munmap(data, sizeof(int));
45 return 0;
46 }
- 第38行: 打开文件 /mnt/pmem/file;
- 第39行: 我们通过调用posix_fallocate()来确保文件中有足够的空间来分配整数;
- 第40行: 我们内存映射文件/mnt/pmem/file;
- 第43行: 写1234到内存;
- 第44行: 取消内存映射。
如果使用清单12-7运行pmemcheck,我们将无法获得任何有用的信息,因为pmemcheck无法知道哪些内存地址是持久的,哪些是不稳定的。这在将来的版本中可能会改变。要运行pmemcheck,我们将–tool=pmemcheck参数传递给valgrind,如清单12-8所示。结果显示未检测到问题。
Listing 12-8. Running pmemcheck with code Listing 12-7
$ valgrind --tool=pmemcheck ./listing_12-7
==116951== pmemcheck-1.0, a simple persistent store checker
==116951== Copyright (c) 2014-2016, Intel Corporation
==116951== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==116951== Command: ./listing_12-9
==116951==
==116951==
==116951== Number of stores not made persistent: 0
==116951== ERROR SUMMARY: 0 errors
我们可以使用清单12-9的第52行所示的VALGRIND_PMC_REGISTER_PMEM_MAPPING宏通知pmemcheck哪些内存区域是持久的。我们必须包括pmemcheck的valgrind/pmemcheck.h头,第36行,它定义了VALGRIND_ PMC_REGISTER_PMEM_MAPPING宏和其他宏。
Listing 12-9. Example of writing to persistent memory using Valgrind macros without flushing
33 #include <stdio.h>
34 #include <sys/mman.h>
35 #include <fcntl.h>
36 #include <valgrind/pmemcheck.h>
37
38 int main(int argc, char *argv[]) {
39 int fd, *data;
40
41 // open the file and allocate enough space for an
42 // integer
43 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
44 posix_fallocate(fd, 0, sizeof(int));
45
46 // memory map the file and register the mapped
47 // memory with VALGRIND
48 data = (int *) mmap(NULL, sizeof(int),
49 PROT_READ|PROT_WRITE,
50 MAP_SHARED_VALIDATE | MAP_SYNC,
51 fd, 0);
52 VALGRIND_PMC_REGISTER_PMEM_MAPPING(data,
53 sizeof(int));
54
55 // write to pmem
56 *data = 1234;
57
58 // unmap the memory and un-register it with
59 // VALGRIND
60 munmap(data, sizeof(int));
61 VALGRIND_PMC_REMOVE_PMEM_MAPPING(data,
62 sizeof(int));
63 return 0;
64 }
我们使用VALGRIND_PMC_REMOVE_PMEM_MAPPING宏从pmemcheck中删除持久内存映射标识。如前所述,当您希望从分析中排除部分持久内存时,这非常有用。清单12-10显示了使用清单12-9中修改后的代码执行pmemcheck,该代码现在报告了一个问题。
Listing 12-10. Running pmemcheck with code Listing 12-9
$ valgrind --tool=pmemcheck ./listing_12-9
==8904== pmemcheck-1.0, a simple persistent store checker
...
==8904== Number of stores not made persistent: 1
==8904== Stores not made persistent properly:
==8904== [0] at 0x4008B4: main (listing_12-9.c:56)
==8904== Address: 0x4027000 size: 4 state: DIRTY
==8904== Total memory not made persistent: 4
==8904== ERROR SUMMARY: 1 errors
请参阅pmemcheck检测到在写入清单12-9.c的第56行之后没有flush数据。为了解决这个问题,我们创建了一个新的flush() 函数,接受地址和大小,使用CLFLUSH machine指令(umclflush())刷新存储数据任何部分的所有CPU缓存线。清单12-11显示了修改后的代码。
Listing 12-11. Example of writing to persistent memory using Valgrind with flushing
33 #include <emmintrin.h>
34 #include <stdint.h>
35 #include <stdio.h>
36 #include <sys/mman.h>
37 #include <fcntl.h>
38 #include <valgrind/pmemcheck.h>
39
40 // flushing from user space
41 void flush(const void *addr, size_t len) {
42 uintptr_t flush_align = 64, uptr;
43 for (uptr = (uintptr_t)addr & ~(flush_align - 1);
44 uptr < (uintptr_t)addr + len;
45 uptr += flush_align)
46 _mm_clflush((char *)uptr);
47 }
48
49 int main(int argc, char *argv[]) {
50 int fd, *data;
51
52 // open the file and allocate space for one
53 // integer
54 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
55 posix_fallocate(fd, 0, sizeof(int));
56
57 // map the file and register it with VALGRIND
58 data = (int *)mmap(NULL, sizeof(int),
59 PROT_READ | PROT_WRITE,
60 MAP_SHARED_VALIDATE | MAP_SYNC, fd, 0);
61 VALGRIND_PMC_REGISTER_PMEM_MAPPING(data,
62 sizeof(int));
63
64 // write and flush
65 *data = 1234;
66 flush((void *)data, sizeof(int));
67
68 // unmap and un-register
69 munmap(data, sizeof(int));
70 VALGRIND_PMC_REMOVE_PMEM_MAPPING(data,
71 sizeof(int));
72 return 0;
73 }
通过pmemcheck运行修改后的代码不会报告任何问题,如清单12-12所示。
Listing 12-12. Running pmemcheck with code Listing 12-11
$ valgrind --tool=pmemcheck ./listing_12-11
==9710== pmemcheck-1.0, a simple persistent store checker
...
==9710== Number of stores not made persistent: 0
==9710== ERROR SUMMARY: 0 errors
因为Intel Inspector–Persistence Inspector不认为未flush的写是一个问题,除非存在与其他变量的写依赖关系,所以我们需要展示一个比在清单12-7中编写单个变量更复杂的示例。您需要了解写入持久内存的程序是如何设计来知道写入持久介质的数据的哪些部分是有效的,哪些部分不是。请记住,如果未显式flush最近的写操作,则它们可能仍位于CPU缓存上。
事务通过使用日志回滚或应用未提交的更改来解决半写数据的问题;因此,读取数据的程序可以确保写入的所有内容都是有效的。在没有事务的情况下,不可能知道写入持久内存的数据是否有效,特别是在程序崩溃的情况下。
写入程序可以通过设置“有效”标志或使用水印变量(如果是数组,则为索引)最后一个有效的写入内存位置来通知读取器数据是以两种方式之一正确写入的。
清单12-13显示了如何实现“valid”标志方法的伪代码。
Listing 12-13. Pseudocode showcasing write dependency of var1 with var1_valid
1 writer() {
2 var1 = "This is a persistent Hello World
3 written to persistent memory!";
4 flush (var1);
5 var1_valid = True;
6 flush (var1_valid);
7 }
8
9 reader() {
10 if (var1_valid == True) {
11 print (var1);
12 }
13
14 }
如果var1_valid标志设置为True(第10行),reader() 将读取var1中的数据,而var1_valid只能在var1已刷新(第4行和第5行)时为True。
我们现在可以修改清单12-7中的代码来引入这个“valid”标志。在清单12-14中,我们将代码分为writer和reader程序,并映射两个整数而不是一个(以适应标志)。清单12-15显示了对持久内存的读取示例。
Listing 12-14. Example of writing to persistent memory with a write dependency; the code does not flush
33 #include <stdio.h>
34 #include <sys/mman.h>
35 #include <fcntl.h>
36 #include <string.h>
37
38 int main(int argc, char *argv[]) {
39 int fd, *ptr, *data, *flag;
40
41 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
42 posix_fallocate(fd, 0, sizeof(int)*2);
43
44 ptr = (int *) mmap(NULL, sizeof(int)*2,
45 PROT_READ | PROT_WRITE,
46 MAP_SHARED_VALIDATE | MAP_SYNC,
47 fd, 0);
48
49 data = &(ptr[1]);
50 flag = &(ptr[0]);
51 *data = 1234;
52 *flag = 1;
53
54 munmap(ptr, 2 * sizeof(int));
55 return 0;
56 }
Listing 12-15. Example of reading from persistent memory with a write dependency
33 #include <stdio.h>
34 #include <sys/mman.h>
35 #include <fcntl.h>
36
37 int main(int argc, char *argv[]) {
38 int fd, *ptr, *data, *flag;
39
40 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
41 posix_fallocate(fd, 0, 2 * sizeof(int));
42
43 ptr = (int *) mmap(NULL, 2 * sizeof(int),
44 PROT_READ | PROT_WRITE,
45 MAP_SHARED_VALIDATE | MAP_SYNC,
46 fd, 0);
47
48 data = &(ptr[1]);
49 flag = &(ptr[0]);
50 if (*flag == 1)
51 printf("data = %d\n", *data);
52
53 munmap(ptr, 2 * sizeof(int));
54 return 0;
55 }
使用持久性检查器Persistence Inspector检查代码分三步完成。
步骤1:我们必须运行before-unfortunate-event阶段分析(参见清单12-16),它对应于清单12-14中的writer代码。
Listing 12-16. Running Intel Inspector – Persistence Inspector with code Listing 12-14 for before-unfortunate-event phase analysis
$ pmeminsp cb -pmem-file /mnt/pmem/file -- ./listing_12-14
++ Analysis starts
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-14"
参数cb是check-before-unfortunate-event的缩写,它指定了分析的类型。我们还必须传递应用程序将使用的持久性内存文件,以便持久性检查器知道哪些内存访问对应于持久性内存。默认情况下,分析的输出存储在 .pmeminsdata目录下的本地目录中。(也可以指定自定义目录;运行pmeminsp-help以获取有关可用选项的信息。)
步骤2:我们运行after-unfortunate-event阶段分析(参见清单12-17)。这与发生不幸事件(如进程崩溃)后将读取数据的代码相对应。
Listing 12-17. Running Intel Inspector – Persistence Inspector with code Listing 12-15 for after-unfortunate-event phase analysis
$ pmeminsp ca -pmem-file /mnt/pmem/file -- ./listing_12-15
++ Analysis starts
data = 1234
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-15"
参数ca是check-after-unfortunate-event的缩写。同样,分析的输出存储在当前工作目录中的 .pmeminsdata中。
步骤3:生成最终报告。为此,我们传递选项rp(report的缩写)和两个程序的名称,如清单12-18所示。
Listing 12-18. Generating a final report with Intel Inspector – Persistence Inspector from the analysis done in Listings 12-16 and 12-17
$ pmeminsp rp -- listing_12-16 listing_12-17
#=======================================================
# Diagnostic
# 1: Missing cache flush
#------------------
The first memory store
of size 4 at address 0x7F9C68893004 (offset 0x4 in /mnt/pmem/file)
in /data/listing_12-16!main at listing_12-16.c:51 - 0x67D
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_ line> - 0x223D3
in /data/listing_12-16!_start at <unknown_file>:<unknown_line> - 0x534
is not flushed before
the second memory store
of size 4 at address 0x7F9C68893000 (offset 0x0
in /mnt/pmem/file)
in /data/listing_12-16!main at listing_12-16.c:52 - 0x687
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_ line> - 0x223D3
in /data/listing_12-16!_start at <unknown_file>:<unknown_line> - 0x534
while
memory load from the location of the first store
in /data/listing_12-17!main at listing_12-17.c:51 - 0x6C8
depends on
memory load from the location of the second store
in /data/listing_12-17!main at listing_12-17.c:50 - 0x6BD
#========================================================== # Diagnostic # 2: Missing cache flush
#------------------
Memory store
of size 4 at address 0x7F9C68893000 (offset 0x0 in /mnt/pmem/file)
in /data/listing_12-16!main at listing_12-16.c:52 - 0x687
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_ line> - 0x223D3
in /data/listing_12-16!_start at <unknown_file>:<unknown_line> - 0x534
is not flushed before
memory is unmapped
in /data/listing_12-16!main at listing_12-16.c:54 - 0x699
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_ line> - 0x223D3
in /data/listing_12-16!_start at <unknown_file>:<unknown_line> - 0x534
Analysis complete. 2 diagnostic(s) reported.
输出非常详细,但是很容易理解。我们得到两个遗漏的缓存flush(诊断1和2),对应于清单12-16.c的第51行和第52行。我们将这些写入到由变量标志和数据指向的映射持久内存中的位置。第一个诊断表明,第一个内存存储不会在第二个存储之前刷新,同时,第一个存储与第二个存储之间存在负载依赖关系。这正是我们想要的。
第二个诊断说,第二个存储(到标志)本身在结束之前从来没有真正flush过。即使我们在写入标志之前正确地flush了第一个存储,我们仍然必须flush标志以确保依赖项工作。
要在Intel Inspector GUI中打开结果,可以在生成报告时使用-insp选项,例如:
$ pmeminsp rp -insp -- listing_12-16 listing_12-17
这将在分析目录中生成一个名为r000pmem的目录(默认为.pmeminsdata)。启动运行inspxe-gui的GUI并打开结果文件,方法是转到File ➤ Open ➤ Result并选择文件r000pmem/r000pmem.inspxe。您应该看到类似于图12-3所示的内容。
GUI显示的信息与命令行分析相同,但通过在源代码中直接突出显示错误,显示的信息更可读。如图12-3所示,对标志的修改称为“primary store”。
在图12-4中,在Problems窗格中选择了第二个诊断,显示了标记本身缺少的flush。
为了结束这一部分,我们修复了代码并使用持久性检查器重新运行分析。清单12-19中的代码将必要的flush添加到清单12-14中。
Listing 12-19. Example of writing to persistent memory with a write dependency. The code flushes both writes
33 #include <emmintrin.h>
34 #include <stdint.h>
35 #include <stdio.h>
36 #include <sys/mman.h>
37 #include <fcntl.h>
38 #include <string.h>
39
40 void flush(const void *addr, size_t len) {
41 uintptr_t flush_align = 64, uptr;
42 for (uptr = (uintptr_t)addr & ~(flush_align - 1);
43 uptr < (uintptr_t)addr + len;
44 uptr += flush_align)
45 _mm_clflush((char *)uptr);
46 }
47
48 int main(int argc, char *argv[]) {
49 int fd, *ptr, *data, *flag;
50
51 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
52 posix_fallocate(fd, 0, sizeof(int) * 2);
53
54 ptr = (int *) mmap(NULL, sizeof(int) * 2,
55 PROT_READ | PROT_WRITE,
56 MAP_SHARED_VALIDATE | MAP_SYNC,
57 fd, 0);
58
59 data = &(ptr[1]);
60 flag = &(ptr[0]);
61 *data = 1234;
62 flush((void *) data, sizeof(int));
63 *flag = 1;
64 flush((void *) flag, sizeof(int));
65
66 munmap(ptr, 2 * sizeof(int));
67 return 0;
68 }
清单12-20对清单12-19中修改的代码执行持久性检查器,然后对清单12-15中的读取器代码执行持久性检查器,最后运行报告,报告中说没有检测到任何问题。
清单12-20。对代码清单为12-19和12-15使用Intel Inspector- Persistence Inspector运行完整分析。
$ pmeminsp cb -pmem-file /mnt/pmem/file -- ./listing_12-19
++ Analysis starts
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-19"
$ pmeminsp ca -pmem-file /mnt/pmem/file -- ./listing_12-15
++ Analysis starts
data = 1234
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-15"
$ pmeminsp rp -- listing_12-19 listing_12-15
Analysis complete. No problems detected.
12.3.2 未加入事务的Stores
在事务块中工作时,假定所有修改过的持久内存地址都是在开始时添加到事务块中的,这也意味着它们以前的值被复制到undo中。这允许事务隐式flush块末尾添加的内存地址,或在发生意外故障时回滚到旧值。在事务中修改未添加到事务中的地址是一个必须注意的bug。
考虑清单12-21中使用PMDK中libpmemobj库的代码。它展示了一个使用事务未显式跟踪的内存地址在事务中写入的示例。
Listing 12-21. Example of writing within a transaction with a memory address not added to the transaction
33 #include <libpmemobj.h>
34
35 struct my_root {
36 int value;
37 int is_odd;
38 };
39
40 // registering type 'my_root' in the layout
41 POBJ_LAYOUT_BEGIN(example);
42 POBJ_LAYOUT_ROOT(example, struct my_root);
43 POBJ_LAYOUT_END(example);
44
45 int main(int argc, char *argv[]) {
46 // creating the pool
47 PMEMobjpool *pop= pmemobj_create("/mnt/pmem/pool",
48 POBJ_LAYOUT_NAME(example),
49 (1024 * 1024 * 100), 0666);
50
51 // transation
52 TX_BEGIN(pop) {
53 TOID(struct my_root) root
54 = POBJ_ROOT(pop, struct my_root);
55
56 // adding root.value to the transaction
57 TX_ADD_FIELD(root, value);
58
59 D_RW(root)->value = 4;
60 D_RW(root)->is_odd = D_RO(root)->value % 2;
61 } TX_END
62
63 return 0;
64 }
注意:要刷新清单12-21中使用的布局、根对象或宏的定义,请参见第7章,在这里我们将介绍libpmemobj。
在第35-38行中,我们创建了一个my_root数据结构,它有两个整数成员:value和is_odd。这些整数在事务(第52-61行)中修改,设置值为4,奇数为0。在第57行,我们只将值变量添加到事务中,剩下的是奇数。鉴于C中不是原生支持持久内存,编译器无法对此发出警告。编译器无法区分指向易失性内存和指向持久性内存的指针。
清单12-22显示了通过pmemcheck运行代码的响应结果。
清单12-22。运行pmemcheck,代码清单12-21
$ valgrind --tool=pmemcheck ./listing_12-21
==48660== pmemcheck-1.0, a simple persistent store checker
==48660== Copyright (c) 2014-2016, Intel Corporation
==48660== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==48660== Command: ./listing_12-21
==48660==
==48660==
==48660== Number of stores not made persistent: 1
==48660== Stores not made persistent properly:
==48660== [0] at 0x400C2D: main (listing_12-25.c:60)
==48660== Address: 0x7dc0554 size: 4 state: DIRTY
==48660== Total memory not made persistent: 4
==48660==
==48660== Number of stores made without adding to transaction: 1
==48660== Stores made without adding to transactions:
==48660== [0] at 0x400C2D: main (listing_12-25.c:60)
==48660== Address: 0x7dc0554 size: 4
==48660== ERROR SUMMARY: 2 errors
尽管它们都与相同的根本原因有关,pmemcheck发现了两个问题。一个是我们预期的错误;也就是说,我们在一个未添加到事务中的事务中有一个store。另一个错误是我们没有flush这个store。由于事务性store在程序退出事务时会自动flush,因此在pmemcheck时,通常会发现未包含在事务中的每个store位置有两个错误。
Persistence Inspector有一个更友好的输出,如清单12-23所示。
Listing 12-23. Generating a report with Intel Inspector – Persistence Inspector for code Listing 12-21
$ pmeminsp cb -pmem-file /mnt/pmem/pool -- ./listing_12-21
++ Analysis starts
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-21"
$
$ pmeminsp rp -- ./listing_12-21
#=======================================================
# Diagnostic # 1: Store without undo log
#------------------
Memory store
of size 4 at address 0x7FAA84DC0554 (offset 0x3C0554 in /mnt/pmem/pool)
in /data/listing_12-21!main at listing_12-21.c:60 - 0xC2D
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_ line> - 0x223D3 in /data/listing_12-21!_start at <unknown_file>:<unknown_line> - 0x954
is not undo logged in
transaction
in /data/listing_12-21!main at listing_12-21.c:52 - 0xB67
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_ line> - 0x223D3
in /data/listing_12-21!_start at <unknown_file>:<unknown_line> - 0x954
Analysis complete. 1 diagnostic(s) reported.
我们在这里不执行after-unfortunate-event阶段分析,因为我们只关心事务。
我们可以通过使用TX_ADD(root)将整个root对象添加到事务中来修复清单12-23中报告的问题,如清单12-24的第53行所示。
Listing 12-24. Example of adding an object and writing it within a transaction
32 #include <libpmemobj.h>
33
34 struct my_root {
35 int value;
36 int is_odd;
37 };
38
39 POBJ_LAYOUT_BEGIN(example);
40 POBJ_LAYOUT_ROOT(example, struct my_root);
41 POBJ_LAYOUT_END(example);
42
43 int main(int argc, char *argv[]) {
44 PMEMobjpool *pop= pmemobj_create("/mnt/pmem/pool",
45 POBJ_LAYOUT_NAME(example),
46 (1024 * 1024 * 100), 0666);
47
48 TX_BEGIN(pop) {
49 TOID(struct my_root) root
50 = POBJ_ROOT(pop, struct my_root);
51
52 // adding full root to the transaction
53 TX_ADD(root);
54
55 D_RW(root)->value = 4;
56 D_RW(root)->is_odd = D_RO(root)->value % 2;
57 } TX_END
58
59 return 0;
60 }
如果我们通过pmemcheck运行代码,如清单12-25所示,则不会报告任何问题。
Listing 12-25. Running pmemcheck with code Listing 12-24
$ valgrind --tool=pmemcheck ./listing_12-24
==80721== pmemcheck-1.0, a simple persistent store checker
==80721== Copyright (c) 2014-2016, Intel Corporation
==80721== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==80721== Command: ./listing_12-24
==80721==
==80721==
==80721== Number of stores not made persistent: 0
==80721== ERROR SUMMARY: 0 errors
类似地,Persistence Inspector在清单12-26中没有报告任何问题。
Listing 12-26. Generating report with Intel Inspector – Persistence Inspector for code Listing 12-24
$ pmeminsp cb -pmem-file /mnt/pmem/pool -- ./listing_12-24
++ Analysis starts
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-24"
$
$ pmeminsp rp -- ./listing_12-24
Analysis complete. No problems detected.
在将要修改的所有内存正确添加到事务之后,两个工具都报告没有发现问题。
12.3.3 添加到两个不同事务的内存
在一个程序可以同时处理多个事务的情况下,将同一内存对象添加到多个事务可能会损坏数据。这可能发生在PMDK中,例如,在PMDK中,库为每个线程维护不同的事务。如果两个线程在不同事务中写入同一对象,则在应用程序崩溃后,一个线程可能会覆盖另一个线程在不同事务中所做的修改。在数据库系统中,这个问题被称为脏读。脏读违反了ACID(原子性、一致性、隔离性、持久性)属性中的隔离要求,如图12-5所示。
在图12-5中,时间显示在y轴上,时间向下推进。这些操作按以下顺序发生:
- 应用程序启动时假设X=0;
- main()创建两个线程:线程1和线程2。这两个线程都打算启动自己的事务并获取用于修改X的锁;
- 由于线程1首先运行,它首先获取X上的锁。然后将X变量添加到事务中,然后将X增5。对程序来说是透明的,当X被添加到事务中时,X(X=0)的值被添加到undo日志中。由于事务尚未完成,应用程序尚未显式flush该值;
- 线程2启动,开始自己的事务,获取锁,读取X的值(现在是5),将X=5添加到撤消日志中,并将其增加5。事务成功完成,线程2 flush CPU缓存。现在,x=10;
- 不幸的是,在线程2成功完成其事务之后,但在线程1能够完成其事务并flush其值之前,程序崩溃。
此场景将应用程序保留了一个无效但一致的x=10值。由于事务是原子的,因此在事务中所做的所有更改在成功完成之前都是无效的。
当应用程序启动时,它知道由于上一次崩溃,它必须执行恢复操作,并将重放undo日志以撤消线程1所做的部分更新。撤消日志恢复X=0的值,这在线程1添加其条目时是正确的。在这种情况下,X的期望值应该是X=5,但是undo日志将X=0。您应该会看到这种情况可能产生的数据损坏的巨大潜力。
我们在第14章中描述了多线程应用程序的并发性。使用libpmemobj-cpp,将C++语言绑定库绑定到libpmemobj,并发问题很容易解决,因为API允许我们在创建事务时使用lambda函数传递锁列表。第8章详细讨论了libpmemobj_cpp和lambda函数。
清单12-27显示了如何使用单个互斥锁锁定整个事务。如果互斥对象位于易失性内存中,则此互斥对象可以是标准互斥对象(std::mutex),如果互斥对象位于永久内存中,则可以是pmem互斥对象(pmem::obj::mutex)。
清单12-27是libpmemobj++事务的示例,在多线程场景中,该事务的写操作是原子的(就持久内存而言)和隔离的。互斥锁作为参数传递给事务:
Listing 12-27. Example of a libpmemobj++ transaction whose writes are both atomic – with respect to persistent memory – and isolated – in a multithreaded scenario. The mutex is passed to the transaction as a parameter
transaction::run (pop, [&] { ... // all writes here are atomic and thread safe ... }, mutex);
考虑清单12-28中的代码,它同时将相同的内存区域添加到两个不同的事务中。
Listing 12-28. Example of two threads simultaneously adding the same persistent memory location to their respective transactions
33 #include <libpmemobj.h>
34 #include <pthread.h>
35
36 struct my_root {
37 int value;
38 int is_odd;
39 };
40
41 POBJ_LAYOUT_BEGIN(example);
42 POBJ_LAYOUT_ROOT(example, struct my_root);
43 POBJ_LAYOUT_END(example);
44
45 pthread_mutex_t lock;
46
47 // function to be run by extra thread
48 void *func(void *args) {
49 PMEMobjpool *pop = (PMEMobjpool *) args;
50
51 TX_BEGIN(pop) {
52 pthread_mutex_lock(&lock);
53 TOID(struct my_root) root
54 = POBJ_ROOT(pop, struct my_root);
55 TX_ADD(root);
56 D_RW(root)->value = D_RO(root)->value + 3;
57 pthread_mutex_unlock(&lock);
58 } TX_END
59 }
60
61 int main(int argc, char *argv[]) {
62 PMEMobjpool *pop= pmemobj_create("/mnt/pmem/pool",
63 POBJ_LAYOUT_NAME(example),
64 (1024 * 1024 * 10), 0666);
65
66 pthread_t thread;
67 pthread_mutex_init(&lock, NULL);
68
69 TX_BEGIN(pop) {
70 pthread_mutex_lock(&lock);
71 TOID(struct my_root) root
72 = POBJ_ROOT(pop, struct my_root);
73 TX_ADD(root);
74 pthread_create(&thread, NULL,
75 func, (void *) pop);
76 D_RW(root)->value = D_RO(root)->value + 4;
77 D_RW(root)->is_odd = D_RO(root)->value % 2;
78 pthread_mutex_unlock(&lock);
79 // wait to make sure other thread finishes 1st
80 pthread_join(thread, NULL);
81 } TX_END
82
83 pthread_mutex_destroy(&lock);
84 return 0;
85 }
- 第 69行: 主线程启动事务并向其添加root数据结构(第73行).
- 第74行: 我们通过调用pthread_create()创建一个新线程,并让它执行func()函数。此函数还启动事务(第51行)并向其添加root数据结构(第55行)。
两个线程在完成其事务之前将同时修改全部或部分相同的数据。我们通过使主线程等待pthread_join()来强制第二个线程首先完成。
清单12-29显示了带有pmemcheck检查的代码的执行结果,结果警告我们在不同的事务中注册了重叠的区域。
Listing 12-29. Running pmemcheck with Listing 12-28
$ valgrind --tool=pmemcheck ./listing_12-28
==97301== pmemcheck-1.0, a simple persistent store checker
==97301== Copyright (c) 2014-2016, Intel Corporation
==97301== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==97301== Command: ./listing_12-28
==97301==
==97301==
==97301== Number of stores not made persistent: 0
==97301==
==97301== Number of overlapping regions registered in different transactions: 1
==97301== Overlapping regions: ==97301== [0] at 0x4E6B0BC: pmemobj_tx_add_snapshot (in /usr/lib64/ libpmemobj.so.1.0.0)
==97301== by 0x4E6B5F8: pmemobj_tx_add_common.constprop.18 (in /usr/ lib64/libpmemobj.so.1.0.0)
==97301== by 0x4E6C62F: pmemobj_tx_add_range (in /usr/lib64/libpmemobj. so.1.0.0)
==97301== by 0x400DAC: func (listing_12-28.c:55)
==97301== by 0x4C2DDD4: start_thread (in /usr/lib64/libpthread-2.17.so)
==97301== by 0x5180EAC: clone (in /usr/lib64/libc-2.17.so)
==97301== Address: 0x7dc0550 size: 8 tx_id: 2
==97301== First registered here:
==97301== [0]' at 0x4E6B0BC: pmemobj_tx_add_snapshot (in /usr/lib64/ libpmemobj.so.1.0.0)
==97301== by 0x4E6B5F8: pmemobj_tx_add_common.constprop.18 (in /usr/ lib64/libpmemobj.so.1.0.0)
==97301== by 0x4E6C62F: pmemobj_tx_add_range (in /usr/lib64/libpmemobj. so.1.0.0)
==97301== by 0x400F23: main (listing_12-28.c:73)
==97301== Address: 0x7dc0550 size: 8 tx_id: 1
==97301== ERROR SUMMARY: 1 errors
清单12-30显示了使用Persistence Inspector运行的同一代码,该检查器在诊断25中也报告了“在不同事务中注册的重叠区域”。前24个诊断结果与未添加到我们的事务中的stores有关,这些stores对应于volatile互斥锁的锁定和解锁;这些可以忽略。
Listing 12-30. Generating a report with Intel Inspector – Persistence Inspector for code Listing 12-28
$ pmeminsp rp -- ./listing_12-28 ...
#=======================================================
# Diagnostic # 25: Overlapping regions registered in different transactions
#------------------
Transaction
in /data/listing_12-28!main at listing_12-28.c:69 - 0xEB6
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3 in /data/listing_12-28!_start at <unknown_file>:<unknown_line> - 0xB44
protects
memory region
in /data/listing_12-28!main at listing_12-28.c:73 - 0xF1F
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3 in /data/listing_12-28!_start at <unknown_file>:<unknown_line> - 0xB44
overlaps with
memory region
in /data/listing_12-28!func at listing_12-28.c:55 - 0xDA8
in /lib64/libpthread.so.0!start_thread at <unknown_file>:<unknown_line> - 0x7DCD in /lib64/libc.so.6!__clone at <unknown_file>:<unknown_line> - 0xFDEAB
Analysis complete. 25 diagnostic(s) reported.
12.3.4 内存覆盖
当对同一持久性内存位置的多个修改发生在该位置成为持久性(即flush)之前时,将发生内存覆盖。如果程序崩溃,这是一个潜在的数据损坏源,因为持久变量的最终值可能是上次flush和崩溃之间写入的任何值。重要的是要知道,如果设计如此,这就不是问题。我们建议对临时数据使用volatile变量,并且只把持久化数据时写入持久化变量。
考虑清单12-31中的代码,在我们第64行调用flush()之前,将两次写入main()函数(第62行和第63行)中的数据变量。
Listing 12-31. Example of persistent memory overwriting – variable data – before flushing
33 #include <emmintrin.h>
34 #include <stdint.h>
35 #include <stdio.h>
36 #include <sys/mman.h>
37 #include <fcntl.h>
38 #include <valgrind/pmemcheck.h>
39
40 void flush(const void *addr, size_t len) {
41 uintptr_t flush_align = 64, uptr;
42 for (uptr = (uintptr_t)addr & ~(flush_align - 1);
43 uptr < (uintptr_t)addr + len;
44 uptr += flush_align)
45 _mm_clflush((char *)uptr);
46 }
47
48 int main(int argc, char *argv[]) {
49 int fd, *data;
50
51 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
52 posix_fallocate(fd, 0, sizeof(int));
53
54 data = (int *)mmap(NULL, sizeof(int),
55 PROT_READ | PROT_WRITE,
56 MAP_SHARED_VALIDATE | MAP_SYNC,
57 fd, 0);
58 VALGRIND_PMC_REGISTER_PMEM_MAPPING(data,
59 sizeof(int));
60
61 // writing twice before flushing
62 *data = 1234;
63 *data = 4321;
64 flush((void *)data, sizeof(int));
65
66 munmap(data, sizeof(int));
67 VALGRIND_PMC_REMOVE_PMEM_MAPPING(data,
68 sizeof(int));
69 return 0;
70 }
清单12-32显示了来自pmemcheck的报告,代码来自清单12-31。要使pmemcheck查找覆盖,必须使用–mult stores=yes选项。
Listing 12-32. Running pmemcheck with Listing 12-31
$ valgrind --tool=pmemcheck --mult-stores=yes ./listing_12-31
==25609== pmemcheck-1.0, a simple persistent store checker
==25609== Copyright (c) 2014-2016, Intel Corporation
==25609== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==25609== Command: ./listing_12-31
==25609==
==25609==
==25609== Number of stores not made persistent: 0
==25609==
==25609== Number of overwritten stores: 1
==25609== Overwritten stores before they were made persistent:
==25609== [0] at 0x400962: main (listing_12-31.c:62)
==25609== Address: 0x4023000 size: 4 state: DIRTY
==25609== ERROR SUMMARY: 1 errors
pmemcheck报告我们覆盖了stores。如果忘记flush,我们可以通过在两次写入之间插入flush指令来解决此问题;如果某个stores响应的是临时数据,则可以通过将其store到volatile data解决问题。
在发布时,Persistence Inspector不支持检查覆盖的stores。如您所见,除非存在写依赖,否则Persistence Inspector不会认为缺少flush是一个问题。此外,它不认为这是一个性能问题,因为在短时间内写入同一个变量很可能会命中CPU缓存,从而使DRAM和持久内存之间的延迟差异变得无关紧要。
12.3.5 不必要的flush
flush时要小心。检测不必要的flush(如冗余flush)有助于提高代码性能。清单12-33中的代码在第64行显示了对flush()函数的冗余调用。
Listing 12-33. Example of redundant flushing of a persistent memory variable
33 #include <emmintrin.h>
34 #include <stdint.h>
35 #include <stdio.h>
36 #include <sys/mman.h>
37 #include <fcntl.h>
38 #include <valgrind/pmemcheck.h>
39
40 void flush(const void *addr, size_t len) {
41 uintptr_t flush_align = 64, uptr;
42 for (uptr = (uintptr_t)addr & ~(flush_align - 1);
43 uptr < (uintptr_t)addr + len;
44 uptr += flush_align)
45 _mm_clflush((char *)uptr);
46 }
47
48 int main(int argc, char *argv[]) {
49 int fd, *data;
50
51 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
52 posix_fallocate(fd, 0, sizeof(int));
53
54 data = (int *)mmap(NULL, sizeof(int),
55 PROT_READ | PROT_WRITE,
56 MAP_SHARED_VALIDATE | MAP_SYNC,
57 fd, 0);
58
59 VALGRIND_PMC_REGISTER_PMEM_MAPPING(data,
60 sizeof(int));
61
62 *data = 1234;
63 flush((void *)data, sizeof(int));
64 flush((void *)data, sizeof(int)); // extra flush
65
66 munmap(data, sizeof(int));
67 VALGRIND_PMC_REMOVE_PMEM_MAPPING(data,
68 sizeof(int));
69 return 0;
70 }
我们可以使用pmemcheck的–flush check=yes选项检测冗余flush,如清单12-34所示。
Listing 12-34. Running pmemcheck with Listing 12-33
$ valgrind --tool=pmemcheck --flush-check=yes ./listing_12-33
==104125== pmemcheck-1.0, a simple persistent store checker
==104125== Copyright (c) 2014-2016, Intel Corporation
==104125== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==104125== Command: ./listing_12-33
==104125==
==104125==
==104125== Number of stores not made persistent: 0
==104125==
==104125== Number of unnecessary flushes: 1
==104125== [0] at 0x400868: flush (emmintrin.h:1459)
==104125== by 0x400989: main (listing_12-33.c:64)
==104125== Address: 0x4023000 size: 64
==104125== ERROR SUMMARY: 1 errors
为了展示Persistence Inspector,清单12-35中的代码有一个写依赖项,类似于清单12-19中的清单12-11。多余的flush发生在第65行。
Listing 12-35. Example of writing to persistent memory with a write dependency. The code does an extra flush for the flag
33 #include <emmintrin.h>
34 #include <stdint.h>
35 #include <stdio.h>
36 #include <sys/mman.h>
37 #include <fcntl.h>
38 #include <string.h>
39
40 void flush(const void *addr, size_t len) {
41 uintptr_t flush_align = 64, uptr;
42 for (uptr = (uintptr_t)addr & ~(flush_align - 1);
43 uptr < (uintptr_t)addr + len;
44 uptr += flush_align)
45 _mm_clflush((char *)uptr);
46 }
47
48 int main(int argc, char *argv[]) {
49 int fd, *ptr, *data, *flag;
50
51 fd = open("/mnt/pmem/file", O_CREAT|O_RDWR, 0666);
52 posix_fallocate(fd, 0, sizeof(int) * 2);
53
54 ptr = (int *) mmap(NULL, sizeof(int) * 2,
55 PROT_READ | PROT_WRITE,
56 MAP_SHARED_VALIDATE | MAP_SYNC,
57 fd, 0);
58 data = &(ptr[1]);
59 flag = &(ptr[0]);
60
61 *data = 1234;
62 flush((void *) data, sizeof(int));
63 *flag = 1;
64 flush((void *) flag, sizeof(int));
65 flush((void *) flag, sizeof(int)); // extra flush
66
67 munmap(ptr, 2 * sizeof(int));
68 return 0;
69 }
清单12-36使用清单12-15中相同的reader程序来显示Persistence Inspector的分析。和前面一样,我们首先从writer程序收集数据,然后从reader程序收集数据,最后运行报告以识别问题。
Listing 12-36. Running Intel Inspector – Persistence Inspector with Listing 12-35 (writer) and Listing 12-15 (reader)
$ pmeminsp cb -pmem-file /mnt/pmem/file -- ./listing_12-35
++ Analysis starts
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-35"
$ pmeminsp ca -pmem-file /mnt/pmem/file -- ./listing_12-15
++ Analysis starts
data = 1234
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-15"
$ pmeminsp rp -- ./listing_12-35 ./listing_12-15
#=======================================================
# Diagnostic # 1: Redundant cache flush
#------------------
Cache flush
of size 64 at address 0x7F3220C55000 (offset 0x0 in /mnt/pmem/file)
in /data/listing_12-35!flush at listing_12-35.c:45 - 0x674
in /data/listing_12-35!main at listing_12-35.c:64 - 0x73F
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3
in /data/listing_12-35!_start at <unknown_file>:<unknown_line> - 0x574
is redundant with regard to
cache flush of size 64 at address 0x7F3220C55000 (offset 0x0 in /mnt/pmem/file) in /data/listing_12-35!flush at listing_12-35.c:45 - 0x674 in /data/listing_12-35!main at listing_12-35.c:65 - 0x750 in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3 in /data/listing_12-35!_start at <unknown_file>:<unknown_line> - 0x574
of
memory store of size 4 at address 0x7F3220C55000 (offset 0x0 in /mnt/pmem/file)
in /data/listing_12-35!main at listing_12-35.c:63 - 0x72D
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3
in /data/listing_12-35!_start at <unknown_file>:<unknown_line> - 0x574
Persistence Inspector报告警告清单12-35.c程序文件第65行main()函数中的冗余缓存刷新-“main at listing 12-35.c:65”。解决这些问题就像删除所有不必要的刷新一样简单,结果将提高应用程序的性能。
12.3.6 无序写入
为持久内存开发软件时,请记住,即使缓存线未显式flush,也不意味着数据仍在CPU缓存中。例如,由于缓存压力或其他原因,CPU可能已将其逐出。此外,与未正确flush的写操作在意外应用程序崩溃时可能产生错误的方式相同,如果脏缓存线违反应用程序所依赖的某些预期写入顺序,则会自动逐出脏缓存线。
为了更好地理解这个问题,探究flush在x86_64和AMD64体系结构中的工作方式。我们可以从用户空间中分发以下任一指令,来确保写入操作到达持久介质:
- CLFLUSH
- CLFLUSHOPT (needs SFENCE)
- CLWB (needs SFENCE)
- Non-temporal stores (needs SFENCE)
确保每个flush按顺序发出的唯一指令是CLFUSH,因为每个CLFLUSH指令总是执行隐式fence指令(SFENCE)。其他指令是异步的,可以以任何顺序并行发出。CPU只能保证在显式执行新的SFENCE指令时,自上一个SFENCE之后发出的所有flush都已完成。将SFENCE指令视为同步点(见图12-6)。有关这些说明的更多信息,请参阅Intel软件开发人员手册和AMD软件开发人员手册。
如图12-6所示,我们不能保证A和B最终写入持久内存的顺序。之所以会发生这种情况,是因为对A和B的store和flush是在同步点之间完成的。C的情况不同。使用SFENCE指令,可以确保C总是在A和B被flush之后执行。
知道了这一点,您现在可以想象,在程序崩溃时,无序写入可能是一个问题。如果对同步点之间的写入顺序进行了假设,或者如果忘记在写入和flush之间添加同步点,而必须使用严格的顺序(请考虑变量写入的“有效标志”,其中变量需要在标志设置为有效之前写入),您可能会遇到数据一致性问题。考虑清单12-37中的伪代码。
Listing 12-37. Pseudocode showcasing an out-of-order issue
1 writer () {
2 pcounter = 0;
3 flush (pcounter);
4 for (i=0; i<max; i++) {
5 pcounter++;
6 if (rand () % 2 == 0) {
7 pcells[i].data = data ();
8 flush (pcells[i].data);
9 pcells[i].valid = True;
10 } else {
11 pcells[i].valid = False;
12 }
13 flush (pcells[i].valid);
14 }
15 flush (pcounter);
16 }
17
18 reader () {
19 for (i=0; i<pcounter; i++) {
20 if (pcells[i].valid == True) {
21 print (pcells[i].data);
22 }
23 }
24 }
为了简单起见,假设清单12-37中的所有flush也是同步点;也就是说,flush()使用CLFLUSH。程序的逻辑很简单。有两个持久内存变量:pcells和pcounter。第一个是元组数组{data,valid},其中data包含数据,valid是一个标志,指示数据是否有效。第二个变量是一个计数器,指示数组中有多少元素已正确写入持久内存。在这种情况下,pcell中的valid标志不表示该数组位置的数据是否正确写入持久内存。标志的含义仅指示是否调用了函数data(),即数据是否具有有意义的数据。
乍一看,程序似乎是正确的。每次循环的新迭代,计数器都会递增,然后写入和flush数组位置。然而,在写入数组之前,pcounter会递增,从而在pcounter和数组中实际提交的条目数之间产生差异。尽管事实上pcounter在循环之后会被flush,但只有当我们假设对pcounter的更改驻留在CPU缓存中(在这种情况下,循环中间的程序崩溃只会将计数器保留为零)时,程序才会在崩溃后正确。
如本节开头所述,我们不能作出这种假设。缓存线可以被随时逐出。在清单12-37中的伪代码示例中,我们将遇到一个错误,其中pcounter指示数组比实际长度长,从而使reader()读取未初始化的内存。
清单12-38和12-39中的代码提供了清单12-37中的伪代码的C++实现。两者都使用PMDK中的libpmemobj cpp。清单12-38是writer程序,清单12-39是reader程序。
Listing 12-38. Example of writing to persistent memory with an out-of-order write bug
33 #include <emmintrin.h>
34 #include <unistd.h>
35 #include <stdio.h>
36 #include <string.h>
37 #include <stdint.h>
38 #include <libpmemobj++/persistent_ptr.hpp>
39 #include <libpmemobj++/make_persistent.hpp>
40 #include <libpmemobj++/make_persistent_array.hpp>
41 #include <libpmemobj++/transaction.hpp>
42 #include <valgrind/pmemcheck.h>
43
44 using namespace std;
45 namespace pobj = pmem::obj;
46
47 struct header_t {
48 uint32_t counter;
49 uint8_t reserved[60];
50 };
51 struct record_t {
52 char name[63];
53 char valid;
54 };
55 struct root {
56 pobj::persistent_ptr<header_t> header;
57 pobj::persistent_ptr<record_t[]> records;
58 };
59
60 pobj::pool<root> pop;
61
62 int main(int argc, char *argv[]) {
63
64 // everything between BEGIN and END can be
65 // assigned a particular engine in pmreorder
66 VALGRIND_PMC_EMIT_LOG("PMREORDER_TAG.BEGIN");
67
68 pop = pobj::pool<root>::open("/mnt/pmem/file",
69 "RECORDS");
70 auto proot = pop.root();
71
72 // allocation of memory and initialization to zero
73 pobj::transaction::run(pop, [&] {
74 proot->header
75 = pobj::make_persistent<header_t>();
76 proot->header->counter = 0;
77 proot->records
78 = pobj::make_persistent<record_t[]>(10);
79 proot->records[0].valid = 0;
80 });
81
82 pobj::persistent_ptr<header_t> header
83 = proot->header;
84 pobj::persistent_ptr<record_t[]> records
85 = proot->records;
86
87 VALGRIND_PMC_EMIT_LOG("PMREORDER_TAG.END");
88
89 header->counter = 0;
90 for (uint8_t i = 0; i < 10; i++) {
91 header->counter++;
92 if (rand() % 2 == 0) {
93 snprintf(records[i].name, 63,
94 "record #%u", i + 1);
95 pop.persist(records[i].name, 63); // flush
96 records[i].valid = 2;
97 } else
98 records[i].valid = 1;
99 pop.persist(&(records[i].valid), 1); // flush
100 }
101 pop.persist(&(header->counter), 4); // flush
102
103 pop.close();
104 return 0;
105 }
Listing 12-39. Reading the data structure written by Listing 12-38 to persistent memory
33 #include <stdio.h>
34 #include <stdint.h>
35 #include <libpmemobj++/persistent_ptr.hpp>
36
37 using namespace std;
38 namespace pobj = pmem::obj;
39
40 struct header_t {
41 uint32_t counter;
42 uint8_t reserved[60];
43 };
44 struct record_t {
45 char name[63];
46 char valid;
47 };
48 struct root {
49 pobj::persistent_ptr<header_t> header;
50 pobj::persistent_ptr<record_t[]> records;
51 };
52
53 pobj::pool<root> pop;
54
55 int main(int argc, char *argv[]) {
56
57 pop = pobj::pool<root>::open("/mnt/pmem/file",
58 "RECORDS");
59 auto proot = pop.root();
60 pobj::persistent_ptr<header_t> header
61 = proot->header;
62 pobj::persistent_ptr<record_t[]> records
63 = proot->records;
64
65 for (uint8_t i = 0; i < header->counter; i++) {
66 if (records[i].valid == 2) {
67 printf("found valid record\n");
68 printf(" name = %s\n",
69 records[i].name);
70 }
71 }
72
73 pop.close();
74 return 0;
75 }
清单12-38(writer)使用VALGRIND_PMC_EMIT_LOG宏在程序运行到第66行和第87行时发出pmreorder消息。当我们稍后使用pmemcheck引入无序分析时,这将是有意义的。
现在我们首先运行Persistence Inspector。要执行无序分析,必须在报告阶段使用-check-out-of-order store选项。清单12-40显示了收集before和after数据,然后运行报表。
Listing 12-40. Running Intel Inspector – Persistence Inspector with Listing 12-38 (writer) and Listing 12-39 (reader)
$ pmempool create obj --size=100M --layout=RECORDS /mnt/pmem/file
$ pmeminsp cb -pmem-file /mnt/pmem/file -- ./listing_12-38
++ Analysis starts
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-38"
$ pmeminsp ca -pmem-file /mnt/pmem/file -- ./listing_12-39
++ Analysis starts
found valid record
name = record #2
found valid record
name = record #7
found valid record
name = record #8
++ Analysis completes
++ Data is stored in folder "/data/.pmeminspdata/data/listing_12-39"
$ pmeminsp rp -check-out-of-order-store -- ./listing_12-38 ./listing_12-39
#======================================================
# Diagnostic # 1: Out-of-order stores
#------------------
Memory store
of size 4 at address 0x7FD7BEBC05D0 (offset 0x3C05D0 in /mnt/pmem/file)
in /data/listing_12-38!main at listing_12-38.cpp:91 - 0x1D0C
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3
in /data/listing_12-38!_start at <unknown_file>:<unknown_line> - 0x1624
is out of order with respect to
memory store
of size 1 at address 0x7FD7BEBC068F (offset 0x3C068F in /mnt/pmem/file)
in /data/listing_12-38!main at listing_12-38.cpp:98 - 0x1DAF
in /lib64/libc.so.6!__libc_start_main at <unknown_file>:<unknown_line> - 0x223D3
in /data/listing_12-38!_start at <unknown_file>:<unknown_line> - 0x1624
Persistence Inspector报告标识一个无序存储问题。该工具表示,在第91行(main at listing_-38.cpp:91)中递增计数器与在第98行(main at listing_-38.cpp:98)中的记录内写入有效标志不符。
为了使用pmemcheck进行无序分析,我们必须引入一个新的工具pmreorder。pmreorder工具包含在PMDK 1.5版以后的版本中。这个独立的Python工具使用存储重新排序机制对持久程序执行一致性检查。pmemcheck工具无法执行此类分析,尽管它一直用于生成pmreorder可以解析的应用程序发出的所有store和flush的详细日志。例如,参考清单12-41。
Listing 12-41. Running pmemcheck to generate a detailed log of all the stores and flushes issued by Listing 12-38
$ valgrind --tool=pmemcheck -q --log-stores=yes --log-storesstacktraces=yes --log-stores-stacktraces-depth=2 --print-summary=yes --log-file=store_log.log ./listing_12-38
每个参数意义如下:
- -q 消除pmreorder无法解析的不必要pmemcheck日志;
- –log-stores=yes 告诉pmemcheck 记录所有store日志;
- –log-stores-stacktraces=yes 导出带有store日志的stacktrace,这有助于定位源代码中的问题;
- –log-stores-stacktraces-depth=2 记录stacktraces的深度.据所需的信息级别进行调整。
- –print-summary=yes 程序退出时打印汇总信息。为什么不呢?【建议设置成yes】
- –log-file=store_log.log 记录所有信息到store_log.log.
pmreorder工具使用“引擎”的概念,例如,ReorderFull引擎检查store和flush的所有可能的重新排序组合的一致性。对于某些程序,此引擎可能非常慢,因此可以使用其他引擎,如ReorderPartial或NoReorderDoCheck。有关更多信息,请参阅pmreorder页面,该页面具有指向手册页的链接(https://pmem.io/pmdk/pmreorder/)。 在运行pmreorder之前,我们需要一个程序,它可以遍历内存池中包含的记录列表,当数据结构一致时返回0,否则返回1。这个程序类似于清单12-42所示的reader。
Listing 12-42. Checking the consistency of the data structure written in Listing 12-38
33 #include <stdio.h>
34 #include <stdint.h>
35 #include <libpmemobj++/persistent_ptr.hpp>
36
37 using namespace std;
38 namespace pobj = pmem::obj;
39
40 struct header_t {
41 uint32_t counter;
42 uint8_t reserved[60];
43 };
44 struct record_t {
45 char name[63];
46 char valid;
47 };
48 struct root {
49 pobj::persistent_ptr<header_t> header;
50 pobj::persistent_ptr<record_t[]> records;
51 };
52
53 pobj::pool<root> pop;
54
55 int main(int argc, char *argv[]) {
56
57 pop = pobj::pool<root>::open("/mnt/pmem/file",
58 "RECORDS");
59 auto proot = pop.root();
60 pobj::persistent_ptr<header_t> header
61 = proot->header;
62 pobj::persistent_ptr<record_t[]> records
63 = proot->records;
64
65 for (uint8_t i = 0; i < header->counter; i++) {
66 if (records[i].valid < 1 or
67 records[i].valid > 2)
68 return 1; // data struc. corrupted
69 }
70
71 pop.close();
72 return 0; // everything ok
73 }
清单12-42中的程序遍历了我们希望正确写入持久内存的所有记录(第65-69行)。它检查每个记录的有效标志,该标志应为1或2,以便记录正确(第66行)。如果检测到问题,检查器将返回1,指示数据损坏。清单12-43显示了分析程序的三个步骤:
- 在/mnt/pmem/file上创建一个大小为100MiB的对象类型持久内存池(称为内存映射文件),并将内部布局命名为“RECORDS”;
- 在程序运行时,使用pmemcheck Valgrind工具记录数据和调用堆栈;
- pmreorder实用程序使用ReorderFull引擎处理来自pmemcheck的store.log输出文件,以生成最终报告。
清单12-43。首先,为清单12-38创建一个池。然后,运行pmemcheck以获得清单12-38所示的所有store和flush的详细日志。最后,pmreorder与engine ReorderFull一起运行。
$ pmempool create obj --size=100M --layout=RECORDS /mnt/pmem/file
$ valgrind --tool=pmemcheck -q --log-stores=yes --log-storesstacktraces=yes --log-stores-stacktraces-depth=2 --print-summary=yes --log-file=store.log ./listing_12-38
$ pmreorder -l store.log -o output_file.log -x PMREORDER_ TAG=NoReorderNoCheck -r ReorderFull -c prog -p ./listing_12-38
每个pmreorder选项的意义如下:
- -l store_log.log 是pmemcheck生成的输入文件,包含应用程序发出的所有store和flush;
- -o output_file.log 包含无序分析结果的输出文件;
- -x PMREORDER_TAG=NoReorderNoCheck 指定NoReorderNoCheck引擎来分析PMREORDER所圈起来的代码(参见清单12-38的第66-87行)。这样做只是为了将分析聚焦于循环上(清单12-38的第89-105行)。
- -r ReorderFull 设置初始无序引擎,此例中,为ReorderFull.
- -c prog 一致性检查器类型。可以是prog (program) 或者 lib (library).
- -p ./checker 一致性检查器
打开生成的文件output_file.log,您将看到类似于清单12-44中的条目,这些条目突出显示了在代码中检测到的不一致和问题。
清单12-44。pmreorder生成的“output_file.log”中的内容,显示了无序分析期间检测到的不一致性
WARNING:pmreorder:File /mnt/pmem/file inconsistent WARNING:pmreorder:Call trace: Store [0]: by 0x401D0C: main (listing_12-38.cpp:91)
报告指出,问题出在listing_12-38.cpp writer程序的第91行。要修复listing_12-38.cpp,请在记录中的所有数据flush到持久介质后再修改计数器递增。清单12-45展示了修改后的正确代码。
Listing 12-45. Fix Listing 12-38 by moving the incrementation of the counter to the end of the loop (line 95)
86 for (uint8_t i = 0; i < 10; i++) {
87 if (rand() % 2 == 0) {
88 snprintf(records[i].name, 63,
89 "record #%u", i + 1);
90 pop.persist(records[i].name, 63);
91 records[i].valid = 2;
92 } else
93 records[i].valid = 1;
94 pop.persist(&(records[i].valid), 1);
95 header->counter++;
96 }
12.4 本章小结
本章介绍了每种工具并描述了如何使用它们。在开发周期的早期发现问题可以节省以后无数小时调试复杂代码的时间。本章介绍了三种有价值的工具:Persistence Inspector、pmemcheck和pmreorder,持久性内存程序员希望将它们集成到开发和测试周期中以检测问题。我们演示了这些工具在检测许多不同类型的常见编程错误方面的用处。
持久内存开发工具包(PMDK)使用这里描述的工具来确保每个版本在发布之前都得到了充分的验证。这些工具紧密集成到PMDK持续集成(CI)开发周期中,因此您可以快速捕获和修复问题。