[轉]大內高手—共享記憶體與執行緒區域性儲存
城裡的人想出去,城外的人想進來。這是《圍城》裡的一句話,它可能比《圍城》本身更加有名。我想這句話的前提是,要麼住在城裡,要麼住 在城外,二者只能居其一。否則想住在城裡就可以住在城裡,想住在城外就可以住在城外,你大可以選擇單日住在城裡,雙日住在城外,也就沒有心思去想出去還是 進來了。
理想情況是即可以住在城裡又可以住在城外,而不是走向極端。儘管像青蛙一樣的兩棲動物絕不會比人類更高階,但能適應於更多環境的能力畢 竟有它的優勢。技術也是如此,共享記憶體和執行緒區域性儲存就是例項,它們是為了防止走向記憶體完全隔離和完全共享兩個極端的產物。
當我們發明了MMU時, 大家認為天下太平了,各個程序空間獨立,互不影響,程式的穩定性將大提高。但馬上又認識到,程序完全隔離也不行,因為各個程序之間需要資訊共享。於是就搞 出一種稱為共享記憶體的東西。
當我們發明了執行緒的時,大家認為這下可爽了,執行緒可以併發執行,建立和切換的開銷相對程序來說小多了。執行緒之間的記憶體是共享的,執行緒間 通訊快捷又方便。但馬上又認識到,有些資訊還是不共享為好,應該讓各個執行緒保留一點隱私。於是就搞出一個執行緒區域性儲存的玩意兒。
共享記憶體和執行緒區域性儲存是兩個重要又不常用的東西,平時很少用,但有時候又離不了它們。本文介紹將兩者的概念、原理和使用方法,把它們 放在自己的工具箱裡,以供不時之需。
1. 共享記憶體
大家都知道程序空間是獨立的,它們之間互不影響。比如同是0xabcd1234地址的記憶體,在不同的程序中,它們的資料是不同的,沒有關係的。這樣做的好處很多:每個程序的地址空 間變大了,它們獨佔4G(32位)的地址空間,讓程式設計實現更容易。 各個程序空間獨立,一個程序死掉了,不會影響其它程序,提高了系統的穩定性。
要做到程序空間獨立,光靠軟體是難以實現的,通常要依賴於硬體的幫助。這種硬體通常稱為MMU(Memory Manage Unit),即所謂的記憶體管理單元。在這種體系結構下,記憶體分為實體記憶體和虛擬記憶體兩種。實體記憶體就是實際的 記憶體,你機器上裝了多大記憶體就有多大記憶體。而應用程式中使用的是虛擬記憶體,訪問記憶體資料時,由MMU根 據頁表把虛擬記憶體地址轉換對應的實體記憶體地址。
MMU把 各個程序的虛擬記憶體對映到不同的實體記憶體上,這樣就保證了程序的虛擬記憶體是獨立的。然而,實體記憶體往往遠遠少於各個程序的虛擬記憶體的總和。怎麼辦呢,通常 的辦法是把暫時不用的記憶體寫到磁碟上去,要用的時候再載入回記憶體中來。一般會搞一個專門的分割槽儲存記憶體資料,這就是所謂的交換分割槽。
這些工作由核心配合MMU硬 件完成,記憶體管理是作業系統核心的重要功能。其中為了優化效能,使用了不少高階技術,所以記憶體管理通常比較複雜。比如:在決定把什麼資料換出到磁碟上時, 採用最近最少使用的策略,把常用的記憶體資料放在實體記憶體中,把不常用的寫到磁碟上,這種策略的假設是最近最少使用的記憶體在將來也很少使用。在建立程序時使 用COW(Copy on Write)的技術,大大減少了記憶體資料的複製。為了提高 從虛擬地址到實體地址的轉換速度,硬體通常採用TLB技術,把剛轉換的地址存在cache裡,下次可以直接使用。
從虛擬記憶體到實體記憶體的對映並不是一個位元組一個位元組對映的,而是以一個稱為頁(page)最小單位的為基礎的,頁的大小視硬體平臺而定,通常是4K。當應用程式訪問的記憶體所在頁面不在實體記憶體中時,MMU產生一個缺頁中斷,並掛起當前程序,缺頁中斷負責把相應的資料從磁碟讀入記憶體中,再喚醒掛起的程序。
程序的虛擬記憶體與實體記憶體對映關係如下圖所示(灰色 頁為被不在實體記憶體中的頁):
也許我們很少直接使用共享記憶體,實際上除非效能上有特殊要求,我更願意採用socket或者管道作為程序間通訊的方式。但我們常常間接的使用共享記憶體,大家都知道共享庫(或稱為動態庫)的 優點是,多個應用程式可以公用。如果每個應用程式都載入一份共享庫到記憶體中,顯然太浪費了。所以作業系統把共享庫放在共享記憶體中,讓多個應用程式共享。另 外,同一個應用程式執行多個例項時,也採用同樣的方式,保證記憶體中只有一份可執行程式碼。這樣的共享記憶體是設為只讀屬性的,防止應用程式無意中破壞它們。當 偵錯程式要設定斷點時,相應的頁面被拷貝一分,設定為可寫的,再向其中寫入斷點指令。這些事情完全由作業系統等底層軟體處理了,應用程式本身無需關心。
共享記憶體是怎麼實現的呢?我們來看看下圖(黃色 頁為共享記憶體):
由上圖可見,實現共享記憶體非常容易,只是把兩個程序的虛擬記憶體對映同一塊實體記憶體就行了。不過要注 意,實體記憶體相同而虛擬地址卻不一定相同,如圖中所示程序1的page5和程序2的page2都對映到實體記憶體的page1上。
如何在程式中使用共享記憶體呢?通常很簡單,作業系統或者函式庫提供了一些API給我們使用。如:
Linux:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset); int munmap(void *start, size_t length); |
Win32:
HANDLE CreateFileMapping( HANDLE hFile, // handle to file LPSECURITY_ATTRIBUTES lpAttributes, // security DWORD flProtect, // protection DWORD dwMaximumSizeHigh, // high-order DWORD of size DWORD dwMaximumSizeLow, // low-order DWORD of size LPCTSTR lpName // object name ); BOOL UnmapViewOfFile( LPCVOID lpBaseAddress // starting address ); |
2.
執行緒區域性儲存(TLS)
同一個程序中的多個執行緒,它們的記憶體空間是共享的(棧除外),在一個執行緒修改的記憶體內容,對所有執行緒 都生效。這是一個優點也是一個缺點。說它是優點,執行緒的資料交換變得非常快捷。說它是缺點,一個執行緒死掉了,其它執行緒也性命不保; 多個執行緒訪問共享資料,需要昂貴的同步開銷,也容易造成同步相關的BUG;。
在unix下,大家一直都對執行緒不是 很感興趣,直到很晚以後才引入執行緒這東西。像X Sever要同時處理N個客戶端的連線,每秒鐘要響應上百萬個請求,開發人員寧願自己實現排程機制也不用執行緒。讓人很難想象X Server是單程序單執行緒模型的。再如Apache(1.3x),在unix下的實現也是採用多程序模 型的,把像記分板等公共資訊放入共享記憶體中,也不願意採用多執行緒模型。
正如《unix程式設計藝術》中所說,執行緒局 部儲存的出現,使得這種情況出現了轉機。採用執行緒區域性儲存,每個執行緒有一定的私有空間。這可以避免部分無意的破壞,不過仍然無法避免有意的破壞行為。
個人認為,這完全是因為unix程 序不喜歡面向物件方法引起的,資料沒有很好的封裝起來,全域性變數滿天飛,在多執行緒情況下自然容易出問題。如果採用面向物件的方法,可以讓這種情況大為改 觀,而無需要執行緒區域性儲存來幫忙。
當然,多一種技術就多一種選擇,知道執行緒區域性儲存還是有用的。儘管只用過幾次執行緒區域性儲存的方法,在那種情況下,沒有執行緒區域性儲存, 確實很難用其它辦法實現。
執行緒區域性儲存在不同的平臺有不同的實現,可移植性不太好。幸好要實現執行緒區域性儲存並不難,最簡單的辦 法就是建立一個全域性表,通過當前執行緒ID去查詢相應的資料,因為各個執行緒的ID不 同,查到的資料自然也不同了。
大多數平臺都提供了執行緒區域性儲存的方法,無需要我們自己去實現:
linux:
方法一: int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); int pthread_key_delete(pthread_key_t key); void *pthread_getspecific(pthread_key_t key); int pthread_setspecific(pthread_key_t key, const void *value); 方法二: __thread int i; |
Win32
方法一: DWORD TlsAlloc(VOID); BOOL TlsFree( DWORD dwTlsIndex // TLS index ); BOOL TlsSetValue( DWORD dwTlsIndex, // TLS index LPVOID lpTlsValue // value to store ); LPVOID TlsGetValue( DWORD dwTlsIndex // TLS index ); 方法二: `__declspec( thread ) int tls_i = 1;` |
如非註明轉載, 均為原創. 本站遵循知識共享CC協議,轉載請註明來源