进程间通信之共享内存知识共享
1 共享内存的需要性
共享内存是允许两个或更多的进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的IPC。一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。由于共享内存在多个进程之间提供了一种有效的数据传递方式,所以它的使用对进程间通信有很大的帮助。
2 共享内存的创建与获得
与其它两种IPC机制一样,进程在使用共享内存区域以前,必须创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数shmget完成,其定义如下:
int shmget(ket_t key , size_tsize , int shmflg)
这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。
在IPC的通信模式下,不管是使用消息队列还是共享内存,甚至是信号量,每个IPC的对象(object)都有唯一的名字,称为“键”(key)。通过“键”,进程能够识别所用的对象。“键”与IPC对象的关系就如同文件名称之于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够共用一个文件。而在IPC的通讯模式下,通过“键”的使用也使得一个IPC对象能为多个进程所共用。
shmflg主要和一些标志有关。其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当.
IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
如果单独使用IPC_CREAT,shmget()函数要么返回一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。如果将IPC_CREAT和IPC_EXCL标志一起使用,shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。
3 共享内存的连接
在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射到进程的虚拟地址空间,然后才能使用该共享内存区域。映射由函数shmat完成,其定义如下:
char *shmat(int shm_id,char*shm_addr,int shmflg);
参数shm_id是shmget返回的共享内存标识符。
shm_addr是该共享内存区域在进程的虚拟地址空间对应的虚拟地址,如果shm_addr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域;否则,就用shmaddr作为映射的虚拟地址。
4 共享内存的分离
当进程不再需要共享内存的时候就要将其从当前队列中分离,但是将共享内存分离并不是删除它,只是使用共享内存的当前进程不再可用。分离共享内存由函数shmdt完成,其定义如下:
int shmdt(char *shmaddr);
其中shmaddr是进程要分离的共享内存开始的虚拟地址
5 共享内存的控制
控制操作包括获得共享内存对象的状态,设置共享内存对象的参数,将共享内存对象在内存中锁定和释放,释放共享内存对象资源等。其函数定义如下:
int shmctl(int shm_id , intcommand , struct shmid_ds *buf)
第一个参数shm_id是shmget返回共享内存的标识符
第二个参数command是要采取的动作它可以有三个值,
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET : 如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID: 删除共享内存
第三个参数buf是指向包含共享内存模式和访问权限的结构指针
6 使用共享内存需要注意的问题
无论我们是建立还是打开一个已经存在的共享内存我们得到的都不是共享内存的实际物理地址,而是我们当前进程被映射的虚拟地址。多个进程都把共享内存区域映射到自己的虚拟地址空间,这些进程可以直接访问该共享内存区域,从而可以通过该区域进行通信。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。
共享内存与进程的映射关系如图所示:
从图中我们可以看出每个进程连接共享内存都对应着自己的虚拟地址,所以在使用时千万不要把它当成实际的物理地址存储或者跨进程引用。
6.1 问题实例
pallas互联网兴趣分析系统就需要使用共享内存实现数据共享,其中有一个共享内存队列是用来存放其它共享内存的地址,此时在多进程使用队列的时候就会出现问题。假设上图进程A创建了共享内存区域并且它对应的虚拟地址为0X3000~0X5000,此时我将这段地址存放在一个队列当中,我在进程B中同样打开此共享内存想通过队列(队列也是共享内存)的内容(即存放的地址)访问此共享内存,此时就可能出现无效的地址等一些错误,错误的原因就是在进程B中对应的共享内存虚拟地址和进程A不一样,在B进程中的地址是0X5000~0X7000。
6.2 问题实例有好的解决办法吗?
通过查找各种资料分析暂时看来还没有好的解决办法,所以不同进程之间不可能用地址队列(队列也是共享内存)去存放另一个共享内存的地址,就算在一个进程中对队列进行了入队那么存放的也是虚拟地址。一个可行的方法,本人认为地址队列可以存放共享内存的相对地址(即偏移量)在其他进程使用队列时得到的也是相对地址,然后再根据本进程的虚拟地址加偏移量实现对共享内存的访问。但是这样实现起来比较复杂各个进程的工作量自然就增加了对其他模块的透明度也不好。
6.3 Pallas系统本身避免了问题的产生
由于我们的系统采用的是父进程创建多个子进程,而子进程是父进程的资源拷贝,所以我们可以在子进程创建之前将所有的共享内存创建并连接,那么子进程创建之后也相当于与所有的共享内存建立了连接并且虚拟内存地址也是相同的,所以就不存在上面实例中所提到的问题。但并不意味着上面的问题得到了很好的解决。
7 参考资料
LINUX 程序设计第3版 人民邮电出版社
UNIX环境高级编程第2版 人民邮电出版社
http://www.diybl.com/course/6_system/linux/Linuxjs/2008929/146631.html
http://topic.youkuaiyun.com/u/20090428/10/79fd9da9-2e16-4a46-943a-9fb3dde2a183.html