进程间通信(8)-共享内存(posix)
⽬录
1.前⾔
本篇⽂章的所有例⼦,基于RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)。
2.共享内存介绍
本系列中前⾯⼏篇⽂章,所讲述的Linux下⾯的各种进程间通信⽅式,例如:pipe(管道),FIFO(命名管道),message queue(消息队列),它们的共同点都是通过内核来进⾏通信(假设posix消息队列也是在内核中实现的,因为posix标准没有规定它的具体实现⽅式)。向pipe,fifo,message queue写⼊数据时,需要把数据从⽤户空间(⽤户进程)复制到内核,⽽从这些IPC读取数据时,⼜需要把数据从内核复制到⽤户空间。
因此,所有的这些IPC⽅式,都需要在内核与⽤户进程之间进⾏2次数据复制,即进程间的通信必须通过内核来传递,如下图1所⽰:
图1:通过内核进⾏进程间通信(IPC)
共享内存也是⼀种IPC,它是⽬前最快的IPC,它的使⽤⽅式是将同⼀个内存区映射到共享它的不同进程的地址空间中,这样这些进程间的通信就不再需要通过内核,只需对该共享的内存区域进程操作就可以了。和其他IPC不同的是,共享内存的使⽤需要⽤户⾃⼰进⾏同步操作。
下图2是共享内存区IPC的通信:
图2:共享内存区进程间通信(IPC)
3.映射函数mmap
每个进程都有⾃⼰的虚拟地址空间。
我们知道除了堆中的虚拟内存可以由程序员灵活分配和释放,其他区域的虚拟内存都由系统控制,那么还有没有其他⽅法让程序员去灵活控制虚拟内存呢?
linux下的mmap函数就由此⽽来。mmap函数可以为我们在进程的虚拟空间开辟⼀块新的虚拟内存,可以将⼀个⽂件映射到这块新的虚拟内存,所以操作新的虚拟内存就是操作这个⽂件。
因此,mmap函数主要的功能就是将⽂件或设备映射到调⽤进程的地址空间中。当使⽤mmap映射⽂件到进程后,就可以直接操作这段虚拟地址进⾏⽂件的读写等操作,不必再调⽤read,write等系统调⽤。
《UNIX⽹络编程第⼆卷进程间通信》对mmap函数进⾏了说明。该函数主要⽤途有三个:
a)将⼀个普通⽂件映射到内存中,通常在需要对⽂件进⾏频繁读写时使⽤,这样⽤内存读写取代I/O读写,以获得较⾼的性能。可以提供⽆亲缘进程间的通信;
b)将特殊⽂件进⾏匿名内存映射,可以为亲缘进程提供共享内存空间;
c)为⽆亲缘的进程提供共享内存空间,⼀般也是将⼀个普通⽂件映射到内存中。
void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);
//成功返回映射到进程地址空间的起始地址,失败返回MAP_FAILED
参数start:指向想要映射的内存起始地址,通常设为 NULL,代表让系统⾃动选定地址,映射成功后返回该地址。
参数len:映射到进程地址空间的字节数,它从被映射⽂件开头的第offset个字节处开始,offset通常被设置为0。
参数prot:映射区域的保护⽅式。可以为以下⼏种⽅式的组合:
·  PROT_READ:数据可读;
·  PROT_WRITE:数据可写;
·  PROT_EXEC:数据可执⾏;
·  PROT_NONE:数据不可访问;
参数flags:设置内存映射区的类型标志,POSIX标志定义了以下三个标志:
· MAP_SHARED:该标志表⽰,调⽤进程对被映射内存区的数据所做的修改对于共享该内存区的所有进程都可见,⽽且确实改变其底层的⽀撑对象(⼀个⽂件对象或是⼀个共享内存区对象)。
·  MAP_PRIVATE:调⽤进程对被映射内存区的数据所做的修改只对该进程可见,⽽不改变其底层⽀撑对象。
·  MAP_FIXED:该标志表⽰准确的解释start参数,⼀般不建议使⽤该标志,对于可移植的代码,应该把start参数置为NULL,且不指定MAP_FIXED标志。
上⾯三个标志是在POSIX.1-2001标准中定义的,其中MAP_SHARED和MAP_PRIVATE必须选择⼀个。在Linux中也定义了⼀些⾮标准的标志,例如MAP_ANONYMOUS(MAP_ANON),MAP_LOCKED等,具体参考Linux⼿册。
参数fd:有效的⽂件描述符。如果设定了MAP_ANONYMOUS(MAP_ANON)标志,在Linux下⾯会忽略fd参数,⽽有的系统实现如BSD需要置fd为-1;
参数offset:相对⽂件的起始偏移。
4.映射删除munmap
从进程的地址空间中删除⼀个映射关系,需要⽤到下⾯的函数:
#include <sys/mman.h>
int munmap(void *start, size_t len);  //成功返回0,出错返回-1
参数start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
参数len:映射区的⼤⼩。
5.映射同步
对于⼀个MAP_SHARED的内存映射区,内核的虚拟内存算法会保持内存映射⽂件和内存映射区的同步,也就是说,对于内存映射⽂件所对应内存映射区的修改,内核会在稍后的某个时刻更新该内存映射⽂件。如果我们希望硬盘上的⽂件内容和内存映射区中的内容实时⼀致,那么我们就可以调⽤msync开执⾏这种同步:
int msync(void *start, size_t len, int flags);  //成功返回0,出错返回-1
参数start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
参数len:映射区的⼤⼩。
参数flags:同步标志,有以下三个标志:
· MS_ASYNC:异步写,⼀旦写操作由内核排⼊队列,就⽴刻返回;
· MS_SYNC:同步写,要等到写操作完成后才返回。
. MS_INVALIDATE:使该⽂件的其他内存映射的副本全部失效。
6.内存映射区的⼤⼩
Linux下的内存是采⽤页式管理机制。通过mmap进⾏内存映射,内核⽣成的映射区的⼤⼩都是以页⾯⼤⼩PAGESIZE为单位,即为PAGESIZE的整数倍。如果mmap映射的长度不是页⾯⼤⼩的整数倍,那么多余空间也会被闲置浪费。
可以通过下⾯的⽅式来查看Linux的页⾯⼤⼩:
#include<iostream>
#include<unistd.h>
int main()
{
int pSize=getpagesize();
//或者int pSize=sysconf(_SC_PAGE_SIZE);
std::cout<<"The page size is: "<<pSize<<std::endl;
return 0;
}
运⾏结果:
The page size is: 4096
从上述的运⾏结果,可以很明显的看出,当前系统的页⼤⼩为4k字节。
下⾯对映射⽂件的⼤⼩和映射长度的不同情况进⾏讨论。
6.1映射⽂件的⼤⼩等于映射长度
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#define  PATH_NAME "/tmp/memmap"
int main(int argc, char** argv) {
int fd;
fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
if (fd < 0) {
std::cout << "open file " << PATH_NAME << " failed.";
std::cout << strerror(errno) << std::endl;
return -1;
}
if (ftruncate(fd, 5000) < 0) //修改⽂件⼤⼩为5000
{
std::cout << "change file size  failed.";
std::cout << strerror(errno) << std::endl;
close(fd);
return -1;
}
char* memPtr;
//指定映射长度为5000,与映射⽂件⼤⼩相等
memPtr = (char*)mmap(NULL, 5000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);    close(fd);
if (memPtr == MAP_FAILED) {
std::cout << "mmap failed." << strerror(errno) << std::endl;
return -1;
}
std::cout << "[0]:" << (int)memPtr[0] << std::endl;
std::cout << "[4999]:" << (int)memPtr[4999] << std::endl;
std::cout << "[5000]:" << (int)memPtr[5000] << std::endl;
std::cout << "[8191]:" << (int)memPtr[8191] << std::endl;
std::cout << "[8192]:" << (int)memPtr[8192] << std::endl;
std::cout << "[4096*3-1]:" << (int)memPtr[4096 * 3 - 1] << std::endl;
std::cout << "[4096*3]:" << (int)memPtr[4096 * 3] << std::endl;
return 0;
}
输出:
[0]:0
[4999]:0
[5000]:0
[8191]:0
[8192]:60
[4096*3-1]:0
Segmentation fault (core dumped)
可以使⽤下图来分析上⾯的执⾏结果:
偏移0                        4999
|======⽂件=======|
下标0                          4999                              4096*2-1                4096*3-1
|====内存映射区====|====第⼆页剩余部分====|====第三页====|====
|--------------这段区间内访问不会出现问题------------|----------SIGSEGV
执⾏结果可以看到,能够完整的访问到前三页,在访问第四页的时候会产⽣SIGSEGV信号,发⽣Segmentation fault段越界访问错误。按照《UNIX ⽹络编程 卷2:进程间通信》中P257的讲解,内核会为该内存映射两个页⾯,访问前两个页⾯不会有问题,但访问第三个页⾯会产⽣SIGSEGV错误信号。这个差异具体应该是与底层实现有关。
6.2映射⽂件的⼤⼩⼩于映射长度
在上⾯代码的基础上,修改mmap内存映射函数中的第⼆个参数如果,即映射长度修改为4096*3,⼤于映射⽂件的⼤⼩(5000)。memPtr = (char *)mmap(NULL, 4096 * 3, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
则运⾏结果为:
[root@MiWiFi-R1CM csdnblog]# ./a.out
[0]:0
[4999]:0
[5000]:0
[8191]:0
Bus error (core dumped)
再次修改访问代码如下,让程序访问4096*3以后的内存映射区域。
std::cout << "[0]:" << (int)memPtr[0] << std::endl;
std::cout << "[4999]:" << (int)memPtr[4999] << std::endl;
std::cout << "[5000]:" << (int)memPtr[5000] << std::endl;
std::cout << "[8191]:" << (int)memPtr[8191] << std::endl;
std::cout << "[4096*3]:" << (int)memPtr[4096 * 3] << std::endl;
进程通信方式
std::cout << "[4096*4-1]:" << (int)memPtr[4096 * 4 - 1] << std::endl;
std::cout << "[4096*4]:" << (int)memPtr[4096 * 4] << std::endl;
输出结果:
[0]:0
[4999]:0
[5000]:0
[8191]:0
[4096*3]:60
[4096*4-1]:0
Segmentation fault (core dumped)
使⽤下图来分析上⾯的执⾏结果:
偏移0                            4999
|====⽂件=========|
下标0                          4999                                  4096*2-1              4096*3-1              4096*4-1
|====内存映射区====|====第⼆页剩余部分====|====第三页====|====第四页====|==========
|-------------这段区间内访问不会出现问题-------------|------SIGBUG-----|---访问不出问题--|-----SIGSEGV
|-------------------------------------mmap⼤⼩-------------------------------------|
该执⾏结果和之前的映射⽂件⼤⼩和映射长度相同的情况有⽐较⼤的区别,在访问内存映射区内部但超出底层⽀撑对象的⼤⼩的区域部分会产⽣SIGBUS错误信息,产⽣⽣BUS error错误,但访问第四页不会出问题,访问第四页以后的内存区就会产⽣ SIGSEGV错误信息。按照《UNIX ⽹络编程 卷2:进程间通信》中P258的讲解,访问第三个页⾯以后的内存会产⽣SIGSEGV错误信号。这个差异具体应该是底层实现有关。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。