共享内存是一种重要的IPC方式。在项目中多次用到共享内存,只是用而并未深入研究。这次趁研究APR代码的机会复习了共享内存的相关资料。

APR共享内存封装的源代码的位置在$(APR_HOME)/shmem目录下,本篇blog着重分析unix子目录下的shm.c文件内容,其相应头文件为$(APR_HOME)/include/apr_shm.h。

一、共享内存简单小结
共享内存是最快的IPC方式,因为一旦这样的共享内存段映射到各个进程的地址空间,这些进程间通过共享内存的数据传递就不需要内核的帮忙了。Stevens的解释是“各进程不是通过执行任何进入内核的系统调用来传递数据,显然内核的责任仅仅是建立各进程地址空间与共享内存的映射,当然像处理页面故障这一类的底层活还是要做的”。相比之下,管道和消息队列交换数据时都需要内核来中转数据,速度就相对较慢。

Unix“历史悠久”,所以在历史上不同版本的Unix提供了不同的支持共享内存的方式,我想这也是Stevens在《Unix网络编程第2卷》中花费三章来讲解共享内存的原因吧。你也不妨先看看shm.c中的代码,代码用条件宏分割不同Share Memory的实现。

二、APR共享内存封装
APR提供多种创建共享内存的方式,其中最主要的就是apr_shm_create接口,其伪码如下:
apr_shm_create
{
 if (要创建匿名shm) {
#if APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON

#if APR_USE_SHMEM_MMAP_ZERO
  xxxx ———- (1)
#elif APR_USE_SHMEM_MMAP_ANON
  xxxx ———- (2)
#endif

#endif /* APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON */

#if APR_USE_SHMEM_SHMGET_ANON
  xxxx ———- (3)
#endif

 } else { /* 创建有名shm */

#if APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM

#if APR_USE_SHMEM_MMAP_TMP
  xxxx ———- (4)
#endif

#if APR_USE_SHMEM_MMAP_SHM
  xxxx ———- (5)
#endif

#endif /* APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM */

#if APR_USE_SHMEM_SHMGET
  xxxx ———- (6)
#endif  
 }
}

apr_shm_create函数代码很长,之所以这样是因为其支持多种创建Share Memory的方式,在上面的伪代码中共用条件宏分隔了6种方式,这6种方式将在下面分析。可以看出shmem主要分为"匿名的"和"有名的",其中"有名的"都是通过filename来标识(或通过ftok转换filename而得到的shmid来标识)。
其中不同版本Unix创建匿名shmem的做法如下:
(1) SVR4通过映射"/dev/zero"设备文件来获得匿名共享内存,其代码一般为:
fd = open("/dev/zero", ..);
ptr = mmap(…, MAP_SHARED, fd, …);

(2) 4.4 BSD提供更加简单的方式来支持匿名共享内存(注意标志参数MAP_XX)
ptr = mmap(…, MAP_SHARED | MAP_ANON, -1, …);

(3) System V匿名共享内存区的做法如下:
shmid = shmget(IPC_PRIVATE, …);
ptr = shmat(shmid, …);

匿名共享内存一般都用于有亲缘关系的进程间的数据通讯。由父进程创建共享内存,子进程自动继承下来。由于是匿名,没有亲缘关系的进程是不能动态连接到该共享内存区的。

不同版本Unix创建有名shmem的做法如下:
(4) 由于是有名的shmem,所以与匿名不同的地方在于用filename替代"/dev/zero"做映射。
fd = open(filename, …);
apr_file_trunc(…);
ptr = mmap(…, MAP_SHARED, fd, …);

(5) Posix共享内存的做法
fd = shm_open(filename, …);
apr_file_trunc(…);
ptr = mmap(…, MAP_SHARED, fd, …);
值得注意的一点就是通过shm_open映射的共享内存可以供无亲缘关系的进程共享。apr_file_trunc用于重新设定共享内存对象长度。

(6) System V有名共享内存区的做法如下:
shmkey = ftok(filename, 1);
shmid = shmget(shmkey, …); //相当于open or shm_open
ptr = shmat(shmid, …); //相当于mmap

有名共享内存一般都与一个文件相关,该文件映射到共享内存段,而不同的进程(包括无亲缘关系的进程)则都映射到该文件以达到目的。在APR中通过apr_shm_attach可以动态将调用进程连接到已存在的共享内存区上,前提是你必须知道该共享内存区的标识,在APR中一律用filename做标识。

三、总结
内核架起了多个进程间共享数据的纽带–共享内存。通过上面的叙述你会发现共享内存的创建其实并不困难,真正困难的是共享内存的管理[注1],在正规的软件公司像内存/共享内存管理这样的重要底层功能都是封装成库形式的,当然内存管理的内容不是这篇blog重点涉及的内容。

四、参考资料:
1、《Unix网络编程第2卷》
2、《Unix环境高级编程》

[注1] SIGSEGV和SIGBUS
涉及共享内存的管理就不能不提到访问共享内存对象。谈到访问共享内存对象就要留神“SIGSEGV和SIGBUS”这两个信号。
系统分配内存页来承载内存映射区,由于内存页大小是固定的,所以存在多余的页空间空闲,比如待映射文件大小为5000 bytes,内存映射区大小也为5000 bytes。而一个内存页大小4096,系统势必要分配两页来承载,这时空闲的有效空间为从5000-8191,如果进程访问这段地址空间也不会发生错误。但是要超出8191,就会收到SIGSEGV信号,导致程序停止。关于SIGBUS信号的来历,这里也举例说明:若待映射文件大小为5000 bytes,我们在mmap时指定内存映射区size = 15000 > 5000,这时内核真正的共享区承载体大小只有8192(能包容映射文件大小即可),此时在[0,8191]内访问均没问题,但在[8192, 14999]之间会得到SIGBUS信号;超出15000访问时会触发SIGSEGV信号。

© 2005, bigwhite. 版权所有.

Related posts:

  1. APR源代码分析-文件IO篇
  2. APR源代码分析-信号篇
  3. APR源代码分析-整体篇
  4. APR源代码分析-设计篇
  5. APR源代码分析-高级IO篇