使用持久内存开发工具包 (PMDK) 创建持久内存感知队列

简介

本文将介绍如何使用链表和 持久内存开发工具包 (PMDK) libpmemobj 库的 C++ 绑定实施持久内存 (PMEM) 感知队列。

队列是 一种先进先出 (FIFO) 数据结构,支持 推送弹出 操作。在推动操作中,新元素将添加到队列的尾部。在弹出操作中,队列开头的元素被删除。

PMEM 感知队列与普通队列的不同之处在于,其数据结构永久驻留在持久内存中,程序或机器崩溃可能导致队列输入不完整和队列损坏。为了避免这种情况,队列操作必须是事务性的。这并不简单,但 PMDK 提供专门针对持久内存编程的这一操作和其他操作的支持。

我们将提供一个代码示例,介绍使用 libpmemobj 创建 PMEM 感知队列的核心概念和设计注意事项。您可以按照本文后面提供的 说明 构建和运行代码示例。

如欲获取关于持久内存和 PMDK 的背景信息,请阅读 文章“借助英特尔持久内存进行编程的简介” 并观看 持久内存编程视频系列

libpmemobj 中的 C++ 支持

libpmemobj 的 C++ 绑定的主要特性包括:

  • 事务
  • 基本类型的包装程序:在事务处理过程中自动创建数据快照
  • 持久指针

事务

事务是 libpmemobj 操作的核心。这是因为,就持久性而言,当前的 x86-64 CPU 只能保证 8 字节存储的原子性。实际应用以较大的数据块进行更新。以字符串为例,仅将 8 个相邻字节从一个一致的字符串状态更改为另一个状态几乎没有意义。为了支持对较大数据块中持久内存的原子更新,libpmemobj 实施了事务。

出于可见性原因,

Libpmemobj 将使用基于撤消日志的事务,而不是基于重做日志的事务。用户所作的变更立即可见。这可实现更自然的代码结构和执行流程,从而改进代码可维护性。这也意味着,如果事务处理过程中发生中断,对持久状态所做的所有更改都将被回滚。

事务具有类似 ACID(原子性、一致性、隔离性和耐久性)的属性。以下是这些属性与 PKDK 编程的关系:

原子性:事务在持久性方面是原子性的;当事务成功完成时,在事务中做出的所有更改都会被提交,或全部不提交。

一致性:PMDK 支持用户保持数据一致性。

隔离:PMDK 库提供持久内存驻留同步机制,使开发人员能够保持隔离。

耐用性:所有交易的锁都保留到交易完成,以确保耐用性。

事务是在每个线程的基础上完成的,所以调用将返回调用线程所执行的最后一个事务的状态。事务是断电安全的,但不是线程安全的。

< p > 属性

在事务中,撤销日志用于创建用户数据快照。<p> 模板包装类是用于自动生成用户数据快照的基本构建模块,因此应用开发人员无需手动执行这一步骤(就像 libpmemobj 的 C 实施一样)。这一包装类只支持基本类型。它的实施基于赋值运算符,并且每次为这个包装类的变量分配一个新值时,系统都将为该变量的旧值创建快照。由于创建快照是一个计算密集型操作,因此不鼓励在堆栈变量中使用<p>属性。

持久指针

PMDK 中的库建立在内存映射文件的概念之上。由于文件可以映射到进程虚拟地址空间的不同地址,因此不能使用存储绝对地址的传统指针。PMDK 引入了一种新的指针类型,它有两个字段:一个池的 ID(用于从转换表访问当前的池虚拟地址)以及与池起点之间的偏移。持久指针是围绕这个基本 C 类型的 C ++ 包装。其理念与 std::shared_ptr相似。

libpmemobj 核心概念

根对象

通过 libpmemobj 创建具有 PMEM 感知能力的代码始终需要设计持久化数据对象的类型,这是第一步工作。第一种需要定义的类型是根对象。这一对象是强制性的,用于固定在持久内存池中创建的所有其他对象(将池视为 PMEM 设备中的文件)。

池是一个连续的 PMEM 区域,由用户提供的标识符(称为布局)进行识别。可以使用不同的布局字符串创建多个池。

使用 C++ 绑定实施队列

本示例中的队列被实施为一个单独链接列表,表头和表尾展示了如何使用 libpmemobj 的 C ++ 绑定。

设计决策

数据结构

我们首先需要一个数据结构来描述队列中的节点。每个条目都有一个值和一个指向下一个节点的链接。根据下图,这两个变量都是持久内存感知变量。

Data structure map
图 1.描述队列实施的数据结构。

代码走查

现在我们来深入了解一下该程序的主要功能。运行代码时,您需要提供三个参数。一个是池文件的绝对位置,第二个是需要执行的实际队列操作。支持的队列操作是 推送 (插入元素)、弹出(返回和移除元素)以及 显示 (返回元素)。

if (argc < 3) {
	std::cerr << "usage: " << argv[0]
	<< " file-name [push [value]|pop|show]" << std::endl;
	return 1;
}

在下面的代码片段中,我们检查池文件是否存在。如果存在,池将打开。如果不存在,则创建池。布局字符串标识我们请求打开的池。在这里,我们打开布局名称为“队列”的池,其由程序中的宏 LAYOUT 定义。

const char *path = argv[1];
queue_op op = parse_queue_op(argv[2]);
pool<examples::pmem_queue> pop;

if (file_exists(path) != 0) {
	pop = pool<examples::pmem_queue>::create(
		path, LAYOUT, PMEMOBJ_MIN_POOL, CREATE_MODE_RW);
} else {
	pop = pool<examples::pmem_queue>::open(path, LAYOUT);
}

弹出 是指向池的指针,我们可以从中访问根对象(examples::pmem_queue 的实例)的指针,Create 函数创建一个examples::pmem_queue 类型的新 pmemobj 池。根对象就像文件系统的根,因为它可以用来访问池中的所有其他对象(只要这些对象链接正确,并且不会由于编码错误而丢失指针)。

auto q = pop.get_root();

一旦获得指向队列对象的指针,程序就会检查第二个参数,以便确定队列应执行的操作类型;也就是推送弹出显示

switch (op) {
	case QUEUE_PUSH:
		q->push(pop, atoll(argv[3]));
		break;
	case QUEUE_POP:
		std::cout << q->pop(pop) << std::endl;
		break;
	case QUEUE_SHOW:
		q->show();
		break;
	default:
		throw std::invalid_argument("invalid queue operation");
}

队列操作

推送

我们来看看如何实施 push 函数,使其具有持久编程感知能力。如下面的代码所示,事务代码被实施为一个封装在 C++ 闭包中的 lambda 函数(支持轻松读取和遵循代码)。如果发生断电,数据结构不会被损坏,因为所有更改都会回滚。如欲了解关于如何用 C++ 实施事务的更多信息,请阅读 pmem.io 上的 libpmemobj 的 C++ 绑定(第六部分) - 事务。

分配函数也是事务性的,它们使用事务逻辑来支持持久状态的分配/删除回滚;make_persistent() 是构造函数,delete_persistent() 是析构函数。

在事务内调用 make_persistent() 会分配一个对象并返回一个持久对象指针。由于分配现在是事务的一部分,因此如果中止,分配将进行回滚,将内存分配恢复到其原始状态。

分配之后,n 的值将被初始化为队列中的新值,并且下一个指针将被设置为 null。

void push(pool_base &pop, uint64_t value) {
	transaction::exec_tx(pop, [&] {
		auto n = make_persistent<pmem_entry>();

		n->value = value;
		n->next = nullptr;

		if (head == nullptr && tail == nullptr) {
			head = tail = n;
		} else {
			tail->next = n;
			tail = n;
		}
	});
}

Data structure map for push functionality
2.推送功能的数据结构。

弹出

推送类似,弹出 函数如下所示。这里我们需要一个临时变量来存储队列中下一个 pmem_entry 的指针。在使用 delete_persistent() 删除队列头后,我们需要使用临时变量将队列头设为下一个 pmem_entry。由于这是使用事务完成的,因此具有持久感知能力。

uint64_t pop(pool_base &pop){
	uint64_t ret = 0;
	transaction::exec_tx(pop, [&] {
		if (head == nullptr)
			transaction::abort(EINVAL);

		ret = head->value;
		auto n = head->next;

		delete_persistent<pmem_entry>(head);
		head = n;

		if (head == nullptr)
			tail = nullptr;
	});

	return ret;
}

Data structure map for pop functionality.
图 3.弹出功能的数据结构。

构建指令

运行代码示例的说明

从 PMDK GitHub* 库下载源代码:

  1. Git 克隆https://github.com/pmem/pmdk.git

    command window with GitHub command
    图 4.从 GitHub* 库下载源代码。

  2. cd pmdk,在命令行上运行 make,如下所示。这将构建完整的源代码树。

    command window with code
    图 5.构建源代码。

  3. cd pmdk/src/examples/libpmemobj++/queue
  4. 查看队列程序的命令行选项:
    ./queue
  5. 推送命令:
    ./queue TESTFILE push 8

    Command window with code
    图 6.使用命令行的推送命令。

  6. 弹出命令:
    ./queue TESTFILE pop
  7. 显示命令:
    ./queue TESTFILE show

    Command window with code
    图 7.使用命令行的弹出命令。

总结

在本文中,我们使用 PMDK 库 libpmemobj 的 C++ 绑定展示了一个简单的 PMEM 感知队列实施。如欲了解更多关于借助 PMDK 进行持久内存编程的信息,请访问英特尔® 开发人员专区 (Intel® DZ) 持久内存编程 板块。您可以在该板块中找到相关文章、视频以及面向 PMEM 开发人员的其他重要资源的链接。

关于作者

Praveen Kundurthy 是英特尔的开发人员宣传官,在应用开发、优化和移植到英特尔平台方面拥有超过 14 年的经验。在过去的几年里,他一直从事存储技术、游戏、虚拟现实和基于英特尔平台的 Android 等方面的工作。

有关编译器优化的更完整信息,请参阅优化通知