什么是管道
管道本质上是内核中的一段缓冲区,它可以连接两个进程,进行数据流的传输。
匿名管道
匿名管道字如其名是匿名的。他不可见于文件系统,所以只能用于具有亲缘关系的进程间通信。
#include <unistd.h>
功能:创建⼀⽆名管道
原型
int pipe(int fd[2]);
参数
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码
父进程创建管道。再用fork()函数创建子进程,所以他们具有相同的文件描述符表。根据管道的数据传输方向,关闭父子进程不用的那一端文件描述符即可。
代码实现
模拟实现 grep ssh | ps -ef
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <errno.h>
5 #include <string.h>
6
7 int main()
8 {
9 int pipefd[2];
10
11 if (pipe(pipefd) < 0) {
12 perror("pipe error");
13 return -1;
14 }
15 int pid = fork();
16 if (pid < 0) {
17 exit(-1);
18 }else if (pid == 0) {//child
19 //grep ssh
20 //grep这个进程本身从标准输入获取数据,但是我们想的是,不要
21 //从标准输入拿数据了,而是从管道中取数据
22 //因此将标准输入进行重定向到管道读取端
23 close(pipefd[1]);
24 dup2(pipefd[0], 0);
25 execlp("grep", "grep", "ssh", NULL);
26 }else {
27 close(pipefd[0]);
28 //ps这个命令最终将结果是打印到显示器上,然而,我们并不想让
29 //它打印到界面,而是将结果写入到管道
30 //因此将标准输出重定向到管道写入端
31 dup2(pipefd[1], 1);
32 execlp("ps", "ps", "-ef", NULL);
33 close(pipefd[1]);
34 }
35 return -0;
36 }
管道读写规则
当没有数据可读时
O_NONBLOCK 不启用: read调⽤阻塞,即进程暂停执⾏,⼀直等到有数据来到为⽌。
O_NONBLOCK 启用: read调用不阻塞,调⽤返回-1, errno值为EAGAIN。
当管道满的时候
O_NONBLOCK 不启用: write调⽤阻塞,直到有进程读⾛数据
O_NONBLOCK 启用:write调用不阻塞,调⽤返回-1, errno值为EAGAIN
如果所有管道写端对应的⽂件描述符被关闭,则read返回0
如果所有管道读端对应的⽂件描述符被关闭,则write操作会产⽣信号SIGPIPE,进⽽可能导致write
进程退出
当要写⼊的数据量不⼤于PIPE_BUF时, linux将保证写⼊的原⼦性,不会被打断。
当要写⼊的数据量⼤于PIPE_BUF时, linux将不再保证写⼊的原⼦性,可能被打断。
匿名管道的特点
- 匿名管道是半双工的,只能实现单向通信,倘若要达到相互通信的目的,需要创建两个管道。
- 匿名管道的生命周期随进程,当连接管道的两个进程都退出后,管道自动释放
- 管道是面向字节流传输的
- 匿名管道仅能用于亲缘间进程通信。
- 管道传输自带同步与互斥:
同步:数据访问的可控时序型(数据访问按顺序)
互斥:同一时间数据访问的唯一性
命名管道
相对于匿名管道,命名管道是可见于文件系统的,所以适用于同一主机上任意两个进程间的数据传输。
可以通过命令行方式和接口函数实现:
命令行: mkfifo+ filename
接口函数: int mkfifo(const char* pathname,mode_t mode);
pathname:管道文件的路径名
mode:管道文件的权限
返回值: 成功:0 失败:-1
代码实现:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 #include<string.h>
5 #include<fcntl.h>
6 #include<stdlib.h>
7
8
9 int main()
10 {
11 //1.创建命名管道
12 if (mkfifo("./named_pipe",0664) < 0)//倘若创建不成功,需要考虑该管道是否已经存在,已经存在则不用退出.
13 {
14 if (errno != EEXIST)
15 {
16 perror("mkfifo error!");
17 return -1;
18 }
19 }
20
21 //2.连接命名管道
22 //打开规则:命名管道在打开时,必须以读写方式打开,单独以只读,只写方式打开都会阻塞
23 //此处为了演示不同的两个进程间通信,故在此只以读方式打开,另一端以写方式打开
24 int fd = open("./named_pipe",O_RDONLY);
25 if (fd < 0)
26 {
27 perror("open error!");
28 return -1;
29 }
30
31 printf("named_piped is working!\n");
32 //3.开始进行数据传输
33 while (1)
34 {
35 //本进程是以只读方式打开的,所以只能从管道中读数据,同时注意管道的读写规则:
36 //
37 //1.如果管道没有数据,读取操作会阻塞(描述符的默认属性---阻塞属性),
38 //如果描述符被设置为非阻塞属性,那么这个操作不会被阻塞,而是直接报错返回。
39 //
40 //2.如果管道数据已满,写入操作会阻塞(描述符的默认属性---阻塞属性),
41 //如果描述符被设置为非阻塞属性,那么这个操作也不会被阻塞,而是直接报错返回。
42 //
43 //3.如果管道的写端全部关闭,那么read读取数据的时候会返回0
44 //
45 //4.如果管道的读端全部关闭,那么write写入数据的时候会触发异常,
46 //操作系统会发送SIGPIPE信号到进程,导致进程退出。
47 //
48 char buff[1024]={0};
49 int ret = read(fd,buff,1023);
50
51
52 if(ret > 0)//读到数据
53 {
54 printf("read point get the data:%s\n",buff);
55 }
56 else//根据读写规则,当所有写端都关闭时,read返回0
57 {
58 printf("all write point is closed!!\n");
59 sleep(1);
60 }
61 }
62 close(fd);
63 return 0;
64 }
当我们运行这个程序时会阻塞在open处,因为没有另一个进程以只写方式打开,我们利用echo将本应该输出到屏幕上的信息重定向到管道named_pipe中,就实现了数据传输。如图:
命名管道的打开规则(重要)
命名管道的打开特性:
1.如果以只读打开,会阻塞等待这个命名管道被其他进程以写方式打开
2.如果以只写打开,会阻塞等待这个命名管道被其他进程以读方式打开
3.如果以读写打开,则不会阻塞
命名管道的读写特性(重要)
1.如果管道没有数据,读取操作会阻塞(描述符的默认属性—阻塞属性)。
如果描述符被设置为非阻塞属性,那么这个操作不会被阻塞,而是直接报错返回。
2.如果管道数据已满,写入操作会阻塞(描述符的默认属性—阻塞属性),
如果描述符被设置为非阻塞属性,那么这个操作也不会被阻塞,而是直接报错返回。
3.如果管道的写端全部关闭,那么read读取数据的时候会返回0
4.如果管道的读端全部关闭,那么write写入数据的时候会触发异常,操作系统会发送SIGPIPE信号到进程,导致进程退出。
5.当写入的数据大小超过PIPE_BUF,则这个操作是一个非原子操作,有可能被打断。
匿名管道与命名管道的区别(非常重要)
-
匿名管道由pipe函数创建并打开,而命名管道要由mkfifo函数创建,并手动将两个进程绑定在管道两端。
-
匿名管道是依附于进程的,进程退出后匿名管道会随之释放,而命名管道是一个实实在在的文件,创建后就一直存在,他并不会跟着进程的退出而释放.
-
他们的区别只是在创建和打开方式不同, 但一旦管道开始使用,他们的意义都是一样的。