【轉載】共享內存與線程局部存儲


博客分類:
操作系統
多線程LinuxITeyeUnix編程
出自:http://blog.csdn.net/absurd
城裏的人想出去,城外的人想進來。這是《圍城》裏的一句話,它可能比《圍城》本身更加有名。我想這句話的前提是,要麼住在城裏,要麼住在城外,二者只能居其一。否則想住在城裏就可以住在城裏,想住在城外就可以住在城外,你大可以選擇單日住在城裏,雙日住在城外,也就沒有心思去想出去還是進來了。
理想情況是即可以住在城裏又可以住在城外,而不是走向極端。儘管像青蛙一樣的兩棲動物絕不會比人類更高級,但能適應於更多環境的能力畢竟有它的優勢。技術也是如此,共享內存和線程局部存儲就是實例,它們是爲了防止走向內存完全隔離和完全共享兩個極端的產物。
當我們發明了MMU時,大家認爲天下太平了,各個進程空間獨立,互不影響,程序的穩定性將大提高。但馬上又認識到,進程完全隔離也不行,因爲各個進程之間需要信息共享。於是就搞出一種稱爲共享內存的東西。
當我們發明了線程的時,大家認爲這下可爽了,線程可以併發執行,創建和切換的開銷相對進程來說小多了。線程之間的內存是共享的,線程間通信快捷又方便。但馬上又認識到,有些信息還是不共享爲好,應該讓各個線程保留一點隱私。於是就搞出一個線程局部存儲的玩意兒。
共享內存和線程局部存儲是兩個重要又不常用的東西,平時很少用,但有時候又離不了它們。本文介紹將兩者的概念、原理和使用方法,把它們放在自己的工具箱裏,以供不時之需。
1,共享內存
大家都知道進程空間是獨立的,它們之間互不影響。比如同是0xabcd1234地址的內存,在不同的進程中,它們的數據是不同的,沒有關係的。這樣做的好處很多:每個進程的地址空間變大了,它們獨佔4G(32位)的地址空間,讓編程實現更容易。各個進程空間獨立,一個進程死掉了,不會影響其它進程,提高了系統的穩定性。
要做到進程空間獨立,光靠軟件是難以實現的,通常要依賴於硬件的幫助。這種硬件通常稱爲MMU(Memory Manage Unit),即所謂的內存管理單元。在這種體系結構下,內存分爲物理內存和虛擬內存兩種。物理內存就是實際的內存,你機器上裝了多大內存就有多大內存。而應用程序中使用的是虛擬內存,訪問內存數據時,由MMU根據頁表把虛擬內存地址轉換對應的物理內存地址。
MMU把各個進程的虛擬內存映射到不同的物理內存上,這樣就保證了進程的虛擬內存是獨立的。然而,物理內存往往遠遠少於各個進程的虛擬內存的總和。怎麼辦呢,通常的辦法是把暫時不用的內存寫到磁盤上去,要用的時候再加載回內存中來。一般會搞一個專門的分區保存內存數據,這就是所謂的交換分區。
這些工作由內核配合MMU硬件完成,內存管理是操作系統內核的重要功能。其中爲了優化性能,使用了不少高級技術,所以內存管理通常比較複雜。比如:在決定把什麼數據換出到磁盤上時,採用最近最少使用的策略,把常用的內存數據放在物理內存中,把不常用的寫到磁盤上,這種策略的假設是最近最少使用的內存在將來也很少使用。在創建進程時使用COW(Copy on Write)的技術,大大減少了內存數據的複製。爲了提高從虛擬地址到物理地址的轉換速度,硬件通常採用TLB技術,把剛轉換的地址存在cache裏,下次可以直接使用。
從虛擬內存到物理內存的映射並不是一個字節一個字節映射的,而是以一個稱爲頁(page)最小單位的爲基礎的,頁的大小視硬件平臺而定,通常是4K。當應用程序訪問的內存所在頁面不在物理內存中時,MMU產生一個缺頁中斷,並掛起當前進程,缺頁中斷負責把相應的數據從磁盤讀入內存中,再喚醒掛起的進程。
進程的虛擬內存與物理內存映射關係如下圖所示(灰色頁爲被不在物理內存中的頁):
【轉載】共享內存與線程局部存儲 - jackiechung308 - jackiechung308的博客
也許我們很少直接使用共享內存,實際上除非性能上有特殊要求,我更願意採用socket或者管道作爲進程間通信的方式。但我們常常間接的使用共享內存,大家都知道共享庫(或稱爲動態庫)的優點是,多個應用程序可以公用。如果每個應用程序都加載一份共享庫到內存中,顯然太浪費了。所以操作系統把共享庫放在共享內存中,讓多個應用程序共享。另外,同一個應用程序運行多個實例時,也採用同樣的方式,保證內存中只有一份可執行代碼。這樣的共享內存是設爲只讀屬性的,防止應用程序無意中破壞它們。當調試器要設置斷點時,相應的頁面被拷貝一分,設置爲可寫的,再向其中寫入斷點指令。這些事情完全由操作系統等底層軟件處理了,應用程序本身無需關心。
【轉載】共享內存與線程局部存儲 - jackiechung308 - jackiechung308的博客
由上圖可見,實現共享內存非常容易,只是把兩個進程的虛擬內存映射同一塊物理內存就行了。不過要注意,物理內存相同而虛擬地址卻不一定相同,如圖中所示進程1的page5和進程2的page2都映射到物理內存的page1上。
如何在程序中使用共享內存呢?通常很簡單,操作系統或者函數庫提供了一些API給我們使用。如:
Linux:
C代碼 

void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
int munmap(void *start, size_t length);

void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);    int munmap(void *start, size_t length);  

Win32:
C代碼
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);
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:
方法一:
C代碼
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);
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);  

方法二:
C代碼
__thread int i;
__thread int i;  

Win32
方法一:
C代碼
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
);
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    );  

方法二:
C代碼
__declspec( thread ) int tls_i = 1;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章