简介
本文展示了如何转换简单的 C++ 程序,选取了著名的 UNIX 命令行实用程序的简化版本 grep 作为示例,以利用持久内存(PMEM)。本文提供了详细的代码,首先描述了易失性版 grep 程序的作用。然后讨论了如何将 grep 添加至搜索结果的持久高速缓存中,以改进 grep。高速缓存通过1添加容错(FT)功能和2加速对已发现搜索模式的查询改进了 grep。本文使用 libpmemobj 的 C++ 绑定继续描述 grep 的持久版本,libpmemobj 是 持久内存开发套件(PMDK)集中的一个核心库。最后,使用线程和 PMEM 感知型同步添加了并行处理(在文件粒度中)。
易失性 Grep
描述
如果您熟悉任何类似于 UNIX* 的操作系统,如 GNU/Linux*,可能也比较了解命令行实用程序 grep(代表以正规表示法进行全域查找并打印)。实质上,grep 接收两种参数(其余是选项):采用正则表达式形式的模式和输入文件(包括标准输入)。grep 的目标是逐行扫描输入,然后输入匹配给定模式的行。更多详情敬请参阅 grep 手册页面(在终端输入手册 grep 或查看 Linux 手册页面,以在线了解 grep)。
我的简化版 grep 只使用了上述两种参数(模式和输入),输入应为单个文件或目录。如果提供了目录,目录内容将被扫描,以查找输入文件(通常以递归的形式扫描子目录)。为了查看实际的运行方式,我们将它的源代码用作输入,将“int”用作模式,并运行该程序。
代码可从 GitHub* 中下载。为了从 pmdk-examples 存储库的根编译代码,输入 make simple-grep。 libpmemobj和 C++ 编译器必须安装在您的系统。为了兼容 Windows* 操作系统,代码不会调用任何针对 Linux 的函数。相反,使用 Boost C++ 库集合(主要用于处理文件系统输入/输出)。如果您使用 Linux,可能已面向您最喜爱的分发版配备了 Boost C++。例如,在 Ubuntu* 16.04 中,您可以通过以下方式安装这些库:
# sudo apt-get install libboost-all-dev
如果程序得以正确编译,我们可以按照以下方式运行程序:
$ ./grep int grep.cpp FILE = grep.cpp 44: int 54: int ret = 0; 77: int 100: int 115: int 135: int 136: main (int argc, char *argv[])
如您所见,grep 共找到 7 行包含“int”的代码(第 44、54、77、100、115、135 和 136 行)。作为一项合理性检查,我们可以使用系统提供的 grep 运行相同的查询:
$ grep int –n grep.cpp 44:int 54: int ret = 0; 77:int 100:int 115:int 135:int 136:main (int argc, char *argv[])
截至目前,我们已经得到了预期的输出。代码如下表所示(注:以上代码片段中的行数与下表不匹配,因为代码格式不同于初始源文件):
#include <boost/filesystem.hpp> #include <boost/foreach.hpp> #include <fstream> #include <iostream> #include <regex> #include <string.h> #include <string> #include <vector> using namespace std; using namespace boost::filesystem; /* auxiliary functions */ int process_reg_file (const char *pattern, const char *filename) { ifstream fd (filename); string line; string patternstr ("(.*)("); patternstr += string (pattern) + string (")(.*)"); regex exp (patternstr); int ret = 0; if (fd.is_open ()) { size_t linenum = 0; bool first_line = true; while (getline (fd, line)) { ++linenum; if (regex_match (line, exp)) { if (first_line) { cout << "FILE = "<< string (filename); cout << endl << flush; first_line = false; } cout << linenum << ": "<< line << endl; cout << flush; } } } else { cout << "unable to open file " + string (filename) << endl; ret = -1; } return ret; } int process_directory_recursive (const char *dirname, vector<string> &files) { path dir_path (dirname); directory_iterator it (dir_path), eod; BOOST_FOREACH (path const &pa, make_pair (it, eod)) { /* full path name */ string fpname = pa.string (); if (is_regular_file (pa)) { files.push_back (fpname); } else if (is_directory (pa) && pa.filename () != "."&& pa.filename () != ".."){ if (process_directory_recursive (fpname.c_str (), files) < 0) return -1; } } return 0; } int process_directory (const char *pattern, const char *dirname) { vector<string> files; if (process_directory_recursive (dirname, files) < 0) return -1; for (vector<string>::iterator it = files.begin (); it != files.end (); ++it) { if (process_reg_file (pattern, it->c_str ()) < 0) cout << "problems processing file "<< *it << endl; } return 0; } int process_input (const char *pattern, const char *input) { /* check input type */ path pa (input); if (is_regular_file (pa)) return process_reg_file (pattern, input); else if (is_directory (pa)) return process_directory (pattern, input); else { cout << string (input); cout << " is not a valid input"<< endl; } return -1; } /* MAIN */ int main (int argc, char *argv[]) { /* reading params */ if (argc < 3) { cout << "USE "<< string (argv[0]) << " pattern input "; cout << endl << flush; return 1; } return process_input (argv[1], argv[2]); }
我知道代码很长,但是相信我,代码不难运行。我只需要在 process_input()
中检查输入是一个文件,还是一个目录。如果是前一种情况,将会在 process_reg_file()
中直接处理文件。如果是后一种情况,将会在 process_directory_recursive()
中扫描目录下的文件,然后通过调用每个文件上的 process_reg_file()
,在 process_directory()
中逐一处理被扫描的文件。处理文件时,检查每行是否匹配模式。如果匹配,将该行打印为标准输出。
持久内存
现在我们得到了一个正常运行的 grep,我们看一下如何对它进行改进。首先,我们发现 grep 不会保存任何状态。完成了对输入的分析并生成输出后,程序随即停止。假设我们计划每周都对一个大型目录(拥有几十万份文件)进行扫描,以查找相关的特定模式。假设目录中的文件可能会不断变化(尽管所有文件不可能同时改变),也可能会添加新的文件。如果我们使用经典的 grep 执行该任务,可能会重复扫描某些文件,浪费了宝贵的 CPU 周期。这个限制可以通过添加一个高速缓存来克服:如果已经针对特定模式对文件进行了扫描(而且上次扫描之后内容没有发生变化),grep 将返回缓存的结果,而不是重新扫描文件。
可以通过多种方式实施高速缓存。例如,一种方法是创建一个特定的数据库(DB),以存储每个被扫描文件和模式的结果(还会添加一个时间戳,以检测文件修改)。虽然该方法行之有效,但是,不要求安装与运行 DB 引擎的解决方案将是一个更好的选择,况且该方法需要在每次分析文件时,执行 DB 查询(将产生网络与输入/输出开销)。另一种方法是将该缓存存储为常规文件。在开始时,将缓存加载至易失性内存,在执行结束时或每次分析新文件时,对其进行更新。这种方法看似好用,但是我们不得不创建两个数据模型,一个用于易失性 RAM,另一个用于二级持久存储(文件),并且需要写入代码,以便在两个模型之间反复转化。如果能够避免这种额外的编码工作,那就最好不过。
持久 Grep
设计注意事项
使用 libpmemobj 编写 PEME 感知型代码的第一步通常是设计需要持久存储的数据对象类型。根对象是需要定义的第一类对象。该对象具有强制性,用于固定 PMEM 池中创建的所有其他对象(将池视为 PMEM 设备中的文件)。我的 grep 示例使用了以下持久数据结构:
图 1.PMEM 感知型 grep 的数据结构。
通过创建从根类中挂起的模式链表来整理高速缓存数据。每当搜索新模式时,将会创建一个新的类模式对象。如果当前搜索的模式在过去被搜索过,便无需创建对象(模式字符串被存储于 patternstr)。我们从类模式中挂起被扫描文件的链表。文件包括名称(在本示例中,名称和文件系统路径相同)、修改时间(用于检查文件是否已被修改)和匹配该模式的行的矢量。我们只针对未被扫描的文件创建新的类文件对象。
首先需要注意的是特殊类p<>
(面向基础类型)和 persistent_ptr<>
(面向复杂类型的指针)。这些类被用于通知库在交易时注意这些内存区(发生故障时,记录与回滚对象的更改)。得益于虚拟内存的性质,persistent_ptr<>
将始终适用于 PMEM 中的指针。当池被程序打开,并且映射至虚拟内存地址空间时,池的位置可能与同一程序(或者访问同一个池的其他程序)使用的先前位置不同。在 PMDK 的示例中,持久指针被实施为胖指针;它们包含一个池 ID(用于从转换表中访问当前的池虚拟地址)+偏置(在池开始时)。有关 PMDK 中指针的更多信息,请参阅libpmemobj 中类型安全的宏和面向 libpmemobj 的 C++ 绑定(第 2 部分)-持久智能指针。
您可能想知道为什么行的矢量(std::vector
)不被声明为持久指针。原因是没有必要这样做。表示矢量和行的对象一经创建(类文件对象的构建过程中),便不会改变。因此,无需在交易过程中追踪对象。尽管如此,矢量本身也会在内部分配(与删除)对象。因此,我们不能单纯依靠来自 std::vector
的默认分配器(该分配器只了解易失性内存,分配堆中的所有对象);我们需要传输 libpmemobj 提供的定制化分配器(了解 PMEM)。该分配器为 pmem::obj::allocator<line>
。我们以这种方式声明矢量后,便可以按照任意正常易失性代码中的使用方式使用它。事实上,您可以以这种方式使用任何标准容器类。
代码修改
现在,我们跳至代码部分。为了避免重复,只列出新代码(完整代码可在 pmemgrep/pmemgrep.cpp 中获取)。我们从定义(新的标头、宏、命名空间、全局变量和类)着手:
... #include <libpmemobj++/allocator.hpp> #include <libpmemobj++/make_persistent.hpp> #include <libpmemobj++/make_persistent_array.hpp> #include <libpmemobj++/persistent_ptr.hpp> #include <libpmemobj++/transaction.hpp> ... #define POOLSIZE ((size_t) (1024 * 1024 * 256)) /* 256 MB */ ... using namespace pmem; using namespace pmem::obj; /* globals */ class root; pool<root> pop; /* persistent data structures */ struct line { persistent_ptr<char[]> linestr; p<size_t> linenum; }; class file { private: persistent_ptr<file> next; persistent_ptr<char[]> name; p<time_t> mtime; vector<line, pmem::obj::allocator<line>> lines; public: file (const char *filename) { name = make_persistent<char[]> (strlen (filename) + 1); strcpy (name.get (), filename); mtime = 0; } char * get_name (void) { return name.get (); } size_t get_nlines (void) { return lines.size (); /* nlines; */ } struct line * get_line (size_t index) { return &(lines[index]); } persistent_ptr<file> get_next (void) { return next; } void set_next (persistent_ptr<file> n) { next = n; } time_t get_mtime (void) { return mtime; } void set_mtime (time_t mt) { mtime = mt; } void create_new_line (string linestr, size_t linenum) { transaction::exec_tx (pop, [&] { struct line new_line; /* creating new line */ new_line.linestr = make_persistent<char[]> (linestr.length () + 1); strcpy (new_line.linestr.get (), linestr.c_str ()); new_line.linenum = linenum; lines.insert (lines.cbegin (), new_line); }); } int process_pattern (const char *str) { ifstream fd (name.get ()); string line; string patternstr ("(.*)("); patternstr += string (str) + string (")(.*)"); regex exp (patternstr); int ret = 0; transaction::exec_tx ( pop, [&] { /* dont leave a file processed half way through */ if (fd.is_open ()) { size_t linenum = 0; while (getline (fd, line)) { ++linenum; if (regex_match (line, exp)) /* adding this line...*/ create_new_line (line, linenum); } } else { cout << "unable to open file " + string (name.get ()) << endl; ret = -1; } }); return ret; } void remove_lines () { lines.clear (); } }; class pattern { private: persistent_ptr<pattern> next; persistent_ptr<char[]> patternstr; persistent_ptr<file> files; p<size_t> nfiles; public: pattern (const char *str) { patternstr = make_persistent<char[]> (strlen (str) + 1); strcpy (patternstr.get (), str); files = nullptr; nfiles = 0; } file * get_file (size_t index) { persistent_ptr<file> ptr = files; size_t i = 0; while (i < index && ptr != nullptr) { ptr = ptr->get_next (); i++; } return ptr.get (); } persistent_ptr<pattern> get_next (void) { return next; } void set_next (persistent_ptr<pattern> n) { next = n; } char * get_str (void) { return patternstr.get (); } file * find_file (const char *filename) { persistent_ptr<file> ptr = files; while (ptr != nullptr) { if (strcmp (filename, ptr->get_name ()) == 0) return ptr.get (); ptr = ptr->get_next (); } return nullptr; } file * create_new_file (const char *filename) { file *new_file; transaction::exec_tx (pop, [&] { /* allocating new files head */ persistent_ptr<file> new_files = make_persistent<file> (filename); /* making the new allocation the actual head */ new_files->set_next (files); files = new_files; nfiles = nfiles + 1; new_file = files.get (); }); return new_file; } void print (void) { cout << "PATTERN = "<< patternstr.get () << endl; cout << "\tpattern present in "<< nfiles; cout << " files"<< endl; for (size_t i = 0; i < nfiles; i++) { file *f = get_file (i); cout << "###############"<< endl; cout << "FILE = "<< f->get_name () << endl; cout << "###############"<< endl; cout << "*** pattern present in "<< f->get_nlines (); cout << " lines ***"<< endl; for (size_t j = f->get_nlines (); j > 0; j--) { cout << f->get_line (j - 1)->linenum << ": "; cout << string (f->get_line (j - 1)->linestr.get ()); cout << endl; } } } }; class root { private: p<size_t> npatterns; persistent_ptr<pattern> patterns; public: pattern * get_pattern (size_t index) { persistent_ptr<pattern> ptr = patterns; size_t i = 0; while (i < index && ptr != nullptr) { ptr = ptr->get_next (); i++; } return ptr.get (); } pattern * find_pattern (const char *patternstr) { persistent_ptr<pattern> ptr = patterns; while (ptr != nullptr) { if (strcmp (patternstr, ptr->get_str ()) == 0) return ptr.get (); ptr = ptr->get_next (); } return nullptr; } pattern * create_new_pattern (const char *patternstr) { pattern *new_pattern; transaction::exec_tx (pop, [&] { /* allocating new patterns arrray */ persistent_ptr<pattern> new_patterns = make_persistent<pattern> (patternstr); /* making the new allocation the actual head */ new_patterns->set_next (patterns); patterns = new_patterns; npatterns = npatterns + 1; new_pattern = patterns.get (); }); return new_pattern; } void print_patterns (void) { cout << npatterns << " PATTERNS PROCESSED"<< endl; for (size_t i = 0; i < npatterns; i++) cout << string (get_pattern (i)->get_str ()) << endl; } } ...
此处显示的是图 1 中图表的代码示例。您也可以看到 libpmemobj 的标头、定义池大小的宏(POOLSIZE)和用于存储开源池的全局变量(pop,您可以将 pop 视作特殊的文件描述符)。请注意如何使用交易保护 root::create_new_pattern()
、pattern::create_new_file()
和 file::create_new_line()
中的所有数据结构修改。在 libpmemobj 的 C++ 绑定中,使用 lambda 函数便捷地实施了交易(使用 lambdas 要求您的编译器至少兼容 C++11)。如果您因为某些原因不喜欢 lambda,可以尝试另一种方法。
请注意如何通过 make_persistent<>()
(而非常规 malloc()
或 C++“新”结构)对所有内存进行分配。
旧版 process_reg_file()
的功能被迁移至 file::process_pattern()
方法。新版 process_reg_file()
实施了用于检查当前文件是否已经进行模式扫描的逻辑(查看文件是否处于当前模式下,并且自上次后并未被修改):
int process_reg_file (pattern *p, const char *filename, const time_t mtime) { file *f = p->find_file (filename); if (f != nullptr && difftime (mtime, f->get_mtime ()) == 0) /* file exists */ return 0; if (f == nullptr) /* file does not exist */ f = p->create_new_file (filename); else /* file exists but it has an old timestamp (modification) */ f->remove_lines (); if (f->process_pattern (p->get_str ()) < 0) { cout << "problems processing file "<< filename << endl; return -1; } f->set_mtime (mtime); return 0; }
对其他函数仅实施了一项更改-添加修改时间。例如,process_directory_recursive()
现在返回 tuple<string, time_t>
的矢量(而不单单返回vector<string>):
int process_directory_recursive (const char *dirname, vector<tuple<string, time_t>> &files) { path dir_path (dirname); directory_iterator it (dir_path), eod; BOOST_FOREACH (path const &pa, make_pair (it, eod)) { /* full path name */ string fpname = pa.string (); if (is_regular_file (pa)) { files.push_back ( tuple<string, time_t> (fpname, last_write_time (pa))); } else if (is_directory (pa) && pa.filename () != "."&& pa.filename () != ".."){ if (process_directory_recursive (fpname.c_str (), files) < 0) return -1; } } return 0; }
运行示例
接下来,我们使用“int”和“void”两种模式运行该代码。假设 PMEM 设备(真实设备或 使用 RAM 模拟的设备 )安装在 /mnt/mem:
$ ./pmemgrep /mnt/mem/grep.pool int pmemgrep.cpp $ ./pmemgrep /mnt/mem/grep.pool void pmemgrep.cpp $
如果运行没有参数的函数,我们将得到高速缓存模式:
$ ./pmemgrep /mnt/mem/grep.pool 2 PATTERNS PROCESSED void int
传输模式时,我们将得到实际的高速缓存结果:
$ ./pmemgrep /mnt/mem/grep.pool void PATTERN = void 1 file(s) scanned ############### FILE = pmemgrep.cpp ############### *** pattern present in 15 lines *** 80: get_name (void) 86: get_nlines (void) 98: get_next (void) 103: void 110: get_mtime (void) 115: void 121: void 170: void 207: get_next (void) 212: void 219: get_str (void) 254: void 255: print (void) 326: void 327: print_patterns (void) $ $ ./pmemgrep /mnt/mem/grep.pool int PATTERN = int 1 file(s) scanned ############### FILE = pmemgrep.cpp ############### *** pattern present in 14 lines *** 137: int 147: int ret = 0; 255: print (void) 327: print_patterns (void) 337: int 356: int 381: int 395: int 416: int 417: main (int argc, char *argv[]) 436: if (argc == 2) /* No pattern is provided.Print stored patterns and exit 438: proot->print_patterns (); 444: if (argc == 3) /* No input is provided.Print data and exit */ 445: p->print (); $
当然,我们可以继续将文件添加至现有的模式:
$ ./pmemgrep /mnt/mem/grep.pool void Makefile $ ./pmemgrep /mnt/mem/grep.pool void PATTERN = void 2 file(s) scanned ############### FILE = Makefile ############### *** pattern present in 0 lines *** ############### FILE = pmemgrep.cpp ############### *** pattern present in 15 lines *** 80: get_name (void) 86: get_nlines (void) 98: get_next (void) 103: void 110: get_mtime (void) 115: void 121: void 170: void 207: get_next (void) 212: void 219: get_str (void) 254: void 255: print (void) 326: void 327: print_patterns (void)
并行持久 Grep
既然我们已经讲到了这里,不添加多线程支持未免太可惜了,尤其是该支持只需添加少量的代码(完整代码可从 pmemgrep_thx/pmemgrep.cpp
中获取)。
首先需要添加面向 pthread 和持久互斥体(稍后将详细介绍)的相应标头:
... #include <libpmemobj++/mutex.hpp> ... #include <thread>
添加了全新的全局变量,以设置程序中的线程数量,现在接收用于设置线程数量(-nt=number_of_threads
)的命令行选项。如果没有明确设置 -nt,将默认使用一个线程:
int num_threads = 1;
接下来,将持久互斥体添加至模式类。使用互斥体同步文件链表的写入(在文件粒度中完成并行化):
class pattern { private: persistent_ptr<pattern> next; persistent_ptr<char[]> patternstr; persistent_ptr<file> files; p<size_t> nfiles; pmem::obj::mutex pmutex; ...
您可能想知道为什么需要互斥体的 pmem::obj
版本(为什么不使用 C++ 标准版)。这是因为互斥体存储于 PMEM 中,并且 libpmemobj 需要能在崩溃时重置它。如果不能得到正确恢复,损坏的互斥体将创建一个永久的死锁;您可以参阅使用 libpmemobj 进行同步一文,以了解更多信息。
虽然将互斥体存储于 PMEM 对关联互斥体和特定的持久数据对象有所帮助,但并不是在所有情况下都有此强制性要求。事实上,在本示例中,易失性内存中的单个标准互斥体变量已足够(因为所有线程一次只能处理一个模式)。我使用持久互斥体是为了显示它的存在。
一旦拥有了互斥体,无论持久与否,我们可以将互斥体传输至 transaction::exec_tx()
(最后一个参数),以同步 pattern::create_new_file()
中的写入:
transaction::exec_tx (pop, [&] { /* LOCKED TRANSACTION */ /* allocating new files head */ persistent_ptr<file> new_files = make_persistent<file> (filename); /* making the new allocation the * actual head */ new_files->set_next (files); files = new_files; nfiles = nfiles + 1; new_file = files.get (); }, pmutex); /* END LOCKED TRANSACTION */
最后一步是调整 process_directory()
,以创建与连接线程。已面向线程逻辑创建了一个新函数 process_directory_thread()—
(该函数根据线程 ID 拆分任务):
void process_directory_thread (int id, pattern *p, const vector<tuple<string, time_t>> &files) { size_t files_len = files.size (); size_t start = id * (files_len / num_threads); size_t end = start + (files_len / num_threads); if (id == num_threads - 1) end = files_len; for (size_t i = start; i < end; i++) process_reg_file (p, get<0> (files[i]).c_str (), get<1> (files[i])); } int process_directory (pattern *p, const char *dirname) { vector<tuple<string, time_t>> files; if (process_directory_recursive (dirname, files) < 0) return -1; /* start threads to split the work */ thread threads[num_threads]; for (int i = 0; i < num_threads; i++) threads[i] = thread (process_directory_thread, i, p, files); /* join threads */ for (int i = 0; i < num_threads; i++) threads[i].join (); return 0; }
总结
本文展示了如何转换简单的 C++ 程序,选取了著名的 UNIX 命令行实用程序的简化版本 grep 作为示例,以利用 PMEM。本文提供了详细的代码,首先描述了易失性版 grep 程序的作用。
然后,使用 libpmemobj(PMDK 中的一款核心库)的 C++ 添加了一个 PMEM 高速缓存,对程序进行了改进。最后,使用线程和 PMEM 感知型同步添加了并行处理(在文件粒度中)。
关于作者
Eduardo Berrocal 于 2017 年 7 月加入英特尔,担任云软件工程师。此前,他在伊利诺斯州芝加哥市的伊利诺理工大学(IIT)获得了计算机科学博士学位。他的博士研究方向主要为(但不限于)数据分析和面向高性能计算的容错。他曾担任过贝尔实验室(诺基亚)的暑期实习生、阿贡国家实验室的研究助理,芝加哥大学的科学程序员和 web 开发人员以及西班牙 CESVIMA 实验室的实习生。
资料来源
- 持久内存开发套件(PMDK),http://pmem.io/pmdk/。
- grep 命令手册页面,https://linux.die.net/man/1/grep。
- Boost C++ 库集合,http://www.boost.org/。
- libpmemobj 中类型安全的宏,http://pmem.io/2015/06/11/type-safety-macros.html。
- 面向 libpmemobj 的 C++ 绑定(第 2 部分)-持久智能指针,http://pmem.io/2016/01/12/cpp-03.html。
- 面向 libpmemobj 的 C++ 绑定(第 6 部分)-交易,http://pmem.io/2016/05/25/cpp-07.html。
- 如何模拟持久内存,http://pmem.io/2016/02/22/pm-emulation.html。
- GitHub 中的示例代码链接。