CSAPP系统 I/O
输入输出是在主存和外部设备(如硬盘,网络,终端)之间拷贝数据的过程,Unix将所有I/O设备都抽象为文件,输入输出被当作对相应文件的读和写来执行。
文件
什么是文件
Unix文件是一个m个字节的序列,系统将一切I/O设备(如硬件设备,终端,网络)都抽象为文件,再通过描述符(内核返回的非负整数)这个接口去操作,使所有设备的访问都是以文件的方式进行,优雅且统一。
文件的类型
普通文件
一般来说需要区分出文本文件和二进制文件。文本文件只包含 ASCII 或 Unicode 字符。除此之外的都是二进制文件(对象文件, JPEG 图片, 等等)。对于内核来说其实并不能区分出个中的区别。文本文件就是一系列的文本行,每行以
\n结尾,新的一行是0xa,和 ASCII 码中的 line feed 字符(LF) 一样。不同系统用用判断一行结束的符号不同目录
目录文件包含其他文件的信息,目录包含一个链接(link)数组,并且每个目录至少包含两条记录:
.(dot) 当前目录..(dot dot) 上一层目录
套接字
一种通过网络与其他进程通信的文件
对文件的操作
打开文件
应用程序通过内核打开相应文件,获得描述符。Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0),标准输出(1),标准错误(2)
1 | //返回值是一个小的整型称为文件描述符(file descriptor),如果这个值等于 -1 则说明发生了错误 |
flags参数指明了进程打算如何访问这个文件,mode参数制定了新文件的访问权限
改变文件当前位置
对每个打开的文件,内核都保持一个文件位置k,初始为0,是从文件开头的字节偏移量,可通过seek显示的设置当前位置
读写文件
就是字节在文件到存储器间的相互转移呗
1 | ssize_t read(int fd, void *buf, size_t n); //成功返回读的字节数,若EOF则为0,出错为-1 |
ssize_t 被定义为 int,size_t 被定义为 unsigned int
其中读取文件的元数据(元数据是用来描述数据的数据,包含访问模式、大小和创建时间等,由内核维护)可以通过 stat 和 fstat 函数
1 | int stat(conse char *filename, struct stat * buf); |
关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。进程终止时就关闭该进程打开的所有文件
1 |
|
共享文件和重定向
内核用三个相关的数据结构来表示打开的文件:
- 描述符表:每个独立的进程1张,表项由进程打开的文件描述符来索引,每个打开的表项指向一打开的文件表;
- 文件表:包括打开文件位置,引用数量,以及一个指向元数据的v-node指针
- v-node表:包含stat结构的大部分信息;

使用 fork时子进程实际上是会继承父进程打开的文件的。在 fork 之后,子进程实际上和父进程的指向是一样的,会把引用计数加 1

了解了这个,我们我们就可以知道所谓的重定向(用于将磁盘文件和标准输入输出联系起来,并不是链接中的重定位)是怎么实现的了。其实很简单,只要调用 dup2(oldfd, newfd) 函数即可。我们只要改变文件描述符指向的文件,也就完成了重定向的过程,下图中我们把原来指向终端的文件描述符指向了磁盘文件,也就把终端上的输出保存在了文件中:

将新的fd加到老的fd上面,删除掉newfd以前的内容,如果newfd已打开还会被关闭。
I/O函数

Unix IO
在最底层,适用于读取文件元信息,其中的方法都是异步信号安全(async-signal-safe)的,也就是说,可以在信号处理器中调用,更适合于网络应用
标准I/O
C 标准库中包含一系列高层的标准 IO 函数,适用于在磁盘和终端中输入输出,标准 C I/O 中的函数都不是异步信号安全(async-signal-safe)的,所以并不能在信号处理器中使用。标准 C I/O 不适合用于处理网络套接字
RIO(Robust健壮的)
是课程中提供的包,是对标准的封装,带有缓冲的read 和 write。它采用的解决办法就是利用 read 函数一次读取一块数据,然后再由高层的接口,一次从缓冲区读取一个字符(当缓冲区用完的时候需要重新填充)
小结
Unix提供少量的系统级函数来为应用程序提供读,写,打开,关闭文件的功能,但它的读写操作会出现不足值,使用一般我们使用封装过的包来应对这种情况。
Unix内核使用三个相关的数据结构来表示打开的文件,描述符表,打开文件表,v-node表,他们间的关系可以使我们搞清文件的共享和I/O重定向。