管道

什么是管道

目前在任何一个shell中,都可以使用“|”连接两个命令,shell会将前后两个进程的输入输出用一个管道相连,以便达到进程间通信的目的:

1
2
[zorro@zorro-pc pipe]$ ls -l /etc/ | wc -l
183

管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。Linux系统直接把管道实现成了一种文件系统,借助VFS给应用程序提供操作接口。

虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。

管道的分类

Linux中管道分为两种类型

  1. 匿名管道
  2. 命名管道

匿名管道最常见的形态就是我们在shell操作中最常用的”|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性,于是系统提供了命名管道

由于本次实验主要用到的是匿名管道,所以不再赘述命名管道

匿名管道

管道的创建

管道 pipe 是进程间通信最基本的一种机制,两个进程可以通过管道一个在管道一端向管道发送其输出,给另一进程可以在管道的另一端从管道得到其输入.管道以半双工方式工作,即它的数据流是单方向的.因此使用一个管道一般的规则是读管道数据的进程关闭管道写入端,而写管道进程关闭其读出端.

pipe 系统调用的语法为:

1
2
3
4
 #include <unistd.h> 
int pipe(int pipe_id[2]);

// 如果 pipe 执行成功返回 0, `pipe_id[0]`中和` pipe_id[1]`将放入管道两端的描述符. // 出错返回-1

如果对于 pipe_id[1] 写入,调用的是 write(),向 pipe_buffer 里面写入数据;如果对于 pipe_id[0] 的读入,调用的是 read(),也就是从 pipe_buffer 里面读取数据。至此,我们在一个进程内创建了管道,但是尚未实现进程间通信。

要注意,在管道中没有数据的情况下,对管道的读操作会阻塞,直到管道内有数据为止。这就是为什么示例实验中的父子进程之间的执行是交替的。

管道的内核实现

管道实际上就是内核控制的一个内存缓冲区,既然是缓冲区,就有容量上限。我们把管道一次最多可以缓存的数据量大小叫做 PIPESIZE。内核在处理管道数据的时候,底层也要调用类似 read 和 write 这样的方法进行数据拷贝,这种内核操作每次可以操作的数据量也是有限的,一般的操作长度为一个 page,即默认为 4k 字节。我们把每次可以操作的数据量长度叫做 PIPEBUF。POSIX 标准中,对 PIPEBUF 有长度限制,要求其最小长度不得低于 512 字节。PIPEBUF 的作用是,内核在处理管道的时候,如果每次读写操作的数据长度不大于 PIPEBUF 时,保证其操作是原子的。而 PIPESIZE 的影响是,大于其长度的写操作会被阻塞,直到当前管道中的数据被读取为止。

匿名管道通信

  1. 父进程创建管道,得到两个⽂件描述符指向管道的两端
  2. 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道
  3. 禁用父进程的读,禁用子进程的写,即从父进程写入从子进程读出,从而实现了单向管道,避免了混乱。管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。

img

独立实验

与示例实验大体相似,由于只需要存储f(x)、f(y)两个数据,可以只使用一个管道,在两个子进程依次存入x、y,在父进程依次读出。由于是由子进程向父进程发送数据,所以子进程关闭读、父进程关闭写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>

// 函数定义
int fx(int x);
int fy(int y);

int main(int argc, char *argv[])
{
int pid1,pid2; //进程号
int pipexy[2]; //存放第一个无名管道标号
int x,y; // 存放要传递的整数

// 自定义输入x、y
printf("input a number x:");
scanf("%d", &x);

printf("input a number y:");
scanf("%d", &y);

//使用 pipe()系统调用建立两个无名管道。
pipe(pipexy);

//使用 fork()系统调用建立子进程
pid1 = fork();
if (pid1 == 0) {
close(pipexy[0]);
int res_x = fx(x);
write(pipexy[1],&res_x,sizeof(res_x));
close(pipexy[1]);
printf("f(x) calculate done\n");
}
pid2 = fork();
if (pid2 == 0 && pid1 > 0) {
int res_y = fy(y);
close(pipexy[0]);
write(pipexy[1],&res_y,sizeof(res_y));
close(pipexy[1]);
printf("f(y) calculate done\n");
}
if(pid1 > 0 && pid2 > 0){
int ans_x,ans_y,ans_xy;
close(pipexy[1]);
read(pipexy[0],&ans_x,sizeof(ans_x));
read(pipexy[0],&ans_y,sizeof(ans_y));
ans_xy = ans_x + ans_y;
printf("f(x) = %d , f(y) = %d , f(x,y) = %d\n",ans_x,ans_y,ans_xy);
//读写完成后,关闭管道
close(pipexy[0]);
//子进程执行结束
exit(EXIT_SUCCESS);
}

//父进程执行结束
return EXIT_SUCCESS;
}

int fx (int x)
{
if (x == 1)
return 1;
return x + fx(x-1);
}

int fy (int y)
{
if (y == 1 || y == 2)
return 1;
return fy(y-1) + fy(y-2);
}