内核资料收集
1. 概述
pipe管道是所有unix都提供的一种进程间通信机制. 管道是进程之间的一个单向数据流; 一个进程写入管道的所有数据都由内核
定向到另一个进程, 另一个进程由此就可以从管道中读取数据.
unix的shell命令可以使用"|"操作符来创建管道.例如语句"ls | more"会通知shell创建两个进程,并使用一个管道把两个进程连接在一
起. 第一个进程(执行ls程序)的标准输出被重定向到管道中; 第二个进程(执行more程序)从这个管道中读取输入. 执行下面这两条命
令也可以得到相同的结果:
$ ls > temp
$ more < temp
第一个命令把ls的输出重定向到一个普通文件中; 接下来, 第二个命令强制more从这个普通文件中读取输入. 通常使用管道比使
用临时文件更方便, 原因如下:
a. shell语句短, 比较简单
b. 没有必要创建将来还必须删除的临时普通文件
2. 使用管道
管道可以看作是打开的文件, 但已安装的文件系统中没有相应的映像.
使用pipe()系统调用来创建一个新的管道, 这个系统调用返回一对文件描述符.然后进程通过fork()把这两个描述符传递给它的子进
程,由此与子进程共享管道. 进程可以在read()系统调用中使用第一个文件描述符从管道中读取数据,同样也可以在write()系统调用中使
用第二个文件描述符向管道中写入数据.
POSIX只定义了半双工管道, 因此即使pipe()系统调用返回了两个描述符,每个进程在使用一个文件描述符之前仍得把另外一个文件
描述符关闭. 如果所需要的是双向数据流,那么进程必须通过两次调用pipe()来使用两个不同的管道.
有些unix系统如SVR4实现全双工管道. 在全双工管道中,允许两个文件描述符既可以被写入也可被读出, 这就是两个双向信息通道.
linux采用另外一种解决方法:每个管道的文件描述符仍然都是单向的,但是在使用一个描述符之前不必把另外一个描述符关闭.
当shell命令对ls | more语句进行解释时, 执行操作如下:
a. 调用pipe()系统调用; 假设pipe()返回文件描述符3和4.
b. 两次调用fork()系统调用
c. 两次调用close()系统调用来释放文件描述符3和4
第一个子进程必须执行ls程序, 它执行以下操作:
a. 调用dup(4, 1)把文件描述符4拷贝到文件描述符1. 从现在开始,文件描述符1就代表该管道的写管道.
b. 两次调用close()系统调用释放文件描述符3和4.
c. 调用execve()系统调用来执行ls程序.缺少情况下,这个程序要把自己的输出写到文件描述符为1的那个文件中.也就是写入管道中.
第二个子进程必须执行more程序; 因此该进程执行以下操作:
a. 调用dup(3, 0)把文件描述符3拷贝到文件描述符0. 从现在开始, 文件描述符0就代表管道的读通道
b. 两次调用close()系统调用来释放文件描述符3和4
c. 调用execve()系统调用来执行more程序. 缺少情况下,这个程序要从文件描述符为0的那个文件中读取输入, 也就是从管道中
读取输入.
此例中, 管道完全被两个进程使用. 但是由于管道的这种实现方式, 一个管道可以供任意个进程使用. 显然如果两个或者更多个
进程对同一个管道进程读写, 那么这些进程必须使用文件加锁机制或者IPC信号量机制对自己的访问进行显式的同步.
除了pipe()系统调用之外,很多unix系统都提供了两个名为popen()和pclose()的封装函数来处理在使用管道的过程中产生的所有脏工
作. 只要使用popen()函数创建一个管道,就可以使用包含在C函数库中的高级I/O函数对这个管道进行操作.
在linux中, popen()和pclose()都包含在C函数库中. popen()函数接收两个参数; 可执行文件的路径名为filename和定义数据传输
方向的字符串type. 该函数返回一个指向FILE数据结构的指针. popen()函数实际上执行以下操作:
a. 使用pipe()系统调用创建一个新管道
b. 创建一个新进程,该进程又执行以下操作:
1. 如果type是r, 就把与管道的写通道相关的文件描述符拷贝到文件描述符1; 否则, 如果type是w, 就把与管道的读通道相的
文件描述符拷贝到文件描述符0.
2. 关闭pipe()返回的文件描述符.
3. 调用execve()系统调用执行filename所指定的程序.
c. 如果type是r, 就关闭与管道的写通道相关的文件描述符;否则,如果type是w, 就关闭与管道的读通道相关的文件描述符.
d.返回FILE文件指针所指向的地址,这个指针指向仍然打开的管道所涉及的任一文件描述符.
在popen函数被调用后,父进程和子进程就可以通过管道交换信息: 父进程可以使用该函数所返回的FILE指针来读(如果是r)写(如果
是w)数据. 子进程所执行的程序分别把数据写入标准输出或从标准输入中读取数据.
pclose()函数接收popen()所返回的文件指针作为参数,它会简单调用wait4()系统调用并等待popen所创建的进程结束.
3. 管道数据结构
管道一被创建,进程就可以使用read()/write()这两个VFS系统调用来访问管道.因此,对于每个管道来说,内核都要创建一个索引节点对
象和两个文件对象, 一个文件对象用于读, 另一个对象用于写. 当进程希望从管道中读取数据或向管道中写入数据时,必须使用适当
的文件描述符.
当索引节点指向的是管道时,其i_pipe字段指向pipe_inode_info结构.如下:
struct wait_queue * wait 管道/FIFO等待队列
unsigned int nrbufs 包含待读数据的缓冲区数
unsigned int curbuf 包含待读数据的第一个缓冲区的索引
struct pipe_buffer[16] bufs 管道缓冲区描述符数组
struct page * tmp_page 高速缓存页框指针
unsigned int start 当前管道缓冲区数读的位置
unsigned int readers 读进程的标志(或编号)
unsigned int writers 写进程的标志(或编号)
unsigned int waiting_writers 等待队列中睡眠的写进程的个数
unsigned int r_counter 与readers类似,但当等待读取FIFO的进程时使用
unsigned int w_counter 与writers类似,但当等待写入FIFO的进程时使用
struct fasync_struct * fasync_readers 用于通过信号进行的异步I/O通知
struct fasync_struct * fasync_writers 用于通过信号进行的异步I/O通知
除了一个索引节点对象和两个文件对象外,每个管道还有自己的管道缓冲区(pipe buffer). 实际上,它是一个单独的页,其中包含了已经
写入管道等待读出的数据. pipe_inode_info数据结构的bufs字段存放一个具有16个pipe_buffer对象的数组, 每个对象代表一个管道缓
冲区. 该对象的字段如下:
struct page * page 管道缓冲区页框描述符地址
unsigned int offset 页框内有效数据的当前位置
unsigned int len 页框内有效数据的长度
struct pipe_buf_operations * ops 管道缓冲区方法的地址
ops字段指向管道缓冲区方法anon_pipe_buf_ops, 它是一个类型为pipe_buf_operations的数据结构. 为了避免对管道数据结构的竞
争条件, 内核使用包含在索引节点对象中的i_sem信号量.
4. pipefs 特殊文件系统
管道作为一组VFS对象来实现,因此没有对应的磁盘映像. linux把这些VFS对象组织为pipefs特殊文件系统以加速这处理. pipefs文
件系统在系统目录树中没有安装点,因此用户根本看不到它. 但是,有了pipefs, 管道完全被整合到VFS层,内核就可以命名管道或FIFO的
方式处理它们, FIFO是以终端用户认可的文件而存在的. init_pipe_fs函数注册pipefs文件系统并安装它.
5. 创建和撤销管道
pipe系统调用由sys_pipe()函数处理,后者又会调用do_pipe()函数. do_pipe()执行创建新管道的关键过程. 执行过程......
发出pipe()系统调用的进程最初是唯一一个可以读写访问新管道的进程. 为了表示该管道实际上既有一个读进程,又有一个写进程,就
要把pipe_inode_info数据结构的readers和writers字段都初始化成1. 通常, 只要相应管道的文件对象仍然由某个进程打开,这两个字段
中的每个字段就应该都被设置成1; 如果相应的文件对象已经被释放,那么这个字段就被设置成0, 因为不会再有任何进程访问这个管道.
创建一个新进程并不会增加readers和writers字段的值,因此这两个值从不超过1. 但是父进程仍然使用的所有文件对象的引用计数器
的值都会增加. 因此即使父进程死亡时这个对象都不会被释放,管道仍会一直打开供子进程使用.
只要进程对与管道相关的一个文件描述符调用close()系统调用, 内核就对相应的文件对象执行fput()函数, 减少它的引用计数器的
值. 如果这个计数器的值变成0, 那么该函数就调用这个文件操作的release()方法.
根据文件是与读通道还是与写通道关联, release方法或者由pipe_read_release()或者由pipe_write_release()函数来实现. 这两个
函数都调用pipe_release(), 后者把pipe_inode_info结构的readers字段或writers字段设置成0, pipe_release()还检查readers和writers
是否都是0, 如果是, 就调用所有管道缓冲区的release方法, 向伙伴系统(buddy system)释放所有管道缓冲区页框; 此外函数还释放
由tmp_page字段指向的高速缓存页框. 否则,readers或者writers字段不为0, 函数唤醒在管道在等待队列上睡眠的任一进程, 以使它
们可以识为管道状态的变化.
6. 从管道中读取数据
read()系统调用可用于从管道中读取数据. 内核最终调用与这个文件描述符相关的文件操作表中的read方法. 在读管道的情况下,
read方法在read_pipe_fops中的表项指向pipe_read()函数.
posix标准定义了管道的读操作的一些要求(具体要求略). 下表概述了所期望的read()系统调用的行为, 该系统调用从一个管道大小为
p的管道中读取n个字节.
此系统调用可能以两种方式阻塞当前进程:
a. 当系统调用开始时管道缓冲区为空
b. 管道缓冲区没有包含所有请求的字节,写进程在等待缓冲区的空间时曾被置为睡眠.
注意2点:
a.读操作可以是非阻塞的. 此时只要所有可用的字节(可以是0个)一被拷贝到用户地址空间中, 读操作就完成.
b.只要管道为空而且当前没有进程正在使用与管道的写通道相关的文件对象时, read()系统调用才会返回0.
pipe_read()函数执行的具体操作(略)
7. 向管道中写数据
write()系统调用可用于向管道中写入数据. 内核最终调用与这个文件描述符相关的文件操作表中的write方法. 在写管道的情况
下, write方法在write_pipe_fops中的表项指向pipe_write()函数.
下表概述了由posix标准所定义的write()系统调用的行为, 该系统调用请求把n个字节写入一个管道中,而该管道在它的缓冲区中有u
个未用的字节. 具体地说,该标准要求涉及少量字节数的写操作必须原子地执行. 更确切地说,如果两个或者多个进程并发地在写入一
个管道,那么任何少于4096个字节的写操作都必须单独完成,而不能与唯一进程对同一个管道的写操作交叉进行.但是, 超过4096个字节
的写操作是可分割的,也可以强制调用进程睡眠.
如果管道没有读进程, 那么任何对管道执的写操作都会失败. 在这种情况下,内核会向写进程发送一个SIGPIPE信号,并停止write()
系统调用,使其返回一个-EPIPE错误码, 这个错误码就表示我们熟悉的"Broken pipe"消息.
pipe_write()函数执行具体操作(略)