Linux下进程间通信的手段基本是从Unix平台继承而来。管道是第一个广泛应用的进程间通信手段。日常在终端执行shell命令时,经常会将上一个命令的输出,作为下一个命令的输入,有多个命令配合完成一件事情,会大量用到管道。
管道是一种文件,可以调用read,write,close等操作文件的接口来操作管道。它属于一种特殊的文件系统:Pipe File System。管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转化成这块缓冲区的操作。
管道中的内容是阅后即焚的,即读取管道里的内容是消耗行为,一个进程读取了管道的一些内容之后,这些内容既不会继续存在。
一般来讲管道是单向的,一个进程负责往管道里面写内容,另一个进程读取管道里面的内容。若两个进程都往管道中写,或都从里面读,则管道里面的内容会变得混乱,无法完成进程间通信的任务。如果两个进程想进行双向通信,可以建立两个管道。如图所示:
管道可分为无名管道和有名管道。
无名管道
无名管道的作用是在有亲缘关系的进程之间传递消息,即进程之间用于共同的祖先,可以用于父子进程,兄弟进程,祖孙进程,叔侄进程。只要有共同的祖先曾经调用了pipe函数,打开的管道文件就会在fork之后被各个后代进程所共享。父子进程共享fork之前打开的文件描述符。
管道实质是一个字节流,若多个进程发送的数据混合在一起,则无法识别出各自的内容。
所以一般是两个有亲缘关系的进程用一条管道来进行通信。
在Linux下,可用如下接口创建管道:
#include<unistd.h>
int pipe(int pipefd[2]);
如果成功,返回值为0,失败返回值为-1,并且设置error。有一下三种出错情况:
EMFILE
该进程使用的文件描述符已经多于MAX_OPEN-2
ENFILE
系统中同时打开的文件已经超过了系统的限制
EFAULT
pipefd参数不合法
无名管道没有文件名与之关联,只能通过文件描述符来访问管道,只有能看到这两个文件描述符的进程和其子孙进程才能够使用管道。成功调用pipe函数之后,会返回两个打开的文件描述符,一个是管道的读取端文件描述符pipefd[0],另一个是管道的写入端描述符pipefd[1]。可以对写入端描述符调用pipefd[1]调用write,向管道里面写入数据。
write(pipefd[1],buff,count);
写入成功之后,就可以对读取端描述符pipefd[0]调用read,读出管道里面的内容。
read(pipefd[0],buff,count);
返回的字节数等于请求字节数count和当前管道存在的字节数的最小值。如果管道为空,read调用阻塞(若没有设置O_NONBLOCK标志)。
思考:若对读取端进行写入操作,对写入端进行读取操作,会如何???
进程调用pipe函数之后如下如所示。但是一个进程管道,只是自言自语,不是通信。
调用pipe函数的进程随后调用fork函数,创建了子进程,子进程复制父进程打开的文件描述符,建立起两条通信管道。此时,父进程可以往管道里面写(读),子进程可以从里面读(写)。父子进程也可以关闭各自不用的文件描述符,管道成为一个通信的通道。如下图:
实现方式为父进程关闭pipefd[0]端口,子进程关闭pipefd[1]端口。代码示例如下
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
int main()
{
int fd[2] = {-1,-1};
int ret = pipe(fd);
assert(ret != -1);
pid_t n = fork();
assert(n != -1);
if(n == 0)//子进程
{
close(fd[0]);//关闭读端
char q[127];
printf("Please Input:");
scanf("%s",q);
ret = write(fd[1],q,127);
printf("Write Successfully!\n");
close(fd[1]);//关闭写端
exit(0);
}
else
{
int fp = open("./c.txt",O_WRONLY);
close(fd[1]);//关闭写端
char s[127] = {0};
ret = read(fd[0],s,127);
int i = 0;
printf("%s\n",s);
for(i = 0;i < strlen(s);i++)
{
printf("%c",s[i]);
}
printf("\nread successfully!\n");
int a = write(fp,s,strlen(s));
if(a > 0)
{
printf("Write in file Successfully!\n");
}
close(fd[0]);
}
return 0;
}
所以,任何两个有亲缘关系的进程,只要共同的祖先打开了一个管道,总能够通过关闭不相关进程的某些管道文件描述符,来建立起两者之间单向通信的管道。
关闭未使用的管道文件描述符的必要性:
Ø 让数据流向更为清晰
Ø 节省文件描述符。
Ø 关闭未使用的管道文件描述符对管道的正确使用影响重大。
管道有如下三条性质:
1 只有当所有的写入端描述符都已关闭,且管道中的数据都被读出,对读取端描述符调用read函数才会返回0(即读到EOF标志)
2 如果所有读取端描述符都已关闭,此时进程再次往管道里面写入数据,写操作会失败,errno被设置为EPIPE,同时内核会向写入进程发送一个SIGPIPE的信号。
3 当所有的读取端和写入端都关闭后,管道才能被销毁。
从管道读取数据的进程,须要关闭其持有的管道写入端描述符。不参与通信的其他有亲缘关系的进程也应该关闭管道写入端描述符。当read函数返回0时,则表明写入过程已经运行完毕,读取进程就不需要再等待。
如果负责读取的进程或者与通信无关的进程,不关闭管道的写入端描述符,就会有管道写入端描述符泄露。当所有负责写入的进程都关闭了写入端描述符之后,负责读的进程调用read时,(若没有设置O_NONBLOCK标志)会阻塞于此,且永不返回。因为内核维护的引用计数发现还有进程可以写入管道,因此read函数依旧会阻塞。
如果写入管道的进程不关闭管道的读取文件描述符,哪怕其他进程都已经关闭了读取端,该进程仍然可以向管道中写入数据,但是管道最终会被写满,后续的写入请求会被阻塞。
当所有的管道读取端都不复存在时,写入操作会失败。即没有了消费者,生产者也就没有生产的必要。
管道对应的内存区大小
管道本质是一片内存区域,自然有大小。自Linux 2.6.11版本起,管道的默认大小是65536字节,可以调用fcntl来获取和修改这个值地大小。
pipe_capacity = fcntl(fd,?F_GETPIPE_SZ);//获取管道大小
ret = fcntl(fd,?F_SETPIPE_SZ,size);//设置管道大小
管道内存区域的大小必须在页面大小和上限值之间,其上限记录在
/proc/sys/fs/pipe-max-size里(我的在file-max),特权用户还可以修改该上限值。
缩小管道容量时,如果当前管道中已存在的内容大于fcntl函数调用中指定的size,此时返回失败,错误码为EBUSY。
管道有大小,写入须谨慎。管道写满,写入进程被阻塞。