第十章 系统级I/O
输入/输出(I/O)是在主存和外部设备之间拷贝数据的过程,输入操作时从I/O设备拷贝数据到主存,而输出操作是从主存拷贝数据到I/O设备。
所有语言的运行时系统都提供执行I/O的较高级别的工具。
10.1 Unix I/O
一个Unix文件就是一个m个字节的序列,所有的I/O设备都被模型化为文件,所有的输入和输出都被当做对应的文件的读和写操作来执行。
- 打开文件
一个应用程序通过要求内核打开相应的文件,来宣告他想要访问一个I/O设备,内核返回一个小的非负整数叫做描述符,他在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。
Unix外壳创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0),标准输出(1),标准错误(2)。
- 改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置时从文件开头起始的字节偏移量,应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
- 读写文件
一个读操作就是从文件拷贝n>0个字节到存储器,从文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个EOF条件,检测到这个条件。类似的写操作就是存存储器拷贝n>0个字节到一个文件,从当前文件位置k开始然后更新k。
- 关闭文件
当应用完成了对文件的访问之后,他就通知内核关闭这个文件,作为响应,内核释放文件打开是创建的数据结构,并将这个描述符恢复到可用的描述符池中,无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的存储器资源。
10.2 打开和关闭文件
flags参数指明进程如何访问文件
-
O_RDONLY:只读
-
O_WRONLY:只写
-
O_RDWR:可读可写
flags参数也可以是一个或者更多位掩码的或:
-
O_CREAT:如果文件不存在,就创建他的一个截断的空文件
-
O_TRUNC:如果文件已经存在就截断它
-
O_APPEND:在每次写操作前,设置文件位置到文件的结尾处
访问权限位:
10.3 读和写文件
应用程序是通过分别调用read和write函数来执行输入和输出的。
read函数从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf。返回值-1表示一个错误而返回值0表示EOF否则返回值表示的是实际传送的字节数量。
write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置,read和write调用一次一个字节地从标准输入拷贝到标准输出。
一次一个字节地从标准输入拷贝到标准输出:
通过调用lseek函数,应用程序能够显示地修改当前文件的位置。
ssize_t 和 size_t有些什么区别
size_t 被定义为unsigned int,而ssize_t 则被定义为int,read函数返回一个有符号的大小,而不是一个无符号的大小,这是因为出错时他必须返回-1
有些情况下,read和write传送的字节比应用程序要求的要少,这些不足值不表示有错误,原因如下:
-
读时遇到了EOF
-
从终端读文本行
-
读和写网络套接字socket
除了EOF,在读磁盘文件时,将不会遇到不足值,而且在写磁盘文件时,也不会遇到不足值。
10.4 用RIO包健壮地读写
RIO包会自动为你处理上文中所述的不足值,RIO提供了两类不同的函数:
- 无缓冲的输入输出函数。
这些函数直接在存储器和文件之间传送数据,没用应用级缓冲,它们对将二进制数据读写到网络和从网络读写二进制数据尤其重要有用。
- 带缓冲的输入函数
允许高效低从文件中读取文本行和二进制数据,内容缓存在应用及缓存区内。
10.4.1 RIO的无缓冲的输入输出函数
rio_readn函数从描述符fd的当前文件位置最多传送n个字节到存储器位置usrbuf,rio_writen函数从位置usrbuf传送n个字节到描述符fd,rio_readn函数在遇到EOF时只能返回一个不足值,rio_writen函数绝不会返回不足值,对于同一个描述符可以交错任意地调用rio_readn和rio_writen。
10.4.2 RIO的带缓存的输入函数
一个文本行就是一个又换行符结尾的ASCII码字符序列
rio_readn 和 rio_writen函数:
RIO读程序和核心是rio_read函数,该函数是unix read函数的带缓冲的版本。
内部的rio_read函数:
对于一个应用程序,rio_read函数和Unix函数有同样的语义,出错时,返回值-1,适当设置errno,EOF时,返回值0,如果要求的字节数超过了读缓冲区内未读的字节数量,它会返回一个不足值。
10.5 读取文件元数据
应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(元数据)
stat函数以一个文件名作为输入,fstat函数以文件描述符而不是文件名作为输入。
stat数据结构:
st_size成员包含了文件的字节数大小,st_mode成员则编码了文件访问许可位和文件类型。普通文件包括某种类型的二进制或文本数据,对于内核而言,文本文件和二进制文件毫无区别,目录文件包含关于其他文件的信息,套接字是一种用来通过网络与其他进程通信的文件
根据st_mode位确定文件类型的宏指令:
10.6 共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表
每个进程都有他独立的描述符表,它的表项是由进程打开的文件描述符来索引的,每个打开的描述符表项指向文件表中的一个表项。
- 文件表
打开文件的集合是由一张文件表来展示的,所有的进程共享这张表,每个文件表的表项组成包括有当前的文件位置,引用计数(即当前指向该表项的描述符表项数)以及一个指向v-node表中对应表项的指针,关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为0。
- v-node表
同文件表一样,所有的进程共享这张v-node表,每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
典型的打开文件的内核数据结构:
文件共享:
子进程如何继承父进程的打开文件:
10.7 I/O重定向
I/O重定向操作符允许用户将磁盘文件和标准输入输出联系起来。I/O重定向使用dup2函数工作。
dup2函数拷贝描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容,如果newfd已经打开了,dup2会在拷贝oldfd之前关闭newfd。
通过调用dup2(4,1)重定向标准输出之后的内核数据结构:
10.8 标准I/O
ANSI C定义了一组高级输入输出函数称为标准I/O库,该库(libc)提供了打开和关闭文件的函数(fopen和fclose)、读和写字节的函数(fread和fwrite)、读和写字符串的函数(fgets和fputs),以及复杂的格式化I/O函数(scanf和printf)。
标准的I/O库将一个打开的文件模型化为一个流,一个流就是一个指向FILE类型的结构的指针,每个ANSI C程序开始时都有三个大开的流stdin、stdout和stderr分别对应于标准输入、标准输出和标准错误。
类型为FILE的流是对文件描述符和流缓冲区的抽象,流缓冲区目的和RIO读缓冲区目的一样:就是使开销较高的unix I/O系统调用的数量尽可能的小。
10.9 综合:我该使用哪些I/O函数
unix I/O、标准I/O和RIO之间的关系:
unix对网络的抽象是一种称为套接字的文件类型,套接字也是用文件描述符来引用的,在这种情况下称为套接字描述符。
标准I/O流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。
对流的限制和对套接字的先知,有时候会相互冲突:
-
限制一:跟在输出函数之后的输入函数
-
限制二:跟在输入函数之后的输出函数
对流I/O的第一个限制能够通过采用在每个输入操作前刷新缓冲区这样的规则来满足,满足第二个限制的唯一办法是,对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写。
建议在网络套接字上不要用标准I/O来进行输入和输出,而是使用健壮的RIO函数。
10.10 小结
unix提供了少量的系统级函数,它们允许用用程序打开、关闭、读和写文件,提取文件的元数据,以及执行I/O重定向,unix的读和写操作会出现不足值,应用程序必须能正确地预计和处理这种情况,应用程序不应该直接调用unix I/O 函数,而应该使用RIO包,RIO包通过反复执行读写操作,直到传送完所有的请求数据,自动处理不足值。
unix内核是用三个相关的数据结构来表示打开的文件,描述符表中的表项指向打开文件表中的表项,而打开文件表中的表项又指向v-node表中的表项,每个进程都有他自己单独的描述符表,而所有的进程共享同一个打开文件表和v-node表。
标准I/O库是基于unix I/O实现的,并提供了一组强大的高级I/O例程。对于大多数应用程序而言,标准I/O更简单,是优于unix I/O的选择,然而,对于标准I/O和网络文件的一些相互不兼容限制,unix I/O比标准I/O更适用于网络应用程序。