输入/输出(I/O)是在主存和外部设备(如磁盘驱动器、终端和网络)之间拷贝数据的过程。

Unix I/O

一个 Unix 文件就是一个 m 个字节的序列。 所有的 I/O 设备,都被模型化为文件,而所有的输入和输出都被当做对应的文件的读和写来执行。 这种将设备优雅的映射为文件的方式,允许 Unix 内核引出一个简单、低级的应用接口,称为 Unix I/O。

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。 内核返回描述符。内核记录有关打开这个文件的所有信息,应用程序只需记住这个描述符。 Unix 外壳创建的每个进程开始时都有三个打开的文件:标准输入(0)、标准输出(1)和标准错误(2)。
  2. 改变当前的文件位置。 对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作,显式的设置文件的当前位置为 k。
  3. 读写文件。一个读操作就是从文件拷贝 n > 0 个字节到存储器。一个写操作就是从存储器拷贝 n > 0 个字节到一个文件。
  4. 关闭文件。当应用完成对文件的访问之后,应用就通知内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的存储器资源。

打开和关闭文件

进程通过调用 open 函数来打开一个已存在的文件或者创建一个新文件。若成功则返回新文件描述符。 返回的描述符总是进程中当前没有打开的最小描述符。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(char *filename, int flags, mode_t mode);

flags 参数指明了进程打算如何访问这个文件:

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:可读可写

flags 参数也可以是一个或者更多位掩码的或。mode 参数指定了新文件的访问权限位。

最后,进程通过 close 函数来关闭一个打开的文件。

读和写文件

应用程序通过调用 read 和 write 函数来执行输入和输出。

#include <unistd.h>

ssize_t read(int fd, void *buf, ssize_t n);

ssize_t write(int fd, const void *buf, ssize_t n);

用 RIO 包健壮地读写

RIO(Robust I/O)包是一个 I/O 包,会自动处理上下文中所述的不足值。RIO 提供了两类不同的函数:

  • 无缓冲的输入输出函数。这些函数直接在存储器和文件之间传送数据,没有应用级缓冲。对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
  • 带缓冲的输入函数。 这些函数允许你高校地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内。

共享文件

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

  1. 描述符表(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。 每个打开的描述符表项只想文件表中的一个表项。

  2. 文件表(file table)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。 每个文件表的表项组成包括:1)当前的文件位置;2)引用计数(reference count),即,当前指向该表项的描述符表项数;3)一个指向 v-node 表中对应表项的指针。 关闭一个描述符会减少相应的文件表表项的引用计数。当文件表表项的引用计数为0时,内核会删除这个文件表表项。

  3. v-node 表。所有的进程共享 v-node 表,每个表项包含 stat 结构中的大多数信息,包括 st_mode 和 st_size成员。

I/O 重定向

dup2 函数拷贝描述符表表项的 oldfd 到描述符表表项 newfd,覆盖描述符表表项 newfd 以前的内容。

#include <unistd.h>

int dup2(int oldfd, int newfd);

笔记来源

深入理解计算机系统(原书第2版)