# 10.5.2 RIO 的带缓冲的输入函数
假设我们要编写一个程序来计算文本文件中文本行的数量,该如何来实现呢?一种方法就是用 read 函数来一次一个字节地从文件传送到用户内存,检查每个字节来查找换行符。这个方法的缺点是效率不是很高,每读取文件中的一个字节都要求陷入内核。
一种更好的方法是调用一个包装函数(rio_readlineb),它从一个内部读缓冲区复制一个文本行,当缓冲区变空时,会自动地调用 read 重新填满缓冲区。对于既包含文本行也包含二进制数据的文件(例如 11.5.3 节中描述的 HTTP 响应),我们也提供了一个 rio_readn 带缓冲区的版本,叫做 rio_readnb,它从和 rio_readlineb 一样的读缓冲区中传送原始字节。
#include "csapp.h"
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);
返回: 若成功则为读的字节数,若 EOF 则为 0,若出错则为 -1。
每打开一个描述符,都会调用一次 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 函数交叉使用。
在本书剩下的部分中将给出大量的 RIO 函数的示例。图 10-5 展示了如何使用 RIO 函数来一次一行地从标准输入复制一个文本文件到标准输出。
code/io/cpfile.c
#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);
}
图 10-5 从标准输入复制一个文本文件到标准输出
图 10-6 展示了一个读缓冲区的格式,以及初始化它的 rio_readinitb 函数的代码。rio_readinitb 函数创建了一个空的读缓冲区,并且将一个打开的文件描述符和这个缓冲区联系起来。
code/include/csapp.h
#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;
code/src/csapp.c
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
图 10-6 一个类型为 rio_t 的读缓冲区和初始化它的 rio_readinitb 函数
RIO 读程序的核心是图 10-7 所示的 rio_read 函数。rio_read 函数是 Linux read 函数的带缓冲的版本。当调用 rio_read 要求读 n 个字节时,读缓冲区内有 rp->rio_cnt 个未读字节。如果缓冲区为空,那么会通过调用 read 再填满它。这个 read 调用收到一个不足值并不是错误,只不过读缓冲区是填充了一部分。一旦缓冲区非空,rio_read 就从读缓冲区复制 n 和 rp->rio_cnt 中较小值个字节到用户缓冲区,并返回复制的字节数。
code/src/csapp.c
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
int cnt;
while (rp->rio_cnt <= 0) { /* Refill if buf is empty */
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf,
sizeof(rp->rio_buf));
if (rp->rio_cnt < 0) {
if (errno != EINTR) /* Interrupted by sig handler return */
return -1;
}
else if (rp->rio_cnt == 0) /* EOF */
return 0;
else
rp->rio_bufptr = rp->rio_buf; /* Reset buffer ptr */
}
/* Copy min(n, rp->rio_cnt) bytes from internal buf to user 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;
}
图 10-7 内部的 rio_read 函数
对于一个应用程序,rio_read 函数和 Linux read 函数有同样的语义。在出错时,它返回值 -1,并且适当地设置 errno。在 EOF 时,它返回值 0。如果要求的字节数超过了读缓冲区内未读的字节的数量,它会返回一个不足值。两个函数的相似性使得很容易通过用 rio_read 代替 read 来创建不同类型的带缓冲的读函数。例如,用 rio_read 代替 read,图 10-8 中的 rio_readnb 函数和 rio_readn 有相同的结构。相似地,图 10-8 中的 rio_readlineb 程序最多调用 maxlen-1 次 rio_read。每次调用都从读缓冲区返回一个字节,然后检查这个字节是否是结尾的换行符。
旁注 RIO 包的起源
RIO 函数的灵感来自于 W. Richard Stevens 在他的经典网络编程作品 [110] 中描述的
readline、readn和writen函数。rio_readn和rio_writen函数与 Stevens 的readn和writen函数是一样的。然而,Stevens 的readline函数有一些局限性在 RIO 中得到了纠正。第一,因为readline是带缓冲的,而readn不带,所以这两个函数不能在同一描述符上一起使用。第二,因为它使用一个static缓冲区,Stevens 的readline函数不是线程安全的,这就要求 Stevens 引入一个不同的线程安全的版本,称为readline_r。我们已经在rio_readlineb和rio_readnb函数中修改了这两个缺陷,使得这两个函数是相互兼容和线程安全的。
code/src/csapp.c
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; /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return -1; /* Error */
}
*bufp = 0;
return n-1;
}
code/src/csapp.c
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; /* errno set by read() */
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* Return >= 0 */
}
图 10-8 rio_readlineb 和 rio_readnb 函数