权当是把书搬到 md 上面
I/O 是在 主存 和 外部设备 之间复制数据的过程
输入:
输出
ANSI C 提供标准 IO 库
C++
<<
输入>>
输出标准 IO 库没有提供读取 元文件 数据的方式,例如文件大小或文件创建时间
一个 Linux 文件 是一个 m 个字节的序列:
\[B_0,B_1,...,B_k,...,B_{m-1} \]所有 IO 设备(网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当成对相应文件的 读 和 写 来执行。
Linux 将设备映射为文件 的这种方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O:
打开文件:
Linux shell 创建的每个进程开始时都有三个打开的文件:
<unistd.h>
定义了常量 STDIN_FILENO
、STDOUT_FILENO
和 STDERR_FILENO
,他们可以用来代替显式的描述符值改变当前的文件位置
seek
操作,显式地设置文件的当前位置为 k读写文件
关闭文件
Linux 文件都有一个 类型 type 表名它在系统中的角色:
普通文件(regular file)
包含任意数据
应用程序需要区分
对内核而言,文本文件和 二进制文件没有区别
Linux 文本文件包含了一个 文本行(text line) 序列,其中每一行都是一个字符序列,以一个 新行符("\n") 结束
0x0a
目录(directory)
mkdir
make directory 创建一个目录ls
list 查看内容rmdir
remove directory 删除目录套接字 socket
命名通道 named pipe
符号链接 symbolic link
字符和块设备 character and block device
Linux 内核将所有文件都同一组织成一个 目录层次结构(directory hierarchy),
由名为 /
(斜杠) 的根目录确定
系统中的每个文件都是根目录的直接或间接后代
下图是 Linux 目录层次的一部分,尾部有斜杠表示是目录
![image-20211023150852386](D:\Documents\Study Data\Notes\深入理解计算机系统\Chapter 10 系统级 IO.assets\image-20211023150852386.png)
每个进程都有一个 当前工作目录(current working directory) 来确定其在 目录层次结构中的当前位置
/home/droh/hello.c
/home/droh
是当前工作目录,则 hello.c 的相对路径名就是 ./hello.c
/home/bryant
是当前工作目录,则 hello.c 的相对路径名就是 ../home/droh/hello.c
进程通过调用 open 函数 打开一个已存在的文件或者创建一个新文件:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(char *filename, int flags, mode_t mode); // 返回 :若成功则为新文件描述符,若出错则为 -1
open 函数将 filename 转换为一个文件描述符,且返回描述符数字。
返回的描述符数字总是在进程中当前没有打开的最小描述符。
O_RDONLY
只读O_WRONLY
只写O_RDWR
可读可写下面代码说明: 如何以读的方式打开一个已存在的文件
fd = Open("foo.txt", O_RDONLY, 0)
flags 参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:
O_CREAT
:如果文件不存在,就创建它的一个 截断的(truncated)(空) 文件O_TRUNC
: 如果文件已经存在,就截断它(删除它???)O_APPEND
: 在每次写操作前,设置文件位置到文件的结尾处(添加?)下面代码说明: 打开一个已存在文件,并在后面添加一些数据:
fd = Open("foo.txt", O_WRONLY|O_APPEND, 0)
| 表示管道:前面的结果作为后面的输入
指定了新文件的 访问权限位
这些位 的符号名字如表
访问权限位。在 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 吗
应用程序调用 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.
示例代码
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 函数,应用程序能够显式地修改当前文件的位置(书中没讲)
在某些情况下, read 和write 传送的字节比应用程序要求的要少。这些不足值( short count )不表示有错误
#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
所以要一个有符号的大小
有时候 read 和 write 传送的字节数比 app 要求的少,这些 不足值 short count 不表示有错误
也就是说,不足值是已经读到的文本
原因:
Robust I/O ,健壮的 I/O 包
自动处理上文的 不足值
RIO 提供两种函数
printf
这样的标准 I/O 函数提供的缓冲区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; }
如何计算文本中有多少行?
法1
法2
调用一个包装函数 rio_readlineb
从一个 内部缓冲区 复制一个文本行,当缓冲区变空,会自动地调用 read 重新填满缓冲区
对于 文件 既包含文本行也包含 二进制数据
提供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); }