C++永久对象存储


  C++ 永远对象存储 (Persistent Object Storage for C++)

  简介

   形容对象类型 从存储器中 调配和 开释对象 永远对象 协定 存储器 构造函数 打开存储器 POST++ 的安装 POST++ 类库 和 POST++一同 使用 STL 类 替换 标准 调配子 如何 使用 POST++ S调试 POST++ 利用的细节 关于 POST++ 更多的一些信息 简介

  POST++ 提供了对 利用对象的 方便有效的存储 。 POST++ 基于内存文件镜像机制和页面镜像 解决 。POST++ 肃清了对 永远对象 拜访的开销 。 此外 POST++ 支撑多存储,虚函数, 数据更新原子操作, 高效的内存 调配和为指定 开释内存 模式下可选的垃圾收集器 。 POST++ 同样 可以很好的工作在多继承和包括指针的对象上 。

   形容对象类型

  POST++ 存储治理需要一些信息以使 永远对象类型 支撑垃圾收集器,装载时 引用重定位和初始化虚表内函数指针 。但 可怜的是C++语言没有提供运行时从类中兴许这些信息的机制 。为了幸免 使用一些特殊的工具(预 解决器)或“脏哄骗” 路径(从调试信息中猎取类信息),这些信息必须由程序员来指明 。这些称为类注册器的东西 可以 方便的通过POST++提供的一些宏来实现 。

  POST++ 在从存储器重载入对象时调用缺省 构造函数来初始化对象 。为了使对象句柄 可以存储,程序员必须在类定义中包括宏 CLASSINFO(NAME, FIELD_LIST) . NAME 指明对象的名字 。 FIELD_LIST 形容类的的 引用字段 。在头文件 classinfo.h 定义了三个宏用于 形容字段:

  REF(x) 形容一个字段 。 REFS(x) 形容一个一维固定数组字段 。 。 (例如:定长数组) 。 VREFS(x) 形容可变一维数组字段 。可变数组不得不是类的最终一个成员 。当你定义类的时候,你 可以指定一个仅包括一个元素的数组 。具体对象实例中的元素个数 可以在生成时指定 。

  这些宏列表必须用空格 离开: REF(a) REF(b) REFS(c) 。 宏 CLASSINFO 定义了缺省 构造函数 (没有参数的 构造函数) 和类 形容符 。 类 形容符是类的一个静态成员名为 self_class. 这样类 foo 的 形容符 可以通过 foo::self_class 拜访 。 基类和成员的缺省 构造函数会被编译器自动调用,你 毋庸 担心需要明确调用他们 。然而关于序列化的类中的结组成员不要 淡忘在 构造定义中 使用 CLASSINFO 宏 。 而后通过存储器治理注册该类使其可被 拜访 。这个过程由宏 REGISTER(NAME) 实现 。类名将和对象一同放在存储器中 。在 打开存储器的时候类在存储和 利用程序中间被镜像 。存储器中的类名和程序中的类名进行 比较 。假如有类没有被程序定义或 利用程序和存储器中的类有不同的大小,程序断言将失败 。

  下面的例子 阐述了这些 规定:

  struct branch { object* obj; int key; CLASSINFO(branch, REF(obj));};class foo : public object { protected: foo* next; foo* prev; object* arr[10]; branch branches[8]; int x; int y; object* childs[1]; public: CLASSINFO(foo, REF(next) REF(prev) REFS(arr) VREFS(linked)); foo(int x, int y);};REGISTER(1, foo);main() { storage my_storage("foo.odb"); if (my_storage.open()) { my_root_class* root = (my_root_class*)my_storage.get_root_object(); if (root == NULL) { root = new_in(my_storage, my_root)("some parameters for root"); } … int n_childs = …; size_t varying_size = (n_childs-1)*sizeof(object*); // We should subtract 1 from n_childs, because one element is already // present in fixed part of class. foo* fp = new (foo:self_class, my_storage, varying_size) foo(x, y); … my_storage.close(); } }

  从存储器中 调配和 开释对象

  POST++ 为了治理存储内存提供了特殊的内存 调配子 。 这个 调配子 使用两种不同的 步骤: 针对 调配小对象和大对象 。全部的存储内存被划分为页面(页面的大小和操作系统的页面大小无关,目前版本的 POST++ 中采纳了 512 字节) 。 小对象是这样一些对象,他们的大小小于或等于256字节(页面大小/2) 。 这些对象被 调配成固定大小的块链接起来 。每一个 链包括 雷同大小的块 。 调配对象的大小以8个字节为单位 。为每个对象 调配的包括这些块大小为256的的链的数量最好不要大于14(不同的 均衡页面数) 。 在每个对象之前 POST++ 调配一个对象头,包括有对象标识和对象大小 。考量到头部刚好8个字节,而且在C++中对象的大小总大于0,大小为8的块链 可以舍弃 。 调配和 开释小对象通常状况下是十分快的: 惟独要从L1队列中进行一次插入/删除操作 。 假如链为空而且我们试图 调配新的对象,新页被 调配用来存储像目前大小的对象(页被划分成块增加到链表中) 。大对象(大于256字节)所需要的空间从空暇页队列中 调配 。大对象的大小和页边界对齐 。POST++ 使用第一次喂给随机定位算法 保护空暇页队列(全部页的空暇段依照地址罗列并用一个特殊的指针尾随队列的目前位置) 。存储治理的实现见文件 storage.cxx

   使用显式还是隐含的内存 开释取决于程序员 。显式内存 开释要快(特殊是对小对象而言)然而隐含内存 开释(垃圾收集)更加牢靠 。在 POST++ 中 使用 标记和 革除垃圾收集机制 。在存储中存在一个特殊的对象:根对象 。垃圾收集器首先 标记全部的对象可被根对象 拜访(也便是 可以从根对象 到达,和通过 引用遍历) 。这样在第一次GC阶段全部未被 标记的对象被 开释 。垃圾收集器 可以在对象从文件载入的时候生成(假如你传递 do_garbage_collection 属性给 storage::open() 步骤) 。也 可以在程序运行期间调用 storage::do_mark_and_sweep() 步骤调用垃圾收集器 。然而请务必确定没有被程序变量指向的对象不可从根对象 拜访(这些对象将被GC 开释) 。

  基于多继承C++类在对象中 可以有非零偏移而且对象内也可能有 引用 。这是我们为何要 使用特殊的技术 拜访对象头的缘由 。POST++ 保护页 调配位图,其中每一个位对应存储器中的页 。假如一些大对象 调配在几个页中,全部这些对象占用的页所对应的位除了第一个外都被置为1 。全部 其余页在位图中有对应清空位 。要找到对象起始地址,我们首先按页大小罗列指针值 。 而后 POST++ 从位图中搜索对象起始页(该页在位图中有零位) 。 而后从页开始处包括的对象头中 存入对象大小的信息 。假如大小大于页大小的一半那我们已经找到了对象 形容:它在该页的开始处 。反之我们计算页中所 使用的固定块的大小而且把页中指针偏移按块大小计算出来 。这种头部定位 方案被垃圾收集器 使用,类 object 定义了 operator delete,和被从对象头部解析出对象大小和类信息的 步骤 使用 。

  在 POST++ 中提供了特殊重载的 new 步骤用于存储中的对象 调配 。这个 步骤需要 缔造对象的类 形容, 缔造对象的存储器,以及可选的对象实例可变 部分的大小作为额外的参数 。宏 new_in(STORAGE, CLASS) 提供 永远对象 缔造“语法糖” 。 永远对象 可以被重定义的 operator delete 删除 。

   永远对象 协定

  在 POST++ 中全部的 永远对象的类必须继承自 object.h 中定义的类 object  。这个类不含任何变量并提供了 调配/ 开释对象及运行时得到类信息和大小的 步骤 。类 object 可以是多继承中一个基类(基类的 秩序无所谓) 。每一个 永远类必须有一个供POST++ 系统 使用的 构造函数(见 Describing object class 一节) 。这 象征着你不能 使用没有参数的 构造函数来初始化 。假如你的类 构造函数甚至没有有 意思的参数,你必须加一个虚构的以和宏 CLASSINFO 缔造的 构造函数区别开来 。

  为了 拜访 永远存储器中的对象程序员需要某种根对象,通过它 可以 使用一般的C指针 拜访到每一个 其余对象 。POST++ 存储器提供了两个 步骤用于指定和得到根对象的 引用:

  void set_root_object(object* obj); object* get_root_object();

  当你 缔造新存储时 get_root_object() 返回 NULL 。你需要通过 set_root_object() 步骤 缔造根对象而且在其中 保留 引用 。下一次你 打开存储时,根对象 可以通过 get_root_object() 得到 。

   揭示:在实际 利用中类通常在程序开发和 保护过程中被转变 。 可怜的是 POST++ 考量到的 方便没有提供自动对象转换的工具(参见 GOODS 中的 懈怠对象更新设计示例),所 认为了幸免增加新的字段到对象中,我不得不 提议你在对象中保留 部分空间供 将来 使用 。这对根对象来说 意思尤其重大,由于它是新加入对象的优选者 。你也需要幸免转换根对象的 引用 。假如没有 其余对象含有指向根对象的 引用,那么根对象 可以被 方便的转变(通过 set_root_object 步骤)到新类的实例 。POST++ 存储提供设置和 获得村出版标识的 步骤 。这个标识 可以用于 利用依据存储器和 利用的版 原来更新存储器中对象 。

  存储器 构造函数你 可以在 利用中同时 使用几个存储器 。存储器 构造函数有一个 必须的参数 - 存储文件路径 。假如这个文件没有 扩大名,那么 POST 为文件名增加一个后缀“ 。odb” 。这个文件名也被 POST++ 用于 构成几个辅助文件的名字:

  文件 形容 使用 机会后缀包括新存储器映像的暂时文件用于非事务 解决模式下 保留存储器新映像".tmp"事务记录文件用于事务模式下 保留镜像页面".log" 保留存储器文件备份仅用于Windows-95下重命名暂时文件".sav"

  存储器 构造函数的另两个参数 存在缺省值 。第一个参数 max_file_size 指出存储器文件 扩大 制约 。假如存储器文件大于 storage::max_file_size 那么它不会被切除然而也不可能更进一步的 扩大 。假如 max_file_size 大于文件大小,行为依赖于 打开存储器的模式 。在事务模式下,文件在读写 掩护下被镜像到内存中 。Windows-NT/95 扩大文件大小到 max_file_size 。文件大小被 storage::close() 步骤缩小到存储器中最终一个对象的边界 。在 Windows 中为了以读写模式 打开存储器需要在磁盘上至少有 storage::max_file_size 的空暇字节数 即便你不 预备向其中加入新对象 。

  存储器 构造函数的最终一个参数是 max_locked_objects,这个参数仅在事务模式下用于提供镜像页面的写事务记录文件的缓冲区 。为了提供数据 统一性 POST++ 必须 保障 批改页在刷新到磁盘前镜像页被 保留在事务记录文件中 。POST++ 使用两个 路径中的一个:同步记录写 (max_locked_objects == 0) 和在内存中页面锁定的缓冲写 。通过内存中锁定页面,我们 可以 保障它在事务记录缓冲钱不被 交换到磁盘上 。镜像页面在异步 模式下被写到事务记录文件中 (包括启用操作系统缓冲) 。当锁定页面数超过 max_locked_pages,记录文件缓冲被刷新到磁盘上而且全部锁定页面被解锁 。这个 步骤 可以卓著的 普及事务 解决 威力(在NT下 普及了5倍) 。然而 可怜的是不同的操作系统 使用不同的 步骤在内存中锁定页面 。

  Windows 95 根本不 支撑 。 在 Windows NT 每个 历程 可以锁定它的页面,然而锁定页面的总数不 可以超过 历程运行配置 制约 。在缺省状况下 历程 可以锁定超过30个的页面 。假如你指定 max_locked_pages 参数大于30,那么 POST++ 将试图 扩大 历程配置 合适你的需要 。然而从我的 教训来看30个和60个锁定页面中间性能的差距是十分小的 。 在Unix下惟独超级消费者 可以在内存中锁定页面 。这是之所以文件 构造函数 审查 历程是不是 存在足够的权限 使用锁定操作 。 因此假如你指定 max_locked_pages 参数大于0,那么在存储类 缔造时将决定 使用同步还是异步写事务记录文件 。假如你 盼望 使用内存锁定机制带来的 好处(2-5 倍,依据事务类型),你需要转变你的 利用的全部者为 root 而且赋予 set-user-ID 权限:chmod +s application.

   打开存储器

POST++ 使用内存内存映射机制 拜访文件中的数据 。在 POST++ 通过两个不同的 步骤提供数据 统一性 。首先而且更加先进的是基于事务机制 使用的镜像页面在出错后来提供存储 复原和事务回滚 。在写镜像页面前 缔造运算被 使用 。这个运算以如下 模式执行:全部文件映射页面被设置为只读 掩护 。任何对这些页面的写 拜访将引起 拜访违反 异样 。这个 异样被一个特殊的句柄 拿获,它转变页面 掩护为可读写并放这个页面的拷贝在事务记录文件中(记录文件名为原文件名和后追“ 。log”的组合) 。全部接下来这个页面的写操作将不再引起页面 舛误 。存储器 步骤 commit() 刷新全部的转变页面到磁盘上并截断记录文件 。storage::commit() 步骤被 storage::close() 隐含调用 。假如 舛误在 storage::commit() 操作前 产生,全部的转变将通过拷贝事务记录中转变的页面到存储数据文件被 复原 。同样全部的转变 可以通过显式调用