12.2 Linux_进程间通信_共享内存

概述

什么是共享内存:

共享内存又叫内存映射,可以通过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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值