Chinaunix首页 | 论坛 | 博客
  • 博客访问: 4558537
  • 博文数量: 1214
  • 博客积分: 13195
  • 博客等级: 上将
  • 技术积分: 9105
  • 用 户 组: 普通用户
  • 注册时间: 2007-01-19 14:41
个人简介

C++,python,热爱算法和机器学习

文章分类

全部博文(1214)

文章存档

2021年(13)

2020年(49)

2019年(14)

2018年(27)

2017年(69)

2016年(100)

2015年(106)

2014年(240)

2013年(5)

2012年(193)

2011年(155)

2010年(93)

2009年(62)

2008年(51)

2007年(37)

分类: 系统运维

2011-02-23 17:22:50

文章来源:http://www.ibm.com/developerworks/cn/aix/library/au-cn-sharemem/index.html
不完全转载

共享内存是一种非常重要且常用的进程间通信方式,相对于其它IPC机制,因其速度最快、效率最高,被广泛应用于各类软件产品及应用开发中。 System V IPC 为UNIX平台上的共享内存应用制定了统一的API标准,从而为在UNIX/Linux平台上进行跨平台开发提供了极大的便利;开发人员基于一套基本相同 的源代码,便可开发出同时支持AIX、Solaris、HP-UX、Linux等平台的产品。

然而,各个平台对System V 标准的API在实现上各有差异,由此对相关应用开发带来影响,甚至引入难以调试的问题。本文将结合作者在Tivoli产品开发中的实际经验,对这些平台相关的问题,以及具有共性的问题,逐一进行分析,并提出解决方法。

根据pathname指定的文件(或目录)名称,以及proj_id参数指定的数字,ftok函数为IPC对象生成一个唯一性的键值。在实际应用 中,很容易产生的一个理解是,在proj_id相同的情况下,只要文件(或目录)名称不变,就可以确保ftok返回始终一致的键值。 然而,这个理解并非完全正确,有可能给应用开发埋下很隐晦的陷阱。因为ftok的实现存在这样的风险,即在访问同一共享内存的多个进程先后调用ftok函 数的时间段中,如果pathname指定的文件(或目录)被删除且重新创建,则文件系统会赋予这个同名文件(或目录)新的i节点信息,于是这些进程所调用 的ftok虽然都能正常返回,但得到的键值却并不能保证相同。由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到 的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对 象进行数据传输的目的将无法实现。

AIX、Solaris、HP-UX均明确指出,key文件被删除并重建后,不保证通过ftok得到的键值不变,比如AIX上ftok的man帮助信息即声明:

Attention: If the Path parameter of the ftok subroutine names a file that has been removed while keys still refer to it, the ftok subroutine returns an error. If that file is then re-created, the ftok subroutine will probably return a key different from the original one.


AIX系统中,System V各类进程间通信机制在使用中均存在限制。区别于其它UNIX操作系统对IPC机制的资源配置方式,AIX使用了不同的方法;在AIX中定义了 IPC 机制的上限, 且是不可配置的。就共享内存机制而言,在4.2.1及以上版本的AIX系统上,存在下列限制:

  • 对于64位进程,同一进程可连接最多268435456个共享内存段;
  • 对于32位进程,同一进程可连接最多11个共享内存段,除非使用扩展的shmat;

上述限制对于64位应用不会带来麻烦,因为可供连接的数量已经足够大了;但对于32位应用,却很容易带来意外的问题,因为最大的连接数量只有11 个。在某些事件触发的多线程应用中,新的线程不断地为进行事件处理而被创建,这些线程如果都需要去连接特定的共享内存,则极有可能造成该进程连接的共享内 存数量超过11个,事实上同时拥有几十个甚至上百个处理线程的应用并不少见。一旦超个这个限制值,则所有后续的处理线程都将无法正常工作,从而导致应用运 行失败。

AIX给予的解释是:

The number of shared memory segments attached to the calling process exceeds the system-imposed limit。

解决这个问题的方法是,使用扩展的shmat;具体而言就是,在运行相关应用之前(确切地说,是在共享内存被创建之前),首先在shell中设置EXTSHM环境变量,通过它扩展shmat,对于源代码本身无需作任何修改:


export EXTSHM=ON

值得注意的是,虽然设置环境变量,在程序中也可通过setenv函数来做到,比如在程序的开始,加入下列代码:


setenv("EXTSHM", "ON", 1);

但实践证明这样的方法在解决这个问题上是无效的;也就是说唯一可行的办法,就是在shell中设置EXTSHM环境变量,而非在程序中。

在AIX上配置32位DB2实例时,也要求确保将环境变量 EXTSHM 设为 ON,这是运行 Warehouse Manager 和 Query Patroller 之前必需的操作:


export EXTSHM=ON db2set DB2ENVLIST=EXTSHM db2start

其原因即来自我们刚刚介绍的AIX中32位应用连接共享内存时,存在最大连接数限制。这个问题同样普遍存在于AIX平台上Oracle等软件产品中。

在HP-UX平台上,如果同时运行32位应用和64位应用,而且它们访问的是一个相同的共享内存区,则会遇到兼容性问题。

在HP-UX中,应用程序设置IPC_CREAT标志调用shmget,所创建的共享内存区,只可被同类型的应用所访问;即32位应用程序所创建的共享内存区只可被其它的32位应用程序访问,同样地,64位应用程序所创建的共享内存区只可被其它的64位应用程序访问。

如果,32位应用企图访问一个由64位应用创建的共享内存区,则会在调用shmget时失败,得到EINVAL错误码,其解释是:

A shared memory identifier exists for key but is in 64-bit address space and the process performing the request has been compiled as a 32-bit executable.

解决这一问题的方法是,当64位应用创建共享内存时,合并IPC_CREAT标志,同时给定IPC_SHARE32标志:


shmget(mem_key, size, 0666 | IPC_CREAT | IPC_SHARE32)

对于32位应用,没有设定IPC_SHARE32标志的要求,但设置该标志并不会带来任何问题,也就是说无论应用程序将被编译为32位还是64位模式,都可采用如上相同的代码; 并且由此解决32位应用和64位应用在共享内存访问上的兼容性问题。

在HP-UX上,应用进程对同一个共享内存区的连接次数被限制为最多1次;区别于上面第3节所介绍的AIX上的连接数限制,HP-UX并未对指向 不同共享内存区的连接数设置上限,也就是说,运行在HP-UX上的应用进程可以同时连接很多个不同的共享内存区,但对于同一个共享内存区,最多只允许连接 1次;否则,shmat调用将失败,返回错误码EINVAL,在shmat的man帮助中,对该错误码有下列解释:

shmid is not a valid shared memory identifier, (possibly because the shared memory segment was already removed using shmctl(2) with IPC_RMID), or the calling process is already attached to shmid.

这个限制会对多线程应用带来无法避免的问题,只要一个应用进程中有超过1个以上的线程企图连接同一个共享内存区,则都将以失败而告终。

解决这个问题,需要修改应用程序设计,使应用进程具备对同一共享内存的多线程访问能力。相对于前述问题的解决方法,解决这个问题的方法要复杂一些。

作为可供参考的方法之一,以下介绍的逻辑可以很好地解决这个问题:

基本思路是,对于每一个共享内存区,应用进程首次连接上之后,将其键值(ftok的返回值)、系统标识符(shmid,shmget调用的返回 值)和访问地址(即shmat调用的返回值)保存下来,以这个进程的全局数组或者链表的形式留下记录。在任何对共享内存的连接操作之前,程序都将先行检索 这个记录列表,根据键值和标志符去匹配希望访问的共享内存,如果找到匹配记录,则从记录中直接读取访问地址,而无需再次调用shmat函数,从而解决这一 问题;如果没有找到匹配目标,则调用shmat建立连接,并且为新连接上来的共享内存添加一个新记录。

记录条目的数据结构,可定义为如下形式:


typedef struct _Shared_Memory_Record { key_t mem_key; // key generated by ftok() int mem_id; // id returned by shmget() void* mem_addr; // access address returned by shmat() int nattach; // times of attachment } Shared_Memory_Record;

其中,nattach成员的作用是,记录当前对该共享内存区的连接数目;每一次打开共享内存的操作都将对其进行递增,而每一次关闭共享内存的操作将其递减,直到nattach的数值降到0,则对该共享内存区调用shmdt进行真正的断开连接。

打开共享内存的逻辑流程可参考如下图一:



图一

关闭共享内存的逻辑流程可参考如下图二:



图二

Solaris系统中的shmdt调用,在原型上与System V标准有所不同,


Default int shmdt(char *shmaddr);

即形参shmaddr的数据类型在Solaris上是char *,而System V定义的是void * 类型; 实际上Solaris上shmdt调用遵循的函数原型规范是SVID-v4之前的标准;以Linux系统为例,libc4和libc5 采用的是char * 类型的形参,而遵循SVID-v4及后续标准的glibc2及其更新版本,均改为采用void * 类型的形参。

如果仍在代码中采用System V的标准原型,就会在Solaris上编译代码时造成编译错误;比如:


Error: Formal argument 1 of type char* in call to shmdt(char*) is being passed void*.

解决方法是,引入一个条件编译宏,在编译平台是Solaris时,采用char * 类型的形参, 而对其它平台,均仍采用System V标准的void * 类型形参,比如:


#ifdef _SOLARIS_SHARED_MEMORY shmdt((char *)mem_addr); #else shmdt((void *)mem_addr); #endif

当进程断开与共享内存区的连接后,一般通过如下代码删除该共享内存:


shmctl(mem_id, IPC_RMID, NULL);

从HP-UX上shmctl函数的man帮助,我们可以看到对IPC_RMID操作的说明:

IPC_RMID Remove the shared memory identifier specified by shmid from the system and destroy the shared memory segment and data structure associated with it. If the segment is attached to one or more processes, then the segment key is changed to IPC_PRIVATE and the segment is marked removed. The segment disappears when the last attached process detaches it.

其它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.P2进程的第6到第9次连接都是失败的,也就说明,在AIX、HP-UX、Solaris平台上一旦通过shmctl对共享内存进行了删除操作,则该共享内存将不能再接受任何新的连接,即使它依然存在于系统中!

而且,上面的运行结果,也证明了,对共享内存进行了删除操作之后,当已有的连接全部断开,该共享内存将被系统自动销毁(运行结果的最后一行,说明该共享内存已经不存在了)。

本节的目的在于说明,在AIX、HP-UX、Solaris平台上调用shmctl的IPC_RMID删除操作,是存在潜在风险的,需要足够的谨慎。

如果,可以确知,在删除之后不可能再有新的连接,则执行删除操作是安全的;

否则,在删除操作之后如仍有新的连接发生,则这些连接都将失败!

对共享内存的操作,往往是产品或者应用中数据传输的基础,对其可靠性和性能至关重要;而且作为底层的IPC机制,相关代码具有不易调试的特点,由其造成的问题往往关键却不容易解决。

本文从应用实现的角度上,对在UNIX/Linux平台上使用共享内存可能会遇到的问题,进行了全面的介绍和分析,并给出了解决方法或建议,可供相关的应用开发人员参考。


刘 新华,IBM 中国软件开发中心软件工程师。参与过 IBM Rational、Tivoli 等产品开发,最近从事 IBM Tivoli OMEGAMON XE for Message Transaction Tracking 的产品开发工作,对 Linux 内核技术、以及实时和嵌入式应用也有浓厚的兴趣。


阅读(1150) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~