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

时间:2022-12-21 21:55:10

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:读写文件时的信息并不是与文件完全相同的,只有当调用了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变量替换,之后对读写相关的指针都进行了更新。