ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-11-17 01:02:04  阅读:259  来源: 互联网

标签:文件 CSAPP 字节 读书笔记 read 第十章 rio 描述符 fd


权当是把书搬到 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);
}

标签:文件,CSAPP,字节,读书笔记,read,第十章,rio,描述符,fd
来源: https://www.cnblogs.com/icewalnut/p/15564606.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有