写点什么

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

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

    阅读完需:约 15 分钟

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
voidsetbuf (FILE *fp, char *buf){ _IO_setbuffer (fp, buf, BUFSIZ); }
// glibc/libio/iosetbuffer.cvoid_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.cvoid_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 变量替换,之后对读写相关的指针都进行了更新。

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

桑榆

关注

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

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

评论

发布
暂无评论
cstdio的源码学习分析08-设置文件流buffer函数setbuf_源码刨析_桑榆_InfoQ写作社区