Persistent Memory编程简介


本文主要目的是介绍PM基础的的编程方法、管理工具、监测手段等

编程

  1. 持久内存开发套件(Persistent Memory Development Kit-PMDK) - pmem.io: PMDK
  2. PMDK based Persistent Memory Programming

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 calling pmem_drain() once to ensure the flushes are complete.
  • 还有一个flagPMEM_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 or CLFLUSH 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 the SFENCE instruction to ensure that all CLWBs are executed. If the instruction called by pmem_flush is CLFLUSH, 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

libpmem的上层封装,所有对pmem的操作都抽象为obj pool的形式。

  1. pmemobj_create创建obj pool
  2. pmemobj_open打开已经创建的obj
  3. pmemobj_close关闭对应的obj
  4. pmemobj_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("%s\n", 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

libpmemobj使用了一系列macro来将persistent pointer和某个具体类型联系起来

FeatureAnonymous unionsNamed 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-name\n", 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, "EOF\n");
        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("%s\n", 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的管理工具

  1. ipmctl create -goal PersistentMemoryType=AppDirect创建AppDirect GOAL
  2. ipmctl show -firmware查看DIMM固件版本
  3. ipmctl show -dimm列出DIMM
  4. ipmctl show -sensor获取更多详细信息,类似SMART
  5. ipmctl show -topology定位device位置

ndctl

管理“libnvdimm”对应的系统设备(Non-volatile Memory),常用命令:

  1. 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,有以下几种选择:

  1. libpmem:使用fsdax配置pmem namespace的模式,也是比较常用的模式。这里提供了个小例子
  2. dev-dax:针对devdax的pmem设备
  3. pmemblk:使用libpmemblk库读写pm
  4. 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()

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                --|
|---------------------------------------||---------------------------------------|

参考链接

  1. Direct Write to PMem how to disable DDIO
  2. Correct, Fast Remote PersistenceDDIO是在CPU层面enable的。
  3. 基于RDMA和NVM的大数据系统一致性协议研究
  4. pmem/valgrind
  5. PMDK based Persistent Memory Programming
  6. Running FIO with pmem engines
  7. Documentation for ndctl and daxctl
  8. AEPWatch
  9. CHAPTER 5. USING NVDIMM PERSISTENT MEMORY STORAGE
  10. I/O Alignment Considerations里面有一些常用的命令
  11. peresistent memory programming the remote access perspective
  12. pmem_flush
  13. Create Memory Allocation Goal - IPMCTL User Guide
  14. 磁盘I:O 性能指标 以及 如何通过 fio 对nvme ssd,optane ssd, pmem 性能摸底
  15. 2MB FSDAX 使用2Mpagesize的PM FSDAX namespace
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值