共享內存共享內存是進(jìn)程間通信中最簡(jiǎn)單的方式之一。共享內存允許兩個(gè)或更多進(jìn)程訪(fǎng)問(wèn)同一塊內存,就如同 malloc() 函數向不同進(jìn)程返回了指向同一個(gè)物理內存區域的指針。當一個(gè)進(jìn)程改變了這塊地址中的內容的時(shí)候,其它進(jìn)程都會(huì )察覺(jué)到這個(gè)更改。
快速本地通信 因為所有進(jìn)程共享同一塊內存,共享內存在各種進(jìn)程間通信方式中具有最高的效率。訪(fǎng)問(wèn)共享內存區域和訪(fǎng)問(wèn)進(jìn)程獨有的內存區域一樣快,并不需要通過(guò)系統調用或者其它需要切入內核的過(guò)程來(lái)完成。同時(shí)它也避免了對數據的各種不必要的復制。
因為系統內核沒(méi)有對訪(fǎng)問(wèn)共享內存進(jìn)行同步,您必須提供自己的同步措施。例如,在數據被寫(xiě)入之前不允許進(jìn)程從共享內存中讀取信息、不允許兩個(gè)進(jìn)程同時(shí)向同一個(gè)共享內存地址寫(xiě)入數據等。解決這些問(wèn)題的常用方法是通過(guò)使用信號量進(jìn)行同步。不過(guò),我們的程序中只有一個(gè)進(jìn)程訪(fǎng)問(wèn)了共享內存,因此在集中展示了共享內存機制的同時(shí),我們避免了讓代碼被同步邏輯搞得混亂不堪。
內存模型 要使用一塊共享內存,進(jìn)程必須首先分配它。隨后需要訪(fǎng)問(wèn)這個(gè)共享內存塊的每一個(gè)進(jìn)程都必須將這個(gè)共享內存綁定到自己的地址空間中。當完成通信之后,所有進(jìn)程都將脫離共享內存,并且由一個(gè)進(jìn)程釋放該共享內存塊。
理解 Linux 系統內存模型可以有助于解釋這個(gè)綁定的過(guò)程。在 Linux 系統中,每個(gè)進(jìn)程的虛擬內存是被分為許多頁(yè)面的。這些內存頁(yè)面中包含了實(shí)際的數據。每個(gè)進(jìn)程都會(huì )維護一個(gè)從內存地址到虛擬內存頁(yè)面之間的映射關(guān)系。盡管每個(gè)進(jìn)程都有自己的內存地址,不同的進(jìn)程可以同時(shí)將同一個(gè)內存頁(yè)面映射到自己的地址空間中,從而達到共享內存的目的。
分配一個(gè)新的共享內存塊會(huì )創(chuàng )建新的內存頁(yè)面。因為所有進(jìn)程都希望共享對同一塊內存的訪(fǎng)問(wèn),只應由一個(gè)進(jìn)程創(chuàng )建一塊新的共享內存。再次分配一塊已經(jīng)存在的內存塊不會(huì )創(chuàng )建新的頁(yè)面,而只是會(huì )返回一個(gè)標識該內存塊的標識符。一個(gè)進(jìn)程如需使用這個(gè)共享內存塊,則首先需要將它綁定到自己的地址空間中。這樣會(huì )創(chuàng )建一個(gè)從進(jìn)程本身虛擬地址到共享頁(yè)面的映射關(guān)系。當對共享內存的使用結束之后,這個(gè)映射關(guān)系將被刪除。當再也沒(méi)有進(jìn)程需要使用這個(gè)共享內存塊的時(shí)候,必須有一個(gè)(且只能是一個(gè))進(jìn)程負責釋放這個(gè)被共享的內存頁(yè)面。
所有共享內存塊的大小都必須是系統頁(yè)面大小的整數倍。系統頁(yè)面大小指的是系統中單個(gè)內存頁(yè)面包含的字節數。在 Linux 系統中,內存頁(yè)面大小是4KB,不過(guò)您仍然應該通過(guò)調用 getpagesize 獲取這個(gè)值。
分配 進(jìn)程通過(guò)調用shmget(Shared Memory GET,獲取共享內存)來(lái)分配一個(gè)共享內存塊。該函數的第一個(gè)參數是一個(gè)用來(lái)標識共享內存塊的鍵值。彼此無(wú)關(guān)的進(jìn)程可以通過(guò)指定同一個(gè)鍵以獲取對同一個(gè)共享內存塊的訪(fǎng)問(wèn)。不幸的是,其它程序也可能挑選了同樣的特定值作為自己分配共享內存的鍵值,從而產(chǎn)生沖突。用特殊常量IPC_PRIVATE作為鍵值可以保證系統建立一個(gè)全新的共享內存塊。 該函數的第二個(gè)參數指定了所申請的內存塊的大小。因為這些內存塊是以頁(yè)面為單位進(jìn)行分配的,實(shí)際分配的內存塊大小將被擴大到頁(yè)面大小的整數倍。
第三個(gè)參數是一組標志,通過(guò)特定常量的按位或操作來(lái)shmget。這些特定常量包括:
IPC_CREAT:這個(gè)標志表示應創(chuàng )建一個(gè)新的共享內存塊。通過(guò)指定這個(gè)標志,我們可以創(chuàng )建一個(gè)具有指定鍵值的新共享內存塊。 IPC_EXCL:這個(gè)標志只能與 IPC_CREAT 同時(shí)使用。當指定這個(gè)標志的時(shí)候,如果已有一個(gè)具有這個(gè)鍵值的共享內存塊存在,則shmget會(huì )調用失敗。也就是說(shuō),這個(gè)標志將使線(xiàn)程獲得一個(gè)“獨有”的共享內存塊。如果沒(méi)有指定這個(gè)標志而系統中存在一個(gè)具有相通鍵值的共享內存塊,shmget會(huì )返回這個(gè)已經(jīng)建立的共享內存塊,而不是重新創(chuàng )建一個(gè)。 模式標志:這個(gè)值由9個(gè)位組成,分別表示屬主、屬組和其它用戶(hù)對該內存塊的訪(fǎng)問(wèn)權限。其中表示執行權限的位將被忽略。指明訪(fǎng)問(wèn)權限的一個(gè)簡(jiǎn)單辦法是利用<sys/stat.h>中指定,并且在手冊頁(yè)第二節stat條目中說(shuō)明了的常量指定。例如,S_IRUSR和S_IWUSR分別指定了該內存塊屬主的讀寫(xiě)權限,而 S_IROTH和S_IWOTH則指定了其它用戶(hù)的讀寫(xiě)權限。 下面例子中shmget函數創(chuàng )建了一個(gè)新的共享內存塊(當shm_key已被占用時(shí)則獲取對一個(gè)已經(jīng)存在共享內存塊的訪(fǎng)問(wèn)),且只有屬主對該內存塊具有讀寫(xiě)權限,其它用戶(hù)不可讀寫(xiě)。
int segment_id = shmget (shm_key, getpagesize (), IPC_CREAT | S_IRUSR| S_IWUSR ); 如果調用成功,shmget將返回一個(gè)共享內存標識符。如果該共享內存塊已經(jīng)存在,系統會(huì )檢查訪(fǎng)問(wèn)權限,同時(shí)會(huì )檢查該內存塊是否被標記為等待摧毀狀態(tài)。
綁定和脫離 要讓一個(gè)進(jìn)程獲取對一塊共享內存的訪(fǎng)問(wèn),這個(gè)進(jìn)程必須先調用 shmat(SHared Memory Attach,綁定到共享內存)。將 shmget 返回的共享內存標識符 SHMID 傳遞給這個(gè)函數作為第一個(gè)參數。該函數的第二個(gè)參數是一個(gè)指針,指向您希望用于映射該共享內存塊的進(jìn)程內存地址;如果您指定NULL則Linux會(huì )自動(dòng)選擇一個(gè)合適的地址用于映射。第三個(gè)參數是一個(gè)標志位,包含了以下選項:
SHM_RND表示第二個(gè)參數指定的地址應被向下靠攏到內存頁(yè)面大小的整數倍。如果您不指定這個(gè)標志,您將不得不在調用shmat的時(shí)候手工將共享內存塊的大小按頁(yè)面大小對齊。 SHM_RDONLY表示這個(gè)內存塊將僅允許讀取操作而禁止寫(xiě)入。 如果這個(gè)函數調用成功則會(huì )返回綁定的共享內存塊對應的地址。通過(guò) fork 函數創(chuàng )建的子進(jìn)程同時(shí)繼承這些共享內存塊;如果需要,它們可以主動(dòng)脫離這些共享內存塊。 當一個(gè)進(jìn)程不再使用一個(gè)共享內存塊的時(shí)候應通過(guò)調用 shmdt(Shared Memory Detach,脫離共享內存塊)函數與該共享內存塊脫離。將由 shmat 函數返回的地址傳遞給這個(gè)函數。如果當釋放這個(gè)內存塊的進(jìn)程是最后一個(gè)使用該內存塊的進(jìn)程,則這個(gè)內存塊將被刪除。對 exit 或任何exec族函數的調用都會(huì )自動(dòng)使進(jìn)程脫離共享內存塊。
控制和釋放共享內存塊 調用 shmctl("Shared Memory Control",控制共享內存)函數會(huì )返回一個(gè)共享內存塊的相關(guān)信息。同時(shí) shmctl 允許程序修改這些信息。該函數的第一個(gè)參數是一個(gè)共享內存塊標識。
要獲取一個(gè)共享內存塊的相關(guān)信息,則為該函數傳遞 IPC_STAT 作為第二個(gè)參數,同時(shí)傳遞一個(gè)指向一個(gè) struct shmid_ds 對象的指針作為第三個(gè)參數。
要刪除一個(gè)共享內存塊,則應將 IPC_RMID 作為第二個(gè)參數,而將 NULL 作為第三個(gè)參數。當最后一個(gè)綁定該共享內存塊的進(jìn)程與其脫離時(shí),該共享內存塊將被刪除。
您應當在結束使用每個(gè)共享內存塊的時(shí)候都使用 shmctl 進(jìn)行釋放,以防止超過(guò)系統所允許的共享內存塊的總數限制。調用 exit 和 exec 會(huì )使進(jìn)程脫離共享內存塊,但不會(huì )刪除這個(gè)內存塊。 要查看其它有關(guān)共享內存塊的操作的描述,請參考shmctl函數的手冊頁(yè)。
示例程序 代碼 5.1 中的程序展示了共享內存塊的使用。
代碼 5.1 (shm.c) 嘗試共享內存
#include <stdio.h>
#include <sys/shm.h>
#include <sys/stat.h>
int main()
{
int segment_id;
char* shared_memory;
struct shmid_ds shmbuffer;
int segment_size;
const int shared_segment_size = 0x6400; /* 分配一個(gè)共享內存塊 */
segment_id = shmget(IPC_PRIVATE, shared_segment_size, IPC_CREAT|IPC_EXCL|S_IRUSR|S_IWUSR ); /* 綁定到共享內存塊 */
shared_memory = (char*)shmat(segment_id, 0, 0);
printf("shared memory attached at address %p\n", shared_memory); /* 確定共享內存的大小 */
shmctl(segment_id, IPC_STAT, &shmbuffer);
segment_size = shmbuffer.shm_segsz;
printf("segment size: %d\n", segment_size); /* 在共享內存中寫(xiě)入一個(gè)字符串 */
sprintf(shared_memory, "Hello, world."); /* 脫離該共享內存塊 */
shmdt(shared_memory); /* 重新綁定該內存塊 */
shared_memory = (char*)shmat(segment_id, (void*) 0x500000, 0);
printf("shared memory reattached at address %p\n", shared_memory); /* 輸出共享內存中的字符串 */
printf("%s\n", shared_memory); /* 脫離該共享內存塊 */
shmdt(shared_memory); /* 釋放這個(gè)共享內存塊 */
shmctl(segment_id, IPC_RMID, 0);
return 0;
}
調試 使用ipcs 命令可用于查看系統中包括共享內存在內的進(jìn)程間通信機制的信息。指定-m參數以獲取有關(guān)共享內存的信息。例如,以下的示例表示有一個(gè)編號為1627649的共享內存塊正在使用中:
% ipcs -m ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 1627649 user 640 25600 0 如果這個(gè)共享內存塊在程序結束后沒(méi)有被刪除而是被錯誤地保留下來(lái),您可以用ipcrm命令刪除它。
% ipcrm shm 1627649
優(yōu)點(diǎn)和缺點(diǎn) 共享內存塊提供了在任意數量的進(jìn)程之間進(jìn)行高效雙向通信的機制。每個(gè)使用者都可以讀取寫(xiě)入數據,但是所有程序之間必須達成并遵守一定的協(xié)議,以防止諸如在讀取信息之前覆寫(xiě)內存空間等競爭狀態(tài)的出現。不幸的是,Linux無(wú)法嚴格保證提供對共享內存塊的獨占訪(fǎng)問(wèn),甚至是在您通過(guò)使用IPC_PRIVATE創(chuàng )建新的共享內存塊的時(shí)候也不能保證訪(fǎng)問(wèn)的獨占性。 同時(shí),多個(gè)使用共享內存塊的進(jìn)程之間必須協(xié)調使用同一個(gè)鍵值。