LINUX共享内存使用常见陷阱与分析

所谓共享内存就是使得多个进程能够访问同一块内存空间,是最快的可用IPC形 式。是针对其余通讯机制运行效率较低而设计的。每每与其它通讯机制,如信号量结合使用,来达到进程间的同步及互斥。其余进程能把同一段共享内存段“链接 到”他们本身的地址空间里去。全部进程都能访问共享内存中的地址。若是一个进程向这段共享内存写了数据,所作的改动会即时被有访问同一段共享内存的其余进 程看到。共享内存的使用大大下降了在大规模数据处理过程当中内存的消耗,可是共享内存的使用中有不少的陷阱,一不注意就很容易致使程序崩溃。linux

 

l         超过共享内存的大小限制?web

    在一个linux服务器上,共享内存的整体大小是有限制的,这个大小经过SHMMAX参数来定义(以字节为单位),您能够经过执行如下命令来肯定 SHMMAX 的值:安全

 

# cat /proc/sys/kernel/shmmax服务器

 

若是机器上建立的共享内存的总共大小超出了这个限制,在程序中使用标准错误perror可能会出现如下的信息:数据结构

 

unable to attach to shared memory函数

 

 

解决方法:spa

1、设置 SHMMAX操作系统

SHMMAX 的默认值是 32MB 。通常使用下列方法之一种将 SHMMAX 参数设为 2GB 设计

 

经过直接更改 /proc 文件系统,你不需从新启动机器就能够改变 SHMMAX 的默认设置。我使用的方法是将如下命令放入 /etc/rc.local 启动文件中:指针

 

# >echo "2147483648" > /proc/sys/kernel/shmmax

 

您还可使用 sysctl 命令来更改 SHMMAX 的值:

 

# sysctl -w kernel.shmmax=2147483648

 

最后,经过将该内核参数插入到 /etc/sysctl.conf 启动文件中,您可使这种更改永久有效:

 

# echo "kernel.shmmax=2147483648" >> /etc/sysctl.conf

 

2、设置 SHMMNI

 

咱们如今来看 SHMMNI 参数。这个内核参数用于设置系统范围内共享内存段的最大数量。该参数的默认值是4096 。这一数值已经足够,一般不须要更改。

 

您能够经过执行如下命令来肯定 SHMMNI 的值:

 

# cat /proc/sys/kernel/shmmni

 

4096

 

3、设置 SHMALL

 

最后,咱们来看 SHMALL 共享内存内核参数。该参数控制着系统一次可使用的共享内存总量(以页为单位)。简言之,该参数的值始终应该至少为:

 

ceil(SHMMAX/PAGE_SIZE)

 

SHMALL 的默认大小为 2097152 ,可使用如下命令进行查询:

 

# cat /proc/sys/kernel/shmall

 

2097152

 

SHMALL 的默认设置对于咱们来讲应该足够使用。

 

注意:  i386 平台上 Red Hat Linux  页面大小  4096 字节。可是,您可使用 bigpages ,它支持配置更大的内存页面尺寸。

 

 

 

l         屡次进行shmat会出现什么问题?

当首次建立共享内存段时,它并不能被任何进程所访问。为了使共享内存区能够被访问,则必须经过 shmat函数将其附加( attach )到本身的进程空间中,这样进程就与共享内存创建了链接。该函数声明在 linux/shm.h中:

 

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);


参数 shmid  shmget() 的返回值,是个标识符;
参数 shmflg 是存取权限标志;若是为 0 ,则不设置任何限制权限。在 <bits/shm.h> 中定义了几个权限:
 

#define SHM_RDONLY      010000         
#define SHM_RND         020000         
#define SHM_REMAP       040000        


若是指定 SHM_RDONLY ,那么共享内存区只有读取权限。

参数 shmaddr 是共享内存的附加点,不一样的取值有不一样的含义:

Ø         若是为空,则由内核选择一个空闲的内存区;若是非空,返回地址取决于调用者是否给 shmflg 参数指定SHM_RND 值,若是没有指定,则共享内存区附加到由 shmaddr 指定的地址;不然附加地址为 shmaddr 向下舍入一个共享内存低端边界地址后的地址 (SHMLBA ,一个常址)

Ø         一般将参数 shmaddr 设置为 NULL 

shmat() 调用成功后返回一个指向共享内存区的指针,使用该指针就能够访问共享内存区了,若是失败则返回-1

 

其映射关系以下图所示:

 

1.1 共享内存映射图

 

其中,shmaddr表示的是物理内存空间映射到进程的虚拟内存空间时候,虚拟内存空间中该块内存的起始地址,在使用中,由于咱们通常不清楚进程中哪些地址没有被占用,因此很差指定物理空间的内存要映射到本进程的虚拟内存地址,通常会让内核本身指定:

 

void ptr = shmat(shmid, NULL,0);

 

这样挂载一个共享内存若是是一次调用是没有问题的,可是一个进程是能够对同一个共享内存屡次 shmat进行挂载的,物理内存是指向同一块,若是shmaddrNULL,则每次返回的线性地址空间都不一样。并且指向这块共享内存的引用计数会增长。也就是进程多块线性空间会指向同一块物理地址。这样,若是以前挂载过这块共享内存的进程的线性地址没有被shmdt掉,即申请的线性地址都没有释放,就会一直消耗进程的虚拟内存空间,颇有可能会最后致使进程线性空间被使用完而致使下次shmat或者其余操做失败

解决方法:

能够经过判断须要申请的共享内存指针是否为空来标识是不是第一次挂载共享内存,如果则使用进行挂载,若不是则退出。

       void* ptr = NULL;

...

if  (NULL !=  ptr)

    return;

ptr  =  shmat(shmid,ptr,0666);

附:

 函数shmat将标识号为shmid共享内存映射到调用进程的地址空间中,映射的地址由参数shmaddrshmflg共同肯定,其准则为:
  (1) 若是参数shmaddr取值为NULL,系统将自动肯定共享内存连接到进程空间的首地址。
  (2) 若是参数shmaddr取值不为NULL且参数shmflg没有指定SHM_RND标志,系统将运用地址shmaddr连接共享内存。
  (3) 若是参数shmaddr取值不为NULL且参数shmflg指定了SHM_RND标志位,系统将地址shmaddr对齐后连接共享内存。其中选项SHM_RND的意思是取整对齐,常数SHMLBA表明了低边界地址的倍数,公式“shmaddr - (shmaddr % SHMLBA)”的意思是将地址shmaddr移动到低边界地址的整数倍上。

 

l        Shmget建立共享内存,当key相同时,什么状况下会出错?

 

shmget() 用来建立一个共享内存区,或者访问一个已存在的共享内存区。该函数定义在头文件 linux/shm.h中,原型以下:
 

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);


参数 key是由 ftok() 获得的键值;

参数 size 是以字节为单位指定内存的大小;

参数 shmflg 是操做标志位,它的一些宏定义以下:

IPC_CREATE : 调用 shmget 时,系统将此值与其余共享内存区的 key 进行比较,若是存在相同的 key ,说明共享内存区已存在,此时返回该共享内存区的标识符,不然新建一个共享内存区并返回其标识符。

IPC_EXCL : 该宏必须和 IPC_CREATE 一块儿使用,不然没意义。当 shmflg  IPC_CREATE | IPC_EXCL 时,表示若是发现内存区已经存在则返回 -1,错误代码为 EEXIST 

注意,当建立一个新的共享内存区时,size 的值必须大于 0 ;若是是访问一个已经存在的内存共享区,则置size  0 

 

通常咱们建立共享内存的时候会在一个进程中使用shmget来建立共享内存,

Int shmid =  shmget(key, size, IPC_CREATE|0666);

而在另外的进程中,使用shmget和一样的key来获取到这个已经建立了的共享内存,

Int shmid =  shmget(key, size, IPC_CREATE|0666);

若是建立进程和挂接进程key相同,而对应的size大小不一样,是否会shmget失败?

 

Ø         已经建立的共享内存的大小是能够调整的,可是已经建立的共享内存的大小只能调小,不能调大

 

  shm_id = shmget(key,4194304,IPC_CREAT);

建立了一个4M大小的共享内存,若是这个共享内存没有删掉,咱们再使用

shm_id = shmget(key,10485760,IPC_CREAT);

来建立一个10M大小的共享内存的时候,使用标准错误输出会有以下错误信息:

shmget error: Invalid argument

可是,若是咱们使用

shm_id = shmget(key,3145728,IPC_CREAT);

来建立一个3M大小的共享内存的时候,并不会输出错误信息,只是共享内存大小会被修改成3145728,这也说明,使用共享内存的时候,是用key来做为共享内存的惟一标识的,共享内存的大小不能区分共享内存。

这样会致使什么问题?

当多个进程都能建立共享内存的时候,若是key出 现相同的状况,而且一个进程须要建立的共享内存的大小要比另一个进程要建立的共享内存小,共享内存大的进程先建立共享内存,共享内存小的进程后建立共享 内存,小共享内存的进程就会获取到大的共享内存进程的共享内存,并修改其共享内存的大小和内容,从而可能致使大的共享内存进程崩溃。

 

解决方法:

方法一:

在全部的共享内存建立的时候,使用排他性建立,即便用IPC_EXCL标记:

           Shmget(key, size,IPC_CREATE|IPC_EXCL);

在共享内存挂接的时候,先使用排他性建立判断共享内存是否已经建立,若是还没建立则进行出错处理,若已经建立,则挂接

          Shmid =  Shmget(key, size,IPC_CREATE|IPC_EXCL);

          If (-1 != shmid)

{

   Printf("error");
    }

  Shmid =  Shmget(key, size,IPC_CREATE);

 

 

方法二:

 

     虽然都但愿本身的程序能和其余的程序预先约定一个惟一的键值,但实际上并非总可能的成行的,由于本身的程序没法为一块共享内存选择一个键值。所以,在此把key设为IPC_PRIVATE,这样,操做系统将忽略键,创建一个新的共享内存,指定一个键值,而后返回这块共享内存IPC标识符ID。而将这个新的共享内存的标识符ID告诉其余进程能够在创建共享内存后经过派生子进程,或写入文件或管道来实现,即这种方法不使用key来建立共享内存,由操做系统来保证惟一性。

 

 

l        ftok是否必定会产生惟一的key值?

系统创建IPC通信(如消息队列、共享内存时)必须指定一个ID值。一般状况下,该id值经过ftok函数获得。

ftok原型以下:

key_t ftok( char * pathname, int proj_id)

pathname就时你指定的文件名,proj_id是子序号。

 

在通常的UNIX实现中,是将文件的索引节点号取出,前面加上子序号获得key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的proj_id值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002

查询文件索引节点号的方法是: ls -i

 

但当删除重建文件后,索引节点号由操做系统根据当时文件系统的使用状况分配,所以与原来不一样,因此获得的索引节点号也不一样。

 

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

因此若是要确保key_t值不变,要么确保ftok的文件不被删除,要么不用ftok,指定一个固定的key_t值。

若是存在生成key_t值的文件被删除过,则颇有可能本身如今使用的共享内存key_t值会和另一个进程的key_t值冲突,以下面这种状况:

 

进程1使用文件1ftok生成了key10000,进程2使用文件2ftok生成了key 11111,此时若是进程1和进程2都须要下载文件,并将文件的内容更新到共享内存,此时进程12都须要先下文件,再删掉以前的共享内存,再使用ftok生成新的key,再用这个key去申请新的共享内存来装载新的问题,可是可能文件2比较大,下载慢,而文件1比较小,下载比较慢,因为文件1和文件2都被修改,此时文件1所占用的文件节点号多是文件2以前所占用的,此时若是下载的文件1ftok生成的key11111的话,就会和此时尚未是否11111这个key的进程2的共享内存冲突,致使出现问题。

 

解决方法:

方法一:

在有下载文件操做的程序中,对下载的文件使用ftok获取key的时候,须要进行冲突避免的措施,如使用独占的方式获取共享内存,若是不成功,则对key进行加一操做,再进行获取共享内存,一直到不会产生冲突为止。

方法二:

下载文件以前,将以前的文件进行mv一下,先“占”着这个文件节点号,防止其余共享内存申请key的时候获取到。

 

另外:

建立进程在通知其余进程挂接的时候,建议不使用ftok方式来获取Key,而使用文件或者进程间通讯的方式告知。

 

l        共享内存删除的陷阱?

当进程结束使用共享内存区时,要经过函数 shmdt 断开与共享内存区的链接。该函数声明在 sys/shm.h中,其原型以下:
 

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);


参数 shmaddr  shmat 函数的返回值。
进程脱离共享内存区后,数据结构 shmid_ds 中的 shm_nattch 就会减 1 。可是共享段内存依然存在,只有shm_attch  0 后,即没有任何进程再使用该共享内存区,共享内存区才在内核中被删除。通常来讲,当一个进程终止时,它所附加的共享内存区都会自动脱离。

 

咱们经过

int shmctl( int shmid , int cmd , struct shmid_ds *buf );

来删除已经存在的共享内存。

第一个参数,shmid,是由shmget所返回的标记符。

第二个参数,cmd,是要执行的动做。他能够有三个值:

命令        描述
IPC_STAT   
 设置shmid_ds结构中的数据反射与共享内存相关联的值。
IPC_SET    
 若是进程有相应的权限,将与共享内存相关联的值设置为shmid_ds数据结构中所提供的值。
IPC_RMID   
 删除共享内存段。

第三个参数,buf,是一个指向包含共享内存模式与权限的结构的指针,删除的时候能够默认为0

 

若是共享内存已经与全部访问它的进程断开了链接,则调用IPC_RMID子命令后,系统将当即删除共享内存的标识符,并删除该共享内存区,以及全部相关的数据结构;
若是仍有别的进程与该共享内存保持链接,则调用IPC_RMID子命令后,该共享内存并不会被当即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为"已被删除"(使用ipcs命令能够看到dest字段);直到已有链接所有断开,该共享内存才会最终从系统中消失。

须要说明的是:一旦经过shmctl对共享内存进行了删除操做,则该共享内存将不能再接受任何新的链接,即便它依然存在于系统中!因此,能够确知,在对共享内存删除以后不可能再有新的链接,则执行删除操做是安全的;不然,在删除操做以后如仍有新的链接发生,则这些链接都将可能失败!

 

Shmdtshmctl的区别:

Shmdt  是将共享内存从进程空间detach出来,使进程中的shmid无效化,不可使用。可是保留空间。
shmctl(sid,IPC_RMID,0)则是删除共享内存,完全不可用,释放空间。

相关文章
相关标签/搜索