共享内存
共享内存是第二种IPC工具。他允许两个无关的进程访问相同的逻辑内存。共享内存是在两个运行的程序之间传递数据的有效手段。尽管X/Open标准并没有要求,很可能绝大数的共享内存实现都是会将不同进程之间正在共享的内存安排在相同的物理内存中。
共享内存为在多个进程之间共享与传递数据提供一个有效的手段。因为他并没有提供同步的方法,所以通常我们需要使用其他的机制来同步对共享内存的访问。通常,我们也许会使用共享内存来提供对大块内存区的有效访问,并且传递少量的消息来同步对此内存的访问。
共享内存是由IPC为一个进程所创建并且出现在这个进程的地址空间中的一段特殊的地址序列。其他的进程可以将同样的共享内存段关联到他们自己的地址空间中。所有的进程都可以访问这段内存地址,就如同这段内存是由malloc所分配的。如果一个进程写入共享内存,这些改变立即就可以为访问相同共享内存的其他进程所见。
就其自身而言,共享内存并没有提供任何共享方法。并没有自动的方法来阻止在第一个进程完成写入共享内存之前第二个进程开始读取共享内存。同步访问是程序员的责任。图14-2显示共享内存是如何工作的。
每一个箭头显示的是每一个进程的逻辑地址空间到可用的物理内存的映射。实际的情形要更为复杂,因为可用的内存是由物理内存与交换到磁盘上的内存混合构成的。
用于共享内存的函数如下:
#include void *shmat(int shm_id, const void *shm_addr, int shmflg); int shmctl(int shm_id, int cmd, struct shmid_ds *buf); int shmdt(const void *shm_addr); int shmget(key_t key, size_t size, int shmflg);
与信号量相类似,通常需要在包含shm.h文件之前包含sys/types.h与sys/ipc.h这两个头文件。
shmget
我们使用shmget函数创建共享内存:
int shmget(key_t key, size_t size, int shmflg);
与信号量相类似,这个函数也提供了key,这可以有效的命名共享内存段,而且shmget函数会返回一个共享内存标识符,这个标识符可以用于后续的共享内存函数中。还有一个特殊的关键值,IPC_PRIVATE,这可以创建进程私有的共享内存。我们通常并不会使用这个值,而且与信号量相类似,我们会发现私有的共享内存在许多Linux系统上实际上并不是私有的。
第二个参数,size,以字节形式指定了所需要的内存数量。
第三个参数,shmflg,是由9个权限标记所组成的,这些标记的使用与用于创建文件的模型参数相同。IPC_CREAT定义了一个特殊位,必须与权限标记进行位或操作来创建一个新的共享内存段。设置IPC_CREAT标记并且传递一个已经存在的共享内存段并不是错误。如果不需要,IPC_CREAT只是简单的被忽略掉。
权限标记是十分有用的,因为这些权限标记可以允许创建共享内存所有者进程可以写入而其他用户所创建的进程只能读取的共享内存。我们可以应用这个特点通过将数据放入共享内存中,从而提供对于只读数据的有效访问,而不必担心数据被其他用户修改的风险。
如果共享内存成功创建,shmget会返回一个非负整数,共享内存标识符。如果失败,则会返回-1。
shmat
当我们第一次创建一个共享内存段时,他并不能为任何进程所访问。为了能够访问共享内存,我们必须将其与一个进程地址空间关联到一起。我们可以使用shmat函数来达到这一目的:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一个参数,shm_id,是由shmget函数所返回的共享内存标识符。
第二个参数,shm_addr,是将要关联到当前进程的共享内存所在的位置。这个参数应总是一个空指针,从而可以允许系统来选择内存出现的地址。
第三个参数,shmflg,是一个位标记集合。两个可能的值为SHM_RND与SHM_RDONLY。前者与shm_addr联合,控制将被关联的共享内存所在的地址;而后者使得关联的内存只读。通常很少需要来控制被关联的内存所在的地址;我们通常应允许系统来为我们选择一个地址,否则就会使得程序变得高度硬件相关。
如果shmat调用成功,他会返回一个指向共享内存第一字节的指针。如果失败,则会返回-1。
共享内存将会依据所有者(共享内存的创建者),权限与当前进程的所有者而具有读或写权限。共享内存上的权限与文件上的权限相类似。
shmfgl & SHM_RDONLY为真的情况是这个规则的一个例外。此时共享内存并不可写,尽管权限已经允许了写访问。
shmdt
shmdt函数将共享内存与当前进程相分离。他传递一个指向由shmat所返回的地址的指针。如果成功,则会返回0;如果失败,则会返回-1。注意,分离共享内存并不会删除他;他只是使得内存对于当前进程不可用。
shmctl
共享内存的控制函数要比复杂的信号量控制函数简单得多:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
shmid_ds结构至少具有下列成员:
struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; }
第一个参数,shm_id,是由shmget所返回的标记符。
第二个参数,command,是要执行的动作。他可以有三个值:
命令 描述 IPC_STAT 设置shmid_ds结构中的数据反射与共享内存相关联的值。 IPC_SET 如果进程有相应的权限,将与共享内存相关联的值设置为shmid_ds数据结构中所提供的值。 IPC_RMID 删除共享内存段。
第三个参数,buf,是一个指向包含共享内存模式与权限的结构的指针。
如果成功,则返回0,如果失败,则会返回-1。X/Open并没有说明如果我们尝试删除一个已经关联的共享内存时会发生什么。通常,一个已经关联但是被删除的共享内存通常会继续发挥作用,直到他与最后一个进程相分离。然而,因为这个行为并没有被规范,所以最好不要依赖于他。
试验--共享内存
现在我们已经了解了共享内存函数,我们可以编写一些代码来使用这些函数。我们将会编写一对程序,shm1.c与shm2.c。第一个程序(消费者)将会创建一个共享内存段并且显示写入共享内存中的数据。第二个程序(生产者)将会关联已经存在的共享内存段并且允许我们进入内存段中的数据。
1 首先,我们创建一个通用头文件来描述我们希望传递的共享内存。我们将其命名为shm_com.h。
#ifndef _SHM_COM_H #define _SHM_COM_H 1
#define TEXT_SZ 2048
struct shared_use_at { int written_by_you; char some_text[TEXT_SZ]; };
#endif
这个文件定义了在消费者程序与生产者程序中都会用到的结构。当数据已经写入结构的其他部分并且认为我们需要传送2k文本时,我们使用一个int标记written_by_you来通知消费者。
2 我们的第一个程序用于消费者。在包含头文件之后,我们通过调用shmget函数,指定IPC_CREAT位来创建一个共享内存段(我们共享内存结构的大小):
#include #include #include #include
#include #include #include
#include "shm_com.h"
int main() { int running = 1; void *shared_memory = (void *)0; struct shared_use_st *shared_stuff; int shmid;
srand((unsigned int) getpid());
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); }
3 我们现在使用共享内存可以为程序所访问:
shared_memory = shmat(shmid, (void *)0, 0); if(shared_memory == (void *)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); }
printf("Memory attached at %X\n", (int)shared_memroy);
4 程序的接下来部分将shared_memroy段赋给shared_stuff,后者会输出written_by_you中的任何文本。程序继续循环直到 written_by_you中的文本为end。sleep调用会强制消费者停留在其临界区中,这会使得生产者程序等待。
shared_stuff = (struct_shared_use_st *)shared_memory; shared_stuff->written_by_you = 0; while(running) { if(shared_stuff->written_by_you) { printf("You wrote: %s", shared_stuff->some_text); sleep(rand() % 4); shared_stuff->written_by_you = 0; if(strncmp(shared_stuff->some_text, "end", 3)==0) { running = 0; } } }
5 最后共享内存被分离并被删除:
if(shmdt(shared_memory)==-1) { fprintf(stderr, "shmdt failed\n"); exit(EXIT_FAILURE); }
if(shmctl(shmid, IPC_RMID, 0)==-1) { fprintf(stderr, "shmctl(IPC_RMID) failed\n"); exit(EXIT_FAILURE); }
exit(EXIT_SUCCESS); }
6 我们的第二个程序,shm2.c,是生产者程序;他允许我们进入消费者的数据。这个程序与shm1.c程序十分相似:
#include #include #include #include
#include #include #include
#include "shm_com.h"
int main() { int runnint = 1; void *shared_memory = (void *)0; struct shared_use_st *shared_stuff; char buffer[BUFSIZ]; int shmid;
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if(shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); }
shared_memory = shmat(shmid, (void *)0, 0); if(shared_memory == (void *)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); }
printf("Memory attached at %X\n", (int)shared_memory);
shared_stuff = (struct shared_use_st *)shared_memory;
while(running) { while(shared_stuff->written_by_you == 1) { sleep(1); printf("waiting for client...\n"); }
printf("Enter some text: "); fgets(buffer, BUFSIZ, stdin);
strncpy(shared_stuff->some_text, buffer, TEXT_SZ); shared_stuff->written_by_you = 1;
if(strncmp(buffer, "end", 3) == 0) { running = 0; } }
if(shmdt(shared_memory) == -1) { fprintf(stderr, "shmdt failed\n"); exit(EXIT_FAILURE); }
exit(EXIT_SUCCESS); }
当我们运行这些程序,我们会得到下面的输出:
$ ./shm1 & [1] 294 Memory attached at 40017000 $ ./shm2 Memory attached at 40017000 Enter some text: hello You wrote: hello waiting for client... waiting for client... Enter some text: Linux! You wrote: Linux! waiting for client... waiting for client... waiting for client... Enter some text: end You wrote: end $
工作原理
第一个程序,shm1,创建共享内存段并其关联到他的地址空间。我们在共享内存的第一部分揭示了shared_use_st结构。这个结构有一个标记,written_by_you,当数据可用时会设置这个标记。当设置了这个标记时,程序会读取文本,输出文本,并且清除标记来表示程序已经读取数据了。我们使用一个特殊的字符串,end,来进行由循环中的退出。程序然后分离共享内存并且删除他。
第二个程序,shm2,获得并关联共享内存段,因为他使用相同的键值,1234。然后他提示用户输入一些文本。如果设置了written_by_you标记,shm2就会知道客户端程序还没有读取前面输入的数据并且进行等待。当其他进程清除了这个标记,shm2会写入新的数据并且设置标记。他也使用字符串 end来结束并分离共享内存段。
注意,我们必须提供我们自己的,相当粗糙的同步标记,written_by_you,这会导致一个低效的忙等待。在实际的程序中,我们会传递一个消息,或者使用管道,或者使用IPC消息(我们会在稍后讨论),生成信息,或是使用信号量来在程序的读取与写入部分提供同步。 |