一、小谈管道历史
关于管道,有这么一句话:
如果说Unix是计算机文明中最伟大的发明,那么,Unix下的Pipe管道就是跟随Unix所带来的另
一个伟大的发明。
管道的出现就是为了使软件开发更加“高内聚,低耦合”。管道的发明者,Malcolm Douglas McIlroy,同时也是Unix创建者及Unix文化缔造者,他的Unix哲学:
程序应该只关注一个目标,并尽可能把它做好。让程序能够互相协同工作。应该让程序处理文本
数据流,因为这是一个通用的接口。
也就是说,管道存在的意义就是让程序能够专注自己的目标,而不同进程之间可以相互通信协作,完成一个更大的目标。比如:在主机上,客户进程可以通过管道给文件服务器进程发送文件名,文件服务器打开文件然后将文件数据通过管道传给客户进程,如此,客户进程和文件服务器相互独立,同时相互协作。这其实不仅仅是管道的特点,同时也是其他进程间通信(IPC)手段的特点。不同的IPC实现各有不同,但是目的、哲学道理都是差不多的。
IPC技术:管道,共享内存,消息队列,信号量,本地套接字等。
管道一般是单向的,半双工的,像花园的软水管,一边进一边出。要想实现全双工,一般需要两条管道。
二、管道基本实现
每个进程都有自己的独立的地址空间,要想实现进程之间的相互通信,必须采取必要的手段: 共享内存或者借助内核。管道的实现就是借助内核。
管道的本质就是内核维护的一段内存。因为linux“一切皆文件”的思想,管道自然而然就被实现为“管道文件”(向普通文件一样管理),隶属管道文件系统pipefs。因此,和普通文件一样,内核负责维护文件的细节,返回给用户进程的只是一个个“文件描述符”,通过文件描述符,进程可以执行打开管道、读写管道的操作。
管道分为两种:无名管道pipe和有名管道FIFO
无名管道pipe
何为无名管道?无名管道就是用户进程只能通过文件描述符fd才能找到的管道,内核没有给其他方式告诉管道在哪里。即没有名字,只有句柄(文件描述符)。一般来讲,文件描述符只会在有亲缘关系的进程间继承,这就限制了无名管道一般只用在有亲缘关系进程间通信。
有名管道FIFO
何为有名管道?有名管道,对比无名管道,它跟实体文件名绑定。任何进程只要知道跟管道绑定的文件名,就可以尝试打开管道并操作。这意味着,有名管道可以使用在没有亲缘关系的进程间通信使用。
三、无名管道pipe
创建无名管道pipe
1 | //原型 |
无名管道使用(搭配进程fork)
无名管道因其只以文件描述符索引,所以一般的使用方式:
某个祖先进程(父进程)先创建管道,然后fork出若干子进程,父子进程或兄弟进程之间通过继承得到
的管道文件描述符来读写管道,从而达到通信的目的。
1 | //Example: |
注意点 :管道的双方要关闭不需要使用到的文件描述符,为什么?这涉及管道读取端如何判断对方已经不再写了?管道写端进程如何判断管道另一头已经没有进程在读了?
- 当读取端已无进程等待(即fd[0]的引用计数为0 (close)), 此时若有进程对写端继续写,返回EPIPE错误。并产生信号SIGPIPE。
- 当写端已无进程再继续写(即fd[1]的引用计数为0 (close)), 若管道的数据读完,将返回文件结束符EOF。
因此,为了能够正常判断结束条件,进程要关闭其没有使用的管道文件描述符。
无名管道是不是只能用在用亲缘关系的进程中?
答案是否定的。因为借助本地套接字,可以在进程间传递文件描述符。
popen/pclose
1 |
|
popen行为:fork并执行shell进程,shell进程fork并执行command进程,并返回文件描述符。该文件描述符fd或为读端(管道写端关联到command进程的标准输出),或为写端(管道读端关联到command进程的标准输入)。这取决与popen是以读模式还是以写模式打开。
四、命名管道FIFO
创建管道
1 | int mkfifo(const char* path, mode_t mode); |
参数:
- path: 文件路径名
- mode: 文件权限位
创建管道用mkfifo,打开管道用open
调用mkfifo成功创建管道后,其他进程可以通过open相应的“文件名”从而获取管道,进行读或写。
五、管道属性
管道的打开行为、读写行为
管道open和O_NONBLOCK
管道默认是阻塞的。和普通文件描述符一样,任何时候可以通过fcntl(瑞士军刀)设置成非阻塞。
- 管道阻塞, open + O_RDONLY : 此时没有进程为写打开,则阻塞。
- 管道阻塞, open + O_WRONLY :此时没有进程为读打开,则阻塞。
- 管道非阻塞, open + O_RDONLY : 无论有没有进程为写打开,立即返回。
- 管道非阻塞, open + O_WRONLY : 没有读进程,则返回-1,errno = ENXIO。
管道读写
- 阻塞、空管道或FIFO,read:有写进程,阻塞到有数据;无写进程,返回EOF;
- 非阻塞、空管道或FIFO,read: 有写进程,返回EAGAIN;无写进程,返回EOF;
- 阻塞、管道或FIFO,write: 有读进程,见下文;无读进程,返回SIGPIPE;
- 非阻塞、管道或FIFO,write: 有读进程,见下文;无读进程,返回SIGPIPE;
管道写行为、PIPEBUF、原子性
管道有一特点,尽力保证写入的原子性:
如果请求写入的数据字节数小于等于PIPE_BUF,那么write操作保证是原子的。大于PIPE_BUF,则不保证。
如此,在非阻塞情况下:
- 请求写入字节数小于等于PIPE_BUF:
- 当前管道空间足够,则直接写入;
- 当前管道空间不够,为保证原子性,先返回EAGAIN,告诉进程稍后尝试;
- 请求写入字节数大于PIPE_BUF:
- 当前管道空间至少还有1字节,直接写相应字节数,返回;
- 当前管道满,返回EAGAIN。
PIPE_BUF的大小可以通过fcntl来设置。
从上面的讨论可以看出,管道的读写需要一些技巧。所谓“管道有大小,写入需谨慎”。
- 一次写入数据量不超过PIPE_BUF,以保证写入是原子的。即使存在多个进程同时读,也不会被打断。
- 写端不要大量输入,会造成阻塞。
- 读端,要及时读取,避免造成写入阻塞。