基本概念
共享内存就是将内存进行共享,它允许多个不相关的进程访问同一块逻辑内存。因此,共享内存是效率最高的一种进程间(IPC
)通信机制,它可以在多个进程之间共享和传递数据,进程间需要共享的数据被放在共享内存区域,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去,因此所有进程都可以访问共享内存中的地址,就好像访问 malloc
分配的内存一样。
但是,这种共享的内存需要进程自己去维护好,如同步、互斥等工作,比如当进程1在读取共享内存的数据时, 进程2却修改了共享内存中的数据,那么必然会造成数据的混乱,进程1读取到的数据就是错误的,因此,共享内存是属于临界资源,在某一时刻最多只能有一个进程对其操作(读/写数据), 共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制。
共享内存的思想非常简单,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的,但是可以通过某些方式, 使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这样的效果就相当于多个进程的虚拟内存空间部分重叠在一起, 如下图所示
当进程1向共享内存写入数据后,共享内存的数据就变化了,那么进程2就能立即读取到变化了的数据, 而这中间并未经过内核的拷贝,因此效率极高。
优缺点
先来说优点,使用共享内存进行进程间的通信非常方便,而且函数的接口也简单,数据的共享使进程间的数据不用传送, 而是直接访问内存,加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的 血缘 关系,系统中的任意进程都可以对共享内存进行读写操作。
再来说缺点,共享内存没有提供同步的机制。我们在使用共享内存进行进程间通信时,需要借助其他的手段(如信号量、互斥量等)来进行进程间的同步工作。
相关函数
创建共享内存函数
内核提供了 shmget()
函数的创建或获取一个共享内存对象,并返回共享内存标识符。函数原型如下:
int shmget(key_t key, size_t size, int shmflg);
参数说明:
-
key:标识共享内存的键值,可以有以下取值:
0 或
IPC_PRIVATE
。当key
的取值为IPC_PRIVATE
,则函数shmget()
创建一块新的共享内存;如果key
的取值为0,而参数shmflg
中设置了IPC_PRIVATE
这个标志,则同样将创建一块新的共享内存。大于0的32位整数:视参数
shmflg
来确定操作。 -
size:要创建共享内存的大小,所有的内存分配操作都是以页为单位的,所以即使只申请只有一个字节的内存, 内存也会分配整整一页。
-
shmflg:表示创建的共享内存的模式标志参数,在真正使用时需要与
IPC
对象存取权限(如0600)进行“|”运算来确定共享内存的存取权限。msgflg
有多种情况:IPC_CREAT:如果内核中不存在关键字与
key
相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符。IPC_EXCL:如果内核中不存在键值与
key
相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错。SHM_HUGETLB:使用大页面来分配共享内存,所谓的大页面指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4
KB
)更大的分页,以减少缺页中断。Linux
内核支持以2MB
作为物理页面分页的基本单位。SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。
返回值:shmget()
函数的返回值是共享内存的 ID
。
映射函数
shmat()
函数就是把共享内存区对象映射到调用进程的地址空间。函数原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
-
shmid:共享内存
ID
,通常是由shmget()
函数返回的。 -
shmaddr:如果不为
NULL
,则系统会根据shmaddr
来选择一个合适的内存区域, 如果为NULL
,则系统会自动选择一个合适的虚拟内存空间地址去映射共享内存。 -
shmflg:操作共享内存的方式:
SHM_RDONLY:以只读方式映射共享内存。
SHM_REMAP:重新映射,此时
shmaddr
不能为NULL
。NULLSHM:自动选择比
shmaddr
小的最大页对齐地址。
返回值: 调用成功后返回共享内存的起始地址。
共享内存的映射有以下注意的要点:
共享内存只能以只读或者可读写方式映射,无法以只写方式映射。
shmat()
第二个参数 shmaddr
一般都设为 NULL
,让系统自动找寻合适的地址。但当其确实不为空时,那么要求 SHM_RND
在 shmflg
必须被设置,这样的话系统将会选择比 shmaddr
小而又最大的页对齐地址(即为 SHMLBA
的整数倍)作为共享内存区域的起始地址。如果没有设置 SHM_RND
,那么 shmaddr
必须是严格的页对齐地址。
解除映射函数
shmdt()
函数与 shmat()
函数相反,是用来解除进程与共享内存之间的映射的,在解除映射后, 该进程不能再访问这个共享内存。函数原型:
int shmdt(const void *shmaddr);
参数说明:
- shmaddr:映射的共享内存的起始地址。
shmdt()
函数调用成功返回0,如果出错则返回-1,并且将错误原因存于 error
中。
获取或设置属性函数
shmctl()
用于获取或者设置共享内存的相关属性。函数原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
-
shmid:共享内存标识符。
-
cmd:函数功能的控制命令,其取值如下:
IPC_STAT:获取属性信息,放置到
buf
中。
IPC_SET:设置属性信息为buf
指向的内容。
IPC_RMID:删除这该共享内存。
IPC_INFO:获得关于共享内存的系统限制值信息。
SHM_INFO:获得系统为共享内存消耗的资源信息。
SHM_STAT:与IPC_STAT
具有相同的功能,但shmid
为该SHM
在内核中记录所有SHM
信息的数组的下标, 因此通过迭代所有的下标可以获得系统中所有SHM
的相关信息。
SHM_LOCK:禁止系统将该SHM
交换至swap
分区。
SHM_UNLOCK:允许系统将该SHM
交换至swap
分。 -
buf:共享内存属性信息结构体指针,设置或者获取信息都通过该结构体,
shmid_ds
结构如下:
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; /* 创建者PID */
pid_t shm_lpid; /* 后一次映射或解除映射者PID */
shmatt_t shm_nattch; /* 映射该SHM的进程个数 */
...
};
其中权限信息结构体如下
struct ipc_perm {
key_t __key; /* 该共享内存的键值key */
uid_t uid; /* 所有者的有效UID */
gid_t gid; /* 所有者的有效GID */
uid_t cuid; /* 创建者的有效UID */
gid_t cgid; /* 创建者的有效GID */
unsigned short mode; /* 读写权限 + SHM_DEST + SHM_LOCKED 标记 */
unsigned short __seq; /* 序列号 */
};
使用示例
先来说说,使用共享内存的一般步骤
- 创建或获取共享内存
ID
- 将共享内存映射至本进程虚拟内存空间的某个区域
- 当不再使用时,解除映射关系
- 当没有进程再需要这块共享内存时,删除它
下面来看个具体的示例,实现两个进程,一个是共享内存写进程,另一个是共享内存读进程,使用信号量来控制临界区。
共享内存写进程 write.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#define SHM_SIZE 1024
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void P(int sem_id) {
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = -1;
sb.sem_flg = SEM_UNDO;
semop(sem_id, &sb, 1);
}
void V(int sem_id) {
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
semop(sem_id, &sb, 1);
}
int main() {
int shmid, semid;
key_t key;
char *shm, *msg;
union semun sem_union;
// 创建共享内存段
key = ftok(".", 'R');
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 连接共享内存
shm = shmat(shmid, NULL, 0);
if (shm == (char *) -1) {
perror("shmat");
exit(1);
}
// 创建信号量
semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
exit(1);
}
// 初始化信号量
sem_union.val = 1;
if (semctl(semid, 0, SETVAL, sem_union) == -1) {
perror("semctl");
exit(1);
}
int counter = 1;
while (1) {
// 等待读进程准备好
P(semid);
// 写入消息到共享内存
printf("Message %d", counter);
sprintf(shm, "Message %d", counter);
counter++;
// 释放信号量
V(semid);
sleep(1); // 等待1秒再写入下一条消息
}
// 断开共享内存连接
shmdt(shm);
return 0;
}
共享内存读进程 read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#define SHM_SIZE 1024
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void P(int sem_id) {
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = -1;
sb.sem_flg = SEM_UNDO;
semop(sem_id, &sb, 1);
}
void V(int sem_id) {
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
semop(sem_id, &sb, 1);
}
int main() {
int shmid, semid;
key_t key;
char *shm;
union semun sem_union;
// 获取共享内存段
key = ftok(".", 'R');
shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 连接共享内存
shm = shmat(shmid, NULL, 0);
if (shm == (char *) -1) {
perror("shmat");
exit(1);
}
// 获取信号量
semid = semget(key, 1, 0666);
if (semid == -1) {
perror("semget");
exit(1);
}
while (1) {
// 等待写进程准备好
P(semid);
// 读取共享内存中的消息并输出
printf("Received: %s\n", shm);
// 释放信号量
V(semid);
sleep(1); // 等待1秒再读取下一条消息
}
// 断开共享内存连接
shmdt(shm);
return 0;
}
接着,编译下2个进程
gcc -o write write.c
gcc -o read read.c
完成后,在两个终端中相继启动2个可执行文件
./write
./read
可以得到结果
写进程将会不断地向共享内存中写入不同的消息,而读进程将会从共享内存中读取并输出这些消息。两个进程之间通过信号量进行同步,使得写和读操作交替进行。值得注意的是,共享内存和信号量是一种较底层的进程间通信机制,需要开发者自行管理同步和互斥。在实际应用中,还需要考虑异常处理、错误处理以及进程退出等情况,以确保程序的稳定性和正确性。