cstdio 中的文件访问函数
stdio.h 中定义了一系列文件访问函数(fopen,fclose,fflush,freopen,setbuf,setvbuf),接下来我们一起来分析一下 setvbuf 对应的源码实现。
- fopen:打开文件
- fclose:关闭文件
- fflush:刷新文件流
- freopen:重新打开文件流(不同的文件或访问模式)
- setbuf:设置 stream buf
- setvbuf:改变 stream buf
设置文件流 buffer 函数 setbuf
指定对应文件流 stream 的 IO 操作 buffer,此时该 stream 就一定是使用缓存 buffer 的,或者如果 buffer 指针为 NULL,那么此时的 stream 会被禁用缓存 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 对象,避免两个线程同时进行修改
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。
6._IO_setbuffer---调用_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*操作锁
总结
setbuf 函数有两种使用方法,传入 buf 指针为空时禁用 buffer,有值且指向 BUFSIZ 大小的 buffer 时,使用该块 buffer 替换 fp 内部的_IO_buf_base,中间依次调用了_IO_setbuffer->_IO_SETBUF->_IO_new_file_setbuf->_IO_default_setbuf,先对文件内容进行同步保存,然后完成_IO_buf_base 变量替换,之后对读写相关的指针都进行了更新。
评论