一、概念
我们都知道,在 Linux 中,每个进程都有属于自己的虚拟地址空间,并且还有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射。倘若现在有两个不同的进程,它们各自的虚拟地址通过页表映射到了同一块物理空间,那么这块物理空间就叫做共享内存。
所以说,共享内存就是允许两个或多个不相关的进程共享同一块内存空间,当有一个进程向共享内存中写入了数据,那么这个进程所做的改动将立即影响到可以访问与它同一个共享内存的任何其他进程,这样也就能让这些进程之间可以共享和传递数据。
二、特点
- 共享内存是最快的进程间通信方式。因为在这种方式下,进程之间传递的数据直接保存在内存中,而不是像其他通信方式那样,需要将数据拷贝到内核中,再通过系统调用来传递;(优点)
- 共享内存没有提供任何的同步与互斥机制,所以我们在使用共享内存进行进程间通信时,往往要借助其他的手段(比如信号量)来进行进程间的同步工作;(缺点)
- 共享内存的实现采用了引用计数的原理。当某个进程挂接上了共享存储区,计数器加一;当某个进程脱离了共享存储区,计数器减一,只有当计数器变为零时,才能删除这个共享存储区。
三、共享内存数据结构
共享内存具有自身特有的数据结构 shmid_ds,该结构说明了共享内存的一些状态信息等等,详细信息可参阅文件 /usr/include/linux/shm.h
。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
四、共享内存相关函数
1. shmget
- 【头文件】:
#include <sys/ipc.h>
、#include <sys/shm.h>
- 【函数原型】:
int shmget(key_t key, size_t size, int shmflg);
- 【功能】:创建一个共享内存
- 【参数】:
(1)key:共享内存的键值,由 ftok() 生成
(2)size:申请共享内存的大小 (字节)。在操作系统中,申请内存的最小单位为页,而一页是4k字节,所以为了避免出现内存碎片,一般申请的内存大小为页的整数倍
(3)msgflg:权限标志位,由两部分组成,一部分为IPC对象存取权限(含义同 ipc_perm 中的 mode),另一部分为IPC对象创建模式标志(IPC_CREAT、IPC_EXCL),一般会将这两部分进行|
运算,从而完成对IPC对象创建的管理
- 【返回值】:成功返回一个共享内存标识符(非负整数);失败则返回 -1,并将 errno 设置为错误标识符
【示例】:演示 IPC_CREAT|IPC_EXCL
的使用效果
【代码】:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
int main() {
extern int errno;
int shmid = shmget(0x123, 4*1024, 0644|IPC_CREAT|IPC_EXCL);
if(shmid == -1) {
printf("shmget error!\n");
printf("errno: %d\n", errno);
perror("result");
return -1;
}
printf("shmget success!\n");
return 0;
}
【执行结果】:
【分析】:
从执行结果来看,当我们第一次运行程序时,成功创建了共享内存。但是,当第二次运行程序后,发现创建失败,errno
被置为17,也就是 EEXIST
,错误信息为 File exists
,出现这样的结果是因为我们在第一次程序运行后,该共享内存就已经在系统中存在了,而我们在创建共享内存又使用了 IPC_EXCL
,这才导致 shmget
出错返回。
2. shmctl
- 【头文件】:
#include <sys/ipc.h>
、#include <sys/shm.h>
- 【函数原型】:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 【功能】:获取或设置共享内存的有关信息
- 【参数】:
(1)shmid:共享内存标识符
(2)cmd:对共享内存要采取的动作
取值 说明 IPC_STAT
读取共享内存的 shmid_ds 结构信息,并将其存储在 buf 指定的地址空间中 IPC_SET
设置共享内存的 shmid_ds 结构信息,这些取值来自于 buf 指定地址空间中的参数(需要足够权限) IPC_RMID
删除共享内存 (3)buf:描述共享内存 shmid_ds 数据结构的变量
- 【返回值】:成功时具体的返回值依赖于cmd;失败则返回 -1,并将errno设置为错误标识符
【示例】:演示 IPC_STAT
的使用效果
【代码】:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
int shmid = shmget(0x123, 4*1024, 0644|IPC_CREAT);
if(shmid == -1) {
printf("shmget error!\n");
return -1;
}
struct shmid_ds buf;
if(shmctl(shmid, IPC_STAT, &buf) == -1) {
printf("shmctl error!\n");
return -1;
}
printf("buf.shm_perm.mode = %o\n", buf.shm_perm.mode);
printf("buf.shm_perm.__key = %x\n", buf.shm_perm.__key);
return 0;
}
【执行结果】:
3. shmat
- 【头文件】:
#include <sys/types.h>
、#include <sys/shm.h>
- 【函数原型】:
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 【功能】:将共享内存空间挂接到进程中去
- 【参数】:
(1)shmid:共享内存标识符
(2)shmaddr:指定共享内存的连接地址,一般为 NULL (由系统自动分配地址)
(3)shmflg:访问权限和地址映射条件
取值 说明 0
以读写方式挂接(默认) SHM_RDONLY
以只读方式挂接 SHM_RND
如果 shmaddr 不为 NULL,并且 shmflg 被设置为 SHM_RND,则连接的地址必须是最接近 SHMLBA 整数倍的地址,公式为: shmaddr- (shmaddr % SHMLBA )
- 【返回值】:成功返回共享内存的首地址;失败则返回 (void*) -1,并将errno设置为错误标识符
4. shmdt
- 【头文件】:
#include <sys/types.h>
、#include <sys/shm.h>
- 【函数原型】:
int shmdt(const void *shmaddr);
- 【功能】:将共享内存与当前进程脱离(只是说当前进程与共享内存不再有联系,并没有删除共享内存)
- 【参数】:
shmaddr:shmat() 函数返回的共享内存首地址
- 【返回值】:成功返回 0;失败则返回 -1,并将 errno 设置为错误标识符
五、综合应用
【示例】:利用共享内存实现服务端与客户端通信
【代码】:
/*
* 服务端 server.c
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x1234
int main() {
//创建共享内存键值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
return -1;
}
//创建共享内存
int shmid = shmget(key, 4096, 0644|IPC_CREAT|IPC_EXCL);
if(shmid < 0) {
perror("shmget error");
return -1;
}
//将共享内存挂接到当前进程
char* addr = (char*)shmat(shmid, NULL, 0);
if(addr == (void*)-1) {
perror("shmat error");
return -1;
}
//向共享内存中写入数据
char text[] = "Hello Shared Memory";
size_t i = 0;
for(; i < strlen(text); ++i)
addr[i] = text[i];
addr[i] = '\0';
printf("Write complete!\n");
//将共享内存与当前进程脱离
if(shmdt(addr) < 0) {
perror("shmdt error");
return -1;
}
return 0;
}
/*
* 客户端 client.c
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x1234
int main() {
//创建共享内存键值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
perror("ftok error");
return -1;
}
//打开共享内存
int shmid = shmget(key, 4096, 0644|IPC_CREAT);
if(shmid < 0) {
perror("shmget error");
return -1;
}
//将共享内存挂接到当前进程
char* addr = (char*)shmat(shmid, NULL, 0);
if(addr == (void*)-1) {
perror("shmat error");
return -1;
}
//从共享内存中接收数据
printf("Receive: ");
for(size_t i = 0; addr[i] != '\0'; ++i)
printf("%c", addr[i]);
printf("\n");
//将共享内存与当前进程脱离
if(shmdt(addr) < 0) {
perror("shmdt error");
return -1;
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, NULL) < 0) {
perror("shmctl error");
return -1;
}
return 0;
}
【执行结果】: