NVML是Intel提供的一个操作NVM的库。背景是把NVM硬件当成一个文件,通过mmap映射到内存空间中来。为了方便使用,在裸的NVM的基础上包装了一些函数,类似于libc中malloc、free等函数,来方便程序员进行内存、事务等管理。其中,libpmem是NVML里面最基础的一个库,也是比较简单的。本文分析了libpmem的源码。

通过libpmem的文档,我们可以发现,libpmem提供的接口分成四个部分:

  1. 基本操作,就是map/unmap 文件到内存空间,以及把数据刷到文件中去;
  2. flush操作,把数据从cache刷到内存中。
  3. copy操作,把数据从普通内存拷贝到NVM中
  4. 版本check。这个应该是生产环境下用的。忽略之。

background:关于mmap的用法,可以参考这篇文章

基本操作

pmem_map_file : 简单来说就是把一个文件映射到内存地址空间;如果文件不存在可以根据flag来决定是否创建。开头是一堆flag的设置和check;然后试图打开/创建文件。如果是创建的话,还需要用ftruncate/posix_fallocate来控制文件大小。如果是open就检查文件大小。最后调用一个mmap,将该文件映射到一个地址addr上,返回addr。
pmem_unmap : 基本就是调用一个unmap。

pmem_is_pmem : 检查一个地址是否在pmem上,这个函数比较复杂。实际上就是检查map之后对应的虚拟地址空间是不是persistent的。如果返回false,说明没有映射到的地址空间不是persistent的,所以需要调用msync来确保durable;如果返回true,说明这一段地址空间数据已经是durable的了,所以直接把数据从cache刷到内存中即可。
下面是伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int is_pmem_proc(const void* addr, size_t len){
/* 打开该程序的map文件
该文件的格式是
lo-hi xxx
xxxx
VMFlags: xx xx xx
*/
open('/proc/self/smaps');
for (line in fd){ // 遍历所有的行
get lo, hi from line; // 获取地址范围
if (addr in lo-hi){
check 'mm' in VMFlags //这个flag的意义我暂时还没搞明白
if (true){ // 检查下一段。因为该地址空间可能跨多个内存块。
len-= (hi - addr)
addr = hi
if (len <= 0){
return 1
}
}
}
}
return 0
}

现阶段如果没有pmem,这个函数总是返回false。性能很差,所以就覆盖掉了,让它总是返回true。这也就是为什么想要测试性能的时候,需要加上PMEM_IS_PMEM_FORCE=1 去跳过这个函数。

pmem_persist : 通过调用flush的操作和fence,进行持久化。

pmem_msync : 调用msync进行同步,注意的是msync一般都是4kB为单位的,所以需要做一个地址的补齐。

刷数据操作

这里提供了两个函数:pmem_flush 和 pmem_drain。前者是把数据从cache刷下去,后者是建立一个fence。pmem_persist也就是通过调用这两个函数来实现的。其中,根据硬件条件,会分为以下几种情况:

  1. 支持CLWB。
    flush 调用flush_clwb, fence 调用sfence。
  2. 支持clflushopt
    flush 调用clflushopt, fence 调用sfence。
  3. 支持clflush
    flush直接用clflush,fence为空。

这些判断会在pmem_init的时候设定好。

拷贝数据操作

除此之外,该库还提供了几个内存拷贝的操作,把数据从DRAM中拷到NVM中。
包括:pmem_memmove_persist、pmem_memcpy_persist、pmem_memset_persist、pmem_memmove_nodrain、pmem_memcpy_nodrain、pmem_memset_nodrain。
从名字就能看出来具体的含义。move和copy执行的是同一个操作,xx_xx_persist函数都是后面的xx_xx_nodrain函数加上一个fence的。

所以核心操作基本是:

1
2
3
memmove()/memcopy()
pmem_persist()
pmem_drain()

其中 memmove和memcopy会根据是不是支持streaming SIMD 来进行优化。

最后贴一下pmem中初始化的函数。如下所示,会根据cpu支持的技术来确定不同的flush、fence、memmove和memset的策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
static void
pmem_get_cpuinfo(void)
{

if (is_cpu_clflush_present()) {
Func_is_pmem = is_pmem_proc;
LOG(3, "clflush supported");
}

if (is_cpu_clflushopt_present()) {
LOG(3, "clflushopt supported");

char *e = getenv("PMEM_NO_CLFLUSHOPT");
if (e && strcmp(e, "1") == 0)
LOG(3, "PMEM_NO_CLFLUSHOPT forced no clflushopt");
else {
Func_flush = flush_clflushopt;
Func_predrain_fence = predrain_fence_sfence;
}
}

if (is_cpu_clwb_present()) {
LOG(3, "clwb supported");

char *e = getenv("PMEM_NO_CLWB");
if (e && strcmp(e, "1") == 0)
LOG(3, "PMEM_NO_CLWB forced no clwb");
else {
Func_flush = flush_clwb;
Func_predrain_fence = predrain_fence_sfence;
}
}

if (Func_flush == flush_clwb)
LOG(3, "using clwb");
else if (Func_flush == flush_clflushopt)
LOG(3, "using clflushopt");
else if (Func_flush == flush_clflush)
LOG(3, "using clflush");
else
ASSERT(0);

if (is_cpu_sse2_present()) {
LOG(3, "movnt supported");

char *e = getenv("PMEM_NO_MOVNT");
if (e && strcmp(e, "1") == 0)
LOG(3, "PMEM_NO_MOVNT forced no movnt");
else {
Func_memmove_nodrain = memmove_nodrain_movnt;
Func_memset_nodrain = memset_nodrain_movnt;
}
}

if (Func_memmove_nodrain == memmove_nodrain_movnt)
LOG(3, "using movnt");
else if (Func_memmove_nodrain == memmove_nodrain_normal)
LOG(3, "not using movnt");
else
ASSERT(0);
}