[置顶] 进程通信方法之一--管道(的容量与实现)

一、进程通信

1、进程间通信的作用

        进程间需要数据传输、资源共享和事件通知。

2、进程间通信的方式

进程间通信主要包括管道,系统ipc(包括消息队列,信号量,共享存储),socket。

        管道通信(无名管道和命名管道)

        信号通信

        内存资源共享

        消息队列

       信号量

集合上述两种从物理和内容方式的划分,可以这样理解上图:
(1)同主机进程间数据交互机制:无名管道(PIPE)、有名管道(FIFO)、消息队列(Message Queue)和共享内存(Shared Memory)。
(2)同主机进程间同步通信机制:信号量(Semaphore)。
(3)同主机进程间异步通信机制:信号(Signal)。
(4)不同主机间进程数据交互机制:套接字(Socket)、远程调用RPC(Remote Procedure Call)。

进程通信方式如下图:


二、管道

       管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)在管道的头部读出数据。数据被一个进程读出后,将被从管道中删除,其它读进程将再不能读到这些数据。管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。

1、管道的实现原理

(1 )Linux管道的实现机制

在Linux中,管道是一种使用非常频繁的通信机制。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现为:

·      限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux中,该缓冲区的大小为1页,即4K(这里现在为65536byte)字节,使得它的大小不象文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。

·      读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。

注意:从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。

2)管道的结构

     在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如图所示。

 

 

 

 

 

 

 

 

 

 

 

 

图中有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

(3)管道的读写

      管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。

     当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:

       ·内存中有足够的空间可容纳所有要写入的数据;

       ·内存没有被读程序锁定。

如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。

     管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。

   因为管道的实现涉及很多文件的操作,因此,当读者学完有关文件系统的内容后来读pipe.c中的代码,你会觉得并不难理解。

<1>管道创建

       管道包括无名管道有名管道两种,前者用于父进程和子进程之间的通信,后者可用于运行同一系统中任意两个进程间的通信。

       无名管道由pipe()函数创建:

       int pipe(int pipefiledis[2])

       当一个管道创建时,它会建立两个文件描述符:

 filedis[0]用于读管道,filedis[1]用于写管道。

<2>管道关闭

        管道关闭只需将这两个文件描述符关闭即可,可以使用普通的close函数进行关闭。
<3>管道的读写

 1.无名管道(用于父、子进程间的通信)

       管道用于不同进程间的通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。

注:必须在系统调用fork()前调用pipe(),否则子进程将不会继承文件描述符。

2.命名管道(用于任意两个进程间的通信)

2.1 创建

#include<sys/types.h>

#include<sys/stat.h>

int mkfifo(const char *pathname,mode_t mode)

pathname:FIFO文件名

mode:属性

一旦创建了FIFO就可以用open打开它,一般的访问函数(close、write、read等)都可以用于FIFO。

实质上来讲命名管道就是一个文件

2.2 操作

     当FIFO打开时,非阻塞标志(O_NONBLOCK)

     将对以后的读写产生如下的影响:

     1.没有使用O_NONBLOCK:访问要求无法满足时进程将阻塞。如试图读取空的FIFO,将导致进程阻塞;

     2.使用O_NONBLOCK:访问要求无法满足时不阻塞,立刻出错返回,errno是ENXIO。

管道只是在内核内存中保持的一个缓冲区。这个缓冲区有一个最大值容量。一旦一个管道是充分的,进一步写入到管块,直到读者删除来自管道的一些数据。

2、管道容量(pipe capacity)

下面我们用代码进行管道容量的探究

当管道满时,

O_NONBLOCK discable: write调用阻塞,直到有程序读走数据

O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

管道是一块内存缓冲区,可写下面程序测试管道的容量pipe capacity:

  1 #include <stdio.h>                                                                                                       
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <unistd.h>
  5 #include <stdlib.h>
  6 #include <fcntl.h>
  7 #include <error.h>
  8 #include <string.h>
  9 #include <signal.h>
 10 #include <errno.h>
 11 #define ERR_EXIT(m)\
 12     do{\
 13         perror(m);\
 14         exit(EXIT_FAILURE);\
 15     }while(0) 
 16 int main()
 17 {
 18     int pipefd[2];
 19     if(pipe(pipefd)==-1)
 20     {
 21         ERR_EXIT("pipe error");
 22     }
 23     int ret;
 24     int count=0;
 25     int flags=fcntl(pipefd[1],F_GETFL);
 26     fcntl(pipefd[1],F_SETFL,flags|O_NONBLOCK);//feizuse
 27     while(1)
 28     {
 29         ret=write(pipefd[1],"A",1);
 30         if(ret==-1)
 31         {
 32             printf("err=%s\n",strerror(errno));
 33             break;
 34         }
 35 
 36         count++;
 37     }
 38     printf("count=%d\n",count);//guandaorongliang
 39     return 0;
 40 }   

程序中将写端文件描述符设置为非阻塞,当管道被写满时不会等待其他进程读取数据,而是直接返回-1并置errno,结果如下:


如此可见,管道的容量及pipe capacity为65536。APUE中说PIPE_BUF定义了内核管道缓冲区的大小,linux中为4096 byte,而TLPI中说,管道是维持在内核内存中的一个缓冲区,自Linux 2.6.11开始,该缓冲区大小为65536 byte

注意:

在次区别pipe_buf和pipe capacity:

pipe_buf指原子操作的最大值,而pipe capacity指管道的最大值,即容量;管道的大小,也就是确定pipe_buf的值,这个值由内核设定,




你可能感兴趣的:(linux,通信,管道)