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 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信号。

解疑sigsuspend

Unix提供了等待信号的系统调用,sigsuspend就是其中一个,在CU(www.chinaunix.net)上曾经讨论过一个关于该系统调用的问题,这里也做一下解疑。

CU网友讨论的问题的核心就是到底sigsuspend先返回还是signal handler先返回。这个问题Stevens在《Unix环境高级编程》一书中是如是回答的“If a signal is caught and if the signal handler returns, then sigsuspend returns and the signal mask of the process is set to its value before the call to sigsuspend.”,由于sigsuspend是原子操作,所以这句给人的感觉就是先调用signal handler先返回,然后sigsuspend再返回。但其第一个例子这么讲又说不通,看下面的代码:
CU上讨论该问题起于中的该例子:
int main(void) {
   sigset_t   newmask, oldmask, zeromask;

   if (signal(SIGINT, sig_int) == SIG_ERR)
      err_sys("signal(SIGINT) error");

   sigemptyset(&zeromask);

   sigemptyset(&newmask);
   sigaddset(&newmask, SIGINT);
   /* block SIGINT and save current signal mask */
   if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
      err_sys("SIG_BLOCK error");

   /* critical region of code */
   pr_mask("in critical region: ");

   /* allow all signals and pause */
   if (sigsuspend(&zeromask) != -1)
      err_sys("sigsuspend error");
   pr_mask("after return from sigsuspend: ");

   /* reset signal mask which unblocks SIGINT */
   if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
      err_sys("SIG_SETMASK error");

   /* and continue processing … */
   exit(0);
}

static void sig_int(int signo) {
   pr_mask("\nin sig_int: ");
   return;
}
 
结果:
$a.out
in critical region: SIGINT
^C
in sig_int: SIGINT
after return from sigsuspend: SIGINT

如果按照sig_handler先返回,那么SIGINT是不该被打印出来的,因为那时屏蔽字还没有恢复,所有信号都是不阻塞的。那么是Stevens说错了么?当然没有,只是Stevens没有说请在sigsuspend的原子操作中到底做了什么?
sigsuspend的整个原子操作过程为:
(1) 设置新的mask阻塞当前进程;
(2) 收到信号,恢复原先mask;
(3) 调用该进程设置的信号处理函数;
(4) 待信号处理函数返回后,sigsuspend返回。
大致就是上面这个过程,噢,原来signal handler是原子操作的一部分,而且是在恢复屏蔽字后执行的,所以上面的例子是没有问题的,Stevens说的也没错。由于Linux和Unix的千丝万缕的联系,所以在两个平台上绝大部分的系统调用的语义是一致的。上面的sigsuspend的原子操作也是从《深入理解Linux内核》一书中揣度出来的。书中的描述如下:
The sigsuspend( ) system call puts the process in the TASK_INTERRUPTIBLE state, after having blocked the standard signals specified by a bit mask array to which the mask parameter points. The process will wake up only when a nonignored, nonblocked signal is sent to it. The corresponding sys_sigsuspend( ) service routine executes these statements:

mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
spin_lock_irq(¤t->sigmask_lock);
saveset = current->blocked;
siginitset(¤t->blocked, mask);
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
regs->eax = -EINTR;
while (1) {
    current->state = TASK_INTERRUPTIBLE;
    schedule(  );
    if (do_signal(regs, &saveset))
        return -EINTR;
}
而最后的do_signal函数调用则是负责调用User Signal Handler的家伙。我想到这CU上的那个问题该被解疑清楚了吧。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats