共享内存使用需要注意的问题.docx
- 文档编号:12121379
- 上传时间:2023-04-17
- 格式:DOCX
- 页数:15
- 大小:25.16KB
共享内存使用需要注意的问题.docx
《共享内存使用需要注意的问题.docx》由会员分享,可在线阅读,更多相关《共享内存使用需要注意的问题.docx(15页珍藏版)》请在冰豆网上搜索。
共享内存使用需要注意的问题
UNIX共享内存应用中的问题及解决方法
简介
共享内存是一种非常重要且常用的进程间通信方式,相对于其它IPC机制,因其速度最快、效率最高,被广泛应用于各类软件产品及应用开发中。
SystemVIPC为Unix平台上的共享内存应用制定了统一的API标准,从而为在UNIX/Linux平台上进行跨平台开发提供了极大的便利;开发人员基于一套基本相同的源代码,便可开发出同时支持AIX、Solaris、HP-UX、Linux等平台的产品。
然而,各个平台对SystemV标准的API在实现上各有差异,由此对相关应用开发带来影响,甚至引入难以调试的问题。
本文将结合作者在Tivoli产品开发中的实际经验,对这些平台相关的问题,以及具有共性的问题,逐一进行分析,并提出解决方法。
1.SystemV共享内存概述
SystemV进程间通信(IPC)包括3种机制:
消息队列、信号量、共享内存。
消息队列和信号量均是内核空间的系统对象,经由它们的数据需要在内核和用户空间进行额外的数据拷贝;而共享内存和访问它的所有应用程序均同处于用户空间,应用进程可以通过地址映射的方式直接读写内存,从而获得非常高的通信效率。
SystemV为共享内存定义了下列API接口函数:
#include
#include
#include
key_t ftok(constchar*pathname,intproj_id);
int shmget(key_tkey,intsize,intshmflg);
void* shmat(intshmid,constvoid*shmaddr,intshmflg);
int shmdt(void*shmaddr);
int shmctl(intshmid,intcmd,structshmid_ds*buf);
ftok
函数用于生成一个键值:
key_tkey,该键值将作为共享内存对象的唯一性标识符,并提供给为shmget函数作为其输入参数;ftok函数的输入参数包括一个文件(或目录)路径名:
pathname,以及一个额外的数字:
proj_id,其中pathname所指定的文件(或目录)要求必须已经存在,且proj_id不可为0;
shmget
函数用于创建(或者获取)一个由key键值指定的共享内存对象,返回该对象的系统标识符:
shmid;
shmat
函数用于建立调用进程与由标识符shmid指定的共享内存对象之间的连接;
shmdt
函数用于断开调用进程与共享内存对象之间的连接;
shmctl
函数用于对已创建的共享内存对象进行查询、设值、删除等操作;
2.ftok的陷阱
根据pathname指定的文件(或目录)名称,以及proj_id参数指定的数字,ftok函数为IPC对象生成一个唯一性的键值。
在实际应用中,很容易产生的一个理解是,在proj_id相同的情况下,只要文件(或目录)名称不变,就可以确保ftok返回始终一致的键值。
然而,这个理解并非完全正确,有可能给应用开发埋下很隐晦的陷阱。
因为ftok的实现存在这样的风险,即在访问同一共享内存的多个进程先后调用ftok函数的时间段中,如果pathname指定的文件(或目录)被删除且重新创建,则文件系统会赋予这个同名文件(或目录)新的i节点信息,于是这些进程所调用的ftok虽然都能正常返回,但得到的键值却并不能保证相同。
由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对象进行数据传输的目的将无法实现。
AIX、Solaris、HP-UX均明确指出,key文件被删除并重建后,不保证通过ftok得到的键值不变,比如AIX上ftok的man帮助信息即声明:
Attention:
IfthePathparameteroftheftoksubroutinenamesafilethathasbeenremovedwhilekeysstillrefertoit,theftoksubroutinereturnsanerror.Ifthatfileisthenre-created,theftoksubroutinewillprobablyreturnakeydifferentfromtheoriginalone.
Linux没有提供类似的明确声明,但我们可以通过下面的简单例程test01.c,得到相同的印证:
#include
#include
voidmain(intargc,char*argv[])
{
if(argc!
=2){
printf("Usage:
%sKeyFilene.g.%s/tmp/mykeyfilen",argv[0],argv[0]);
return;
}
printf("Keygeneratedbyftok:
0x%xn",ftok(argv[1],1));
}
将上述例程在RedHatEnterpriseLinuxASrelease4平台上编程成可执行程序test01,并且通过touch命令在/tmp目录下创建一个新文件mykeyfile,然后为该文件生成键值:
#touch /tmp/mykeyfile
#./test01/tmp/mykeyfile
Keygeneratedbyftok:
0x101000b
然后,将/tmp/mykeyfile删除,并且通过vi命令重新创建该文件,再次生成键值:
#./test01/tmp/mykeyfile
Keygeneratedbyftok:
0x1010017
我们可以看到,虽然文件名称都是/tmp/mykeyfile,并未改变,但由于中间发生了文件删除并重新创建的操作,前后两次所得到的键值已经不再相同。
避免此类问题最根本的方法,就是采取措施保证pathname所指定的文件(或目录)在共享内存的使用期间不被删除,不要使用有可能被删除的文件;或者干脆直接指定键值,而不借助ftok来获取键值。
3.AIX中shmat的问题
AIX系统中,SystemV各类进程间通信机制在使用中均存在限制。
区别于其它UNIX操作系统对IPC机制的资源配置方式,AIX使用了不同的方法;在AIX中定义了IPC机制的上限,且是不可配置的。
就共享内存机制而言,在4.2.1及以上版本的AIX系统上,存在下列限制:
对于64位进程,同一进程可连接最多268435456个共享内存段;
对于32位进程,同一进程可连接最多11个共享内存段,除非使用扩展的shmat;
上述限制对于64位应用不会带来麻烦,因为可供连接的数量已经足够大了;但对于32位应用,却很容易带来意外的问题,因为最大的连接数量只有11个。
在某些事件触发的多线程应用中,新的线程不断地为进行事件处理而被创建,这些线程如果都需要去连接特定的共享内存,则极有可能造成该进程连接的共享内存数量超过11个,事实上同时拥有几十个甚至上百个处理线程的应用并不少见。
一旦超个这个限制值,则所有后续的处理线程都将无法正常工作,从而导致应用运行失败。
下面的例程test02.c演示了这个问题,为了精简代码,它反复连接的是同一个共享内存对象;实际上,无论所连接的共享内存对象是否相同,该限制制约的是连接次数:
#include
#include
#include
#include
#include
#define MAX_ATTACH_NUM 15
voidmain(intargc,char*argv[])
{
key_t mem_key;
long mem_id;
void* mem_addr[MAX_ATTACH_NUM];
int i;
if((mem_key=ftok("/tmp/mykeyfile",1))==(key_t)(-1)) {
printf("Failedtogeneratesharedmemoryaccesskey,ERRNO=%dn",
errno);
gotoMOD_EXIT;
}
if((mem_id=shmget(mem_key,256,IPC_CREAT))==(-1)) {
printf("FailedtoobtainsharedmemoryID,ERRNO=%dn",errno);
gotoMOD_EXIT;
}
for(i=1;i<=MAX_ATTACH_NUM;i++) {
if((mem_addr[i]=(void*)shmat(mem_id,0,0))==(void*)(-1))
printf("Failedtoattachsharedmemory,times[%02d],errno:
%dn",i,
errno);
else
printf("Successfullyattachedsharedmemory,times[%02d]n",i);
}
MOD_EXIT:
shmctl(mem_id,IPC_RMID,NULL);
}
在AIX系统上,我们将其编译为test02,并运行,可以看到如下输出:
Successfullyattachedsharedmemory,times[01]
Successfullyattachedsharedmemory,times[02]
Successfullyattachedsharedmemory,times[03]
Successfullyattachedsharedmemory,times[04]
Successfullyattachedsharedmemory,times[05]
Successfullyattachedsharedmemory,times[06]
Successfullyattachedsharedmemory,times[07]
Successfullyattachedsharedmemory,times[08]
Successfullyattachedsharedmemory,times[09]
Successfullyattachedsharedmemory,times[10]
Successfullyattachedsharedmemory,times[11]
Failedtoattachsharedmemory,times[12],errno:
24
Failedtoattachsharedmemory,times[13],errno:
24
Failedtoattachsharedmemory,times[14],errno:
24
Failedtoattachsharedmemory,times[15],errno:
24
说明超出11个连接之后,所有后续的共享内存连接都将无法建立。
错误码24的定义是EMFILE,AIX给予的解释是:
Thenumberofsharedmemorysegmentsattachedtothecallingprocessexceedsthesystem-imposedlimit。
解决这个问题的方法是,使用扩展的shmat;具体而言就是,在运行相关应用之前(确切地说,是在共享内存被创建之前),首先在shell中设置EXTSHM环境变量,通过它扩展shmat,对于源代码本身无需作任何修改:
exportEXTSHM=ON
值得注意的是,虽然设置环境变量,在程序中也可通过setenv函数来做到,比如在程序的开始,加入下列代码:
setenv("EXTSHM","ON",1);
但实践证明这样的方法在解决这个问题上是无效的;也就是说唯一可行的办法,就是在shell中设置EXTSHM环境变量,而非在程序中。
在AIX上配置32位DB2实例时,也要求确保将环境变量EXTSHM设为ON,这是运行WarehouseManager和QueryPatroller之前必需的操作:
exportEXTSHM=ON
db2setDB2ENVLIST=EXTSHM
db2start
其原因即来自我们刚刚介绍的AIX中32位应用连接共享内存时,存在最大连接数限制。
这个问题同样普遍存在于AIX平台上Oracle等软件产品中。
4.HP-UX中shmget和shmat的问题
4.132位和64位应用兼容问题
在HP-UX平台上,如果同时运行32位应用和64位应用,而且它们访问的是一个相同的共享内存区,则会遇到兼容性问题。
在HP-UX中,应用程序设置IPC_CREAT标志调用shmget,所创建的共享内存区,只可被同类型的应用所访问;即32位应用程序所创建的共享内存区只可被其它的32位应用程序访问,同样地,64位应用程序所创建的共享内存区只可被其它的64位应用程序访问。
如果,32位应用企图访问一个由64位应用创建的共享内存区,则会在调用shmget时失败,得到EINVAL错误码,其解释是:
AsharedmemoryidentifIErexistsforkeybutisin64-bitaddressspaceandtheprocessperformingtherequesthasbeencompiledasa32-bitexecutable.
解决这一问题的方法是,当64位应用创建共享内存时,合并IPC_CREAT标志,同时给定IPC_SHARE32标志:
shmget(mem_key,size,0666|IPC_CREAT|IPC_SHARE32)
对于32位应用,没有设定IPC_SHARE32标志的要求,但设置该标志并不会带来任何问题,也就是说无论应用程序将被编译为32位还是64位模式,都可采用如上相同的代码;并且由此解决32位应用和64位应用在共享内存访问上的兼容性问题。
4.2对同一共享内存的连接数限制
在HP-UX上,应用进程对同一个共享内存区的连接次数被限制为最多1次;区别于上面第3节所介绍的AIX上的连接数限制,HP-UX并未对指向不同共享内存区的连接数设置上限,也就是说,运行在HP-UX上的应用进程可以同时连接很多个不同的共享内存区,但对于同一个共享内存区,最多只允许连接1次;否则,shmat调用将失败,返回错误码EINVAL,在shmat的man帮助中,对该错误码有下列解释:
shmidisnotavalidsharedmemoryidentifier,(possiblybecausethesharedmemorysegmentwasalreadyremovedusingshmctl
(2)withIPC_RMID),orthecallingprocessisalreadyattachedtoshmid.
这个限制会对多线程应用带来无法避免的问题,只要一个应用进程中有超过1个以上的线程企图连接同一个共享内存区,则都将以失败而告终。
解决这个问题,需要修改应用程序设计,使应用进程具备对同一共享内存的多线程访问能力。
相对于前述问题的解决方法,解决这个问题的方法要复杂一些。
作为可供参考的方法之一,以下介绍的逻辑可以很好地解决这个问题:
基本思路是,对于每一个共享内存区,应用进程首次连接上之后,将其键值(ftok的返回值)、系统标识符(shmid,shmget调用的返回值)和访问地址(即shmat调用的返回值)保存下来,以这个进程的全局数组或者链表的形式留下记录。
在任何对共享内存的连接操作之前,程序都将先行检索这个记录列表,根据键值和标志符去匹配希望访问的共享内存,如果找到匹配记录,则从记录中直接读取访问地址,而无需再次调用shmat函数,从而解决这一问题;如果没有找到匹配目标,则调用shmat建立连接,并且为新连接上来的共享内存添加一个新记录。
记录条目的数据结构,可定义为如下形式:
typedefstruct _Shared_Memory_Record
{
key_t mem_key; //keygeneratedbyftok()
int mem_id; //id returnedbyshmget()
void* mem_addr; //accessaddressreturnedbyshmat()
int nattach; //timesofattachment
}Shared_Memory_Record;
其中,nattach成员的作用是,记录当前对该共享内存区的连接数目;每一次打开共享内存的操作都将对其进行递增,而每一次关闭共享内存的操作将其递减,直到nattach的数值降到0,则对该共享内存区调用shmdt进行真正的断开连接。
打开共享内存的逻辑流程可参考如下图一:
图一
关闭共享内存的逻辑流程可参考如下图二:
图二
5.Solaris中的shmdt函数原型问题
Solaris系统中的shmdt调用,在原型上与SystemV标准有所不同,
Default
intshmdt(char*shmaddr);
即形参shmaddr的数据类型在Solaris上是char*,而SystemV定义的是void*类型;实际上Solaris上shmdt调用遵循的函数原型规范是SVID-v4之前的标准;以Linux系统为例,libc4和libc5采用的是char*类型的形参,而遵循SVID-v4及后续标准的glibc2及其更新版本,均改为采用void*类型的形参。
如果仍在代码中采用SystemV的标准原型,就会在Solaris上编译代码时造成编译错误;比如:
Error:
Formalargument1oftypechar*incalltoshmdt(char*)
isbeingpassedvoid*.
解决方法是,引入一个条件编译宏,在编译平台是Solaris时,采用char*类型的形参,而对其它平台,均仍采用SystemV标准的void*类型形参,比如:
#ifdef _SOLARIS_SHARED_MEMORY
shmdt((char*)mem_addr);
#else
shmdt((void*)mem_addr);
#endif
6.通过shmctl删除共享内存的风险
当进程断开与共享内存区的连接后,一般通过如下代码删除该共享内存:
shmctl(mem_id,IPC_RMID,NULL);
从HP-UX上shmctl函数的man帮助,我们可以看到对IPC_RMID操作的说明:
IPC_RMIDRemovethesharedmemoryidentifierspecifiedbyshmidfromthesystemanddestroythesharedmemorysegmentanddatastructureassociatedwithit.Ifthesegmentisattachedtooneormoreprocesses,thenthesegmentkeyischangedtoIPC_PRIVATEandthesegmentismarkedremoved.Thesegmentdisappearswhenthelastattachedprocessdetachesit.
其它UNIX平台也有类似的说明。
关于shmctl的IPC_RMID操作,其使用特点可简述如下:
如果共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构;
如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为"已被删除";直到已有连接全部断开,该共享内存才会最终从系统中消失。
于是,存在这样的一种状态:
N个进程(进程1至进程N)已经与某共享内存区连接;
进程1已完成对此共享内存的操作,断开连接后,调用shmctl的IPC_RMID子命令,企图删除该共享内存;
由于进程2至进程N仍保持与该共享内存的连接,因此在它们全部断开连接之前,这个共享内存区毫无疑问地会依然存在。
此时,如果有其它的进程(比如第N+1号进程)想建立对这个共享内存的连接,是否能够成功呢?
类似的状态,在Windows上同样存在,只是程序借助的API有所不同,比如通过CreateFileMapping函数创建共享内存,通过MapViewOfFile函数建立连接,通过UnmapViewOfFile函数断开连接,通过CloseHandle函数删除共享内存等。
在Windows上,对此问题的回答是肯定的;也就是说,只要共享内存依然存在,则进程总是可以建立对它的连接,而无论之前是否有进程对其执行过删除操作。
然而,对于包括AIX、Solaris、HP-UX等在内的UNIX平台,答案却是否定的!
这也正是本节所讨论的使用shmctl中的风险所在;通过以下test03.P1.c和test03.P2.c两个例程,我们可以很直观地得到答案:
test03.P1.c:
创建共享内存,并建立连接,保持10秒后(在此期间,test03.P2将反复连接、并删除该共享内存),断开连接,并最后再次尝试连接以验证该共享内存是否已被真正删除;
test03.P2.c:
反复连接由test03.P1创建的共享内存,并在期间通过shmctl的IPC_RMID子命令删除该共享
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 共享 内存 使用 需要 注意 问题