管道的概述
管道也叫无名管道,他是UNIX
系统IPC
的最古老形式,所有的UNIX
系统都支持这种通信机制。作用与有血缘关系的进程之间,完成数据传递。
管道的特征
管道有如下特点
- 管道的本质是一个伪文件,不属于某个文件系统(实际为内核缓冲区)
- 写入管道的数据遵循先入先出的规则
- 管道所传送的数据是无格式的,这要求管道的读出写入方必须实现约定好数据的格式,如多少字节算一个消息等。
管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4K)实现
管道的局限性:
- 数据不能进程自己写,自己读
- 从管道读操作时一次性操作,数据一旦被读走,他就从管道中被抛弃 ,释放更多空间写更多的数据
- 管道为为双向半双工,数据在同一时刻只能在一个方向上流动
- 管道没有名字,只能在具有血缘关系的进程之间使用
无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
常见的通信方式有:单工通信、半双工通信、全双工通信。
管道的基本用法
pipe函数
函数原型:int pipe(int pipefd[2]);
作用:创建匿名管道
头文件:
- #include <unistd.h>
参数:
- pipefd: 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
当一个管道建立时,它会创建两个文件描述符 fd[0]
和 fd[1]
。其中 fd[0]
固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O 的函数都可以用来操作管道( lseek() 除外)。
返回值:
- 成功:0
- 失败:-1
使用pipe
函数创建的管道默认都是打开的,无需open,但需要手动close。规定 fd[0] -> r
,
fd[1] -> w
。向管道文件进行读写数据其实就是在读写内核缓冲区。
案例
下面我们写这个一个例子,子进程通过无名管道给父进程传递一个字符串数据:
#include<stdio.h>
#include <unistd.h>
#include <error.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
pid_t pid;
int fd[2];
int ret = pipe(fd); //创建管道
if(ret == -1){
perror("pipe error");
exit(1);
}
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0) {
close(fd[1]);//在子进程内关闭写端
char buf[1024];
int n = read(fd[0], buf, sizeof(buf));//从fd[0]中读取数据到 buf 缓冲区中
write(STDOUT_FILENO, buf, n); //把读到的东西输出到屏幕上
close(fd[0]);
} else {
close(fd[0]); //关闭读端
char *str = "hello pipe\n";
write(fd[1],str, strlen(str)); //向fd[1]里面写
close(fd[1]);
}
return 0;
}
程序执行结果如下
在上述代码中有一个BUG
,就是没有使用sleep
或者 wait
函数去保证 fork
后是父进程先写,子进程在读,可是仍然完成可我们想要的形式,这就牵扯到了管道的读写行为
管道的读写行为
每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者” 进程就要睡眠等待,具体过程如下图所示:
默认的情况下,从管道中读写数据,最主要的特点就是阻塞问题,当管道里没有数据,另一个进程默认用 read() 函数从管道中读数据是阻塞的。
特点总结如下:
- 读管道:
- 管道中有数据,read返回实际读到的字节数
- 管道中无数据:
-
- 管道写端全部被关闭,read 返回 0 (类似读到文件结尾)
-
- 管道写端没有全部被关闭,read 阻塞等待(此时会让出cpu)
-
- 写管道:
- 管道读端全部被关闭,进程异常终止(也可以使用SIGPIPE信号,使进程不终止)
- 管道读端没有全部关闭
- 1.管道以满,write 阻塞
-
- 管道未满, write 将数据写入,并返回实际写入的字节数
案例
使用管道实现父子进程间通信,完成 ls | wc - l
假定父进程实现 ls
, 子进程实现 wc
#include<stdio.h>
#include <unistd.h>
#include <error.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
pid_t pid;
int fd[2];
int ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0) {
close(fd[1]);
dup2(fd[0], STDIN_FILENO); //将子进程的输入由屏幕数去重定向为从管道读取数据
execlp("wc", "wc", "-l",NULL);//执行 wc -l 命令
perror("execlp error");
} else {
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);//将输出由向屏幕输出重定向为向管道输出
execlp("ls", "ls", NULL);
perror("execlp error");
}
return 0;
}
程序执行结果如下
程序执行,发现程序执行结束 shell 还在阻塞等待用户输入,这是因为 shell -> fork -> ./ls_wc_l
,程序 ls_wc_l
的子进程将 stdin
重定向给管道,父进程执行的 ls
会将结果集通过管道写给子进程。若父进程在子进程打印 wc
的结果到屏幕之前被 shell
调用 wait
回收,那么 shell
就会先输出 $
提示符,也就是图片的情况。
上述代码还有一个bug
就是文件描述符没有被关闭,但是由于笔者目前水平不够,没有办法处理 在调用 exec
族函数后关闭文件描述符的能力,后期学了信号在改吧。
兄弟进程间通信
使用管道实现兄弟进程间通信。兄执行:ls
命令,弟执行:wc -l
命令,父:等待回收子进程,要求使用循环创建多个子进程的方式创建兄弟进程。
#include<stdio.h>
#include <unistd.h>
#include <error.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[]){
pid_t pid;
int fd[2];
int ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}
int i = 0;
for(i = 0;i < 3;i++){
pid = fork();
if(pid == 0)
break;
}
if(i == 0){
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l",NULL);
perror("execlp error");
} else if(i == 1) {
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
perror("execlp error");
} else {
close(fd[0]); //这里必须关闭,如果不关闭,不满足管道内的数据使单向流通的,具体看下图
close(fd[1]);
wait(NULL);
wait(NULL);
}
return 0;
}
管道缓冲区的大小
可以使用 ulimit -a
命令查看当前系统中创建管道文件所对应的内核缓冲区大小,通常为:
pipe size (512 bytes, -p) 8
一些无聊的测试
只要保证管道内的数据是单向流动的,可以实现多个读端一个写端,也可以实现多个写端一个读端,或者多个读多个写,但是建议还是一对一,可以保障数据流动的稳定。
管道优劣
优点:简单,相比信号、套接字实现进程间通信简答的多
缺点:
- 只能单向通信,双向通信需要建立两个管道
- 只能用于父子、兄弟(有公共祖先)进程间通信,后面可以使用FIFO有名管道解决。