一、概念
在 Linux 中,每个进程都有属于自己的虚拟地址空间,并且还有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射。倘若现在有两个不同的进程,它们各自的虚拟地址通过页表映射到了同一块物理空间,那么这块物理空间就叫做共享内存。
所以说,共享内存就是允许两个或多个不相关的进程共享同一块内存空间,当有一个进程向共享内存中写入了数据,那么这个进程所做的改动将立即影响到可以访问与它同一个共享内存的任何其他进程,这样也就能让这些进程之间可以共享和传递数据。
二、特点
-
共享内存是最快的进程间通信方式。因为在这种方式下,进程之间传递的数据直接保存在内存中,而不是像其他通信方式那样,需要将数据拷贝到内核中,再通过系统调用来传递;(优点)
-
共享内存没有提供任何的同步与互斥机制,所以我们在使用共享内存进行进程间通信时,往往要借助其他的手段(比如信号量)来进行进程间的同步工作;(缺点)
-
共享内存的实现采用了引用计数的原理。当某个进程挂接上了共享存储区,计数器加一;当某个进程脱离了共享存储区,计数器减一,只有当计数器变为零时,才能删除这个共享存储区。
-
在 Linux 中,共享内存有如下限制:
(1)SHMMNI:系统范围内共享内存的最大数量
(2)SHMMIN:单个共享内存的最小大小(字节),默认通常是 1
(3)SHMMAX:单个共享内存的最大大小(字节)
(4)SHMALL:系统范围内所有共享内存的总大小限制(单位是页,通常是4KB)查看方法如下:
方法一:通过ipcs -lm
命令查看
方法二:查看系统内核信息
三、共享内存数据结构
共享内存具有自身特有的数据结构 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:共享内存的键值
(2)size:申请共享内存的字节大小
(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, 4096, IPC_CREAT|IPC_EXCL|0640);
if(shmid == -1) {
perror("shmget");
printf("errno: %d\n", errno);
return -1;
}
printf("shmid: %d\n", shmid);
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, 4096, IPC_CREAT|0640);
if(shmid == -1) {
perror("shmget");
return -1;
}
printf("shmid: %d\n", shmid);
struct shmid_ds buf;
if(shmctl(shmid, IPC_STAT, &buf) == -1) {
perror("shmctl");
return -1;
}
printf("key: %#x\n", buf.shm_perm.__key);
printf("mode: %#o\n", buf.shm_perm.mode);
printf("segsz: %zu\n", buf.shm_segsz);
printf("nattch: %lu\n", buf.shm_nattch);
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 <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define PATHNAME "."
#define PROJ_ID 123
int main() {
//生成IPC键值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key == -1) {
perror("ftok");
return -1;
}
//创建并打开共享内存
int shmid = shmget(key, 4096, IPC_CREAT|IPC_EXCL|0640);
if(shmid == -1) {
perror("shmget");
return -1;
}
printf("Create shm(id:%d) successfully!\n", shmid);
//附加共享内存到当前进程
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if(shmaddr == (void*)-1) {
perror("shmat");
return -1;
}
//发送消息
char msg[32] = "Hello";
strcpy(shmaddr, msg);
printf("Sent message: %s\n", msg);
//分离共享内存
if(shmdt(shmaddr) == -1) {
perror("shmdt");
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 123
int main() {
//生成IPC键值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key == -1) {
perror("ftok");
return -1;
}
//打开共享内存
int shmid = shmget(key, 0, 0);
if(shmid == -1) {
perror("shmget");
return -1;
}
//附加共享内存到当前进程
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if(shmaddr == (void*)-1) {
perror("shmat");
return -1;
}
//接收消息
printf("Received message: %s\n", shmaddr);
//分离共享内存
if(shmdt(shmaddr) == -1) {
perror("shmdt");
return -1;
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return -1;
}
printf("Remove shm(id:%d) successfully!\n", shmid);
return 0;
}
【执行结果】: