APR分析-共享内存篇

APR分析-共享内存篇

共享内存是一种重要的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 orshm_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信号。

猜你喜欢

转载自blog.csdn.net/zhangge3663/article/details/107013144