共享存儲允許兩個或更多進程共享一個給定的存儲區。因為數據不需要在客戶進程和服務器進程之間復制,所以這是最快的一種ipC。使用共享存儲時要掌握的唯一竅門是多個進程之間對一個給定存儲區的同步訪問。若服務器進程正在將數據放入共享存儲區,則在它做完這一操作之前,客戶進程不應當去取這些數據。通常,信號量被用來實現對共享存儲訪問的同步。(記錄鎖也可以用于這種場合。)
內核為每個共享存儲段設置了一個shmid_ds結構。
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 */ ...};
(按照支持共享存儲段的需要,每種實現會在shmid_ds結構中增加其他成員。)
shmatt_t類型定義為不帶符號整型,它至少與unsigned short一樣大。
為獲得一個共享存儲標識符,調用的第一個函數通常是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中標識符和鍵部分,說明了將key變換為標識符的規則,討論了是否創建一個新集合,或是引用一個現存集合。
當創建一個新段時,初始化shmid_ds結構的下列成員:
參數size是該共享存儲段的長度(單位:字節)。實現通常將其向上取為系統頁長的整數倍。但是,若應用指定的size值并非系統頁長的整數倍,那么最后一頁的余下部分是不可使用的。如果正在創建一個新段(一般是在服務器進程中),則必須指定其size。如果正在引用一個現存的段(一個客戶進程),則將size指定為0。當創建一新段時,段內的內容初始化為0。
shmctl函數對共享存儲段執行多種操作。
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);返回值:若成功則返回0,若出錯則返回-1
cmd參數指定下列5中命令中一種,使其在shmid指定的段上執行。
IPC_STAT 取此段的shmid_ds結構,并將它存放在由buf指向的結構中。
IPC_SET 按buf指向結構中的值設置與此段相關結構中的下列三個字段:shm_perm.uid、shm_perm.gid以及shm_perm.mode。此命令只能由下列兩種進程執行:一種是其有效用戶ID等于shm_perm.cuid或shm_perm.uid的進程;另一種是具有超級用戶特權的進程。
IPC_RMID 從系統中刪除該共享存儲段。因為每個共享存儲段有一個連接計數(shmid_ds結構中的shm_nattach字段),所以除非使用該段的最后一個進程終止或與該段脫節,否則不會實際上刪除該存儲段。不管此段是否仍在使用,該段標識符立即被刪除,所以不能再用shmat與該段連接。此命令只能由下列兩種進程執行:一種是其有效用戶ID等于shm_perm.cuid或shm_perm.uid的進程,另一種是具有超級用戶特權的進程。
linux和Solaris提供了下列另外兩種命令,但它們并非Single UNIX Specification的組成部分:
SHM_LOCK 將共享存儲段鎖定在內存中。此命令只能由超級用戶執行。
SHM_UNLOCK 解鎖共享存儲段。此命令只能由超級用戶執行。
一旦創建了一個共享存儲段,進程就可調用shmat將其連接到它的地址空間中。
#include <sys/shm.h>void *shmat(int shmid, const void *addr, int flag);返回值:若成功則返回指向共享存儲的指針,若出錯則返回-1
共享存儲段連接到調用進程的哪個地址上與addr參數以及在flag中是否指定SHM_RND位有關。
除非只計劃在一種硬件上運行應用程序(這在當今是不大可能的),否則不應指定共享段所連接到的地址。所以一般應指定addr為0,以便由內核選擇地址。
如果在flag中指定了SHM_RDONLY位,則以只讀方式連接此段。否則以讀寫方式連接此段。
shmat的返回值是該段所連接的實際地址,如果出錯則返回-1。如果shmat成功執行,那么內核將使該共享存儲段shmid_ds結構中的shm_nattach計數器值加1.
當對共享存儲段的操作已經結束時,則調用shmdt脫接該段。注意,這并不從系統中刪除其標識符以及數據結構。該標識符仍然存在,直至某個進程(一般是服務器進程)調用shmctl(帶命令IPC_RMID)特地刪除它。
#include <sys/shm.h>int shmdt(void *addr);返回值:若成功則返回0,若出錯則返回-1
addr參數是以前調用shmat時的返回值。如果成功,shmdt將使相關shmid_ds結構中的shm_nattach計數器值減1。
實例
內核將以addr=0連接的共享存儲段放在什么位置上與系統密切相關。程序清單15-11打印以寫信息,它們與特定系統將各種不同類型的數據放在什么位置有關。
程序清單15-11 打印各種不同類型的數據所存放的位置
#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);}
本人系統上運行此程序,根據輸出結果可以描繪存儲區大致分布,發現它與http://www.CUOXin.com/nufangrensheng/p/3508169.html中的圖7-3中所示的典型存儲區布局類似。
http://www.CUOXin.com/nufangrensheng/p/3559664.html中曾說明mmap函數可將一個文件的若干部分映射至進程地址空間。這在概念上類似與用shmat XSI IPC函數連接一共享存儲段。兩者之間的主要區別是:用mmap映射的存儲段是與文件相關聯的,而XSI共享存儲段則并無這種關聯。
實例:/dev/zero的存儲映射
共享存儲可由不相關的進程使用。但如果進程是相關的,則某些實現提供了一種不同的技術。
在讀設備/dev/zero時,該設備是0字節的無限資源。它也接收寫向它的任何數據。但又忽略這些數據。我們對此設備作為IPC的興趣在于,當對其進行存儲映射時,它具有一些特殊的性質:
程序清單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設備,然后指定長整型的長度調用mmap。注意,一旦存儲區映射成功,就關閉此設備。然后,進程創建一個子進程。因為在調用mmap時指定了MAP_SHARED,所以一個進程寫到存儲映射區的數據可由另一個進程見到。(如果已指定MAP_PRIVATE,則此示例程序不能工作)
然后,父、子進程交替運行,使用http://www.CUOXin.com/nufangrensheng/p/3510306.html中的同步函數各自對共享存儲映射區中的長整型數加1。存儲映射區由mmap初始化為0。父進程先對它進行增1操作,使其成為1,然后子進程對其進程增1操作,使其成為2,然后父進程使其成為3......注意,當在update函數中對長整型值增1時,因為增加的是其值,而不是指針,所以必須使用括號。
以上述方式使用/dev/zero的優點是:在調用mmap創建映射區之前,無需存在一個實際文件。映射/dev/zero自動創建一個指定長度的映射區。這種技術的缺點是:它只在相關進程間起作用。但在相關進程之間使用線程可能更為簡單、有效。注意,無論使用哪一種技術,都需對共享數據進行同步訪問。
實例:匿名存儲映射
很多實現提供了一種類似于/dev/zero的設施,稱為匿名存儲映射。為了使用這種功能,在調用mmap時指定MAP_ANON標志,并將文件描述符指定為-1。結果得到的區域是匿名的(因為它并不通過一個文件描述符與一個路徑名相結合),并且創建一個可與后代進程共享的存儲區。
注意,Linux為此定義了MAP_ANONYMOUS標志,并將MAP_ANON標志定義為與它相同的值以改善應用的可移植性。
為使程序清單15-12所示的程序應用這種特征,對它做了三處修改:一是刪除了對于/dev/zero的open語句;二是刪除了對于fd的close語句;三是將mmap調用修改成:
if((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0)) == MAP_FAILED)
的形式。在此調用中,指定了MAP_ANON標志,并將文件描述符取為-1。程序的其余部分則沒有改變。
最后兩個例子說明了在多個相關進程之間如何使用共享存儲段。如果在無關進程之間使用共享存儲段,那么有兩種替換的方法。其一是應用程序使用XSI共享存儲函數;另一種是使用mmap將同一文件映射至它們的地址空間,為此使用MAP_SHARED標志。
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答