管道

xiaoxiao2021-02-28  123

在操作系统中每个进程都有各自不同的地址空间,其中的数据不能与其它进程直接交互。不同的进程间想要通信,我们需要借助其它机制。 在进程之间通信的最简单的方法是通过一个文件,其中有一个进程写文件,而另一个进程从文件中读,这种方法比较简单,其优点体现在: • 只要进程对该文件具有访问权限,那么,两个进程间就可以进行通信; • 进程之间传递的数据量可以非常大。 尽管如此,使用文件进行进程间通信也有两大缺点。 • 空间的浪费。写进程只有确保把新数据加到文件的尾部,才能使读进程读到数据,对 长时间存在的进程来说,这就可能使文件变得非常大。 • 时间的浪费。如果读进程读数据比写进程写数据快,那么,就可能出现读进程不断地 读文件尾部,使读进程做很多无用功。 要克服以上缺点又使进程间通信相对简单,管道是一种较好的选择。


什么是管道?

管道是用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,称pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接收管道输出的接收进程(即读进程),可从管道中接收数据。 管道本质上是在内核中开辟的一块固定大小的缓冲区。在Linux操作系统中,该缓冲区的大小为1 页,即4KB,使得它的大小不像文件那样不加检验地增长。它类似于我们现实中的水管,进程A将数据从一端塞进去,然后进程B将数据从另一端拿出来,就完成了进程间的通信。需要注意的是它只能实现进程间的单向通行,如果需要双向通信就需要两个管道,如下图:

管道的结构

在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file 结构和VFS 的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个VFS 索引节点又指向一个物理页面而实现的。如图 图中有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

管道的特点

为了协调双方的通信,管道通信机制必须提供以下几个方面的协调能力。 • 互斥。当一个进程正在对pipe 进行读/写操作时,另一个进程必须等待。 • 同步。当写(输入)进程把一定数量(如4KB)数据写入pipe 后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空pipe 时,也应睡眠等待,直至写进程将数据写入管道后,才将它唤醒。 • 对方是否存在。只有确定对方已存在时,才能进行通信。 • 限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux 中,该缓冲区大小为1 页,即4KB,使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将 默认地被据被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。 • 读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。 注意,从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。

管道的实现

通过pipe()函数创建管道。

#include <unistd.h> int pipe(int filedes[2]);

函数参数:filedes是输出型参数,该参数传出两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端。 返回值:成功返回1,失败返回-1。

调用完pipe()函数成功后,系统给我们开辟了一块缓冲区,然后将该缓冲区的一个读端和一个写端通过filedes[2]数组传出,所以管道在⽤用户程序看起来就像⼀一个打开 的⽂文件,通过read(filedes[0]);或者write(filedes[1]),向这个⽂文件读写数据其实是在读写内核缓冲区。

管道创建完成后就可以进行进程间的通信了(此处指的是父子进程间)。 下面我们简单的将父子进程间的通信步骤抽象成如下图: 1. 父进程创建管道 2.父进程forlk出子进程 3. 父进程关闭写端,子进程关闭读端,即让子进程往管道里写,父进程读取管道内容

下面通过代码演示上述步骤:

#include <stdio.h> #include <memory.h> #include <unistd.h> int main() { int _pipe[2]; int ret = pipe(_pipe);// 创建管道 if (-1 == ret)//如果创建失败则 { perror("pipe"); return 1; } pid_t id = fork(); if (id < 0) { perror("fork"); return 2; } else if(0 == id)//子进程 { close(_pipe[0]);//关闭读端,只往管道里写内容 nt i = 100; char* _mesg = NULL; while(i) { _mesg = "i am a child!"; write(_pipe[1], _mesg, strlen(_mesg)+1); sleep(1); i--; } } else//父进程 { close(_pipe[1]);//关闭写端,只从管道里读取内容 int i = 100; char _mesg[1024]; while(i) { memset(_mesg, '\0', sizeof(_mesg)); read(_pipe[0], _mesg, sizeof(_mesg)); printf("child say: %s\n", _mesg); i--; } } return 0; }

子进程每隔一秒往管道里写一字符串,父进程一直从管道里读内容 。

上述父子进程间通过管道通信,我们发现,上面的管道并没有名字。我们称这种只能在具有血缘关系的进程间通信 管道为匿名管道。

命名管道

Linux 还支持另外一种管道形式,称为命名管道,或 FIFO,这是因为这种管道的操作方式基于“先进先出”原理。上面讲述的管道类型也被称为“匿名管道”。命名管道中,首先写入管道的数据是首先被读出的数据。匿名管道是临时对象,而 FIFO 则是文件系统的真正实体,如果进程有足够的权限就可以使用 FIFO。FIFO 和匿名管道的数据结构以及操作极其类似,二者的主要区别在于,FIFO 在使用之前就已经存在,用户可打开或关闭 FIFO;而匿名管道只在操作时存在,因而是临时对象。

显然,命名管道是一种有名字的管道。但它最重要的一点是可以实现非血缘关系间的通信。 Linux下有两种⽅方式创建命名管道。一是在Shell下交互地建立一个命名管道,二是在程序中使⽤用系统函数建⽴立命名管道。Shell⽅方式下可使⽤用mknod或mkfifo命令,下面是两个创建命名管道的函数原型:

#include <sys/types.h> #include <sys/stat.h> int mknod(const char *path,mode_t mod,dev_t dev); int mkfifo(const char *path,mode_t mode);

函数mknod参数中path为创建的命名管道的全路径名:mod为创建的命名管道的模式,指明其存取权限;dev为设备值,该值取决于⽂文件创建的种类,它只在创建设备⽂文件时才会用到。这两个函数调⽤用成功都返回0,失败都返回-1。 下面使用mkfifo()创建一个命名管道:

if(mkfifo("./mypipe", 0666 | S_IFIFO) < 0)//创建命名管道,默认权限是0666 { perror("mkfifo"); return 1; }

“S_IFIFO|0666”指明创建⼀一个命名管道且存取权限为0666,即创建者、与创建者同组的⽤用户、其他⽤用户对该命名管道的访问权限都是可读可写。 命名管道是一个存在于磁盘上的管道文件,在使用时我们必须要像打开普通文件那样使用open()打开。调⽤用open()打开命名管道的进程可能会被阻塞。但如果同时⽤用读写⽅方式(O_RDWR)打开,则⼀一定不会导致阻塞;如果以只读⽅方式(O_RDONLY)打开,则调⽤用open()函数的进程将会被阻塞直到有写⽅方打开管道;同样以写⽅方式(O_WRONLY)打开也会阻塞直到有读⽅方式打开管道。

使用mkfifo()函数创建,然后让进程A以只读方式打开,进程B以只写方式打开,进程B往管道里写数据,进程A从管道里读数据。其实这有点像通过文件实现进程间通信,但他的效率比文件操作实现要高得多。 下面是演示代码:

server.c文件

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { if(mkfifo("./mypipe", 0666 | S_IFIFO) < 0)//创建命名管道,默认权限是0666 { perror("mkfifo"); return 1; } int fd = open("./mypipe", O_RDONLY); char buff[1024]; while(1) { ssize_t s = read(fd, buff, sizeof(buff)-1); if(s > 0) { buff[s-1] = 0; printf("client says: %s\n", buff); } else { printf("client quit! server begin quit!\n"); break; } } return 0; }

client.c文件

#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("./mypipe", O_WRONLY); if(fd < 0) { perror("open"); return 2; } char buff[1024]; while(1) { printf("Please Enter: "); fflush(stdout); int s = read(0, buff, sizeof(buff)); buff[s-1] = '\0'; write(fd, buff, sizeof(buff)-1); } return 0; }

运行结果:

【作者:果冻 http://blog.csdn.net/jelly_9】

——谢谢!

转载请注明原文地址: https://www.6miu.com/read-18141.html

最新回复(0)