C++永久Object Storage Service (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() 操作前發生,所有的改變將通過拷貝事務記錄中改變的頁面到儲存資料檔案被複原。同樣所有的改變可以通過顯式調用
storage::rollback() 方法來複原。通過指定 storage::open() 方法的 storage::use_transaction_log 屬性來選擇檔案訪問事務所基於的模式。
另外一個提供資料一致性的手段基於寫拷貝機制。在這種情況下源檔案沒有受到影響。任何試圖對檔案鏡像頁面的改變,導致產生一個該頁面的拷貝,它從系統交換區種分配並具有讀寫許可。檔案直到顯式調用 storage::flush() 方法時才更新。這個方法寫資料到臨時檔案(帶尾碼“。tmp”)然後重新命名為原來的。因此這個操作形成檔案的一個原子更新(當然假設在作業系統能保證 rename() 操作的原子數)。
注意:如果你沒有使用交易處理,storage::close() 方法不會刷資料到檔案中。所以如果你在此前調用 storage::flush() 方法所有的自上次 flush 之後的改變將會丟失。
Windows 95 細節:在 Windows 95 中重新命名到已有的檔案是不行的,所以源檔案首先被儲存為帶尾碼“。sav”的檔案名稱。然後尾碼為“。tmp”的臨時檔案被重新命名為原來的名字以及最後的舊的拷貝被刪除。所以如果錯誤發生在 flush() 操作中並且之後你找不到隱藏檔,請不要驚慌,只需找到以尾碼“。sav”結束的檔案並且重新命名為原來的就可以了。
提示:如果你計劃在程式執行期間儲存資料我強烈建議你使用交易處理。也可以採用寫拷貝的途徑但是這樣需要多得多的消耗。同樣如果儲存非常大交易處理也通常更好,因為產生臨時的檔案拷貝需要很多的磁碟空面和時間。
這裡有幾個屬性供儲存空間 open()
方法使用:
support_virtual_functions 如果儲存空間中的對象帶有虛函數則必須設定這個屬性。如果沒有設定這個屬性,POST++ 假定所有的永久對象在儲存中只包含有引用(對儲存空間中其他對象的)。所以只有在資料檔案映像的基地址發生改變時才需要調整引用(這個地址被存放在資料檔案的第一個字中並且 POST++ 通常試圖映像檔案到相同的地址上來避免不必要的引用調整)。但是如果對象類包含虛函數,指向虛表的指標被放在對象內。如果你重新編譯你的應用,這個標的地址可能改變。POST++ 庫比較執行對象的時間戳記和這個應用產生的資料庫的時間戳記進行比較。如果這個時間戳記不等的話,則會校正虛表的指標。為了得到應用時間戳記 POST++ 必須可以定位執行檔案對象。不幸的是沒有找到執行檔案名稱的簡便的方法。在 Unix 下POST++ 看命令列解譯器設定的環境變數“_”的值。但如果進程不是從命令列執行的(比如通過system())或者工作目錄被 chdir() 改變這個方法將不起作用。最簡單的方法是使用檔案comptime.cxx,它必須在每次重編譯你的應用時被編譯並和存放庫一同被連結。在 Windows 中沒有這個問題,執行映像的名稱可以通過 Win32 API 得到。在儲存空間開啟時 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 以外的位元組將被保留在檔案映像空間的後面。當儲存大小擴充時(因為分配新對象),這些頁面將被提交(在 Windows NT)並被使用。如果檔案大小大於 file::max_file_size 或者使用了 read_only 模式,那麼映像地區的大小和檔案大小一致。在後一種情況下不可能進行儲存擴充。在 Windows 中我使用 GlobalMemoryStatus() 方法來得到關於系統真實可分配的虛擬記憶體的資訊並減少 file::max_file_size 為該值。不幸我發現在 Unix 中沒有輕便的調用可用來達到相同的目的(getrlimit 不返回使用者進程可使用的虛擬記憶體的確切資訊)。
Object Storage Service的介面在檔案 storage.h 定義並且實現部分可在 storage.cxx 中看到。依賴於作業系統的映像記憶體檔案的部分被封裝在 file 類中,其定義在 file.h 實現在 file.cxx.
POST++ 的安裝
POST++ 的安裝十分簡單。目前在以下系統已經過測試:Digital Unix, Linux, Solaris, Windows NT 4.0, Windows 95. 我希望對於大部分所有其他新 Unix 方言(AIX, HP-UX 10, SCO…)也沒有問題。不幸的是我沒有時用過這些系統。在 Windows 下我使用 Microsoft Visual C++ 5.0 和 Borland 5.02 compilers 編譯。Visual C++ 的 Makefiel 是 makefile,Broland C++ 的 Makefile 是 makefile。
為使用 POST++ 唯一你需要的東西就是函數庫(在 Unix 下是 libstorage.a 在 Windows 下是and storage.lib)。這個庫可以通過運行 make 命令產生。有個特別的 MAKE.BAT 用於 Microsoft Visual C++,它使用 makefile.mvc 作為輸入調用 NMAKE (如果你正在使用 Borland 請編輯這個檔案或者通過 make.exe -f makefile.bcc 命令調用)。
在 Unix 下的安裝可以通過拷貝 POST++ 庫和同檔案到一些標準系統目錄下來完成。你必須為 makefile 裡 INSTALL_LIB_PATH 和 INSTALL_INC_PATH 變數設定恰當的值並運行 make install 命令。INSTALL_LIB_PATH 的預設值為 /usr/local/lib INSTALL_INC_PATH 的是 /usr/local/include。你可以通過明確指定路徑到 POST++ 目錄來編譯和連結而避免拷貝檔案到系統目錄中。
POST++ 類庫
POST++ 包括一些持久類的定義,她們可以用於你的應用也可以作為開發 POST++ 類的好例子。你可以看見那些類的實現中甚至沒有 POST 特定的代碼。這些類包括 array(數組), matrix(矩陣), string(字串), L2-list(L2-列表), hash table(雜湊表), AVL-tree(AVL-樹), R-tree(R-數), text object(文字物件)。 R-tree 提供了提供空間對象(包含空間對等物的對象)的快速存取。文字物件包含了 Boyer and Moore 演算法的修正,擴充到由 OR/AND 關係組合的多樣式搜尋。這些類的定義可以在以下檔案中發現:
描述介面實現Arrays of scalars and references, matrixes and stringsarray.harray.cxxL2-list and AVL-treeavltree.havltree.cxxHash table with collision chainshashtab.hhashab.cxxR-tree with quadratic method of nodes splittingrtree.hrtree.cxxT-tree (combination of AVL tree and array)ttree.httree.cxxText object with modified Boyer and Moore search algorithmtextobj.htextobj.cxx
在論文 "A study of index structures for main memory database management systems" 中 T.J. Lehman and M.J Carey 提議使用 T-trees 作為主要記憶體資料庫的一個儲存的有效資料結構。T-trees 基於 AVL 樹由 Adelson-Velsky and Landis 提議。在這個分段中,我們提供 T-trees 一個概覽作為在 POST++ 中的實現。
像 AVL 樹一樣,T-tree的左子樹和右子樹高度差不大於1。和 AVL 樹不一樣,T-tree的每一個節點排列次序儲存多個關索引值而不是單一關索引值。一個節點裡最左邊和最右邊關索引值定義包含在這個節點內關索引值的範圍。這樣,一個節點左子樹只包含比最左關索引值小的關索引值,而右子樹包含比該節點最右關索引值大的關索引值。節點內落在最小和最大關索引值之間的關索引值被稱作被該節點約束( bounded )。注意等於最大關鍵字或最小關鍵字的關鍵字可以根據索引是否全域唯一和尋找條件認為是被約束或不被約束(e.g. “大於”("greater-than") 相對於 “大於等於”("greater-than or equal-to"))。
一個既有左子樹也有右子樹的節點被歸為內部節點(internal node),僅有一個子樹的節點被歸為不完全葉(semi-leaf),一個沒有子樹的節點被歸為葉(leaf)。為了保持高佔用率,每個內部節點具有一個必須包含的關索引值的最小數目(一般為 k-2,如果 k 是可以排列在一個節點裡的關鍵字的最大數目)。然而,對於葉子和不完全葉來說沒有佔用率條件。
在T-tree中尋找相當於直接尋找。對於每一個節點,檢查看一下關索引值是否在該節點最左和最右關索引值之間;如果是這種情況,就是說節點裡包這個關索引值的話就返回該值(否則關索引值不包含在樹中)。否則,如果關索引值比最左關索引值小,那麼尋找左子樹,反之搜尋右子樹。重複這個過程直到發現這個關索引值或者尋找到null節點。
在T-tree中插入和刪除稍微複雜一些。對於插入操作,首先使用上面的搜尋過程來尋找約束插入關索引值的節點。如果存在這樣的節點,那麼節點中有空餘的話關索引值被插入到這個節點中。如果節點中沒有空餘,那麼關索引值被插入到節點中,該節點的最左關索引值被插入到該節點的左子樹中(如果左子樹為空白,就分配一個新的節點把最左關索引值插入)。如果沒有發現約束節點,那我們用N來表示搜尋失敗的最後遇到的節點並按下述步驟繼續:如果N有空餘,那麼關索引值就插入到N中;否則,關索引值將被插入到一個新節點並根據關索引值以及最左關索引值和最右關索引值作為N的右或左子節點/樹。
刪除關索引值從判定包含關索引值的節點開始,並從節點中刪除關索引值。如果刪除關索引值後剩下一個空的葉結點,那麼節點也被刪除。如果刪除後剩下一個內部節點或者不完全葉其中包含比最小數量少的關索引值,那麼不足的將通過移動左子樹中最大的關索引值到節點中或者合并節點和右子樹來補充。
無論插入還是刪除,分配/釋放節點可能導致樹不平衡和進行旋轉操作(RR, RL, LL, LR)。(下面的描述中子樹的高度包括了插入和刪除的影響。)在插入情況下,檢查沿著新分配節點開始的節點到根節點的節點直到
發現一個節點有兩個等高的子樹(在這種情況下不需要旋轉了),或者 發現一個節點的左子樹和右子樹有大於1的不同高度並執行包含節點的單一旋轉。
在刪除情況下,檢查沿著釋放節點的父母開始到根節點的節點直到發現一個節點它的子樹高度相差1。而且每一次遇到一個節點的子樹的高度相差大於1時,進行一次旋轉。注意釋放一個節點可能導致多次旋轉。
有幾個測試程式來測試 POST++ 持久類庫中的類,他們被包含在預設的make目標中:
ProgramTested classestesttree.cxxAVL-tree, l2-node, hash tabletesttext.cxxtext, stringtestspat.cxxrectangle, R-tree, hash tabletestperf.cxxT-tree insert, find and remove operations
和POST++一起使用STL類
從 POST++ 儲存空間中儲存和取得STL類是可能的。POST++ 提供了特別的STL分配子並重載了new/delete操作符來保持STL對象。有幾個使得STL持久的模型,通過以下幾個宏來控制:
USE_MICROSOFT_STL 使用 Microsoft STL 類,隨 Microsoft Visual C++ 一起附帶。這個類庫和C++ STL 標準不完全相容。 USE_STD_ALLOCATORS 使用和C++標準中一樣的分配子。分配子物件包含在STL對象中,分配子物件中的執行個體方法用於分配/釋放空間。所以有可能執行一個“聰明的”分配子:僅在對象包含這個分配子的情況下分配子才為 POST++ 儲存空間中的對象分配空間而且這個分配子也放在儲存空間中。否則,對象所佔空間將通過標準 malloc() 函數這個正常途徑來分配。這個選項可以和 SGI STL 庫以及 Microsoft STL 庫一起使用。注意順從標準的 SGI STL 分配子使用了許多沒有廣泛實現的語言特性。特別是,他們依賴於成員模板,局部特殊化,局部分類(排序)的方法樣板,typename 關鍵字,以及使用 template 關鍵字來引用依賴類型的模板成員。所以在指定這個宏定義時只有少數 C++ 編譯器可以編譯 SGI STL 庫。如果沒有設定這個宏,那麼 POST 提供一個含有靜態成員函數的分配子並且所有對象都從 POST++ 儲存空間中分配。使用這個分配子的應用一次只能開啟一個POST++ 儲存空間。 REDEFINE_DEFAULT_ALLOCATOR 有兩個途徑使得 STL 對象持久,一個途徑是引入新類型:
typedef basic_string, post_alloc > post_string;
另外一個途徑是使所有類具有持久能力。當定義了 REDEFINE_DEFAULT_ALLOCATOR 宏,任何 STL 類可以在 POST++ 儲存空間中分配。為了在持久儲存空間中建立新對象,你必須指定儲存空間作為 new 操作符額外的參數。如果儲存空間被忽略,那麼對象將使用標準 malloc() 函數。
POST++ 到 STL 的介面不需要任何 STL 類的改變,所以你可以使用你想執行的任何 STL。但是作為一個結果,STL類不包含類型描述所以 POST++ 沒有STL 對象的格式資訊。所以 POST++ 不能夠進行垃圾收集和引用對齊。當使用了 STL 介面時,POST++ 儲存空間必須始終鏡像到相同的虛擬位址上。如果你傳遞 storage::fixed 標誌到 storage::open(int flags) 方法,那麼如果不能鏡像儲存空間到相同的記憶體位址 POST++ 將報告錯誤並且返回 false。如果你的應用只使用了一個儲存空間並且不鏡像其他對象到虛擬記憶體中,那麼幾乎所有的操作下都可能鏡像儲存空間到相同的記憶體位址。
POST++ 到 STL 庫的介面定義在在標頭檔 post_stl.h 中。這個檔案必須包含在任何 STL 包含檔案中。同樣宏 REDEFINE_DEFAULT_ALLOCATOR,USE_STD_ALLOCATORS 以及 USE_MICROSOFT_STL 被在 post_stl.h 之前定義。
POST++ 包含使用 STL++ 類的例子 stltest.cxx。這個例子使用兩個 STL 類 - string 和 vector。從標準輸入讀進來的行被壓入到 vector 中並且程式被再次啟動所有的vector的元素被列印到標準輸出上。這個例子被包含在為 Microsoft Visual C++ 配置的 makefile 的預設目標中(其使用VC 中附帶的 Microsoft STL 類)。你也可以嘗試用其他版本的 STL 庫來構建這個測試但是不要忘記關於 REDEFINE_DEFAULT_ALLOCATOR 和 USE_STD_ALLOCATORS 宏。這個例子同樣可以和 SGI STLport 3.12 以及 GCC 2.8.1 一起測試。
替換標準分配子
在前面一節中我解釋了如何和 STL 庫一起使用 POST++。但是仍然有很多其他你想用的C++和C庫以及應用,而且沒有提供象 STL 這種易通融的分配機制。在這種情況下唯一可能的解決方案(如果你不想改變此庫的任何原始碼的話)就是用一個 POST++ 提供的來替換標準的分配機制。這樣任何動態指派至都從 POST++ 儲存空間中分配(除了一個不能在這種情況下使用的儲存空間)。
POST++ 發行包中包含的檔案 postnew.cxx 重定義了標準的 malloc,free,realloc 和 calloc 函數。當開啟儲存空間時,所有的對象被在儲存空間中分配。否則 sbrk() 函數被用來指派至(為這些對象分配的空間沒有回收)。可能不需要接觸這些標準C分配函數而僅需重載預設的C++操作符 new 和 delete。當編譯 postnew.cxx 時定義 DO_NOT_REDEFINE_MALLOC 宏來這麼做。從 postnew.cxx 產生的目標檔案必須在標準C庫前傳遞給連結程式。
作為一個 POST++ 這樣使用的例子可以參見 testnew.cxx 和 testqt.cxx。第一個舉例說明了標準C++數組如何持久化。第二個舉例說明了POST++如何和Qt類庫一起工作。
就 POST++ 沒有關於儲存類的格式資訊這裡有一些限制在 POST++ 的使用上:
包含虛函數的類不被支援(POST++ 不能正確的初始化到虛函數表的指標)。 隱式記憶體釋放(垃圾收集器)是不可能的 - POST++ 沒有關於對象內部指標位置的資訊。 儲存空間必須總映射到相同的虛擬位址上因為如果基地址改變了 POST++ 不能調整指標。
如果所有這些限制不是你的應用所必需的,你可以使其持久化而不需要任何代碼的改動。這個方法在C和C++程式中都可以使用。
如何使用 POST++
這裡有幾個 POST++ 類和應用的例子。其中最簡單的就是遊戲“猜動物”。這個遊戲的演算法非常簡單並且結果看起來給人以深刻的印象(有些象人工智慧)。此外這個遊戲是一個非常好的例子,闡明了持久Object Storage Service的好處。這個遊戲的原始碼在檔案 guess.cxx 中。建立這個遊戲包含在預設的make目標中。執行guess來運行它。
Unix specific: 當你準備和 POST++ 庫連結你的Unix應用並且持久對象中波阿含虛函數,請不要忘記重編譯 comptime.cxx 檔案並包含在連結清單中。這個檔案是必須的用於 POST++ 提供可執行檔的時間戳記,被放在儲存空間中用來判定什麼時候應用被改變並在需要的時候重新初始化對象內的虛函數表。Attention! 這個檔案必須在你每次重新連結你的應用時被重新編譯。我建議你讓編譯器為你調用連結程式並包含 comptime.cxx 源檔案在為運行映像目標檔案提供的對象檔案清單中(see makefile)。
調試 POST++ 應用的細節
這一節的內容對使用了事務的應用是非常有意義的。POST++ 使用頁面保護機制來提供當源頁面修改時產生影子頁面,當儲存空間開啟或事務提交時所有檔案頁面的映像是唯讀保護的。所以任何試圖修改分配在這些頁面裡對象的內容將導致一個訪問違例異常。這個異常被指定的 POST++ 控制代碼處理。但是如果你使用調試器,它將首先捕獲這個異常並停止應用程式。如果你想調試你的應用你必須作一些準備:
在 Unix 可以充分的告訴調試器不要捕獲 SIGSEGV 訊號。比如對於 GDB 它可以通過命令來完成:handle SIGSEGV nostop noprint pass。如果 SIGSEGV 訊號不是由儲存頁面保護違例產生,但是是程式中的一個錯誤,POST++ 例外處理常式將“理解”它不是自己的異常並送出一個 SIGABRT 訊號到己進程中,這可以被調試器捕獲。 在 Windows POST++ 使用不處理異常過濾器來( Unhandled Exception Filter )處理儲存空間頁面保護違例。不幸的是不可能讓 Microsoft Debugger 忽略不處理異常。如果你準備調試你的應用,你必須把所有你的程式碼(main 或者 WinMain 函數)封裝為結構化的異常阻塞。你必須在 Borland C++ 中總使用結構化異常處理,因為 Unhandled Exception Filter 沒有在Borland中被正確調用。請使用兩個宏 SEN_TRY 和 SEN_ACCESS_VIOLATION_HANDLER() 來封裝 main(或 WinMain)的函數體:
main() { SEN_TRY { … } SEN_ACCESS_VIOLATION_HANDLER(); return 0; }
請確定調試器對此異常的行為是“如果沒有處理就停止”而不是“總是停止”(你可以在 Debug/Exceptions 菜單中檢查它)。在檔案 testrans.cxx 中你可以發現使用結構化異常處理的例子。
關於 POST++ 的更多的一些資訊
POST++ 是 freeware。開發出她希望是有用的。通過她你可以做任何你想做的(在開發產品中使用 POST++ 沒有任何限制)。我將很高興來協助你使用 POST++ 和得到關於 POST++ 任何類型的資訊(錯誤報表,建議…)。POST++ 的免費軟體情形並不意味著缺少支援。我保證將努力修正任何報告的錯誤。也提供 e-mail 支援。POST++ 有幾種用途:在不同期間儲存資訊,在檔案中儲存對象系統,快照, 資訊系統… 但是如果你感到你需要在你的應用使用更重要的物件導向資料庫以提供並發,分布式以及交易處理,請訪問 GOODS (Generic Object Oriented Database System) home page。