CSAPP学习笔记 -- 第十章 系统级I/O

这一章偏向于I/O库的讲解,半数以上都是函数解析,概念相对于前几章较少。
具体函数的参数表和返回值请自行查阅。
 
10.1 Unix I/O

一个Linux文件就是一个m个字节的序列:B0,B1,…,Bk,…,Bm-1
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。UnixI/O 使得所有的输入和输出都能以一种统一且一致的方式来执行:
  • 打开文件
    • 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linux shell创建的每个进程开始时都有三个打开的文件
    • 标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)
  • 改变当前的文件位置
    • 对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
  • 读写文件
    • 一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件
    • 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
 
 
10.2 文件

文件类型
  • 普通文件(regular file)包含任意数据。
    • 应用程序常常要区分文本文件(text  file)和二进制文件(binary file)
    • 文本文件是只含有ASCII或Unicode字符的普通文件
    • 二进制文件是所有其他的文件。
    • 对内核而言,文本文件和二进制文件没有区别。
  • 目录(directory)
    • 是包含一组链接(link)的文件,其中每个链接都将一个文件名(filename)映射到一个文件,这个文件可能是另一个目录。每个日录至少含有两个条目:“.”是到该日录自身的链接,以及“..”是到目录层次结构(见下文)中父目录(parent directory)的链接。你可以用mkdir命令创建一个目录,用1s查看其内容,用rmdir删除该目录。
  • 套接字(socket)
    • 是用来与另一个进程进行跨网络通信的文件(11.4节)
  • 其他文件类型包括命名通道、符号链接、字符和块设备。
 
Linux内核将所有文件都组织成一个目录层次结构(directory hierarchy),由名为/(斜杠)的根目录确定。系统中的每个文件都是根目录的直接或间接的后代。
 
路径名:指定目录层次结构中的位置
  • 绝对路径名(absolute pathname)以一个斜杠开始,表示从根节点开始的路径。例如,在图10-1中,hello.c的绝对路径名为/home/droh/hello.c。
  • 相对路径名(relative pathname)以文件名开始,表示从当前工作目录开始的路径。例如,在图10-1中,如果/home/droh是当前工作目录,那么hello.c的相对路径名就是./hel1o.c。反之,如果/home/bryant是当前工作目录,那么相对路径名就是../home/droh/hel1o.c。
 
 
10.3 打开和关闭文件

进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的
 
进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。
 
 
10.4 读写文件

应用程序是通过分别调用read和write函数来执行输入和输出的。
在某些情况下,read和write传送的字节比应用程序要求的要少。这些不足值(short count)不表示有错误。出现这样情况的原因有:
  • 读时遇到EOF
    • 假设我们准备读一个文件,该文件从当前文件位置开始只含有20多个字节,而我们以50个字节的片进行读取。这样一来,下一个read返回的不足值为20,此后的read将通过返回不足值0来发出EOF信号。
  • 从终端读文本行
    • 如果打开文件是与终端相关联的(如键盘和显示器),那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小。
  • 读和写网络套接字(socket)
    • 如果打开的文件对应于网络套接字(11.4节),那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。对Linux管道(pipe)调用read和write时,也有可能出现不足值,这种进程间通信机制不在我们讨论的范围之内。
 
 
10.5 用RIO包健壮地读写(网络编程涉及)

RIO(Robust I/O,健壮的I/O)包,它会自动为你处理上文中所述的不足值。在像网络程序这样容易出现不足值的应用中,RIO包提供了方便、健壮和高效的I/O。RIO提供了两类不同的函数:
  • 无缓冲的输入输出函数
    • 这些函数直接在内存和文件之间传送数据,没有应用级缓冲。它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
  • 带缓冲的输入函数
    • 这些函数允许你高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于为printf这样的标准I/O函数提供的缓冲区。与[110]中讲述的带缓冲的I/O例程不同,带缓冲的RIO输入函数是线程安全的(12.7.1节),它在同一个描述符上可以被交错地调用。例如,你可以从一个描述符中读一些文本行,然后读取一些二进制数据,接着再多读取一些文本行。
 
 
10.6 读取文件元数据(网络编程涉及)

应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(有时也称为文件的元数据(metadata))
 
 
 
10.7 读取目录和内容

应用程序可以用readdir系列函数来读取目录的内容。
函数opendir以路径名为参数,返回指向目录流(directory stream)的指针。
流是对条目有序列表的抽象,在这里是指目录项的列表。
每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更多目录项则返回NULL。
函数closedir关闭流并释放其所有的资源。
 
 
10.8 共享文件

内核用三个相关的数据结构来表示打开的文件
  • 描述符表(descriptor table)
    • 每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
  • 文件表(file table)
    • 打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成(针对我们的目的)包括当前的文件位置、引用计数(reference count)(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
  • v-node表(v-node table)
    • 同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括stmode和st_size成员。
  • 文件共享
  • 调用fork后的情况(图10-14)
    • 子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。
 
 
 
 
10.9 I/O重定向

dup2函数可实现I/O重定向。
dup2函数复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项new-fd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
 
 
10.10 标准I/O

C语言定义了一组高级输入输出函数,称为标准I/O库,为程序员提供了Unix  I/O的较高级别的替代。这个库(libc)提供了打开和关闭文件的函数(fopen和fclose)、读和写字节的函数(fread和fwrite)、读和写字符串的函数(fgets和fputs),以及复杂的格式化的I/O函数(scanf和printf)。
类型为FILE的流是对文件描述符和流缓冲区的抽象。
流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的Linux1/O系统调用的数量尽可能得小。
 
 
10.11 综合:我该使用哪些I/O函数(个人认为是重点)

使用这些函数中的哪一个的指导原则
  • G1:只要有可能就使用标准I/O
    • 对磁盘和终端设备I/O来说,标准I/O函数是首选方法。大多数C程序员在其整个职业生涯中只使用标准I/O,从不受较低级的UnixI/O函数的困扰(可能stat除外,因为在标准I/O库中没有与它对应的函数)。只要可能,我们建议你也这样做。
  • G2:不要使用 scanf或rio_readlineb来读二进制文件
    • 像scanf或rio_read-lineb这样的函数是专门设计来读取文本文件的。学生通常会犯的一个错误就是用这些函数来读取二进制文件,这就使得他们的程序出现了诡异莫测的失败。比如,二进制文件可能散布着很多0xa字节,而这些字节又与终止文本行无关。
  • G3:对网络套接字的1/O使用RIO函数
    • 不幸的是,当我们试着将标准I/O用于网络的输入输出时,出现了一些令人讨厌的问题。如同我们将在11.4节所见,Linux对网络的抽象是一种称为套接字的文件类型。就像所有的Linux文件一样,套接字由文件描述符来引用,在这种情况下称为套接字描述符。应用程序进程通过读写套接字描述符来与运行在其他计算机的进程实现通信。
 
标准I/O流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。然而,对流的限制和对套接字的限制,有时候会互相冲突,而又极少有文档描述这些现象:
  • 限制一:跟在输出函数之后的输入函数。
    • 如果中间没有插入对fflush、fseek、fsetpos或者rewind的调用,一个输入函数不能跟随在一个输出函数之后。fflush函数清空与流相关的缓冲区。后三个函数使用Unix I/O lseek函数来重置当前的文件位置。
  • 限制二:跟在输入函数之后的输出函数。
    • 如果中间没有插入对fseek、fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个文件结束。
这些限制给网络应用带来了一个问题,因为对套接字使用lseek函数是非法的。对流I/O的第一个限制能够通过采用在每个输入操作前刷新缓冲区这样的规则来满足。然而,要满足第二个限制的唯一办法是,对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写。
 
posted @ 2020-10-27 23:06  Yoke_cc  阅读(90)  评论(0编辑  收藏  举报