共享内存
共享内存允许两个或多个进程访问给定的同一块存储区域。已知当一个进程被启动时,系统会为其创建一个0~4G的虚拟内存空间,根据虚拟地址与物理地址之间的映射关系,进程可以通过操作虚拟地址,实现对物理页面的操作。一般情况下,每个进程的虚拟地址空间会与不同的物理地址进行映射,但是当使用共享内存进行通信时,系统会将同一段物理内存映射给不同的进程。两个进程的虚拟地址空间与共享内存之间的映射关系如图1所示。
图1 映射关系
系统中的物理内存和虚拟内存都通过页面来管理,为多个进程分配共享内存,实际是为进程分配一个或多个物理页面,因此,共享内存的大小必须是系统中页面大小的整数倍。若进程需要使用共享内存,应首先将虚拟内存空间与共享内存进行映射,映射完成后,进程对虚拟地址的读写,就相当于直接对物理内存的读写。另外,与申请堆空间类似,当通信完成之后,也应释放物理内存,解除进程与共享内存的映射关系。
内存映射也是效率最高的一种进程通信方式,它节省了不同进程间多次读写的时间:若有多个进程将自己的虚拟地址与此块物理内存进行绑定,那么当一个进程对此块内存中的数据进行修改时,其它进程可以直接获得修改后的数据。当然,在写进程的操作尚未完成时,不应有进程从共享内存中读取数据,共享内存自身不限制进程对共享内存的读写次序,但程序开发人员应自觉遵循读写规则,一般情况下,共享内存应与信号量一起使用,由信号量帮它实现读写操作的同步。
Linux内核提供了一些系统调用,用于实现共享内存的申请、管理与释放,这些函数分别为:shmget()、shmat()、shmdt()和shmctl(),下面将分别对它们进行讲解。
① shmget()
shmget()函数的功能是创建一块新的共享内存,或打开一块已经存在的共享内存,该函数存在于函数库sys/shm.h中,其定义如下:
int shmget(key_t key, size_t size, int shmflg);
shmget()函数若调用成功,将会返回一个共享内存标识符(该标识符是一个非负整数);若调用失败,将会返回-1,并对errno进行设置。
shmget()函数中的第一个参数key通常为整数,代表共享内存的键值;参数size用于设置共享内存的大小;参数shmflg用于设置shmget()函数的创建条件(一般设置为IPC_CREAT或IPC_EXCL)及进程对共享内存的读写权限。
② shmat()
shmat()函数的功能是进行地址映射,将共享内存映射到进程虚拟地址空间中,该函数存在于函数库sys/shm.h中,其定义如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat()函数若调用成功,会返回映射的地址,并更改共享内存shmid_ds结构中的属性信息;若调用失败,会返回-1,并设置errno。
shmat()函数中的第一个参数shmid为共享内存标识符,该标识符一般由shmget()函数返回;参数shmaddr为一个指针类型的传入参数,用于指定共享内存映射到虚拟内存时的虚拟地址,当设置为NULL时,映射地址由系统决定;参数shmflg用于设置共享内存的使用方式,若shmflg设置为SHM_RDONLY,则共享内存将以只读的方式进行映射,当前进程只能从共享内存中读取数据。
③ shmdt()
shmdt()函数的功能是解除物理内存与进程虚拟地址空间的映射关系,该函数存在于函数库sys/shm.h中,其定义如下:
int shmdt(const void *shmaddr);
shmdt()函数中的参数为shmat()函数返回的虚拟空间地址,函数调用成功则返回0,并修改共享内存的shmid_ds结构中的属性信息;否则返回-1。
④ shmctl()
shmctl()函数的功能是对已存在的共享内存进行操作,具体的操作由参数决定,该函数存在于函数库sys/shm.h中,其定义如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl()函数调用成功则返回0,否则返回-1,并设置errno。该函数中的参数shmid表示共享内存标识符;参数cmd表示要执行的操作,常用的设置为IPC_RMID,功能为删除共享内存;参数buf用于对共享内存的管理信息进行设置,该参数是一个结构体指针,这个结构体是一个为了方便对共享内存进行管理,由内核维护的存储共享内存属性信息的结构体,该结构体的类型定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; //所有者和权限标识
size_t shm_segsz; //共享内存大小
time_t shm_atime; //最后映射时间
time_t shm_dtime; //最后解除映射时间
time_t shm_ctime; //最后修改时间
pid_t shm_cpid; //创建共享内存进程的id
pid_t shm_lpid; //最近操作共享内存进程的id
shmatt_t shm_nattch; //与共享内存发生映射的进程数量
...
};
需要注意的是,共享内存与消息队列以及信号量相同,在使用完毕后都应该进行释放。另外,当调用fork()函数创建子进程时,子进程会继承父进程已绑定的共享内存;当调用exec()函数更改子进程功能以及调用exit()函数时,子进程中都会解除与共享内存的映射关系,因此在必要时仍应使用shmctl()函数对共享内存进行删除。
下面通过一个案例,来展示共享内存通信中函数接口的使用方法。
案例6:创建两个进程,使用共享内存机制实现这两个进程间的通信。
shm_w.c
1 #include <stdio.h>
2 #include <sys/ipc.h>
3 #include <sys/shm.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6 #include <string.h>
7 #define SEGSIZE 4096 //定义共享内存容量
8 typedef struct{ //读写数据结构体
9 char name[8];
10 int age;
11 } Stu;
12 int main()
13 {
14 int shm_id, i;
15 key_t key;
16 char name[8];
17 Stu *smap;
18 key = ftok("/", 0); //获取关键字
19 if (key == -1)
20 {
21 perror("ftok error");
22 return -1;
23 }
24 printf("key=%d\n", key);
25 //创建共享内存
26 shm_id = shmget(key, SEGSIZE, IPC_CREAT | IPC_EXCL | 0664);
27 if (shm_id == -1)
28 {
29 perror("create shared memory error\n");
30 return -1;
31 }
32 printf("shm_id=%d\n", shm_id);
33 smap = (Stu*)shmat(shm_id, NULL, 0); //将进程与共享内存绑定
34 memset(name, 0x00, sizeof(name));
35 strcpy(name, "Jhon");
36 name[4] = '0';
37 for (i = 0; i < 3; i++) //写数据
38 {
39 name[4] += 1;
40 strncpy((smap + i)->name, name, 5);
41 (smap + i)->age = 20 + i;
42 }
1 if (shmdt(smap) == -1) //解除绑定
2 {
3 perror("detach error");
4 return -1;
5 }
43 return 0;
44 }
shm_r.c
6 #include <stdio.h>
7 #include <string.h>
8 #include <sys/ipc.h>
9 #include <sys/shm.h>
10 #include <sys/types.h>
11 #include <unistd.h>
12 typedef struct{
13 char name[8];
14 int age;
15 } Stu;
16 int main()
17 {
18 int shm_id, i;
19 key_t key;
20 Stu *smap;
21 struct shmid_ds buf;
22 key = ftok("/", 0); //获取关键字
23 if (key == -1)
24 {
25 perror("ftok error");
26 return -1;
27 }
28 printf("key=%d\n", key);
29 shm_id = shmget(key, 0, 0); //创建共享内存
30 if (shm_id == -1)
31 {
32 perror("shmget error");
33 return -1;
34 }
35 printf("shm_id=%d\n", shm_id);
36 smap = (Stu*)shmat(shm_id, NULL, 0); //将进程与共享内存绑定
37 for (i = 0; i < 3; i++) //读数据
38 {
39 printf("name:%s\n", (*(smap + i)).name);
40 printf("age :%d\n", (*(smap + i)).age);
41 }
42 if (shmdt(smap) == -1) //解除绑定
43 {
44 perror("detach error");
45 return -1;
46 }
47 shmctl(shm_id, IPC_RMID, &buf); //删除共享内存
48 return 0;
49 }
分别使用如下两条命令编译程序shmserver.c和shmclient.c:
[itheima@localhost ~]$ gcc shm_w.c -o shm_w
[itheima@localhost ~]$ gcc shm_r.c -o shm_r
在终端中运行可执行程序,先执行shm_w创建共享内存,并向共享内存中写入数据;之后使用shm_r从共享内存中读取数据,数据读取完毕之后将共享内存删除。程序执行后终端打印的信息分别如下。
./shm_w //写
key=131074
shm_id=819217
./shm_r //读
key=131074
shm_id=819217
name:Jhon1
age :20
name:Jhon2
age :21
name:Jhon3
age :22
之后再次执行程序shm_r,终端打印的信息如下:
key=131074
shmget error: No such file or directory
由打印结果可知,共享内存区域已在程序shm_r执行结束前被删除。
结合以上打印结果可知,案例6实现成功。
多学一招:struct ipc_perm结构体
观察消息队列、信号量及共享内存机制的函数接口与属性信息结构体,可以发现,它们包含一个相同的结构体,即struct ipc_perm结构体。该结构体同样由内核管理,用于设置Sys V IPC系列通信机制中介质(队列、信号量、共享存储段)的访问权限和权限标识,该结构体的定义如下:
struct ipc_perm {
key_t __key; //键值
uid_t uid; //所有者有效用户id
gid_t gid; //所属组有效组id
uid_t cuid; //创建者有效用户id
gid_t cgid; //创建者有效组id
unsigned short mode; //访问权限
unsigned short __seq; //序列号
};
ipc_perm结构体在Sys V IPC系列通信机制调用各自的get函数(msgget()、semget()、shmget())时创建,创建时除序列号外,其它信息都会被初始化;之后该结构体由内核进行管理,除超级用户外,只有创建该结构体的进程可以通过各自的ctl函数(msgctl()、semctl()、shmctl())修改uid、gid和mode信息,其中mode信息与open()函数中的mode类似,但ipc_perm的mode没有执行权限。