C++永久物件儲存 (Persistent Object Storage for C++) (轉)

worldblog發表於2007-12-09
C++永久物件儲存 (Persistent Object Storage for C++) (轉)[@more@]

 

 

 

C++永久 (Persistent Storage for C++)

  • /editor1/editor.htm#introduction">簡介
  • ging">S POST++ 應用的細節

POST++ 提供了對應用物件的簡單有效的儲存. POST++ 基於映象機制和頁面映象處理。POST++ 消除了對永久物件訪問的開銷. 此外 POST++ 支援多儲存,虛擬函式, 資料原子操作, 高效的記憶體分配和為指定釋放記憶體方式下可選的垃圾收集器. POST++ 同樣可以很好的工作在多繼承和包含指標的物件上。

 

POST++ 儲存管理需要一些資訊以使永久物件型別支援垃圾收集器,裝載時引用重定位和初始化虛表內函式指標。但不幸的是C++語言沒有提供執行時從類中或許這些資訊的機制。為了避免使用一些特殊的工具(預)或“髒哄騙”途徑(從除錯資訊中獲取類資訊),這些資訊必須由員來指明。這些稱為類註冊器的東西可以簡單的透過POST++提供的一些宏來實現。

POST++ 在從儲存器過載入物件時預設建構函式來初始化物件。為了使物件控制程式碼能夠儲存,程式設計師必須在類定義中包含宏 CLASSINFO(NAME, FIELD_LIST) . NAME 指明物件的名字。 FIELD_LIST 描述類的的引用欄位。在標頭檔案 定義了三個宏用於描述欄位:

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.o"); if (my_storage.open()) { my__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++ 使用第一次餵給隨機定位演算法維護空閒頁佇列(所有頁的空閒段按照地址排列並用一個特別的指標跟隨佇列的當前位置)。儲存管理的實現見檔案

使用顯式還是隱含的記憶體釋放取決於程式設計師。顯式記憶體釋放要快(特別是對小物件而言)但是隱含記憶體釋放(垃圾收集)更加可靠。在 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 。這個類不含任何變數並提供了分配/釋放物件及執行時得到類資訊和大小的方法。類 object 可以是多繼承中一個基類(基類的次序無所謂)。每一個永久類必須有一個供POST++ 系統使用的建構函式(見 一節)。這意味著你不能使用沒有引數的建構函式來初始化。如果你的類建構函式甚至沒有有意義的引數,你必須加一個虛構的以和宏 CLASSINFO 建立的建構函式區別開來。

為了訪問永久儲存器中的物件程式設計師需要某種根物件,透過它可以使用普通的C指標訪問到每一個其他物件。POST++ 儲存器提供了兩個方法用於指定和得到根物件的引用:

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


當你建立新儲存時 get_root_object() 返回 NULL。你需要透過 set_root_object() 方法建立根物件並且在其中儲存引用。下一次你開啟儲存時,根物件可以透過 get_root_object() 得到。

提示:在實際應用中類通常在程式開發和維護過程中被改變。不幸的是 POST++ 考慮到的簡單沒有提供自動物件轉換的工具(參見 中的懶惰物件更新設計示例),所以為了避免新增新的欄位到物件中,我只能建議你在物件中保留部分空間供將來使用。這對根物件來說意義尤其重大,因為它是新加入物件的優選者。你也需要避免轉換根物件的引用。如果沒有其他物件含有指向根物件的引用,那麼根物件可以被簡單的改變(透過 set_root_object 方法)到新類的例項。POST++ 儲存提供設定和取得村出版標識的方法。這個標識可以用於應用根據儲存器和應用的版本來更新儲存器中物件。

 

你可以在應用中同時使用幾個儲存器。儲存器建構函式有一個必需的引數 - 儲存檔案路徑。如果這個檔案沒有副檔名,那麼 POST 為檔名新增一個字尾“.odb”。這個檔名也被 POST++ 用於形成幾個輔助檔案的名字:

 

檔案描述使用時機字尾 包含新儲存器映像的臨時檔案用於非事務處理下儲存儲存器新映像 ".tmp" 事務記錄檔案用於事務模式下儲存映象頁面 ".log" 儲存儲存器檔案 僅用於-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 根本不支援。
  • 在 每個程式可以鎖定它的頁面,但是鎖定頁面的總數不可以超過程式執行限制。在預設情況下程式可以鎖定超過30個的頁面。如果你指定 max_locked_pages 引數大於30,那麼 POST++ 將試圖擴充套件程式配置適合你的需求。但是從我的來看30個和60個鎖定頁面之間的差距是非常小的。
  • 在下只有超級可以在記憶體中鎖定頁面。這是之所以檔案建構函式檢查程式是否具有足夠的使用鎖定操作。因此如果你指定 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() 改變這個方法將不起作用。最簡單的方法是使用檔案,它必須在每次重編譯你的應用時被編譯並和儲存庫一同被連結。在 Windows 中沒有這個問題,執行映像的名稱可以透過 得到。在儲存器開啟時 POST++ 比較這個時間戳和資料檔案的時間輟,如果他們不等並且指定了 support_virtual_functions 屬性那麼校正所有物件(透過呼叫預設建構函式)。
read_only
透過設定這個屬性程式設計師說明他只需要資料檔案讀許可權。POST++ 將建立資料檔案的只讀檢視並且任何改變儲存器中的物件或者分配新物件的嘗試將會導致保護違例錯。這裡有一個例外:如果不能夠映像資料檔案到相同的地址或者應用程式發生改變時並且指定了 support_virtual_functions ,那麼對此區域的保護被臨時改變為寫複製並且裝載的物件被轉換。
use_transaction_log
設定這個屬性強制對所有資料檔案更新使用事務。影子頁面被用來執行事務。事務在第一次修改儲存後被開啟。透過 storage::commit() 或者 storage::rollback() 操作顯式的關閉。方法 storage::commit() 儲存所有的改變頁面到磁碟上並且截斷事務記錄,方法 storage::rollback() 忽略此次事務中的所有改變。
no_file_map
預設情況下 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 不返回使用者程式可使用的虛擬記憶體的確切資訊)。

物件儲存的介面在檔案 定義並且實現部分可在 中看到。依賴於作業系統的映像記憶體檔案的部分被封裝在 file 類中,其定義在 實現在 .

 

POST++ 的安裝十分簡單。目前在以下系統已經過測試:Digital Unix, , , Windows NT 4.0, Windows 95. 我希望對於大部分所有其他新 Unix 方言(AIX, 10, ...)也沒有問題。不幸的是我沒有時用過這些系統。在 Windows 下我使用 Visual C++ 5.0 和 Borland 5.02 compilers 編譯。Visual C++ 的 Makefiel 是 ,Broland C++ 的 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_PATHINSTALL_INC_PATH 變數設定恰當的值並執行 make install 命令。INSTALL_LIB_PATH 的預設值為 /usr/local/lib  INSTALL_INC_PATH 的是 /usr/local/include。你可以透過明確指定路徑到 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 strings L2-list and AVL-tree Hash table with collision chains R-tree with quadratic method of nodes splitting T-tree (combination of AVL tree and array) Text object with modified Boyer and Moore search algorithm

在論文 "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. 發現一個節點有兩個等高的子樹(在這種情況下不需要旋轉了),或者
  2. 發現一個節點的左子樹和右子樹有大於1的不同高度並執行包含節點的單一旋轉。

在刪除情況下,檢查沿著釋放節點的父母開始到根節點的節點直到發現一個節點它的子樹高度相差1。而且每一次遇到一個節點的子樹的高度相差大於1時,進行一次旋轉。注意釋放一個節點可能導致多次旋轉。

有幾個測試程式來測試 POST++ 持久類庫中的類,他們被包含在預設的make目標中:

 

Program Tested classes AVL-tree, l2-node, hash table text, string rectangle, R-tree, hash table T-tree insert, find and remove operations

 

從 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 庫的介面定義在在標頭檔案 中。這個檔案必須包含在任何 STL 包含檔案中。同樣宏 REDEFINE_DEFAULT_ALLOCATOR,USE_STD_ALLOCATORS 以及 USE_MICROSOFT_STL 被在 post_stl.h 之前定義。

POST++ 包含使用 STL++ 類的例子 。這個例子使用兩個 STL 類 - string 和 vector。從標準輸入讀進來的行被壓入到 vector 中並且程式被再次啟動所有的vector的元素被列印到標準輸出上。這個例子被包含在為 Microsoft Visual C++ 配置的 makefile 的預設目標中(其使用中附帶的 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++ 的使用上:

 

  1. 包含虛擬函式的類不被支援(POST++ 不能正確的初始化到虛擬函式表的指標)。
  2. 隱式記憶體釋放(垃圾收集器)是不可能的 - POST++ 沒有關於物件內部指標位置的資訊。
  3. 儲存器必須總對映到相同的虛擬地址上因為如果基地址改變了 POST++ 不能調整指標。

如果所有這些限制不是你的應用所必需的,你可以使其持久化而不需要任何程式碼的改動。這個方法在C和C++程式中都可以使用。

 

這裡有幾個 POST++ 類和應用的例子。其中最簡單的就是遊戲“猜動物”。這個遊戲的演算法非常簡單並且結果看起來給人以深刻的印象(有些象人工智慧)。此外這個遊戲是一個非常好的例子,闡明瞭持久物件儲存的好處。這個遊戲的原始碼在檔案 中。建立這個遊戲包含在預設的make目標中。執行guess來執行它。

Unix specific: 當你準備和 POST++ 庫連結你的Unix應用並且持久物件中波阿含虛擬函式,請不要忘記重編譯 檔案幷包含在連結列表中。這個檔案是必須的用於 POST++ 提供可執行檔案的時間戳,被放在儲存器中用來判定什麼時候應用被改變並在需要的時候重新初始化物件內的虛擬函式表。Attention! 這個檔案必須在你每次重新連結你的應用時被重新編譯。我建議你讓編譯器為你呼叫連結程式幷包含 comptime.cxx 原始檔在為執行映像目標檔案提供的物件檔案列表中(see )。

 

這一節的內容對使用了事務的應用是非常有意義的。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 選單中檢查它)。在檔案 中你可以發現使用結構化異常處理的例子。

POST++ 是 freeware。開發出她希望是有用的。透過她你可以做任何你想做的(在開發產品中使用 POST++ 沒有任何限制)。我將很高興來幫助你使用 POST++ 和得到關於 POST++ 任何型別的資訊(錯誤報告,建議...)。POST++ 的免費情形並不意味著缺少支援。我保證將努力修正任何報告的錯誤。也提供 e- 支援。POST++ 有幾種用途:在不同期間儲存資訊,在檔案中儲存物件系統,快照, 資訊系統... 但是如果你感到你需要在你的應用使用更重要的物件導向資料庫以提供併發,分散式以及事務處理,請訪問 。


| .net"> me about bugs and problems


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990103/,如需轉載,請註明出處,否則將追究法律責任。

相關文章