进程间通信方式–共享内存
共享内存允许两个或多个进程访问同一块内存,就如同malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
共享内存
当一个程序加载进内存后,它就被分成叫作页的块。总之,当一个程序想和另外一个程序通信的时候,那内存将会为这两个程序生成一块公共的内存区域。这块被两个进程分享的内存区域叫做共享内存。
在各种进程间通信方式中“共享内存”具有最高的效率,因为所有进程共享同一块内存。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
同步问题
因为系统内核没有对访问共享内存进行同步,所以必须自己提供同步措施。解决这些问题的常用方法是通过使用信号量进行同步。为了简化共享数据的完整性和避免同时存取数据,内核提供了一种专门存取共享内存资源的机制。这称为互斥体或者mutex对象。
例如,在数据被写入之前不允许进程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据等。
当一个进程想和另外一个进程通信的时候,它将按以下顺序运行:
获取mutex对象,锁定共享区域。
将要通信的数据写入共享区域。
释放mutex对象。
当一个进程从这个区域读数据时,它将重复同样的步骤,只是将第二步变成读取。
内存模型
要使用一块共享内存:
进程必须首先分配它;
随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中;
当完成通信之后,所有进程都将脱离共享内存,并且由一个进程释放该共享内存块。
地址映射
每个进程都会维护一个从物理内存地址到虚拟内存页面之间的映射关系----页表。尽管每个进程都有自己的内存地址,不同的进程可以同时将一个内存页面映射到自己的虚拟地址空间中,从而达到共享内存的目的。
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。
一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过仍然应该通过调用 getpagesize 获取这个值。
用于共享内存的函数
(1)ftok()函数:获得一个ID号
在IPC中,我们经常用key_t的值来创建或者打开信号量,共享内存和消息队列。
//pathname:路径名
//一个1-255之间的一个整数值,典型的值是一个ASCII值。
key_t ftok(const char *pathname, int proj_id);
1
2
3
它可以根据传入路径及id自动生成一个key,你可以在后续的shmget()调用中使用这个key用做共享内存的标识,不同进程间使用同一共享内存必须知道这个key的。当然,你也完全可以自己定义一个key来标识共享内存以避免路径变化时不同进程生成的key发生不一致的坑。
当成功执行的时候,一个key_t值将会被返回,否则-1被返回。我们可以使用strerror(errno)来确定具体的错误信息。
(2)shmget()函数:创建共享内存
//key:一个用来标识共享内存块的键值
//size:指定了所申请的内存块的大小
//shmflg:操作共享内存的标识
int shmget(key_t key ,int size,int shmflg);
1
2
3
4
key:用来标识共享内存块的键值。
彼此无关的进程可以通过指定同一个键以获取对同一个共享内存块的访问。不幸的是,其它程序也可能挑选了同样的特定值作为自己分配共享内存的键值,从而产生冲突。
key标识共享内存的键值:0/IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget将创建一块新的共享内存;如果key的取值为0,而参数中又设置了IPC_PRIVATE这个标志,则同样会创建一块新的共享内存。
size:指定了所申请的内存块的大小。
因为这些内存块是以页面为单位进行分配的,实际分配的内存块大小将被扩大到页面大小的整数倍。
shmflg:一组标志,通过特定常量的按位或操作来shmget。
这些常量包括:
IPC_CREAT:这个标志表示应创建一个新的共享内存块。通过指定这个标志,我们可以创建一个具有指定键值的新共享内存块。
IPC_EXCL:这个标志只能与 IPC_CREAT 同时使用。当指定这个标志的时候,如果已有一个具有这个键值的共享内存块存在,则shmget会调用失败。也就是说,这个标志将使线程获得一个“独有”的共享内存块。如果没有指定这个标志而系统中存在一个具有相同键值的共享内存块,shmget会返回这个已经建立的共享内存块,而不是重新创建一个。
(3)shmat():映射共享内存
shmat()是用来允许本进程访问一块共享内存,将这个内存区映射到本进程的虚拟地址空间。如果这个函数调用成功,则会返回绑定的共享内存块对应的地址。失败时返回-1。通过 fork 函数创建的子进程同时继承这些共享内存块。
int shmat(int shmid,char *shmaddr,int flag)
1
shmid:那块共享内存的ID,是shmget函数返回的共享存储标识符。
shmaddr:是共享内存的起始地址,如果shmaddr为NULL,内核会自动把共享内存映射到进程的一个合适的地址上;如果shmaddr不为NULL,内核会把共享内存映像到shmaddr指定的位置。所以一般把shmaddr设为NULL。
shmflag:是本进程对该内存的操作模式。
SHM_RND:表示第二个参数指定的地址应被向下靠拢到内存页面大小的整数倍。如果您不指定这个标志,您将不得不在调用shmat的时候手工将共享内存块的大小按页面大小对齐。
SHM_RDONLY:表示这个内存块将仅允许读取操作而禁止写入。
这里存在一个坑:如果当前进程多次调用shmat(),并不会出现任何错误,得到的结果反而是在当前进程的虚拟内存地址空间出现多个共享内存地址映射,最终可能导致应用程序的地址空间资源耗尽,同时也可能使共享内存的引用最终无法得到正常的释放。
(4)shmdt():共享内存解除映射(Shared Memory Detach,脱离共享内存块)
//shmaddr:调用shmat返回的地址
int shmdt(char *shmaddr)
1
2
如果当释放这个内存块的进程是最后一个使用该内存块的进程,则这个内存块将被删除。对 exit 或任何exec族函数的调用都会自动使进程脱离共享内存块。
(5)shmctl():控制释放(控制对这个共享内存的使用)
int shmctl( int shmid , int cmd , struct shmid_ds *buf );
1
shmid:共享内存块的标识。
cmd:控制命令。
buf:一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定。
要获取一个共享内存块的相关信息,则为该函数传递 IPC_STAT 作为第二个参数,同时传递一个指向一个 struct shmid_ds 对象的指针作为第三个参数。
要删除一个共享内存块,则应将 IPC_RMID 作为第二个参数,而将 NULL 作为第三个参数。当最后一个绑定该共享内存块的进程与其脱离时,该共享内存块将被删除。
程序中应当在结束使用每个共享内存块的时候都使用 shmctl 进行释放,以防止超过系统所允许的共享内存块的总数限制。调用 exit 和 exec 会使进程脱离共享内存块,但不会删除这个内存块。