概述
什么是共享内存:
共享内存又叫内存映射,可以通过mmap()映射普通文件。
实际上就是将磁盘中的一个文件映射到内存的一个缓冲区中去,这样进程就可以直接将这块空间当作普通内存来访问,不需要再使用I/O中的read/write去访问这个文件。
映射之后,内存中读写数据就是在文件中读写数据。
共享内存使用方法:
1、open打开一个文件
2、mmap创建共享内存映射,注意mmap权限要<=open时的权限
3、直接按照内存方式访问共享内存。
共享内存分配的原理:
内存是按页进行分配的,一页的大小为4K。假设文件大小为1K,但它实际所分配的空间是一页,即4K。但可操作的空间是1K。
当对1K的文件申请1K的共享内存时,分配的映射空间大小实际为4K,即:分配的映射空间大小为4K的整数倍。同理,申请2K的共享内存时,分配的映射空间大小也为4K;申请5K的共享内存时,分配的映射空间大小就变成了8K。
按照上述情况进行分配时,在代码中允许写入的映射区地址范围为0~4K,但是只有0~1K的空间可以对文件内容产生影响,在1K~4K空间进行写入数据不会报错,但也不会对文件产生影响,但申请5K共享内存后,访问4K~5K的空间会产生总线错误报错,因为文件空间只有0~4K。
下图是文件大小为5K,实际分配8K空间,在mmap申请5K的示意图:
相关函数
1、创建共享内存映射
mmap的参数在内核中的示意图:
mmap将指定文件(fd)的指定位置(off~off+len)的空间,映射到指定内存(add)中并返回这块内存的首地址(void*返回值)。
函数声明如下:
void *mmap(void *addr,
size_t length,
int prot,
int flags,
int fd,
off_t offset);
返回值:成功返回映射区的首地址,失败返回MAP_FAILED
addr:指定的内存映射地址,NULL代表自动分配
length:映射空间大小,单位字节,从offset参数开始计算。
注意:length会自动与4K进行补齐,如:length=1K,实际分配4K
prot:共享内存的访问权限,多个权限之间可用 "按位或 |" 连接
注意:prot权限应该 <= open时的权限
权限 | 含义 |
PROT_READ | 可读 |
PROT_WRITE | 可写 |
PROT_EXEC | 可执行 |
PROT_NONE | 不可访问 |
flags:共享内存的属性,进程间通信时写入MAP_SHARED,代表映射内存允许共享。
fd:要进行映射的文件的文件描述符
offset:要进行映射的文件的偏移量,写0代表从头部开始映射。
注意:offset值为4K的整数倍,因为内存按页进行分配,页的大小为4K
2、共享内存读写数据
//写入数据
void *memcpy(void *dest, const void *src, size_t n);
//读取数据,读取数据就是直接访问内存
printf("%s\n",(char*)addr);
dest:内存首地址
src:写入数据首地址
n:写入数据的大小
3、释放内存映射
int munmap(void *addr, size_t length);
返回值:成功返回0,失败返回-1
addr:mmap的返回值
length:mmap开辟的内存大小,与mmap写入相同的参数即可
实验代码
1、单工数据传输
实验现象:A进程不断的写入数据'A',B进程每隔1s读取一下共享内存中的内容。
A.c代码如下:
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_PATH "./mmap"
int main(){
int fd;
void* mmap_addr = NULL;
int i=0;
char buf[100] = {0};
//打开文件
if((fd=open(FILE_PATH,O_RDWR)) < 0){
perror("open");
return -1;
}
//创建共享内存映射
if((mmap_addr = mmap(NULL,4*1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)) == MAP_FAILED){
perror("mmap");
return -1;
}
memset(mmap_addr,0,lseek(fd,0,SEEK_END));//清空缓冲区
close(fd);//创建共享内存映射后可以关闭文件描述符
//进程间通信
while(1){
memcpy(mmap_addr+i,"A",strlen("A"));
i++;
sleep(1);
}
return 0;
}
B.c代码如下:
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_PATH "./mmap"
int main(){
int fd;
void* mmap_addr = NULL;
int i=0;
char buf[100] = {0};
//打开文件
if((fd=open(FILE_PATH,O_RDWR)) < 0){
perror("open");
return -1;
}
//创建共享内存映射
if((mmap_addr = mmap(NULL,4*1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)) == MAP_FAILED){
perror("mmap");
return -1;
}
close(fd);//创建共享内存映射后可以关闭文件描述符
//进程间通信
while(1){
printf("read:%s\n",(char*)mmap_addr);
sleep(1);
}
return 0;
}
代码运行结果如下:
2、AB进程互传数据
实验现象:A发送开始信号后,AB进程开始互传数据
A.c代码如下:
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#define FILE_PATH "./mmap"
int main(){
int fd;
void* mmap_addr = NULL;
int i=0;
char buf[100] = {0};
sem_t* sem_mmap;
//打开文件
if((fd=open(FILE_PATH,O_RDWR)) < 0){
perror("open");
return -1;
}
//创建共享内存映射
if((mmap_addr = mmap(NULL,4*1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)) == MAP_FAILED){
perror("mmap");
return -1;
}
memset(mmap_addr,lseek(fd,0,SEEK_END),strlen(mmap_addr));//清空缓冲区
close(fd);//创建共享内存映射后可以关闭文件描述符
//创建信号量
if((sem_mmap = sem_open("sem_mmap",O_CREAT,0666,1)) == SEM_FAILED){
perror("sem_open");
return -1;
}
//进程间通信
memcpy(mmap_addr,"A Start SIG",strlen("A Start SIG"));
while(1){
sem_wait(sem_mmap);
if(*(char*)mmap_addr == 'B'){
//读出B进程写入的内容
printf("A read:%s\n",(char*)mmap_addr+strlen("B"));//读取数据,不读取数据来源标号
memset(mmap_addr,0,strlen(mmap_addr));//清空缓冲区
//写入新数据
memcpy(mmap_addr,"A",strlen("A"));//数据来源标号
sprintf(buf,"A_Data:%d",i++); //新数据
memcpy(mmap_addr+strlen("A"),buf,strlen(buf));
}
sem_post(sem_mmap);
}
return 0;
}
B.c代码如下:
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#define FILE_PATH "./mmap"
int main(){
int fd;
void* mmap_addr = NULL;
int i=0;
char buf[100] = {0};
sem_t* sem_mmap;
//打开文件
if((fd=open(FILE_PATH,O_RDWR)) < 0){
perror("open");
return -1;
}
//创建共享内存映射
if((mmap_addr = mmap(NULL,4*1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)) == MAP_FAILED){
perror("mmap");
return -1;
}
//创建信号量
if((sem_mmap = sem_open("sem_mmap",O_CREAT,0666,1)) == SEM_FAILED){
perror("sem_open");
return -1;
}
close(fd);//创建共享内存映射后可以关闭文件描述符
//进程间通信
while(1){
sem_wait(sem_mmap);
if(*(char*)mmap_addr == 'A'){
//读出B进程写入的内容
printf("B read:%s\n",(char*)mmap_addr+strlen("A"));
memset(mmap_addr,0,strlen(mmap_addr));//清空缓冲区
//写入新数据
memcpy(mmap_addr,"B",strlen("B"));//数据来源标号
sprintf(buf,"B get A data,B data is %d",i++); //新数据
memcpy(mmap_addr+strlen("B"),buf,strlen(buf));
sleep(1);
}
sem_post(sem_mmap);
}
return 0;
}
共享内存注意事项
1、共享内存创建隐含读操作:
当mmap创建共享内存后,会自动的将要进行映射的文件的内容读取到映射区。
2、总线错误报错原因:
原因1:
当用于映射的文件大小为0,且指定非0大小的映射区时,会报错总线错误。
解决方法:将空文件中加一个空格,使得文件不是空文件即可。
原因2:
当映射的文件的大小<映射区的大小时,这时不进行报错。但如果访问空间超出了页的范围,则会报错总线错误。
3、非法参数错误报错原因:
原因1:mmap传入的length值为0
原因2:mmap传入的offset值不为4K的整数倍
4、写入字符不全原因:
用于映射的文件大小为A,指定映射区大小为B,当A<B时,只会写入A大小的数据,即:最多写入的数据大小是文件的大小。
5、映射区建立后,便可关闭文件:
在mmap之后就可用关闭进行映射的文件,这时对共享内存进行写入依旧可以修改磁盘文件的内容。
匿名映射
匿名映射不需要文件,但只能用于血缘关系进程之间的通信。
mmap的flags参数写入MAP_SHARED|MAP_ANONYMOUS,fd参数写入-1
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
int main(){
pid_t pid;
int i=0;
void* mmap_addr = NULL;
//创建共享内存映射
if((mmap_addr = mmap(NULL,4*1024,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0)) == MAP_FAILED){
perror("mmap");
return -1;
}
if((pid = fork())<0){
perror("fork");
return -1;
}else if(pid == 0){
while(1){
memcpy(mmap_addr+i,"A",strlen("A"));
i++;
sleep(1);
}
}else{
while(1){
printf("%s\n",(char*)mmap_addr);
sleep(1);
}
}
return 0;
}
systemV共享内存
使用步骤:
- 生成key
- 创建/打开共享内存
- 映射共享内存
- 读写共享内存
- 撤销共享内存
- 删除共享内存
共享内存相关命令:
ipcs 查看共享内存、消息队列、信号灯
ipcrm -m <shmid>:删除指定的共享内存
1、生成key
key_t ftok(const char *pathname, int proj_id);
返回值:成功返回key,失败返回-1
pathname:文件路径
proj_id:用于生成key的数字,范围1~255
该函数可以将pathname的节点号与proj_id进行结合,生成一个整数key,能够确保key不重复。
2、创建/打开共享内存
int shmget(key_t key, size_t size, int shmflg);
返回值:成功返回共享内存的id,失败返回EOF
key:和共享内存关联的key值,由ftok生成或写入IPC_PRIVATE
size_t:共享内存大小,单位字节
shmflg:标志位,写入IPC_CREAT|0666,代表创建共享内存权限可读可写
3、映射共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
返回值:成功返回映射后的地址,失败返回(void*)-1
shmid:共享内存的id
shmaddr:映射内存空间,NULL代表由系统自动分配
shmflg:标志位,0代表可读可写,SHM_RDONLY代表只读
4、撤销共享内存
int shmdt(const void *shmaddr);
返回值:成功返回0,失败返回EOF
shmaddr:shmat返回的地址
进程结束时,会自动撤销共享内存。
注意:撤销之后只代表映射的内存空间不存在了,但共享内存还在。
5、删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存的id
cmd:要执行的操作,写入IPC_RMID
buf:保存或设置共享内存属性的地址,写入NULL即可
注意:共享内存在创建后,不再使用一定要删除,否则会导致内存泄漏。
示例代码
写端代码如下:
#include <string.h>
#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(){
key_t key;
int shmid;
void* shmaddr = NULL;
//1.生成key
if((key=ftok(".",1))<0){
perror("ftok");
return -1;
}
perror("ftok");
printf("key = %d\n",key);
//2.创建/打开共享内存
if((shmid=shmget(key,100,IPC_CREAT|0666))<0){
perror("shmget");
return -1;
}
printf("shmid = %d\n",shmid);
//3.映射共享内存
if((shmaddr=shmat(shmid,NULL,0)) == (void*)-1){
perror("shmat");
return -1;
}
//4.读写共享内存
strcpy(shmaddr,"hello");
//memcpy(shmaddr,"hello",strlen("hello"));
//5.撤销共享内存
shmdt(shmaddr);
//6.删除共享内存
return 0;
}
读端代码如下:
#include <string.h>
#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(){
key_t key;
int shmid;
void* shmaddr = NULL;
//1.生成key
if((key=ftok(".",1))<0){
perror("ftok");
return -1;
}
printf("key = %d\n",key);
//2.创建/打开共享内存
if((shmid=shmget(key,100,0666))<0){//以可读可写方式打开共享内存
perror("shmget");
return -1;
}
printf("shmid = %d\n",shmid);
//3.映射共享内存
if((shmaddr=shmat(shmid,NULL,0)) == (void*)-1){
perror("shmat");
return -1;
}
//4.读写共享内存
printf("read:%s\n",(char*)shmaddr);
//5.撤销共享内存
shmdt(shmaddr);
//6.删除共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}