libc 中提供的基于 FILE 文件指针的读写函数
libc 中提供的基于 FILE 文件指针的读写函数是带 buffer 的读写函数。对于读来说它会读取比用户请求数目更多的字节到内部缓冲区中,然后从缓冲区中返回用户请求读取的长度。这样的行为能够减少系统调用的次数,提高读取的效率。
linux 系统调用中提供的基于文件描述符的读写函数
linux 中提供的基于文件描述符的读写函数是不带 buffer 的读写函数。话虽如此,但其实内核中也存在类似的读写缓冲行为,这里的 no buffering 是面向上层函数的说法。
libc 库中的读写相关函数依赖内核提供的系统调用来完成功能,读写缓冲隐藏在 c 库中的读写接口之下,并不为上层用户所感知。
FILE 的结构
在我的系统中,FILE 结构的主体是在 /usr/include/bits/types/struct_FILE.h
中定义的,具体的代码内容如下:
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
FILE 结构体与 utmplib 中的缓冲区的对比
在学习《unix-linux 编程实践教程》时,who 命令示例代码 utmplib.c 中也使用了缓冲区的功能。
相关的字段定义如下:
#define NRECS 16
#define NULLUT ((struct utmp *)NULL)
#define UTSIZE (sizeof(struct utmp))
static char utmpbuf[NRECS * UTSIZE];
static int num_recs;
static int cur_rec;
static int fd_utmp = -1;
utmplib.c 中只有读缓冲,相应的字段为读缓冲的缓冲区、当前的位置、缓冲区缓冲的内容计数。
libc 库中 FILE 结构体的如下变量可以与 utmplib.c 中读缓冲区的字段类比。
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
当前位置与结束位置可以对应到 utmplib.c 中的当前位置与缓冲区缓冲内容计数,read_base 是否可对应到读缓冲区(可能不是这个地址,不过读缓冲区肯定是有的)。
尽管 utmplib.c 中实现的上述读缓冲相对简单,但它与 libc 库中的读写函数使用的缓冲功能类似,在变量的设定上也有类似的表现形式。
缓冲区功能的推广
上述两个实例都是缓冲区功能的良好范例,可其实我们可以对缓冲区功能进行推广。
计算机架构中的缓冲模型
现行的计算机架构也可以看为一个多层次的缓冲区。
我从 《csapp》中拷贝到了如下计算机存储模型的金字塔结构图。
上述金字塔结构可以看为是一个多层的缓冲区,上层从下层获取数据,如此递归下去直到最底层。在这些层次中,每一个下层都可以看做是上层的缓冲区。
常规情况下上层直接从下层缓冲区中获取数据,命中就是最好的情况。如果不命中,就访问更下一层的缓冲区,直到获取到数据,这常常会带来额外的性能损耗。获取到数据后会根据情况逐层更新缓冲区内容。
在这个过程中 cache 预取的行为与缓冲行为最为接近。cache 预取会读取比寄存器访存地址更多的空间到一个 cache 行中,缓存的数据是寄存器访存临近的数据。这里基于空间局部性的特定,机器预计此次访问地址相邻的空间可能也会马上被访问,这样在下一次访问的时候可以直接从 cache 中获取到数据,这就是一次 cache 命中过程。
空间局部性
这里提到了一个关键的点就是空间局部性。其含义在于程序执行的过程中大多数时间都是在访问一个小范围连续地址空间的数据,这是程序执行的常态。这其实也是缓冲技术能够提高效率的关键因素。
我们这里的缓冲是针对未来的访问做出的优化,如果没有空间局部性的特点,每个时间点都访问完全没有关联的数据,那么缓冲功能将失效。
也许我们可以将所有的数据全部缓冲起来,但这与没有缓冲又有什么区别呢?
生活中的缓冲模型
生活中也有类似的缓冲模型,其中的一个模型表示如下:
这里缓冲的内容是不同的商品,对象发生了变化,但是其中的原理与计算机中的缓冲原理类似,甚至于我们可以说计算机中的缓冲模型其源头可能就是来自于生活中的不同模型。