CSAPP: 文件I/O,各大输出流的不同

(一) Unix I\O , RIO ,标准I\O

刚接触文件I/O的时候,原来一直习惯,也只会使用printf的我被几个输入函数绕得团团转,于是利用这篇文章整理一下这三大代表,从不同在哪,为什么需要这种不同分析,(每一个轮子的诞生总是因为他被需要

Unix I\O

write() 向描述符为fd的文件中写入最多n个字节到buf中,read则为读取,write和read做为Unix I\O,是在操作系统内核中实现的,实际上RIO和标准I\O也都是对他进行二次封装,因此他实际上是一个系统调用,上代码看效果‘

#include "csapp.h"
  int main(void)
 {
  char c;
  while(Read(STDIN_FILENO, &c, 1) != 0)    //STDIN_FILENO代表标准输入的描述符
  {
  write(STDOUT_FILENO, &c, 1);                 //STDOUT_FILENO代表标准输出的描述符
  }                   
  exit(0);
}

用strace工具跟踪查看他的运行效果

每一行代表一次系统调用,由于我们设定是1,因此每一次的系统调用都只写入和读取一次字符。

RIO

Unix I\O是底层的调用,可以通过他完成所有的读写任务,但也因为他在最底层,所以在任务上他并不能完成得很好,会遗留下一些漏洞,也正是为了解决这些漏洞这些需求,诞生了后面的I\O函数,每一个漏洞就对应着一个新的解决方法。

如果说Unix IO是底层的调用,标准I\O(也就是我们平时用的printf)就是属于比较高层的封装了,而RIO是在CSAPP中的包,我认为它处于一个中间位置,一方面我们可以看到他的源码其实是通过还是基于Unix I\O的实现,另一方向我们可以看到他的实现很好的解决了一些Unix I\O的漏洞,因此通过他可以把上下两层的东西都串在一起。

首先是Unix IO遗留下的第一个漏洞,不足值

在第一次看书的时候没有细看,所有一直不太理解为什么需要一个RIO的实现,直到我做了第一个服务器的连接练习回来看到这段话的时候才深有体会,不然我会一直觉得不足值似乎就是一个大多时候可以忽略的东西。

想象一下一个简单的网络应用,你是客户端,对面的妹子是服务端,然后你通过把你想说的话写在一个文件上,这个文件会传到妹子那里进行读取,你们之间的交流是通过读写文件。OK,开始,你想说“我想追你朋友”,于是你用write写入这句话write(file,”我想追你朋友”,6),你要写入6个字,然后系统调用,由于某种原因出错,他只写进了4个字然后就停止了,于是系统回来告诉你我只写了4个字而且我发过去了,你…..(¥#@¥%

因此rio_write的出现就是为了避免这种悲剧的发送,他会让系统一直写直到6个字都写进去为止,上源码

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

rio_writen记录了要读数量n,每次write后都会获取已读数量并以此计算剩余未读数量,在while循环里面,rio_write会一直等待到剩余未读数量为0为止,因此我们不会出现只写了一半的数量然后就退出的情况。如果中途write返回小于等于0,则判断是否被打断,如果是被打断,则设置重新写一次,否则则是读取到EOF,说明已经读到了尽头,这种情况下不足值是允许的,因此退出。

rio_writen很好的解读了第一个漏洞,不足值的问题,这也是为什么书上会推荐我们在进行网络编程的时候使用这个接口去代替write。接下来是第二个漏洞,这个漏洞存在于以上的两种方法。

我们继续使用strace工具去跟踪rio_writen的运行,如图:

可以看到每一次读写都是一次系统调用(write调用),这和write的调用是一样的,系统调用的时候需要从用户态切换到系统态,因此这种做法会导致系统频繁的从用户态切换到系统态从而产生效率问题。

为解决这个问题,RIO的rio_read还有另外一个缓存版本,原理就是每次读先把尽量多的内容读取到缓存区,然后再根据用户的需要读取数量,比如我想要读取4个字符,实际上这个函数会读取40个字符到缓存区再返回前4个字符,这个方法的好处在于往后我再去读的时候就可以不需要进行系统调用,可以直接从缓存区读取以达到节省效率。上代码。

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

   void rio_readinitb(rio_t *rp, int fd)
{
   rp->rio_fd = fd;
   rp->rio_cnt = 0;
   rp->rio_bufptr = rp->rio_buf;
}

        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;
}
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缓存版本之前要先使用rio_readinitb(rio_t *rp, int fd),我们可以看到这个函数实际上是把fd文件描述符和一个rp指针建立联系,rp结构体里面存储着文件描述符和缓存区。static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)是内部使用的读函数,ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)是外部使用的读函数,为什么会有两个读函数,看实现就发现内部的读函数是用来代替无缓存版本(就是上面那个rio版本)的read,他不再是直接从描述符直接读取,而是判断缓存区是否为空,空则将数据读取进缓存区,否则直接从缓存区读取。而外部使用的函数和无缓存版本的是一样,他使用内部读取函数读取字符,并且保证读取字符数量是用户设定的数量。

两个版本的区别

无缓存版本
有缓存版本

标准I\O

最后的标准I\O也就是通常我们使用的fprintf,fputs,fgets系列,他使用的实际类似的这种缓存机制,因此有时候我们会发现printf(“hello”)没有输出,关键就是在于aaa只被缓存,而没有被传入到标准输出,你会发现printf(“hello\n”)就可以了,因为遇到了换行符”\n”这段字符就从缓存被传入到了标准输出。

在日常的使用中我们通常都可以直接使用标准I\O,因为他足以保证高效率,但在多线程或者网络编程的场景中,标准I\O却不被推荐,原因也是因为缓存机制。这里有一点要注意到,当你使用标准I\O打开一个文件进行同时读写,读写操作都是有缓存的,而且还是用一块缓存!想象一下我们先写满缓存,然后没有进行fflush(清空缓存区)再进行读操作,是什么东西会被读回来。这种情况下会很容易造成缓存区的混乱。因此书上推荐我们使用RIO I\O,一开始我疑惑RIO不是也是缓存机制吗,后来才想到RIO只有读缓存。

再想象一下多个线程使用标准输出,使用同一块缓存区,如果没有及时清空,同样很容易就会造成缓存区的混乱输出一些莫名的东西出来。

标准I\O在日常中普遍被使用,但是充分了解这些机制有助于我们看清有时候一些莫名其妙的输入。

参考:

1.代码片段均摘抄自CSAPP

2.知乎大佬们的回答

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇