http://www.ibm.com/developerworks/cn/aix/library/au-cn-sharemem/html
共享内存是一种很是重要且经常使用的进程间通讯方式,相对于其它IPC机制,因其速度最快、效率最高,被普遍应用于各种软件产品及应用开发中。System V IPC 为UNIX平台上的共享内存应用制定了统一的API标准,从而为在UNIX/Linux平台上进行跨平台开发提供了极大的便利;开发人员基于一套基本相同的源代码,即可开发出同时支持AIX、Solaris、HP-UX、Linux等平台的产品。shell
然而,各个平台对System V 标准的API在实现上各有差别,由此对相关应用开发带来影响,甚至引入难以调试的问题。本文将结合做者在Tivoli产品开发中的实际经验,对这些平台相关的问题,以及具备共性的问题,逐一进行分析,并提出解决方法。编程
回页首数组
System V 进程间通讯(IPC)包括3种机制:消息队列、信号量、共享内存。消息队列和信号量均是内核空间的系统对象,经由它们的数据须要在内核和用户空间进行额外的数据拷贝;而共享内存和访问它的全部应用程序均同处于用户空间,应用进程能够经过地址映射的方式直接读写内存,从而得到很是高的通讯效率。数据结构
System V 为共享内存定义了下列API接口函数:多线程
# include <sys/types.h> # include <sys/ipc.h> # include <sys/shm.h> key_t ftok(const char *pathname, int proj_id); int shmget(key_t key, int size, int shmflg); void* shmat(int shmid, const void *shmaddr, int shmflg); int shmdt(void *shmaddr); int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
ftok | 函数用于生成一个键值:key_t key,该键值将做为共享内存对象的惟一性标识符,并提供给为shmget函数做为其输入参数;ftok 函数的输入参数包括一个文件(或目录)路径名:pathname,以及一个额外的数字:proj_id,其中pathname所指定的文件(或目录)要求必须已经存在,且proj_id不可为0; |
shmget | 函数用于建立(或者获取)一个由key键值指定的共享内存对象,返回该对象的系统标识符:shmid; |
shmat | 函数用于创建调用进程与由标识符shmid指定的共享内存对象之间的链接; |
shmdt | 函数用于断开调用进程与共享内存对象之间的链接; |
shmctl | 函数用于对已建立的共享内存对象进行查询、设值、删除等操做; |
回页首app
根据pathname指定的文件(或目录)名称,以及proj_id参数指定的数字,ftok函数为IPC对象生成一个惟一性的键值。在实际应用中,很容易产生的一个理解是,在proj_id相同的状况下,只要文件(或目录)名称不变,就能够确保ftok返回始终一致的键值。 然而,这个理解并不是彻底正确,有可能给应用开发埋下很隐晦的陷阱。由于ftok的实现存在这样的风险,即在访问同一共享内存的多个进程前后调用ftok函数的时间段中,若是pathname指定的文件(或目录)被删除且从新建立,则文件系统会赋予这个同名文件(或目录)新的i节点信息,因而这些进程所调用的ftok虽然都能正常返回,但获得的键值却并不能保证相同。由此可能形成的后果是,本来这些进程意图访问一个相同的共享内存对象,然而因为它们各自获得的键值不一样,实际上进程指向的共享内存再也不一致;若是这些共享内存都获得建立,则在整个应用运行的过程当中表面上不会报出任何错误,然而经过一个共享内存对象进行数据传输的目的将没法实现。ide
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.
Linux没有提供相似的明确声明,但咱们能够经过下面的简单例程test01.c,获得相同的印证:
#include <stdio.h> #include <sys/ipc.h> void main(int argc, char* argv[]) { if (argc !=2 ) { printf("Usage: %s KeyFile\n e.g. %s /tmp/mykeyfile\n", argv[0], argv[0]); return; } printf("Key generated by ftok: 0x%x\n", ftok(argv[1], 1)); } |
将上述例程在Red Hat Enterprise Linux AS release 4平台上编程成可执行程序test01,而且经过touch命令在 /tmp目录下建立一个新文件mykeyfile,而后为该文件生成键值:
# touch /tmp/mykeyfile # ./test01 /tmp/mykeyfile Key generated by ftok: 0x101000b |
而后,将/tmp/mykeyfile删除,而且经过vi命令从新建立该文件,再次生成键值:
# ./test01 /tmp/mykeyfile Key generated by ftok: 0x1010017 |
咱们能够看到,虽然文件名称都是 /tmp/mykeyfile,并未改变,但因为中间发生了文件删除并从新建立的操做,先后两次所获得的键值已经再也不相同。
避免此类问题最根本的方法,就是采起措施保证pathname所指定的文件(或目录)在共享内存的使用期间不被删除,不要使用有可能被删除的文件;或者干脆直接指定键值,而不借助ftok来获取键值。
AIX系统中,System V各种进程间通讯机制在使用中均存在限制。区别于其它UNIX操做系统对IPC机制的资源配置方式,AIX使用了不一样的方法;在AIX中定义了 IPC 机制的上限, 且是不可配置的。就共享内存机制而言,在4.2.1及以上版本的AIX系统上,存在下列限制:
上述限制对于64位应用不会带来麻烦,由于可供链接的数量已经足够大了;但对于32位应用,却很容易带来意外的问题,由于最大的链接数量只有11个。在某些事件触发的多线程应用中,新的线程不断地为进行事件处理而被建立,这些线程若是都须要去链接特定的共享内存,则极有可能形成该进程链接的共享内存数量超过11个,事实上同时拥有几十个甚至上百个处理线程的应用并很多见。一旦超个这个限制值,则全部后续的处理线程都将没法正常工做,从而致使应用运行失败。
下面的例程test02.c演示了这个问题,为了精简代码,它反复链接的是同一个共享内存对象;实际上,不管所链接的共享内存对象是否相同,该限制制约的是链接次数:
#include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define MAX_ATTACH_NUM 15 void main(int argc, 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("Failed to generate shared memory access key, ERRNO=%d\n", errno); goto MOD_EXIT; } if ( ( mem_id = shmget(mem_key, 256, IPC_CREAT) ) == (-1) ) { printf("Failed to obtain shared memory ID, ERRNO=%d\n", errno); goto MOD_EXIT; } for ( i=1; i<=MAX_ATTACH_NUM; i++ ) { if ( ( mem_addr[i] = (void *)shmat(mem_id, 0, 0) ) == (void *)(-1) ) printf("Failed to attach shared memory, times [%02d], errno:%d\n", i, errno); else printf("Successfully attached shared memory, times [%02d]\n", i); } MOD_EXIT: shmctl(mem_id, IPC_RMID, NULL); } |
在AIX系统上,咱们将其编译为test02,并运行,能够看到以下输出:
Successfully attached shared memory, times [01] Successfully attached shared memory, times [02] Successfully attached shared memory, times [03] Successfully attached shared memory, times [04] Successfully attached shared memory, times [05] Successfully attached shared memory, times [06] Successfully attached shared memory, times [07] Successfully attached shared memory, times [08] Successfully attached shared memory, times [09] Successfully attached shared memory, times [10] Successfully attached shared memory, times [11] Failed to attach shared memory, times [12], errno:24 Failed to attach shared memory, times [13], errno:24 Failed to attach shared memory, times [14], errno:24 Failed to attach shared memory, times [15], errno:24 |
说明超出11个链接以后,全部后续的共享内存链接都将没法创建。错误码24的定义是EMFILE,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_PRIVATE状态,并被标记为"已被删除";直到已有链接所有断开,该共享内存才会最终从系统中消失。
因而,存在这样的一种状态:
此时,若是有其它的进程(好比第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 子命令删除该共享内存,以观察共享内存被执行删除操做以后,在被完全销毁以前是否还能接受链接;
/******* test03.P1.c ********/ #include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int main(int argc, char* argv[]) { key_t mem_key; long mem_id; void* mem_addr; int isAttached = 0; mem_key = ftok("/tmp/mykeyfile", 1); mem_id = shmget(mem_key, 256, IPC_CREAT); if ( ( mem_addr = (void *)shmat(mem_id, 0, 0) ) == (void *)(-1) ) printf("%s, Failed to attach shared memory, errno:%d\n", argv[0], errno); else { isAttached = 1; printf("%s, +.Successfully attached shared memory\n", argv[0]); } /* sleep 10 seconds, to wait test03.P2 to run */ sleep(10); if (isAttached) { // Attention: the following line should be "shmdt((char *)mem_addr);" if on Solaris shmdt((void *)mem_addr); printf("%s, -.Successfully detached shared memory\n", argv[0]); } /* try to attach the shared memory which has been removed! */ if ( ( mem_addr = (void *)shmat(mem_id, 0, 0) ) == (void *)(-1) ) printf("%s, Failed to attach the removed shared memory, errno:%d\n", argv[0], errno); return 0; } /******* test03.P2.c ********/ #include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int main(int argc, char* argv[]) { key_t mem_key; long mem_id; void* mem_addr; int i, isAttached; mem_key = ftok("/tmp/mykeyfile", 1); mem_id = shmget(mem_key, 0, 0); // repeated attaching & detaching for (i=1; i<10; i++) { isAttached = 0; if ( ( mem_addr = (void *)shmat(mem_id, 0, 0) ) == (void *)(-1) ) printf("%s, Failed to attach shared memory, times [%02d], errno:%d\n", argv[0], i, errno); else { isAttached = 1; printf("%s, +.Successfully attached shared memory, times [%02d]\n",argv[0], i); } if (isAttached) { // Attention: the following line should be "shmdt((char *)mem_addr);", if on Solaris shmdt((void *)mem_addr); printf("%s, -.Successfully detached, times [%02d]\n", argv[0], i); } // purposely remove the shared memory at times [5] if (i==5) { shmctl(mem_id, IPC_RMID, NULL); printf("%s, *.Remove executed, times [%02d], errno=%d\n", argv[0], i, errno); } } return 0; } |
上述程序都可在AIX、HP-UX、Linux平台上编译经过;在Solaris平台上只需按注释提示的要求,将shmdt的参数强制为char *类型也可编译经过(第5节中已介绍过)。
将test03.P1.c、test03.P2.c各自编译为可执行程序test03.P一、test03.P2,并经过下面的shell脚本:runtest,运行它们:
#!/bin/sh ./test03.P1& sleep 2 ./test03.P2 |
在Linux平台(Red Hat 8.0)上的运行结果以下:
[root@localhost tmp]# ./runtest ./test03.P1, +.Successfully attached shared memory ./test03.P2, +.Successfully attached shared memory, times [01] ./test03.P2, -.Successfully detached, times [01] ./test03.P2, +.Successfully attached shared memory, times [02] ./test03.P2, -.Successfully detached, times [02] ./test03.P2, +.Successfully attached shared memory, times [03] ./test03.P2, -.Successfully detached, times [03] ./test03.P2, +.Successfully attached shared memory, times [04] ./test03.P2, -.Successfully detached, times [04] ./test03.P2, +.Successfully attached shared memory, times [05] ./test03.P2, -.Successfully detached, times [05] ./test03.P2, *.Remove executed, times [05], errno=0 ./test03.P2, +.Successfully attached shared memory, times [06] ./test03.P2, -.Successfully detached, times [06] ./test03.P2, +.Successfully attached shared memory, times [07] ./test03.P2, -.Successfully detached, times [07] ./test03.P2, +.Successfully attached shared memory, times [08] ./test03.P2, -.Successfully detached, times [08] ./test03.P2, +.Successfully attached shared memory, times [09] ./test03.P2, -.Successfully detached, times [09] [root@localhost tmp]# ./test03.P1, -.Successfully detached shared memory ./test03.P1, Failed to attach the removed shared memory, errno:22 |
根据运行结果,咱们能够看到,在Linux平台上,即使对共享内存执行了删除操做(在第5次链接以后,test03.P2进程调用了shmctl的IPC_RMID删除操做),只要该共享内存依然存在(test03.P1进程保持着链接,所以共享内存不会被当即删除),则它仍然是可链接的(test03.P2进程的第6到第9次链接均是成功的)。
然而,在AIX、HP-UX、Solaris平台上的运行结果却不一样于Linux:
# ./runtest ./test03.P1, +.Successfully attached shared memory ./test03.P2, +.Successfully attached shared memory, times [01] ./test03.P2, -.Successfully detached, times [01] ./test03.P2, +.Successfully attached shared memory, times [02] ./test03.P2, -.Successfully detached, times [02] ./test03.P2, +.Successfully attached shared memory, times [03] ./test03.P2, -.Successfully detached, times [03] ./test03.P2, +.Successfully attached shared memory, times [04] ./test03.P2, -.Successfully detached, times [04] ./test03.P2, +.Successfully attached shared memory, times [05] ./test03.P2, -.Successfully detached, times [05] ./test03.P2, *.Remove executed, times [05], errno=0 ./test03.P2, Failed to attach shared memory, times [06], errno:22 ./test03.P2, Failed to attach shared memory, times [07], errno:22 ./test03.P2, Failed to attach shared memory, times [08], errno:22 ./test03.P2, Failed to attach shared memory, times [09], errno:22 # ./test03.P1, -.Successfully detached shared memory ./test03.P1, Failed to attach the removed shared memory, errno:22 |
根据结果,能够发现,test03.P2进程的第6到第9次链接都是失败的,也就说明,在AIX、HP-UX、Solaris平台上一旦经过shmctl对共享内存进行了删除操做,则该共享内存将不能再接受任何新的链接,即便它依然存在于系统中!
并且,上面的运行结果,也证实了,对共享内存进行了删除操做以后,当已有的链接所有断开,该共享内存将被系统自动销毁(运行结果的最后一行,说明该共享内存已经不存在了)。
本节的目的在于说明,在AIX、HP-UX、Solaris平台上调用shmctl的IPC_RMID删除操做,是存在潜在风险的,须要足够的谨慎。
若是,能够确知,在删除以后不可能再有新的链接,则执行删除操做是安全的;
不然,在删除操做以后如仍有新的链接发生,则这些链接都将失败!
对共享内存的操做,每每是产品或者应用中数据传输的基础,对其可靠性和性能相当重要;并且做为底层的IPC机制,相关代码具备不易调试的特色,由其形成的问题每每关键却不容易解决。
本文从应用实现的角度上,对在UNIX/Linux平台上使用共享内存可能会遇到的问题,进行了全面的介绍和分析,并给出了解决方法或建议,可供相关的应用开发人员参考。