26、深入理解计算机系统笔记,系统级I/O
1、一个unix文件就是一个m字节的序列(b0b1b2...bm-1)。所有的IO设备,如网络,磁盘,终端,都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
2、所有的输入和输出都被当作统一的方式来处理:
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个IO设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录这个打开文件的所有信息,而应用程序只需记住这个描述符。
Unix创建的每个进程开始都有三个打开的文件:标准输入(描述符为0),标准输出(描述符1),标准错误(描述符为2)。头文件unistd.h中定义了相关常量。
2)改变当前的位置。内核保持一个文件位置k,对于每个打开文件,初始为0。这个文件位置是从文件开头起始的字节偏移量。
3)内核释放文件打开时创建的数据结构,并恢复描述符到可获得描述符池中。
3、相关函数
open,close,read,write。具体定义可以参见man手册,或相关网页,或本书11.3。
1)某些情况下,原始的read,write传送的字节比应用程序要求的要少,这些不足值(short count)不一定是错误(意为,比如read函数参数要读5个字节,结果只读了3个字节,即小于5),下面的情况下将可能发生不足值:
(1)读时遇到EOF。
(2)从终端读文本行。
(3)读和写网络套接字。
为了避免不足值问题(健壮程序的需求,如Web服务器这样的网络应用),就需要处理不足值。本书作者11.4节开发了Rio包满足这个情况。
4、函数是线程安全的:它在同一个描述符上可以被交替地调用。
5、元数据:关于文件的信息,如创建时间,修改时间,metadata。可以调用stat,fstat函数。
6、对于内核而言,文件文件和二进制文件毫无区别。
7、共享文件
内核用三种相关的数据结构来表示打开的文件。
1)描述符表:它的表项由进程打开的文件描述符来索引的。
2)文件表:打开文件的集合是一张文件表来表示的,所有的进程共享这张表。
3)v-node表:同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大部分信息,如文件大小等。
多个描述符也可以通过不同的文件表项来引用同一个文件。
下图展示一个fork创建子进程的情况:子进程有一个父进程描述符的副本;父子进程共享相同的打开文件表集合,因此共享相同的文件位置。注意点:在内核删除相应文件表表项前,父子进程必须都关闭了它们的描述符。
8、IO重定向
unix > ls > foo.txt
实际是dup2函数(one case)。dup2函数对文件描述符表项进行重定位。
9、标准IO
标准函数如fopen,fclose,fread,fwrite,fgets,fputs,scanf,printf。
标准IO库将一个打开的文件模型化为一个流,对于程序员来说,一个流就是一个指向类型为FILE结构的一个指针。每个ANSI C程序开始都有三个打开的流stdin,stdout,stderr,分别对应于标准输入、输出、错误。
类型为FILE的流是对文件描述符和流缓冲区的抽象。
10、示例图
11、标准IO流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。但是有一些限定:
1)限定1:输入函数跟有输出函数之后。如果中间没有fflush,fseek,fsetpos或rewind的调用,一个输入函数不能跟随在一个输出函数之后。fflush清空与流相关的缓冲区,后三个函数使用unix IO lseek函数来重置当前文件的位置。
2)限定2:输出函数跟有输入函数之后。情况类同上。
这些限定给网络应用带来一个问题:因为对套接字使用lseek是非法的。用Rio包可以解决这个问题。用sprintf把输出格式化一个字符串,通过rio_writen输出,傅rio_readlineb来读一行,用scanf来从文本中提取不同的字段。