概述
Persistent Memory编程简介
- 编程
- libpmem
- 持久化函数
- libpmemobj
- 跟对象 root object
- 例程
- 事务支持
- type safety
- 线程安全
- 管理工具
- ipmctl
- ndctl
- create-namespace
- 例子
- 测试工具
- fio
- pmembench
- ipmwatch
- emon
- pcm
- 参考链接
本文主要目的是介绍PM基础的的编程方法、管理工具、监测手段等
编程
- 持久内存开发套件(Persistent Memory Development Kit-PMDK) - pmem.io: PMDK
- PMDK based Persistent Memory Programming
libpmem
- libpmem简介
peme底层库,不支持事务,编程方法如下:
#include <libpmem.h>
// 其他头文件省略
/* using 4k of pmem for this example */
#define PMEM_LEN 4096
int
main(int argc, char *argv[])
{
int fd;
char *pmemaddr;
int is_pmem;
/* 1. 打开pm文件 */
if ((fd = open("/pmem-fs/myfile", O_CREAT|O_RDWR, 0666)) < 0) {
perror("open");
exit(1);
}
/* 2. 创建固定的文件大小,分配4k大小 */
if ((errno = posix_fallocate(fd, 0, PMEM_LEN)) != 0) {
perror("posix_fallocate");
exit(1);
}
/* 3. mmap这个pm文件 */
// 这里也可以用系统调用mmap,只不过pmem版本效率更高
// 也可以使用pmem_map_file直接map文件
if ((pmemaddr = pmem_map(fd)) == NULL) {
perror("pmem_map");
exit(1);
}
// 4. 只要mmap之后,fd就可以关闭了。
close(fd);
/* determine if range is true pmem */
is_pmem = pmem_is_pmem(pmemaddr, PMEM_LEN);
/* 使用libc系统调用访问pm,但是这种方法无法确定该数据何时落盘PM,cacheline刷盘的顺序也不保证 */
// 这里多说一句,cpu的cacheline下刷机制本身就是没有顺序保证的。
strcpy(pmemaddr, "hello, persistent memory");
/* 通过正确的方式访问PM */
if (is_pmem) {
// 这个函数拷贝完后会直接持久化
pmem_memcpy(pmemaddr, buf, cc);
} else {
memcpy(pmemaddr, buf, cc);
pmem_msync(pmemaddr, cc);
}
/* copy the file, saving ~the last flush step to the end */
while ((cc = read(srcfd, buf, BUF_LEN)) > 0) {
// 只拷贝,不持久化
pmem_memcpy_nodrain(pmemaddr, buf, cc);
pmemaddr += cc;
}
if (cc < 0) {
perror("read");
exit(1);
}
/* 和上述的nodrain联合使用,持久化数据 */
pmem_drain();
/* 持久化cacheline中的数据
*/
if (is_pmem)
// 通过在用户态调用CLWB and CLFLUSHOPT指令,达到高效刷盘的目的
pmem_persist(pmemaddr, PMEM_LEN);
else
// 实际上就是系统调用msync()
pmem_msync(pmemaddr, PMEM_LEN);
}
注意,mmap的一般用法是mmap一个普通文件,其持久化的方法是使用系统调用msync()
来flush,这个指令在pmem上是相对较慢的,所以如果使用pmem(可以用pmem_is_pmem确认)可以使用pm的persist函数pmem_persist
,可以使用环境变量PMEM_IS_PMEM_FORCE=1
强行指定不适用msync()
持久化函数
以下是目前所有的和持久化相关的函数
#include <libpmem.h>
void pmem_persist(const void *addr, size_t len); // 将对应的区域强制持久化下去,相当于调用msync(),调用该函数不需要考虑align(如果不align,底层会扩大sync范围到align)
int pmem_msync(const void *addr, size_t len);
// 相当于调用msync,和pmem_persist功能一致。 Since it calls msync(), this function works on either persistent memory or a memory mapped file on traditional storage. pmem_msync() takes steps to ensure the alignment of addresses and lengths passed to msync() meet the requirements of that system call.
void pmem_flush(const void *addr, size_t len);
// 这个的粒度应该是cacheline
void pmem_deep_flush(const void *addr, size_t len); (EXPERIMENTAL)
// 不考虑PMEM_NO_FLUSH变量,一定会flushcpu寄存器
int pmem_deep_drain(const void *addr, size_t len); (EXPERIMENTAL)
int pmem_deep_persist(const void *addr, size_t len); (EXPERIMENTAL)
void pmem_drain(void);
int pmem_has_auto_flush(void); (EXPERIMENTAL)
// 检测CPU是否支持power failure时自动flush cache
int pmem_has_hw_drain(void);
调用pmem_persist
相当于调用了sync和drain
void
pmem_persist(const void *addr, size_t len)
{
/* flush the processor caches */
pmem_flush(addr, len);
/* wait for any pmem stores to drain from HW buffers */
pmem_drain();
}
讨论x86-64环境
pmem_flush
含义是调用clflush
将对应的区域flush下去。flush系指令的封装,只不过libpmem会在装载时获取相关信息自动选择最优的指令- CLFLUSH会命令cpu将对应cacheline逐出,强制性的写回介质,这在一定程度上可以解决我们的问题,但是这是一个同步指令,将会阻塞流水线,损失了一定的运行速度,于是Intel添加了新的指令CLFLUSHOPT和CLWB,这是两个异步的指令。尽管都能写回介质,区别在前者会清空cacheline,后者则会保留,这使得在大部分场景下CLWB可能有更高的性能。
- 一般的
pmem_memmove(), pmem_memcpy() and pmem_memset()
在下发完成之后都会flush的,除非指定PMEM_F_MEM_NOFLUSH
pmem_drain
含义是调用sfense
等待所有的pipline都下刷到PM完成(等待其他的store指令都完成才会返回)- 上面flush异步的代价是我们对于cache下刷的顺序依旧不可预测,考虑到有些操作需要顺序保证,于是我们需要使用SFENCE提供保证,SFENCE强制sfence指令前的写操作必须在sfence指令后的写操作前完成。
- 考虑到pmem_drain可能会阻塞一些操作,更好的做法是对数据结构里互不相干的几个字段分别flush,最后一并调用pmem_drain,以将阻塞带来的问题降到最低。
- programs using
pmem_flush()
to flush ranges of memory should still follow up by callingpmem_drain()
once to ensure the flushes are complete. - 还有一个flag
PMEM_F_MEM_NONTEMPORAL
,使用这个flag下发的IO,会绕过CPU cache,直接下刷到PM里。
The main feature of libpmem library is to provide a method to flush dirty data to persistent memory. Commonly used functions mainly include pmem_flush, pmem_drain, pmem_memcpy_nodrain. Since the timing and sequence of the CPU CACHE content flashing to the PM is not controlled by the user, a specific instruction is required for forced flashing. The function of pmem_flush is to call the
CLWB
,CLFLUSHOPT
orCLFLUSH
instructions to force the content in the CPU CACHE (in cache line as a unit) to be flushed to the PM; after the instruction is initiated, because the CPU is multi-core, the order of the content in the cache to the PM is different, so It also needs pmem_drain to call theSFENCE
instruction to ensure that allCLWBs
are executed. If the instruction called by pmem_flush isCLFLUSH
, the instruction contains sfence, so in theory there is no need to call pmem_drain, in fact, if it is this instruction, pmem_drain does nothing.The above describes the function of flashing the contents of the CPU cache to the PM. The following describes memory copy, which means copying data from memory to PM. This function is completed by
pmem_memcpy_nodrain
, calling the MOVNT instruction (MOV or MOVNTDQ), the instruction copy does not go through the CPU CACHE, so this function does not require flush. But you need to establish a sfence at the end to ensure that all data has been copied to the PM.
libpmemobj
- libpmemobj简介
- libpmemobj api之类的文档
libpmem的上层封装,所有对pmem的操作都抽象为obj pool的形式。
pmemobj_create
创建obj poolpmemobj_open
打开已经创建的objpmemobj_close
关闭对应的objpmemobj_check
对metadata进行校验
libpmemobj的内存指针是普通指针的两倍大,它说明了该pool是指向那个obj pool的,和其中的offset
typedef struct pmemoid {
uint64_t pool_uuid_lo;
// 具体的某个obj,通过cuckoo hash table的两层哈希对应到实际的地址pool
uint64_t off;
// 对应的offset
} PMEMoid;
// 我们把它叫做persistent pointer
因此,从这个指针数据结构需要(void *)((uint64_t)pool + oid.off)
这样的转换,才能转到实际的地址,这就是pmemobj_direct
作的事情。
跟对象 root object
根据官方的说法,根对象的作用就是一个访问持久内存对象的入口点,是一个锚的作用。使用如下方式
pmemobj_root(PMEMobjpool* pop, size_t size)
:非类型化的原始API。create或者resize根对象,根据官方文档的描述,当你初次调用这个函数的时候,如果size大于0并且没有根对象存在,则会分配空间并创建一个根对象。当size大于当前根对象的size的时候会进行重分配并resize。POBJ_ROOT(PMEMobjpool* pop, TYPE)
:这是一个宏,传入的TYPE是根对象的类型,并且最后返回值类型是一个void指针
例程
#include <stdio.h>
#include <string.h>
#include <libpmemobj.h>
// layout
#define LAYOUT_NAME "intro_0" /* will use this in create and open */
#define MAX_BUF_LEN 10 /* maximum length of our buffer */
struct my_root {
size_t len; /* = strlen(buf) */
char buf[MAX_BUF_LEN];
};
int main(int argc, char *argv[])
{
// 创建pool
PMEMobjpool *pop = pmemobj_create(argv[1], LAYOUT_NAME, PMEMOBJ_MIN_POOL, 0666);
if (pop == NULL) {
perror("pmemobj_create");
return 1;
}
// 创建pm root对象(已经zeroed了),并通过pmemobj_direct将其转化为一个void指针
PMEMoid root = pmemobj_root(pop, sizeof (struct my_root));
struct my_root *rootp = pmemobj_direct(root);
char buf[MAX_BUF_LEN];
// 先给pm对象赋值
rootp->len = strlen(buf);
// 然后持久化,记得8byte原子写
pmemobj_persist(pop, &rootp->len, sizeof (rootp->len));
// 写数据,顺便持久化
pmemobj_memcpy_persist(pop, rootp->buf, my_buf, rootp->len);
// 持久化之后就可以像正常内存那样读写了
if (rootp->len == strlen(rootp->buf))
printf("%sn", rootp->buf);
pmemobj_close(pop);
return 0;
}
事务支持
/* TX_STAGE_NONE */
TX_BEGIN(pop) {
/* TX_STAGE_WORK */
} TX_ONCOMMIT {
/* TX_STAGE_ONCOMMIT */
} TX_ONABORT {
/* TX_STAGE_ONABORT */
} TX_FINALLY {
/* TX_STAGE_FINALLY */
} TX_END
/* TX_STAGE_NONE */
整个事务的流程可以通过这几个宏以及代码块来定义,并且将事务分成了多个阶段,中间的三个阶段为可选的,最基本的一个事务流程是TX_BEGIN-TX_END,这也是最常用的部分,其他的几个部分在嵌套事务中使用较多。
除了基本的事务代码块,libpmemobj还提供了相应的事务操作API。
一个是事务性数据写入API:pmemobj_tx_add_range&pmemobj_tx_add_range_direct,add_range函数主要有三个参数:root object、offset以及size,该函数表示我们将会操作[offset, offset+size)这段内存空间,PMDK将会自动在undo log中分配一个新的对象,然后将这段空间的内容记录到undo log中,这样我们就能随机去修改这段空间的内容并且保证一致性。带上direct标志的函数用法一致,区别在于direct函数直接操作的是一段虚拟地址空间。
type safety
- An introduction to pmemobj (part 3) - types
- Type safety macros in libpmemobj
libpmemobj使用了一系列macro来将persistent pointer和某个具体类型联系起来
Feature | Anonymous unions | Named unions |
---|---|---|
Declaration | + | - |
Assignment | - | + |
Function parameter | - | + |
Type numbers | - | + |
pmdk/src/examples/libpmemobj/string_store_tx_type/writer.c
例程如下:
#include <stdio.h>
#include <string.h>
#include <libpmemobj.h>
#include "layout.h"
int
main(int argc, char *argv[])
{
if (argc != 2) {
printf("usage: %s file-namen", argv[0]);
return 1;
}
PMEMobjpool *pop = pmemobj_create(argv[1],
POBJ_LAYOUT_NAME(string_store), PMEMOBJ_MIN_POOL, 0666);
if (pop == NULL) {
perror("pmemobj_create");
return 1;
}
char buf[MAX_BUF_LEN] = {0};
int num = scanf("%9s", buf);
if (num == EOF) {
fprintf(stderr, "EOFn");
return 1;
}
TOID(struct my_root) root = POBJ_ROOT(pop, struct my_root);
// D_RW 写
TX_BEGIN(pop) {
TX_MEMCPY(D_RW(root)->buf, buf, strlen(buf));
} TX_END
// D_RO()读
printf("%sn", D_RO(root)->buf);
pmemobj_close(pop);
return 0;
}
通过TOID_VALID
验证对应的type是否合法
if (TOID_VALID(D_RO(root)->data)) {
/* can use the data ptr safely */
} else {
/* declared type doesn't match the object */
}
在transaction里面可以使用TX_NEW
创建新的对象
TOID(struct my_root) root = POBJ_ROOT(pop);
TX_BEGIN(pop) {
TX_ADD(root); /* we are going to operate on the root object */
TOID(struct rectangle) rect = TX_NEW(struct rectangle);
D_RW(rect)->x = 5;
D_RW(rect)->y = 10;
D_RW(root)->rect = rect;
} TX_END
线程安全
所有的libpmemobj函数都是线程安全的。除了管理obj pool的函数例如open、close和pmemobj_root
,宏里面只有FOREACH
的不是线程安全的。
我们可以将pthread_mutex_t
类放到pm里,叫做pmem-aware lock,下面是一个简单的例子
struct foo {
PMEMmutex lock;
int bar;
};
int fetch_and_add(TOID(struct foo) foo, int val) {
pmemobj_mutex_lock(pop, &D_RW(foo)->lock);
int ret = D_RO(foo)->bar;
D_RW(foo)->bar += val;
pmemobj_mutex_unlock(pop, &D_RW(foo)->lock);
return ret;
}
管理工具
ipmctl
PM的管理工具
ipmctl create -goal PersistentMemoryType=AppDirect
创建AppDirect GOALipmctl show -firmware
查看DIMM固件版本ipmctl show -dimm
列出DIMMipmctl show -sensor
获取更多详细信息,类似SMARTipmctl show -topology
定位device位置
ndctl
管理“libnvdimm
”对应的系统设备(Non-volatile Memory),常用命令:
ndctl list -u
create-namespace
通过fsdax, devdax, sector, and raw这四种方式管理PM的namespace
- fsdax,默认模式,创建之后将在文件系统下创建块设备
/dev/pmemX[.Y]
,可以在其上创建xfs、ext4文件系统。**DAX(direct access) removes the page cache from the I/O path and allows mmap to establish direct mappings to persistent memory media.**使用这种的好处是可以多个进程共享同一块PM。 - devdax,创建之后在文件系统下创建char device
/dev/daxX.Y
,没有块设备映射出来。但是使用这种方式仍然可以通过mmap映射。(只可以使用open()
,close()
,mmap()
)
一个create-namespace的典型命令如下:
ndctl create-namespace --type=pmem --mode=fsdax --region=X [--align=4k]
# --region 指定某个pmem设备,不写的话默认是all,全部设备
# --align,内部的对齐的pagesize,默认2M,每次page fault之后读上2M的页
例子
- 通过FSDAX初始化pmem
ndctl create-namespace
mkfs.xfs -f -d su=2m,sw=1 /dev/pmem0
mkdir /pmem0
mount -o dax /dev/pmem0 /pmem0
xfs_io -c "extsize 2m" /pmem0
测试工具
fio
首先要选ioengine,有以下几种选择:
- libpmem:使用fsdax配置pmem namespace的模式,也是比较常用的模式。这里提供了个小例子
- dev-dax:针对devdax的pmem设备
- pmemblk:使用
libpmemblk
库读写pm - mmap:非PM特有,使用posix系统调用跑IO(mmap、fdatasync…)
- 默认的读操作是将PM中的数据拷贝到内存中
- 默认的写操作是将内存中的数据拷贝到PM中,
--sync=sync
或者--sync=dsync
或者--sync=1
代表每次写数据之后都会drain,默认或者--sync=0
代表按需调用pmem_drain()
(调用pmem_memcpy
的时候会增加标志位PMEM_F_MEM_NODRAIN
),使用--direct=1
增加标志位PMEM_F_MEM_NONTEMPORAL
- 可以使用fio选项
fsync=int
或者fdatasync=int
,确保在下发多少个write命令之后,会下发一个sync也就是pmem_drain()
。
- 可以使用fio选项
pmembench
ipmwatch
查看吞吐,包括PM内部真正的读写数据,在Intel VTune Amplifier 2019 since Update 5有包含,安装vtune_profiler_2020
里面肯定有,我把一些数据名称列在下面
bytes_read (derived) bytes_written (derived) read_hit_ratio (derived) write_hit_ratio (derived) wdb_merge_percent (derived) media_read_ops (derived) media_write_ops (derived) read_64B_ops_received write_64B_ops_received ddrt_read_ops ddrt_write_ops
emon
查看耗时
pcm
intel的pcm工具集有一系列工具查看cpu和其访问memory的性能指标。例如pcm-memory.x
可以查看当前PM的性能指标
|---------------------------------------|
|--
Socket
0
--|
|---------------------------------------|
|--
Memory Channel Monitoring
--|
|---------------------------------------|
|-- Mem Ch
0: Reads (MB/s):
227.67 --|
|--
Writes(MB/s):
43.34 --|
|--
PMM Reads(MB/s)
:
0.00 --|
|--
PMM Writes(MB/s)
:
0.00 --|
|-- Mem Ch
1: Reads (MB/s):
0.00 --|
|--
Writes(MB/s):
0.00 --|
|--
PMM Reads(MB/s)
:
355.99 --|
|--
PMM Writes(MB/s)
:
355.99 --|
|-- Mem Ch
2: Reads (MB/s):
209.37 --|
|--
Writes(MB/s):
42.72 --|
|--
PMM Reads(MB/s)
:
0.00 --|
|--
PMM Writes(MB/s)
:
0.00 --|
|-- Mem Ch
3: Reads (MB/s):
211.65 --|
|--
Writes(MB/s):
42.81 --|
|--
PMM Reads(MB/s)
:
0.00 --|
|--
PMM Writes(MB/s)
:
0.00 --|
|-- Mem Ch
4: Reads (MB/s):
0.00 --|
|--
Writes(MB/s):
0.00 --|
|--
PMM Reads(MB/s)
:
356.08 --|
|--
PMM Writes(MB/s)
:
356.08 --|
|-- Mem Ch
5: Reads (MB/s):
205.36 --|
|--
Writes(MB/s):
42.57 --|
|--
PMM Reads(MB/s)
:
0.00 --|
|--
PMM Writes(MB/s)
:
0.00 --|
|-- NODE 0 Mem Read (MB/s) :
854.05 --|
|-- NODE 0 Mem Write(MB/s) :
171.44 --|
|-- NODE 0 PMM Read (MB/s):
712.08 --|
|-- NODE 0 PMM Write(MB/s):
712.08 --|
|-- NODE 0.0 NM read hit rate :
1.00 --|
|-- NODE 0.1 NM read hit rate :
1.00 --|
|-- NODE 0.2 NM read hit rate :
0.00 --|
|-- NODE 0.3 NM read hit rate :
0.00 --|
|-- NODE 0 Memory (MB/s):
2449.64 --|
|---------------------------------------|
|---------------------------------------||---------------------------------------|
|--
System DRAM Read Throughput(MB/s):
854.05
--|
|--
System DRAM Write Throughput(MB/s):
171.44
--|
|--
System PMM Read Throughput(MB/s):
712.08
--|
|--
System PMM Write Throughput(MB/s):
712.08
--|
|--
System Read Throughput(MB/s):
1566.12
--|
|--
System Write Throughput(MB/s):
883.52
--|
|--
System Memory Throughput(MB/s):
2449.64
--|
|---------------------------------------||---------------------------------------|
参考链接
- Direct Write to PMem how to disable DDIO
- Correct, Fast Remote PersistenceDDIO是在CPU层面enable的。
- 基于RDMA和NVM的大数据系统一致性协议研究
- pmem/valgrind
- PMDK based Persistent Memory Programming
- Running FIO with pmem engines
- Documentation for ndctl and daxctl
- AEPWatch
- CHAPTER 5. USING NVDIMM PERSISTENT MEMORY STORAGE
- I/O Alignment Considerations里面有一些常用的命令
- peresistent memory programming the remote access perspective
- pmem_flush
- Create Memory Allocation Goal - IPMCTL User Guide
- 磁盘I:O 性能指标 以及 如何通过 fio 对nvme ssd,optane ssd, pmem 性能摸底
- 2MB FSDAX 使用2Mpagesize的PM FSDAX namespace
最后
以上就是快乐跳跳糖为你收集整理的Persistent Memory编程简介的全部内容,希望文章能够帮你解决Persistent Memory编程简介所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复