目录
一、dup2
二、 引入
三、C语言FILE中的缓冲区
3.1 缓冲区的作用
3.2 缓冲区的刷新机制
3.3 对引入代码现象的解释
3.4 模拟实现C语言中的FILE
四、文件系统中的缓冲区
4.1 fsync
在本期内容正式开始之前,我们先介绍一个上期遗漏的知识点:
一、dup2
我们如果要想用代码实现重定向,先要关闭对应的标准流文件,再使用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<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h> #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<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h> 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中的缓冲区
3.1 缓冲区的作用
我们知道在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 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
};
在这个结构体中会有文件缓冲区的存在,就是在FILE结构体内部动态开辟的一块空间
这个文件缓冲区有什么用呢?在我们使用fwrite、fread这些函数时,这是一个IO的过程。IO的次数越多,那么时间浪费越多,效率越低。如果使用一个东西预先存储这些数据,当到达一定规模时统一写入文件,那么IO的次数就会减少,进而效率提升。
因此,缓冲区存在的意义就是通过减少IO次数达到效率上的提升。
那这个缓冲区什么时候与文件进行数据交换呢?这就涉及到缓冲区的刷新机制了:
3.2 缓冲区的刷新机制
缓冲区共有四种刷新规则:立即刷新(无缓冲)、行刷新(行缓冲)、满刷新(全缓冲)、强制刷新
立即刷新(无缓冲):所谓立即刷新就是字面意义,每当向缓冲区中写入数据就刷新一次。
行刷新(行缓冲):当在数据中检测到换行符'\n'时刷新,这叫行刷新。
比如向显示器printf("hello world\n");时会执行行刷新。
满刷新(全缓冲):当缓冲区中数据已满时刷新,这叫满刷新。
强制刷新:即由人强制缓冲区执行刷新操作,例如fflush就是一种强制刷新。
不同文件对应刷新规则不同,这是主要是由文件使用性质决定的。
对于标准输入输出流(显示器、键盘)来说,其采用行刷新策略。
对于磁盘文件来说,其采用满刷新策略。
3.3 对引入代码现象的解释
在我们第一次运行代码时并没有重定向,所以默认输出流stdout是显示器文件,其对应的缓冲区刷新机制为行刷新(行缓冲),所以在调用fprintf函数时会立即打印传入的字符串
但是在第二次运行时,我们输出重定向了,重定向文件为test.txt,对于磁盘文件来说,其采用满刷新策略。所以在我们调用fprintf函数时不会立即打印传入的字符串(因为这些字符串还不足以将缓冲区填满),不过调用系统级接口write时会立即写入文件(因为系统级接口中并没有缓冲区),在最后fork函数创进程时,子进程会复制父进程的缓冲区。所以在程序最后结束对缓冲区进行刷新时,会写实拷贝出两个缓冲区,两个缓冲区的内容都要刷新到文件中,就造成了最后的现象
3.4 模拟实现C语言中的FILE
下面我们写一个小demo,简单模拟实现一下C语言中的FILE:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <unistd.h>
#include <assert.h>#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,冲刷给OSwrite(fp->fd, fp->outputbuffer, fp->current);fp->current = 0;//刷新完置0return 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
4.1 fsync
#include <unistd.h>
int fsync(int fd);
该函数可以将fd对应的文件描述符中的缓冲区的内容刷新到磁盘中
所以我们可以在之前模拟实现的my_fflush函数中强制刷新一下文件系统中的缓冲区:
int my_fflush(MY_FILE *fp)
{assert(fp);//将用户缓冲区中的数据,通过系统调用接口write,冲刷给OSwrite(fp->fd, fp->outputbuffer, fp->current);fp->current = 0;//刷新完置0fsync(fp->fd);//强制刷新文件系统的缓冲区到磁盘return 0;
}
下期不见不散~