写点什么

cstdio 的源码学习分析 09- 改变文件流文件流 buffer 函数 setvbuf

作者:桑榆
  • 2022 年 10 月 11 日
    广东
  • 本文字数:4847 字

    阅读完需:约 16 分钟

cstdio 中的文件访问函数

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

  • fopen:打开文件

  • fclose:关闭文件

  • fflush:刷新文件流

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

  • setbuf:设置 stream buf

  • setvbuf:改变 stream buf

改变文件流文件流 buffer 函数 setvbuf

指定对应文件流 stream 的 IO 操作 buffer,同时设定该块缓存 buffer 的操作 mode 和 size 大小,如果 buffer 指针是空指针,那么 setvbuf 函数将会自动分配一块 size 大小的 buffer 作为缓存使用。

int setvbuf ( FILE * stream, char * buffer, int mode, size_t size );
复制代码

注意:上面的 mode 有以下的选择

  • _IOFBF:Full Buffering:输出操作中,数据在 buffer 写满后写入物理文件;输入操作中,buffer 只有在全为空时才被填写,填充的可能是多行数据;

  • _IOLBF:Line Buffering:输出操作中,数据在新的一行插入 FILE 流对象或 buffer 写满时触发写入物理文件;输入操作中,buffer 只有在 buffer 全为空时,写入新的一行到 buffer 中。

  • _IONBF:No Buffering:不使用缓存 buffer,所有输入输出操作都尽可能快地写入物理文件,当前模式下,buffer 和 size 参数将会被忽略

注意:setvbuf 的调用时机,在一个文件流对象绑定到一个打开的文件之后,对该文件流对象进行文件读写操作之前。

我们可以看如下的例子:

我们打开了一个 pFIle 对象,并将其 buffer 设置为 NULL(函数内部将自动生成一块大小为 1024Byte 大小的 buffer),mode 设置为_IOFBF。那么,在进行文件操作过程中,如向文件写入过程中,每写满 1024 字节才会触发一次将数据写入物理文件。

/* setvbuf example */#include <stdio.h>int main (){  FILE *pFile;
pFile=fopen ("myfile.txt","w");
setvbuf ( pFile , NULL , _IOFBF , 1024 );
// File operations here
fclose (pFile);
return 0;}
复制代码

函数入口分析

函数作用描述与上面所说的基本一致,这里,我们也看到三个 mode 的宏定义。

在 Glibc 中,这个函数实际的实现是_IO_setvbuf,我们跳转到这个函数进行分析。

// glibc/libio/stdio.h
/* Make STREAM use buffering mode MODE. If BUF is not NULL, use N bytes of it for buffering; else allocate an internal buffer N bytes long. */extern int setvbuf (FILE *__restrict __stream, char *__restrict __buf, int __modes, size_t __n) __THROW; /* The possibilities for the third argument to `setvbuf'. */#define _IOFBF 0 /* Fully buffered. */#define _IOLBF 1 /* Line buffered. */#define _IONBF 2 /* No buffering. */
// glibc/libio/iosetvbuf.cweak_alias (_IO_setvbuf, setvbuf)
复制代码


1.函数的大致框架

可以看到,整个函数的调用逻辑其实比较简单,使用 switch-case,针对每一种设置模式进行处理,在之前进行参数检查和获取锁,在后面做释放锁和返回值的操作。

int_IO_setvbuf (FILE *fp, char *buf, int mode, size_t size){  int result;  CHECK_FILE (fp, EOF);  _IO_acquire_lock (fp);    switch (mode)    {    case _IOFBF:        ...        break;    case _IOLBF:        ...        break;    case _IONBF:        ...        break;    }  result = _IO_SETBUF (fp, buf, size) == NULL ? EOF : 0;  unlock_return:  _IO_release_lock (fp);  return result;}
复制代码

2.参数检查

检查 fp 指针是否有效,fp 的_flags 是否符合规范值

// glibc/libio/libioP.h#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._IOFBF---full buffering 的情况

这里主要做了以下几件事情:

  • 首先将_IO_LINE_BUF 和_IO_UNBUFFERED 位置为 0,因为目前是要求 full buffering 的;

  • 然后我们检查输入参数 buf,如果为空的话,我们要尝试进行分配 buffer 分配;

  • 再次我们检查 fp->_IO_buf_base 参数,这里指向的是 fp 预先分配的缓存 buffer,只有这里也为空,那就说明完全没有缓存 buffer 可用,那我们就真的需要进行分配了;

  • 调用_IO_DOALLOCATE 对 fp 进行 buffer 分配

  • 根据分配 buffer 是否失败决定是直接返回错误 EOF,还是重新只将_IO_LINE_BUF 置为 0(因为当前已经分配 buffer 成功了)

  • 注意了,上面都是 buf 为空,需要重新分配的情况,如果 buf 不为空,那么我们会跳到

result = _IO_SETBUF (fp, buf, size) == NULL ? EOF : 0;

的执行中,进行 buf 设置;如果 fp->_IO_buf_base 不等于 NULL,那我们实际上是默认使用这块 buffer 的,返回 0,退出函数

    case _IOFBF:      fp->_flags &= ~(_IO_LINE_BUF|_IO_UNBUFFERED);      if (buf == NULL)    {      if (fp->_IO_buf_base == NULL)        {          /* There is no flag to distinguish between "fully buffered         mode has been explicitly set" as opposed to "line         buffering has not been explicitly set".  In both         cases, _IO_LINE_BUF is off.  If this is a tty, and         _IO_filedoalloc later gets called, it cannot know if         it should set the _IO_LINE_BUF flag (because that is         the default), or not (because we have explicitly asked         for fully buffered mode).  So we make sure a buffer         gets allocated now, and explicitly turn off line         buffering.
A possibly cleaner alternative would be to add an extra flag, but then flags are a finite resource. */ if (_IO_DOALLOCATE (fp) < 0) { result = EOF; goto unlock_return; } fp->_flags &= ~_IO_LINE_BUF; } result = 0; goto unlock_return; } break;
复制代码


4._IOLBF---line buffering 的情况---_IO_DOALLOCATE

这个函数的核心作用就是为 fp->_IO_buf_base 分配一块合理大小的 buffer 用作缓存,我们来看看它的一些具体逻辑:

  • 默认 size 大小是 size = BUFSIZ (8192 字节)

  • 对 fp 指针状态进行设置,将_IO_LINE_BUF 置位;

  • 通过获取该 IO 流的 stat 信息 st,决定是否有必要采用其中 st_blksize 更新 size(主要是考虑使用一个比 8192 更小的 size,分配足够的就行,不一定要最大的 size)

  • 通过 malloc 分配对应大小的 buffer,然后调用_IO_setb 将 fp->_IO_buf_base 设置为刚才申请的地址

// glibc/libio/filedoalloc.c
/* Allocate a file buffer, or switch to unbuffered I/O. Streams for TTY devices default to line buffered. */int_IO_file_doallocate (FILE *fp){ size_t size; char *p; struct __stat64_t64 st;
size = BUFSIZ; if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) { if (S_ISCHR (st.st_mode)) { /* Possibly a tty. */ if (#ifdef DEV_TTY_P DEV_TTY_P (&st) ||#endif local_isatty (fp->_fileno)) fp->_flags |= _IO_LINE_BUF; }#if defined _STATBUF_ST_BLKSIZE if (st.st_blksize > 0 && st.st_blksize < BUFSIZ) size = st.st_blksize;#endif } p = malloc (size); if (__glibc_unlikely (p == NULL)) return EOF; _IO_setb (fp, p, p + size, 1); return 1;}
复制代码

5._IOLBF---line buffering 的情况

这种情况是按行使用 buffer,主要做了以下操作:

  • 设置 tag,将_IO_UNBUFFERED 置 0,将_IO_LINE_BUF 置位;

  • 如果入参 buf 为空,那就直接返回 0,结束函数;否则等待执行_IO_SETBUF (fp, buf, size)

思考:这里为什么不重新检查 fp->_IO_buf_base 然后分配内存呢?

从上一种情况中我们注意到,在分配 buffer 后我们都默认将_IO_LINE_BUF 置位,即这是一种默认模式,所以我们无需检查 fp->_IO_buf_base 的状态

    case _IOLBF:      fp->_flags &= ~_IO_UNBUFFERED;      fp->_flags |= _IO_LINE_BUF;      if (buf == NULL)    {      result = 0;      goto unlock_return;    }      break;
复制代码

6._IONBF---no buffering 的情况

这种情况的操作就更为简单了,禁用了 buffer,我们将_IO_LINE_BUF 置 0,_IO_UNBUFFERED 置位,然后将入参 buf 置为 NULL,size 置为 0,等待调用_IO_SETBUF (fp, buf, size)

    case _IONBF:      fp->_flags &= ~_IO_LINE_BUF;      fp->_flags |= _IO_UNBUFFERED;      buf = NULL;      size = 0;      break;
复制代码

7.default 情况

这种情况直接将 result 置为 EOF(操作失败),退出函数

    default:      result = EOF;      goto unlock_return;
复制代码

8._IO_SETBUF 调用

这里的调用其实与前文中 setbuf 函数的调用流程也就基本一致了,我们就不重复一遍了。

实际上做的操作就是使用给定的 buffer 去更新

  • buffer 基地址:_IO_buf_base,_IO_buf_end

  • buffer 写地址:_IO_write_base,_IO_write_ptr,_IO_write_end

  • buffer 读地址:_IO_read_base,_IO_read_ptr,_IO_read_end

  result = _IO_SETBUF (fp, buf, size) == NULL ? EOF : 0;    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)
复制代码

总结

setvbuf 函数针对_IOFBF,_IOLBF,_IONBF 三种不同的模式,对缓存 buffer 进行不同的操作,若输入 buf 为空,则自动申请。这个函数能控制读写缓存 buffer 的更新机制,按 buffer 大小更新/按行更新/不使用 buffer,使得程序员能都手动控制输入写出写回真实物理文件的频率。

发布于: 刚刚阅读数: 3
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020.02.29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
cstdio的源码学习分析09-改变文件流文件流buffer函数setvbuf_源码刨析_桑榆_InfoQ写作社区