3 操作系统对持久内存的支持
本章描述操作系统如何将持久内存作为平台资源进行管理,并描述它们为应用程序使用持久内存提供的选项。我们首先比较了流行的计算机体系结构中的内存和存储,然后描述了操作系统是如何扩展为持久内存的。
3.1 操作系统对内存和存储的支持
图3-1显示了操作系统如何管理存储和易失性内存的简化视图。如图所示,易失性内存通过内存总线直接连接到CPU。操作系统直接管理内存区域到应用程序可见内存地址空间的映射。存储器通常以比CPU慢得多的速度运行,通过I/O控制器连接。操作系统通过加载到操作系统的I/O子系统中的设备驱动程序模块处理对存储的访问。
应用程序对易失性内存的直接访问与操作系统对存储设备的I/O访问相结合,支持在入门编程课程中讲授的最常见的应用程序编程模型。在这个模型中,开发人员分配数据结构并在内存中以字节粒度对其进行操作。当应用程序想要保存数据时,它使用标准的文件API系统调用将数据写入打开的文件。在操作系统中,文件系统通过对存储设备执行一个或多个I/O操作来执行此写操作。因为这些I/O操作通常比CPU速度慢得多,所以操作系统通常会挂起应用程序,直到I/O完成。
由于持久内存可以由应用程序直接访问,并且可以将数据持久化,因此它允许操作系统支持一种新的编程模型,这种模型结合了内存的性能,同时像非易失性存储设备一样持久化数据。幸运的是,在开发第一代持久内存的同时,Microsoft Windows和Linux的设计师、架构师和开发人员在存储和网络行业协会(SNIA)中合作,定义了一个通用的编程模型,因此,本章描述的使用持久内存的方法在这两个操作系统中都是可用的。更多细节可以在SNIA NVM编程模型规范中找到。
(参见:https://www.snia.org/ tech_activities/standards/curr_standards/npm)。
3.2 持久内存作为块存储
持久内存的第一个操作系统扩展,是检测持久内存模块的存在,并将设备驱动程序加载到操作系统的I/O子系统中,如图3-2所示。这个NVDIMM驱动有两个重要的功能。首先,它为管理和系统管理员实用程序提供了一个接口,用于配置和监视持久内存硬件的状态。其次,它的功能类似于存储设备驱动程序。
NVDIMM驱动,把持久内存当作块存储
NVDIMM驱动程序将持久内存作为快速块存储设备提供给应用程序和操作系统模块。这意味着应用程序、文件系统、卷管理器和其他存储中间件层可以像现在使用存储一样使用持久内存,而无需修改。【也就是说当持久内存作为一个块存储时,应用、文件系统等都可以直接使用,不用修改。】
BTT驱动,保障块原子性
图3-2还显示了块转换表(BTT)驱动程序,它可以选择性地配置到I/O子系统中。hdd和ssd之类的存储设备呈现本机块大小,512k和4k字节是两种常见的本机块大小。一些存储设备,特别是NVM-Express ssd,提供了一种保证,即当块写入正在进行时发生电源故障或服务器故障时,将写入所有块或不写入任何块。当使用持久内存作为块存储设备时,BTT驱动程序提供同样的保证。大多数应用程序和文件系统都依赖于这种原子写入保证,并且应该配置为使用BTT驱动程序,尽管操作系统还提供了一个选项,可以绕过BTT驱动程序,使应用程序能够针对部分块更新实现自己的保护。
3.3 感知持久内存的文件系统
操作系统的下一个扩展是让文件系统知道并针对持久内存优化。为持久内存扩展的文件系统包括Linux Ext4和XFS,以及Microsoft Windows NTFS,这些文件系统可以使用I/O子系统中的块驱动程序(如前一节所述),也可以绕过I/O子系统直接使用持久内存作为字节可寻址的内存(通过load/store),这是访问持久内存中数据的最快和最短路径。除了消除I/O操作外,此路径还使得小数据写入比传统的块存储设备执行得更快,传统的块存储设备要求文件系统读取时以设备的块大小为粒度,修改块,然后再将整个块写回设备。
这些持久内存感知文件系统继续向应用程序提供了熟悉的标准文件API,包括open、close、read和write系统调用。这使得应用程序继续使用熟悉的文件API同时,还可以受益于持久内存更高的性能。
3.4 内存映射文件
在描述下一个使用持久内存的操作系统选项之前,本节将回顾Linux和Windows中的内存映射文件。当内存映射一个文件时,操作系统会将一个范围添加到应用程序的虚拟地址空间(对应于文件的一个范围),并根据需要将文件数据分页到物理内存中。这允许应用程序访问和修改文件数据,使其成为可在内存中寻址的字节数据结构。这有可能提高性能并简化应用程序开发,特别是对文件数据进行频繁、小规模更新的应用程序。
应用程序内存映射文件的方法是首先打开文件,然后将生成的文件句柄作为参数传递给Linux中的mmap()系统调用或Windows中的MapViewOfFile()。两者都返回指向文件某一部分内存副本的指针。清单3-1展示了一个Linux C代码示例,内存映射一个文件,通过像内存一样访问该文件来将数据写入该文件,然后使用msync系统调用执行I/O操作,将修改后的数据写入存储设备上的文件。清单3-2显示了Windows上的等效操作。我们遍历并突出显示两个代码示例中的关键步骤。
Listing 3-1. mmap_example.c – Memory-mapped file on Linux example
50 #include <err.h>
51 #include <fcntl.h>
52 #include <stdio.h>
53 #include <stdlib.h>
54 #include <string.h>
55 #include <sys/mman.h>
56 #include <sys/stat.h>
57 #include <sys/types.h>
58 #include <unistd.h>
59
60 int
61 main(int argc, char *argv[])
62 {
63 int fd;
64 struct stat stbuf;
65 char *pmaddr;
66
67 if (argc != 2) {
68 fprintf(stderr, "Usage: %s filename\n",
69 argv[0]);
70 exit(1);
71 }
72
73 if ((fd = open(argv[1], O_RDWR)) < 0)
74 err(1, "open %s", argv[1]);
75
76 if (fstat(fd, &stbuf) < 0)
77 err(1, "stat %s", argv[1]);
78
79 /*
80 * Map the file into our address space for read
81 * & write. Use MAP_SHARED so stores are visible
82 * to other programs.
83 */
84 if ((pmaddr = mmap(NULL, stbuf.st_size,
85 PROT_READ|PROT_WRITE,
86 MAP_SHARED, fd, 0)) == MAP_FAILED)
87 err(1, "mmap %s", argv[1]);
88
89 /* Don't need the fd anymore because the mapping
90 * stays around */
91 close(fd);
92
93 /* store a string to the Persistent Memory */
94 strcpy(pmaddr, "This is new data written to the
95 file");
96
97 /*
98 * Simplest way to flush is to call msync().
99 * The length needs to be rounded up to a 4k page.
100 */
101 if (msync((void *)pmaddr, 4096, MS_SYNC) < 0)
102 err(1, "msync");
103
104 printf("Done.\n");
105 exit(0);
106 }
- 第73行: 打开文件,文件不存在时会创建文件;
- 第76行: 当映射内存文件时,使用长度检索文件统计信息;
- 第84行: 映射文件到应用地址空间。第二个参数,传递文件长度,需要Linux初始化整个文件。后面的参数指定了可以读写,也允许其他进程映射相同的文件;
- 第91行: 一旦映射成功,就不需要了,可以close先前打开的文件句柄;
- 第94行: 像写内存一样写数据;
- 第101行: 显示调用将刚才写的内存写回到存储设备。
Listing 3-2 是Windows C代码样例,示例了内存映射文件、像访问内存一样写数据到文件,并且使用FlushViewOfFile() 和FlushFileBuffers() 系统调用执行IO操作把修改的数据写到存储设备上。
Listing 3-2. Memory-mapped file on Windows example
45 #include <fcntl.h>
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <string.h>
49 #include <sys/stat.h>
50 #include <sys/types.h>
51 #include <Windows.h>
52
53 int
54 main(int argc, char *argv[])
55 {
56 if (argc != 2) {
57 fprintf(stderr, "Usage: %s filename\n",
58 argv[0]);
59 exit(1);
60 }
61
62 /* Create the file or open if the file exists */
63 HANDLE fh = CreateFile(argv[1],
64 GENERIC_READ|GENERIC_WRITE,
65 0,
66 NULL,
67 OPEN_EXISTING,
68 FILE_ATTRIBUTE_NORMAL,
69 NULL);
70
71 if (fh == INVALID_HANDLE_VALUE) {
72 fprintf(stderr, "CreateFile, gle: 0x%08x",
73 GetLastError());
74 exit(1);
75 }
76
77 /*
78 * Get the file length for use when
79 * memory mapping later
80 * */
81 DWORD filelen = GetFileSize(fh, NULL);
82 if (filelen == 0) {
83 fprintf(stderr, "GetFileSize, gle: 0x%08x",
84 GetLastError());
85 exit(1);
86 }
87
88 /* Create a file mapping object */
89 HANDLE fmh = CreateFileMapping(fh,
90 NULL, /* security attributes */
91 PAGE_READWRITE,
92 0,
93 0,
94 NULL);
95
96 if (fmh == NULL) {
97 fprintf(stderr, "CreateFileMapping,
98 gle: 0x%08x", GetLastError());
99 exit(1);
100 }
101
102 /*
103 * Map into our address space and get a pointer
104 * to the beginning
105 * */
106 char *pmaddr = (char *)MapViewOfFileEx(fmh,
107 FILE_MAP_ALL_ACCESS,
108 0,
109 0,
110 filelen,
111 NULL); /* hint address */
112
113 if (pmaddr == NULL) {
114 fprintf(stderr, "MapViewOfFileEx,
115 gle: 0x%08x", GetLastError());
116 exit(1);
117 }
118
119 /*
120 * On windows must leave the file handle(s)
121 * open while mmaped
122 * */
123
124 /* Store a string to the beginning of the file */
125 strcpy(pmaddr, "This is new data written to
126 the file");
127
128 /*
129 * Flush this page with length rounded up to 4K
130 * page size
131 * */
132 if (FlushViewOfFile(pmaddr, 4096) == FALSE) {
133 fprintf(stderr, "FlushViewOfFile,
134 gle: 0x%08x", GetLastError());
135 exit(1);
136 }
137
138 /* Flush the complete file to backing storage */
139 if (FlushFileBuffers(fh) == FALSE) {
140 fprintf(stderr, "FlushFileBuffers,
141 gle: 0x%08x", GetLastError());
142 exit(1);
143 }
144
145 /* Explicitly unmap before closing the file */
146 if (UnmapViewOfFile(pmaddr) == FALSE) {
147 fprintf(stderr, "UnmapViewOfFile,
148 gle: 0x%08x", GetLastError());
149 exit(1);
150 }
151
152 CloseHandle(fmh);
153 CloseHandle(fh);
154
155 printf("Done.\n");
156 exit(0);
157 }
- 第45-75行:与前面的Linux示例一样,我们采用通过argv来传递文件名,并打开该文件;
- 第81行:检索文件大小,以便以后在内存映射时使用;
- 第89行:通过创建文件映射,我们迈出了内存映射文件的第一步。此步骤还尚未将文件映射到应用程序的内存空间;
- 第106行:这一步将文件映射到我们的内存空间;
- 第125行:和前面的Linux示例一样,我们在文件的开头写一个字符串,像访问内存一样访问文件;
- 第132行:我们将修改后的内存页刷到后端存储器;
- 第139行:我们将整个文件刷到后端存储,包括由Windows维护的任何其他文件元数据;
- 第146-157行:解压文件,关闭文件,然后退出程序;
- 第125行: 像访问内存一样,写数据。
图3-4显示了应用在Linux上调用mmap(),以及在Windows上调用 CreateFileMapping()时操作系统的内部机理。操作系统从内存页缓存中分配内存,映射内存到应用地址空间,并通过存储设备驱动创建与文件的关联。
当应用程序在内存中读取文件的页面时,如果这些页面不在内存中,则会向操作系统引发页面错误异常,然后操作系统将通过存储I/O操作将该页面读入主内存。操作系统还跟踪对这些内存页的写操作,并安排异步I/O操作,将修改写回存储设备上文件的主副本。或者,如果应用程序希望在继续执行代码示例中的操作之前确保更新被写回存储,那么Linux上的msync系统调用或Windows上的flushviewoffice都可以把更新刷到磁盘。这可能会导致操作系统在写入完成之前挂起程序,类似于前面描述的文件写入操作。
使用存储的内存映射文件,其突出缺点有三:首先,主内存中有限的内核内存页缓存的一部分得用于存储文件的副本;其次,对于无法装入内存的文件,当操作系统通过I/O操作在内存和存储之间移动页面时,应用程序可能会遇到不可预知的暂停,该暂停的时长不固定,也不可预知;第三,对内存副本的更新在写回存储器之前不是持久的,因此在发生故障时可能会丢失。
3.5 持久内存直访(DAX)
操作系统中的持久内存直接访问特性,在Linux和Windows上称为DAX,就是使用前面所述的内存映射文件接口,利用持久内存本身的能力达到存储数据和当作内存使用的双重目的。持久内存本身可以被映射成应用内存,因此操作系统不需要在易失性主存中缓存文件。
要使用DAX,系统管理员在持久内存模块上创建一个文件系统,并将该文件系统装载到操作系统的文件系统树中。对于Linux用户,持久内存设备将显示为/dev/pmem*设备特殊文件。为了显示持久内存物理设备,系统管理员可以使用清单3-3和3-4中所示的ndctl和ipmctl实用程序。
Listing 3-3. 在Linux上显示持久内存物理设备和Region域。
# ipmctl show -dimm
DimmID | Capacity | HealthState | ActionRequired | LockState | FWVersion =========================================================== 0x0001 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x0011 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x0021 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x0101 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x0111 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x0121 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x1001 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x1011 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x1021 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x1101 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x1111 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
0x1121 | 252.4 GiB | Healthy | 0 | Disabled | 01.02.00.5367
# ipmctl show -region
SocketID | ISetID | PersistentMemoryType | Capacity | FreeCapacity | HealthState =========================================================
0x0000 | 0x2d3c7f48f4e22ccc | AppDirect | 1512.0 GiB | 0.0 GiB | Healthy
0x0001 | 0xdd387f488ce42ccc | AppDirect | 1512.0 GiB | 1512.0 GiB | Healthy
Listing 3-4. 在Linux上显示持久内存物理设备、Region域、Namespace
# ndctl list -DRN
# ndctl list -DRN {
"dimms":[
{
"dev":"nmem1",
"id":"8089-a2-1837-00000bb3",
"handle":17,
"phys_id":44,
"security":"disabled"
},
{
"dev":"nmem3",
"id":"8089-a2-1837-00000b5e",
"handle":257,
"phys_id":54,
"security":"disabled"
},
[...snip...]
{
"dev":"nmem8",
"id":"8089-a2-1837-00001114",
"handle":4129,
"phys_id":76,
"security":"disabled"
}
],
"regions":[
{
"dev":"region1",
"size":1623497637888,
"available_size":1623497637888,
"max_available_extent":1623497637888,
"type":"pmem",
"iset_id":-2506113243053544244,
"mappings":[
{
"dimm":"nmem11",
"offset":268435456,
"length":270582939648,
"position":5
},
{
"dimm":"nmem10",
"offset":268435456,
"length":270582939648,
"position":1
},
{
"dimm":"nmem9",
"offset":268435456,
"length":270582939648,
"position":3
},
{
"dimm":"nmem8",
"offset":268435456,
"length":270582939648,
"position":2
},
{
"dimm":"nmem7",
"offset":268435456,
"length":270582939648,
"position":4
},
{
"dimm":"nmem6",
"offset":268435456,
"length":270582939648,
"position":0
}
],
"persistence_domain":"memory_controller"
},
{
"dev":"region0",
"size":1623497637888,
"available_size":0,
"max_available_extent":0,
"type":"pmem",
"iset_id":3259620181632232652,
"mappings":[
{
"dimm":"nmem5",
"offset":268435456,
"length":270582939648,
"position":5
},
{
"dimm":"nmem4",
"offset":268435456,
"length":270582939648,
"position":1
},
{
"dimm":"nmem3",
"offset":268435456,
"length":270582939648,
"position":3
},
{
"dimm":"nmem2",
"offset":268435456,
"length":270582939648,
"position":2
},
{
"dimm":"nmem1",
"offset":268435456,
"length":270582939648,
"position":4
},
{
"dimm":"nmem0",
"offset":268435456,
"length":270582939648,
"position":0
}
],
"persistence_domain":"memory_controller",
"namespaces":[
{
"dev":"namespace0.0",
"mode":"fsdax",
"map":"dev",
"size":1598128390144,
"uuid":"06b8536d-4713-487d-891d-795956d94cc9",
"sector_size":512,
"align":2097152,
"blockdev":"pmem0"
}
]
}
]
}
当文件系统创建并挂载后,可以使用df命令查看信息,如Listing 3-5所示:
Listing 3-5. 在Linux上定位持久内存
$ df -h /dev/pmem* Filesystem
Size Used Avail Use% Mounted on /dev/pmem0
1.5T 77M 1.4T 1% /mnt/pmemfs0 /dev/pmem1
1.5T 77M 1.4T 1% /mnt/pmemfs1
Windows开发者使用PowerShellCmdlets 来查看,如Listing 3-6所示。
Listing 3-6. 在Windows上定位持久内存。
PS C:\Users\Administrator> Get-PmemDisk
Number Size Health Atomicity Removable Physical device IDs Unsafe shutdowns
---- ---- ------ --------- --------- ------------------- ---------------
2 249 GB Healthy None True {1} 36
PS C:\Users\Administrator> Get-Disk 2 | Get-Partition
PartitionNumber DriveLetter Offset Size Type
--------------- ----------- ------ ---- ---
1 24576 15.98 MB Reserved
2 D 16777216 248.98 GB Basic
以文件方式管理持久内存有以下几个好处:
- 可以利用主流文件系统的丰富功能来组织、管理、命名和限制对用户持久内存文件和目录的访问;
- 可以应用熟悉的文件系统权限和访问权限管理来保护存储在持久内存中的数据,并在多个用户之间共享持久内存;
- 系统管理员可以使用依赖于文件系统变更历史记录来进行跟踪变化的现有备份工具;
- 可以构建在前述现有内存映射API和当前使用内存映射文件的应用程序上,并且可以直接使用持久内存而无需修改。
一旦持久内存创建并被打开,应用要调用mmap() 或者MapViewOfFile()来获得一个指向持久内存的指针。如图3-5所示,持久内存感知文件系统识别到文件是在持久内存上的,并通过CPU中的MMU单元直接映射持久内存到应用地址空间。既不需要COPY到内核内存,也不需要通过IO操作同步到存储。应用使用mmap() 或者MapViewOfFile()返回的指针来操作持久内存上的数据。因为不需要内核IO操作,并且整个文件已经映射到应用内存,所以可以操作大型数据对象集合,并可以提供比IO存储更高的性能和一致性。
清单3-7 展示了一个C代码样例,使用DAX直接写一个string到持久内存中。该样例使用了libpmem库中的持久内存API。该库在Linux和Windows上都支持。尽管这些库在后面章节中有深入的讨论,但我们还是在后面的步骤中描述了libpmem中的两个函数。Libpmem中的API通常Linux和Windows上都一样,所以该样例可以在这两个系统上通用。
Listing 3-7. DAX programming example
32 #include <sys/types.h>
33 #include <sys/stat.h>
34 #include <fcntl.h>
35 #include <stdio.h>
36 #include <errno.h>
37 #include <stdlib.h>
38 #ifndef _WIN32
39 #include <unistd.h>
40 #else
41 #include <io.h>
42 #endif
43 #include <string.h>
44 #include <libpmem.h>
45
46 /* Using 4K of pmem for this example */
47 #define PMEM_LEN 4096
48
49 int
50 main(int argc, char *argv[])
51 {
52 char *pmemaddr;
53 size_t mapped_len;
54 int is_pmem;
55
56 if (argc != 2) {
57 fprintf(stderr, "Usage: %s filename\n",
58 argv[0]);
59 exit(1);
60 }
61
62 /* Create a pmem file and memory map it. */
63 if ((pmemaddr = pmem_map_file(argv[1], PMEM_LEN,
64 PMEM_FILE_CREATE, 0666, &mapped_len,
65 &is_pmem)) == NULL) {
66 perror("pmem_map_file");
67 exit(1);
68 }
69
70 /* Store a string to the persistent memory. */
71 char s[] = "This is new data written to the file";
72 strcpy(pmemaddr, s);
73
74 /* Flush our string to persistence. */
75 if (is_pmem)
76 pmem_persist(pmemaddr, sizeof(s));
77 else
78 pmem_msync(pmemaddr, sizeof(s));
79
80 /* Delete the mappings. */
81 pmem_unmap(pmemaddr, mapped_len);
82
83 printf("Done.\n");
84 exit(0);
85 }
- 第38-42行: 处理linux和windows上的不同;
- 第44行: 引入libpmem中所用API的头文件;
- 第56-60行: 从命令行参数传入文件路径;
- 第63-68行: libpmem中的pmem_map_file函数处理打开文件并将其映射到Windows和Linux上的地址空间。由于文件驻留在持久内存中,操作系统对CPU中的硬件MMU进行编程,以将持久内存区域映射到应用程序的虚拟地址空间中。指针 pmemaddr 被设置成指向region域的开始位置。 pmem_map_file函数用来映射磁盘文件到内存,也可以映射持久内存到内存,如果文件在持久内存上, is_pmem要设置成TRUE,否则设成FALSE;
- 第72行:写一个string 到持久内存;
- 第75-78行: 如果文件驻留在持久内存中,pmem_uupersist函数使用用户空间机器指令(在第2章中描述)来确保字符串通过CPU缓存刷到电源故障安全域,并最终刷到持久内存。如果我们的文件驻留在基于磁盘的存储上,那么将使用Linux mmap或Windows FlushViewOfFile刷到存储。注意,我们可以在这里传递小的尺寸(本例中使用的是所写字符串的大小),而不需要在使用msync() 或FlushViewOfFile() 时以页面粒度进行刷新;
- 第81行: 最后,取消到持久内存region域的映射。
3.6 本章小结
图3-6 展示了本章节所述的操作系统持久内存接口完整视图。应用可以把持久内存当前快速SSD,直接通过持久内存感知文件系统,或者直通过DAX接映射到应用内存空间。DAX有效利用了操作系统的内存映射文件服务,并利用硬件能力直接映射持久内存到应用地址空间。这避免了在主存和存储之间移动数据。接下来的章节描述了在持久内存中直接处理数据的仔细考量,并讨论为了简化开发的这些API。