用RIO包健壮地读写

用RIO包健壮地读写

RIO 包全名为 Robust IO 函数包。包中函数是对 Linux 基本 I/O 函数的封装,使其更加健壮、高效,更适用于网络编程。具体来说,它会自动处理读写中的不足值情况。这种情况在网络应用中经常出现,因此编写网络程序时,我们经常用到它。

CSAPP 中实现的 RIO 提供了两类不同的函数:

  • 无缓冲的 I/O 函数。无应用级的缓冲,对二进制数据读写到网络和从网络读写到二进制数据尤为有用。
  • 有缓冲的 I/O 函数。从文本读取文本行和二进制,并会被缓存在应用级缓冲区中,该缓冲区是线性安全的。即同一个描述符上可以被交错地调用

此外,RIO 包还有一个重要的数据结构:rio_t

无缓冲区的 I/O 函数

rio_readnrio_writen,通常用于内存和文件之间直接传输数据。

ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);

rio_readn() 函数尝试从 fd 中读取 n 个字符到 usrbuf 中,与 read() 函数相比,它被信号处理函数中断后会再次尝试读取。因此,除了可读字符数小于 n 情况下,该函数可以保证读取 n 个字节。而若提取遇到 EOF ,就返回一个不足值。而 rio_writen() 绝不会返回不足值,对同一个描述符,交错使用 rio_readnrio_writen 是可以的。

下面是他们的具体实现。注意到,当中断产生后,rio_readn() 函数会自动开始下一轮循环,因为 errno == EINTRnread = 0,随后就直接开始下一轮循环,变量值都未受影响。它没有缓冲区,直接写入了目标地址,返回值是实际读取的字节数,若出错返回-1。

// rio_readn - Robustly read n bytes (unbuffered)
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) /* Interrupted by sig handler return */
        nread = 0;        /* and call read() again */
      else
        return -1; /* errno set by read() */
    } else if (nread == 0)
      break; /* EOF */
    nleft -= nread;
    bufp += nread;
  }
  return (n - nleft); /* Return >= 0 */
}

接下来看 rio_writen() 函数,若在写入时被中断,则 nwritten = 0,也会自动下一轮循环。除非出错返回 -1,否则 n 个字节一定会被全部写入,故一直返回 n

// rio_writen - Robustly write n bytes (unbuffered)
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) /* Interrupted by sig handler return */
        nwritten = 0;     /* and call write() again */
      else
        return -1; /* errno set by write() */
    }
    nleft -= nwritten;
    bufp += nwritten;
  }
  return n;
}

注意一下出错信息 EINTR :通常,当一个进程在某个慢系统调用(可能是 readwrite 函数)中阻塞时,在捕获到某个信号,且该信号处理函数返回时,那么该慢系统调用会被中断,并返回错误 EINTR。在 GNU C 的文档中有非常棒的解释:

A signal can arrive and be handled while an I/O primitive such as open or read is waiting for an I/O device. If the signal handler returns, the system faces the question: what should happen next?

POSIX specifies one approach: make the primitive fail right away. The error code for this kind of failure is EINTR. This is flexible, but usually inconvenient. Typically, POSIX applications that use signal handlers must check for EINTR after each library function that can return it, in order to try the call again. Often programmers forget to check, which is a common source of error.

The GNU C Library provides a convenient way to retry a call after a temporary failure, with the macro TEMP_FAILURE_RETRY:

BSD avoids EINTR entirely and provides a more convenient approach: to restart the interrupted primitive, instead of making it fail. If you choose this approach, you need not be concerned with EINTR.


带有缓冲区的 RIO 函数

加入应用级缓冲区,其一目的是减少调用系统级 I/O 函数的次数,降低陷入内核态而带来的额外开销。在 Linux 中,readwrite 是系统级 I/O 函数。在使用系统级 I/O 函数时,进程会从用户态进入内核态,并读取文件中的数据。而从用户态切换到内核态,需要一定的性能开销:处理器要将返回地址(当前指令的下一条指令地址)和额外的寄存器等压入到内核栈中。此外,进程使用系统调用还可能会被抢占。当内核态中系统调用被阻塞,那么相应地,进程就会进入休眠状态,延长了等待时间;而若在系统调用期间出现了一个优先级更高的进程,则该进程可能会抢占系统调用,内核态会优先处理优先级高的进程,也增加了该进程的等待时间。

rio_t 数据结构

rio_t 是 RIO 包中最常用也是最基础的数据结构。它与一个文件描述符一一对应,即 rio_fd ,但在读/写该文件时,还加上了一个缓冲区 rio_buf ,并用指针 rio_bufptr 记录缓冲区当前读到的位置,用 rio_cnt 记录缓冲区中未读到的字节数。总的来说,它就是给文件描述符加一个缓冲区,所以我们在上一节不会用到它。

#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;

在使用 rio_t 之前,需要先使用 rio_initb 函数,与文件描述符相连。

// rio_readinitb - Associate a descriptor with a read buffer and reset buffer
void rio_readinitb(rio_t *rp, int fd) {
  rp->rio_fd = fd;
  rp->rio_cnt = 0;
  rp->rio_bufptr = rp->rio_buf;
}

带缓冲区的读 API 函数

rio_read() 函数是对 Unix read() 函数的包装,读入的字节内容先存储在 rio_t 内部的缓冲区,然后根据需要一点点拿出,当缓冲区为空时,程序会一次性将缓冲区补满。若缓冲区不为空,那就使用 memcpy 移动 min(n, rio_cnt) 个字节给用户。该函数是

/**
 *    rio_read - This is a wrapper for the Unix read() function that
 *    transfers min(n, rio_cnt) bytes from an internal buffer to a user
 *    buffer, where n is the number of bytes requested by the user and
 *    rio_cnt is the number of unread bytes in the internal buffer. On
 *    entry, rio_read() refills the internal buffer via a call to
 *    read() if the internal buffer is empty.
 */
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) {
  int cnt;
  /* 填充缓冲区,bufptr 指示数据用到哪了,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 */
  }
  /* 如果缓冲区中还有内容, 那么直接运行下面代码,将 */
  /* min(n, rp->rio_cnt)个字节从内部缓冲区传给用户 */
  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_readnb() 是带缓冲区的 read(),最多读取 n 个字节,读到 EOF 后退出,注意到该函数调用了 rio_read() 而不是 read()

// rio_readnb - Robustly read n bytes (buffered)
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 */
}

rio_readlineb 函数用来读取每一行的文本,从实现看,它看似非常“低效”,因为它调用了 rio_read() 但每次却只读一个字节,直到读到 \n
才跳出循环,结束函数。但因为有缓冲区的存在,很多情况下其实不需要直接调用系统级 I/O,因此总体效率是可以接受的。rio_readlinebrio_readnb 都使用了 rio_t 带的缓冲区,因此就算这两个函数对一个 rio_t 交错调用,最终面对的都是同一个缓冲区,因此它们相互兼容,且线程安全。

// rio_readlineb - Robustly read a text line (buffered)
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;
}

总的来说,对于一个应用程序,rio_read() 函数代替了 read() 函数,并具有与 read() 函数一样的语义。在出错时,会返回 -1,并设置 errno,在 EOF 时,返回 0,如果要求读的字节数超过了缓冲区的字计数,会返回不足值。

你可能感兴趣的:(Linux,网络,c语言)