C++永久对象存储.docx
《C++永久对象存储.docx》由会员分享,可在线阅读,更多相关《C++永久对象存储.docx(11页珍藏版)》请在冰豆网上搜索。
C++永久对象存储
C++永久对象存储
核心提示:
从存储器中分配和释放对象永久对象协议存储器构造函数打开存储器POST++的安装POST++类库和POST++一起使用STL类替换标准分配子如何使用POST++S调试POST++应用的细节关于POST++更多的一些信息
C++永久对象存储(PersistentObjectStorageforC++)
简介
描述对象类型从存储器中分配和释放对象永久对象协议存储器构造函数打开存储器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)完成。
类名将和对象一起放在存储器中。
在打开存储器的时候类在存储和应用程序之间被镜像。
存储器中的类名和程序中的类名进行比较。
如果有类没有被程序定义或应用程序和存储器中的类有不同的大小,程序断言将失败。
下面的例子阐述了这些规则:
structbranch{object*obj;intkey;CLASSINFO(branch,REF(obj));};classfoo:
publicobject{protected:
foo*next;foo*prev;object*arr[10];branchbranches[8];intx;inty;object*childs[1];public:
CLASSINFO(foo,REF(next)REF(prev)REFS(arr)VREFS(linked));foo(intx,inty);};REGISTER(1,foo);main(){storagemy_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)("someparametersforroot");}…intn_childs=…;size_tvarying_size=(n_childs-1)*sizeof(object*);//Weshouldsubtract1fromn_childs,becauseoneelementisalready//presentinfixedpartofclass.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定义了operatordelete,和被从对象头部解析出对象大小和类信息的方法使用。
在POST++中提供了特别重载的new方法用于存储中的对象分配。
这个方法需要创建对象的类描述,创建对象的存储器,以及可选的对象实例可变部分的大小作为额外的参数。
宏new_in(STORAGE,CLASS)提供永久对象创建“语法糖”。
永久对象可以被重定义的operatordelete删除。
永久对象协议
在POST++中所有的永久对象的类必须继承自object.h中定义的类object。
这个类不含任何变量并提供了分配/释放对象及运行时得到类信息和大小的方法。
类object可以是多继承中一个基类(基类的次序无所谓)。
每一个永久类必须有一个供POST++系统使用的构造函数(见Describingobjectclass一节)。
这意味着你不能使用没有参数的构造函数来初始化。
如果你的类构造函数甚至没有有意义的参数,你必须加一个虚构的以和宏CLASSINFO创建的构造函数区别开来。
为了访问永久存储器中的对象程序员需要某种根对象,通过它可以使用普通的C指针访问到每一个其他对象。
POST++存储器提供了两个方法用于指定和得到根对象的引用:
voidset_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倍)。
但是不幸的是不同的操作系统使用不同的方法在内存中锁定页面。
Windows95根本不支持。
在WindowsNT每个进程可以锁定它的页面,但是锁定页面的总数不可以超过进程运行配置限制。
在缺省情况下进程可以锁定超过30个的页面。
如果你指定max_locked_pages参数大于30,那么POST++将试图扩展进程配置适合你的需求。
但是从我的经验来看30个和60个锁定页面之间性能的差距是非常小的。
在Unix下只有超级用户可以在内存中锁定页面。
这是之所以文件构造函数检查进程是否具有足够的权限使用锁定操作。
因此如果你指定max_locked_pages参数大于0,那么在存储类创建时将决定使用同步还是异步写事务记录文件。
如果你希望使用内存锁定机制带来的好处(2-5倍,根据事务类型),你需要改变你的应用的所有者为root并且给予set-user-ID权限:
chmod+sapplication.
打开存储器
POST++使用内存内存映射机制访问文件中的数据。
在POST++通过两个不同的方法提供数据一致性。
首先而且更加先进的是基于事务机制使用的镜像页面在出错后来提供存储恢复和事务回滚。
在写镜像页面前创建运算被使用。
这个运算以如下方式执行:
所有文件映射页面被设置为只读保护。
任何对这些页面的写访问将引起访问违反异常。
这个异常被一个特别的句柄捕获,它改变页面保护为可读写并放这个页面的拷贝在事务记录文件中(记录文件名为原文件名和后追“。
log”的组合)。
所有接下来这个页面的写操作将不再引起页面错误。
存储器方法commit()刷新所有的改变页面到磁盘上并截断记录文件。
storage:
:
commit()方法被storage:
:
close()隐含调用。
如果错误在storage:
:
commit()操作前发生,所有的改变将通过拷贝事务记录中改变的页面到存储数据文件被复原。
同样所有的改变可以通过显式调用
storage:
:
rollback()方法来复原。
通过指定storage:
:
open()方法的storage:
:
use_transaction_log属性来选择文件访问事务所基于的模式。
另外一个提供数据一致性的手段基于写拷贝机制。
在这种情况下源文件没有受到影响。
任何试图对文件镜像页面的改变,导致产生一个该页面的拷贝,它从系统交换区种分配并具有读写许可。
文件直到显式调用storage:
:
flush()方法时才更新。
这个方法写数据到临时文件(带后缀“。
tmp”)然后重命名为原来的。
因此这个操作形成文件的一个原子更新(当然假设在操作系统能保证rename()操作的原子数)。
注意:
如果你没有使用事务处理,storage:
:
close()方法不会刷数据到文件中。
所以如果你在此前调用storage:
:
flush()方法所有的自上次flush之后的改变将会丢失。
Windows95细节:
在Windows95中重命名到已有的文件是不行的,所以源文件首先被保存为带后缀“。
sav”的文件名。
然后后缀为“。
tmp”的临时文件被重命名为原来的名字以及最后的旧的拷贝被删除。
所以如果错误发生在flush()操作中并且之后你找不到存储文件,请不要惊慌,只需找到以后缀“。
sav”结束的文件并且重命名为原来的就可以了。
提示:
如果你计划在程序执行期间保存数据我强烈建议你使用事务处理。
也可以采用写拷贝的途径但是这样需要多得多的消耗。
同样如果存储非常大事务处理也通常更好,因为生成临时的文件拷贝需要很多的磁盘空面和时间。
这里有几个属性供存储器open()
方法使用:
support_virtual_functions如果存储器中的对象带有虚函数则必须设置这个属性。
如果没有设置这个属性,POST++假定所有的永久对象在存储中只包含有引用(对存储器中其他对象的)。
所以只有在数据文件映像的基地址发生改变时才需要调整引用(这个地址被存放在数据文件的第一个字中并且POST++通常试图映像文件到相同的地址上来避免不必要的引用调整)。
但是如果对象类包含虚函数,指向虚表的指针被放在对象内。
如果你重新编译你的应用,这个标的地址可能改变。
POST++库比较执行对象的时间戳和这个应用产生的数据库的时间戳进行比较。
如果这个时间戳不等的话,则会校正虚表的指针。
为了得到应用时间戳POST++必须可以定位执行文件对象。
不幸的是没有找到执行文件名的简便的方法。
在Unix下POST++看命令行解释器设置的环境变量“_”的值。
但如果进程不是从命令行执行的(比如通过system())或者工作目录被chdir()改变这个方法将不起作用。
最简单的方法是使用文件comptime.cxx,它必须在每次重编译你的应用时被编译并和存储库一同被链接。
在Windows中没有这个问题,执行映像的名称可以通过Win32API得到。
在存储器打开时POST++比较这个时间戳和数据文件的时间辍,如果他们不等并且指定了support_virtual_functions属性那么校正所有对象(通过调用缺省构造函数)。
read_only通过设置这个属性程序员说明他只需要数据文件读权限。
POST++将创建数据文件的只读视图并且任何改变存储器中的对象或者分配新对象的尝试将会导致保护违例错。
这里有一个例外:
如果不能够映像数据文件到相同的地址或者应用程序发生改变时并且指定了support_virtual_functions,那么对此区域的保护被临时改变为写拷贝并且装载的对象被转换。
use_transaction_log设置这个属性强制对所有数据文件更新使用事务。
影子页面被用来执行事务。
事务在第一次修改存储后被打开。
通过storage:
:
commit()或者storage:
:
rollback()操作显式的关闭。
方法storage:
:
commit()保存所有的改变页面到磁盘上并且截断事务记录,方法
storage:
:
rollback()忽略此次事务中的所有改变。
no_file_mapping缺省情况下POST++将映像数据文件到进程虚拟内存中。
这种情况下打开数据库的时间将大大减少,因为文件页面将在需要时调入。
但是如果数据库大小不是特别大或者数据库中所有数据需要立即访问,那么把文件读入内存优于使用虚拟内存映像因为这种情况下没有额外的页面溢出错误。
标志no_file_mapping阻止POST++映像文件并根据分配的内存段读文件。
fault_tolerant这个标志被应用程序用于在系统或应用出错情况下想保护数据库的一致性。
如果使用了事务use_transaction_log这个标志不必指定,因为一致性可以由事务机制来提供。
如果没有指定use_transaction_log标志并且设置了fault_tolerant标志,POST++将不改变源文件而保持它的一致性。
这依靠读文件到内存中(如果没有设置no_file_mapping标志)或者使用写拷贝页面保护。
在后一种情况下试图改变映像到文件的页面将导致在系统交换文件中生成页面拷贝。
flush()方法将保存内存内数据库的映像到临时文件中然后使用原子操作重命名到源文件。
如果没有指定fault_tolerant标志,POST++在数据库页面上原有位置进行修改,提供最大的应用性能(因为没有拷贝修改页面和保存数据库映像到临时文件的额外开销)。
在修改页面没有立刻刷新到磁盘的条件下,部分改变可能因为系统错误而丢失(最坏的事是部分修改的页面保存了而另外一些没有保存-这样数据库的一致性可能被搅乱了)。
do_garbage_collection当设置了这个属性时POST++将在打开储存器时执行垃圾收集。
垃圾收集操作和指针对齐联系在一起。
使用垃圾收集往往比手工内存释放来的安全(考虑到挂起的引用问题),但是显式内存释放开销较少。
POST++中的垃圾收集相比显式内存分配有一个更大的优势:
内存收集器对小对象使用的页面进行优化。
如果页中没有已分配的小对象那么垃圾收集器将在空闲页中包含这一页。
这不会在显式释放时完成,因为小对象的空闲单元被串成链不能简单从这个链中移开(在垃圾收集器中所有的链被重新构造)。
即使你使用显式内存释放,我仍建议你每隔一定时间做垃圾收集来检查引用的一致性和没有内存泄漏(garbage_collection方法返回释放对象的数目,如果你确信你已经释放了所有的不能到达的对象,那么这个值将会是0)。
考虑到垃圾收集器修改存储中所有的对象(设置掩码位),重连链中空闲对象),在事务模式下运行GC可能是消耗时间和磁盘空间的操作,因为所有文件中的页将被拷贝到事务记录文件中)。
你可以通过file:
:
max_file_size变量指定存储文件的最大尺寸。
如果数据文件的大小比file:
:
max_file_size并且模式不是read_only,那么虚拟空间size_of_file-file:
:
max_file_size以外的字节将被保留在文件映像空间的后面。
当存储大小扩展时(因为分配新对象),这些页面将被提交(在WindowsNT)并被使用。
如果文件大小大于file:
:
max_file_size或者使用了read_only模式,那么映像区域的大小和文件大小一致。
在后一种情况下不可能进行存储扩展。
在Windows中我使用GlobalMemoryStatus()方法来得到关于系统真实可分配的虚拟内存的信息并减少file:
:
max_file_size为该值。
不幸我发现在Unix中没有轻便的调用可用来达到相同的目的(getrlimit不返回用户进程可使用的虚拟内存的确切信息)。
对象存储的接口在文件storage.h定义并且实现部分可在storage.cxx中看到。
依赖于操作系统的映像内存文件的部分被封装在file类中,其定义在file.h实现在file.cxx.
POST++的安装
POST++的安装十分简单。
目前在以下系统已经过测试:
DigitalUnix,Linux,Solaris,WindowsNT4.0,Windows95.我希望对于大部分所有其他新Unix方言(AIX,HP-UX10,SCO…)也没有问题。
不幸的是我没有时用过这些系统。
在Windows下我使用MicrosoftVisualC++5.0和Borland5.02compilers编译。
VisualC++的Makefiel是makefile,BrolandC++的Makefile是makefile。
为使用POST++唯一你需要的东西就是函数库(在Unix下是libstorage.a在Windows下是andstorage.lib)。
这个库可以通过运行make命令生成。
有个特别的MAKE.BAT用于MicrosoftVisualC++,它使用makefile.mvc作为输入调用NMAKE(如果你正在使用Borland请编辑这个文件或者通过make.exe-fmakefile.bcc命令调用)。
在Unix