Linux 0.11 支持的进程通信方式只有管道和信号。这篇文章只关注前者。
每个管道允许两个进程交互数据,一个进程向管道写入数据,一个进程从管道读出数据。如下图所示:
图片来源:《Linux 内核设计的艺术》
操作系统在内存中为每个管道开辟一页内存,给这一页赋予了文件的属性。这一页内存由两个进程共享,但不会分配给任何进程,只由内核掌控。
管道创建
从技术上看,管道就是一页内存,但进程要以操作文件的方式对其进行操作,这就要求这页内存具备一些文件属性并减少页属性。
具备一些文件属性表现为,创建管道相当于创建一个文件
,如进程 task_struct 中 *filp[20] 和 file_table[64] 挂接、新建 i 节点、file_table[64] 和文件 i 节点挂接等工作要在创建管道过程中完成,最终使进程只要知道自己在操作管道类型的文件就可以了,其他的都不用关心。
减少页属性表现为,这页内存毕竟要当作一个文件使用
,进程不能像访问自己用户空间的数据一样访问它,不能映射到进程的线性地址空间内。另外,两个进程操作这个页面,一个读一个写,也不能产生页写保护异常吧页面另复制一份,否则无法共享管道。
典型例子
一个典型的利用管道进行通信的例子如下:
1 |
|
上述程序中的管道继承可用下图表示:
图片来源:APUE
注:该例子为突出重点,假设 close、write、read 函数都成功执行。
sys_pipe 函数
创建管道文件的函数是 pipe
。该函数经过映射最终调用的是 sys_pipe
函数。下边详细介绍管道创建过程。
1 | // fs/pipe.c ----------------------------------- |
需要注意的是,管道的创建工作都是由父进程完成,但子进程通过进程派生,继承了父进程的 *filp[20]。而在父进程中,*filp[20] 已经和 file_table[] 建立好关系,inode_table[] 也已经和 file_table[] 挂接好。子进程直接继承了这些关系。
管道读写操作
Linux 0.11 管道操作要实现的效果是,读管道进程执行时,如果管道中有未读数据,就读取数据,没有未读数据就挂起,这样就不会读取垃圾数据;写管道进程执行时,如果管道中有剩余空间,就写入数据,没有剩余空间了,就挂起,这样就不会覆盖尚未读取的数据。另外,管道大小只有一个页面,所以写或读到页面尾端后,读写指针要能够回滚到页面首端以便继续操作。
对于读管道操作,数据是从管道尾读出,并使管道尾指针前移‘读取字节数’个位置;对于写管道操作,数据是向管道头部写入,并使管道头指针前移‘写入字节数’个位置。可参见管道缓冲区操作示意图如下:
图片来源:《Linux 内核完全注释》
注:《Linux 内核设计的艺术》第 8.1.2 节“管道的操作”举了一个特别好的例子,值得参考!
读操作
读操作代码如下:
1 | // fs/read_write.c ----------------------------- |
写操作
写操作代码如下:
1 | // fs/read_write.c ----------------------------- |
管道特性
管道特点
如下:
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
- 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的主要局限性
正体现在它的特点上:
- 只支持单向数据流(现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不能预先假定系统提供此特性。);
- 只能用于具有亲缘关系的进程之间;
- 没有名字(有名管道是 FIFO);
- 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
- 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;