行业知识
『Linux从入门到精通』第 ⑳ 期 - 一探文件缓冲区的奥秘
文章目录
- 专栏导读
- 文章导读
- 一个奇怪的现象!
- 文件缓冲区
- 解释现象
- 模拟实现 MY_FILE 及其接口
- 定义 MY_FILE 结构
- my_fopen 模拟实现
- my_fwrite 模拟实现
- my_fclose 模拟实现
- 验证 MY_FILE 及其接口
- ���件缓冲区存在的意义
专栏导读
作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
文章导读
本章我们将深入学习Linux文件系统,深入理解文件缓冲区及其作用与原理。并将根据所学来实现一个自己的文件接口~
一个奇怪的现象!
首先我们使用C库函数 fprintf 与系统调用 write 在标准输出流(屏幕)中各输出一条语句。
#include #include #include int main(){ // C库 fprintf(stdout, "hello fprintf\n"); // 系统调用 const char *msg = "hello write\n"; write(1, msg, strlen(msg)); return 0;}接下来我们将输出的内容重定向到一个文件中,如下:
到目前为止,一切都还很正常。接下来我们将在程序中调用 fork 函数,为当前进程创建子进程,然后重复上述的操作。
#include #include #include int main(){ // C库 fprintf(stdout, "hello fprintf\n"); // 系统调用 const char *msg = "hello write\n"; write(1, msg, strlen(msg)); fork(); return 0;}此时奇怪的现象终于出现了。为什么在 fork 后,"hello write" 只输出了一次,而 "hello fprintf" 却输出了两次呢?接下来就不得不本章的主角——文件缓冲区了。
文件缓冲区
上一章中我们曾学习过,在一个进程维护的 task_struct 结构体中,包含一个 files_struct 字段。files_struct 中包含有一个 file* fd_array[] 的数组,该数组对应的下标即为文件描述符。该数组中的元素为一个个 struct file 对象,该 file 结构体中包含了文件相关的属性信息,其中有一个很重要的成员便是文件缓冲区。
fprintf 内部会访问一个 FILE 类型的对象,而 FILE 结构本质是对 struct file 的封装。(感兴趣的同学可以看一下FILE结构体)
在/usr/include/libio.hstruct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags//缓冲区相关/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */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; //封装的文件描述符#if 0int _blksize;#elseint _flags2;#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};当我们调用 fprintf 输出语句时,其内部实现并不会将要输出的内容直接写入到文件中,而是会暂存到缓冲区中。而C库会结合相应的刷新策略将缓冲区中的数据通过系统调用(如 write )写入到目标文件中。
具体的刷新策略有如下三种:
- 无缓冲(每次输出都会直接写入文件,没有缓冲);
- 行缓冲(当输出的数据包含换行符时,或者缓冲区满时,数据才会被写入文件);
- 全缓冲(数据会在缓冲区满时或者调用fflush函数时才会被写入文件);
首先终端设备上(显示器)一般采用的刷新策略是行缓冲,而普通文件(磁盘文件)采用的是全缓冲。
现在我们可以解释之前出现的奇怪现象了!
解释现象
#include #include #include int main(){ // C库 fprintf(stdout, "hello fprintf\n"); // 系统调用 const char *msg = "hello write\n"; write(1, msg, strlen(msg)); fork(); return 0;}首先,write 是系统调用,并不是C库提供的函数,所以它并不会考虑缓冲区的问题。在 fork 之前就已经将数据写入到文件中了。
但是 fprintf 则面对的是两种不同的情况,当我们输出在屏幕上时,此时采取的刷新策略是行缓冲,所以在 fork 之前,它也已经输出到屏幕上了,并不会有奇怪的现象。
但当我们将输出的数据重定向到 test.txt 文件中时,此时刷新策略由行缓冲变为了全缓冲,由于缓冲区此时并没有被写满,所以暂时不会输出数据。在 fork 之后,父子进程在结束时会刷新各自的缓冲区,此时会发生写时拷贝,所以 “hello fprintf” 被打印了两次。
模拟实现 MY_FILE 及其接口
为了更加清晰的认识缓冲区的工作原理,我们尝试模仿C库中的 FILE 来实现一个自己的文件接口。
首先是定义一个 MY_FILE 结构体,以下仅作粗略的模仿,所以我们只需要几个关键的字段。
定义 MY_FILE 结构
/* mystdio.h 文件下 */#pragma once #include #define NUM 1024 // 缓冲区大小#define BUFF_NONE 0x1 // 无缓冲#define BUFF_LINE 0x2// 行缓冲#define BUFF_ALL 0x4// 全缓冲typedef struct _MY_FILE{ int fd; // 文件描述符 int flags; // 文件打开方式 char outputbuffer[NUM]; // 文件缓冲区 int current; // 当前写入的位置} MY_FILE;MY_FILE* my_fopen(const char* path, const char* mode);size_t my_fwrite(const void* ptr, size_t size, size_t num, MY_FILE* stream);int my_fclose(MY_FILE* fp);int my_fflush(MY_FILE* fp);my_fopen 模拟实现
/* mystdio.c 文件下*/#include "mystdio.h"#include #include #include #include #include #include #include MY_FILE* my_fopen(const char* path, const char* mode){ // 1.识别标志位 int flag = 0; if(strcmp(mode, "r") == 0) flag |= O_RDONLY; else if(strcmp(mode, "w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC); else if(strcmp(mode, "a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND); else { // r+/w+.... } // 2.尝试打开文件 mode_t m = 0666; int fd = 0; if(flag & O_CREAT) fd = open(path, flag, m); else fd = open(path, flag); if(fd fd = fd; mf->flags = 0; mf->flags |= BUFF_LINE; memset(mf->outputbuffer, 'my_fwrite 模拟实现
', sizeof(mf->outputbuffer)); mf->current = 0; // 5.返回打开的文件 return mf;}int my_fflush(MY_FILE* fp){ assert(fp); // 将用户缓冲区中的数据,通过系统调用接口冲刷给OS write(fp->fd, fp->outputbuffer, fp->current); fp->current = 0; fsync(fp->fd); return 0;}size_t my_fwrite(const void* ptr, size_t size, size_t num, MY_FILE* stream){ // 1. 缓冲区如果满了就直接写入 if(stream->current == NUM) my_fflush(stream); // 2. 根据缓冲区剩余情况,进行数据拷贝即可 size_t user_size = size * num; // 用户要写入的大小 size_t my_size = NUM - stream->current; // 缓冲区剩余大小 size_t writen = 0; // 成功写入的字节数 if(my_size >= user_size) { memcpy(stream->outputbuffer, ptr, user_size); // 3. 更新计数器字段 stream->current += user_size; writen = user_size; } else // 若缓冲区剩余空间不足,则只拷贝剩余空间大小 { memcpy(stream->outputbuffer, ptr, my_size); // 3. 更新计数器字段 stream->current += my_size; writen = my_size; } // 4. 开始策划刷新 if(stream->flags & BUFF_ALL) { if(stream->current != NUM) my_fflush(stream); } else if(stream->flags & BUFF_LINE) { if(stream->outputbuffer[stream->current - 1] == '\n') my_fflush(stream); } else { } return writen;}my_fclose 模拟实现
int my_fclose(MY_FILE* fp){ assert(fp); // 1. 冲刷缓冲区 if(fp->current > 0) my_fflush(fp); // 2. 关闭文件 close(fp->fd); // 3. 释放堆空间 free(fp); // 4. 指针置空 fp = NULL; return 0;}验证 MY_FILE 及其接口
/* main.c 文件中 */#include "mystdio.h"#include #include #define MYFILE "log.txt"int main(){ MY_FILE* fp = my_fopen(MYFILE, "w"); if(fp == NULL) return 1; const char* str = "hello my write!"; int cnt = 500; // 操作文件 while(cnt) { char buffer[1024]; snprintf(buffer, sizeof(buffer), "%s : %d\n", str, cnt--); size_t size = my_fwrite(buffer, strlen(buffer), 1, fp); sleep(1); printf("当前成功写入: %lu 个字节\n", size); } my_fclose(fp); return 0;}首先我们来验证自己实现的 MY_FILE 及其接口是否正确,像平时使用 FILE 一样来使用我们自己实现的 MY_FILE,如下:
$ gcc -o mytest main.c mystdio.c
$ while : ; do cat log.txt; echo "#####"; sleep 1; done
使用分屏模式,在另一半屏幕进行监控。
文件缓冲区存在的意义
如图所示,可以清晰的观察到每隔一秒,my_fwrite 都会将 buffer 中的内容写入到 log.txt 中。
目前为止,现象背后的原因我们已经搞明白了。但是,我们还需要理解为什么需要文件缓冲区的存在呢?我们直接将数据写入文件难道不是更方便吗?
文件缓冲区的存在主要是为了提高文件操作的性能。在文件操作中,频繁地读取或写入磁盘是相对较慢的操作,而内存的读写速度通常要快得多。因此,为了减少对磁盘的直接访问次数,操作系统和编程语言通常会引入文件缓冲区来缓存文件的读取和写入。
主要的原因包括:
-
性能优化: 直接在磁盘上进行读写操作可能会导致性能瓶颈,因为磁盘访问相对较慢。通过使用内存中的文件缓冲区,可以减少对磁盘的实际访问次数,提高文件操作的速度。
-
减少系统调用次数: 文件操作通常涉及到系统调用,而系统调用的开销相对较高。通过将数据缓存在内存中,可以减少需要进行系统调用的次数,从而提高效率。
-
提高数据一致性: 缓冲区可以用于暂时存储数据,等待适当的时机一次性写入磁盘。这样可以减少频繁的磁盘写入,提高磁盘写入的效率,同时确保数据的一致性。
虽然文件缓冲区提高了性能,但也需要注意,缓冲区中的数据在某些情况下可能会丢失(例如系统崩溃时)。因此,一些涉及到重要数据的应用程序可能需要通过特定的手段来确保数据的持久性,比如调用 fsync 等操作。
本章的内容到这里就结束了!如果觉得对你有所帮助的话,欢迎三连~
-
上一篇:邮箱服务器怎么查找?
下一篇:聪明孩英文怎么写?