系统级I/O
文件
所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O
Linux 文件有主要有三种类型:
- 普通文件
- 目录
- 网络socket(套接字)
当然还有命名通道(named pipe)、 符号链接(symbolic link),以及字符和块设备(character and block device)等类型先不予讨论。
改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置 k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作,显式地设置文件的当前位置为 k。
对于这种行为,对于普通文件是有效的,对于如socket、目录等类型文件无效。
当我们调用open函数两次打开同一个普通文件foo.txt,那么这两次打开均会从文件起始位置开始,然后记录file_offset, read和write的读写操作均会改变file_offset。
且两次open返回的是不同的文件描述符。有点像CSAPP书上下图:
关于文件描述符,文件表,v-node表在fork
后,重定向
后如何均有提及。
RIO
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
// 返回:若成功则为读的字节数,若 EOF 则为0,若出错为 -1。
ssize_t write(int fd, const void *buf, size_t n);
// 返回:若成功则为写的字节数,若出错则为 -1。
在 x86-64 系统中,size_t 被定义为 unsigned long,而 ssize_t(有符号的大小)被定义为 long。
read 函数返回一个有符号的大小,而不是一个无符号大小,这是因为出错时它必须返回 -1。
RIO(Robust I/O,健壮的 I/O)包,其实现的思路和目的为:
-
处理不足值:在某些情况下,read 和 write 传送的字节比应用程序要求的要少。这些不足值(short count)不表示有错误。出现这样情况的原因有:
- 读时遇到 EOF。这个时候说明确实没有内容可以读了,直接返回。
- 从终端读文本行。这个时候每个 read 函数将一次传送一个文本行。
- 读和写网络套接字(socket)。那么内部缓冲约束和较长的网络延迟会引起 read 和 write 返回不足值。这个时候必须通过反复调用 read 和 write 处理不足值,直到所有需要的字节都传送完毕。
-
实现无缓冲的输入输出函数:
rio_readn
和rio_writen
。这些函数直接在内存和文件之间传送数据,没有应用级缓冲。它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
// 返回:若成功则为传送的字节数,若 EOF 则为 0(只对 rio_readn 而言),若出错则为 -1。
- 实现带缓冲的输入函数。这些函数允许你高效地从文件中读取文本行和二进制数据。
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; /* Descriptor for this internal buf */
int rio_cnt; /* Unread bytes in internal buf */
char *rio_bufptr; /* Next unread byte in internal buf */
char rio_buf[RIO_BUFSIZE]; /* Internal buffer */
} rio_t;
//初始化rp指针
void rio_readinitb(rio_t *rp, int fd);
// 返回:无。
//对于文本数据,rio_readlineb,它从一个内部读缓冲区复制一个文本行,当缓冲区变空时,会自动地调用 read 重新填满缓冲区。
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
//对于既包含文本行也包含二进制数据的文件,提供了一个 rio_readn 带缓冲区的版本,叫做 rio_readnb,它从和 rio_readlineb 一样的读缓冲区中传送原始字节。
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
// 返回:若成功则为读的字节数,若 EOF 则为 0,若出错则为 -1。
手动实现一下
标准I/O
C 语言定义了一组高级输入输出函数,称为标准 I/O 库,为程序员提供了 Unix I/O 的较高级别的替代。
标准 I/O 库将一个打开的文件模型化为一个流。对于程序员而言,一个流就是一个指向 FILE 类型的结构的指针。
类型为 FILE 的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和 RIO 读缓冲区的一样:就是使开销较高的 Linux I/O 系统调用的数量尽可能得小。
#include <stdio.h>
extern FILE *stdin; /* Standard input (descriptor 0) */
extern FILE *stdout; /* Standard output (descriptor 1) */
extern FILE *stderr; /* Standard error (descriptor 2) */
-
G1:只要有可能就使用标准 I/O。
-
G2:不要使用 scanf 或 rio_readlineb 来读二进制文件。
因为scanf和rio_readlineb中均有通过换行符或终止符来判断是否需要'截断'的操作,二进制文件可能散布着很多 Oxa 字节,而这些字节又与终止文本行无关。 -
G3:对网络套接字的 I/O 使用 RIO 函数,而不要使用标准I/O。
具体理由和标准I/O内部实现有关,比如标准I/O需要使用 Unix I/O lseek 函数来重置当前的文件位置,但是socket没有文件位置这个概念。