进程通信--共享内存详解

共享内存概述

  可以说,共享内存是一种最为高效的进程间通信方式。因为进程可以直接读写内存,不需要任何数据的复制。为了在多个进程间交换信息,内核专门留出 了一块内存区。这段内存区可以由需要访问的进程将其映射到自己的私有地址空间。因此,进程就可以直接读写这一内存区而不需要进行数据的复制,从而大大提高 了效率。当然,由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等(请参考本章的共享内存实验)。其原理示意图如图1所示。

进程间通信--内存共享-LMLPHP

1 共享内存原理示意图

 

使用共享内存共分四步:


 

我们用一个简短的例子来体验一下这个过程:

int *addr;      //用来保存映射的地址

int shmid;      //用来保存共享内存的ID标识符

 

shmid = shmget(IPC_PRIVATE, 4, 0);         //获得四个字节大小的共享内存,返回的共享内存ID标识符被保存到shmid

addr = shmat(shmid, 0, 0);              //映射共享内存,获得共享内存地址

*addr = 3;             //对共享内存进行操作,将这个地址赋值为3

shmdt(addr);           //解除映射,但共享内存依然存在于系统中,没有被删除

shmctl(shmid, IPC_RMID, NULL);         //删除共享内存

 

上面只是一个简短示例,用来体会一下应用过程,实际编写程序时,需要对返回值进行检检查,以免出错。

共享内存应用

共享内存的实现分为两个步骤,

第一步是创建共享内存,这里用到的函数是shmget(),也就是从内存中获得一段共享内存区域,

第二步映射共享 内存,也就是把这段创建的共享内存映射到具体的进程空间中,这里使用的函数是shmat()

到这里,就可以使用这段共享内存了,也就是可以使用不带缓冲 的I/O读写命令对其进行操作。除此之外,当然还有撤销映射的操作,其函数为shmdt()。这里就主要介绍这3个函数。

 

共享内存的函数使用介绍

现在解释一下这四个函数的用法:

1.int shmget(key_t key, int size, int shmflg);

       key取值为IPC_PRIVATE时,建立新的共享内存,大小由size决定。

共享内存的键值,多个进程可以通过它访问同一个共享内存,其中有个特殊值IPC_PRIVATE。它用于创建当前进程的私有共享内存

       key取值不为IPC_PRIVATE时,而且也不是已建立的共享内存IPC key,则视参数shmflg中是否有IPC_CREAT标志,来决定是否建立新的共享内存,并且以key值为新共享内存的IPC key。如果没有IPC_CREAT标志,则会返回错误。

       key取值为已建立的共享内存IPC key时,如果参数shmflg中包含IPC_CREATIPC_EXCL,则返回错误。如果只包含这两个标志中的其中之一,或者两个都不包含,则视size的大小是否小于等于这个已存在的共享内存的大小,如果小于等于,则返回这个共享内存的ID标识符;否则,返回错误。

     参数shmflg也可以用来设置共享内存的存取权限,其值相当于open()函数的mode用法,执行位不用。

返回值:若成功返回共享内存的ID标识符,错误返回-1,错误原因存于errno

 

2.void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid为共享内存ID标识符shmget 返回的

shmaddr指定映射的地址:

如果shmaddr0,则由内核自动分配。

如果不为0shmflg也没有指定IPC_RND旗标,则以参数shmaddr为连接地址。

如果不为0shmflg指定了IPC_RND旗标,则将自动将参数shmflg调整为SHMLBA的整数倍。

 

SHMLBA的定义有两种情况,如下:

#define   SHMLBA   PAGE_SIZE

#define   SHMLBA   (4 * PAGE_SIZE)

 

shmflg还可以有SHM_RDONLY,表示映射的内存只可以读。

 

附加说明:1.在经过fork()之后,映射的地址会被继承到子进程。

          2.在经过exec()之后,映射的地址连接会自动脱离。

          3.程序运行结束之后,映射的地址连接也会自动脱离。

返回值:若成功返回映射得到的地址,错误返回-1,错误原因存于errno

 

3. int shmdt(const void *shmaddr);

     shmaddrshmat()映射后获得的地址,此函数解除shmaddr与它连接的共享内存之间的映射。

返回值:成功返回0,否则返回-1,错误原因存于errno

 

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

     shmctl()提供了几种方式来控制共享内存的操作,shmid为共享内存的ID标识符,cmd为操作的命令,有如下操作:

     IPC_STAT          把共享内存的shmid_ds结构数据复制到buf(每个共享内存中,都包含有一个shmid_ds结构,里面保存了一些关于本共享内存的信息)

     IPC_SET           将参数所指的shmid_ds结构中的shm_perm.uidshm_perm.gidshm_perm.mode复制到共享内存的shmid_ds结构内。

     IPC_RMID          删除共享内存和其包含的数据结构

     SHM_LOCK         不让此共享内存置换到 swap    (什么是swap,这个本人也不理解,如果有高手知道,希望不吝赐教)

     SHM_UNLOCK      允许此共享内存置换到swap

     SHM_LOCKSHM_UNLOCKLinux特有,且唯有超级用户允许使用。

 

     shmid_ds结构定义(linux-2.6.32.2):

struct shmid_ds {

    struct ipc_permshm_perm;/* operation perms */

    int        shm_segsz;/* size of segment (bytes) */

__kernel_time_tshm_atime;/* last attach time */

    __kernel_time_tshm_dtime;/* last detach time */

    __kernel_time_tshm_ctime;/* last change time */

__kernel_ipc_pid_t shm_cpid;/* pid of creator */

__kernel_ipc_pid_t shm_lpid; /* pid of last operator */

    unsigned shortshm_nattch;/* no. of current attaches */

    unsigned short shm_unused;/* compatibility */

    void *shm_unused2;/* ditto - used by DIPC */

    void *shm_unused3;/* unused */

};

 

shm_segsz        共享内存的大小(bytes)

shm_atime        最后一次attach(映射)此共享内存的时间

shm_dtime        最后一次detach(解除映射)此共享内存的时间

shm_ctime        最后一次更动此共享内存的时间。

shm_cpid         建立此共享内存的进程识别码。

shm_lpid          最后一个操作此共享内存的进程识别码。

 

Demo程序

下面是一个经典范例:


  1. #include <unistd.h>

  2. #include <sys/ipc.h>

  3. #include <sys/shm.h>



  4. #define KEY 1234

  5. #define SIZE 1024

  6.  

  7. int main()

  8. {

  9.        int shmid;

  10.        char *shmaddr;

  11.        struct shmid_ds buf;

  12.        shmid = shmget(KEY, SIZE, IPC_CREAT | 0600); /*建立共享内存*/

  13.  

  14.        if (fork() == 0)

  15.        {

  16.               shmaddr = (char *) shmat(shmid, 0, 0);

  17.               strcpy(shmaddr, "Hi! I am Chiled process!/n");

  18.               printf("Child: write to shared memery: /" I am Chiled process!/"/n");

  19.               shmdt(shmaddr);

  20.               return;

  21.        }

  22.        else

  23.        {

  24.               sleep(3); /*等待子进程执行完毕*/

  25.               shmctl(shmid, IPC_STAT, &buf); /*取得共享内存的状态*/

  26.               printf("shm_segsz = %d bytes/n", buf.shm_segsz);

  27.               printf("shm_cpid = %d/n", buf.shm_cpid);

  28.               printf("shm_lpid = %d/n", buf.shm_lpid);

  29.               shmaddr = (char*) shmat(shmid, 0, SHM_RDONLY);

  30.               printf("Father: %s/n", shmaddr); /*显示共享内存内容*/

  31.               shmdt(shmaddr);

  32.               shmctl(shmid, IPC_RMID, NULL); /*删除共享内存*/

  33.        }

  34. }

3.使用实例

  该实例说明如何使用基本的共享内存函数。首先是创建一个共享内存区(采用的共享内存的键值为IPC_PRIVATE,是因为本实例中创建的共享内存是父子进程之间的共用部分),之后创建子进程,在父子两个进程中将共享内存分别映射到各自的进程地址空间之中。

  父进程先等待用户输入,然后将用户输入的字符串写入到共享内存,之后往共享内存的头部写入“WROTE”字符串表示父进程已成功写入数据。子进 程一直等到共享内存的头部字符串为“WROTE”,然后将共享内存的有效数据(在父进程中用户输入的字符串)在屏幕上打印。父子两个进程在完成以上工作之 后,分别解除与共享内存的映射关系。

  最后在子进程中删除共享内存。因为共享内存自身并不提供同步机制,所以应该额外实现不同进程之间的同步(例如:信号量)。为了简单起见,在本实例中用标志字符串来实现非常简单的父子进程之间的同步。

  这里要介绍的一个命令是ipcs,这是用于报告进程间通信机制状态的命令。它可以查看共享内存、消息队列等各种进程间通信机制的情况,这里使用了system()函数用于调用shell命令“ipcs”。程序源代码如下所示:

  /* shmem.c */  

  1. #include <sys/types.h>

  2.   #include <sys/ipc.h>

  3.   #include <sys/shm.h>

  4.   #include <stdio.h>

  5.   #include <stdlib.h>

  6.   #include <string.h>

  7.   #define BUFFER_SIZE 2048

  8.   int main()

  9.   {

  10.    pid_t pid;

  11.    int shmid;

  12.    char *shm_addr;

  13.    char flag[] = "WROTE";

  14.    char *buff;

  15.    /* 创建共享内存 */

  16.    if ((shmid = shmget(IPC_PRIVATE, BUFFER_SIZE, 0666)) < 0)

  17.    {

  18.    perror("shmget");

  19.    exit(1);

  20.    }

  21.    else

  22.    {

  23.    printf("Create shared-memory: %d\n",shmid);

  24.    }

  25.    /* 显示共享内存情况 */

  26.    system("ipcs -m");

  27.    pid = fork();

  28.    if (pid == -1)

  29.    {

  30.    perror("fork");

  31.    exit(1);

  32.    }

  33.    else if (pid == 0) /* 子进程处理 */

  34.    {

  35.    /*映射共享内存*/

  36.    if ((shm_addr = shmat(shmid, 0, 0)) == (void*)-1)

  37.    {

  38.    perror("Child: shmat");

  39.    exit(1);

  40.    }

  41.    else

  42.    {

  43.    printf("Child: Attach shared-memory: %p\n", shm_addr);

  44.    }

  45.    system("ipcs -m");  

  46.    /* 通过检查在共享内存的头部是否标志字符串"WROTE"来确认

  47.   父进程已经向共享内存写入有效数据 */

  48.    while (strncmp(shm_addr, flag, strlen(flag)))

  49.    {

  50.    printf("Child: Wait for enable data...\n");

  51.    sleep(5);

  52.    }  

  53.    /* 获取共享内存的有效数据并显示 */

  54.    strcpy(buff, shm_addr + strlen(flag));

  55.    printf("Child: Shared-memory :%s\n", buff);  

  56.    /* 解除共享内存映射 */

  57.    if ((shmdt(shm_addr)) < 0)

  58.    {

  59.    perror("shmdt");

  60.    exit(1);

  61.    }

  62.    else

  63.    {

  64.    printf("Child: Deattach shared-memory\n");

  65.    }

  66.    system("ipcs -m");  

  67.    /* 删除共享内存 */

  68.    if (shmctl(shmid, IPC_RMID, NULL) == -1)

  69.    {

  70.    perror("Child: shmctl(IPC_RMID)\n");

  71.    exit(1);

  72.    }

  73.    else

  74.    {

  75.    printf("Delete shared-memory\n");

  76.    }  

  77.    system("ipcs -m");

  78.    }

  79.    else /* 父进程处理 */

  80.    {

  81.    /*映射共享内存*/

  82.    if ((shm_addr = shmat(shmid, 0, 0)) == (void*)-1)

  83.    {

  84.    perror("Parent: shmat");

  85.    exit(1);

  86.    }

  87.    else

  88.    {

  89.    printf("Parent: Attach shared-memory: %p\n", shm_addr);

  90.    }  

  91.    sleep(1);

  92.    printf("\nInput some string:\n");

  93.    fgets(buff, BUFFER_SIZE, stdin);

  94.    strncpy(shm_addr + strlen(flag), buff, strlen(buff));

  95.    strncpy(shm_addr, flag, strlen(flag));  

  96.    /* 解除共享内存映射 */

  97.    if ((shmdt(shm_addr)) < 0)

  98.    {

  99.    perror("Parent: shmdt");

  100.    exit(1);

  101.    }

  102.    else

  103.    {

  104.    printf("Parent: Deattach shared-memory\n");

  105.    }

  106.    system("ipcs -m");  

  107.    waitpid(pid, NULL, 0);

  108.    printf("Finished\n");

  109.    }

  110.    exit(0);

  111.   }

 

/*总结:

1.父子进程可以在fork之前只映射一次内存shmat

2.当然也在父子进程中分别映射,但是调用shmat后的映射地址是一样的

3.共享内存试用于任何进程之间的通信,不只是父子进程之间,需要通过其他通信方式jiang KEY send out

*/



09-14 07:04