管道是进程间的通信方式之一,在学习管道之前,首先了解一下进程间为什么要通信?
- 进程间为什么要进行通信?
因为进程的独立性,使得进程的交流变得困难、复杂,因此就产生了进程间的各种通信方式。
- 进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另外一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或另一组进程发送消息,通知它(它们)发生了某事件
- 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。
- 什么是管道?
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,管道是Unix中最古老的进程间通信的形式,管道的本质是内核的一块缓冲区。
- 管道的分类:
- 匿名管道:创建的缓冲区是没有名字的,不可见于文件系统,仅用于具有亲缘关系的进程间的通信。
- 命名管道:可见于文件系统,是一个特殊文件(管道类型文件),可用于同一主机上的任意进程间的通信。
- 管道的创建:
因为Linux下一切接文件,操作系统为管道提供的操作的方法:文件操作
a).匿名管道的创建: pipe(int fd[2])
匿名管道创建成功后悔返回两个文件描述符供我们对管道进行操作,这个操作也就是我们的io操作: fd[0]:用于管道读取数据;fd[1]:用于向管道写入数据
匿名管道的原理其实就是创建一个子进程,子进程复制了父进程的描述附表,因此也有两个描述符,并且它们指向的是同一管道,这时候它们连个都能访问到这个管道,因此他们之间就可以通信了。
- 命名管道的创建:
i)命令创建: mkfifo pipe_filename
ii)代码创建: int mkfifo(const char*pathname, mode_t mode)
一个命名管道打开之后,则所有特性和匿名管道完全相同
- 管道的读写规则:
(1)管道无数据:读取
如果描述符是默认的阻塞属性,读取会挂起等待,直到管道有数据
如果描述符被设置为非阻塞属性,读取操作将不具备条件,直接报错返回EAGAIN。
(2)管道数据写满了:写入
如果描述符是默认的阻塞属性,写入将会挂起等待,直到有数据被读取
如果描述符被设置为非阻塞属性,写入操作将不具备条件,直接报错回EAGIN。
- 如果写入端全部关闭,这时候如果读取数据,读取完管道中的数据,然后返回0.
- 如果读端全部关闭,这时候写入数据,则会触发异常,操作系统会给进程发送SIGPIPE信号,进程收到这个信号会退出。
- 当写入的数据大小超过PIPE_BUFFER,那么这个操作是一个非原子操作,有可能被打断。
- 管道的打开特性
匿名管道在创建后就直接打开返回描述符
命名管道打开特性:
(1)如果以只读打开命名管道,那么open函数将阻塞等待,直到有其他进程以写的方式打开这个命名管道
(2)如果以只写打开命名管道,那么open函数将阻塞等待,直到有其他的进程以读的方式打开这个命名管道
(3)如果命名管道以读写方式打开,将不会阻塞
8.管道的特点:
(1)管道的通信方式是半双工,单向通信,需要双方通信时,应建立两个管道。
(2)管道提供面向字节流的数据传输服务(传输的数据无边界,无规则,收发灵活)。
(3)进程退出,管道便被释放,所以管道的生命周期随进程
(4)管道自带同步与互斥
同步:临界资源访问的时序可控性
互斥:对临界资源同一时间的唯一访问性
9.匿名管道与命名管道的区别:
(1)匿名管道由pipe函数创建打开,命名管道由mkfifo函数创建,且打开方式与匿名管道的打开方式不同,一旦这些工作完成后,它们具有相同的语义。
(2)匿名管道仅用于具有亲缘关系间的进程通信,而命名管道可用于统一主机上任意进程间的通信。
匿名管道的创建代码:
//这是一个匿名管道的实现,功能:父进程写如数据,子进程读取数据
//匿名管道仅能用于具有亲缘关系的进程间的通信
//int pipe(int pipefd[2]);
//pipefd :用于接收匿名管道创建成功后返回的两个描述符
//pipefd[0]:用于从管道读取数据
//pipefd[1]:用于向管道写入数据
//pipe 函数调用成功,就会返回一对描述符
//
//成功返回0,失败返回-1
//
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
int fd[2];
//管道需要在创建子进程之前创建好,这样才能复制
if(pipe<0){
perror("pipe error");
return -1;
}
int pid =-1;
pid = fork ();
if (pid < 0){
return -1;
}else if(pid ==0){
//child read
close(fd[1]);
char buff[1024]={0};
sleep(3);
read(fd[0],"buff",1024);
printf("child: %s",buff);
close(fd[0]);
}else{
//parent write
//父进程写入数据,因此需要将管道的读取端关闭
close(fd[0]);
write(fd[1],"hello",5);
write(fd[1],"world",5);
close(fd[1]);
}
return 0;
}
命名管道的创建代码:
//这是一个命名管道的操作代码,从命名管道中读取数据
//1.创建一个命名管道:
// int mkfifo(const char* pathname , mode_t mode);
// pathname :管道文件的路径名
// mode : 管道文件的权限
// 成功 :0 失败:-1
// 2.打开管道open
// 3.从管道读取数据 read
// 4.关闭管道文件 close
//
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
umask(0);
//1.创建命名管道
//为了防止因为管道文件已经存在,每次创建失败
if(mkfifo("./test.fifo",0664)<0){
if(errno == EEXIST){
}else{
perror("mkfifo error");
}
return -1;
}
//2.打开管道文件
int fd = open ("./test.fifo‘, O_RDONLY ");
if(fd < 0){
perror("open fifo error");
return -1;
}
printf("open fifo file sucess!! read start!!");
while(1){
char buff[1024] = {0};
int ret=read(fd,buff,1023);
if(ret >0){
printf("client say:[%s]\n",buff);
}else if(ret == 0){
//管道特性:如果所有的写段都关闭,那么读取时返回0
printf("all write point close!!\n");
sleep(1);
}
}
close(fd);
return 0;
}