進程間通信方式–共享內存
??共享內存允許兩個或多個進程訪問同一塊內存,就如同malloc() 函數向不同進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。
共享內存
??當一個程序加載進內存后,它就被分成叫作頁的塊。總之,當一個程序想和另外一個程序通信的時候,那內存將會為這兩個程序生成一塊公共的內存區域。這塊被兩個進程分享的內存區域叫做共享內存。
??在各種進程間通信方式中“共享內存”具有最高的效率,因為所有進程共享同一塊內存。訪問共享內存區域和訪問進程獨有的內存區域一樣快,并不需要通過系統調用或者其它需要切入內核的過程來完成。同時它也避免了對數據的各種不必要的復制。
同步問題
??因為系統內核沒有對訪問共享內存進行同步,所以必須自己提供同步措施。解決這些問題的常用方法是通過使用信號量進行同步。為了簡化共享數據的完整性和避免同時存取數據,內核提供了一種專門存取共享內存資源的機制。這稱為互斥體或者mutex對象。
??例如,在數據被寫入之前不允許進程從共享內存中讀取信息、不允許兩個進程同時向同一個共享內存地址寫入數據等。
當一個進程想和另外一個進程通信的時候,它將按以下順序運行:
獲取mutex對象,鎖定共享區域。
將要通信的數據寫入共享區域。
釋放mutex對象。
當一個進程從這個區域讀數據時,它將重復同樣的步驟,只是將第二步變成讀取。
內存模型
要使用一塊共享內存:
進程必須首先分配它;
隨后需要訪問這個共享內存塊的每一個進程都必須將這個共享內存綁定到自己的地址空間中;
當完成通信之后,所有進程都將脫離共享內存,并且由一個進程釋放該共享內存塊。
地址映射
??每個進程都會維護一個從物理內存地址到虛擬內存頁面之間的映射關系----頁表。盡管每個進程都有自己的內存地址,不同的進程可以同時將一個內存頁面映射到自己的虛擬地址空間中,從而達到共享內存的目的。
??分配一個新的共享內存塊會創建新的內存頁面。因為所有進程都希望共享對同一塊內存的訪問,只應由一個進程創建一塊新的共享內存。再次分配一塊已經存在的內存塊不會創建新的頁面,而只是會返回一個標識該內存塊的標識符。
??一個進程如需使用這個共享內存塊,則首先需要將它綁定到自己的地址空間中。這樣會創建一個從進程本身虛擬地址到共享頁面的映射關系。當對共享內存的使用結束之后,這個映射關系將被刪除。當再也沒有進程需要使用這個共享內存塊的時候,必須有一個(且只能是一個)進程負責釋放這個被共享的內存頁面。
??所有共享內存塊的大小都必須是系統頁面大小的整數倍。系統頁面大小指的是系統中單個內存頁面包含的字節數。在 Linux 系統中,內存頁面大小是4KB,不過仍然應該通過調用 getpagesize 獲取這個值。
用于共享內存的函數
(1)ftok()函數:獲得一個ID號
在IPC中,我們經常用key_t的值來創建或者打開信號量,共享內存和消息隊列。
//pathname:路徑名
//一個1-255之間的一個整數值,典型的值是一個ASCII值。
key_t ftok(const char *pathname, int proj_id);
1
2
3
??它可以根據傳入路徑及id自動生成一個key,你可以在后續的shmget()調用中使用這個key用做共享內存的標識,不同進程間使用同一共享內存必須知道這個key的。當然,你也完全可以自己定義一個key來標識共享內存以避免路徑變化時不同進程生成的key發生不一致的坑。
??當成功執行的時候,一個key_t值將會被返回,否則-1被返回。我們可以使用strerror(errno)來確定具體的錯誤信息。
(2)shmget()函數:創建共享內存
//key:一個用來標識共享內存塊的鍵值
//size:指定了所申請的內存塊的大小
//shmflg:操作共享內存的標識
int shmget(key_t key ,int size,int shmflg);
1
2
3
4
key:用來標識共享內存塊的鍵值。
??彼此無關的進程可以通過指定同一個鍵以獲取對同一個共享內存塊的訪問。不幸的是,其它程序也可能挑選了同樣的特定值作為自己分配共享內存的鍵值,從而產生沖突。
??key標識共享內存的鍵值:0/IPC_PRIVATE。當key的取值為IPC_PRIVATE,則函數shmget將創建一塊新的共享內存;如果key的取值為0,而參數中又設置了IPC_PRIVATE這個標志,則同樣會創建一塊新的共享內存。
size:指定了所申請的內存塊的大小。
??因為這些內存塊是以頁面為單位進行分配的,實際分配的內存塊大小將被擴大到頁面大小的整數倍。
shmflg:一組標志,通過特定常量的按位或操作來shmget。
??這些常量包括:
IPC_CREAT:這個標志表示應創建一個新的共享內存塊。通過指定這個標志,我們可以創建一個具有指定鍵值的新共享內存塊。
IPC_EXCL:這個標志只能與 IPC_CREAT 同時使用。當指定這個標志的時候,如果已有一個具有這個鍵值的共享內存塊存在,則shmget會調用失敗。也就是說,這個標志將使線程獲得一個“獨有”的共享內存塊。如果沒有指定這個標志而系統中存在一個具有相同鍵值的共享內存塊,shmget會返回這個已經建立的共享內存塊,而不是重新創建一個。
(3)shmat():映射共享內存
??shmat()是用來允許本進程訪問一塊共享內存,將這個內存區映射到本進程的虛擬地址空間。如果這個函數調用成功,則會返回綁定的共享內存塊對應的地址。失敗時返回-1。通過 fork 函數創建的子進程同時繼承這些共享內存塊。
int shmat(int shmid,char *shmaddr,int flag)
1
shmid:那塊共享內存的ID,是shmget函數返回的共享存儲標識符。
shmaddr:是共享內存的起始地址,如果shmaddr為NULL,內核會自動把共享內存映射到進程的一個合適的地址上;如果shmaddr不為NULL,內核會把共享內存映像到shmaddr指定的位置。所以一般把shmaddr設為NULL。
shmflag:是本進程對該內存的操作模式。
SHM_RND:表示第二個參數指定的地址應被向下靠攏到內存頁面大小的整數倍。如果您不指定這個標志,您將不得不在調用shmat的時候手工將共享內存塊的大小按頁面大小對齊。
SHM_RDONLY:表示這個內存塊將僅允許讀取操作而禁止寫入。
??這里存在一個坑:如果當前進程多次調用shmat(),并不會出現任何錯誤,得到的結果反而是在當前進程的虛擬內存地址空間出現多個共享內存地址映射,最終可能導致應用程序的地址空間資源耗盡,同時也可能使共享內存的引用最終無法得到正常的釋放。
(4)shmdt():共享內存解除映射(Shared Memory Detach,脫離共享內存塊)
//shmaddr:調用shmat返回的地址
int shmdt(char *shmaddr)
1
2
??如果當釋放這個內存塊的進程是最后一個使用該內存塊的進程,則這個內存塊將被刪除。對 exit 或任何exec族函數的調用都會自動使進程脫離共享內存塊。
(5)shmctl():控制釋放(控制對這個共享內存的使用)
int shmctl( int shmid , int cmd , struct shmid_ds *buf );
1
shmid:共享內存塊的標識。
cmd:控制命令。
buf:一個結構體指針。IPC_STAT的時候,取得的狀態放在這個結構體中。如果要改變共享內存的狀態,用這個結構體指定。
??要獲取一個共享內存塊的相關信息,則為該函數傳遞 IPC_STAT 作為第二個參數,同時傳遞一個指向一個 struct shmid_ds 對象的指針作為第三個參數。
??要刪除一個共享內存塊,則應將 IPC_RMID 作為第二個參數,而將 NULL 作為第三個參數。當最后一個綁定該共享內存塊的進程與其脫離時,該共享內存塊將被刪除。
??程序中應當在結束使用每個共享內存塊的時候都使用 shmctl 進行釋放,以防止超過系統所允許的共享內存塊的總數限制。調用 exit 和 exec 會使進程脫離共享內存塊,但不會刪除這個內存塊。