管道是Unix中最古老古老的进程间通信手段, 人们把从一个进程连接到另一个进程的数据流称为“管道” . Linux中的管道从
Unix继承而来 .
管道分为匿名管道(pipe)和命名管道(named pipe / FIFO)
匿名管道实际上是由内核管理内核中的一块缓冲区 , 是一种半双工通信手段, 通过让不同进程都能访问同一块缓冲区,
来实现进程间通讯 . 匿名管道仅限于本地父子进程之间通信, 结构简单, 相对于命名管道, 其占用小, 实现简单.
匿名管道是内核中的一块缓冲区, 其本质就是一个文件, 但这个文件没有名字, 所以称之为"匿名管道". 但一个文件没有名字的话,
不同进程又如何知道要访问哪一块内存来实现进程间通信呢 ? 实际上这是利用了父进程创建子进程时, 子进程的task_struct(PCB)
会获取到大部分父进程task_struct中的信息, 其中就包括父进程创建的匿名管道时返回的两个匿名管道的文件描述符, 这样,子进程
也就能通过这两个匿名管道的文件描述符访问匿名管道了. 也就是说匿名管道实现的进程间通信只能限制于有亲缘关系的进程间
才能通信, 也就是只能用于具有公共祖先的进程之间的通信.
仅限于本地父子进程之间通信
同步: 对临界资源访问的合理性, 通常时通过条件判断来实现的 .
同步体现在: 若管道没数据, 读取(read)时会阻塞, 若管道满了, 写入(write)时会阻塞
互斥: 在同一时间, 只有一个进程能对临界资源进行访问, 保证临界资源的安全性.
互斥体现在: 对管道进行数据操作的大小不超过PIPE_BUF时, 则保证操作的原子性.
在Shell中 .
在Shell中 | 是管道符, 当Shell 解析命令时, 就会创建一个管道, | 左面的程序输出流入管道作为 | 右边程序的输入 , 如下图所示:
那么Shell是如何实现匿名管道的创建的呢? Shell是调用pipe()来创建匿名管道的 .
函数: int pipe( int fildes[2] )
头文件: unistd.h
参数 int fildes[2] :匿名管道的文件描述符数组,其中fildes[0]表示读端, fildes[1]表示写端 .
返回值: 成功返回0,失败返回错误代码
pipe()函数的功能就是创建一个管道文件,但与open()创建文件或打开文件不同,函数pipe()将在参数fildes中为进程返回匿名管
道的两个文件描述符fildes[0]和fildes[1]. 其中,fildes[0]是一个具有“只读”属性的文件描述符,fildes[1]是一个具有“只写”属性的文
件描述符,即进程通过fildes[0]只能进行文件的读操作,而通过fildes[1]只能进行文件的写操作。这样,就使得这个文件像一段只
能单向流通的管道一样,一头专门用来输入数据,另一头专门用来输出数据,所以称为管道 .
如果匿名管道的出入口都在一个进程内, 这样的匿名管道是没有多大意义的, 就比如自己家里的水管的入口和出口都在自己家, 黄
无意义. 如下图所示 :
但当父进程创建子进程后, 由于子进程也拿到了匿名管道的文件描述符, 情况就不同了, 如下图:
实际使用中在确定管道的传输方向之后,比如父进程写, 子进程读, 此时就可以在父进程中关闭读端(close(fildes[0])), 在子进
程中关闭写端( close(fildes[0]) ), 于是管道的连接情况就变成如下情况的单向传输管道:
来看个例子 :
模拟父子进程间通信:
#include
#include
#include
#include
int main(){
int pfd[2];
if(pipe(pfd) < 0){
perror("make pipe");
exit(1);
}
pid_t pid = fork();
if(pid < 0){
perror("fork");
}
else if(pid == 0){
char buf[100];
int len, n = 3;
close(pfd[0]);
while(n-- && fgets(buf, 100, stdin)){
len = strlen(buf);
printf("子进程将所输字符串写入管道\n");
if(write(pfd[1], buf, len) != len){
perror("write to pipe");
}
usleep(100);
}
close(pfd[1]);
}
else{
char buf[100];
int len, n = 3;
close(pfd[1]);
while(n--){
usleep(100);
if((len = read(pfd[0], buf, 100)) == -1){
perror("read from pipe");
}
write(1, buf, len);
printf("父进程从管道读出字符串, 打印到屏幕\n");
}
close(pfd[0]);
}
exit(0);
}
由于匿名管道值限制于本地具有亲缘关系的进程之间通信, 极大地限制了匿名管道的使用. 但命名管道就解决了这两个问题.
Linux提供了FIFO方式进行进行间通信, FIFO又叫做命名管道(named pipe).
FIFO (First in, First out)为一种特殊的文件类型, 不同与普通文件, 它在文件系统中有对应的路径. 当一个进程以读(r)的方式
打开该文件,而另一个进程以写(w)的方式打开该文件, 那么内核就会在这两个进程之间建立管道. 所以FIFO实际上也
由内核管理,不与硬盘打交道. 之所以叫FIFO,是因为这个管道本质上是一个先进先出的队列数据结构,最早放入的数据
被最先读出来, 从而保证信息交流的顺序. FIFO只是借用了文件系统来为管道命名, 有了文件名, 就好让不具有亲缘关系的
进程也可以访问到同一块缓冲区. 当删除FIFO文件时,管道连接也随之消失 .
在Shell中
mkfifo命令
可以看到, ls -l 可以看到fifo的类型为p, 是管道文件.
那么Shell是在解析到mkfifo时, 是如何创意命名管道的 ? 其实Shell最终还是调用了mkfifo()函数.
int mkfifo(const char *pathname, mode_t mode);
头文件 : sys/stat.h
函数功能 : 生成名为pathname的FIFO特殊文件, mode指定FIFO文件的权限. 预设权限 = mode&(~umask), 若已存在pathname
同名文件, 则失败, 置errno为EEXIST.
返回值 : 成功返回0. 错误,返回-1(并设置errno)
举个命名的栗子:
named_pipe1.c
#include
#include
#include
#include
#include
#include
int main(){
umask(0);
if(mkfifo("my_named_pipe", 0664) < 0){
perror("mkfifo");
}
int read_fd = open("my_named_pipe", O_RDONLY);
if(read_fd < 0){
perror("open");
}
char buf[1024] = {0};
ssize_t s;
for(int i = 0; i < 5; ++i){
buf[0] = 0;
printf("Please wait ... \n");
s = read(read_fd, buf, 1023);
if(s > 0){
buf[s - 1] = '\0';
printf("client say# %s\n", buf);
}
else if(s == 0){
printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);
}
else{
perror("read");
}
}
close(read_fd);
return 0;
}
named_pipe2.c
#include
#include
#include
#include
#include
#include
int main(){
int write_fd = open("my_named_pipe", O_WRONLY);
if(write_fd < 0){
perror("open");
}
char buf[1024] = {0};
ssize_t s;
for(int i = 0; i < 5; ++i){
printf("Please Enter# ");
fflush(stdout);
s = read(0, buf, 1023);
if(s > 0){
buf[s] = '\0';
write(write_fd, buf, strlen(buf));
}
else{
perror("read");
}
}
close(write_fd);
return 0;
}
运行如下: