共享內存是進程間通信中最簡單的方式之一。共享內存允許兩個或更多進程訪問同一塊內存,就如同malloc()函數向不同進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。
本地通信
因為所有進程共享同一塊內存,共享內存在各種進程間通信方式中具有最高的效率。訪問共享內存區域和訪問進程獨有的內存區域一樣快,并不需要通過系統調用或者其它需要切入內核的過程來完成。同時它也避免了對數據的各種不必要的復制。
因為系統內核沒有對訪問共享內存進行同步,您必須提供自己的同步措施。例如,在數據被寫入之前不允許進程從共享內存中讀取信息、不允許兩個進程同時向同一個共享內存地址寫入數據等。解決這些問題的常用方法是通過使用信號量進行同步。不過,我們的程序中只有一個進程訪問了共享內存,因此在集中展示了共享內存機制的同時,我們避免了讓代碼被同步邏輯搞得混亂不堪。
內存模型
要使用一塊共享內存,進程必須首先分配它。隨后需要訪問這個共享內存塊的每一個進程都必須將這個共享內存綁定到自己的地址空間中。當完成通信之后,所有進程都將脫離共享內存,并且由一個進程釋放該共享內存塊。
理解 linux 系統內存模型可以有助于解釋這個綁定的過程。在 Linux 系統中,每個進程的虛擬內存是被分為許多頁面的。這些內存頁面中包含了實際的數據。每個進程都會維護一個從內存地址到虛擬內存頁面之間的映射關系。盡管每個進程都有自己的內存地址,不同的進程可以同時將同一個內存頁面映射到自己的地址空間中,從而達到共享內存的目的。
分配一個新的共享內存塊會創建新的內存頁面。因為所有進程都希望共享對同一塊內存的訪問,只應由一個進程創建一塊新的共享內存。再次分配一塊已經存在的內存塊不會創建新的頁面,而只是會返回一個標識該內存塊的標識符。一個進程如需使用這個共享內存塊,則首先需要將它綁定到自己的地址空間中。這樣會創建一個從進程本身虛擬地址到共享頁面的映射關系。當對共享內存的使用結束之后,這個映射關系將被刪除。當再也沒有進程需要使用這個共享內存塊的時候,必須有一個(且只能是一個)進程負責釋放這個被共享的內存頁面。
所有共享內存塊的大小都必須是系統頁面大小的整數倍。系統頁面大小指的是系統中單個內存頁面包含的字節數。在 Linux 系統中,內存頁面大小是4KB,不過您仍然應該通過調用 getpagesize 獲取這個值。
分配
進程通過調用shmget(Shared Memory GET,獲取共享內存)來分配一個共享內存塊。
該函數的第一個參數是一個用來標識共享內存塊的鍵值。彼此無關的進程可以通過指定同一個鍵以獲取對同一個共享內存塊的訪問。不幸的是,其它程序也可能挑選了同樣的特定值作為自己分配共享內存的鍵值,從而產生沖突。用特殊常量IPC_PRIVATE作為鍵值可以保證系統建立一個全新的共享內存塊。
該函數的第二個參數指定了所申請的內存塊的大小。因為這些內存塊是以頁面為單位進行分配的,實際分配的內存塊大小將被擴大到頁面大小的整數倍。
第三個參數是一組標志,通過特定常量的按位或操作來shmget。這些特定常量包括:
IPC_CREAT:這個標志表示應創建一個新的共享內存塊。通過指定這個標志,我們可以創建一個具有指定鍵值的新共享內存塊。
IPC_EXCL:這個標志只能與 IPC_CREAT 同時使用。當指定這個標志的時候,如果已有一個具有這個鍵值的共享內存塊存在,則shmget會調用失敗。也就是說,這個標志將使線程獲得一個“獨有”的共享內存塊。如果沒有指定這個標志而系統中存在一個具有相同鍵值的共享內存塊,shmget會返回這個已經建立的共享內存塊,而不是重新創建一個。
模式標志:這個值由9個位組成,分別表示屬主、屬組和其它用戶對該內存塊的訪問權限。其中表示執行權限的位將被忽略。指明訪問權限的一個簡單辦法是利用<sys/stat.h>中指定,并且在手冊頁第二節stat條目中說明了的常量指定。例如,S_IRUSR和S_IWUSR分別指定了該內存塊屬主的讀寫權限,而 S_IROTH和S_IWOTH則指定了其它用戶的讀寫權限。 下面例子中shmget函數創建了一個新的共享內存塊(當shm_key已被占用時則獲取對一個已經存在共享內存塊的訪問),且只有屬主對該內存塊具有讀寫權限,其它用戶不可讀寫。
int segment_id = shmget (shm_key, getpagesize (), IPC_CREAT | S_IRUSR| S_IWUSR ); 如果調用成功,shmget將返回一個共享內存標識符。如果該共享內存塊已經存在,系統會檢查訪問權限,同時會檢查該內存塊是否被標記為等待摧毀狀態。
綁定脫離
要讓一個進程獲取對一塊共享內存的訪問,這個進程必須先調用 shmat(SHared Memory Attach,綁定到共享內存)。將 shmget 返回的共享內存標識符 SHMID 傳遞給這個函數作為第一個參數。該函數的第二個參數是一個指針,指向您希望用于映射該共享內存塊的進程內存地址;如果您指定NULL則Linux會自動選擇一個合適的地址用于映射。第三個參數是一個標志位,包含了以下選項:
SHM_RND表示第二個參數指定的地址應被向下靠攏到內存頁面大小的整數倍。如果您不指定這個標志,您將不得不在調用shmat的時候手工將共享內存塊的大小按頁面大小對齊。 SHM_RDONLY表示這個內存塊將僅允許讀取操作而禁止寫入。 如果這個函數調用成功則會返回綁定的共享內存塊對應的地址。通過 fork 函數創建的子進程同時繼承這些共享內存塊;如果需要,它們可以主動脫離這些共享內存塊。 當一個進程不再使用一個共享內存塊的時候應通過調用 shmdt(Shared Memory Detach,脫離共享內存塊)函數與該共享內存塊脫離。將由 shmat 函數返回的地址傳遞給這個函數。如果當釋放這個內存塊的進程是最后一個使用該內存塊的進程,則這個內存塊將被刪除。對 exit 或任何exec族函數的調用都會自動使進程脫離共享內存塊。
控制釋放
調用 shmctl("Shared Memory Control",控制共享內存)函數會返回一個共享內存塊的相關信息。同時 shmctl 允許程序修改這些信息。該函數的第一個參數是一個共享內存塊標識。
要獲取一個共享內存塊的相關信息,則為該函數傳遞 IPC_STAT 作為第二個參數,同時傳遞一個指向一個 struct shmid_ds 對象的指針作為第三個參數。
要刪除一個共享內存塊,則應將 IPC_RMID 作為第二個參數,而將 NULL 作為第三個參數。當最后一個綁定該共享內存塊的進程與其脫離時,該共享內存塊將被刪除。
您應當在結束使用每個共享內存塊的時候都使用 shmctl 進行釋放,以防止超過系統所允許的共享內存塊的總數限制。調用 exit 和 exec 會使進程脫離共享內存塊,但不會刪除這個內存塊。 要查看其它有關共享內存塊的操作的描述,請參考shmctl函數的手冊頁。
優點缺點
共享內存塊提供了在任意數量的進程之間進行高效雙向通信的機制。每個使用者都可以讀取寫入數據,但是所有程序之間必須達成并遵守一定的協議,以防止諸如在讀取信息之前覆寫內存空間等競爭狀態的出現。不幸的是,Linux無法嚴格保證提供對共享內存塊的獨占訪問,甚至是在您通過使用IPC_PRIVATE創建新的共享內存塊的時候也不能保證訪問的獨占性。 同時,多個使用共享內存塊的進程之間必須協調使用同一個鍵值。