转自:http://blog.csdn.net/anonymalias/article/details/9938865
前面已经讨论过Linux下个各种进程间的通信方式:管道,FIFO,消息队列,他们的共同特点就是通过内核来进行通信(假设POSIX消息队列也是在内核中实现的,因为POSIX标准并没有限定它的实现方式)。向管道,FIFO,消息队列写入数据需要把数据从进程复制到内核,从这些IPC读取数据的时候又需要把数据从内核复制到进程。所以这种IPC方式往往需要2次在进程和内核之间进行数据的复制,即进程间的通信必须借助内核来传递。如下图所示:
图1 通过内核进行通信的IPC
共享内存也是一种IPC,它是目前可用IPC中最快的,它是使用方式是将同一个内存区映射到共享它的不同进程的地址空间中,这样这些进程间的通信就不再需要通过内核,只需对该共享的内存区域进程操作就可以了,和其他IPC不同的是,共享内存的使用需要用户自己进行同步操作。下图是共享内存区IPC的通信,
图2 共享内存IPC通信
1 mmap系列函数简介
mmap函数主要的功能就是将文件或设备映射到调用进程的地址空间中,当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用。在很大程度上提高了系统的效率和代码的简洁性。
使用mmap函数的主要目的是:
- 对普通文件提供内存映射I/O,可以提供无亲缘进程间的通信;
- 提供匿名内存映射,以供亲缘进程间进行通信。
- 对shm_open创建的POSIX共享内存区对象进程内存映射,以供无亲缘进程间进行通信。
下面是mmap函数的接口以及说明:
- #include <sys/mman.h>
- void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);
- //成功返回映射到进程地址空间的起始地址,失败返回MAP_FAILED
start:指定描述符fd应被映射到的进程地址空间内的起始地址,它通常被设置为空指针NULL,这告诉内核自动选择起始地址,该函数的返回值即为fd映射到内存区的起始地址。
len:映射到进程地址空间的字节数,它从被映射文件开头的第offset个字节处开始,offset通常被设置为0。如下图是内存映射文件的一个例子:
图3 内存映射文件的示例
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:相对文件的起始偏移。
从进程的地址空间中删除一个映射关系,需要用到下面的函数:
- #include <sys/mman.h>
- int munmap(void *start, size_t len);
- //成功返回0,出错返回-1
start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
len:映射区的大小。
对于一个MAP_SHARED的内存映射区,内核的虚拟内存算法会保持内存映射文件和内存映射区的同步,也就是说,对于内存映射文件所对应内存映射区的修改,内核会在稍后的某个时刻更新该内存映射文件。如果我们希望硬盘上的文件内容和内存映射区中的内容实时一致,那么我们就可以调用msync开执行这种同步:
- #include <sys/mman.h>
- int msync(void *start, size_t len, int flags);
- //成功返回0,出错返回-1
start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
len:映射区的大小。
flags:同步标志,有一下三个标志:
- MS_ASYNC:异步写,一旦写操作由内核排入队列,就立刻返回;
- MS_SYNC:同步写,要等到写操作完成后才返回。
- MS_INVALIDATE:使该文件的其他内存映射的副本全部失效。
2 mmap内存映射区的大小
Linux下的内存是采用页式管理机制。通过mmap进行内存映射,内核生成的映射区的大小都是以页面大小PAGESIZE为单位,即为PAGESIZE的整数倍。如果mmap映射的长度不是页面大小的整数倍,那么多余空间也会被闲置浪费。
下面可以查看Linux的页面大小
- #include <iostream>
- #include <unistd.h>
- int main()
- {
- std::cout<<"page size:"<<sysconf(_SC_PAGE_SIZE)<<std::endl;
- return 0;
- }
输出结果是:
- page size:4096
那么下面对映射文件的大小和映射长度的不同情况进行讨论。
(1)映射文件的大小和映射长度相同
下面的代码
- #include <iostream>
- #include <cstring>
- #include <cerrno>
- #include <unistd.h>
- #include <fcntl.h>
- #include <sys/mman.h>
- using namespace std;
- #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)
- {
- cout<<"open file "<<PATH_NAME<<" failed...";
- cout<<strerror(errno)<<endl;
- return -1;
- }
- if (ftruncate(fd, 5000) < 0)
- {
- cout<<"change file size failed...";
- cout<<strerror(errno)<<endl;
- close(fd);
- return -1;
- }
- char *memPtr;
- memPtr = (char *)mmap(NULL, 5000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- close(fd);
- if (memPtr == MAP_FAILED)
- {
- cout<<"mmap failed..."<<strerror(errno)<<endl;
- return -1;
- }
- cout<<"[0]:"<<(int)memPtr[0]<<endl;
- cout<<"[4999]:"<<(int)memPtr[4999]<<endl;
- cout<<"[5000]:"<<(int)memPtr[5000]<<endl;
- cout<<"[8191]:"<<(int)memPtr[8191]<<endl;
- cout<<"[8192]:"<<(int)memPtr[8192]<<endl;
- cout<<"[4096 * 3 - 1]:"<<(int)memPtr[4096 * 3 - 1]<<endl;
- cout<<"[4096 * 3]:"<<(int)memPtr[4096 * 3]<<endl;
- return 0;
- }
执行结果如下:
- [0]:0
- [4999]:0
- [5000]:0
- [8191]:0
- [8192]:91
- [4096 * 3 - 1]:0
- Segmentation fault
用下图来分析执行结果:
图4 映射文件的大小和映射长度相同
由执行结果可以看到,能够完整的访问到前三页,在访问第四页的时候会产生SIGSEGV信号,发生Segmentation fault段越界访问错误。按照《UNIX 网络编程 卷2:进程间通信》中P257的讲解,内核会为该内存映射两个页面,访问前两个页面不会有问题,但访问第三个页面会产生SIGSEGV错误信号。这个差异具体应该是底层实现有关,留待以后研究。
(2)映射文件的大小小于映射长度
在上面代码的基础上,修改mmap内存映射还是部分的第二个参数,如下:
- memPtr = (char *)mmap(NULL, 4096 * 3, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
执行结果如下:
- [0]:0
- [4999]:0
- [5000]:0
- [8191]:0
- Bus error
再修改访问代码,访问偏移4096*3以后的内存映射去,结果的情况是
- [4096 * 3]:91
- [4096 * 4 - 1]:0
- Segmentation fault // memPtr[4096*4]
用下图来分析执行结果:
该执行结果和之前的映射文件大小和映射长度相同的情况有比较大的区别,在访问内存映射区内部但超出底层支撑对象的大小的区域部分会产生SIGBUS错误信息,产生生BUS error错误,但访问第四页不会出问题,访问第四页以后的内存区就会产生 SIGSEGV错误信息。按照《UNIX 网络编程 卷2:进程间通信》中P258的讲解,访问第三个页面以后的内存会产生SIGSEGV错误信号。这个差异具体应该是底层实现有关,留待以后研究。
3 mmap实现进程间通信
下面将介绍mmap本身提供的进程间通信的两种方式,分别用于无亲缘和亲缘进程间的通信。
(1)通过匿名内存映射提供亲缘进程间的通信
我们可以通过在父进程fork之前指定MAP_SHARED调用mmap,通过映射一个文件来实现父子进程间的通信,POSIX保证了父子进程的内存映射关系保留到子进程中,父子进程对内存映射去的修改双方都可以看到。
在Linux 2.4以后,mmap提供匿名内存映射机制,即将mmap的flags参数指定为:MAP_SHARED | MAP_ANON。这样就彻底避免了内存映射文件的创建和打开,简化了对文件的操作。匿名内存映射机制的目的就是为了提供一个穿越父子进程间的内存映射区,很方便的提供了亲缘进程间的通信,下面是测试代码:
- #include <iostream>
- #include <cstring>
- #include <cerrno>
- #include <unistd.h>
- #include <fcntl.h>
- #include <sys/mman.h>
- using namespace std;
- int main(int argc, char **argv)
- {
- int *memPtr;
- memPtr = (int *) mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, 0, 0);
- if (memPtr == MAP_FAILED)
- {
- cout<<"mmap failed..."<<strerror(errno)<<endl;
- return -1;
- }
- *memPtr = 0;
- if (fork() == 0)
- {
- *memPtr = 1;
- cout<<"child:set memory "<<*memPtr<<endl;
- exit(0);
- }
- sleep(1);
- cout<<"parent:memory value "<<*memPtr<<endl;
- return 0;
- }
执行结果如下:
- child:set memory 1
- parent:memory value 1
(2)通过内存映射文件提供无亲缘进程间的通信
通过在不同进程间对同一内存映射文件进行映射,来进行无亲缘进程间的通信,如下测试代码:
- //process 1
- #include <iostream>
- #include <cstring>
- #include <errno.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include <sys/mman.h>
- using namespace std;
- #define PATH_NAME "/tmp/memmap"
- int main()
- {
- int *memPtr;
- int fd;
- fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
- if (fd < 0)
- {
- cout<<"open file "<<PATH_NAME<<" failed...";
- cout<<strerror(errno)<<endl;
- return -1;
- }
- ftruncate(fd, sizeof(int));
- memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- close(fd);
- if (memPtr == MAP_FAILED)
- {
- cout<<"mmap failed..."<<strerror(errno)<<endl;
- return -1;
- }
- *memPtr = 111;
- cout<<"process:"<<getpid()<<" send:"<<*memPtr<<endl;
- return 0;
- }
- //process 2
- #include <iostream>
- #include <cstring>
- #include <errno.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include <sys/mman.h>
- using namespace std;
- #define PATH_NAME "/tmp/memmap"
- int main()
- {
- int *memPtr;
- int fd;
- fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
- if (fd < 0)
- {
- cout<<"open file "<<PATH_NAME<<" failed...";
- cout<<strerror(errno)<<endl;
- return -1;
- }
- memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- close(fd);
- if (memPtr == MAP_FAILED)
- {
- cout<<"mmap failed..."<<strerror(errno)<<endl;
- return -1;
- }
- cout<<"process:"<<getpid()<<" receive:"<<*memPtr<<endl;
- return 0;
- }
执行结果如下:
- # ./send
- process:12711 send:111
- # ./recv
- process:12712 receive:111
上面的代码都没进行同步操作,在实际的使用过程要考虑到进程间的同步,通常会用信号量来进行共享内存的同步。
4 基于mmap的POSIX共享内存
上面介绍了通过内存映射文件进行进程间的通信的方式,现在要介绍的是通过POSIX共享内存区对象进行进程间的通信。POSIX共享内存使用方法有以下两个步骤:
- 通过shm_open创建或打开一个POSIX共享内存对象;
- 然后调用mmap将它映射到当前进程的地址空间;
和通过内存映射文件进行通信的使用上差别在于mmap描述符参数获取方式不一样:通过open或shm_open。如下图所示:
POSIX共享内存区对象的特殊操作函数就只有创建(打开)和删除两个函数,其他对共享内存区对象的操作都是通过已有的函数进行的。
- #include <sys/mman.h>
- int shm_open(const char *name, int oflag, mode_t mode);
- //成功返回非负的描述符,失败返回-1
- int shm_unlink(const char *name);
- //成功返回0,失败返回-1
shm_open用于创建一个新的共享内存区对象或打开一个已经存在的共享内存区对象。
name:POSIX IPC的名字,前面关于POSIX进程间通信都已讲过关于POSIX IPC的规则,这里不再赘述。
oflag:操作标志,包含:O_RDONLY,O_RDWR,O_CREAT,O_EXCL,O_TRUNC。其中O_RDONLY和O_RDWR标志必须且仅能存在一项。
mode:用于设置创建的共享内存区对象的权限属性。和open以及其他POSIX IPC的xxx_open函数不同的是,该参数必须一直存在,如果oflag参数中没有O_CREAT标志,该位可以置0;
shm_unlink用于删除一个共享内存区对象,跟其他文件的unlink以及其他POSIX IPC的删除操作一样,对象的析构会到对该对象的所有引用全部关闭才会发生。
POSIX共享内存和POSIX消息队列,有名信号量一样都是具有随内核持续性的特点。
下面是通过POSIX共享内存进行通信的测试代码,代码中通过POSIX信号量来进行进程间的同步操作。
- //process 1
- #include <iostream>
- #include <cstring>
- #include <errno.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include <semaphore.h>
- #include <sys/mman.h>
- using namespace std;
- #define SHM_NAME "/memmap"
- #define SHM_NAME_SEM "/memmap_sem"
- char sharedMem[10];
- int main()
- {
- int fd;
- sem_t *sem;
- fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
- sem = sem_open(SHM_NAME_SEM, O_CREAT, 0666, 0);
- if (fd < 0 || sem == SEM_FAILED)
- {
- cout<<"shm_open or sem_open failed...";
- cout<<strerror(errno)<<endl;
- return -1;
- }
- ftruncate(fd, sizeof(sharedMem));
- char *memPtr;
- memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- close(fd);
- char msg[] = "yuki...";
- memmove(memPtr, msg, sizeof(msg));
- cout<<"process:"<<getpid()<<" send:"<<memPtr<<endl;
- sem_post(sem);
- sem_close(sem);
- return 0;
- }
- //process 2
- #include <iostream>
- #include <cstring>
- #include <errno.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include <semaphore.h>
- #include <sys/mman.h>
- using namespace std;
- #define SHM_NAME "/memmap"
- #define SHM_NAME_SEM "/memmap_sem"
- int main()
- {
- int fd;
- sem_t *sem;
- fd = shm_open(SHM_NAME, O_RDWR, 0);
- sem = sem_open(SHM_NAME_SEM, 0);
- if (fd < 0 || sem == SEM_FAILED)
- {
- cout<<"shm_open or sem_open failed...";
- cout<<strerror(errno)<<endl;
- return -1;
- }
- struct stat fileStat;
- fstat(fd, &fileStat);
- char *memPtr;
- memPtr = (char *)mmap(NULL, fileStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- close(fd);
- sem_wait(sem);
- cout<<"process:"<<getpid()<<" recv:"<<memPtr<<endl;
- sem_close(sem);
- return 0;
- }
程序的执行结果如下:
- # ./send
- process:13719 send:yuki...
- # ./recv
- process:13720 recv:yuki...
在Linux 2.6.18中,对于POSIX信号量和共享内存的名字会在/dev/shm下建立对应的路径名,例如上面的测试代码,会生成如下的路径名:
- # ll /dev/shm/
- total 8
- -rw-r--r-- 1 root root 10 Aug 13 00:28 memmap
- -rw-r--r-- 1 root root 32 Aug 13 00:28 sem.memmap_sem
Aug 13 AM 01:10 @dorm
相关推荐
共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。这篇文章主要介绍了详解Linux进程间通信——使用共享内存,有兴趣的可以了解一下。
之前用过Prosix版本的共享内存和信号量,一直没有实践System V版本的,主要是因为其信号量集的概念操作有些复杂,今天试着写一个SV版本的共享内存进程间通信,使用信号量同步。程序提供了几个简单的用于操作SV版本...
Linux安全攻略——僵尸进程.pdf中讨论了Linux操作系统中的进程管理机制,包括进程的概念、进程调度、进程树、进程标识符、进程生命周期等方面的知识点。 进程概念 在Linux操作系统中,进程是动态的,程序是静态的...
2. **虚拟内存**:Linux采用虚拟内存机制,使得每个进程都有独立的4GB(或更大,取决于架构)地址空间。这通过页表(Page Table)实现,页表映射了虚拟地址到物理地址的关系。Linux还支持分页和分段两种虚拟内存机制...
这可能涉及到进程的创建(fork()),进程间通信(管道、信号量、共享内存),以及进程状态(运行、睡眠、停止、僵尸等)。 4. **系统调用**:描述中提到的“相关的系统编程的函数理解”可能是指Linux中的系统调用,...
linux下共享内存+信号量,不会出奇怪的错误,如信号量和共享内存未清,导致无法再次运行,ctrl+c后能够正常清除信号量及共享内存。
整体来看,本文深入探讨了管道和共享内存这两种Linux进程通信机制的实现原理、方法和使用示例,旨在帮助读者理解和掌握这两种技术在进程间通信中的应用。通过实际的代码实例,我们可以看到如何在Linux环境下使用这些...
在linux实现共享内存的创建、写入数据、读取数据
本篇主要讨论Linux下的几种进程通信机制,包括POSIX无名信号量、System V信号量、System V消息队列以及System V共享内存。 1. **POSIX 无名信号量** POSIX无名信号量是用于同步和保护共享资源的一种机制。信号量的...
关于信号量的文章,生产者消费者问题与读者写者问题---信号量机制,PV操作——进程同步的信号量问题,利用信号机制实现的 父子进程同步,嵌入式linux的学习笔记-进程间通信的信号与信号集(四)1)进程的同步与互斥 ...
另外,进程间通信(IPC)是Linux系统中多进程协同工作的基础,常见的IPC机制包括管道、信号量、消息队列、共享内存等。此外,进程还有调度和优先级的概念,通过nice值和实时优先级来调整进程的执行优先级。 然后,...
"Linux进程间通信——使用共享内存.docx"将详细讲解如何创建、映射和操作共享内存,以及如何配合信号量确保数据一致性。 除了上述文档,链接文件如"CSDN博客中的相关文章"将提供额外的信息,这些博客可能包含作者...
### Linux高级编程知识点总结——进程线程 #### 进程与程序的区别及PCB - **进程与程序的区别**...以上就是关于Linux进程和线程的高级编程知识点总结,涉及到了进程的基础概念、创建、管理以及特殊状态等方面的内容。
4. 进程间通信:了解管道、消息队列、共享内存、套接字等进程间通信方式。 七、系统安全与防火墙 1. SSH安全:学会配置SSH服务,提高远程登录的安全性。 2. 权限策略:设置sudo,实现非root用户的权限控制。 3. ...
Linux提供了多种IPC机制,如管道(Pipe)、命名管道(FIFO)、信号量(Semaphore)、消息队列(Message Queue)、共享内存(Shared Memory)和套接字(Socket)。这些机制允许进程之间交换数据,协同工作。 线程...
在计算机科学中,进程间的通信(Inter-Process Communication, IPC)是操作系统中一个重要的概念,它允许不同进程之间共享数据和资源,...对于更复杂的需求,可以考虑使用消息队列、共享内存或套接字等其他IPC机制。
3. **文件锁**:在多进程或多线程环境中,为了防止多个进程同时读写同一文件导致的数据不一致,Linux提供了文件锁机制。文件锁分为记录锁(记录内部分锁)和共享锁(文件范围锁),通过flock()或fcntl()函数实现。...