目录
一、dup2
二、 引入
三、C语言FILE中的缓冲区
3.1 缓冲区的作用
3.2 缓冲区的刷新机制
3.3 对引入代码现象的解释
3.4 模拟实现C语言中的FILE
四、文件系统中的缓冲区
4.1 fsync
在本期内容正式开始之前,我们先介绍一个上期遗漏的知识点:
我们如果要想用代码实现重定向,先要关闭对应的标准流文件,再使用open函数打开需要的文件,来占据原本关闭的标准流的文件描述符,最终实现重定向
还要关闭原本的标准流文件,这样子是不是太麻烦了
下面我们来介绍一个函数dup2(包含在头文件unistd.h中),可以直接替换文件的文件描述符:
int dup2(int oldfd, int newfd);
// dup2() makes newfd be the copy of oldfd, closing newfd first if necessary
● 该函数会将newfd文件描述符所对应的文件,替换成oldfd文件描述符所对应的文件
我们来试试看:
#include
#include
#include
#include
#include
#define LOG "test.txt"
int main()
{
int fd = open(LOG,O_WRONLY | O_APPEND | O_CREAT , 0666);
dup2(fd,1);
printf("You can see me\n");
printf("You can see me\n");
printf("You can see me\n");
printf("You can see me\n");
close(fd);
return 0;
}
运行效果:
我们先来看下面的一段代码:
#include
#include
#include
#include
#include
int main()
{
const char* n="Welcome to my blog\n";
fprintf(stdout,n);
write(1,n,strlen(n));
fork();
return 0;
}
运行一下:
咦?怎么重定向了之后多向文件中打印了一个Welcome to my blog?
下面我们来慢慢分析
我们知道在C语言中使用文件操作会有FILE*的文件指针,指向一个FILE类型的结构体:
struct _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 0
int _blksize;
#else
int _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
};
在这个结构体中会有文件缓冲区的存在,就是在FILE结构体内部动态开辟的一块空间
这个文件缓冲区有什么用呢?在我们使用fwrite、fread这些函数时,这是一个IO的过程。IO的次数越多,那么时间浪费越多,效率越低。如果使用一个东西预先存储这些数据,当到达一定规模时统一写入文件,那么IO的次数就会减少,进而效率提升。
因此,缓冲区存在的意义就是通过减少IO次数达到效率上的提升。
那这个缓冲区什么时候与文件进行数据交换呢?这就涉及到缓冲区的刷新机制了:
缓冲区共有四种刷新规则:立即刷新(无缓冲)、行刷新(行缓冲)、满刷新(全缓冲)、强制刷新
立即刷新(无缓冲):所谓立即刷新就是字面意义,每当向缓冲区中写入数据就刷新一次。
行刷新(行缓冲):当在数据中检测到换行符'\n'时刷新,这叫行刷新。
比如向显示器printf("hello world\n");时会执行行刷新。
满刷新(全缓冲):当缓冲区中数据已满时刷新,这叫满刷新。
强制刷新:即由人强制缓冲区执行刷新操作,例如fflush就是一种强制刷新。
不同文件对应刷新规则不同,这是主要是由文件使用性质决定的。
对于标准输入输出流(显示器、键盘)来说,其采用行刷新策略。
对于磁盘文件来说,其采用满刷新策略。
在我们第一次运行代码时并没有重定向,所以默认输出流stdout是显示器文件,其对应的缓冲区刷新机制为行刷新(行缓冲),所以在调用fprintf函数时会立即打印传入的字符串
但是在第二次运行时,我们输出重定向了,重定向文件为test.txt,对于磁盘文件来说,其采用满刷新策略。所以在我们调用fprintf函数时不会立即打印传入的字符串(因为这些字符串还不足以将缓冲区填满),不过调用系统级接口write时会立即写入文件(因为系统级接口中并没有缓冲区),在最后fork函数创进程时,子进程会复制父进程的缓冲区。所以在程序最后结束对缓冲区进行刷新时,会写实拷贝出两个缓冲区,两个缓冲区的内容都要刷新到文件中,就造成了最后的现象
下面我们写一个小demo,简单模拟实现一下C语言中的FILE:
#include
#include
#include
#include
#include
#include
#include
#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_fopen("/a/b/c.txt", "a");
// my_fopen("/a/b/c.txt", "r");
// my_fopen("/a/b/c.txt", "w");
MY_FILE *my_fopen(const char *path, const char *mode)
{
//根据传入的mode参数识别标志位
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 {
//other operator...
//"r+", "w+", "a+"
}
mode_t m = 0666;//创建文件时的权限
int fd = 0;
//判断是否有O_CREAT标志位来决定打开文件的方式
if(flag & O_CREAT) fd = open(path, flag, m);
else fd = open(path, flag);
if(fd < 0) return NULL;//打开失败
//构建MY_FILE对象最后返回
MY_FILE *mf = (MY_FILE*)malloc(sizeof(MY_FILE));
if(mf == NULL)
{
close(fd);
return NULL;
}
//初始化MY_FILE对象
mf->fd = fd;
mf->flags = 0;
mf->flags |= BUFF_LINE;//默认所有文件都进行行缓冲
memset(mf->outputbuffer, '\0',sizeof(mf->outputbuffer));
mf->current = 0;
//返回文件对象
return mf;
}
int my_fflush(MY_FILE *fp)
{
assert(fp);
//将用户缓冲区中的数据,通过系统调用接口write,冲刷给OS
write(fp->fd, fp->outputbuffer, fp->current);
fp->current = 0;//刷新完置0
return 0;
}
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb,MY_FILE *stream)
{
// 缓冲区如果已经满了,就直接写入
if(stream->current == NUM) my_fflush(stream);
//根据缓冲区剩余情况,进行数据拷贝即可
size_t user_size = size * nmemb;//用户要写入的字节数
size_t my_size = NUM - stream->current; //缓冲区剩余空间的字节数
size_t writen = 0;//writen记录实际写入的字节数
if(my_size >= user_size)//缓冲区剩余容量大于等于用户写入的字节数
{
memcpy(stream->outputbuffer+stream->current, ptr, user_size);//直接将用户数据拷贝到缓冲区
//更新计数器字段
stream->current += user_size;
writen = user_size;
}
else//缓冲区剩余容量小于用户写入的字节数
{
memcpy(stream->outputbuffer+stream->current, ptr, my_size);//剩余多少空间就写入多少字节
//更新计数器字段
stream->current += my_size;
writen = my_size;
}
//判断刷新机制,根据不同的刷新机制执行不同的方法
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 if(stream->flags & BUFF_NONE)//无缓冲
{
my_fflush(stream);
}
return writen;
}
int my_fclose(MY_FILE *fp)
{
assert(fp);
//冲刷缓冲区
if(fp->current > 0) my_fflush(fp);
//关闭文件
close(fp->fd);
//释放空间
free(fp);
fp = NULL;
return 0;
}
我们在上期博客中也说到过,文件系统中的file结构体也有自己的缓冲区:
那这个缓冲区和C语言中FILE结构体中缓冲区有什么相似之处呢?
我们来看到下图:
我们可以看到,如果我们作为用户调用c标准库中的fopen、fwrite之类的函数,其会将对应FILE结构体中的缓冲区的数据,利用系统调用接口与进程进行交互。 进程再拿着数据交给文件系统中的file结构体中的缓冲区,这个缓冲区的数据最终会被OS刷新到磁盘中(但是对于OS怎么来刷新缓冲区中的数据我们目前还无法弄清楚,它有其自己对应的刷新机制)。
但是系统提供了一个函数接口用来强制刷新文件系统中的缓冲区:fsync
#include
int fsync(int fd);
该函数可以将fd对应的文件描述符中的缓冲区的内容刷新到磁盘中
所以我们可以在之前模拟实现的my_fflush函数中强制刷新一下文件系统中的缓冲区:
int my_fflush(MY_FILE *fp)
{
assert(fp);
//将用户缓冲区中的数据,通过系统调用接口write,冲刷给OS
write(fp->fd, fp->outputbuffer, fp->current);
fp->current = 0;//刷新完置0
fsync(fp->fd);//强制刷新文件系统的缓冲区到磁盘
return 0;
}
下期不见不散~