进程间的共享内存通信:mmap

之前写了一个IPC共享内存库。

后来只到了一个更好的方法:mmap

线程本身就是共享内存的,所以主线程对象可以直接调用其他线程的对象。

但是进程就不行,主进程的对象想要调用其他进程的对象,是比较麻烦的一件事。

简要方法:主进程fork之前,创建一个共享内存(mmap匿名方法创建一个基于自己的内存缓冲)

 memobj = mmap(0, memsize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

然后fork出子进程,每个进程都有这个共享内存对象,便可以通过这个对象进行通信(put、take)。

linux下,每个进程都有自己拥有的内存区域,进程的内存总是私有的。共享内存是从系统的空闲内存池中分配的,希望访问它的每个进程连接它。这个连接过程称为映射。映射后,每个进程都可通过访问自己的内存而访问共享内存区域,从而与其他进程进行通信。如下图: 

这里写图片描述

 

mmap是操作这些设备的一种方法,所谓操作设备,比如IO端口(点亮一个LED)、LCD控制器、磁盘控制器,实际上就是往设备的物理地址读写数据。

但是,由于应用程序不能直接操作设备硬件地址,所以操作系统提供了这样的一种机制——内存映射,把设备地址映射到进程虚拟地址,mmap就是实现内存映射的接口。

操作设备还有很多方法,如ioctl、ioremap

mmap的好处是,mmap把设备内存映射到虚拟内存,则用户操作虚拟内存相当于直接操作设备了,省去了用户空间到内核空间的复制过程,相对IO操作来说,增加了数据的吞吐量。

1.父子进程共享内存通信的三种方法

https://blog.csdn.net/shallnet/article/details/41006717

https://blog.csdn.net/IT_iverson/article/details/79303567

https://blog.csdn.net/gpstrive/article/details/24916647

1.  mmap MAP_ANONYMOUS

 

在支持MAP_ANONYMOUS的系统上,直接用匿名共享内存即可,

 

mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE,   MAP_ANONYMOUS|MAP_SHARED, -1, 0);

 

 

2. mmap  /dev/zero

 

       有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。

 

fd=open("/dev/zero",O_RDWR);

if(fd==-1){

printf("open /dev/zero null\n");

return -1;

}

addr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

if(addr == NULL){

    printf("mmap error\n");

    return -1;

}

 

 

3. shmget shmat shmctl

 

shmget 是老式的system V 共享内存模式,很多系统都支持这种方法。

 

int  id;

//得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符

id = shmget(IPC_PRIVATE, shm->size, (SHM_R|SHM_W|IPC_CREAT));

if(id==-1){

    perror("shmget:");

    return -1;

}

//连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问

addr = shmat(id, NULL, 0);

if(addr == NULL){

    perror("shmat:");

    return -1;

}

//完成对共享内存的控制

if(shmctl(id, IPC_RMID, NULL)==-1){

    perror("shmctl:");

    return -1;

}

之前写的IPC库 就是用的最后一种方法,才知道是老式的。唉

细说mmap

mmap(0, memsize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  其中参数addr为描述符fd应该被映射到进程空间的起始地址,当指定为NULL时内核将自己去选择起始地址,无论addr是为NULL,函数返回值都是fd所映射到内存的起始地址;

   len是映射到调用进程地址空间的字节数,它 从被映射文件开头offset个字节开始算起,offset通常设置为0;

 prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问),该值常设置为PROT_READ | PROT_WRITE 。

 flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED(变动是共享的,对共享内存的修改所有进程可见) , MAP_PRIVATE(变动是私有的,对共享内存修改只对该进程可见)   必选其一,而MAP_FIXED则不推荐使用 。

 

Mmap用来进行进程间通信


每个进程都有独立的进程地址空间,通过页表和MMU,可将虚拟地址转换为物理地址,每个进程都有独立的页表数据,这可解释为什么两个不同进程相同的虚拟地址,却对应不同的物理地址。

TDoubleBuffer* G_TextLog(TDoubleBuffer::Create(0, 4 * 1024 * 1024, true));

即

TDoubleBuffer* G_TextLog=TDoubleBuffer::Create(0, 4 * 1024 * 1024, true)

即

TDoubleBuffer* TDoubleBuffer::Create(void* memobj, size_t memsize, bool isshared)

 memobj = mmap(0, memsize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

   return new (memobj) TDoubleBuffer(memsize, isshared);   

Create函数返回一个新的 双缓冲对象,new()括号内制定分配的地址为memobj ,创建的对象是双缓冲对象。这是一个共享内存对象。

主子进程间共用一个内存块。

于是, G_TextLog就是一个对象,存在的地址位于一个共享内存里。

 

 

2.mmap应用举例

1.匿名映射区,前面的两个例子都是创建文件建立的映射区,但是这个文件在映射区建立好以后进行进程间通信的时候就没有意义了,所以我们来看不使用文件创建匿名映射区

方法1:使用宏只需要将2的 代码改动一下就好,代码如下

注意匿名映射区的fp给成-1就可以了这里我没有给

可以看出程序效果是一样的。

注意宏这种方法只能在linux系统中使用。没有文件的情况下映射区的大小由你自己给

方法2:系统目录dev/zero,字符设备文件,是一个伪文件没有实际大小,但是你想要多大他就可以给你提供多大,所以可以用它来创建映射区,还有一个对应的文件dev/null,好比一个无底洞,什么东西都可以扔进去

同理只需要打开dev/zero这个文件就可以创建映射区了,自己下去

2.无血缘关系的进程间通信,两个进程通信

实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。

注意:这里给两个进程创建映射区的文件必须是相同的文件。


左边的进程在写数据,右边的进程在读数据,实现了无血缘关系进程间通信

最后其实文件也可以进程进程间通信(有血缘关系和无血缘关系都可以,打开同一个文件分别进行读写操作这个自己去验证)。

 

3.MMAP原理

 

https://blog.csdn.net/delphiwcdj/article/details/52565469

https://www.cnblogs.com/wanghuaijun/p/7624564.html

https://blog.csdn.net/qq_36675830/article/details/79283113

mmap()系统调用,在调用进程的虚拟地址空间中创建一个新内存映射。映射分为两种:

文件映射(内存映射文件) 
将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。映射的分页会在需要的时候从文件中自动加载。这种映射也被称为,基于文件的映射,或内存映射文件。

匿名映射 
一个匿名映射没有对应的文件,相反,这种映射的分页会被初始化为0。可以把它看成是一个内容总是被初始化为0的虚拟文件映射。

 

一个进程的映射中的内存,可以与其他进程中的映射共享,即各个进程的页表条目指向RAM中相同分页,这种行为会在以下两种情况下发生:

情况一:当两个进程映射了一个文件的同一个区域时,它们会共享物理内存的相同分页。 
情况二:通过fork()创建的子进程会继承父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同。

 

当两个或更多个进程共享相同分页时,每个进程都有可能会看到其他进程对分页内容做出的变更,当然这要取决于映射是私有的还是共享的。

私有映射(MAP_PRIVATE) 
在映射内容上发生的变更,对其他进程不可见。对于文件映射来讲,变更将不会在底层文件上进行,尽管一个私有映射的分页在上面介绍的情况中初始时是共享的,但对映射内容所做出的变更对各个进程来将则是私有的。内核使用了写时复制(copy-on-write)技术完成了这个任务。这意味着,每当一个进程试图修改一个分页的内容时,内核首先会为该进程创建一个新分页,并将需要修改的分页中的内容复制到新分页中,以及调整进程的页表。正因为这个原因,MAP_PRIVATE映射,也被称为私有写时复制映射。

共享映射(MAP_SHARED) 
在映射内容上发生变更,对所有共享同一个映射的其他进程都可见,对文件映射来讲,变更将会发生在底层的文件上。

以上四种不同的方式(文件、匿名、私有、共享)可以组合实现如下效果:

私有文件映射(文件 + 私有)-> 进程初始化 
映射的内容被初始化为一个文件区域中的内容,多个映射同一个文件的进程初始时会共享同样的内存物理分页,但系统使用写时复制技术,使得一个进程对映射的修改对其他进程不可见。这种映射的主要用途是,使用一个文件的内容来初始化一块内存区域。比如,根据二进制可执行文件,或共享库文件的相应部分来初始化一个进程的文本和数据段。

私有匿名映射(匿名 + 私有)-> malloc大块内存 
每次调用mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的,即不会共享物理分页。尽管子进程会继承父进程的映射,但写时复制语义确保在fork()之后父进程和子进程不会看到其他进程对映射所做出的修改。私有匿名映射的主要用途是,为一个进程分配新内存(用0填充),例如,在分配大块内存时,malloc()会为此而使用mmap()。

共享文件映射(文件 + 共享)-> 无关进程IPC 
所有映射一个文件的同一区域的进程会共享同样的内存物理分页,这些分页的内容将被初始化为该文件区域。对映射内容的修改将直接在文件中进程。这种映射主要用于两个用途:第一,它允许内存映射I/O,这表示一个文件会被加载到进程的虚拟内存中的一个区域中,并且对该块内容的修改会自动写入到这个文件中,因此,内存映射I/O为使用read()和write()来执行文件I/O这种做法提供了一种替代方案。第二,允许无关进程共享一块内容,以便以一种类似于System V共享内存段的方式来执行快速IPC。

共享匿名映射(匿名 + 共享)-> 相关进程IPC 
与私有匿名映射一样,每次调用mmap()创建一个共享匿名映射时,都会产生一个新的,与任何其他映射不共享分页的截然不同的映射。这里的差别在于,映射的分页不会被写时复制,这意味着,当一个子进程在fork()之后继承映射时,父进程和子进程共享同样的RAM分页,并且一个进程对映射内容所做出的变更会对其他进程可见。共享匿名映射允许以一种类似于System V共享内存段的方式来进行IPC,但只有相关进程之间才能这么做。

 

 

文件映射

要创建一个文件映射需要执行下面的步骤: 
1. 获取一个文件的描述符,通常通过调用open()来完成。 
2. 将文件描述符作为fd参数传入mmap()调用。

执行上述步骤后,mmap()会将打开的文件的内容映射到调用进程的地址空间中。一旦mmap()被调用之后就能够关闭文件描述符了,而不会对映射产生任何影响。但在一些情况下,将这个文件描述符保持在打开状态可能是有用的。

除了普通的磁盘文件,使用mmap()还能够映射各种真实和虚拟设备的内容,如硬盘、光盘以及/dev/mem。
在打开描述符fd引用的文件时,必须要具备与prot和flags参数值匹配的权限。offset参数指定了从文件区域中的哪个字节开始映射,它必须是系统分页大小的倍数。将offset指定为0会导致从文件的起始位置开始映射。length参数指定了映射的字节数。offset和length参数一起确定了文件的哪个区域会被映射进内存。

在Linux上,一个文件映射的分页会在首次被访问时被映射进内存。这意味着如果在mmap()调用之后修改了文件区域,但映射的对应部分(即分页)还没有被访问过,那么如果相应分页还没有被加载进内存的话,变更对这个进程可能是可见的。这个行为是依赖于实现的,可移植的应用程序应该避免某个特定内核在这种场景中的行为。
--------------------- 
 

私有文件映射

私有文件映射最常见的两个用途是:

允许多个执行同一个程序或使用同一个共享库的进程共享同样的(只读的)文本段,它是从底层可执行文件或库文件的相应部分映射而来的。
尽管可执行文件的文本段通常是被保护成只允许读取和执行访问(PROT_READ|PROT_EXEC),但在被映射时仍然使用了MAP_PRIVATE而不是MAP_SHARED,这是因为调试器或自修改的程序能够修改程序文本(在修改了内存上的保护信息之后),而这样的变更是不应该发生在底层文件上或影响到其他进程的。
映射一个可执行文件或共享库的初始化数据段。这种映射会被处理成私有,使得对映射数据段内容的变更不会发生在底层文件上。
mmap()的这两种用法通常对程序是不可见的,因为这些映射是由程序加载器和动态链接器创建的。一般可以在/proc/PID/maps的输出中发现这两种映射。

共享文件映射

当多个进程创建了同一个文件区域的共享映射时,它们会共享同样的内存物理分页。此外,对映射内容的变更将会反应到文件上。实际上,这个文件被当成了该块内存区域的分页存储。

共享文件映射存在两个用途:

内存映射I/O

由于共享文件映射中的内容是从文件初始化而来的,并且对映射内容所做出的变更都会自动反映到文件上,因此可以简单地通过访问内存中的字节来执行文件I/O,而依靠内核来确保对内存的变更会被传递到映射文件中。(一般来讲,一个程序会定义一个结构化数据类型来与磁盘文件中的内容对应起来,然后使用该数据类型来转换映射的内容),这项技术被称为内存映射I/O,它是使用read()和write()来访问文件内容这种方案的替代方案。

使用内存访问来取代read()和write()系统调用能够简化一些应用程序的逻辑。
在一些情况下,它能够比使用传统的I/O系统调用执行文件I/O这种做法提供更好的性能。
原因是: 
1. 正常的read()或write()需要两次传输:一次是在文件和内核高速缓冲区之间,另一次是在高速缓冲区和用户空间缓冲区之间。使用mmap()就不需要第二次传输了。对于输入来讲,一旦内核将相应的文件块映射进内存之后,用户进程就能够使用这些数据了;对于输出来讲,用户进程仅仅需要修改内核中的内容,然后可以依靠内核内存管理器来自动更新底层的文件。 
2. 除了节省内核空间和用户空间之间的一次传输之外,mmap()还能够通过减少所需使用的内存来提升性能。当使用read()或write()时,数据将被保存在两个缓冲区中:一个位于用户空间,另个一位于内核空间。当使用mmap()时,内核空间和用户空间会共享同一个缓冲区。此外,如果多个进程正在同一个文件上执行I/O,那么它们通过使用mmap()就能够共享同一个内核缓冲区,从而又能够节省内存的消耗。
注意: 
1. 内存映射I/O所带来的性能优势,在大型文件中执行重复随机访问时最有可能体现出来。如果顺序地访问一个文件,并假设执行I/O时使用的缓冲区大小足够大以至于能够避免执行大量的I/O系统调用,那么与read()和write()相比,mmap()带来的性能上的提升就非常有限,或者根本就没有带来性能上的提升。 
2. 内存映射I/O也有一些缺点。对于小数据量I/O来讲,内存映射I/O的开销(映射、分页故障、解除映射、更新硬件内存管理单元的超前转换缓冲器)实际上要比简单的read()或write()大。

使用共享文件映射的IPC

由于所有使用同样文件区域的共享映射的进程,共享同样的物理分页。因此共享文件映射的第二个用途是作为一种(快速的)IPC方法。这种共享内存区域与System V共享内存对象之间的区别在于区域中内容上的变更会反应到底层的映射文件上。

这种特性,对那些需要共享内存内容在应用程序或系统重启时能够持久化的应用程序来讲是非常有用的。
例子 —— 使用mmap()创建一个共享文件映射

https://github.com/gerryyang/TLPI/blob/master/src/mmap/t_mmap.c

注意:这个简单的程序没有使用任何机制来同步多个进程对映射文件的访问。但现实世界中的应用程序通常需要同步对共享内存的访问,这可以通过多种技术来完成(信号量、文件锁等)。

匿名映射

匿名映射是没有对应文件的一种映射。

MAP_ANONYMOUS和/dev/zero

在Linux上,使用mmap()创建匿名映射存在两种不同但等价的方法。 
1. 在flags中指定MAP_ANONYMOUS并将fd指定为-1。 
2. 打开/dev/zero设备文件并将得到的文件描述符传给mmap()。

/dev/zero是一个虚拟设备,当从中读取数据时它总是会返回0,而写入到这个设备中的数据总会被丢弃。/dev/zero的一个常见用途是使用0来组装一个文件。(如使用dd命令)
MAP_ANONYMOUS和/dev/zero技术并没有在SUSv3进行规定,尽管大多数UNIX实现都支持其中一种或两种。之所以存在两种不同的技术实现同样的语义的原因是,MAP_ANONYMOUS源自BSD,而/dev/zero源自System V。

MAP_PRIVATE匿名映射

fd = open("/dev/zero", O_RDWR);
if (fd == -1) errExit("open");
add = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (add == MAP_FAILED) errExit("mmap");

glibc中malloc()实现使用MAP_PRIVATE匿名映射来分配大小大于MMAP_THRESHOLD字节的内存块。这样在后面将这些内存块传递给free()之后就能高效地释放这些块(munmap())。它还降低了重复分配和释放大内存块而导致内存碎片的可能性。MMAP_THRESHOLD在默认情况下是128KB,但可以通过mallopt()库函数来调整这个参数。
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值