CSAPP-10系统级I/O

Posted by SH on June 14, 2020

CSAPP-10系统级I/O

Unix I/O

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

这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

  • 打开文件。要求内核打开相应的文件,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。
    • Linux shell 创建的每个进程开始都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
  • 改变当前的文件位置。每个打开的文件,内核保持着一个文件位置k,初始为0,表示从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
  • 读写文件。一个读操作就是从文件复制 n>0 个字节到内存,文件位置从k增加到k+n,如果超过文件大小,触发称为end-of-file(EOF)的条件。写操作就是从内存复制 n>0 个字节到一个文件。
  • 关闭文件。通知内核关闭这个文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

文件

类型:

  • 普通文件(regular file)包含任意数据。
    • 文本文件;
    • 二进制文件;
  • 目录(directory)是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。
  • 套接字(socker)是用来与另一个进程进行跨网络通信的文件。
  • 其他文件类型:
    • 命名通道(named pipe);
    • 符号链接(symbolic link);
    • 字符和块设备(character and block device);

打开和关闭文件

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// 返回:若成功则为新文件描述符,若出错为-1
// flags: 只读、只写、可读可写
// mode:指定了新文件的访问权限位
int open(char *filename, int flags, mode_t mode);

// 返回:若成功则为0,若出错则为-1
int close(int fd);

读和写文件

1
2
3
4
5
6
7
#include <unistd.h>

// 返回:若成功则为读的字节数,若EOF则为0,若出错为-1
ssize_t read(int fd, void *buf, size_t n);

// 返回:若成功则为写的字节数,若出错则为-1
ssize_t write(int fd, const void *buf, size_t n);

在某些情况下,read和write传送的字节比应用程序要求的要少。这些不足值(short count)不表示有错误。出现这样情况的原因有:

  • 读时遇到EOF。读超过文件大小,read将通过返回不足值0来发出EOF信号。
  • 从终端读文本行。从终端(键盘和显示器)读,每个read函数一次传送一个文本行,返回的不足值等于文本行的大小。
  • 读和xie网络套接字(socket)。内部缓冲约束和较长的网络延迟会引起read和write返回不足值。
  • 对Linux管道(pipe)调用read和write时,也有可能出现不足值。

RIO包

RIO(Robust I/O,健壮的I/O)包:自动处理不足值。

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

  • 无缓冲的输入输出函数。直接在内存和文件之间传送数据,没有应用及缓冲。对讲二进制数据读写到网络和从网络读写二进制数据尤其有用。
  • 带缓冲的输入函数。允许高效地从文件中读取文本行和二进制数据。

无缓冲的输入输出函数

1
2
3
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
// 返回:若成功则为传送的字节数,若EOF则为0,若出错则为-1

带缓冲的输入函数

1
2
3
4
void rio_readinitb(rio_t *rp, int fd);

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, siez_t n);

读取文件元数据

1
2
int stat(coinst char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

读取目录内容

  • opendir()
  • readdir()
  • closedir()

共享文件

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

  • 描述符表(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。
  • 文件表(file table)。打开文件的集合是由一张文件表来表示的。所有的进程共享这张表。每个文件表的表项组成包括当前的文件位置、引用计数、一个指向v-node表中对应表项的指针。
  • v-node表(v-node table)。所有的进程共享。每个表项包含stat结构中的大多数信息。

image-20210120113309696

父子进程如何共享文件:子进程有一个父进程描述符表的副本。

I/O重定向

1
2
3
#include <unistd.h>

int dup2(int oldfd, int newfd);

image-20210120113523982

标准I/O

libc:

  • fopen、fclose
  • fread、fwrite
  • fgets、fputs
  • scanf、printf

image-20210120113701274