共享存儲允許兩個或更多進程共享一個給定的存儲區(qū)。因為數(shù)據(jù)不需要在客戶進程和服務(wù)器進程之間復(fù)制,所以這是最快的一種ipC。使用共享存儲時要掌握的唯一竅門是多個進程之間對一個給定存儲區(qū)的同步訪問。若服務(wù)器進程正在將數(shù)據(jù)放入共享存儲區(qū),則在它做完這一操作之前,客戶進程不應(yīng)當(dāng)去取這些數(shù)據(jù)。通常,信號量被用來實現(xiàn)對共享存儲訪問的同步。(記錄鎖也可以用于這種場合。)
內(nèi)核為每個共享存儲段設(shè)置了一個shmid_ds結(jié)構(gòu)。
struct shmid_ds { struct ipc_perm shm_perm; size_t shm_segsz; /* size of segment in bytes */ pid_t shm_lpid; /* pid of last shmop() */ pid_t shm_cpid; /* pid of creator */ shmatt_t shm_nattch; /* number of current attaches */ time_t shm_atime; /* last-attach time */ time_t shm_dtime; /* last-detach tiime */ time_t shm_ctime; /* last-change time */ ...};(按照支持共享存儲段的需要,每種實現(xiàn)會在shmid_ds結(jié)構(gòu)中增加其他成員。)
shmatt_t類型定義為不帶符號整型,它至少與unsigned short一樣大。
為獲得一個共享存儲標(biāo)識符,調(diào)用的第一個函數(shù)通常是shmget。
#include <sys/shm.h>int shmget(key_t key, size_t size, int flag);返回值:若成功則返回共享存儲ID,若出錯則返回-1
http://www.CUOXin.com/nufangrensheng/p/3561681.html中標(biāo)識符和鍵部分,說明了將key變換為標(biāo)識符的規(guī)則,討論了是否創(chuàng)建一個新集合,或是引用一個現(xiàn)存集合。
當(dāng)創(chuàng)建一個新段時,初始化shmid_ds結(jié)構(gòu)的下列成員:
參數(shù)size是該共享存儲段的長度(單位:字節(jié))。實現(xiàn)通常將其向上取為系統(tǒng)頁長的整數(shù)倍。但是,若應(yīng)用指定的size值并非系統(tǒng)頁長的整數(shù)倍,那么最后一頁的余下部分是不可使用的。如果正在創(chuàng)建一個新段(一般是在服務(wù)器進程中),則必須指定其size。如果正在引用一個現(xiàn)存的段(一個客戶進程),則將size指定為0。當(dāng)創(chuàng)建一新段時,段內(nèi)的內(nèi)容初始化為0。
shmctl函數(shù)對共享存儲段執(zhí)行多種操作。
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);返回值:若成功則返回0,若出錯則返回-1
cmd參數(shù)指定下列5中命令中一種,使其在shmid指定的段上執(zhí)行。
IPC_STAT 取此段的shmid_ds結(jié)構(gòu),并將它存放在由buf指向的結(jié)構(gòu)中。
IPC_SET 按buf指向結(jié)構(gòu)中的值設(shè)置與此段相關(guān)結(jié)構(gòu)中的下列三個字段:shm_perm.uid、shm_perm.gid以及shm_perm.mode。此命令只能由下列兩種進程執(zhí)行:一種是其有效用戶ID等于shm_perm.cuid或shm_perm.uid的進程;另一種是具有超級用戶特權(quán)的進程。
IPC_RMID 從系統(tǒng)中刪除該共享存儲段。因為每個共享存儲段有一個連接計數(shù)(shmid_ds結(jié)構(gòu)中的shm_nattach字段),所以除非使用該段的最后一個進程終止或與該段脫節(jié),否則不會實際上刪除該存儲段。不管此段是否仍在使用,該段標(biāo)識符立即被刪除,所以不能再用shmat與該段連接。此命令只能由下列兩種進程執(zhí)行:一種是其有效用戶ID等于shm_perm.cuid或shm_perm.uid的進程,另一種是具有超級用戶特權(quán)的進程。
linux和Solaris提供了下列另外兩種命令,但它們并非Single UNIX Specification的組成部分:
SHM_LOCK 將共享存儲段鎖定在內(nèi)存中。此命令只能由超級用戶執(zhí)行。
SHM_UNLOCK 解鎖共享存儲段。此命令只能由超級用戶執(zhí)行。
一旦創(chuàng)建了一個共享存儲段,進程就可調(diào)用shmat將其連接到它的地址空間中。
#include <sys/shm.h>void *shmat(int shmid, const void *addr, int flag);返回值:若成功則返回指向共享存儲的指針,若出錯則返回-1
共享存儲段連接到調(diào)用進程的哪個地址上與addr參數(shù)以及在flag中是否指定SHM_RND位有關(guān)。
除非只計劃在一種硬件上運行應(yīng)用程序(這在當(dāng)今是不大可能的),否則不應(yīng)指定共享段所連接到的地址。所以一般應(yīng)指定addr為0,以便由內(nèi)核選擇地址。
如果在flag中指定了SHM_RDONLY位,則以只讀方式連接此段。否則以讀寫方式連接此段。
shmat的返回值是該段所連接的實際地址,如果出錯則返回-1。如果shmat成功執(zhí)行,那么內(nèi)核將使該共享存儲段shmid_ds結(jié)構(gòu)中的shm_nattach計數(shù)器值加1.
當(dāng)對共享存儲段的操作已經(jīng)結(jié)束時,則調(diào)用shmdt脫接該段。注意,這并不從系統(tǒng)中刪除其標(biāo)識符以及數(shù)據(jù)結(jié)構(gòu)。該標(biāo)識符仍然存在,直至某個進程(一般是服務(wù)器進程)調(diào)用shmctl(帶命令I(lǐng)PC_RMID)特地刪除它。
#include <sys/shm.h>int shmdt(void *addr);返回值:若成功則返回0,若出錯則返回-1
addr參數(shù)是以前調(diào)用shmat時的返回值。如果成功,shmdt將使相關(guān)shmid_ds結(jié)構(gòu)中的shm_nattach計數(shù)器值減1。
實例
內(nèi)核將以addr=0連接的共享存儲段放在什么位置上與系統(tǒng)密切相關(guān)。程序清單15-11打印以寫信息,它們與特定系統(tǒng)將各種不同類型的數(shù)據(jù)放在什么位置有關(guān)。
程序清單15-11 打印各種不同類型的數(shù)據(jù)所存放的位置
#include "apue.h"#include <sys/shm.h>#define ARRAY_SIZE 40000#define MALLOC_SIZE 100000#define SHM_SIZE 100000#define SHM_MODE 0600 /* user read/write */char array[ARRAY_SIZE]; /* uninitialized data = bss */int main(void){ int shmid; char *ptr, *shmptr; PRintf("array[] from %lx to %lx/n", (unsigned long)&array[0], (unsigned long)&array[ARRAY_SIZE]); printf("stack aound %lx/n", (unsigned long)&shmid); if((ptr = malloc(MALLOC_SIZE)) == NULL) err_sys("malloc error"); printf("malloced from %lx to %lx/n", (unsigned long)ptr, (unsigned long)ptr+MALLOC_SIZE); if((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0) err_sys("shmget error"); if((shmptr = shmat(shmid, 0, 0)) == (void *)-1) err_sys("shmat error"); printf("shared memory attched from %lx to %lx/n", (unsigned long)shmptr, (unsigned long)shmptr+SHM_SIZE); if(shmctl(shmid, IPC_RMID, 0) < 0) err_sys("shmctl error"); exit(0);}本人系統(tǒng)上運行此程序,根據(jù)輸出結(jié)果可以描繪存儲區(qū)大致分布,發(fā)現(xiàn)它與http://www.CUOXin.com/nufangrensheng/p/3508169.html中的圖7-3中所示的典型存儲區(qū)布局類似。
http://www.CUOXin.com/nufangrensheng/p/3559664.html中曾說明mmap函數(shù)可將一個文件的若干部分映射至進程地址空間。這在概念上類似與用shmat XSI IPC函數(shù)連接一共享存儲段。兩者之間的主要區(qū)別是:用mmap映射的存儲段是與文件相關(guān)聯(lián)的,而XSI共享存儲段則并無這種關(guān)聯(lián)。
實例:/dev/zero的存儲映射
共享存儲可由不相關(guān)的進程使用。但如果進程是相關(guān)的,則某些實現(xiàn)提供了一種不同的技術(shù)。
在讀設(shè)備/dev/zero時,該設(shè)備是0字節(jié)的無限資源。它也接收寫向它的任何數(shù)據(jù)。但又忽略這些數(shù)據(jù)。我們對此設(shè)備作為IPC的興趣在于,當(dāng)對其進行存儲映射時,它具有一些特殊的性質(zhì):
程序清單15-12 在父、子進程間使用/dev/zero存儲映射I/O的IPC
#include "apue.h"#include <fcntl.h>#include <sys/mman.h>#define NLOOPS 1000#define SIZE sizeof(long); /* size of shared memory area */static intupdate(long *ptr){ return((*ptr)++); /* return value before increment */}intmain(void){ int fd, i, counter; pid_t pid; void *area; if((fd = open("/dev/zero", O_RDWR)) < 0) err_sys("open error"); if((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) err_sys("mmap error"); close(fd); /* can close /dev/zero now that it's mapped */TELL_WAIT(); if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid > 0) /* parent */ { for(i=0; i<NLOOPS; i+=2) { if((counter = update((long *)area)) != i) err_quit("parent: expected %d, got %d", i, counter); TELL_CHILD(pid); WAIT_CHILD(); } } else { for(i = 1; i < NLOOPS + 1; i += 2) { WAIT_PARENT(); if((counter = update((long *)area)) != i) err_quit("child: expected %d, got %d", i, counter); TELL_PARENT(getppid()); } } exit(0);}它打開此/dev/zero設(shè)備,然后指定長整型的長度調(diào)用mmap。注意,一旦存儲區(qū)映射成功,就關(guān)閉此設(shè)備。然后,進程創(chuàng)建一個子進程。因為在調(diào)用mmap時指定了MAP_SHARED,所以一個進程寫到存儲映射區(qū)的數(shù)據(jù)可由另一個進程見到。(如果已指定MAP_PRIVATE,則此示例程序不能工作)
然后,父、子進程交替運行,使用http://www.CUOXin.com/nufangrensheng/p/3510306.html中的同步函數(shù)各自對共享存儲映射區(qū)中的長整型數(shù)加1。存儲映射區(qū)由mmap初始化為0。父進程先對它進行增1操作,使其成為1,然后子進程對其進程增1操作,使其成為2,然后父進程使其成為3......注意,當(dāng)在update函數(shù)中對長整型值增1時,因為增加的是其值,而不是指針,所以必須使用括號。
以上述方式使用/dev/zero的優(yōu)點是:在調(diào)用mmap創(chuàng)建映射區(qū)之前,無需存在一個實際文件。映射/dev/zero自動創(chuàng)建一個指定長度的映射區(qū)。這種技術(shù)的缺點是:它只在相關(guān)進程間起作用。但在相關(guān)進程之間使用線程可能更為簡單、有效。注意,無論使用哪一種技術(shù),都需對共享數(shù)據(jù)進行同步訪問。
實例:匿名存儲映射
很多實現(xiàn)提供了一種類似于/dev/zero的設(shè)施,稱為匿名存儲映射。為了使用這種功能,在調(diào)用mmap時指定MAP_ANON標(biāo)志,并將文件描述符指定為-1。結(jié)果得到的區(qū)域是匿名的(因為它并不通過一個文件描述符與一個路徑名相結(jié)合),并且創(chuàng)建一個可與后代進程共享的存儲區(qū)。
注意,Linux為此定義了MAP_ANONYMOUS標(biāo)志,并將MAP_ANON標(biāo)志定義為與它相同的值以改善應(yīng)用的可移植性。
為使程序清單15-12所示的程序應(yīng)用這種特征,對它做了三處修改:一是刪除了對于/dev/zero的open語句;二是刪除了對于fd的close語句;三是將mmap調(diào)用修改成:
if((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0)) == MAP_FAILED)
的形式。在此調(diào)用中,指定了MAP_ANON標(biāo)志,并將文件描述符取為-1。程序的其余部分則沒有改變。
最后兩個例子說明了在多個相關(guān)進程之間如何使用共享存儲段。如果在無關(guān)進程之間使用共享存儲段,那么有兩種替換的方法。其一是應(yīng)用程序使用XSI共享存儲函數(shù);另一種是使用mmap將同一文件映射至它們的地址空間,為此使用MAP_SHARED標(biāo)志。
本篇博文內(nèi)容摘自《UNIX環(huán)境高級編程》(第二版),僅作個人學(xué)習(xí)記錄所用。關(guān)于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答