cstdio 中的文件访问函数
stdio.h 中定义了一系列文件访问函数(fopen,fclose,fflush,freopen,setbuf,setvbuf),接下来我们一起来分析一下 setvbuf 对应的源码实现。
改变文件流文件流 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.c
weak_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,使得程序员能都手动控制输入写出写回真实物理文件的频率。
评论