Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1800386
  • 博文数量: 438
  • 博客积分: 9799
  • 博客等级: 中将
  • 技术积分: 6092
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-25 17:25
文章分类

全部博文(438)

文章存档

2019年(1)

2013年(8)

2012年(429)

分类: 系统运维

2012-04-02 17:14:23

共享内存允许两个或多个进程共享内存的一块给定的区域。这是最块形式的IPC形式,因为数据不必在客户和服务器之间拷贝。使用共享内存的唯一的麻烦 是同步多个进程对给定区域的访问。如果服务器正把数据放置到一个共享内存区域里,那么客户不应用尝试访问这个数据,直到服务器完成。信号量经常被用来同步 共享内存的访问。(但是正如我们在前一节末看到的,记录也可以被用。)


SUS包含另一组可替代的接口来访问共享内存,在实时扩展的共享内存对象选项里。我们在本文不讨论实现扩展。


内核维护一个结构体,为每个共享内存段包含至少以下成员:


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 */
  ...
};


(每个实现加入其它所需的结构体成员来支持共享内存段。)


类型shmatt_t被定义为一个无符号整型,至少和一个无符号短整型一样大。下表列出影响共享内存的系统限量(15.6.3节)。


影响共享内存的系统限量
描述典型值
FreeBSDLinuxMacSolaris
一个共享内存段的最大字节尺寸335544323355443241943048388608
一个共享内存段的最小字节尺寸1111
系统范围的共享内存段的最大数量192409632100
每个进程的共享内存段的最大数量128409686


第一个调用的函数通常是shmget,来得到一个共享内存标识符。



  1. #include <sys/shm.h>

  2. int shmget(key_t key, size_t size, int flag);

  3. 成功返回共享内存ID,错误返回-1。


在15.6.1节,我们描述了转换key到标识符以及是一个新段被创建还是一个已有段被引用的规则。当一个新的段被创建时,shmid_ds结构体的以下的成员被初始化。


1、ipc_perm结构体被初始化为15.6.2节描述的那样。这个结构体的mode成员被设为flag的对应权限位。这些权限通过15.6.2节里的表里的值来指定。


2、shm_lpid、shm_nattach、shm_atime、和shm_dtime都被设为0。


3、shm_ctime被设为当前时间。


4、shm_segsz被设为请求的size。


size 参数是共享内存段的字节尺寸。实现经常会将这个尺寸往上取到系统页尺寸的倍数,但是如果一个应用指定size为一个不是系统页尺寸的整数倍的值,那么最后 页的剩余数据将不会被使用。如果一个新段被创建(典型地在一个服务器里),那么我们必须指定它的size。如果我们正引用一个已有的段(一个客户),那么 我们可以指定size为0。当一个新的段被创建时,段的内容被初始化为0。


shmctl函数是各种共享内存操作的杂烩。



  1. #include <sys/shm.h>

  2. int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  3. 成功返回0,错误返回-1。


cmd参数指定以下5个要在shmid所指定的段上执行的命令的某一个。


IPC_STAT:得到这个段的shmid_ds结构体,存储到buf所指的结构体里。


IPC_SET: 从buf指向的结构体设置和这个共享内存段相关的shmid_ds结构体里的以下在三个域:shm_perm.uid、shm_perm.gid、和 shm_perm.mode。这个命令只能被其有效用户ID等于shm_perm.cuid或shm_perm.uid,或有超级用户权限的进程执行。


IPC_RMID: 从系统删除共享内存段。因为附加计数为共享内存段而维护(shmid_ds结构体里的shm_nattch域),所以段不被删除,直到最后使用段的进程终 止或分离了它。不管这个段是否在使用,段的标识符被立即删除以便shmat不再能附加到这个段。这个命令只能被其有效用户ID等于 shm_perm.cuid或shm_perm.uid,或有超级用户权限的进程执行。


SHM_LOCK:锁住内存里的共享内存段。这个命令只能被超级用户执行。


SHM_UNLOCK:解锁共享内存段。这个命令只能被超级用户执行。


一旦一个共享内存段被创建,一个进程把它附加到它的地址空间,通过调用shmat。



  1. #include <sys/shm.h>

  2. void *shmat(int shmid, const void *addr, int flag);

  3. 成功返回共享内存段的指针,错误返回-1。


这个在调用进程里的被这个段附加的地址取决于addr参数和SHM_RND位是否在flag里被指定。


1、如果addr为0,那么段被附加到由内核选择的第一个可用的地址。这是推荐的技术。


2、如果addr不为0而SHM_RND不被指定,那么段被附加到addr给出的地址上。


3、 如果addr不为0而SHM_RND被指定,那么段被附加到(addr-(addr % SHMLBA))给定的地址。SHM_RND命令表示“round”,SHMLBA表示“low boundary address multiple” 并总是2的幂。这个算法做的事是把地址往下舍到下一个SHMLBA的倍数。


除非我们准备只在单个类型的硬件上运行这个程序(当今是很不可能的事情),不然我们不该指定段被附加的地址。事实上,我们应该指定一个0的addr并让系统选择这个地址。


如果SHM_RDONLY位在标志里被指定,那么段被只读地附加。否则,段被附加为可读写。


shmat 返回的段被附加的地址,或-1如果有错误发生。如果shmat成功,那么内核将增加共享内存段关联的shmid_ds结构体里的shm_nattch计 数。当我们使用完一个共享内存段时,我们调用shmdt来分离它。注意这不会从系统删除标识符和它关联的数据结构。标识符保持存在,起不到一个进程(通常 是服务器)明确地删除它,通过调用命令为IPC_RMID的shmctl。



  1. #include <sys/shm.h>

  2. int shmdt (void *addr);

  3. 成功返回0,错误返回-1。


addr参数是前一个shmat调用返回的值。如果成功,shmdt将减少在shmid_ds结构体里的shm_nattch计数。


附加到地址为0的共享内存段被一个内核放置的位置是高度系统相关的。下面的代码打开一些关于一个特定系统放置各种类型数据的位置的信息。



  1. #include <sys/shm.h>
  2. #include <unistd.h>

  3. #define ARRAY_SIZE 40000
  4. #define MALLOC_SIZE 100000
  5. #define SHM_SIZE 100000
  6. #define SHM_MODE 0600 /* user read/write */

  7. char array[ARRAY_SIZE]; /* uninitialized data = bss */

  8. int
  9. main(void)
  10. {
  11.     int shmid;
  12.     char *ptr, *shmptr;

  13.     printf("array[] from %lx to %lx\n", (unsigned long)&array[0],
  14.         (unsigned long)&array[ARRAY_SIZE]);
  15.     printf("stack around %lx\n", (unsigned long)&shmid);

  16.     if ((ptr = malloc(MALLOC_SIZE)) == NULL) {
  17.         printf("malloc error\n");
  18.         exit(1);
  19.     }
  20.     printf("malloced from %lx to %lx\n", (unsigned long)ptr,
  21.         (unsigned long)ptr+MALLOC_SIZE);

  22.     if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0) {
  23.         printf("shmget error\n");
  24.         exit(1);
  25.     }
  26.     if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1) {
  27.         printf("shmat error\n");
  28.         exit(1);
  29.     }
  30.     printf("shared memory attached from %lx to %lx\n",
  31.         (unsigned long)shmptr, (unsigned long)shmptr+SHM_SIZE);

  32.     if (shmctl(shmid, IPC_RMID, 0) < 0) {
  33.         printf("shmctl error\n");
  34.         exit(1);
  35.     }

  36.     exit(0);
  37. }

基于Intel的Linux系统上的运行结果为:

array[] from 804a060 to 8053ca0
stack around bffc8394
malloced from 9173008 to 918b6a8
shared memory attached from b76fd000 to b77156a0

可以发现bss位置最低,往上是堆,再往上是共享内存,最上面是栈。也就是说,共享内存在堆和栈之间。


回想下mmap函数(14.9节)可以用来把一个文件的部分映射到一个进程的地址空间。这和使用shmatXSI IPC函数附加一个共享内存段在概念上是相似的。主要区别是mmap映射的内存段是一个文件的前端,而一个XSI共享内存端不和任何文件相关联。


共享内存可以在不相关的进程间使用,但是如果进程是相关的,那么一些实现提供一个不同的技术。


下面的技术工作在FreeBSD、Linux和Solaris上。Mac当前不支持字符设备到一个进程的地址空间的映射。


设备/dev/zero在被读0无尽地产生字节0。这个设备也接受任何向它写入的数据,并忽略这个数据。我们对为IPC使用这个设备的很有兴趣,因为当它被内存映射时它的特殊属性。


1、一个无命名的区域被创建,它的尺寸是mmap的第二个参数,向上取到系统上最近的页尺寸。


2、内存区域被初始化为0.


3、多个进程可以共享这个区域,如果一个通用祖先为mmap指定了MAP_SHARED标志。


下面的代码是使用这个特殊设备的例子。



  1. #include <fcntl.h>
  2. #include <sys/mman.h>
  3. #include "TELL_WAIT.h"

  4. #define NLOOPS 1000
  5. #define SIZE sizeof(long) /* size of shared memory area */

  6. static int
  7. update(long *ptr)
  8. {
  9.     return((*ptr)++); /* return value before increment */
  10. }

  11. int
  12. main(void)
  13. {
  14.     int fd, i, counter;
  15.     pid_t pid;
  16.     void *area;

  17.     if ((fd = open("/dev/zero", O_RDWR)) < 0) {
  18.         printf("open error\n");
  19.         exit(1);
  20.     }
  21.     if ((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
  22.         fd, 0)) == MAP_FAILED) {
  23.         printf("mmap error\n");
  24.         exit(1);
  25.     }
  26.     close(fd); /* can close /dev/zero now that it's mapped */

  27.     TELL_WAIT();
  28.     
  29.     if ((pid = fork()) < 0) {
  30.         printf("fork error\n");
  31.         exit(1);
  32.     } else if (pid > 0) { /* parent */
  33.         for (i = 0; i < NLOOPS; i+=2) {
  34.             if ((counter = update((long *)area)) != i) {
  35.                 printf("prarent: expected %d, got %d", i, counter);
  36.                 exit(1);
  37.             }

  38.             TELL_CHILD(pid);
  39.             WAIT_CHILD();
  40.         }
  41.     } else { /* child */
  42.         for (i = 1; i < NLOOPS + 1; i += 2) {
  43.             WAIT_PARENT();
  44.         
  45.             if ((counter = update((long *)area)) != i) {
  46.                 printf("child: expected %d, got %d", i, counter);
  47.                 exit(1);
  48.             }

  49.             TELL_PARENT(getppid());
  50.         }
  51.     }

  52.     exit(0);
  53. }

程序打开/dev/zero设备并调用mmap,指定一个长整型的尺寸。注意一旦区域被映射,我们可以close这个设备。进程然后创建一个 子进程。因为MAP_SHARED在mmap调用里被指定,所以一个进程对这块映射内存区域的写会被其它进程看到。(如果我们使用 MAP_PRIVATE,那么这个例子不会工作。)

父子然后交替运行,增加在共享内存映射区域里的长整型值,使用8.9节的同步函 数。内存映射区域被mmap初始化为0。父进程把它加到1,子进程接着把它加到2,父进程再把它加到3,如此下去。注意我们必须使用括号,当在 update函数里增加这个长整型值时,因为我们增加值面不是指针。


在我们已经展示的行为里使用/dev/zero的优点是在调用mmap 来创建映射区域之前真实文件不必存在。映射/dev/zero自动创建一个指定尺寸的映射区域。这个技术的缺点是它只在相关进程之间工作。然而,对于相关 的进程,使用线程很可能会更简单和更高效(11、12章)。注意不管哪种技术被使用,我们仍需要同步共享数据的访问。


许多实现提供匿名内存映射,一个和/dev/zero特性类似的设施。为了使用这个设施,我们为mmap指定MAP_ANON标志,并指定文件描述符为-1。结果区域是匿名的(因为通过一个文件描述符没有相关的路径名)并创建一个可以被后代进程共享的内存区域。


匿名内存映射设施被本文四个平台支持。然而,注意,Linux为这个设施定义了MAP_ANONYMOUS标志,但定义了MAP_ANON为相同的值以提高应用可移植性。


为了修改上面的代码来使用这个设施,我们作三处改动:a、删除/dev/zero的open,b、删除fd的close、c、改变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标志。

阅读(1216) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~