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
2
3
4
5
6
7
8
9
//返回值是一个小的整型称为文件描述符(file descriptor),如果这个值等于 -1 则说明发生了错误
int open(char *filename, int flags, mode_t mode)

int fd; // 文件描述符 file descriptor
if ((fd = open("/etc/hosts", O_RDONLY)) < 0)
{
perror("open");
exit(1);
}

flags参数指明了进程打算如何访问这个文件,mode参数制定了新文件的访问权限

改变文件当前位置

对每个打开的文件,内核都保持一个文件位置k,初始为0,是从文件开头的字节偏移量,可通过seek显示的设置当前位置

读写文件

就是字节在文件到存储器间的相互转移呗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t read(int fd, void *buf, size_t n); //成功返回读的字节数,若EOF则为0,出错为-1
ssize_t write(int fd, const void *buf, size_t n); //若成功返回写的字节数,出错为-1

char buf[512];
int fd;
int nbytes;

// 打开文件描述符,并从中读取 512 字节的数据
if ((nbytes = read(fd, buf, sizeof(buf))) < 0)
{
perror("read");
exit(1);
}

// 打开文件描述符,并向其写入 512 字节的数据
if ((nbytes = write(fd, buf, sizeof(buf)) < 0)
{
perror("write");
exit(1);
}

ssize_t 被定义为 int,size_t 被定义为 unsigned int

其中读取文件的元数据(元数据是用来描述数据的数据,包含访问模式、大小和创建时间等,由内核维护)可以通过 statfstat 函数

1
2
int stat(conse char *filename, struct stat * buf);
int fstat(int fd, struct stat *buf); //若成功返回0,出错返回-1

关闭文件

内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。进程终止时就关闭该进程打开的所有文件

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
int close(int fd) //成功则返回0,出错返回-1

int fd; // 文件描述符
int retval; // 返回值
int ((retval = close(fd)) < 0)
{
perror("close");
exit(1);
}

共享文件和重定向

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

  • 描述符表:每个独立的进程1张,表项由进程打开的文件描述符来索引,每个打开的表项指向一打开的文件表;
  • 文件表:包括打开文件位置,引用数量,以及一个指向元数据的v-node指针
  • v-node表:包含stat结构的大部分信息;

mVI96s.png

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

mVIn1J.png

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

mVIy4S.png

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

I/O函数

mVIb34.png

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重定向。

参考

https://wdxtub.com/csapp/thin-csapp-6/2016/04/16/

https://www.jianshu.com/p/6ec9e9af0da6