CSAPP - 第十章 系统级 I/O 读书笔记

权当是把书搬到 md 上面

CSAPP Chapter 10 系统级 I/O

  • I/O 是在 主存 和 外部设备 之间复制数据的过程

  • 输入:

    • 从 I/O 设备 复制数据到主存
  • 输出

    • 从主存复制数据到 IO设备
  • ANSI C 提供标准 IO 库

    • printf 和 scanf 函数
  • C++

    • 重载操作符
    • << 输入
    • >> 输出

标准 IO 库没有提供读取 元文件 数据的方式,例如文件大小或文件创建时间

10.1 Unix I/O

一个 Linux 文件 是一个 m 个字节的序列:

\[B_0,B_1,...,B_k,...,B_{m-1} \]

所有 IO 设备(网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当成对相应文件的 来执行。

Linux 将设备映射为文件 的这种方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O:

  • 打开文件:

    • 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 IO 设备
    • 内核返回一个 小的 非负整数,叫做 描述符
      • 在后续对此文件的所有操作中标识这个文件
    • 内核记录有关这个打开文件的所有信息
    • 应用程序只需记住这描述符
  • Linux shell 创建的每个进程开始时都有三个打开的文件:

    • 标准输入
      • 描述符为 0
    • 标准输出
      • 描述符为 1
    • 标准错误
      • 描述符为 2
    • 头文件 <unistd.h> 定义了常量 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO,他们可以用来代替显式的描述符值
  • 改变当前的文件位置

    • 每个打开的文件,内核保持一个文件位置 k,初始为 0
    • 文件位置是 从文件起始的 字节偏移量
    • 应用程序能够通过执行 seek 操作,显式地设置文件的当前位置为 k
  • 读写文件

    • 读操作
      • 从文件复制 n > 0 个字节到内存,从当前文件位置 k 开始,将 k 增加到 k + n
      • 给定一个大小为 m 字节的文件,当 k >= m 时执行读操作会触发一个称为 end-of-file(EOF) 的条件,应用程序能够检测到这个条件。在文件结尾处并没有明确的 EOF 符号
      • (??? 此处不是太懂 )
    • 写操作
      • 从内存复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k
  • 关闭文件

    • 内核释放文件打开时创建的数据结构
    • 将这个描述符恢复到可用的描述符 中。
    • 无论一个进程因何种原因终止,内核都会关闭所有打开的文件并释放它们的内存资源

10.2 文件

10.2.1 文件类型

Linux 文件都有一个 类型 type 表名它在系统中的角色:

  • 普通文件(regular file)

    • 包含任意数据

    • 应用程序需要区分

      • 文本文件 (text file)
        • 只含有 ASCII 或 Unicode 字符的普通文件
      • 二进制文件(binary file)
        • 所有其它的文件
    • 对内核而言,文本文件和 二进制文件没有区别

    • Linux 文本文件包含了一个 文本行(text line) 序列,其中每一行都是一个字符序列,以一个 新行符("\n") 结束

      • 新行符和 ASCII 的换行符 (LF)是一样的,数字值为 0x0a
  • 目录(directory)

    • 包含一组 链接(link) 的文件
    • 链接将一个 文件名(filename) 映射到一个文件(可能是目录)
    • 每个目录至少含有两个条目
      • "."
        • 到该目录自身的链接
      • ".."
        • 到目录层次结构中父目录(parent directory)的链接
    • 命令
      • mkdir make directory 创建一个目录
      • ls list 查看内容
      • rmdir remove directory 删除目录
  • 套接字 socket

    • 用来与另一个进程进行跨网络通信的文件(见 Chapter 11.4)
  • 命名通道 named pipe

  • 符号链接 symbolic link

  • 字符和块设备 character and block device

10.2.2 目录层次结构

Linux 内核将所有文件都同一组织成一个 目录层次结构(directory hierarchy)

由名为 / (斜杠) 的根目录确定

系统中的每个文件都是根目录的直接或间接后代

下图是 Linux 目录层次的一部分,尾部有斜杠表示是目录

![image-20211023150852386](D:\Documents\Study Data\Notes\深入理解计算机系统\Chapter 10 系统级 IO.assets\image-20211023150852386.png)

  • /
    • bin/
      • bash
    • dev/
      • tty1 (tty1-tty6 是虚拟终端)
    • etc/
      • group
      • passwd/
    • home/
      • drop/
        • hello.c
      • bryant/
    • usr/
      • include/
        • stdio.h
        • sys/
          • unistd.h
      • bin/
        • vim

每个进程都有一个 当前工作目录(current working directory) 来确定其在 目录层次结构中的当前位置

  • 路径名
    • 目录层次结构中的位置用 路径名(pathname) 来指定
    • 绝对路径名(absolute pathname)
      • /home/droh/hello.c
    • 相对路径名
      • 以文件名开始,表示当前工作目录开始的路径
      • 如果 /home/droh 是当前工作目录,则 hello.c 的相对路径名就是 ./hello.c
      • 如果 /home/bryant 是当前工作目录,则 hello.c 的相对路径名就是 ../home/droh/hello.c

10.3 打开和关闭文件

进程通过调用 open 函数 打开一个已存在的文件或者创建一个新文件:

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

int open(char *filename, int flags, mode_t mode);
// 返回 :若成功则为新文件描述符,若出错则为 -1

open 函数将 filename 转换为一个文件描述符,且返回描述符数字。

返回的描述符数字总是在进程中当前没有打开的最小描述符。

flags 参数

  • 指明进程如何访问这个文件
  • O_RDONLY 只读
  • O_WRONLY 只写
  • O_RDWR 可读可写

下面代码说明: 如何以读的方式打开一个已存在的文件

fd = Open("foo.txt", O_RDONLY, 0)

  • FD File Description 文件描述符

flags 参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:

  • O_CREAT :如果文件不存在,就创建它的一个 截断的(truncated)(空) 文件
  • O_TRUNC : 如果文件已经存在,就截断它(删除它???)
  • O_APPEND : 在每次写操作前,设置文件位置到文件的结尾处(添加?)

下面代码说明: 打开一个已存在文件,并在后面添加一些数据:

fd = Open("foo.txt", O_WRONLY|O_APPEND, 0)

| 表示管道:前面的结果作为后面的输入

mode 参数

  • 指定了新文件的 访问权限位

    • 这些位 的符号名字如表

    • 访问权限位。在 sys/stat.h 中定义

      掩码 描述
      usr S_IRUSR 使用者(拥有者)能够读这个文件
      S_IWUSR 使用者(拥有者)能够写这个文件
      S_IXUSR 使用者(拥有者)能够执行这个文件
      grp S_IRGRP 拥有者所在组的成员能够读这个文件
      S_IWGRP 拥有者所在组的成员能够写这个文件
      S_IXGRP 拥有者所在组的成员能够执行这个文件
      oth S_IROTH 其他人(任何人)能够读这个文件
      S_IWOTH 其他人(任何人)能够写这个文件
      S_IXOTH 其他人(任何人)能够执行这个文件

作为上下文的一部分,每个进程都有一个 umask,通过调用 umask 函数来设置

当进程通过带某个 mode 参数的 open 函数来创建一个新文件时,文件的访问权限位被设置为 mode & ~ umask

假设给定下面的 mode 和 umask 默认值:

#define DEF_MODE S_IRUSR|S_IWUSR|S_IGRP|S_IWGRP|S_IROTH|S_IWOTH

#define DEF_UMASK S_IWGRP|S_IWOTH

接下来,下面的代码片段创建一个新文件,文件的拥有者有读写权限,而其他所有的用户都有读写权限

umask(DEF_UMASK);
fd = Open("foo.txt", O_CREAT|O_TRUNC|O_WRONLY, DEF_MODE);

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

#include <unistd.h>
int close(int fd);
// 返回:若成功则为 0, 若出错则为 -1
  • 关闭一个已关闭的描述符会出错

练习

下面程序的输出是什么

#include "csapp.h"

int main()
{
    int fd1, fd2;
    
    fd1 = Open("foo.txt", O_RDONLY, 0);
    Close(fd1);
    fd2 = Open("baz.txt", O_RDONLY, 0);
    printf("fd2 = %d\n", fd2);
    exit(0);
}

输出什么? 2 吗

10.4 读和写文件

应用程序调用 readwrite 函数 输入输出

#include <unistd.h>
// fd 文件描述符        *buf 内存位置		n 大小
ssize_t read(int fd, void *buf, size_t n);
// 返回: 若成功则为读的字节数,若 EOF(end of file) 则为0,若出错则为 -1.

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

10.4.1 read 函数

  • 从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置 buf。
  • 返回值
    • -1 表示一个错误
    • 0 表示 EOF end of file
    • 否则表示时机传送的字节数量

10.4.2 write 函数

  • 从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置

示例代码

code/io/cpstdin.c

#include "csapp.h"

int main(void) 
{
    char c;
    
    while(Read(STDIN_FILENO, &c, 1) != 0)
        Write(STDOUT_FILENO, &c, 1);
    exit(0);
}

上述代码使用 read 和 write 调用一次一个字节地从标准输入复制到标准输出。

调用 lseek 函数,应用程序能够显式地修改当前文件的位置(书中没讲)

  • ssize_t 和 size_t 的区别
    • read() 函数 size_t 为参数,返回值为 ssize_t
    • x86-64 系统中,size_t 定义为 unsigned long,而 ssize_t (有符号的大小) 被定义为 long
    • read 函数返回一个有符号的大小,保证出错时能返回 -1
    • 返回 -1 的可能性使得 read 的最大值减小了一半

10.4.3 不足值 short count

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

10.4 读和写文件

#include <unistd.h>
// fd file description,
// 从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置 buf
// 返回 -1 表示错误
// 返回 0 表示 EOF    end of file
// 否则返回实际传送的字节量
ssize_t read(int fd, void *buf, size_t n);

// 从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置
// 成功则返回写的字节数
// 失败则返回 -1
ssize_t write(int fd, const void *buf, size_t n);

程序 code/io/cpstdin.c

#include "csapp.h"

int main(void) {
    char c;
    // 如果 从 文件描述符为 STDIN_FILENO 的位置复制 1 个 字节到内存位置 &c 成功
    while (Read(STDIN_FILENO, &c, 1) != 0)
        // 从内存位置 &c 复制 1 个字节到 描述符 为 fd 的当前文件位置
        Write(STDOUT_FILENO, &c, 1);
    exit(0);
}

上述程序表示:一次一个字节地从标准输入复制到标准输出

  • size_t 被定义为 unsigned long,无符号长整型

  • ssize_t 被定义为 long。

    • read 函数出错时必须返回 -1

      所以要一个有符号的大小

short count 不足值

有时候 read 和 write 传送的字节数比 app 要求的少,这些 不足值 short count 不表示有错误

也就是说,不足值是已经读到的文本

原因:

  • 读时遇到 EOF
    • 准备读一个 从当前文件位置开始 只含有 20 个字节 的文件
    • 以 50 个字节的片段进行读取
  • 从终端读取文本行
    • 每个 read 函数一次传送一个文本行,返回的不足值等于文本行的大小
  • 读和写网络套接字 socket

10.5 用 RIO 包健壮地读写

10.5.1 RIO

Robust I/O ,健壮的 I/O 包

自动处理上文的 不足值

RIO 提供两种函数

  • 无缓冲的输入输出函数
    • 直接在内存和文件中传送数据,没有应用级缓冲
    • 对 将 二进制数据读写到网络 和 从网络读写二进制数据 尤其有用
  • 带缓冲的输入函数
    • 高效地从文件中读取 文本行二进制数据
    • 这些文件的内容缓存在 应用级缓冲区
    • 类似于为 printf 这样的标准 I/O 函数提供的缓冲区
    • 线程安全(见 Chapter 12.7.1 节)
    • 在同一个描述符上可以被交错地调用
    • 可以从一个描述符中读一些文本行,然后读取一些二进制数据,接着再多读取一些文本行

10.5.2 RIO 的无缓冲的输入输出函数

rio_readnrio_writen

  • 代码

    #include "csapp.h"
    
    // 从描述符 fd 的当前文件位置最多传送 n 个字节到内存位置 usrbuf
    ssize_t rio_readn(int fd, void *usrbuf, size_t n);
    
    // 从位置 usrbuf 传送 n 个字节到描述符 fd
    ssize_t rio_writen(int fd, void *usrbuf, size_t n);
    // 返回:若成功则为传送的字节数,若 EOF 则为0 (只对rio_readn 而言),若出错则为一1
    

    rio_read 还是 rio_readn 函数 遇到 EOF 时只能返回一个不足值 ???

    rio_writnen 函数不会返回不足值

    对同一个描述符,可以任意交错调用 rio_readnrio_writen

    代码

    如果 rio_readnrio_writen 函数被一个从应用信号处理程序的返回中断,那么每个函数都会手动重启 read 或 write。

    为了尽可能有较好的可移植性,允许被中断的系统调用,且在必要时重启他们

    rio_readn 代码

    // fd 文件描述符位置    usrbuf 内存中文件位置     n 文件大小
    ssize_t rio_readn(int fd, void *usrbuf, size_t n) 
    {
        // left 剩余的
    	size_t nleft = n;
        // nread 是读到的字节数
        ssize_t nread;
        // *bufp 内存中位置指针
        char *bufp = usrbuf;
        
        while (nleft > 0) {
            // 如果 read(fd, bufp, nleft) 的返回值为 -1 表示出错
            if ((nread = read(fd, bufp, nleft)) < 0) { 
                // sig 是 signal
                if (errno == ENTER)			/* Interrupted by sig handler return */
                    nread = 0;				/* and call read() again */
                else 
                    return -1;				/* errno set by read */
            }
            // 看 10.4 节 read() 返回 0 表示 EOF end of file
            else if (nread == 0)
                break;						/* EOF */
            
            // nleft = nleft - nread;
            nleft -= nread;
            bufp += nread;
        }
        return (n - nleft);					/* Return >= 0 */
    }
    

    rio_writen 代码

    ssize_t rio_writen(int fd, void *usrbuf, size_t n) 
    {
        // 剩余还没写入的字节数
        size_t nleft = n;
        // 
        ssize_t nwritten;
        // 内存中 位置指针
        char *bufp = usrbuf;
        
        while (nleft > 0) {
            // 写入过程中出错
            if ((nwritten = write(fd, bufp, nleft)) <= 0) {
                if (errno == ENTER)		/* Interrupted by sig handler return */
                    nwritten = 0;		/* and call write() again */
                else 
                    return -1;			/* errno set by write() */
            }
            
            // nleft = nleft - nwritten;
            nleft -= nwritten;
            bufp += nwritten;
        }
        return n;
    }
    

10.5.2 RIO 的带缓冲的输入函数

如何计算文本中有多少行?

  • 法1

    • 用 read 函数一次一个字节从文件传送到用户内存,查找换行符
    • 效率低,每读一个字节都要求陷入内核
  • 法2

    • 调用一个包装函数 rio_readlineb

    • 从一个 内部缓冲区 复制一个文本行,当缓冲区变空,会自动地调用 read 重新填满缓冲区

    • 对于 文件 既包含文本行也包含 二进制数据

      • 如 11.5.3 节中描述的 HTTP 相应

      提供rio_readn 带缓冲区的版本: rio_readnb

  • 代码

#include "csapp.h"

// 返回:无
// 将 描述符 fd 和 地址 rp 处的一个类型为 rio_t 的 读缓冲区 联系起来
void rio_readinitb(rio_t *rp, int fd);

// 成功返回读的字节数;EOF 返回 0,出错则返回 -1
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);

rio_t 是一个 读缓冲区

每打开一个描述符,都会调用一次 rio_readinitb 函数 都会 将 描述符 fd 和 地址 rp 处的一个类型为 rio_t 的 读缓冲区 联系起来

rio_readlineb 函数从文件 rp 读出下一个文本行(包括结尾的换行符),将它复制到内存位置 usrbuf,并且用 NULL 字符来结束这个文本行。

rio_readlineb 函数最多读取 maxlen - 1 个字节,余下的 一个字符留给结尾的 NULL 字符。超过 maxlen - 1 字节的文本行被截断,并用一个 NULL 字符结束

rio_readnb 函数从文件 rp 最多读 n 个字节到内存位置 usrbuf

对同一描述符,对 rio_readlinebrio_readnb 的调用可以任意交叉进行,然而对于这些带缓冲的函数的调用不能和 无缓冲rio_readn 函数交叉使用

  • 代码
/**
 * 从标准输入复制一个文本到标准输出
 */
#include "csapp.h"

int main(int argc, char **argv)
{
    int n;
    rio_t rio;
    char buf[MAXLINE];
    
    Rio_readinitb(&rio, STDIN_FILENO);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
        Rio_writen(STDOUT_FILENO, buf, n);
}
上一篇:Couldn‘t find executable named person_subscriber below /home/yue/catkin_ws/src/... 找不到可执行文件的原因


下一篇:看完CSAPP,我人麻了