Persistent Memory编程简介

  • 编程
    • libpmem
      • 持久化函数
    • libpmemobj
      • 跟对象 root object
      • 例程
        • 事务支持
      • type safety
      • 线程安全
  • 管理工具
    • ipmctl
    • ndctl
      • create-namespace
      • 例子
  • 测试工具
    • fio
    • pmembench
    • ipmwatch
    • emon
    • pcm
  • 参考链接

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

编程

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

libpmem

  • libpmem简介

peme底层库,不支持事务,编程方法如下:

#include <libpmem.h>
// 其他头文件省略
/* using 4k of pmem for this example */
#define PMEM_LEN 4096int
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

  • libpmemobj简介
  • libpmemobj api之类的文档

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的两层哈希对应到实际的地址pooluint64_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[])
{// 创建poolPMEMobjpool *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

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

Persistent Memory编程简介相关推荐

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

    目录 libpmemobj libpmemblk libpmemlog libpmem libpmem2 libvmem libvmmalloc libpmempool pmempool librpm ...

  2. linux c read函数返回值,Linuxc - GNU Readline 库及编程简介

    GNU Readline 库及编程简介 简介 用过 Bash 命令行的一定知道,Bash 有几个特性: TAB 键可以用来命令补全 ↑ 或 ↓ 键可以用来快速输入历史命令 还有一些交互式行编辑快捷键: ...

  3. 异构计算(CPU + GPU)编程简介

    异构计算(CPU + GPU)编程简介 1.概念 所谓异构计算,是指CPU+ GPU或者CPU+ 其它设备(如FPGA等)协同计算.一般我们的程序,是在CPU上计算.但是,当大量的数据需要计算时,CP ...

  4. Gentler编程简介

    by Matt Adesanya 马特·阿德桑亚(Matt Adesanya) Gentler编程简介 (A Gentler Introduction to Programming) This wri ...

  5. CUDA编程(一):GPU计算与CUDA编程简介

    CUDA编程(一):GPU计算与CUDA编程简介 GPU计算 GPU硬件资源 GPU软件资源 GPU存储资源 CUDA编程 GPU计算 NVIDIA公司发布的CUDA是建立在GPU上的一个通用并行计算 ...

  6. OpenCL学习笔记(三):OpenCL安装,编程简介与helloworld

    欢迎转载,转载请注明:本文出自Bin的专栏blog.csdn.net/xbinworld. 技术交流QQ群:433250724,欢迎对算法.技术.应用感兴趣的同学加入. OpenCL安装 安装我不打算 ...

  7. Intel Optane DC Persistent Memory Module (PMM)持久内存

    英特尔已经公开讨论了一年多的Optane DC Persistent Memory Module(PMM),体现了一种新的以数据为中心的体系结构,在这个体系结构中,PMM位于DRAM和Optane D ...

  8. Linux上快速入门英特尔Optane DC Persistent Memory Module的配置与使用

    翻译得不好还请见谅,原文见末尾链接~ 一.简介 英特尔的Optane DC Persistent Memory(Optane DC PMM.DCPMM)是一种颠覆性的技术,它在内存和存储器之间创建了一 ...

  9. 英特尔Optane DC Persistent Memory操作模式说明

    前面介绍了Optane DC Persistent Memory有两种模式: Memory Mode App Direct Mode 服务器将使用DRAM和英特尔Optan DC Persistent ...

最新文章

  1. 用Leangoo看板工具策划一场活动,看板示例
  2. 在Ubuntu上安装Odoo 11(企业版)
  3. 事物(Jdbc) 例子
  4. ios UIScrollView 中控件自动增加间隔
  5. sql server转mysql工具下载_SQL Server转换为MySQL工具推荐(Mss2sql)
  6. css 横线_atom.css正式发布,从此跟CSS框架说拜拜。
  7. 【dfs】栅栏的木料(2012特长生 T4)
  8. spring学习(14):Autowired的使用场景
  9. php调用selenium,通过PHP exec()执行Selenium webdriver
  10. IOS开发之----四舍五入问题
  11. SQL2000升级到2005过程中的用户和登录名问题
  12. Bag-of-words模型
  13. 抑郁自评量表SDS问卷HTML版
  14. 问卷调查:自定义表单设计vue
  15. Python每输出n个换行
  16. 31个惊艳的数据可视化作品,感受“数据之美”!
  17. 移动硬盘插入笔记本会后,右下角有图标显示,但是我的电脑里面不显示,导致打不开硬盘
  18. Linux解压缩解压tar.gz文件
  19. 2019年应届生校招技面随笔
  20. 爬虫入门教程 | 使用selenium爬取微博热门数据

热门文章

  1. android qq 登陆 简书,使用QQ第三方登录
  2. 12.映射表map.rs
  3. Mysql游标循环遍历
  4. OD+IDA6.1破解HideWizardv9.29(无忧隐藏)
  5. IDA+OD双剑合璧=逆向无敌
  6. SQL SERVER 2008安全配置
  7. 趣谈设计模式 | 命令模式(Command):将命令封装为对象
  8. 数据结构与算法 | 二叉树四种的遍历方法(递归与非递归)
  9. 如何优雅的设计和使用缓存?
  10. C/C++学习之路: 多态