初识管道
- 管道是Unix中最古老的进程间通信方式
- 我们把从一个进程连接到另一个进程的一个数据流称为一个”管道”
- 默认情况下,一个进程都默认打开3个设备文件:一个标准输入设备(键盘)、标准输出设备(显示器)、标准错误输出设备(显示器)。且默认从标准输入中读取数据,将正确的信息以及错误的信息写入到标准输出。使用管道” | “可以将两个命令连接起来,从而改变标准的输入输出方式。例如:
- rpm -qa命令表示:列出所有被安装的rpm package;
- grep telnet 命令表示:匹配telnet关键字
- 所以上述命令是将rpm -pa的输出作为grep telnet命令的输入。连接输入/输出的中间设备即为一个管道文件。因此使用管道可以将一个命令的输出作为另一个命令的输入
匿名管道——PIPE
概念
- 在前面一小节中,我们已经初步认识了管道的作用,即可以将一个命令的输出作为另一个命令的输入(运行时,一个命令创建一个进程),而这种管道是临时的,命令执行完成后将自动消失,通常我们称这类管道为匿名管道
- 匿名管道的创建:
#include <unistd.h>
int pipe(int fd[2]);
- 参数:pipefd[2]:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
- 返回值:成功返回0,失败返回错误码
- 管道创建实例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char buf[100];
int fd[2];
int len;
if(pipe(fd) == -1)
{
perror("pipe");
return -1;
}
//1.从标准输入中读取数据
while(fgets(buf,100,stdin))
{
len = strlen(buf);
//2.通过管道的写端将数据写入到管道中
if(write(fd[1],buf,len)!=len)
{
perror("write to pipe!");
return -1;
}
memset(buf,0x00,sizeof(buf));
//3.从管道中读取数据
if(read(fd[0],buf,sizeof(buf)!=len)
{
perror("read from pipe");
return -1;
}
//4.将数据写到屏幕上
if(write(1,buf,len)!=len)
{
perror("write to stdout");
return -1;
}
}
return 0;
}
- 程序运行结果:
读写匿名管道
- 匿名管道的读写和普通文件不一样。任何进程读、写匿名管道时必须确认还存在一个进程(这个进程可以是自己),该进程以写/读的方式(即读对应写,写对应读)访问管道(即可以操作相应的文件描述符)
- 读写管道使用的系统调用是read和write,两者都默认以阻塞方式读写管道
当没有数据可读时:
- O_NONBILOCK disable:read调用阻塞,即进程暂停执行,一直到有数据来为止
- O_NONBILOCK enable:read调用返回-1,errno值为EAGAIN
当管道满的时候:
- O_NONBILOCK disable:write调用阻塞,直到有进程读走数据
O_NONBILOCK enable:write调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性,否则不能保证
进程共享管道原理
- 父进程创建子进程后,关掉各自不用的文件描述符
- 比如图中所示,若我们想要通过管道来实现:子进程从父进程处读取信息,则父进程需要关掉读端,子进程需要关掉写端,父进程往管道中写数据,子进程从管道中读数据,这样父子进程间就可以实现单向通信了
- 通过程序实例来实现上述过程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int fd[2];
char buf[200];
int len;
if(pipe(fd) == -1)
{
perror("pipe");
return -1;
}
fgets(buf,200,stdin);
len=strlen(buf);
pid_t pid;
pid = fork();
if(pid > 0) //父进程
{
//关闭读端,将标准输入写入写端,让子进程来读取
close(fd[0]);
printf("父进程写入管道:%s",buf);
if(write(fd[1],buf,len) != len)
{
perror("write to pipe");
return -1;
}
memset(buf,0x00,sizeof(buf));
}else if(pid == 0) //子进程
{
//关闭写端,从管道读端读取父进程写入的数据,写到屏幕上
close(fd[1]);
if(read(fd[0],buf,sizeof(buf)) != len)
{
perror("read from pipe");
return -1;
}
close(fd[0]);
printf("子进程从管道读取:%s",buf);
sleep(1);
}else
{
perror("fork()");
return -1;
}
printf("\n");
return 0;
}
- 程序运行结果:
管道特点
- 只能用于具有亲缘关系的进程(例如父子进程)之间通信;通常,一个管道有一个进程创建,然后该进程调用fork,此后父、子进程之间就可以通行
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以说管道的生命周期随进程
- 管道是半双工的,数据只能向一个方向流动;需要双方通行时,需要建立起两个管道
有名管道——FIFO
概念
- 上面我们提到匿名管道只能用于具有亲缘关系的进程,那么没有亲缘关系的进程如何通过管道进行通信,命名管道有效的解决了这个问题,命名管道依赖于文件系统,是一个存在的特殊文件,实现不同进程对文件系统下的某个文件的访问是很方便实现的,因此FIFO可以在同主机任意进程之间实现通信
- 命名管道和普通文件一样具有磁盘存放路径、文件权限和其他属性;但是,命名管道和普通管道又有区别,命名管道并没有在磁盘中存放真正的信息,它存储的通信信息在内存中,两个进程结束后自动丢失,拥有一个磁盘路径仅仅是一个接口,其目的是使进程间信息的编程更简单统一
- 通行的两个进程结束后,命名管道的文件路径本身依然存在,这是和匿名管道不一样的地方
- 命名管道的创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- pathname:要创建的管道文件名
- mode: 文件的权限
- 返回值:执行成功返回0,失败返回-1
- mkfifo()建立的FIFO文件,可以通过读写一般文件的方式。当时用open()打开FIFO文件时,O_BLOCKLOCK会有影响
读写命名管道
- 在通过read和write系统调用来执行读写操作前,需要用open()函数打开该文件;另外,操作有名管道的阻塞位置为open位置,而不是匿名管道的读写位置
如果当前打开操作是为读而打开FIFO时:
- O_BLOCKLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_BLOCKLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时:
- O_BLOCKLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_BLOCKLOCK enable:立刻返回失败,错误码为ENXIO
两进程已经完成打开管道操作,中途其中一个进程退出
- 未退出一端若为写操作,将返回SIGPIPE信号
- 未退出一端如果是阻塞读操作,读操作将不再阻塞,直接返回0
命名管道应用实例
- 文件拷贝
- 程序代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
mkfifo("fifo",0644);
//读端
int infd = open("file",O_RDONLY);
if(infd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
//写端
int outfd = open("fifo",O_WRONLY);
if(outfd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
char buf[1024];
int n = read(infd,buf,1024);
if(n > 0)
{
write(outfd,buf,n);
}
close(infd);
close(outfd);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <error.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
//打开copy文件的写端
int outfd = open("file_copy",O_WRONLY | O_CREAT | O_TRUNC,0644);
if(outfd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
//打开管道的读端
int infd = open("fifo",O_RDONLY);
if(infd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
char buf[1024];
int n = read(infd,buf,1024);
if(n > 0)
{
write(outfd,buf,n);
}
close(infd);
close(outfd);
//删除命名管道
unlink("fifo");
return 0;
}
- 运行结果:
- 文件拷贝结果:
- server&&client通信
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>
#include <fcntl.h>
int main(void)
{
mkfifo("svr_cli",0644);
char buf[1024];
int outfd = open("svr_cli",O_RDONLY);
if(outfd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
while(1)
{
buf[0] = 0;
ssize_t n = read(outfd,buf,sizeof(buf)-1);
if(n > 0)
{
buf[n-1] = 0;
printf("client say:%s\n",buf);
}else if(n == 0)
{
printf("client quit...\n");
exit(EXIT_SUCCESS);
}else
{
perror("read");
exit(EXIT_FAILURE);
}
}
close(outfd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <error.h>
int main(void)
{
char buf[1024];
int infd = open("svr_cli",O_WRONLY);
if(infd == -1)
{
perror("svr_cli");
exit(EXIT_FAILURE);
}
while(1)
{
printf("please say#");
fflush(stdout);
ssize_t n = read(0,buf,sizeof(buf)-1);
if(n > 0)
{
buf[n] = 0;
ssize_t s = write(infd,buf,strlen(buf));
if(s < strlen(buf))
{
perror("write");
exit(EXIT_FAILURE);
}
}else
{
perror("read");
exit(EXIT_FAILURE);
}
}
close(infd);
return 0;
}
- 程序运行结果:
管道基本特点总结
- 管道是特殊类型的文件,在满足先入先出的原则条件下可能进行读写,但不能定位读写位置
- 管道是单向的,要实现双向,需要两个管道。匿名管道一般只用于亲缘关系进程间的通信(非亲缘关系进程只能传递文件描述符),而命名管道已磁盘文件的方式存在,可以实现本机任意两进程间通信
- 阻塞问题。匿名管道无须显式打开,创建时直接返回文件描述符,而在读写是需要确定对方的存在,即阻塞于读写位置;而命名管道在打开时需要确定对方的存在,否则阻塞