CSAPP深入理解计算机系统笔记 第10章 系统级I/O 知识点总结,详细

  • 现实世界里,应用程序利用操作系统提供的服务来与I/O设备及其他程序通信。
  • 这一章讲述Unix操作系统提供的基本I/O服务,以及如何使用这些服务来构造应用程序。
    例如Web客户端和服务器,他们是通过Internet彼此通信的。
  • 将学习编写诸如Web服务器这样的可以同时为多个客户端提供服务的并发程序。
    编写并发应用程序,还能使程序在现代多核处理器上执行的更快。
  • 输入/输出(I/O):是在储存和外部设备(磁盘驱动器、中端和网络等)直接复制数据的过程。
  • 输入操作:从I/O设备复制数据到主存。
  • 输出操作:从主存复制数据到I/O设备。
  • Unix I/O
    • 一个Linux文件就是一个m个字节的序列:


      所有的I/O设备(网络、磁盘、终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。
    • Unix I/O:将设备映射为文件的方式,允许Linux内核,引出一个简单、低级的应用接口,称为Unix。
    • 使得所有的输入和输出都能以一种统一且一致的方式来执行:
      • ①打开文件。
        一个应用程序通过要求内核,打开相应的文件,来宣告他想要访问一个I/O设备。内核反回一个小的非负整数,叫描述符,在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
      • ② Linux shell创建的每个进程开始时都有三个打开的文件:
        • 标准输入(描述符为0)
        • 标准输出(描述符为1)
        • 标准错误(描述符为2)
          头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,他们可用来代替显示的描述符值。
      • ③改变当前的文件位置。
        对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
      • ④读写文件。
        一个读操作就是从文件复制n>0个字截到内存,从当前文件位置k开始,然后将k增加到k+n。
        给定一个大小为m字节的文件,当k≥m时,执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
        类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
      • ⑤关闭文件。
        当应用完成对文件的访问之后,就通知内核关闭这个文件。作为响应内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程应为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
  • 文件
    • Linux文件的类型(type):
      • ①普通文件(regular file)包含任意数据。
        应用程序常常要区分文本文件和二进制文件:
        文本文件(text file):只含有ASCII或Unicode字符的普通文件。
        二进制文件(binary file):所有其他的文件。
        对内核而言,本文件和二进制文件没有区别。
        Linux文本文件包含了一个文本行(text file)序列,其中每一行都是一个字符序列,以一个新行符(\n)结束。新行符与ASCII的换行符(LF)是一样的,其数字值为0x0a。
      • ②目录(directory)包含一组链接(link)的文件,其中每个链接都将一个文件名(filename)映射到一个文件,这个文件可能是另一个目录。
        每个目录至少还有两个条目:“.”是到该目录自身的链接,以及“..”适当目录,层次结构中父目录(parent directory)的链接。
        ​mkdir:命令创建一个目录
        ​1s:查看其内容
        ​rmdir:删除该目录
      • ③套接字(socket)用来与另一个进程进行跨网络通信的文件。
    • 其他文件类型包含命名通道(named file)、符号链接(symbolic link)、字符和块设备(character and block device)。
    • Linux内核,将所有文件组织成一个目录层次结构(directory hierarchy),由名为/(斜杠)的根目录确定。下图显示了Linux系统的目录层次结构的一部分:


      系统中的每个文件都是根目录的直接或间接的后代。
    • 每个进程都有一个当前工作目录(current working directory)来确定其在目录层次结构中的当前位置。
      cd:修改shell中的当前工作目录。
    • 目录层次结构中的位置,用路径名(pathname)来指定。
    • 路径名是一个字符串,包括一个可选斜杠,其后紧跟一系列文件名,文件名之间用斜杠分隔。
    • 路径名的两种形式:
      • ①绝对路径名(absolute pathname)以一个斜杠开始,表示从根节点开始的路径。
        例如Hello.c的绝对路径名为/home/droh/hello.c。
      • ②相对路径名(relative pathname)以文件名开始,表示从当前工作目录开始的路径。
        例如如果/home/droh是当前工作目录,那么hello.c的相对路径名就是./hello.c。反之,如果/home/bryant是当前工作目录,那么相对路径名就是../home/droh/hello.c。
  • 打开和关闭文件
    • 进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件。

    • open函数将filename转化为一个文件描述符,并且返回描述符数字。
    • 返回的描述符总是在进程中当前没有打开的最小描述符。
    • flags参数指明了菁纯打算如何访问这个文件:
      • ①O_RDONLY:只读。
      • ②O_WRONLY:只写。
      • ③O_RDWR:可读可写。
    • 以读的方式打开一个已存在的文件:
      • fd = Open("foo.text", O_RDONLY, O);
    • flags参数也可以是一个或者更多位掩码的或,为写提供额外的指示:
      • O_CREAT:如果文件不存在,就创建它的一个截断的(truncated)(空)文件。
      • O_TRUNC:如果文件已经存在,就截断它。
      • O_APPEND:每次写操作前,设置文件位置到文件的结尾处。
    • 如何打开一个已存在的文件,并在后面添加一些数据:
      • fd = Open("foo.text", O_WRONLY|O_APPEND, 0);


        mode参数指定了新文件的访问权限位。这些位的符号名字如上图。
    • 作为上下文的一部分,每个进程都有一个umask,是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限未被设置为mode&~umask。
    • 假设给定下面的mode和umask默认值:

    • 创建一个新文件文件的拥有者,有读写权限,而其他用户都有读权限:
      • umask(DEF_UMASK);
      • fd = Open("foo.text", O_CREAT|O_WRONLY, DEF_MODE);
    • 最后,进程通过调用close函数关闭一个打开的文件。


      返回:若成功为0,若出错为-1。
    • 关闭一个已关闭的描述符会出错。
    • 练习题

  • 读和写文件
    • 应用程序分别调用read和write函数来执行输入和输出:

    • read函数:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
      返回值-1表示一个错误,返回值零表示EOF。否则,返回值表示的是实际传送的字节数量。
    • write函数:从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
    • 下图展示了一个程序使用read和write调用一次一个字节地从标准输入复制到标准输出:

    • lseek函数:应用程序能够显示的修改当前文件的位置。


      这部分内容不在讲述范围之内。
    • 某些情况下,read和write传送的字节比应用程序要求的少。这些不足值(short count)不表示有错误,出现这样情况的原因:
      • ①读时遇到EOF。
      • ②从终端读文本行。
      • ③读和写网络套接字(socket)。
      • (pipe)调用read和write时,也有可能出现不足值这种进程间通信机制不在讨论范围内。
    • 想创建健壮的(可靠的)诸如Web服务器这样的网络应用,必须通过反复调用read和write处理不足值,直到所有需要的字节都传送完毕。
  • 用RIO包健壮地读写
    • RIO(Robust I/O,健壮的I/O)包:会自动处理上文中所述的不足值。(是一个I/O包)
      在像网络程序这样容易出现不足值的应用中,RIO包提供了方便、健壮和高效的I/O。
    • RIO提供了两类不同的函数:
      • ①无缓冲的输入输出函数。
        这些函数直接在内存和文件之间传送数据,没有应用及缓冲。对将二进制读数据读写到网络和从网络读写二进制数据尤其有用。
      • ②带缓冲的输入函数。
        这些函数允许高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于为printf这样的标准函数提供的缓冲区。
        带缓冲的RIO输入函数是线程安全的,在同一个描述符上可以被交错地调用。例如,可以从一个描述符中读一些文本行,然后读取一些二进制数据,接着再多读取一些文本行。
    • RIO的无缓冲的输入输出函数
      • 通过调用rio_readn和rio_writen函数应用程序,可在内存和文件之间直接传送数据。

      • rio_readn函数:从描述符fd的当前文件位置最多传送n个字节到内存位置userbuf。
        rio_readn函数在遇到EOF时,只能返回一个不足值。
      • rio_writen函数:从位置userbuf传送n个字节到描述符fd。
        rio_writen函数绝不会返回不足值。
      • 对同一个描述符可以任意交错的调用rio_readn和rio_writen。
      • 下图显示了rio_readn和rio_writen的代码。注意,如果rio_readn和rio_writen函数被一个从应用信号处理程序的返回中断,那么每个函数都会手动地重启rio_readn或rio_writen,为了尽可能有较好的可移植性,允许被中断的系统调用,且在必要时重启他们。

    • RIO的带缓冲的输入函数
      • rio_readlineb函数:从一个内部读缓冲区复制一个文本行,当缓冲区变空时,会自动调用read重新填满缓冲区。
      • 对于既包含文本行也包含二进制数据的文件,也提供了一个rio_readn带缓冲区的版本rio_readnb, 它从和rio_redlineb一样的读缓冲区中传送原始字节。

      • 每打开一个描述符,都会调用一次rio_readinitb函数:它将描述符fd和地址rp处的一个类型为rio_t的读缓冲区联系起来。
      • rio_readlineb函数从文件rp读出下一个文本行(包括结尾的换行符),将它复制到内存位置userbuf,并且用NULL(零)字符来结束这个文本行。
      • rio_readlineb函数最多读maxlen-1个字节,余下的一个字符留给结尾的NULL字符。超过maxlen-1字节的文本行被截断,并用一个NULL字符结束。
      • rio_readnb函数从文件rp最多读n个字节到内存位置userbuf。
      • 对同一描述符对rio_readlineb和rio_readnb的调用可以任意交叉进行。
      • ★带缓冲的函数的调用,却不应该和无缓冲的rio_read函数交叉使用。
      • 如下是大量的RIO函数示例。
        • 使用RIO函数来一次一行的从标准输入复制一个文本文件到标准输出:

        • 一个读缓冲区的格式,以及初始化它的rio_readinitb函数的代码:

        • RIO读程序的核心是下图所示的rio_read函数。


          rio_read是函数Linux read函数的带缓冲版本。
          ​当调用rio_read要求读n个字节时,读缓冲区内有rp->rio_cnt个未读字节。
          如果缓冲区为空,那么会通过调用read再填满它。
          这个read调用收到一个不足值并不是错误,只不过读缓冲区是填充了一部分。一但缓冲区非空,rio_read就从读缓冲区复制n和rp->rio_cnt中较小值个字节到用户缓冲区,并返回复制的字节数。
      • 对于一个应用程序,rio_read函数和Linux read函数有同样的语义。

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

    • stat函数:以一个文件名作为输入并填写如下图所示的一个stat数据结构中的各个成员。

    • fstat函数:以文件描述符而不是文件名作为输入。
    • st_size成员包含了文件的字节数大小。
    • st_mode成员则编码了文件访问许可位。
    • Linux在sys/stat.h中定义了宏谓语来确定st_mode成员的文件类型:
      • ①S_ISREG(m):这是一个普通文件吗?
      • ②S_ISDIR(m):这是一个目录文件吗?
      • ③S_ISSOCK(m):这是一个网络淘汰制吗?
    • 下图展示了如何使用这些宏和stat函数来读取和解释一个文件的st_mode位。

  • 读取目录内容
    • readdir系列函数:读取目录的内容。

    • opendir函数:以路径名为参数,返回指向目录流(directory scream)的指针。
    • 流:对条目有序列表的抽象。
      在这里是指目录项的列表。
    • 每次对reafdir的调用返回的都是指向流dirp中下一个目录项的指针,如果没有更多目录项则返回NULL。
    • 每个目录项都是一个结构,形式如下:

    • 成员d-name是文件名
    • d_ino是文件位置。
    • 如果出错,则readdir返回NULL, 并设置errno。唯一能区分错误和流结束情况的方法是检查自调用readdir以来errno是否被修改过。

    • closedir函数:关闭流并释放其所有的资源。
    • 下图展示怎样用readdir函数来读取目录的内容:

  • 共享文件
    • 可以用许多不同的方式来共享Linux文件。
    • 内核用三个相关的数据结构来表示打开的文件:
      • ①描述符表(descriptor table)
        每个进程都有独立的描述符表,它的表项是由进程打开的文件描述出来索引的。
        ​每个打开的描述符表项指向文件表中的一个表象。
      • ②文件表(file table)
        打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。
        每个文件表的表项组成包括当前的文件位置、引用计数(当前指向该表象的描述符表项数),以及一个指向v-node表中对应表象的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。
        内核不会删除这个文件,表表象直到它的引用计数为零。
      • ③v-node表(v-node table)
        同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st-mode和st_size成员。
    • 下图展示:描述符1和4通过不同打开文件表表象来引用两个不同文件。这是一种典型情况,没有共享文件,并且每个描述符对应一个不同的文件。

    • 下图所示:多个描述符也可以通过不同的文件表表象来引用同一个文件。


      如果以同一个filename调用open函数两次,就会发生这种情况。
      关键思想:每个描述符都有他自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
    • 在内核删除相应文件表表象之前,父子进程必须都关闭了他们的描述符。

    • 练习题

    • 习题答案
  • I/O重定向
    • Linux shell提供了I/O重定操作符:允许用户将磁盘文件和标准输入输出联系起来。
    • 当一个Web服务器代表客户端运行程序时,它就执行一种相似类型的重定向。
    • 例如,键入:
      • linux> ls > foo.txt
        使得shell加载和执行1s程序将标准输出重定向到磁盘文件foo.txt。
    • I/O重定向如何工作的?一种方式是使用dup2函数。

    • dup2函数复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。
    • 如果newfd已经打开了,dup2会在复制oldfd之间关闭newfd。
    • 调用dup2(4,1)之前,状态如图10-12所示。其中描述符1(标准输出)对应于文件A(比如一个终端),描述符4对应于文件B(比如一个磁盘文件)。
    • 下图显示了调用dup2(4,1)之后的情况。两个描述符现在都指向文件B;文件A已经被关闭了,并且它的文件表和v-node表表项也已经被删除了;文件B的引用计数已经增加了。从此以后,任何写到标准输出的数据都被重定向到文件B。

    • 练习题

    • 习题答案
  • 标准I/O
    • 标准I/O库: C语言定义的一组高级输入输出函数,为程序员提供了Unix I/O的较高级别的替代。
    • 标准I/O库提供了打开和关闭文件的函数(fopen和fclose)、读和写字节的函数(fread和fwrite)、读和写字符串的函数(fgets和fputs),以及复杂的格式化I/O函数(scanf和printf)。
    • 标准库将一个打开的文件模型化为一个流。
    • 对于程序员来说,一个流就是一个指向FILE类型的结构的指针。
    • 每个ANSI C程序开始时都有三个打开的流:
      • ①stdin(标准输入)
      • ②stdout(标准输出)
      • ③stderr(标准错误)

    • 类型为FILE的流是对文件描述符和流缓冲区的抽象。
    • 流缓冲区的目的和RIO读缓冲区的一样:使开销较高的Linux系统调用own的数量尽可能小。
      假设有一个程序,它反复调用标准I/O的gets函数,每次调用返回文件的下一个字符。当第一次调用gets时,库通过调用一次read函数来填充流缓冲区,然后将缓冲区的第一个字节返回给应用程序,。只要缓冲区中还有未读的字节,接下来对gets的调用就能直接从流缓冲区得到服务。
  • 综合:我该使用哪些I/O函数?
    • 这一章里讨论过的各种I/O包:

    • Unix I/O模型是在操作系统内核中实现的。
    • 应用程序可通过诸如open、close、lseek、read、write和stat这样的函数来访问Unix I/O。
    • 较高级别的RIO和标准I/O函数是基于(使用)Unix I/O函数实现。
    • RIO函数是专门为本书开发的read和write的健壮的包装函数。
      会自动处理不足值,并且为文本行提供一种高校的带缓冲的方法。
    • 标准I/O函数提供了Unix I/O函数的一个更加完整的带缓冲的替代品,包括格式化的例程,如printf和scanf。
    • 基本的指导原则(程序中该使用哪个函数):
      • ①只要有可能就使用标准I/O。
      • ②不要使用scanf或rio_readlineb来读二进制文件。
      • ③对网络套接字的I/O使用RIO函数。
    • 标准I/O流,从某种意义上是全双工的,因为程序能够在同一个流上执行输入和输出。
    • 对流的限制和对套件字的限制有时会相互冲突,很少有文档描述这些现象:
      • ①限制一:跟在输出函数之后的输入函数。
        如果中间没有插入对fflush、fseek、fsetpos或者rewind的调用,一个输入函数不能跟随在一个输出函数之后。
        fflush函数清空与流相关的缓冲区。
        ​后三个函数使用Unix I/O lseek函数来重置当前的文件位置。
      • ②限制二:跟在输入函数之后的输出函数。
        如果中间没有插入对fseek、fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个文件结束。
    • 这些限制给网络应用带来一个问题,对套接字使用lseek函数是非法的。
    • 对流I/O的第一个限制:通过采用在每个输入操作前刷新缓冲区来满足。
    • 满足第二个限制的唯一办法:对同一个打开的套接字描述符,打开两个流,一个用来读一个用来写。

    • 但是这种方法要求应用程序在两个流上都要调用fclose,这样才能释放与每个流相关联的内存资源,避免内存泄漏:

    • 这些操作中的每一个都试图关闭同一个底层的套接字描述符,所以第二个close操作就会失败。
    • 对于顺序的程序来说不是问题,但在一个线程化的程序中关闭一个已经关闭了的描述符,会导致灾难。
    • 网络套接字上不要使用标准I/O函数来进行输入和输出,而要使用健壮的RIO函数。
    • 如需格式化输出,使用sprintf函数在内存中格式化一个字符串,然后用rio_written把它发送到套接口。
    • 如需格式化输入,使用rio_readlineb来读一个完整的文本行,然后用sscanf从文本行提取不同的字段。
  • 小结
    • Linux提供了少量基于Unix I/O模型的系统级函数,它们允许应用程序打开、关闭、读和写文件,提取文件的元数据,以及执行I/O重定向。
    • Linux的读和写操作会出现不足值,应用程序必须能正确的预计和处理这种情况。
    • 应用程序不应直接调用Unix I/O函数,而应该使用RIO包,RIO包通过反复执行读写操作,直到传送完所有的请求,数据自动处理不足值。
    • Linux内核使用三个相关数据结构表示打开的文件。
    • 描述符表中的表项指向打开文件表中的表项,而打开文件表中的表项又指向v-nofe表中的表项。
    • 每个进程都有它自己单独的描述符表,而所有进程共享同一个打开文件表和v-node表。
    • 标准I/O库是基于Unix I/O实现的,并提供一组强大的高级I/O例程。
    • 对标准I/O和网络文件的一些相互不兼容的限制,Unix I/O比标准I/O更适用于网络应用程序。

猜你喜欢

转载自blog.csdn.net/qq_51607668/article/details/119749342
今日推荐