10.5.2 RIO 的带缓冲的输入函数

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_readlinebrio_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 就从读缓冲区复制 nrp->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-1rio_read。每次调用都从读缓冲区返回一个字节,然后检查这个字节是否是结尾的换行符。

旁注 RIO 包的起源

RIO 函数的灵感来自于 W. Richard Stevens 在他的经典网络编程作品 [110] 中描述的 readlinereadnwriten 函数。rio_readnrio_writen 函数与 Stevens 的 readnwriten 函数是一样的。然而,Stevens 的 readline 函数有一些局限性在 RIO 中得到了纠正。第一,因为 readline 是带缓冲的,而 readn 不带,所以这两个函数不能在同一描述符上一起使用。第二,因为它使用一个 static 缓冲区,Stevens 的 readline 函数不是线程安全的,这就要求 Stevens 引入一个不同的线程安全的版本,称为 readline_r。我们已经在 rio_readlinebrio_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_readlinebrio_readnb 函数