几种常见进程间通信(IPC)方式之共享存储
几种常见进程间通信(IPC)方式-共享存储
前言
进程间通信是指在不同进程之间传播或交换信息,在Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间,进程之间不能相互访问。必须通过内核才能进行数据交换。如图:
常见的通信方式有以下几种:
- 管道pipe
- 有名管道FIFO
- 消息队列MessageQueue
- 共享存储
- 信号量Semaphore
- 信号Signal
- 套接字Socket
接下来我们将详细介绍共享存储
共享存储
内存映射I/O
在讲解内存映射之前,我们先简单了解一些虚拟内存的概念。
虚拟内存为每个进程提供了一个大的、一致的和私有的地址空间,它提供了3个能力:
- 将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据。
- 它为每个进程提供了一致的地址空间,从未简化了内存管理。
- 它保护了每个进程的地址空间不被其他进程破坏。
VM系统将虚拟内存分割为虚拟页,物理内存也被分隔为物理页。
使用寄存器中的内存管理单元[MMU(Memory Management Unit)],利用存放在主存中的页表来动态翻译虚拟地址,就可以使用虚拟地址去访问相应的物理地址。
内存映射
内存映射,通过将虚拟内存区域与磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容。
磁盘上的对象可以是两类,Linux文件系统中的普通文件,或是匿名文件(由内核创建)。
共享内存映射
将虚拟地址指向同一个物理地址。如图所示
mmap
函数
Linux进程可以使用mmap
函数来创建新的虚拟内存区域,并将对象映射到这些区域中。
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
//成功:返回创建的映射区首地址;失败:MAP_FAILED宏
参数
- addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
- length: 欲创建映射区的大小
- prot: 映射区权限
PROT_READ
、PROT_WRITE
、PROT_READ|PROT_WRITE
- flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED
: 会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE
: 映射区所做的修改不会反映到物理设备。 - fd: 用来建立映射区的文件描述符
- offset: 映射文件的偏移(4k的整数倍)
mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
int munmap(void *addr, size_t length);
//成功:返回0; 失败:返回-1
mmap
进程间通信
- 父子等有血缘关系的进程之间可以通过
mmap
建立的映射区来完成数据通信。但是相应的要在创建映射区的时候指定对应的标志位参参数flags。
MAP_PRIVATE
(私有映射)父子进程各自独占映射区
MAP_SHARED
(共享映射)父子进程共享映射区 - 也可以通过匿名映射,不需要以来一个文件就能够实现。同样也需要依赖于标志位的设定(只适用于有血缘关系的进程之间通信)
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
- 没有血缘关系的进程之间通信时需要,用open打开同一个文件 得到fd,再调
mmap
指定fd,将虚拟内存地映射到同一个物理内存地址上。
代码样例 父子进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放条件.
//将参数fd指定的文件大小改为参数length指定的大小
ftruncate(fd, 4);
//MAP_SHARED
//父子进程各自独占映射区
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//MAP_PRIVATE
//父子进程共享映射区
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork(); //创建子进程
if(pid == 0){
*p = 2000;
//非共享变量
//在子进程中改为1000,父进程中仍为100
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}
文件进程间通信
使用文件也可以完成进程间通信,父进程使用fork
创建子进程后,父子进程共享文件描述符,也就是说,共享打开的文件。
/*
*父子进程共享打开的文件描述符------使用文件完成进程间通信.
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(void)
{
int fd1, fd2; pid_t pid;
char buf[1024];
char *str = "---------test for shared fd in parent child process-----\n";
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
//子进程进入
} else if (pid == 0) {
fd1 = open("test.txt", O_RDWR);
if (fd1 < 0) {
perror("open error");
exit(1);
}
write(fd1, str, strlen(str));
printf("child wrote over...\n");
//父进程进入
} else {
fd2 = open("test.txt", O_RDWR);
if (fd2 < 0) {
perror("open error");
exit(1);
}
sleep(1); //保证子进程写入数据
int len = read(fd2, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
wait(NULL);
}
return 0;
}
看到这里你可能会有一些疑惑,文件进程间通信和有名管道FIFO有什么区别呢?
我们知道管道只有两端,读端和写端,有名管道可以控制读写两端,保证他们的同步性。
但是对于文件进程间进程通信而言,我们必须自己定义一些同步机制来控制读数据和写数据,否则可能会造成一些错误。
总结
共享存储是非常重要的一种进程间通信方式,理解起来也相对来说比较困难有点,但是在了解了一些基本概念之后再去学习就会容易得多。
更多推荐
所有评论(0)