进程间通信简介(五)——共享内存

来源:互联网 发布:java数组排序方法 编辑:程序博客网 时间:2024/05/22 07:44

        共享内存可以说是Linux下最快速、最有效的进程间通信。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间,进程A可以即时看到进程B对共享内存中数据的更新;反之,进程B也可以即时看到进程A对共享内存中数据的更新。

6.1 共享内存的概念

        共享内存就是多个进程可以把一段内存映射到自己的进程空间,以此来实现数据的共享及传输,这也是所有进程间通信方式最快的一种,共享内存是存在于内核级别的一种资源。
        在Shell中可以使用ipcs命令来查看当前系统IPC中的状态,在文件系统中/proc目录下有对其描述的相应文件。在系统内核为一个进程分配内存地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。共享内存机制就是通过该原理来实现的,共享内存机制只是提供数据的传送那个,如何控制服务器端和客户端的读写操作互斥,这就需要一些其他的辅助工具,例如信号量的概念。所以说,共享内存虽然是多个进程间共享和传递数据的一种有效的方式,但由于它并未提供同步机制,所以我们通常需要用其它的机制来同步对共享内存的访问。我们一把用共享内存来提供对大块内存区域的有效访问,同时通过传递小消息来同步对内存的访问。
        采用共享内存通信的一个显而易见的好处就是效率高。因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
        共享内存的最大不足之处在于,由于多个进程对同一块内存区域具有访问的权限,各个进程之间的同步问题显得尤其重要。必须控制同一时刻只有一个进程对共享内存区域写入数据,否则将造成数据的混乱。同步控制问题可通过信号量来解决。
        对于每一个共享存储段,内核会为其维护一个shmid_ds类型的结构体(shmid_ds结构体定义在头文件<sys/shm.h>中)。shmid_ds结构体定义如下:
struct shmid_ds{struct ipc_perm shm_perm;/* 对应于共享内存的ipc_perm结构 */size_t shm_segsz;/* 以字节表示的共享内存区域的大小 */pid_t shm_lpid;/* 最近一次调用shmop函数的进行ID */pid_t shm_cpid;/* 创建该共享内存的进程ID */unsigned short shm_lkcnt;/* 共享内存区域被锁定的时间数 */unsigned long shm_nattch;/* 当期使用该共享内存的进程数 */time_t shm_atime;/* 最近一次附加操作的时间 */time_t shm_dtime;/* 最近一次分离操作的时间 */time_t shm_ctime;/* 最近一次修改的时间 */};
        结果体shmid_ds会根据不同的系统内核版本而略有不同,并且在不同的系统中会对共享存储段的大小有限制,在应用时请查询相应的系统手册。

6.2 共享内存的相关操作

6.2.1 创建或打开共享内存

        创建共享内存的函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflg);
        若成功则返回共享内存ID,失败则返回-1。
        shmget函数除了可以用于创建一个新的共享内存外,也可用于打开一个已存在的共享内存。其中,参数key表示所要创建或打开的共享内存的键值。size表示共享内存区域的大小,只在创建一个新的共享内存时生效。参数shmflg表示调用函数的操作类型,也可用于设置共享内存的访问权限,两者通过逻辑或表示。
        参数key和shmflg决定了调用函数shmget的作用,相应的约定如下:
  • 当key为IPC_PRIVATE时,创建一个新的共享内存,此时参数shmflg的取值无效。
  • 当key不为IPC_PRIVATE时,且shmflg设置了IPC_CREAT位,而没有设置IPC_EXCL位,则执行操作由key取值决定。如果key为内核中某个已存在的共享内存的键值,则执行打开这个键的操作;反之,则执行创建共享内存的操作。
  • 当key不为IPC_PRIVATE时,且shmflg设置了IPC_CREAT位和IPC_EXCL位,则只执行创建共享内存的操作。参数key的取值应与内核中已存在的任何共享内存的键值都不相同,否则函数调用失败,返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开共享内存的函数就可以了(即将shmflg设置为IPC_CREAT,而不设置IPC_EXCL)。
        权限标志对共享内存非常有用,因为它们允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,同时其它用户创建的进程只能读取该共享内存。我们可以利用这个功能来提供一种有效的对数据进行只读访问的方法,通过将数据放入共享内存并设置它的权限,就可以避免数据被其它用户修改。
        另外,当调用shmget函数创建一个新的共享内存时,此共享内存的shmid_ds结构被初始化。ipc_perm中的各个域被设置为相应的值,shm_lpid、shm_nattch、shm_atime、shm_dtime被初始化为0,shm_ctime被设置为系统当前时间。
        接下来演示使用shmget函数创建一块共享内存。程序在调用shmget函数时指定可以参数值为IPC_PRIVATE,这个参数的意义是创建一个新的共享内存区,当创建成功后使用shell命令ipcs -m来显示目前系统下共享内存的状态。
#include <stdio.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include <stdlib.h>#define BUFSZ 1024int main(){        int shm_id;        shm_id = shmget(IPC_PRIVATE, BUFSZ, 0666);        if (shm_id < 0)        {                printf("shmget failed!\n");                exit(EXIT_FAILURE);        }        printf("create a shared memory segment successfully: %d\n", shm_id);        system("ipcs -m");        exit(EXIT_SUCCESS);}
        编译并运行:
$ ./shm_create
create a shared memory segment successfully: 3997706


------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态
0x00000000 294912     regan      600        524288     2          目标    
0x00000000 3866625    regan      600        33554432   2          目标    
0x00000000 3997706    regan      666        1024       0                  


$
        由打印结果可以看出,系统多出一个ID为3997706的共享内存段。

6.2.2 附加

        但有一个共享内存创建或打开后,某个进程如果要使用该共享内存则必须将此内存区域附加到它的地址空间,附加操作的系统调用如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat (int shmid, const void *shmaddr, int shmflg);
        若成功则返回指向共享内存段的指针,反之返回-1。
        参数shmid是由shmget返回的共享内存标识符。shmaddr指定的是共享内存连接到当前进程中的地址位置,它通常是一个空指针,表示让系统来选择共享内存出现的地址。shmflg是一组位标志,它的两个可能取值是SHM_RND(这个标志与shmaddr联合使用,用来控制共享内存连接的地址)和SHM_RDONLY(它使得连接的内存只读)。我们很少需要控制共享内存连接的地址,通常都是让系统来选择一个地址,否则就会使应用程序对硬件的依赖性过高。
        共享内存的读写权限由它的属主(共享内存的创建者)、它的访问权限和当前进程的属主决定。共享内存的访问权限类似于文件的访问权限。这个规则的一个例外是,当shmflg & SHM_RDONLY为true时的情况。此时即使该共享内存的访问权限允许写操作,它都不能被写入。
        shmat函数成功执行后,会将shmid嗦表示共享内存段的shmid_ds结构的shm_nattch计数器的值加1。

6.2.3 分离

        当进程对共享内存段的操作完成后,应调用shmdt函数,作用是将指定的共享内存段从当前进程空间中脱离出去,原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt (const viod *shmaddr);
        若成功返回0,否则返回-1。
        此函数仅用于将共享内存区域与进程的地址空间分离,并不删除共享内存本身。参数shmaddr是调用shmat函数时的返回值。shmdt函数成功执行后,将该共享内存的shmid_ds结构中的shm_nattch计数器减1。

6.2.4 共享内存的控制

        由于共享内存这一特殊的资源类型,使它不同于普通的文件,因此,系统需要为其提供专有的操作函数,而这无疑增加了程序员开发的难度(需要记忆额外的专有函数)。使用函数shmctl可以对共享内存段进行多种操作,原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl (int shmid, int cmd, struct shmid_ds *buf);
        若成功则返回0,否则返回-1。X/Open规范没有定义当你试图删除一个正处于连接状态的共享内存段时将会发生的情况。通常这个已经被删除的处于连接状态的共享内存段还能继续使用,直到它从最后一个进程中分离为止。但因为这个行为并未在规范中定义,所以最好不要依赖它。
        shmid_ds结构至少包含以下成员:
struct shmid_ds{uid_t shm_perm.uid;uid_t shm_perm.gid;mode_t shm_perm.mode;};
        参数shmid为所要操作的共享内存段的标识符,struct shmid_ds型指针参数buf的作用与参数cmd的值相关,参数cmd指明了所要进行的操作,其解释如表6-2-4-1。
表6-2-4-1 cmd的取值及含义取值含义IPC_STAT取shmid所指向内存共享段的shmid_ds结构,对参数buf指向的结构赋值IPC_SET使用buf指向的结构对sh_mid段的相关结构赋值,只对以下几个域有作用,shm_perm.uid、shm_perm.gid及shm_perm.modeIPC_RMID删除shmid所指向的共享内存段,只有当shmid_ds结构的shm_nattch域为零时,才会真正执行删除命令,否则不会删除该段。注意此命令的请求规则与IPC_SET命令相同SHM_LOCK锁定共享内存段在内存,此命令只能由超级用户请求SHM_UNLOCK对共享内存段解锁,此命令只能由超级用户请求        对于上表中的IPC_SET参数选项,只有具备以下条件的进程才可以请求:(1)进程的用户ID等于shm_perm.cuid或者等于shm_perm.uid;(2)超级用户特权进程。
        接下来演示共享内存在进程间传递数据,需要两个程序,shm_write主要是写共享内存,shm_read主要是读共享内存的数据。
        shm_write程序源代码:
#include <sys/ipc.h>#include <sys/shm.h>#include <sys/types.h>#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>typedef struct{        char name[4];        int age;} people;int main(int argc, char **argv){        int shm_id, i;        char tmp;        people *map;        if (argc != 2)        {                printf("Usage: atshm <identifier>");                exit(EXIT_FAILURE);        }        shm_id = atoi(argv[1]);        map = (people *)shmat(shm_id, NULL, 0);        tmp = 'a';        for (i = 0; i < 10; i++)        {                tmp += 1;                memcpy((*(map + i)).name, &tmp, 1);                (*(map + i)).age = 20 + i;        }        if (shmdt(map) == -1)        {                perror("detach error!\n");        }        exit(EXIT_SUCCESS);}
        shm_read程序源代码:
#include <sys/ipc.h>#include <sys/shm.h>#include <sys/types.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>typedef struct{        char name[4];        int age;} people;int main(int argc, char **argv){        int shm_id, i;        people *map;        if (argc != 2)        {                printf("Usage: atshm <identifier>");                exit(EXIT_FAILURE);        }        shm_id = atoi(argv[1]);        map = (people *)shmat(shm_id, NULL, 0);        for (i = 0; i < 10; i++)        {                printf("name: %s\t", (*(map + i)).name);                printf("age %d\n", (*(map + i)).age);        }        if (shmdt(map) == -1)                perror("detach error!\n");        exit(EXIT_SUCCESS);}
        还记得先前我们创建了一个未使用的共享内存3997706吗?现在我们就使用它。编译并运行:
$ ./shm_write 3997706
$ ./shm_read 3997706
name: b age 20
name: c age 21
name: d age 22
name: e age 23
name: f age 24
name: g age 25
name: h age 26
name: i age 27
name: j age 28
name: k age 29
$
        可以看到shm_write执行过程中并没有任何输出,也就是向共享内存段写数据成功了,shm_read也读取到了相应的数据。
        接下来继续演示一下两个进程间传递数据。
        头文件shm_com.h:
#define TEXT_SZ 2048struct shared_use_st{        int written_by_you;        char text[TEXT_SZ];};
        生产者程序shm_producer.c:
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#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);        }        shared_memory = shmat(shmid, (void *)0, 0);        if (shared_memory == (void *)-1)        {                fprintf(stderr, "shmat failed\n");                exit(EXIT_FAILURE);        }        printf("memory attached at %p\n", shared_memory);        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->text);                        sleep(rand() % 4);                        shared_stuff->written_by_you = 0;                        if (strncmp(shared_stuff->text, "end", 3) == 0)                                running = 0;                }        }        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);}
        消费者程序shm_customer.c:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include "shm_com.h"int main(){        int running = 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 %p\n", 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->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);}

        编译并运行:

$ ./shm_producer &
[1] 10229
memory attached at 0x7fc2c3060000
$ ./shm_customer
memory attached at 0x7fdaf18a0000
Enter some text: hello
You wrote: hello
waiting for client...
Enter some text: Welcome to my hometown!
You wrote: Welcome to my hometown!
waiting for client...
Enter some text: end
You wrote: end
$

        shm_producer创建共享内存段,然后将它连接到自己的地址空间中,我们在共享内存的开始处使用了一个结构shared_use_st,该结构中有个标志written_by_you,当共享内存中有数据写入时就设置这个标志。这个标志被设置时,程序就从共享内存中读取文本并打印,然后清除这个标志表示已经读完数据,再用一个end特殊字符串来退出循环,接下来程序分离共享内存段并删除它。

        shm_customer使用相同的键来取得并连接同一个共享内存段。然后它提示用户输入文本。如果标志written_by_you被设置,程序就知道客户进程还未读完上一次的数据,因此就继续等待。当其它进程清除了这个标志后,程序写入新数据并设置该标志。它也是使用end来终止并分离共享内存段。

        注意,这里的同步标志(written_by_you)非常简陋,它包括一个非常缺乏效率的忙等待(不停地循环),实际编程中应该使用信号量或通过传递消息、生成信号的方法来提供应用程序读、写部分之间的一种更有效率的同步机制。


整理自 《Linux程序设计第4版》、《Linux C编程从初学到精通》。

0 0