title: 进程间的通信—管道、内存映射
date: 2019-08-07 08:41:50
tags: [Linux,进程]
categories: Linux
1、进程间的通信方式
Linux 进程间通信方式主要有下面 6 种:
-
管道:(无名管道) 管道有亲缘关系进程间的通信,有名管道还允许无亲缘关系进程间通信。
-
信号 signal:在软件层模拟中断机制,通知进程某事发生
-
消息队列:消息的链表包括 posix 消息队列和 SystemV 消息队列
-
共享内存:多个进程访问一块内存主要用于同步
-
信号量:进程间同步
-
套接字 socket:不同机器间进程通信
管道通信: 有名管道通信&无名管道通信。
2、无名管道
无名管道创建必须在fork创建子进程之前创建,不然子进程没法复制端口号,无法通信
①、无名管道的特点
- 无名管道 应用在具有亲缘关系的进程间通信 。
- 无名管道的创建在内核当中 是不可见的
- 无名管道的具体的操作 是可以通过 read write 来操作
- 管道通信是单向通信,同一时刻只能向一个方向读写数据。
把无名管道 比作一根水管 ,水管一端进水 、一端出水,进水这端可以称作写端口,写端口的作用是向管道写数据;出水这端 就是读端口,读端口的作用就是将管道中的数据读 出来 。
读写端口是一组特殊的文件描述符 。其中 fd[0] ------是读端 fd[1] : ------是写端
②、无名管道的创建
函数原型 | #include <unistd.h> int pipe(int pipefd[2]); |
---|---|
功能 | 创建一个无名管道 |
参数 | fd[0]:读端口 fd[1]:写端口 传出参数 |
成功:0 失败:-1,并设置错误码 |
注意 : 如果读管道,读不到数据的时候 程序会阻塞,直到有数据接收到 。
案例:无名管道只适合应用在具有亲缘关系的进程间通信,下面利用两根管道实现双向通信
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd[2];
int fd2[2];
char buf[50] = {'0'};
int ret=pipe(fd); //创建无名管道
if(ret < 0)
{
perror("pipe\r\n");
exit(1);
}
int ret2=pipe(fd2);
if(ret2 < 0)
{
perror("pipe2\r\n");
exit(1);
}
pid_t pid = fork(); //创建子进程
if(pid < 0)
{
printf("fork is error\r\n");
exit(1);
}else if(pid == 0)
{
while(1)//子进程
{
read(fd[0],buf,sizeof(buf));
printf("父进程回应:%s\r\n",buf);
printf("子进程发送:\r\n");
memset(buf,0,sizeof(buf));
gets(buf);
write(fd2[1],buf,sizeof(buf));
}
}else
{
while(1) //父进程
{
printf("父进程发送\r\n");
memset(buf,0,sizeof(buf));
gets(buf);
write(fd[1],buf,sizeof(buf));
read(fd2[0],buf,sizeof(buf));
printf("子进程回应:%s\r\n",buf);
}
wait(NULL);
}
close(fd[0]);
close(fd[1]);
return 0;
}
思考: 如果当管道的读端口关闭 ,写进程向管道的写端写数据 ,有没有意义 ?
没有意义 ,当管道的读端关闭 ,写进程继续向管道的写端 写数据 ,内核会向 写进程发送一个SIGPIEP 信号,来终止当前的进程 。如果希望 执行完毕 可以 利用 signal 来对信号 进行安装 处理,编写处理函数,即对该信号不执行默认操作。
3、有名管道
管道没有读到数据的时候 一直 会阻塞 直到读到数据之后阻塞结束 。
管道的读端与管道的写端 :
创建一个有名管道之后,先要打开管道文件,对管道进行读写操作
如果以读的方式来打开管道文件,则会打开文件的读端口 ;以写的方式打开文件,则打开文件的写端口 。
①、无名管道的特点
- 解决陌生进程间通信 关系
- 有名管道本质上是一个节点
- 有名管道在用户层 是可见的
- 既然在用户层是可见 ,就是文件 ,要操作文件先要打开文件,再对文件进行读写操作 。
②、有名管道的创建
1、通过命令mkfifo创建
管道的创建 : mkfifo
管道的删除:unlink
管道文件的验证
2、通过函数创建
函数原型 | int mkfifo(const char*pathname, mode_t mode); |
---|---|
函数功能 | 创建一个有名管道 |
参数 | pathname : 创建管道的路径 mode : 与open的mode 一致 ,参数mode为该文件的权限(mode%~umask) |
返回值 | 成功:0 失败:-1 |
函数原型 | int unlink(const char *pathname); |
---|---|
功能 | 删除管道文件 |
参数 | pathname:要删除的管道文件 |
返回值 | 成功:0 失败:-1,并设置错误码 |
案列:运用有名管道,实现两个陌生进程间的通信
实验现象:
4、标准流管道popen、pclose
Linux 的文件操作中有基于标准 I/O 操作一样,管道操作也支持基于文件流的操作,这种基于文件流的管道。主要用来创建一个连接到另一个进程的管道,这里“另一个进程”是可以执行一定操作的可执行文件,例如用户执行“ls -l”或者****./pipe,由于这类操作很常见,所以将一系列创建过程合并到一个函数 popen()中完成。
函数原型 | FILE *popen(const char *command,const char *type); |
---|---|
功能 | 在一个进程当中 实现另外一个进程 |
参数 | commad : 要 执行的指令 比如: ls cat type :以什么样的方式: r -----以读的方式 w: 以写的方式 |
返回值 | 成功:文件流 失败:NULL |
函数原型 | int pclose(FILE *stream); |
---|---|
功能 | 功能: 关闭标准流管道 |
参数 | 上面函数的返回值 |
标准流管道的实现过程 (popen函数实现过程)
- 创建一个 pipe 管道
- fork 一个进程
- 关闭无用的端口
- 在子进程中 调用 exec 函数族 来 执行 要执行的指令 (可以将子进程中的内容读到父进程 ,也可以将父进程中的内容 写到 子进程中)
- 调用wait,waitpid回收子进程
5、内存映射
Linux 系统利用已有的存储管理机制可以很自然的实现进程间的共享存储.mmap是一种 内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写页面带对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
相应的系统调用.
函数原型 | void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); |
---|---|
功能 | 将文件或其他对象映射到进程中 |
参数 | addr:要映射的内存地址,一般使用NULL 表示 系统自动分配 内存地址 length:映射区域的大小 prot:方式,有以下三种 PROT_EXEC: 可执行 PROT_READ : 可读 PROT_WRITE: 可写。使用可读写时可以将两个相或 flags: MAP_SHARED: 共享的 ,经常使用在多进程的通信中,即修改进程映射区域的内容会写回到文件当中,操作进程中映射的区域会同步到文件当中 MAP_PRIVATE:写入时复制,当修改操作内存映射区域,不会将修改的内容 写回到源文件当中。 fd: 要映射的文件 offset : 偏移量一般是文件的起始位置,给0值开始偏移 |
返回值 | 成功:映射建立成功,成功之后的映射区域的首地址 失败:(void *) -1) |
注意: 一般将一个文件映射到进程中,这个文件不能为空文件。
函数原型 | int munmap(void *addr, size_t length); |
---|---|
功能 | 取消mmap建立的映射 |
参数 | addr:mmap的返回值 映射后 的首地址, length:映射区域的大小 |
返回值 | 成功:0 |
案例: 利用映射。实现两个陌生进程通信
结果: