第十章 系统级I/O

time will tell Lv4

10.1 Unix I/O

一个 Linux文件就是一个m个字节的序列:
B1,B2, …, Bk, …,Bm-1

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,输入和输出被当作对应文件的读和写进行

通过如下统一一致的方式执行

打开文件

一个应用程序通过要求内核打开相应的文件,标志访问一个I/O 设备。内核返回一个叫做描述符的小的非负整数,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。

Linuxshell创建进程时打开3个文件:标准输入(0)/输出(1)/错误(2)

头文件< unistd.h> 定义了常量 STDIN_ FILENO、 STDOUT_FILENO 和 STDERR_FILENO, 它们可用来代替显式的描述符值。

改变当前文件位置:初始值和偏移量

每个打开的文件,内核保持着一个文件位置 k, 初始为0 。这个文件位置是从文件开头起始的字节偏移量。通过执行 seek操作,可以显式地设置文件的当前位置为k。

读写文件:EOF符号

读操作从文件复制 n(n>0) 个字节到内存,,从当前文件位置k 开始,然后将K增加到k+n。当k大于等于文件字节时触发一个称为 end-of-file(EOF) 的条件。

关闭文件

当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。重点在于关闭所有打开的文件并释放它们的内存资源。

10.2 文件

每个linux文件都有一个类型(type)表示系统中的角色

  • 普通文件(regular file)包含任意数据
    文本文件:ASCII或Unicode字符的普通文件
    二进制文件:所有其他文件
    文本行:\n,换行符LF/0x0a
  • 目录(directory)包含一组链接(link)文件
    链接将文件名映射到一个文件,”.”该目录到自身的链接,”..”到层次结构中父目录的链接
  • 套接字(socket)用来与另一个进程进行跨网络通信的文件

Linux内核将所有文件都组织成一个目录层次结构(directory hierarchy),而每个进程都有一个当前工作目录(current working directory)来确定其在目录层次结构中的当前位置

1734400579972

目录层次结构中的位置用路径名(pathname)来指定

  • 绝对路径名(absolute pathname)以一个斜杠开始,表示从根节点开始的路径。
    在图10-1中,hel1o.c的绝对路径名为/home/droh/hello.c
  • 相对路径名(relative pathname)以文件名开始,表示从当前工作目录开始的路径
    在图10-1中,如果/home/droh是当前工作目录,那么hello.c的相对路径名就是./hel1o.c。反之,如果/home/bryant是当前工作目录,那么相对路径名就是../home/droh/hello.c.此处有待商酌

10.3 打开和关闭文件

进程通过调用open函数来打开一个已存在的文件或者创建一个新文件

1734401254248

open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。

返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

1734401312026

实际中这样调用

1
fd =Open("foo.txt",O_RDONLY,0);

flags参数为写提供给一些额外的指示,也可以是一个或者更多位掩码的或:

1734401528098

实际中这样调用

1
fd =Open("foo.txt",O_WRONLY|O_APPEND, 0)

mode 参数指定了新文件的访问权限位。

每个进程都有一个umask,它通过调用umask函数来设置。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为 mode &~umask。

1734401715551

访问权限位。在sys/stat.h中定义

我们可以这样调用

1
2
umask(DEF_UMASK),
fd = Open("foo.txt",O_CREAT|O_TRUNC|O_WRONLY, DEF_MODE);

10.4 读写文件

应用程序分别调用 read 和 write 函数来执行输入输出
1734402026145

read 函数从描述符为 fd的当前文件位置复制最多 n 个字节到内存位置 buf。返回值 -1 表示一个错误,而返回值 0 表示 EOF 。否则,返回值表示的是实际传送的字节数量。
write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。

调用一次一个字节从标准输入复制到标准输出1734402136747

调用 lseek 函数,应用程序能够显示地修改当前文件的位置

size_t被定义位unsigned long,ssize_t被定义位long,read返回一个有符号的大小,因为出错时必须返回-1,这导致read的最大值减少了一半

不足值体现了read和write传送的字节比要求的少,但这不一定表示有错误:

  • 读时遇到EOF
  • 从终端读文本行
  • 读和写网络套接字(socket)

10.5 用RIO包健壮地读写(Robust I/O)

RIO提供了两类不同的函数:

  • 无缓冲的输入输出函数,直接在内存和文件之间传送数据,没有应用级缓冲。(将二进制数据读写到网络和从网络读写二进制数据尤其有用)
  • 带缓冲的输入函数,允许高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内(类似于为printf这样的标准I/O函数提供的缓冲区)

接下来详细介绍这两种

10.5.1 RIO的无缓冲的输入输出函数

无缓冲函数包括rio_readn和 rio_writen 函数,在内存和文件之间直接传送数据
1734402930747

rio_readn 函数从描述符 fd的当前文件位置最多传送n个字节到内存位置 usrbuf.

rio_writen函数从位置usrbuf传送n个字节到描述符 fd

rio_read 函数在遇到 EOF 时只能返回一个不足值。rio_writen函数决不会返回不足值。

对同一个描述符,可以任意交错地调用 rio_readn和 rio_writen。

10.5.2 RIO的带缓冲的输入函数

rio_readlineb函数从一个内部读缓冲区复制一个文本行,当缓冲区变空时,会自动地调用read重新填满缓冲区。
实际情况是:rio_readlineb 函数从文件 rp读出下一个文本行(包括结尾的换行符),将它复制到内存位置 usrbuf,并且用 NULL(零)字符来结束这个文本行。rio_readlineb 函数最多读 maxlen-1个字节,余下的一个字符留给结尾的 NULL字符。超过 maxlen-1字节的文本行将被截断,并用一个NULL字符结束。

rio_readnb函数从文件rp最多读n个字节到内存位置usrbuf。对同一描述符,对rio_readlineb和rio_readnb的调用可以任意交叉进行。然而,对这些带缓冲的函数的调用却不应和无缓冲的rio_readn 函数交叉使用。

对于一个应用程序,rio_read 函数和 Linux read 函数有同样的语义。在出错时,它返回值一1,并且适当地设置errno。在EOF时,它返回值0。如果要求的字节数超过了读缓冲区内未读的字节的数量,它会返回一个不足值。两个函数的相似性使得很容易通过用rio_read 代替read来创建不同类型的带缓冲的读函数。

10.6 读取文件元数据

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

stat函数以一个文件名作为输入,并填写一个stat数据结构中的各个成员

fstat函数是相似的,只不过是以文件描述符而不是文件名作为输入

1734404565128

st_size成员包含了文件的字节数大小。st_mode 成员则编码了文件访问许可位(图10-2访问权限位)和文件类型(10.2节)

Linux在sys/stat.h中定义了宏谓词来确定 st_mode 成员的文件类型:
1734404720450

10.7 读取目录内容

应用程序实验readdir系列函数来读取目录的内容

1、opendir

函数 opendir 以路径名为参数,返回指向目录流(directory stream)的指针。
1734404834021

流是对条目有序列表的抽象,在这里是指目录项的列表。

2、readdir

每次对 readdir的调用返回的都是指向流 dirp中下一个目录项的指针,或者,如果没有更多目录项则返回NULL。
1734404868840

或者说每个目录项都是一个结构,形式如下:
1734404894605

如果出错,则readdir返回NULL,并设置errno。唯一能区分错误和流结束情况的方法是检查自调用readdir以来errno是否被修改过。

3、closedir

函数closedir关闭流并释放其所有的资源

10.8 共享文件

内核用三个相关的数据结构来表示打开的文件

  • 描述符表(descriptor table):表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
  • 文件表(file table):打开文件的集合是由一张文件表来表示的,所有的进程共享这
    张表。
    每个文件表的表项组成(针对我们的目的)包括当前的文件位置、引用计数(reference count)(即当前指向该表项的描述符表项数),以及一个指向v-node 表中对应表项的指针。
  • v-node 表(v-node table)。同文件表一样,所有的进程共享这张 v-node 表。每个表项包含 stat 结构中的大多数信息,包括st_mode 和 st_size 成员。

1734419152556

但是多个描述符也可以通过不同的文件表表项来引用同一个文件
1734419211138

父子进程共享文件时,子进程copy一个父进程描述符表的副本。父子进程共享相同的打开文件表合,因此共享相同的文件位置。同样的,在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。
1734419672449

10.9 I/O重定向

定义: 重定向是指改变一个程序的标准输入、输出或错误输出流的目标。

Linux shell提供了 I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。
1734419804903
这一段指令使得 shell加载和执行 ls程序,将标准输出重定向到磁盘文件 foo.txt。

I/O重定向工作一种方式是使用dup2函数
1734420038159

dup2函数复制描述符表表项oldfd 到描述符表表项 newfd,覆盖描述符表表项 newfd 以前的内容。如果 newfd已经打开了,dup2会在复制 oldfd 之前关闭 newfd。

1734420232996

这样能将1和4都重定向到文件B上,此时文件A已经被关闭了,并且它的文件表和v-node 表表项也已经被删除了;文件B的引用计数已经增加了。从此以后,任何写到标准输出的数据都被重定向到文件B。

10.10 标准I/O

标准 I/0库将一个打开的文件模型化为一个流。上文其实也有提到过流的定义:

在这里一个流就是一个指向 FILE类型的结构的指针,类型为FILE的流是对文件描述符和流缓冲区的抽象

每个ANSIC程序开始时都有三个打开的流stdin、stdout和 stderr,分别对应于标准输人、标准输出和标准错误:
1734420674595

流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的LinuxI/O系统调用的数量尽可能得小。

10.11 综合

1734420897770

  • G1:尽可能使用标准I/〇。
    对磁盘和终端设备I/0来说,标准I/O函数是首选方法。大多数C程序员在其整个职业生涯中只使用标准I/0,从不受较低级的UnixI/0函数的困扰(可能stat除外,因为在标准I/0库中没有与它对应的函数)。只要可能,我们建议你也这样做。
  • G2:不要使用 scanfrio_readlineb来读二进制文件。
    像scanf或rio readlineb这样的函数是专门设计来读取文本文件的。学生通常会犯的一个错误就是用这些函数来读取二进制文件,这就使得他们的程序出现了诡异莫测的失败。比如,二进制文件可能散布着很多0xa字节,而这些字节又与终止文本行无关。
  • G3:对网络套接字的I/O使用RIO函数。
    不幸的是,当我们试着将标准I/O用于网络的输入输出时,出现了一些令人讨厌的问题。如同我们将在11.4节所见,Linux对网络的抽象是一种称为套接字的文件类型。就像所有的Linux文件一样套接字由文件描述符来引用,在这种情况下称为套接字描述符。应用程序进程通过读写套接字描述符来与运行在其他计算机的进程实现通信。

标准 I/O流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。然而,对流的限制和对套接字的限制,有时候会互相冲突,因此有如下限制:

  • 限制一:跟在输出函数之后的输入函数。如果中间没有插人对fflush、fseek、fsetpos或者rewind的调用,一个输人函数不能跟随在一个输出函数之后。fflush 函数清空与流相关的缓冲区。后三个函数使用 Unix I/O 1seek函数来重置当前的文件位置。
  • 限制二:跟在输入函数之后的输出函数。如果中间没有插入对fseek、fsetpos或者rewind的调用,一个输出函数不能跟随在一个输人函数之后,除非该输人函数遇到了一个文件结束。
  • Title: 第十章 系统级I/O
  • Author: time will tell
  • Created at : 2024-12-17 09:37:03
  • Updated at : 2024-12-18 11:03:57
  • Link: https://sbwrn.github.io/2024/12/17/第十章 系统级IO/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments