权当是把书搬到 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_FILENO
、STDOUT_FILENO
和STDERR_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)
- 所有其它的文件
-
文本文件 (text file)
-
对内核而言,文本文件和 二进制文件没有区别
-
Linux 文本文件包含了一个 文本行(text line) 序列,其中每一行都是一个字符序列,以一个 新行符("\n") 结束
- 新行符和 ASCII 的换行符 (LF)是一样的,数字值为
0x0a
- 新行符和 ASCII 的换行符 (LF)是一样的,数字值为
-
-
目录(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/
- drop/
- usr/
- include/
- stdio.h
- sys/
- unistd.h
- bin/
- vim
- include/
- bin/
每个进程都有一个 当前工作目录(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 读和写文件
应用程序调用 read
和 write
函数 输入输出
#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_readn
和 rio_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_readn
和rio_writen
代码
如果
rio_readn
和rio_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_readlineb
和 rio_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);
}