1.概述
管道是最初的Unix IPC通信,可追溯到1973年的Unix第三版。尽管对于许多操作来说很有用,但它们的根本局限于没有名字,只能由亲缘关系的进程使用。这一点随着FIFO的加入System III Unix中得以改正。FIFO有时候称为命令管道(named pipe)。管道和FIFO都是使用通常的read和write函数访问的。
从技术上讲,自从可以在进程间传递描述符后,管道也能用于无亲缘关系的进程间。然而通常的说,管道通常用于具有共同祖先的进程间通信。
1.1管道的特点
(1)只能用于具有共同祖先的进程(具有亲缘关系的进程)之间通信,当然除了命名管道,命名管道有一个路径名与之关联,命名管道允许无亲缘关系的进程访问同一个FIFO;通常,一个管道由一个进程创建,然后该进程调用fork,此后,父子进程之间就可应用该管道
(2)管道的进程间通信是基于字节流的
(3)管道是基于文件形式的,自带同步互斥机制,并且只能进行单向传输
(4)一般而言,进程退出,管道释放,所以管道的生命周期是随进程的
2.管道
所有样式的Unix提供管道。它由pipe函数创建,提供一个单路(单向)数据流。
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过pipefd参数传出给用户程序两个文件描述符,pipefd[0]指向管道的读端,fpipefd[1]指向管道的写端。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
3.管道实现进程间通信
开辟了管道之后如何实现两个进程间的通信呢?比如可以按下面的步骤通信。
3.1单个进程中的管道
父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。父进程中可以通过管道一方写入数据,父进程也可以通过管道的另一方读取数据,上图很好的说明了当个进程中的管道通信方式。
3.2 父进程fork出子进程
管道的经典用途是以下方式为两个不同进程(一个是父进程一个是子进程)提供进程间通信手段。首先,由一个进程)(它将成为父进程)创建一个管道后调用fork派生一个自身的副本,如下图所示:
父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。那么这样在父进程和子进程中都有两个文件描述符分别指向管道的写端和读端。父进程和子进程都能对管道进行读写操作。
3.3 父进程关闭fd[0] 子进程关闭fd[1]
父进程关闭在这个管道的读出端fd[0],子进程关闭这个管道的写入端fd[1]。这就在父进程和子进程之间提供了一个单向数据流。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
3.4 代码实现管道通信
子进向父进程发送数据
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2];
if (pipe(fd))
{
perror("pipe");
return 1;
}
// 实现父进程写,子进程读
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 2;
}
else if (id == 0) // child
{
close(fd[1]);
char buf[128];
int cnt = 0;
while (cnt++ < 5)
{
ssize_t _s = read(fd[0], buf, sizeof(buf));
if (_s > 0)
{
buf[_s] = '\0';;
printf("father say to child: %s\n", buf);
}
else if (_s == 0)
{
printf("father close write");
break;
}
else
{
perror("read");
break;
}
}
close(fd[0]);
}
else // father
{
close(fd[0]);
char * msg = "hello world";
int cnt = 0;
while (cnt++ < 5)
{
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]);
}
return 0;
}
3.命名管道(FIFO)
管道没有名字,因此它们的最大的劣势就是只能用于有一个共同祖先进程的各个进程之间的通信。我们无法在无亲缘关系的两个进程之间创建一个管道并将它作为IPC通道(不考虑描述符的传递)。管道的缺点就是只能在有亲缘关系的进程间进行通信,针对这个缺陷,又提出来了命名管道(FIFO)的概念。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。
命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。
FIFO指的就是先进先出(first in,first on),Unix中的FIFO类似管道。他是一个单向(半双工)的数据流。不同于管道的是,每个FIFO有一个路径与之相连,从而允许无亲缘关系之间的进程访问同一个FIFO。FIFO也称为有名管道(named pipe)。
FIFO通过mkfifo函数创建
其中pathname是一个普通的Unix路径名,它是该FIFO的名字。
mode参数指的是文件权限位,类似于open的第二个参数。mkfifo命令也能创建FIOF。可以从shell脚本或者命令行中使用它。在创建一个FIFO后,它必须或者打开来读或者来写,所用的可以是open函数,也可以是某个标准I/O打开函数,列如fopen。FIOFU不能打开来既可以读又可以写,因为他是半双工的。对管道或者FIFO的write总是向末尾添加数据,对他们的read总是从开头添加数据。如果对管道或者FIFO调用lseek,这就返回错误。
3.1FIFO两个属性
1、FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
2、FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走。
3.2 代码实现命令管道通信
server向命令管道中发送数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 创建管道时需要在mode参数位置传S_IFIFO,表明创建的是命名管道
int ret = mkfifo("./.fifo", S_IFIFO | 0644);
if (ret < 0)
{
perror("mkfifo");
return 1;
}
int fd = open("./.fifo", O_WRONLY);
if (fd < 0)
{
perror("open");
return 2;
}
int cnt = 0;
char *msg = "hello world";
while (cnt++ < 5)
{
write(fd, msg, strlen(msg));
sleep(1);
}
close(fd);
return 0;
}
client向命令管道中接收数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("./.fifo", O_RDONLY);
if (fd < 0)
{
perror("open");
return 2;
}
int cnt = 0;
char buf[128];
while (cnt++ < 5)
{
ssize_t _s = read(fd, buf, sizeof(buf) - 1);
if (_s > 0)
{
buf[_s] = '\0';;
printf("server say to client: %s\n", buf);
}
else if (_s == 0)
{
printf("server close write\n");
break;
}
else
{
perror("read");
}
sleep(1);
}
close(fd);
return 0;
}
4. 命令管道和管道的区别
命名管道创建完成后就可以使用,其使用方法与管道一样;
区别在于:
(1)命名管道使用之前需要使用open()打开。这是因为:命名管道是设备文件,它是存储在硬盘上的,而管道是存在内存中的特殊文件。
(2)但是需要注意的是,命名管道调用open()打开有可能会阻塞,但是如果以读写方式(O_RDWR)打开则一定不会阻塞;
(3)命名管道以只读(O_RDONLY)方式打开时,调用open()的函数会被阻塞直到有数据可读;
(4)命名管道如果以只写方式(O_WRONLY)打开时同样也会被阻塞,知道有以读方式打开该管道。