原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50888870
今天我们来介绍Linux下最高效的一种进程间通信方式 – 共享内存。
顾名思义,共享内存就是两个(或多个)进程共同占有一段内存空间,这些进程可以是有亲缘关系的进程,也可以是完全不相关的进程。同一块物理内存空间被映射到两个进程,两个进程都可以访问这段共享空间从而实现了进程间通信。但是值得注意的是:Linux的共享内存通信只提供了数据交换功能,并没有提供同步机制,需要使用其它机制来同步不同进程对共享内存的读写操作(如信号量)。
在Linux下,使用ipcs -m可以查看系统中共享内存的使用情况。下面是我在自己电脑中使用该命令的结果:
fred@ubuntu:~$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 294912 fred 600 524288 2 dest
0x00000000 393217 fred 600 524288 2 dest
0x00000000 491522 fred 600 524288 2 dest
0x00000000 688131 fred 600 524288 2 dest
0x00000000 786436 fred 600 524288 2 dest
0x00000000 819205 fred 600 16777216 2
0x00000000 983046 fred 600 16777216 2 dest
0x00000000 1081351 fred 600 524288 2 dest
0x00000000 1114120 fred 600 1048576 2 dest
0x00000000 1245193 fred 600 524288 2 dest
对于每一个共享内存,内核会为其定义一个shmid_ds结构体类型。shmid_ds结构体定义在头文件<sys/shm.h>
中。
关于共享内存的操作,主要有shmget、shmat、shmdt、shmctl四个函数,它们都定义在<sys/shm.h>
头文件中。下面我们分别介绍一下这几个函数。
我们知道,在Linux中创建或打开一个文件都可以使用open函数。与此类似,shmget调用也可以用来创建或打开一个共享内存区域。原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
若成功则返回共享内存的ID标识,否则返回-1。各个参数的含义如下:
(1)、第一个参数key
参数key表示所要创建或打开的共享内存的键值。无论何时创建一个IPC对象(如调用shmget),都需要指定一个key,它的数据类型是key_t。这个key由内核转变为标识符,它可以由以下方法产生:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
(2)、第二个参数size
参数size以字节为单位制定共享内存区域的长度。
(3)、第三个参数shmflg
参数shmflg用来设置shmget函数的操作类型,也可以用来设置共享内存的访问权限。两者可以通过逻辑或(“|”)来连接。
其中第一个参数key和第三个参数shmflg共同决定了shmget的操作类型,具体如下:
当创建或打开一个共享内存区域后,一个进程如果想要访问该共享内存需要将其附加到该进程的地址空间。该操作使用shmat函数完成。
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
该函数如果调用成功则返回指向共享内存区域的指针,否则返回-1。各参数的含义如下:
(1)、第一个参数shmid
参数shmid表示需要引入的共享内存标识符,也就是shmget函数的返回值。
(2)、第二个参数addr和第三个参数shmflg
为什么要把这两个参数放在一起讲呢?因为共享内存区域附加到调用进程的哪个地址上与addr参数和shmflg参数共同决定。具体规则如下:
如果shmflg中指定了SHM_RDONLY位,则以调用进程只能以“只读方式”访问共享内存,否则以“读写方式”访问。
当进程对共享内存区域操作完成后,应该调用shmdt函数将该共享内存区域从当前进程地址空间中分离开来。
#include <sys/shm.h>
int shmdt(const void *shmaddr);
其中参数shmaddr是shmat函数返回的地址指针。如果调用成功该函数返回0,否则返回-1。
shmdt函数只是将指定的共享内存区域与调用进程的地址空间分离,而不是删除共享内存区域。
共享内存是一种特殊的资源,系统提供了一个专门的操作函数用于共享内存的控制。原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
如果操作成功返回0,否则返回-1。各参数的含义如下:
(1)、第一个参数shmid
参数shmid表示需要操作的共享内存标识符,也就是shmget函数的返回值。
(2)、第二个参数cmd
参数cmd指明了所要进行的操作类型,主要有以下五种:
cmd取值 | 操作类型 |
---|---|
IPC_STAT | 取该共享内存的shmid_ds结构,并存在第三个参数中 |
IPC_SET | 使用buf指定的结构设置相关属性 |
IPC_RMID | 删除指定共享内存,只有当buf中的shm_nattch值为0时才真正删除 |
IPC_LOCK | 在内存中对共享内存加锁(超级用户权限) |
IPC_UNLOCK | 解锁共享内存(超级用户权限) |
(3)第三个参数buf
参数buf主要配合参数cmd使用,shmid_ds至少包含以下字段(摘自《Unix环境高级编程》):
下面我们通过一个实例来演示一下共享内存的使用:
写进程shm_write.c如下:
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <errno.h>
#define BUF_SIZE 4096
int main()
{
void *shm_addr = NULL;
char buffer[BUF_SIZE];
int shmid;
// 使用约定的键值创建共享内存
shmid = shmget((key_t) 1234, BUF_SIZE, 0666 | IPC_CREAT);
printf("shmid : %u\n", shmid);
if (shmid < 0)
{
perror("shmget error!");
exit(1);
}
// 将共享内存附加到本进程
shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *) -1)
{
perror("shmat error!");
exit(1);
}
// 写入数据
bzero(buffer, BUF_SIZE);
sprintf(buffer, "Hello, My PID is %u\n", (unsigned int) getpid());
printf("send data: %s\n", buffer);
memcpy(shm_addr, buffer, strlen(buffer));
sleep(5);
// 分离
if (shmdt(shm_addr) == -1)
{
printf("shmdt error!\n");
exit(1);
}
}
读进程shm_read.c如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define BUF_SIZE 4096
int main()
{
void *shm_addr = NULL;
int shmid;
// 使用约定的键值打开共享内存
shmid = shmget((key_t) 1234, BUF_SIZE, IPC_CREAT);
printf("shmid : %u\n", shmid);
if (shmid == -1)
{
perror("shmget error!");
exit(1);
}
// 将共享内存附加到本进程
shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *) -1)
{
perror("shmat error!");
exit(1);
}
// 读取数据
char tmp[BUF_SIZE];
bzero(tmp, BUF_SIZE);
memcpy(tmp, shm_addr, BUF_SIZE);
printf("read from shared memory: %s\n", tmp);
sleep(5);
// 分离
if (shmdt(shm_addr) == -1)
{
printf("shmdt error!\n");
exit(1);
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1)
{
printf("shmctl error!\n");
exit(1);
}
}
我们先运行写进程,输出如下:
shmid : 1736715
send data: Hello, My PID is 3122
此时,我们使用ipcs -m
命令查看系统中的确存在标识符为1736715的共享内存区域。如下
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
...(省略无关项)
0x000004d2 1736715 root 666 4096 0
此时,写进程已经跟共享内存分离,所以nattch为0。
接下来,我们运行读进程。输出如下:
shmid : 1736715
read from shared memory: Hello, My PID is 3122
读进程执行完毕后删除了共享内存区域,所以使用ipcs -m
命令中发现系统中并没有shmid值等于1736715的共享内存区域。
很明显,由于进程可以直接读写内存,所以采用共享内存进行进程间通信的一个优点就是快速、效率高。像我们前面介绍的匿名管道和命名管道通信方式,需要在用户空间和内核空间之间进行数据拷贝,而共享内存通信方式只在内存中操作,要高效得多。
另外一方面,由于多个进程对同一段内存区域都具有读写权限,在共享内存通信中,进程间的同步问题就显得尤为重要。必须保证同一时刻只有一个进程对共享内存进行写操作,否则会导致数据被覆盖。而共享内存通信又没有提供同步机制,需要使用诸如信号量等手段进行同步控制,这又增加了其复杂性。
参考:《Unix环境高级编程》