从物理上分,可以将管道分为同主机的进程之间的通信和不同主机间的进程之间的通信。从通信方式上来分,管道又分为匿名管道和命名管道,下面就匿名管道和命名管道的特性作以阐述。
匿名管道(pipe)
[含义]:管道是一个进程的数据流到另一个进程的通道,即一个进程的数据的输出作为另一个进程的数据的输入,管道起到桥梁作用。
比如:当我们输入:ls -l | cat test 其中的 ls 和 cat 是两个进程, | 代表管道,意思是执行 ls -l 进程,并将输出结果作为cat test 进程的输入,cat 进程将输入的结果打印在屏幕上。
[本质]:匿名管道之所以可以通信的本质在于,父进程frok子进程,父子进程各自拥有一个文件描述符表,但是两者的内容是一样的,既然内容一样,那么指向的就是同一个管道,即父子进程看到了同一份公共资源。
[管道的创建]:管道是一种最基本的IPC机制,由pipe函数创建:
int pipe(fd[2]);
fd :文件描述符数组
fd[2]:表示管道的输入输出端,输出端数据经过管道流到输入端,函数执行完后,会将这个数组赋值。
fd[0]:表示管道输入端的文件描述符。
fd[1]:表示管道输出端的文件描述符。
[进程间通信示意图]:我们让父进程关闭管道读端,子进程关闭管道写端。父进程可以给管道里面写,子进程可以从管道里面读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
printf("create pipe error!\n");
return 1;
}
pid_t id = fork();
if(id < 0)
{
printf("frok child error!\n");
return 2;
}
else if(id == 0)
{
//child
close(fd[1]); //close write
char msg[1024];
int i = 0;
while(i < 1024)
{
memset(msg,'\0',sizeof(msg));
read(fd[0],msg,sizeof(msg));
printf("%s\n",msg);
++i;
}
}
else
{
//father
close(fd[0]); //close read
int j = 0;
char* str = NULL;
while(j < 1024)
{
str = "i am child";
write(fd[1],str,strlen(str)+1);
sleep(1);
j++;
}
}
return 0;
}

使用管道是有一些限制的,两个进程通过一个管道只能实现单向通信。比如上述例子,父进程写,子进程读,如果需要子进程写,父进程读,就必须另开一个管道。
[管道的五大特性]:
(1)匿名管道只能单向通信。
(2)管道只能进行在有血缘关系的进程间通信,通常用于父子进程。
(3)管道通信依赖于文件系统,即管道的生命周期随进程。
(4)管道的通信被称为面向字节流,与通信格式没有关系。
(5)管道自带同步机制,保证读写顺序一致。
[思考题]:如果只开一个管道,但是父进程不关闭读端,子进程不关闭写端,双方都保留读写端,为什么不能实现双向通信?
解析:管道的读写端是通过打开的文件描述符来传递的,因此要通信的两个进程必须从他们的公共祖先那里继承管道的文件描述符。
[匿名管道的四种特殊情况]:假设都是阻塞I/O操作,没有设置O_NONBLOCK标志。
(1)如果所有指定管道写端的文件描述符都关闭了(管道写端的引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据被读取后,再次read会返回0,就像读到文件末尾一样。
(2)如果有指向管道写端的文件描述符没有关闭(管道写端的引用计数大于0),而持有写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
(3)如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
(4)如果有指向管道读端的文件描述符没有关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读取数据,这时有进程向管道的写端写入数据,那么管道被写满时,再次 write 会阻塞,直到管道中有空位置了才写入数据并返回。
命名管道(FIFO)
[本质]:命名管道在某种程度上可以看做是匿名管道,但它打破了匿名管道只能在有血缘关系的进程间的通信。命名管道之所以可以实现进程间通信在于通过同一个路径名而看到同一份资源,这份资源以FIFO的文件形式存于文件系统中。
值得注意的是,FIFO总是按照先入先出的原则工作,第一个被写入的数据将首先从管道读出。
[管道的创建]:我们可以使用下列函数之一来创建命名管道。
int mkfifo(const char* filename,mode_t mode);
int mknod(const char* filename,mode_t mode,dev_t dev);
这两个函数都可以创建一个FIFO文件,注意是创建一个真实存在于文件系统的文件。
[filename]:指定了文件名。
[mode]:指定了文件的读写权限。
mknod是比较老的函数,而mkfifo函数更加简单和规范,所以建议在可能情况下,尽量使用mkfifo。
[作用]:在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程与子进程之间的通信。而命名管道是一个可见的文件,因此,它可以用于任何两个进程之间的通信。