进程间通信方式--共享内存Shared Memory

时间:2024-03-21 21:41:51

共享内存


定义


共享内存是进程间通信最简单的方式之一。
共享内存是系统在多进程通信而预留的一块内存区。
共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

当两个程序想相互之间通信时,内存会为这两个程序生成一块公共的内存区域。这块被两个程序共享的内存区域叫做共享内存

因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。

在没有共享内存时,那么一个进程无法访问另一个进程的内存部分,会导致通信失败,我们通过信号量来解决这些问题。但此时每一时刻只能有一个进程(线程)能够访问共享资源。

为了简化共享数据的完整性和避免同时存取数据,内核提供了一种专门存取共享内存资源的机制。这称为互斥体或者mutex对象

不允许两个进程同时向共享内存中写入数据

两个进程通信时的运行顺序是:
写入数据的进程:

  • 获取mutex对象,锁定共享区域;
  • 将要通信的数据写入共享区域;
  • 释放mutex对象
    读取数据的进程:
  • 获取mutex对象,锁定共享区域;
  • 从共享区域中读取数据;
  • 释放mutex对象

共享内存使用


要使用一块共享内存

  • 进程分配共享内存(分配空间)
  • 随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中(绑定地址)
  • 当完成通信之后,所有进程都将脱离共享内存,并且由一个进程释放该共享内存块。(脱离,释放空间)

/proc/sys/kernel/目录下,记录着共享内存的一些限制;
内存的最大字节数shmmax
进程间通信方式--共享内存Shared Memory
系统范围内最大共享内存区标识符数shmmni
进程间通信方式--共享内存Shared Memory


实现共享内存的步骤

  • 创建共享内存,使用shmget函数;
  • 映射共享内存,将这段创建的共享内存映射到具体的进程空间中,使用shmat函数。

共享内存的函数


使用共享内存主要会用到下面几个API:ftok()shmget()shmat()shmdt()shmctl().

#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
int shmdt(const void *shm_addr);
int shmget(key_t key, size_t size, int shmflg);

获取进程ID号—ftok()


ftok函数来获取一个ID号,在IPC中经常用key_t的值来创建或者打开信号量、消息队列和共享内存。

key_t ftok(const char *pathname, int proj_id);
参数 描述
pathname 一定要在系统中存在并且进程能够访问的
proj_id 一个1-255之间的一个整数值,典型的值是一个ASCII值

当成功执行的时候,一个key_t值将会被返回,否则-1被返回。我们可以使用strerror(errno)来确定具体的错误信息。

也可以直接定义一个key,不用ftok获得:

#define IPCKEY 0x366378

创建共享内存—shmget()


进程通过调用shmget(Shared Memory GET,获取共享内存)来分配一个共享内存块。

int shmget(key_t key ,int size,int shmflg)
参数 描述
key 一个用来标识共享内存块的键值
size 指定了所申请的内存块的大小
shmflg 操作共享内存的标识

返回值:如果成功,返回共享内存表示符shmid ,如果失败,返回-1。

  • 参数1:key—标识共享内存块的键值
    进程通过指定key值来获取对共享内存的访问。但是,其他进程都可以将这个key值作为自己分配共享内存的键值,会出现冲突。
    用特殊常量IPC_PRIVATE作为键值,会创建一个全新的共享内存块。
    key标识共享内存的键值:0/IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget将创建一块新的共享内存;如果key的取值为0,而参数中又设置了IPC_PRIVATE这个标志,则同样会创建一块新的共享内存。

  • 参数2:size指定了所申请的内存块的大小。
    因为这些内存块是以页面为单位进行分配的,实际分配的内存块大小将被扩大到页面大小的整数倍。

  • 参数3:shmflg是一组标志,通过特定常量的按位或操作来shmget。这些特定常量包括:
    IPC_CREAT:这个标志表示应创建一个新的共享内存块。通过指定这个标志,我们可以创建一个具有指定键值的新共享内存块。

    IPC_EXCL:这个标志只能与IPC_CREAT同时使用。当指定这个标志的时候,如果已有一个具有这个键值的共享内存块存在,则shmget会调用失败。也就是说,这个标志将使线程获得一个“独有”的共享内存块。如果没有指定这个标志而系统中存在一个具有相同键值的共享内存块,shmget会返回这个已经建立的共享内存块,而不是重新创建一个。

    模式标志:这个值由9个位组成,分别表示属主、属组和其它用户对该内存块的访问权限。

映射共享内存—shmat


shmat()是用来允许本进程访问一块共享内存的函数,将这个内存区映射到本进程的虚拟地址空间。

int shmat(int shmid,char *shmaddr,int flag)
参数 描述
shmid 共享内存的ID,shmget的返回
shmadd 共享内存的起始地址
flag 本进程对该内存的操作模式

成功时,这个函数返回共享内存的起始地址。失败时返回-1。

  • 参数1:shmid
    共享内存的ID,是shmget函数返回的共享内存标识符;
  • 参数2:* shmadd
    共享内存的起始地址,shmadd的值是NULL,则Linux会自动选择一个合适的地址用于映射指向您希望用于映射该共享内存块的进程内存地址;如果不是NULL,内核会把共享内存映射到期望的位置。
  • 参数3:flag
    一个标志位,flag的取值有以下选项:
    SHM_RND表示第二个参数指定的地址应被向下靠拢到内存页面大小的整数倍。如果您不指定这个标志,需要在调用shmat的时候手工将共享内存块的大小按页面大小对齐。
    SHM_RDONLY表示这个内存块将仅允许读取操作而禁止写入。 如果这个函数调用成功则会返回绑定的共享内存块对应的地址。通过 fork函数创建的子进程同时继承这些共享内存块。

共享内存解除映射—shmdt


将共享内存区从自己的内存段删除
shmdt(Shared Memory Detach,脱离共享内存块)

int shmdt(char *shmaddr)
参数 描述
shmaddr 那块共享内存的起始地址

成功时返回0。失败时返回-1。

当一个进程不再需要共享内存时,需要调用shmdt函数,将进程和该共享内存脱离,将shmat函数但会的地址传递给这个函数。如果这是最后一个使用该共享内存的进程,脱离后,这个内存块会被删除。进程调用exit函数或者exec族函数都会自动脱离共享内存块。

控制释放—shmctl()


删除共享内存
调用 shmctl(”Shared Memory Control”,控制共享内存)函数会返回一个共享内存块的相关信息。同时 shmctl 允许程序修改这些信息。

int  shmctl( int shmid , int cmd , struct shmid_ds *buf );
参数 描述
shmid 共享内存的ID,shmget的返回
cmd 控制命令
buf 一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定

cmd的取值:

cmd 描述
IPC_STAT 得到共享内存的状态
IPC_SET 改变共享内存的状态
IPC_RMID 删除共享内存

返回值: 成功:0 失败:-1

要获取一个共享内存块的相关信息。则为该函数传递 IPC_STAT 作为第二个参数,同时传递一个指向一个 struct shmid_ds对象的指针作为第三个参数。

要删除一个共享内存块,则应将IPC_RMID 作为第二个参数,而将 NULL 作为第三个参数。当最后一个绑定该共享内存块的进程与其脱离时,该共享内存块将被删除。

应当在结束使用每个共享内存块的时候都使用 shmctl进行释放,以防止超过系统所允许的共享内存块的总数限制。调用exitexec 会使进程脱离共享内存块,但不会删除这个内存块。

简单应用


实现:在server创建共享内存,在client传输大写字符‘Z’-‘A’到共享内存中,server接收并打印,最后在server删除共享内存

common.h

#ifndef __COMMON_H__
#define __COMMON_H__


#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

#define PATHNAME "yanfei"
#define PROJ_ID 0x6666


int CreateShm(int size);   //创建共享内存
int DestroyShm(int shmid);   //删除共享内存
int GetShm(int size);        //已存在共享内存,获取shmid
#endif

common.c

#include "common.h"

static int CommonShm(int size, int flags)
{
	key_t key;
	int shmid = 0;
	
	if((key = ftok(PATHNAME, PROJ_ID)) < 0){
		perror("ftok");
		return -1;
	}
	
	if((shmid = shmget(key,size,flags)) < 0){
		perror("shnget");
		return -2;
	}
	
	return shmid;
}

//创建共享内存
int CreateShm(int size)
{
	return CommonShm(size, IPC_CREAT | IPC_EXCL | 0666);
}

//删除共享内存
int DestroyShm(int shmid)
{
	if(shmctl(shmid, IPC_RMID, NULL) < 0){
		perror("shmctl");
		return -1;
	}
	return 0;
}


//已存在共享内存,获取shmid
int GetShm(int size)
{
	return CommonShm(size, IPC_CREAT);
}

client.c

#include "common.h"

int main()
{
	int shmid;
	char *addr;
	int i = 0;
	
	
	shmid = GetShm(4096);
	
	//映射共享内存
	addr = shmat(shmid, NULL, 0);
	
	while(i < 26){
		addr[i] = 'Z' - i;
		i++;
		addr[i] = 0;
		sleep(1);
	}
	
	shmdt(addr);
	
	return 0;
	
}

server.c

#include "common.h"

int main()
{
	int shmid;
	char *addr;
	int i = 0;
	
	//创建共享内存
	shmid = CreateShm(4096);
	
	//映射共享内存
	addr = shmat(shmid, NULL, 0);
	
	while(i < 26){
		printf("client:%s\n", addr);
		sleep(1);
	}
	
	shmdt(addr);
	DestroyShm(shmid);
	return 0;
	
}

Makefile

.PHONY:all
all:server client
 
client:client.c common.c
	gcc -o [email protected] $^
server:server.c common.c
	gcc -o [email protected] $^
 
.PHONY:clean
clean:
	rm -f client server

程序运行结果

进程间通信方式--共享内存Shared Memory