9翩概、共享內(nèi)存
共享內(nèi)存允許兩個(gè)或者更多的進(jìn)程共享一塊指定區(qū)域的內(nèi)存坯认。這個(gè)是最快的IPC高职,因?yàn)閿?shù)據(jù)不需要在客戶和服務(wù)進(jìn)程之間進(jìn)行拷貝了糙置。共享內(nèi)存唯一一個(gè)需要注意的地方就是同步多個(gè)進(jìn)程訪問(wèn)共享內(nèi)存胁后。如果服務(wù)進(jìn)程正在將數(shù)據(jù)放到共享內(nèi)存區(qū)域蝙砌,那么客戶進(jìn)程不應(yīng)當(dāng)在服務(wù)進(jìn)程做完之前訪問(wèn)這個(gè)共享內(nèi)存中的數(shù)據(jù)怎棱。一般來(lái)說(shuō)旁赊,使用信號(hào)量來(lái)同步共享內(nèi)存的訪問(wèn)(但是前面我們也看到了愉阎,記錄鎖也行)绞蹦。
Single UNIX Specification在共享內(nèi)存對(duì)象的實(shí)時(shí)擴(kuò)展選項(xiàng)中包含一系列的訪問(wèn)共享內(nèi)存的接口集合,但是這里我們不討論實(shí)時(shí)方面的擴(kuò)展榜旦。
內(nèi)核對(duì)于每個(gè)共享內(nèi)存段都維護(hù)一個(gè)至少包含如下成員的數(shù)據(jù)結(jié)構(gòu):
struct shmid_ds {
struct ipc_perm shm_perm; /* see Section 15.6.2 */
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 time */
time_t shm_ctime; /* last-change time */
.
};
(其他的實(shí)現(xiàn)可以添加其他的額外成員)
類型shmatt_t被定義成一個(gè)無(wú)符號(hào)的整數(shù)幽七,其大小至少和unsigned short那么大。文中給出了影響共享內(nèi)存的系統(tǒng)限制溅呢。這里就不列舉了锉走。
shmget
一般首先調(diào)用的函數(shù)是shmget,用來(lái)獲得共享內(nèi)存標(biāo)識(shí)藕届。
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
返回值:如果成功挪蹭,返回共享內(nèi)存的ID,如果錯(cuò)誤返回1休偶。
前面我們討論了將一個(gè)關(guān)鍵字轉(zhuǎn)換成標(biāo)識(shí)的方法梁厉,以及創(chuàng)建一個(gè)新的共享內(nèi)存段和對(duì)已經(jīng)存在的共享內(nèi)存段的引用。當(dāng)創(chuàng)建一個(gè)新的共享內(nèi)存段的時(shí)候踏兜,shmid_ds結(jié)構(gòu)變量的如下成員將會(huì)被初始化词顾。
- ipc_perm結(jié)構(gòu)如同前面描述的那樣被初始化(即所有的成員都會(huì)被初始化)。結(jié)構(gòu)的mode成員將會(huì)被設(shè)置成相應(yīng)的flag權(quán)限位碱妆。
- shm_lpid, shm_nattach, shm_atime, 和 shm_dtime 都被設(shè)置成0.
- shm_ctime 被設(shè)置成當(dāng)前時(shí)間肉盹。
- 被設(shè)置成要求的大小。
size參數(shù)表示共享內(nèi)存段的字節(jié)大小數(shù)目疹尾。具體的實(shí)現(xiàn)經(jīng)常會(huì)將大小向上圓整成系統(tǒng)頁(yè)大小的整數(shù)倍上忍,但是如果應(yīng)用程序如果指定的大小值不是系統(tǒng)頁(yè)的整數(shù)倍的話骤肛,那么最后一頁(yè)剩余的那個(gè)部分是不可用的。如果 創(chuàng)建一個(gè)新的共享內(nèi)存段的時(shí)候(一般都由服務(wù)進(jìn)程創(chuàng)建)窍蓝,我們必須指定它的size參數(shù) 腋颠。如果我們 引用一個(gè)已經(jīng)存在的共享內(nèi)存段(一般都由客戶進(jìn)程引用),我們可以指定size參數(shù)為0 吓笙。如果創(chuàng)建一個(gè)新的共享內(nèi)存段淑玫,那么這個(gè) 新創(chuàng)建的共享內(nèi)存段中的內(nèi)容被初始化成0 。
shmctl
shmctl函數(shù)用來(lái)處理各種類型的共享內(nèi)存操作面睛。
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:如果成功絮蒿,返回OK,如果錯(cuò)誤返回1叁鉴。
cmd參數(shù)指定了如下的五個(gè)命令歌径,這五個(gè)命令用來(lái)操作shmid所指定的共享內(nèi)存段。
- IPC_STAT 獲取共享內(nèi)存段的shmid_ds結(jié)構(gòu)亲茅,并將它存放在buf所指向的數(shù)據(jù)結(jié)構(gòu)指針中。
- IPC_SET 根據(jù)buf指向的數(shù)據(jù)結(jié)構(gòu)狗准,設(shè)置共享內(nèi)存段相應(yīng)的shmid_ds結(jié)構(gòu)相關(guān)的如下三個(gè)成員:shm_perm.uid, shm_perm.gid, 和 shm_perm.mode克锣。這個(gè)命令可以被執(zhí)行的前提是:進(jìn)程的有效用戶ID等于shm_perm.cuid或者shm_perm.uid,或者進(jìn)程具有超級(jí)用戶權(quán)限腔长。
- IPC_RMID 從系統(tǒng)中刪除共享內(nèi)存段袭祟。因?yàn)橛幸粋€(gè)維護(hù)共享內(nèi)存段的附加計(jì)數(shù)值(shmid_ds結(jié)構(gòu)變量中的shm_nattch成員), 共享內(nèi)存段不會(huì)被刪除捞附,這個(gè)狀態(tài)一直維持到最后一個(gè)使用這個(gè)共享內(nèi)存段的進(jìn)程終止巾乳,或者斷開(kāi)和它的連接 。無(wú)論這段內(nèi)存是否仍然在被使用鸟召,這個(gè) 共享內(nèi)存段的標(biāo)識(shí)會(huì)被立即刪除 胆绊,這樣就無(wú)法通過(guò)shmat函數(shù)將這段內(nèi)存再次附加了。這個(gè)命令可以被執(zhí)行的前提是:進(jìn)程的有效用戶ID等于shm_perm.cuid或者shm_perm.uid欧募,或者進(jìn)程具有超級(jí)用戶權(quán)限压状。
Linux和Solaris還提供了兩個(gè)命令,它們不屬于Single UNIX Specification的一個(gè)部分跟继。
- SHM_LOCK 鎖住內(nèi)存中的共享內(nèi)存段种冬。這個(gè)命令只能被超級(jí)用戶執(zhí)行。
- SHM_UNLOCK 解鎖共享內(nèi)存段舔糖。這個(gè)命令只能被超級(jí)用戶執(zhí)行娱两。
shmat
當(dāng)創(chuàng)建了一個(gè)共享內(nèi)存段的時(shí)候,進(jìn)程可以通過(guò)調(diào)用shmat來(lái)將這個(gè)共享內(nèi)存段附加到自己的地址空間上面金吗。
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
返回值:如果成功返回指向共享內(nèi)存段的指針十兢,如果錯(cuò)誤返回1趣竣。
共享內(nèi)存段所附加的在調(diào)用進(jìn)程中的地址取決于參數(shù)addr以及在flag中是否指定了SHM_RND。
- 如果addr是0纪挎,那么共享內(nèi)存段會(huì)被附加到內(nèi)核所選擇的第一個(gè)可用的地址期贫。建議這樣做。
- 如果addr是非空的并且沒(méi)有指定SHM_RND异袄,那么共享內(nèi)存段會(huì)被附加到addr所指定的地址上面通砍。
- 如果addr非空,并且SHM_RND被指定了烤蜕,那么共享內(nèi)存段會(huì)被附加到地址(addr - addr mod SHMLBA)上面封孙。SHM_RND命令表示取整。SHMLBA表示低地址界限倍數(shù)讽营,一般它為2的冪虎忌。這個(gè)運(yùn)算會(huì)導(dǎo)致地址被取整到下一個(gè)SHMLBA倍數(shù)了。
除非我們只在一種硬件類型上面運(yùn)行應(yīng)用程序(目前這個(gè)情況是不可能的)橱鹏,我們不應(yīng)當(dāng)指定共享內(nèi)存段被附加的地址膜蠢。相反我們應(yīng)當(dāng)指定addr為0以讓系統(tǒng)自己選擇地址。
如果SHM_RDONLY位被在flag中指定了莉兰,那么共享內(nèi)存段將會(huì)以只讀的方式被附加挑围。否則共享內(nèi)存段會(huì)以讀寫的方式被附加。
shmat返回的就是被附加的共享內(nèi)存段的地址糖荒,或者如果錯(cuò)誤返回1杉辙。如果shmat成功,那么內(nèi)核會(huì)增加和共享內(nèi)存段相關(guān)聯(lián)的shmid_ds中的shm_nattch計(jì)數(shù)捶朵。
shmdt
當(dāng)我們使用完共享內(nèi)存段之后蜘矢,我們調(diào)用shmdt將其斷開(kāi)。注意:這不會(huì)將共享內(nèi)存標(biāo)識(shí)以及它相關(guān)的數(shù)據(jù)結(jié)構(gòu)從系統(tǒng)中移除综看。標(biāo)識(shí)會(huì)一直存在品腹,直到有進(jìn)程(通常為服務(wù)進(jìn)程)特意地通過(guò)調(diào)用IPC_RMID命令的shmctl將它移除。
#include <sys/shm.h>
int shmdt(void *addr);
Returns: 0 if OK, 1 on error
返回值:如果成功红碑,返回0珍昨,如果錯(cuò)誤返回1。
addr參數(shù)就是之前調(diào)用shmat返回的值句喷。如果成功镣典,那么shmdt將會(huì)減少相關(guān)的shmid_ds結(jié)構(gòu)變量的shm_nattch計(jì)數(shù)成員變量。
舉例:不同的內(nèi)核通過(guò)傳入shmat參數(shù)0來(lái)附加地址段返回的地址依賴于系統(tǒng)唾琼。
下面的代碼展示了一個(gè)例子:
#include <sys/shm.h>
#define ARRAY_SIZE 40000
#define MALLOC_SIZE 100000
#define SHM_SIZE 100000
#define SHM_MODE 0600 /* 用戶 讀/寫 */
char array[ARRAY_SIZE]; /* 非初始化的bss數(shù)據(jù)段 */
int main(void)
{
int shmid;
char *ptr, *shmptr;
/*打印非初始化bss數(shù)據(jù)段(全局變量數(shù)組)地址*/
printf("array[] from %lx to %lx\n", (unsigned long)&array[0], (unsigned long)&array[ARRAY_SIZE]);
/*打印堆棧局部變量地址*/
printf("stack around %lx\n", (unsigned long)&shmid);
if ((ptr = malloc(MALLOC_SIZE)) == NULL)
err_sys("malloc error");
/*打印堆內(nèi)存地址*/
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");
/*打印共享內(nèi)存地址*/
printf("shared memory attached 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);
}
在基于intel的linux系統(tǒng)上面運(yùn)行這個(gè)程序兄春,輸入輸出如下:
$ ./a.out
array[] from 804a080 to 8053cc0
stack around bffff9e4
malloced from 8053cc8 to 806c368
shared memory attached from 40162000 to 4017a6a0
下圖展示了這個(gè)內(nèi)存布局,和我們前面說(shuō)過(guò)的類似锡溯。并且注意赶舆,共享內(nèi)存段放在堆棧下面哑姚。
+----------------------+ \
high address| | \Command Line arguments
| | /And environment variables.
+----------------------+ /
| Stack |<-----0xbffff9e4
| |
| |
+----------------------+
| Shared Memory |<----0x4017a6a0 \ Shared memory of
| |<----0x40162000 / 100,000 bytes.
+----------------------+
| |
| |<----0x0806c368 \ Malloc of
| Heap |<----0x08053cc8 / 100,000 bytes.
+----------------------+
| uninitialized data |<----0x08053cc0 \ array[] of
| (bss) |<----0x0804a080 / 40,000 bytes.
+----------------------+
| |
| initialized data |
+----------------------+
| |
| text |
low address +----------------------+
使用mmap實(shí)現(xiàn)的共享內(nèi)存
使用mmap將文件內(nèi)容映射到地址空間上面和使用XSI IPC函數(shù)附加共享內(nèi)存段的地址的原理類似。不同的主要是芜茵,mmap映射的內(nèi)容由文件來(lái)存放叙量,而共享內(nèi)存映射的內(nèi)容沒(méi)有相應(yīng)的文件。
舉例:關(guān)于/dev/zero的內(nèi)存映射
共享內(nèi)存可以用于不相關(guān)的內(nèi)存之間九串,如果內(nèi)存之間是相關(guān)的绞佩,那么我們可使用其它的技術(shù)。
下面講述的方法可以用于FreeBSD 5.2.1, Linux 2.4.22, 和 Solaris 9猪钮,而Mac OS X 10.3目前不支持將字符設(shè)備映射到進(jìn)程地址空間品山。
設(shè)備/dev/zero在被讀取的時(shí)候會(huì)提供無(wú)限個(gè)0。這個(gè)設(shè)備也接收任何寫入到它的數(shù)據(jù)烤低,但是它會(huì)忽略數(shù)據(jù)肘交。我們?cè)贗PC中利用了它在內(nèi)存映射的時(shí)候的一個(gè)特性。
- 一個(gè)匿名內(nèi)存區(qū)域?qū)?huì)被創(chuàng)建扑馁,它的大小為mmap的第2個(gè)參數(shù)涯呻,并且向上取整為最近的系統(tǒng)頁(yè)大小。
- 內(nèi)存區(qū)域會(huì)被初始化為0腻要。
- 如果一個(gè)祖先進(jìn)程指定了MAP_SHARED標(biāo)記的mmap那么多個(gè)進(jìn)程可以共享這個(gè)內(nèi)存區(qū)域复罐。
下面的程序展示了使用這個(gè)設(shè)備的方法。
#include <fcntl.h>
#include <sys/mman.h>
#define NLOOPS 1000
#define SIZE sizeof(long) /* 共享內(nèi)存區(qū)域大小 */
static int update(long *ptr)
{
return((*ptr)++); /* 返回增加之前的值 */
}
int main(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); /* 映射之后關(guān)閉/dev/zero */
TELL_WAIT();
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* 父進(jìn)程 */
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 { /* 子進(jìn)程 */
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);
}
程序打開(kāi)一個(gè)/dev/zero設(shè)備闯第,然后調(diào)用mmap,指定了一個(gè)長(zhǎng)整數(shù)類型的大小缀拭。需要注意的是咳短,當(dāng)區(qū)域被映射的時(shí)候,我們可以關(guān)閉設(shè)備蛛淋。進(jìn)程然后創(chuàng)建一個(gè)子進(jìn)程咙好。因?yàn)闃?biāo)記MAP_SHARED已經(jīng)被指定,所以向內(nèi)存映射的區(qū)域?qū)懙膬?nèi)容會(huì)被其他的進(jìn)程看到褐荷。如果我們指定MAP_PRIVATE的話勾效,這個(gè)例子就不會(huì)工作了。
父子進(jìn)程然后交替運(yùn)行叛甫,使用前面的同步函數(shù)增加一個(gè)共享內(nèi)存區(qū)域的長(zhǎng)整數(shù)层宫。內(nèi)存映射區(qū)域被mmap初始化為0。父進(jìn)程把它增加到1其监,然后子進(jìn)程把它增加到2萌腿,然后父進(jìn)程把它增加到3,等等抖苦。
使用/dev/zero的方式毁菱,其優(yōu)點(diǎn)在于米死,在我們使用mmap創(chuàng)建映射的內(nèi)存區(qū)域的時(shí)候,實(shí)際的文件不需要存在贮庞。將/dev/zero映射會(huì)自動(dòng)創(chuàng)建一個(gè)指定大小的映射內(nèi)存區(qū)域峦筒。使用這個(gè)技術(shù)的缺點(diǎn)就是,它 只能工作在相關(guān)的進(jìn)程之間 窗慎∥锱纾可能使用線程會(huì)更加簡(jiǎn)單和高效。無(wú)論 使用什么方式捉邢,我們都必須使用同步的機(jī)制來(lái)控制訪問(wèn)的數(shù)據(jù) 脯丝。
匿名內(nèi)存映射的例子
許多實(shí)現(xiàn)提供匿名映射的功能,類似/dev/zero具有的特性伏伐。為了使用這個(gè)功能宠进,我們指定mmap的MAP_ANON標(biāo)記,并且指定文件描述符號(hào)為-1藐翎。返回的區(qū)域是匿名的(因?yàn)樗鼪](méi)有通過(guò)文件描述符號(hào)和任何一個(gè)路徑關(guān)聯(lián))材蹬,并且會(huì)創(chuàng)建一個(gè)可以 被子進(jìn)程共享的內(nèi)存映射區(qū)域 。
匿名內(nèi)存映射在本書的四個(gè)平臺(tái)上面都有支持吝镣。需要注意的是堤器,Linux為這個(gè)功能定義了MAP_ANONYMOUS標(biāo)記,但是為了便于程序的可移植特性末贾,定義MAP_ANON標(biāo)記為同樣的值闸溃。
我們可以做如下三個(gè)修改將前面的例子轉(zhuǎn)化成使用這個(gè)特性:
刪除將/dev/zero打開(kāi)的語(yǔ)句。
刪除將fd關(guān)閉的語(yǔ)句拱撵。
-
修改mmap的調(diào)用如下:
if ((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0)) == MAP_FAILED)
在這個(gè)調(diào)用中辉川,我們指定MAP_ANON標(biāo)記,并且設(shè)置文件描述符號(hào)為-1拴测。剩下的部分沒(méi)有變化乓旗。
總結(jié)
上面的最后兩例子,展示了在 相關(guān)的進(jìn)程之間進(jìn)行內(nèi)存的共享 集索。
如果 共享內(nèi)存在無(wú)關(guān)的進(jìn)程之間進(jìn)行 屿愚,那么有兩個(gè)可選的方法。
- 應(yīng)用程序可以使用XSI的共享內(nèi)存函數(shù)务荆,
- 或者他們可以使用帶有MAP_SHARED標(biāo)記的mmap函數(shù)妆距,將 同一個(gè)文件 映射到它們的地址空間。
譯者注
- 使用shmget函匕,獲得共享內(nèi)存標(biāo)識(shí)毅厚,共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)使用shmid_ds進(jìn)行表示。
- 使用shmctl浦箱,設(shè)置共享內(nèi)存的各個(gè)參數(shù)成員吸耿,以及初始化祠锣,獲取,刪除(只對(duì)標(biāo)識(shí)進(jìn)行刪除咽安,但是內(nèi)存在沒(méi)有使用的進(jìn)程之后才刪除)等伴网。
- 使用shmat,將共享內(nèi)存標(biāo)識(shí)進(jìn)行掛接(使用前)妆棒,返回相應(yīng)的內(nèi)存地址(會(huì)使得shmid_ds中相應(yīng)的計(jì)數(shù)增加)澡腾。
- 使用shmdt,將相應(yīng)地址的共享內(nèi)存進(jìn)行反掛接(不使用了)糕珊。
- 共享內(nèi)存屬于最快的IPC技術(shù)动分,因?yàn)樗愃苖map,不需要在進(jìn)程之間進(jìn)行數(shù)據(jù)的拷貝红选,與mmap不同的是共享內(nèi)存引用的不是文件澜公。
另外,mmap可以使用標(biāo)記MAP_ANON與fd=-1來(lái)實(shí)現(xiàn)匿名映射喇肋,這樣不需要打開(kāi)和關(guān)閉特定的文件路徑了坟乾,適用在父子進(jìn)程之間進(jìn)行通信。如果在無(wú)關(guān)進(jìn)程之間通信蝶防,則可以使用共享內(nèi)存或者將同一文件使用mmap映射到各自進(jìn)程的空間中甚侣。
-
int semget(key_t key, int nsems, int flag)
中,創(chuàng)建的是一個(gè)信號(hào)量集间学,而非單一信號(hào)量殷费。權(quán)限是集合的權(quán)限;集合中每個(gè)信號(hào)量都有特定的值低葫。 -
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */)
中详羡, semnum表示是集合中的第semnum
個(gè)信號(hào)量。 -
int semop(int semid, struct sembuf semoparray[], size_t nops)
中氮采,設(shè)置SEM_UNDO標(biāo)記并非改變信號(hào)操作殷绍,而是額外記錄一個(gè)調(diào)整值以備異常染苛。
記錄鎖性能不如信號(hào)量但是使用簡(jiǎn)單鹊漠。信號(hào)量每次創(chuàng)建一個(gè)信號(hào)量集合,而非單個(gè)信號(hào)量茶行,在某些場(chǎng)景比如:需要的資源不止一種的時(shí)候躯概,可能維護(hù)起來(lái)會(huì)相對(duì)方便,因?yàn)榧卸x資源到集合中了畔师,而非單獨(dú)各自定義娶靡。