IO_FILE fread

Posted Nullan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IO_FILE fread相关的知识,希望对你有一定的参考价值。

@TOC


前言

跟着raycp师傅学IO_FILE。原文链接


提示:以下是本篇文章正文内容,下面案例可供参考

流程

整体流程为fread调用_IO_sgetn,_IO_sgetn调用vtable中的_IO_XSGETN也就是_IO_file_xsgetn,_IO_file_xsgetn是fread实现的核心函数。

判断fp->_IO_buf_base输入缓冲区是否为空,如果为空则调用的_IO_doallocbuf去初始化输入缓冲区。
在分配完输入缓冲区或输入缓冲区不为空的情况下,判断输入缓冲区是否存在数据。
如果输入缓冲区有数据则直接拷贝至用户缓冲区,如果没有或不够则调用__underflow函数执行系统调用读取数据到输入缓冲区,再拷贝到用户缓冲区。

源码分析

对缓冲区初始化

首先是第一部分,在fp->_IO_buf_base为空时,也就是输入缓冲区未建立时,代码调用_IO_doallocbuf函数去建立输入缓冲区。

void
_IO_doallocbuf (_IO_FILE *fp)
{
  if (fp->_IO_buf_base) # 如何输入缓冲区不为空,直接返回
    return;
  if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0) #检查标志位
    if (_IO_DOALLOCATE (fp) != EOF) ## 调用vtable函数
      return;
  _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

函数先检查fp->_IO_buf_base是否为空,如果不为空的话表明该输入缓冲区已被初始化,直接返回。如果为空,则检查fp->_flags看它是不是_IO_UNBUFFERED或者fp->_mode大于0,如果满足条件调用FILE的vtable中的_IO_file_doallocate

_IO_file_doallocate (_IO_FILE *fp)
{
  _IO_size_t size;
  char *p;
  struct stat64 st;

  ...
  size = _IO_BUFSIZ;
  ...
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) # 调用`_IO_SYSSTAT`获取FILE信息
   {
     ... 
     if (st.st_blksize > 0)# 修改相应需要申请的size。
     size = st.st_blksize;
     ...
   }
 p = malloc (size);  
 ...
 _IO_setb (fp, p, p + size, 1); # 调用`_IO_setb`设置FILE缓冲区
  return 1;
}
libc_hidden_def (_IO_file_doallocate)

_IO_file_doallocate函数是分配输入缓冲区的实现函数,首先调用_IO_SYSSTAT去获取文件信息,_IO_SYSSTAT函数是vtable中的 __stat函数。获取文件信息,修改相应需要申请的size。

_IO_file_doallocate函数是分配输入缓冲区的实现函数,首先调用_IO_SYSSTAT去获取文件信息,_IO_SYSSTAT函数是vtable中的 __stat函数,获取文件信息,修改相应需要申请的size。

空间申请出来后,调用_IO_setb

void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
  ...
  f->_IO_buf_base = b; # 设置_IO_buf_base 
  f->_IO_buf_end = eb; # 设置_IO_buf_end
  ...
}
libc_hidden_def (_IO_setb)

设置了_IO_buf_base和_IO_buf_end,可以预料到_IO_setb函数执行完后,fp的这两个指针被赋上值了

就是_IO_buf_base和_IO_buf_end

初始化缓冲区就完成了,函数返回_IO_file_doallocate后,接着_IO_file_doallocate也返回,回到_IO_file_xsgetn函数中。

拷贝输入缓冲区数据

程序就进入到第二部分:拷贝输入缓冲区数据,如果输入缓冲区里存在已输入的数据,则把它直接拷贝到目标缓冲区里。fp->_IO_read_ptr指向的是输入缓冲区的起始地址,fp->_IO_read_end指向的是输入缓冲区的结束地址。
将fp->_IO_read_end-fp->_IO_read_ptr之间的数据通过memcpy拷贝到目标缓冲区里。

执行系统调用读取数据

在输入缓冲区为0或者是不能满足需求的时候则会执行最后一步__underflow去执行系统调用read读取数据,并放入到输入缓冲区里。

因为demo里第一次读取数据,此时的fp->_IO_read_end以及fp->_IO_read_ptr都是0,因此会进入到__underflow

int
__underflow (_IO_FILE *fp)
{
  
  # 额外的检查
  ...
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ...
  # 调用_IO_UNDERFLOW
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

函数稍微做一些检查就会调用_IO_UNDERFLOW函数,其中一个检查是如果fp->_IO_read_ptr小于fp->_IO_read_end则表明输入缓冲区里存在数据,可直接返回,否则则表示需要继续读入数据。

检查都通过的话就会调用_IO_UNDERFLOW函数,该函数是FILE结构体vtable里的_IO_new_file_underflow

int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
  ...
  ## 如果存在_IO_NO_READS标志,则直接返回
  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  ## 如果输入缓冲区里存在数据,则直接返回
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ...
  ## 如果没有输入缓冲区,则调用_IO_doallocbuf分配输入缓冲区
  if (fp->_IO_buf_base == NULL)
    {
      ...
      _IO_doallocbuf (fp);
    }
  ...
  ## 设置FILE结构体指针
  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
  fp->_IO_read_end = fp->_IO_buf_base;
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;
  ##调用_IO_SYSREAD函数最终执行系统调用读取数据
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
           fp->_IO_buf_end - fp->_IO_buf_base);
  ...
  ## 设置结构体指针
  fp->_IO_read_end += count;
  ...
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

这个_IO_new_file_underflow函数,是最终调用系统调用的地方,在最终执行系统调用之前,仍然有一些检查

检查FILE结构体的_flag标志位是否包含_IO_NO_READS,如果存在这个标志位则直接返回EOF,其中_IO_NO_READS标志位的定义是#define _IO_NO_READS 4 /* Reading not allowed */。
如果fp->_IO_buf_base位null,则调用_IO_doallocbuf分配输入缓冲区。
接着初始化设置FILE结构体指针,将他们都设置成fp->_IO_buf_base
调用_IO_SYSREAD(vtable中的_IO_file_read函数),该函数最终执行系统调用read,读取文件数据,数据读入到fp->_IO_buf_base中,读入大小为输入缓冲区的大小fp->_IO_buf_end - fp->_IO_buf_base。
设置输入缓冲区已有数据的size,即设置fp->_IO_read_end为fp->_IO_read_end += count。

第二步里面的如果fp->_IO_buf_base位null,则调用_IO_doallocbuf分配输入缓冲区,似乎有点累赘,因为之前已经分配了,这个原因在最后会说明。

其中第四步的_IO_SYSREAD(vtable中的_IO_file_read函数)的源码比较简单,就是执行系统调用函数read去读取文件数据

_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
   return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
           ? read_not_cancel (fp->_fileno, buf, size)
           : read (fp->_fileno, buf, size));
 }

_IO_file_underflow函数执行完毕以后,FILE结构体中各个指针已被赋值,且文件数据已读入,输入缓冲区里已经有数据,结构体值如下,其中fp->_IO_read_ptr指向输入缓冲区数据的开始位置,fp->_IO_read_end指向输入缓冲区数据结束的位置

_IO_read_ptr和_IO_buf_base一定相等,但是end不相等,因为数据不一定有缓冲区长,read是要读入的数据长度
函数执行完后,返回到_IO_file_xsgetn函数中,由于while循环的存在,重新执行第二部分,此时将输入缓冲区拷贝至目标缓冲区,最终返回。

以上是关于IO_FILE fread的主要内容,如果未能解决你的问题,请参考以下文章

IO_FILE fopen

IO_FILE hack FSOP

IO_file fopen

IO_File

_IO_FILE 的 Pybind11 文件指针包装器

144_IO_File_常用方法_文件名