进程间通信之共享存储

共享存储允许两个或更多进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种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.cnblogs.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,则此段连接到由内核选择的第一个可用地址上。这是推荐的使用方式。
  • 如果addr非0,并且没有指定SHM_RND,则此段连接到addr所指定的地址上。
  • 如果addr非0,并且指定了SHM_RND,则此段连接到(addr-(addr mod ulus SHMLBA))所表示的地址上。SHM_RND命令的意思是“取整”。SHMLBA的意思是“低边界地址倍数”,它总是2的乘方。该算式是将地址向下取最近1个SHMLBA的倍数。

除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不应指定共享段所连接到的地址。所以一般应指定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.cnblogs.com/nufangrensheng/p/3508169.html中的图7-3中所示的典型存储区布局类似。

http://www.cnblogs.com/nufangrensheng/p/3559664.html中曾说明mmap函数可将一个文件的若干部分映射至进程地址空间。这在概念上类似与用shmat XSI IPC函数连接一共享存储段。两者之间的主要区别是:用mmap映射的存储段是与文件相关联的,而XSI共享存储段则并无这种关联。

实例:/dev/zero的存储映射

共享存储可由不相关的进程使用。但如果进程是相关的,则某些实现提供了一种不同的技术。

在读设备/dev/zero时,该设备是0字节的无限资源。它也接收写向它的任何数据。但又忽略这些数据。我们对此设备作为IPC的兴趣在于,当对其进行存储映射时,它具有一些特殊的性质:

  • 创建一个无名(unnamed)存储区,其长度是mmap的第二个参数,将其向上取整为系统的最近页长。
  • 存储区都初始化为0.
  • 如果多个进程的共同祖先进程对mmap指定了MAP_SHARED标志,则这些进程可共享此存储区。

程序清单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 int
update(long *ptr)
{
    return((*ptr)++);    /* return value before increment */
}

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

posted @ 2014-02-23 22:13  ITtecman  阅读(2463)  评论(0编辑  收藏  举报