分类: BSD
2007-06-15 18:37:26
UNIX I/O系统的基本模型是一个可以被随机或顺序访问的字节序列。在典型UNIX用户进程中没有访问方法和控制块。
不同的应用程序期望不同的结构标准,但是内核不能把特定结构强加到I/O上。例如,文本文件的标准是被换行符分割的ASCII字符行,但是内核对这个标准完全不知。为了适应大部分应用程序的需要,I/O模型被更简单化成一个字节数据流或者一个I/O流。这是简单通用的数据模型,成为UNIX的特性,是UNIX工作的基础。一个I/O流可以从一个程序流向几乎其他任何程序。
描述符和I/O
UNIX进程使用描述符来引用I/O流。描述符是一个从open或socket系统调用获得的small unsigned int类型的变量。调用open需要文件名和许可模式两个参数,许可模式用来指定被打开的文件是否可读、可写或者可读写。这个系统调用也可以用来创建一个新的空文件。read和write系统调用可以用描述符来传递数据。close系统调用可以回收任何描述符。
描述符描述被内核支持的对象,被特定对象类型的系统调用创建。在FreeBSD中,描述符可以描述四种类型的对象:file(文件)、pipe(管道)、fifo和socket。
在4.2BSD以前的系统里,pipe是用文件系统实现的。当4.2BSD中引入了socket,pipe就作为socket来实现了。由于效率的原因,FreeBSD不再使用socket来实现pipe和fifo。而是单独实现以优化本地通讯。
内核为每个进程维护一张描述符表。内核用这个表来吧一个描述符从外部表示法翻译成内部表示法。描述符仅仅是这张表的索引号。进程的描述符表是从父进程继承来的,因此描述符描述的对象也是继承的。进程获得描述符主要有两个途径:
另外,socket IPC运行在运行在同一台机器上的进程之间通过消息传递描述符。
每个有效的描述符都有一个和它相联系的文件偏移,这个偏移从对象的开始部分算起,以字节为单位。读写操作都从这个偏移开始,每次数据传输后都会更新文件偏移。对于允许随机访问的对象,它的文件偏移也可以使用lseek系统调用来设置。普通的文件允许随机访问,有些设备文件也允许随机访问。但是pipe、fifo和socket不允许随机访问。
当一个进程终止时,进程会回收该进程使用的所有描述符。如果这个进程持有对象的最后一个引用,此时对象管理器会被通知,以便采取必要的清除操作。比如最终的文件删除或者socket的空间回收。
描述符管理
大多数进程开始运行时有三个描述符已经被打开。这三个描述符是0、1和2,分别是标准输入、标准输出和标准错误。通常登录进程把这三个描述符与用户的终端关联在一起(参加14.5节),通过用户运行的进程调用fork和exec来继承。因此,一个程序能够通过读标准输入来读取用户的输入,通过写标准输出来吧信息显示在用户的屏幕上。标准错误描述符也是为写入而打开的,用于错误的输出,而标准输出用于一般的输出。
这些(和其他的)描述符也可以被映射到终端以外的对象上。这种映射被称为I/O重定向。所有标准shell允许用户进行重定向。关闭描述符1,然后打开一个文件可以产生新的描述符1,这样可以把标准输出定向到一个文件。通过关闭描述符1然后打开文件,同样可以重定向标准输入。
管道(pipe)可以将一个程序的输出作为另外一个程序的输入,而不需要改写或连接任何程序。源进程的描述符1(标准输出)本来是输出到终端,而管道将它设置成为一个管道描述符的输入;同样的,目的进程的描述符0(标准输入)本来是与终端键盘关联,管道将它设置成管道的输出。设置这两个进程和连接管道的结果被称为一个pipeline。管道能够使pipeline被任意长的进程序列连接。
open、pipe和socket系统调用能够产生新的描述符,这个描述符是最小的能被用作描述符的未使用的数字。为了使pipeline能够工作,必须提供一种机制,能够把pipeline映射成0和1。dup系统调用能够产生一个指向相同文件表入库的描述符拷贝。这个新的描述符也是最小未使用的,但是如果期望使用的描述符被关闭了,dup能够映射到这个期望使用的描述符。需要小心的是,如果期望使用描述符1,而此时描述符0也被关闭了,此时不管使用什么方法,dup返回的也是0。为了避免这个问题,系统提供了dup2系统调用;它和dup相似,但是需要一个参数来指明期望的描述符。如果这个描述符已经被打开,dup2会事先关闭它。
设备
硬件设备有自己的文件名,用户可以使用访问普通文件的方法访问这些设备文件。内核能够辨别专门的设备文件,而且能够判断它所代表的设备。但是大多数进程不需要进行这种判断。终端、打印机和磁带设备都作为自己流来访问,就象FreeBSD的磁盘文件一样。因此设备的依赖和特性隐藏在内核中是可能的,甚至其中的许多设备的特性被隔离在驱动中。
进程访问设备的典型方法是通过文件系统中的设备文件。对这些设备文件的I/O操作被常驻内核的软件(设备驱动)控制。大多数网络通讯设备是通过进程间通讯工具访问,在文件系统的命名空间中没有他们的设备文件。因为raw-socket接口提供了比设备文件更自然的接口。
当设备第一次被检测到时,他的设备驱动就在/dev文件系统中为它创建一个设备文件。ioctl系统调用操作有参数指定的设备。它的操作能处理好设备直接的差异。这个系统调用运行访问设备的特殊属性,而不用重载其他的系统调用。例如,ioctl访问声卡,可以直接设置它的音频编码格式,而不用为这个操作写一个特殊的write版本。
Socket IPC
4.2BSD引入了一个比管道(pipe)更灵活的、基于socket的IPC机制。一个socket是通讯的一个端点,可以象操作文件和管道一样,用描述符来引用。两个进程可以各自创建一个socket,然后连接这两个端的来产生一个可靠的字节流。一旦连接成功,这个socket的描述符就可以被进程读写,就象操作管道一样。socket的透明度允许内核将一个进程的输出重定向到另外一台机器上的另一个进程的输入。管道和socket的主要区别是管道需要有一个共同的父进程来建立通讯通道。socket直接的通讯可以被两个不相干的进程创建,这两个进程甚至可以在不同的机器上。
命名管道(fifo)在文件系统中作为与进程无关对象出现,可以对它执行打开操作,也可以通过它发送数据,就好像通过一对socket通讯一样。因此,命名管道不需要一个共同的进程来创建他们。他们能在一对进程运行起来之后被连接。与socket不同,命名管道仅仅能够在用一台机器上使用。他们不能用于不同机器上的进程间的通讯。
socket机制需要扩展传统的Unix I/O系统调用来提供关联命名和连接的功能。开发人员没有重载现有的接口,而是用现有的接口来扩展后面的工作,没有为增加新的功能而进行修改或创建新的接口。read和write系统调用被用于字节流类型的连接,但是增加了六个系统调用来支持收取和发送带地址的消息,例如网络数据报。其中写消息的系统调用包括send、sendto和sendmsg;读消息的系统调用包括recv、recvfrom和recvmsg。
分散/聚合的I/O
除了传统的read和write系统调用外,4.2BSD还引入了执行分散/聚合I/O的能力。分散输入使用readv系统调用,使得一次单一的读取结果能够被放在几个不同的缓存中。相反的,writev系统调用允许把几个不同的缓存作为一次单一的原子写操作被操作。与执行read和write要传递一个单一的缓存和长度参数不同,执行readv和writev需要传递一个包含缓存和缓存长度的数组的指针,以及这个数组的长度描述。
这个工具运行一次原子写操作来操作位于进程地址空间不同部分的缓存,而不需要把他们复制到一个单独的缓存。原子写操作需要底层的数据获取是基于记录的,就象数据报每次请求输出一个单独的消息一样。读取一次请求到多个缓存也是很方便的(例如把记录头放在一个地方,而数据放在另外一个地方)。尽管一个应用程序可以通过读数据到一个大缓存然后再复制到知道的位置来模拟分散数据的能力,但是这种内存到内存的复制经常会对应用程序的运行时间产出超过两倍的影响。
正象send和recv可以作为sendto和recvfrom的库接口来执行一样,用readv模拟read和用writev模拟write也是可能的。然而,read和write使用是很频繁的,因为模拟他们而增加成本是不值得的。
多文件系统的支持
随着网络计算的膨胀,本地和远程文件系统的支持是有必要的。开发人员通过在内核中增加虚拟节点或vnode的接口使得支持多文件系统简单化。从vnode接口发布出的一组操作很像以前被本地文件系统支持的文件系统操作。然而,它可以被更广泛的文件系统支持。
通过动态加载内核模块,FreeBSD允许在使用mount系统调用第一次引用文件系统时动态的载入文件系统。vnode将在6.5节进行描述;它的辅助支持程序将在6.6节描述;几个特殊用途的文件系统将在6.7节描述。