Linux中IPC机制:共享内存区

前言

研究IPMI源码,实现将IPMI获取的性能数据传递给另一独立进程,主要用到的IPC方法就是共享内存。现在来说,基本是对共享内存这一机制有了更加透彻的理解和掌握。记录下。

相关背景:

Linux中IPC有几种实现版本,例如共享内存就有Posix共享内存与System V共享内存,它们的联系与区别?
答:Linux中的进程间通信机制源自于Unix平台上的进程通信机制。Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上的各有所不同,前者对Unix早期的IPC进行系统的改进和扩充,形成了运行在单个计算机上的System V IPC,后者则跳过单机限制,实现了基于socket的进程间通信机制。同时因为Unix版本的多样性,IEEE又单独制定了一套独立的Posix IPC标准。在这三者基础上,Linux所继承的IPC,如图示:

也就是说,Linux上的IPC版本之间只是实现不同,效果并没有区别。

共享内存区

关于共享内存机制具体的介绍我就不多说了,内容网上多的是,有书的话请详细阅读《网络编程,卷二》。这里,我先后使用过Mmap映射同一文件方式与System V API方式分别进行了实验,就其中遇到的一些问题说一说。


1,Mmap 映射

1)指定映射文件

适用于两个独立的任何进程,通过指定同一个文件来实现共享内存方式的进程间通信。需要注意的是,进程之间在共享内存时,共享内存会一直保持到通信完毕为止,数据内容会一直在内存中。在解除映射之后,内存上的内容才写回文件。

原理:所映射文件的实际数据成了被共享内存区的内容。那如何保证各个独立进程寻址到同一内存页面?

<1> page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的;

<2> 文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面;

<3> 进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常;

<4> 对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表;

<5> 所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面;

以上原理出自参考的博文,对整个流程的理解,我用图表示:


实例仍然给出原博文例子,算是属于手抄版吧。。。反正是实例嘛,说明效果就好了。

/* example A */
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

typedef struct
{
  char name[4];
  int  age;
}people;

void main(int argc, char** argv) // map a normal file as shared mem:
{
        int fd,i;
        people *p_map;
        char temp;

        fd = open(argv[1], O_CREAT|O_RDWR|O_TRUNC, 00777); // 注意O_TRUNC选项参数
        lseek(fd, sizeof(people)*5-1, SEEK_SET);
        write(fd, "", 1); 
  
        p_map = (people*) mmap( NULL, sizeof(people)*10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
        close( fd );
        temp = 'a';
        for(i = 0; i < 10; i++)
        {
                temp += 1;
                memcpy( (p_map+i)->name, &temp, 1 );
                (p_map+i)->age = 20+i;
        }
        printf(" initialize over \n ");
        sleep(10);
        munmap( p_map, sizeof(people)*10 );
        printf( "umap ok \n" );
}
/* example B */
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

typedef struct
{
        char name[4];
        int  age;
}people;

void main(int argc, char** argv)  // map a normal file as shared mem:
{
        int fd,i;
        people *p_map;
    
        fd=open( argv[1], O_CREAT|O_RDWR, 00777 );
        p_map = (people*)mmap(NULL, sizeof(people)*10, PROT_READ|PROT_WRITE,
                 MAP_SHARED, fd, 0); 
        for(i = 0; i < 10; i++)
        {   
                printf( "name: %s age %d;\n",(p_map+i)->name, (p_map+i)->age );
        }

        munmap( p_map, sizeof(people)*10 );
}

这里需要注意的是open()文件时O_TRUNC的参数。这个每次打开文件时清零文件的,也就是说,如果有三个以上的进程需要访问此文件,打开时就会把之前两个进程的数据给清零掉,这是我们不愿意看到的。这个问题在一开始时没注意,出了问题还以为是共享内存本身机制的问题,后来逐一对照代码,才发现原因所在。刚学习的同学尤其注意,实例代码也不能拿来就用,还是要自己搞明白才好。

另外需要说明的是,我没有遇到原文中提到的进程B在未解除映射时和解除映射后对共享内存的访问时候的差异,实在没找的原因。对文件大小和映射内存大小的限制也没有体现。贴上实例效果,我在想是否有可能是现在的内核版本中已经完善,就是只要不跨越内存页,超过文件大小的共享内存区域依然能够访问



2)匿名映射文件

适用于具有亲缘关系的父子进程。原理同上。

所谓的匿名映射文件,只不过指定文件映射的一种特殊形式,本质上并无不同。这种形式主要就是基于Linux中父子进程创建的方式。父进程通过在调用fork()函数创建子进程之前,通过mmap()创建共享内存区,之后fork的子进程继承父进程资源,自然也就共享父进程mmap后的映射空间了。

直接贴上手抄版实例:

/* fork example */
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct
{
        char name[4];
        int  age;
}people;

void main(int argc, char** argv)
{
        int i;
        people *p_map;
        char temp;
        p_map = (people *)mmap(NULL, sizeof(people)*10, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); 
        if (fork() == 0)
        {   
                sleep(2);
                for (i = 0;i < 5;i++)
                {
                        printf("child read: the %d people's age is %d\n", i+1, (p_map+i)->age);
                }
                p_map->age = 100;
                munmap(p_map, sizeof(people)*10); //实际上,进程终止时,会自动解除映射。
                exit(1);
        }
        temp = 'a';
        for (i = 0; i < 5;i++)
        {
                temp += 1;
                memcpy((p_map+i)->name, &temp, 1);
                (p_map+i)->age = 20+i;
        }
        sleep(5);
        printf("parent read: the first people,s age is %d\n", p_map->age);
        printf("umap\n");
        munmap(p_map,sizeof(people)*10);
        printf("umap ok\n");
}

3) 对Mmap()返回地址的访问

这一块主要东西我觉得还是看《卷二》吧,其中的内容其实也还好,容易理解。

2,System V API

System V学习起来相对很简单,因为你只要会用接口就成。原理很mmap()文件映射是一样一样的。


总结

总的来说,共享内存对于数据的存取效率是很高效的。但目前为止,我觉得对于数据类型比较多,且结构复杂,像数组嵌套之类的用这个太蛋疼了。。因为我只知道一个共享内存的首地址,所以对内存的访问只能一个萝卜一个坑的去找。很不爽。不过,也可能是目前见识有限,暂就这些吧。

另外,真心觉得写这种技术型文章很费精力啊。。远不是贴两个程序,说两句话就成的。这方面,我做的不够。


主要参考:

博文:Linux环境进程间通信(五): 共享内存(上)

《网络编程,卷二》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值