管道是进程间通信的主要手段之一。一个管道实际上就是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。根据管道的适用范围将其分为:无名管道和命名管道。 1.无名管道 主要用于父进程与子进程之间,或者两个兄弟进程之间。在linux系统中可以通过系统调用建立起一个单向的通信管道, 且这种关系只能由父进程来建立。因此,每个管道都是单向的,当需要双向通信时就需要建立起两个管道。管道两端的进程均将该管道看做一个文件,一个进程负责往管道中写内容,而另一个从管道中读取。这种传输遵循“先入先出”(FIFO)的规则。 对于写管道: 写入管道的数据按到达次序排列。如果管道满,则对管道的写被阻塞,直到管道的数据被读操作读取。对于写操作, 如果一次write调用写的数据量小 于管道容量,则写必须一次完成,即如果管道所剩余的容量不够,write被阻塞直 到管道的剩余容量可以一次写完为止。如果write调用写的数据量大于管 道容量,则写操作分多次完成。 如果用fcntl设置管道写端口为非阻塞方式,则管道满不会阻塞写,而只是对写返回0。 对于读管道: 读操作按数据到达的顺序读取数据。已经被读取的数据在管道内不再存在,这意味着数据在管道中不能重复利用。如果管道为空,且管道的写端口是打开状 态,则读操作被阻塞直到有数据写入为止。一次read调用,如果管道中的数据量不够read指定的数量,则按实际的数量读取,并对read返回实际数量 值。如果读端口使用fcntl设置了非阻塞方式,则当管道为空时,read调用返回0。 对于管道的关闭: 如果管道的读端口关闭,那么在该管道上的发出写操作调用的进程将接收到一个SIGPIPE信号。关闭写端口是给读 端口一个文件结束符的唯一方法。对于写端口关闭后,在该管道上的read调用将返回0。 管道相关的关键概念 管道是Linux支持的最初Unix IPC形式之一,具有以下特点: a.管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; b.只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); c.单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种 文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 数据的读出和写入: 一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从 缓冲区的头部读出数据。 向管道中写入数据: 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。 如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。只有在管道的读端存在时,向管道中写入数据才有 意义。否则,向管道中写入数据的进程将收到内核传来的 SIFPIPE信号,应用程序可以处理该信号,也可以忽略 (默认动作则是应用程序终止)。 从管道中读取数据: 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0; 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不 大于 PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节 数(此时,管道中数据量不小于请求的数据 量)。注:(PIPE_BUF在include/linux/limits.h中定义, 不同的内核版本可能会有所不同。Posix.1要求 PIPE_BUF至少为512字节,red hat 7.2中为4096)。 管道的局限性 管道的主要局限性正体现在它的特点上: 只支持单向数据流; 只能用于具有亲缘关系的进程之间; 没有名字; 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小); 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作 一个消息(或命令、或记录)等等; 2.命名管道 命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。 使用popen时,popen其实是实现了以下三个动作: 1)创建一个pipe; 2)fork一个子进程,使得子进程运行新的程序; 3)将子进程的标准输出或者标准输入重定位到父进程。即,父进程通过管道向子进程写数据或者通过管道从子进程读取 数据。而这里的“读”或者“写”是根据调用popen时的第二个参数决定的。如果使用两次popen,如上面的程序所示, 那么实际上父进程是在与两个子进程通过管道通信,而不是一个! |
|
来自: 书永夜 > 《Linux系统编程》