系统级I/O - CS:APP 第十章
Linux 所有的IO设备都被模型化为文件,所有的输入输出都被抽象成文件的读写。这种将设备映射成文件的方式,允许内核提供一些低级的函数接口还读写,被称为Unix IO。
在Linux中,较高级的IO函数(C标准库IO)是由内核提供的系统级Unix IO来实现的。
文件描述符
定义:一个应用程序要通过内核打开文件,内核返回一个非负小整数,叫做文件描述符,应用程序要操作文件,只需要知道文件描述符即可。
Linux Shell 创建的进程会打开三个文件:
0:标准输入;也可以使用定义在unistd.h
中的STDIN_FILENO
来显示表述
1:标准输出;STDOUT_FILENO
2:标准错误;STDERR_FILENO
EOF:如果一个字节数为m的文件,一个读操作,读到k字节处。如果k >= m,则触发一个End of file条件,应用程序可以检测这个条件,但文件末尾并不是真的有EOF符号。
文件类型
- 普通文件 regular file : 包含任意数据,系统并不会区分二进制文件或者文本文件,那时应用程序需要区分的。
- 目录 directory :是一个包含一组连接的文件,每个链接都映射到一个文件。有两个特殊的链接,每个目录文件一定会有,他们是
.
和..
分别代表自己和上层目录。
- 套接字 socket :用来和其他进程进行跨网络通信的文件。
系统级IO函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h>
int open(char * filename, int flags, mode_t mode);
int close(int fd);
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
|
ssize_t
:signed size type
read 函数会返回不足值(short count) ,即返回的数目并不是要求的size_t n
可能会返回不足值得情况:
- 读时遇到EOF。假设我们准备读一个文件,该文件从当前文件位置开始只含有20多个字节,而我们以50个字节的组块(chunk)进行读取。这样一来,下一个read返回的不足值为20,此后的read将通过返回O发出EOF信号。
- 从终端读文本行。如果打开文件是与终端相关联的(例如,键盘和显示器),那么每个rad函数将一次传送一个文本行,返回的不足值等于文本行的大小。
- 读和写网络套接字(socket)。如果打开的文件对应于网络套接字(12.3.3节),那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。对Unix管道(pipe)调用read和wte,也有可能出现不足值,这种进程间通信机制不在我们讨论的范围之内。
RIO包函数
RIO(Robust I/O):用于网络编程中,会自动处理不足值的情况。
总览:
1 2 3 4 5 6 7 8
| ssize_t rio_readn(int fd, void *usrbuf, size_t n); ssize_t rio_writen(int fd, void *usrbuf, size_t n);
void rio_readinitb(rio_t *rp, int fd); ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen); ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
|
带缓冲区和不带缓冲区的优劣:
带缓冲区:由于每次调用read都要进入内核模式进行系统调用,会比较浪费时间,因此带缓冲区可以在缓冲区为空的时候填满缓冲区,等下次调用的时候直接在缓冲区中取出数据,这样会节省减少不必要的系统调用。
不带缓冲区:比较快速,方便在网络上进行数据传输(臆断)
不带缓冲区的函数
rio_readn()
用来代替read
的RIO函数,无缓冲区,这个函数只有在遇到EOF时,才会返回不足值,其他情况下,绝不会返回不足值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ssize_t rio_readn(int fd, void *usrbuf, size_t n) { size_t nleft = n; ssize_t nread; char *bufp = usrbuf;
while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) nread = 0; else return -1; } else if (nread == 0) break; nleft -= nread; usrbuf += nread; }
return n - nleft; }
|
rio_writen()
用来代替write
的函数,无缓冲区,绝对不可能返回不足值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; bufp += nwritten; } return n; }
|
带缓冲区的函数
缓冲区的代码
1 2 3 4 5 6 7 8
| #define RIO_BUFSIZE 8192 typedef struct { int rio_fd; int rio_cnt; char *rio_bufptr; char rio_buf[RIO_BUFSIZE]; }rio_t;
|
初始化缓冲区
rio_read()
带缓冲区的rio_read() 遇到错误返回-1,遇到EOF返回0,否则返回成功读取的字节数
该函数会返回不足值,除了上述几种不足值,当缓冲区的字节数小于要求的字节数时,也会返回不足值,我们其他的带缓冲区的rio函数都是基于这个函数。
该函数会首先调用read填满缓冲区,如果缓冲区内还有字节则会直接读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) { int cnt;
while (rp->rio_cnt <= 0) { rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf)); if (rp->rio_cnt < 0) { if (errno != EINTR) return -1; } else if (rp->rio_cnt == 0) return 0; else rp->rio_bufptr = rp->rio_buf; }
cnt = n; if (rp->rio_cnt < n) cnt = rp->rio_cnt; memcpy(usrbuf, rp->rio_bufptr, cnt); rp->rio_bufptr += cnt; rp->rio_cnt -= cnt; return cnt; }
|
rio_readlineb()
读取一行,最多读取maxlen-1个字节,最后一个字节要填充'\0'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) { int n, rc; char c, *bufp = usrbuf;
for (n = 1; n < maxlen; n++) { if ((rc = rio_read(rp, &c, 1)) == 1) { *bufp++ = c; if (c == '\n') { n++; break; } } else if (rc == 0) { if (n == 1) return 0; else break; } else return -1; }
*bufp = 0; return n - 1; }
|
rio_readnb()
rio_readn的带缓冲区版本,代码结构与rio_readn 基本相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n) { size_t nleft = n; ssize_t nread; char *bufp = usrbuf;
while (nleft > 0) { if ((nread = rio_read(rp, bufp, nleft)) < 0) return -1; else if (nread == 0) break; nleft -= nread; usrbuf += nread; }
return n - nleft; }
|
读取文件元数据
1 2 3 4 5
| #include <unistd.h> #include <sys/stat.h>
int stat(const char *filename, struct stat *buf); int fstat(int fd, struct stat *buf);
|
stat()
函数以文件名作为输入,填写struct stat *buf
我们只对struct stat
的一些条目感兴趣:
读取目录
1 2 3 4 5 6 7 8 9 10 11 12
| #include <sys/types.h> #include <dirent.h>
DIR *opendir(const char *name);
#include <dirent.h> struct dirent * readdir(DIR *dirp);
struct dirent { ino_t d_ino; char d_name[256]; }
|
IO重定位
1 2 3 4 5 6
| #include <unistd.h>
int dup2(int oldfd, int newfd);
|