管道

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


什么是管道?

管道是用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,称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

——谢谢!

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值