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() 操作前发生,所有的改变将通过拷贝事务记录中改变的页面到存储数据文件被复原。同样所有的改变可以通过显式调用