沒有任何軟件是完全沒有錯誤的,在程序的運行期間,應用程序用戶可能會碰到意想不到的結果。要分析并找出導致這些問題的原因,程序員所廣泛使用的一種方法就是日志記錄。在本文中,您將了解如何使用循環緩沖區通過內存操作(而不是文件操作)高效地進行日志記錄。為該緩沖區選擇合適的大小,從而確保轉儲相關的消息,這將在調試時很有幫助。
引言
“如果有兩種方式可以編寫出沒有錯誤的程序,那么只有第三種方式是有效的。” —Alan J. Perlis
在關鍵的計算機應用程序的生存期中,日志記錄是一件非常重要的活動,特別是當故障的癥狀并不十分明顯時。日志記錄提供了故障前應用程序狀態的詳細信息,如變量的值、函數的返回值等等。在一段時間的運行過程中,將不斷地產生大量的跟蹤數據,并持續地將其寫入到磁盤上的文本文件中。要進行有效的日志記錄,需要使用大量的磁盤空間,并且在多線程環境中,所需的磁盤空間會成倍地增加,因為大量的線程都在記錄它們的跟蹤信息。
使用常規文件進行日志記錄的兩個主要問題是:硬盤空間的可用性,以及在對一個文件寫入數據時磁盤 I/O 的速度較慢。持續地對磁盤進行寫入操作可能會極大地降低程序的性能,導致其運行速度緩慢。通常,可以通過使用日志輪換策略來解決空間問題,將日志保存在幾個文件中,當這些文件大小達到某個預定義的字節數時,對它們進行截斷和覆蓋。
要克服空間問題并實現磁盤 I/O 的最小化,某些程序可以將它們的跟蹤數據記錄在內存中,僅當請求時才轉儲這些數據。這個循環的、內存中的緩沖區稱為循環緩沖區。本文討論了循環緩沖區的一些常見實現,并對多線程程序中循環緩沖區的啟用機制提出了一些觀點。
循環緩沖區
循環緩沖區是一種用于應用程序的日志記錄技術,它可以將相關的數據保存在內存中,而不是每次都將其寫入到磁盤上的文件中。在需要的時候(比如當用戶請求將內存數據轉儲到文件中時、程序檢測到一個錯誤時,或者由于非法的操作或者接收到的信號而引起程序崩潰時)可以將內存中的數據轉儲到磁盤。循環緩沖區日志記錄由一個固定大小的內存緩沖區構成,進程使用這個內存緩沖區進行日志記錄。顧名思義,該緩沖區采用循環的方式進行實現。當該緩沖區填滿了數據時,無需為新的數據分配更多的內存,而是從緩沖區開始的位置對其進行寫操作,因此將覆蓋以前的內容。請參見圖 1 中的示例。
圖 1. 對循環緩沖區進行寫操作
圖 1 顯示了將兩個條目寫入到循環緩沖區后該緩沖區的狀態。在寫入了第一個日志條目(用藍色表示)之后,當該進程嘗試寫入第二個條目(用紅色表示)時,該緩沖區中已經沒有足夠的剩余空間。該進程寫入數據,一直到達緩沖區的末尾,然后將剩余的數據復制到緩沖區的開始位置,覆蓋以前的日志條目。
|
循環緩沖區的優點
當您可以簡單地對一個文件進行寫入操作時,為什么要使用循環緩沖區呢?因為您覆蓋了循環緩沖區中以前的內容,所以在完成該操作后,您將丟失以前的數據。與傳統的文件日志記錄機制相比,循環緩沖區提供了下列優勢。- 速度快。與磁盤的 I/O 操作相比,內存的寫操作要快得多。僅當需要的時候才刷新數據。
- 持續的日志記錄可能會填滿系統中的空間,從而導致其他程序也耗盡空間并且執行失敗。在這樣的情況下,您有兩種選擇,要么手動地刪除日志信息,要么實現日志輪換策略。
- 一旦您啟用了日志記錄,無論您是否需要它,該進程都將持續地填充硬盤上的空間。
- 有時,您僅僅需要程序崩潰之前的相關數據,而不是該進程完整的歷史數據。
- 有一些常見的調試函數,如 printf、write 等,可能會在多線程應用程序的情況下更改一個程序的行為,使得它們難以調試。使用這些函數會導致應用程序隱藏某些平時可能表現出來的錯誤。這些函數都是可撤銷點,并且可能導致在線程環境中產生一個該程序并不期望的掛起信號。清單 1 中假定的示例(偽代碼)和下面的清單 2 更清楚地說明了這一點。
清單 1. 沒有啟用調試的代碼
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL); /* I should not be cancelled in the below section */ var=5; #ifdef DEBUG write(fd,"Value of var = 5n",17); #endif var=pow(var,2); /* I can be cancelled now */ pthread_testcancel(); |
清單 2. 啟用了調試的代碼
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
/* I should not be cancelled in the below section */
var=5;
#ifdef DEBUG
write(fd,"Value of var = 5n",17); <======== Cancel delivered here!
#endif
var=pow(var,2);
/* I can be cancelled now */
pthread_testcancel();
|
在多線程程序中使用循環緩沖區
有時,當其他傳統的日志記錄方法失敗時,可以使用循環緩沖區日志記錄。這個部分介紹了在多線程應用程序中使用循環緩沖區啟用日志記錄時需要考慮的一些重要方面。
在訪問一個公共的資源時,同步 始終是多線程程序不可缺少的部分,日志記錄也不例外。因為每個線程都試圖對全局空間進行寫操作,所以必須確保它們同步地寫入內存,否則消息就會遭到破壞。通常,每個線程在寫入緩沖區之前都持有一個鎖,在完成操作時釋放該鎖。您可以下載一個使用鎖對內存進行寫操作的循環緩沖區示例。
這種方法具有以下的缺點:如果您的應用程序中包含幾個線程,并且每個線程都在進行詳細地日志記錄,那么該進程的整體性能將會受到影響,因為這些線程將在獲得和釋放鎖上花費了大部分的時間。
通過使得每個線程將數據寫入到它自己的內存塊,就可以完全避免同步問題。當收到來自用戶的轉儲數據的請求時,每個線程獲得一個鎖,并將其轉儲到中心位置。因為僅在將數據刷新到磁盤時獲得鎖,所以性能并不會受到很大的影響。在這樣的情況下,您可能需要一個附加的程序對日志信息進行排序(按照線程 ID 或者時間戳的順序),以便對其進行分析。您還可以選擇僅在內存中寫入消息代碼,而不是完整的格式化消息,稍后使用一個外部的實用工具解析轉儲數據,將消息代碼轉換為有意義的文本。
另一種避免同步問題的方法是,分配一個很大的全局內存塊,并將其劃分為較小的槽位,其中每個槽位都可由一個線程用來進行日志記錄。每個線程只能夠讀寫它自己的槽位,而不是整個緩沖區。當每個線程第一次嘗試寫入數據時,它會嘗試尋找一個空的內存槽位,并將其標記為忙碌。當線程獲得了一個特定的槽位時,可以將跟蹤槽位使用情況的位圖中相應的位設置為 1,當該線程退出時,重新將這個位設置為 0。同時需要維護當前使用的槽位編號的全局列表,以及正在使用它的線程的線程信息。
要避免出現這樣的情況,即一個線程已經死亡,但是卻沒有將其槽位在位圖中對應的位設置為 0,您需要一個垃圾收集器線程,它遍歷全局列表,并根據線程 ID 以固定的時間間隔進行輪詢。它負責釋放槽位并修改全局列表。請參見下面的清單 3 中給出的示例。
清單 3. 垃圾收集器線程的示例偽代碼
void Check_and_free(List *ptr){
int slotno,ret_val;
LockList();
while(ptr){
if ( ((ret_val = pthread_kill(ptr->thread_id,0)) == ESRCH) ){
/* Thread has died */
slotno=ptr->slotno;
Free_slot(ptr->thread_id);
Mark_bitmap_free(slotno);
}
ptr=ptr->next;
}
UnlockList();
return ;
}
|
通常線程 ID 會得到重用,因此可能會出現這樣的情況,即一個線程已經死亡,卻沒有釋放相應的槽位,并在垃圾收集器釋放該槽位之前,再次使用了這個線程 ID 并為其分配一個新的槽位。對于新的線程來說,檢查全局列表并且重用相同的槽位(如果以前的實例使用了它的話),這是非常重要的。因為垃圾收集器線程和寫入者線程可能同時嘗試修改全局列表,所以同樣也需要使用某種鎖定機制。
當用戶對進程發出轉儲循環緩沖區數據的信號時,處理該信號的線程將不允許其他的線程再更改緩沖區的內容,并將所用槽位中的內容轉儲到文件中。清單 4 和清單 5 顯示了對循環緩沖區寫入數據并且將其內容轉儲到文件的示例。一旦接收到了轉儲數據的信號,將使用 is_dumping 全局變量禁止其他的線程更改該緩沖區的內容。
清單 4. 對循環緩沖區槽位 “I” 進行寫操作的示例偽代碼
void Write_to_buffer(char *msg){
read_atomically(&is_dumping);
if(!is_dumping)
memcpy(slot[i]->ptr,msg,strlen(msg));
return;
}
|
清單 5. 轉儲循環緩沖區數據的示例偽代碼
void Dump_data(int fd){
change_atomically_to_true(&is_dumping);
for i in each_used_slot {
write_slot_data_to_file(fd,slot[i]);
}
change_atomically_to_false(&is_dumping);
return;
}
|
結束語
通過使用內存操作代替文件操作,循環緩沖區使得日志記錄的效率更高。為緩沖區選擇合適的大小以確保轉儲相關的消息,這在進行程序的事后檢查分析時是很有幫助的。對于那些不斷地進行日志記錄的程序來說,循環緩沖區是一種理想的解決方案,并且在調試的過程中,您并不需要該程序完整的歷史信息。本文介紹了在多線程程序中實現循環緩沖區的方法和一些需要注意的內容。


