cstdio的源码学习分析09-设置文件流buffer函数setbuf

904 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

cstdio中的文件访问函数

stdio.h中定义了一系列文件访问函数(fopen,fclose,fflush,freopen,setbuf,setvbuf),接下来我们一起来分析一下freopen对应的源码实现。

  • fopen:打开文件

  • fclose:关闭文件

  • fflush:刷新文件流

  • freopen:重新打开文件流(不同的文件或访问模式)

  • setbuf:设置stream buf

  • setvbuf:改变stream buf

设置文件流buffer函数setbuf

指定对应文件流stream的IO操作buffer,此时该stream就一定是使用缓存buffer的,或者如果buffer指针为NULL,那么此时的stream会被禁用缓存buffer。

  • 使用缓存buffer:读写文件时的信息并不是与文件完全相同的,只有当调用了fflush函数才会将缓存buffer中的信息同步到文件中;
  • 不使用缓存buffer:那么写入的信息将会尽可能快地同步到文件中。

注意:buffer的size大小有要求为BUFSIZ

void setbuf ( FILE * stream, char * buffer );

我们可以看如下的代码例子:

同时打开了两个FILE对象,其中一个设置为buffer,另一个设置为no buffer,那么pFile1只有再调用fflush(pFile1)之后信息才完全写入文件,而pFile2的信息是尽可能快地写入文件,不必使用fflush,当然,最后fclose之后,buffer中的信息都会同步到文件中,这个在我们之前分析fclose时就知道了。

/* setbuf example */
#include <stdio.h>int main ()
{
  char buffer[BUFSIZ];
  FILE *pFile1, *pFile2;

  pFile1=fopen ("myfile1.txt","w");
  pFile2=fopen ("myfile2.txt","a");

  setbuf ( pFile1 , buffer );
  fputs ("This is sent to a buffered stream",pFile1);
  fflush (pFile1);

  setbuf ( pFile2 , NULL );
  fputs ("This is sent to an unbuffered stream",pFile2);

  fclose (pFile1);
  fclose (pFile2);

  return 0;
}

函数入口分析

这里我们分析的是glibc/libio/stdio.h中的函数定义,从函数前的注释,我们也能看出函数的功能,基本与我们上面描述的一致。

接受一个FILE对象,和char对象(可以为BUFSIZ大小的char数组或空指针),将FILE*对象设置为使用该块buffer或禁用buffer。

BUFSIZ的大小默认是8192字节

// glibc/libio/stdio.h

/* If BUF is NULL, make STREAM unbuffered.
   Else make it use buffer BUF, of size BUFSIZ.  */
extern void setbuf (FILE *__restrict __stream, char *__restrict __buf) __THROW;

/* Default buffer size.  */
#define BUFSIZ 8192

函数逻辑分析

1.调用_IO_setbuffer实现

setbuf 实现在glibc/libio/setbuf.c文件中,实际上还是通过调用_IO_setbuffer实现的。

注意:这里_IO_setbuffer的第三个参数就是我们上面默认的BUFSIZ,这也是传入buffer大小必须固定的原因,这里默认就是这个大小。

// glibc/libio/setbuf.c

void
setbuf (FILE *fp, char *buf)
{
  _IO_setbuffer (fp, buf, BUFSIZ);                                                                                                                                                                          
}

// glibc/libio/iosetbuffer.c
void
_IO_setbuffer (FILE *fp, char *buf, size_t size)
{
  CHECK_FILE (fp, );
  _IO_acquire_lock (fp);
  fp->_flags &= ~_IO_LINE_BUF;
  if (!buf)
    size = 0;
  (void) _IO_SETBUF (fp, buf, size);
  if (_IO_vtable_offset (fp) == 0 && fp->_mode == 0 && _IO_CHECK_WIDE (fp))
    /* We also have to set the buffer using the wide char function.  */
    (void) _IO_WSETBUF (fp, buf, size);
  _IO_release_lock (fp);
}

2._IO_setbuffer---FILE*参数检查

这一步调用CHECK_FILE对fp指针进行了检查:是否为空指针;_flags信息是否正常。

一般调用该宏时会同时传入EOF,如果发现fp指针异常,则将错误码置为EINVAL,然后返回EOF,表示读取文件失败。

// glibc/libio/iosetbuffer.c
void
_IO_setbuffer (FILE *fp, char *buf, size_t size)
{
  CHECK_FILE (fp, );
  
#ifdef IO_DEBUG
# define CHECK_FILE(FILE, RET) do {             \
    if ((FILE) == NULL                      \
    || ((FILE)->_flags & _IO_MAGIC_MASK) != _IO_MAGIC)  \
      {                             \
    __set_errno (EINVAL);                   \
    return RET;                     \
      }                             \
  } while (0)
#else
# define CHECK_FILE(FILE, RET) do { } while (0)
#endif

3._IO_setbuffer---获取对FILE*操作锁

保护fp对象,避免两个线程同时进行修改

_IO_acquire_lock (fp);

4._IO_setbuffer---设置_flags

将_IO_LINE_BUF tag置0,便于后续的操作

fp->_flags &= ~_IO_LINE_BUF;

#define _IO_LINE_BUF          0x0200

5._IO_setbuffer---检查buf更新size

因为之前size被设置为BUFSIZ,如果传入的buf为空指针,那么我们就要将size更新为0。

  if (!buf)
    size = 0;

6._IO_setbuffer---调用_IO_SETBUF/_IO_WSETBUF

根据字符类型(标准字符/宽字符)决定函数调用:

  • 所有字符首先调用_IO_SETBUF进行设置;
  • 如果是宽字符,那就要额外调用_IO_WSETBUF进行设置。

因为两种字符的处理有所不同,这里我们主要分析标准字符_IO_SETBUF的处理流程

  (void) _IO_SETBUF (fp, buf, size);
  if (_IO_vtable_offset (fp) == 0 && fp->_mode == 0 && _IO_CHECK_WIDE (fp))
    /* We also have to set the buffer using the wide char function.  */
    (void) _IO_WSETBUF (fp, buf, size);

7._IO_SETBUF---宏展开后的具体实现:_IO_new_file_setbuf

在经过一系列跳转之后我们调用到了_IO_file_setbuf_mmap函数,然后到了_IO_new_file_setbuf,这里面是具体的实现

/* The 'setbuf' hook gives a buffer to the file.
   It matches the streambuf::setbuf virtual function. */
typedef FILE* (*_IO_setbuf_t) (FILE *, char *, ssize_t);
#define _IO_SETBUF(FP, BUFFER, LENGTH) JUMP2 (__setbuf, FP, BUFFER, LENGTH)                                                                                                                                 
#define _IO_WSETBUF(FP, BUFFER, LENGTH) WJUMP2 (__setbuf, FP, BUFFER, LENGTH)

const struct _IO_jump_t _IO_file_jumps_mmap libio_vtable =
{
...
    JUMP_INIT(setbuf, (_IO_setbuf_t) _IO_file_setbuf_mmap),
...
}

FILE *
_IO_file_setbuf_mmap (FILE *fp, char *p, ssize_t len)
{
  FILE *result;
                                                                                                                                                                                                            
  /* Change the function table.  */
  _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
  fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;

  /* And perform the normal operation.  */
  result = _IO_new_file_setbuf (fp, p, len);

  /* If the call failed, restore to using mmap.  */
  if (result == NULL)
    {
      _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_mmap;
      fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_mmap;
    }

  return result;
}

8._IO_new_file_setbuf---调用逻辑

FILE *
_IO_new_file_setbuf (FILE *fp, char *p, ssize_t len)
{
  if (_IO_default_setbuf (fp, p, len) == NULL)
    return NULL;

  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;
  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);

  return fp;
}
libc_hidden_ver (_IO_new_file_setbuf, _IO_file_setbuf)

9._IO_new_file_setbuf---_IO_default_setbuf调用

可以看到会首先调用_IO_default_setbuf,这里的逻辑如下:

  • 首先对fp进行sync操作(保存fp内部的信息,与外部文件状态做同步),因为后面我们要对内部信息进行操作,如果失败(返回EOF)那么我们就终止此次setbuf操作,返回NULL;
  • 然后根据传入的buffer指针和size决定是哪一种方式(使用buffer作为缓存buffer,还是不使用缓存buffer)

    • 不使用缓存buffer的情况,首先要将_IO_UNBUFFERED置位,然后调用_IO_setb(主要是操作_IO_buf_base和_IO_buf_end),通过函数逻辑可以看到,当入参为_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0)时,只有_IO_USER_BUF tag为0时才释放f->_IO_buf_base,本次传入的信息将_IO_buf_base和_IO_buf_end置为_shortbuf的第一个字节和第二个字节地址,即中间没有可用空间了,达到了禁用buffer的目的,最后将_IO_USER_BUF置位;
    • 使用缓存buffer的情况,首先将_IO_UNBUFFERED置0,然后调用_IO_setb将_IO_buf_base和_IO_buf_end置为我们传入buffer的开始和结尾地址,这样就达到了替换buffer的目的。
  • 上一步禁用buffer或替换buffer后,我们需要将读写指针(base,ptr,end)都恢复到初始值为0。
FILE *
_IO_default_setbuf (FILE *fp, char *p, ssize_t len)
{
    if (_IO_SYNC (fp) == EOF)
    return NULL;
    if (p == NULL || len == 0)
      {
    fp->_flags |= _IO_UNBUFFERED;
    _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
      }
    else
      {
    fp->_flags &= ~_IO_UNBUFFERED;
    _IO_setb (fp, p, p+len, 0);
      }
    fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = 0;
    fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_read_end = 0;
    return fp;
}

void
_IO_setb (FILE *f, char *b, char *eb, int a)
{
  if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
    free (f->_IO_buf_base);
  f->_IO_buf_base = b;
  f->_IO_buf_end = eb;
  if (a)
    f->_flags &= ~_IO_USER_BUF;
  else
    f->_flags |= _IO_USER_BUF;
}
libc_hidden_def (_IO_setb)

10._IO_new_file_setbuf---更新写相关指针

上一步调用_IO_default_setbuf中,我们得到了fp->_IO_buf_base的新地址(fp->_shortbuf或传入的p地址),我们需要使用这个新地址更新写相关的base/ptr/end地址,便于后续写入过程使用该buffer

fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;

11._IO_new_file_setbuf---_IO_setg调用(更新读相关指针)

通过_IO_setg宏,实际上最后还是在更新读相关的宏,都更新为fp->_IO_buf_base

_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);

#define _IO_setg(fp, eb, g, eg)  ((fp)->_IO_read_base = (eb),\                                                                                                                                              
    (fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))

12._IO_setbuffer---释放FILE*操作锁

_IO_release_lock (fp);

总结

setbuf函数有两种使用方法,传入buf指针为空时禁用buffer,有值且指向BUFSIZ大小的buffer时,使用该块buffer替换fp内部的_IO_buf_base,中间依次调用了_IO_setbuffer->_IO_SETBUF->_IO_new_file_setbuf->_IO_default_setbuf,先对文件内容进行同步保存,然后完成_IO_buf_base变量替换,之后对读写相关的指针都进行了更新。