csapp Chapter10 UnixIO

基本概念

首先要有一个思维,Linux 上面基本上所有外部设备都是文件,都是通过 IO 来读取,写入的,包括键盘,网络,终端…..

P.S.:为什么终端是呢,想象一下以前大型计算机,只有一台主机,不同人使用,就是有多个终端(物理设备)来操作系统,只不过现在我们用的微机就是一台主机了。

上一张图

看这张图片,可以看到:

那怎么使用文件呢

一句话:系统打开文件时会生成两个结构一个是在系统表上,一个是在 v-node 表上。前者存储的是某个进程对文件读取的偏移量,后者储存的是文件的固有属性,文件大小,文件权限。

对于某个进程来说:一个进程会打开多个文件,怎么管理呢?有一张表,叫做描述符表,每个打开的文件都有一个 ID,也叫作描述符,你打开文件后,描述符就代表你的文件了。看下面的图

P.S.:每个进程会默认打开三个文件,有三个描述符 0= 标准输入 (Stdin),1= 标准输出 (Stdout),2= 标准错误 (Stderr)

整数值 名称 unistd.h 符号常量 [[1]](https://zh.wikipedia.org/wiki/ 文件描述符#cite_note-1) stdio.h 文件流 [[2]](https://zh.wikipedia.org/wiki/ 文件描述符#cite_note-2)
0 Standard input STDIN_FILENO stdin
1 Standard output STDOUT_FILENO stdout
2 Standard error STDERR_FILENO stderr

Unix IO

打开文件用到了 open 函数返回的是文件描述符,注意两个参数

int open(char *filename, int flags, mode_t mode);
//flags 是进程如何访问这个文件,只读还是只写,或者不存在时新建
//mode 是创建文件时指定权限,就是正常的文件权限,读写执行,这里有一个 umask,
// 每个进程都有一个 umask,通过 umask 函数执行,真正的 mode 为 mode & ~umask
// 出错返回 -1 否则返回文件描述符
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);

读和写文件,Unix IO 读写文件都是无缓冲的,也就是说每次读文件,都要陷入内核态,按照指定的字节大小读取放到给定的字符指针内,如果遇到 EOF 就返回 0,否则返回读取字节大小,出错返回 -1。

RIO 与标准库 IO

这些函数有些事要是有缓冲区的,怎么实现的呢,就是新建一个缓冲结构,每次调用 Unix IO 时尽可能的读取足够多的字节(等于缓冲区大小),陷入内核态费时间,这样的话,如果下次再读,直接去缓冲区拿(空间换时间)。

标准库 io 也是格式化输入输出,并且也有缓冲区,但是也有很多问题,不适合在网络编程上使用,

这里就是一些函数,去看书,看具体实现。

要理解我们 read 文件的时候有才需要有缓冲,write 文件的时候缓冲意义就不大了。

  1. rio read 函数分为两批,参数不同,rio_t 作为参数要先 rio_init。R 大写只不过是封装函数。

    // 用 rio_t 作为参数的,有缓冲的。
    rio_readinitb(rio_t *rp, int fd);
    rio_read();
    rio_readnb();// 与下面的 readline 函数可以混合使用
    rio_readlineb();
    // 用 fd 作为参数的,无缓冲的。
    rio_readn();
    

    write 函数都只需要 fd。

  2. 写入一个文件描述符的时候

    其实 writen 和 write 都是差不多的,只不过 writen 会检查是否全部 write 进去了,因为在网络传输,等情况下的时候,可能不会安装设置 n 的大小写入 n 字节,所以 writen 就是加入了一个循环,如果每个字节都读进去,才会完成。

  3. 我们文件都会有一些元数据 (metadata),例如创建时间,文件类型 balabala 的,有两个函数可一度去这些数据。

    int stat(const char* filename, struct stat *buf);
    int fstat(int fd, struct stat *buf);
    

重定向

Linux shell 中的 >

$bowser> ls > foo.txt

dup2 函数,cgi 编程会利用此函数,将 std_out 重定向到 client_sockfd

int dup2(int oldfd, int newfd);
// 如果 newfd 已经打开,会先关闭它。
// 一句话:把 newtd 指向 oldfd 的文件表项
dup(5, 0);
// 标准输入重定向到描述符 5 指向的文件表
// 会继承 5 打开文件的文件偏移量。因为指向的是同一个文件表。

疑问

明确几点:

  1. 带缓冲函数,就是新建立一个结构,储存了 fd(文件描述符),还有缓冲的字节数组,等等东西,代替 fd 来使用。
  2. 文件偏移量,也就是移动位置是放在文件表结构上的,这里有一个问题,似乎不能多个进程同时打开一个文件。
  3. 文件元数据 (metadata),文件大小,文件类型放在 v-node 表上。
    • 文件类型,文件中有一个 type 结构指定,例如文本文件,二进制文件,sockets 文件。
  4. 标准库 IO 是设计为读取文本文件的,所以其他类型文件不合适。