W...Y的主页 😊
代码仓库分享 💕
前言:我们已经复习了C语言中的接口,并且学习了许多文件系统调用,了解了文件描述符以及重定向。今天我们继续学习文件缓冲区的相关内容。
缓冲区
在学习C语言时,我们经常会遇到问题,如果程序没有正确地处理输入缓冲区,可能会导致用户输入的数据没有被完全读取或处理。所以我们要考虑到缓冲区的存在。但是我们却没有真正理解缓冲区是什么,为什么要有缓冲区的存在?
简单来说缓冲区就是一块内存区域,用来存储数据的。为什么要有缓冲区呢?我们先来说一个故事。往前推上30年,那时候应该没有快递或者快递行业还没有普及。当我想给某人一个东西但距离太远时,我们就要驱车前往他所在的城市。这样一趟又费时又费事。现在快递行业普遍,如果再次遇到同样的事情我们的首选肯定是寄快递发送,这样我们的路程可能从几千公里变成了几百米。这样我们既省时又省心。
而快递站就类似缓冲区,我们要记的快递就是数据,这样做大大提高了使用者的效率。而且快递驿站不可能一次只寄你一个人的快递,而是将很多件快递聚集一起一并发出。而缓冲区就是一次将很多数据一并发出,这样做提高了整体的效率。使用空间来换取时间上的效率!!!
在语言层面上,他们一般都自带缓冲区。而调用系统调用是有空间和时间的成本的。所以我们在用户的层面上使用printf、fputs、fwrite都不是直接写到文件里面的,而是先写入到缓冲区中再调用系统调用将缓存区的内容写到磁盘的文件中。
但是操作系统中也是有缓冲区的,因为操作系统中自己有一套缓冲区刷新策略,所以我们不考虑操作系统中的缓冲区,我们默认将文件从语言层面的缓冲区刷入操作系统就写入磁盘了!!!
缓冲区的刷新方式
对于不同的缓冲区,其都有不同的刷新策略,在语言层面有三种刷新策略:
1.无刷新、无缓冲
2.行刷新(一般是往显示器中进行写入操作)
3.全刷新、全刷新(一般是普通文件进行刷新),将缓冲区写满进行刷新。
还有两种特殊情况:
1.强制刷新
2.进程退出刷新
当我们在使用fwrite或fputs函数会发现接口中都有file*指针:
这些C语言接口中都是file*指针,在系统文件博客中我们说过file*指向的是一个file的结构体,里面有文件描述符等待,那肯定也会有一个缓冲区,所以我们使用fwrite或fputs时只是将内容写入缓冲区中。这样fwrite、fputs函数的效率会大大提升。
FILE结构体源代码:】
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
在/usr/include/libio.h
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
};
缓冲区存在代码验证
直接显示代码进行验证:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
const char* s1 = "hello write\n";
write(1,s1,strlen(s1));
const char* s2 = "hello fwrite\n";
fwrite(s2,strlen(s2),1,stdout);
const char* s3 = "hello fprintf\n";
fprintf(stdout,"%s",s3);
//fork();
return 0;
}
刚开始我们在代码中没有添加子进程,只是使用一个系统接口write和两个C语言接口fwrite和fprintf往显示器上打印,没有问题,使用重定向往文件中写入也是三行内容没有任何问题。
然后我们使用fork()创建子进程后在向文件中重定向写入,我们可以看到结果:
我们正常运行程序打印三行是没有问题的,但是重定向后再文件中写入却是五行,并且write只有一行其余都是两行。证明write函数只调用了一次,fwrite与fprintf函数都调用了两次。这能说明说明问题呢?
如果向显示器打印是行刷新,无论是否有缓冲区,只要有\n就会进行刷新到显示器上。而向文件中重定向是全刷新,系统调用接口没问题只有一个,而C接口中的内容会先输入到缓冲区中,因为fork会创建出子进程,所以当父进程已经把内容写入缓冲区后再创建子进程,子进程会继承父进程的内容包括缓冲区,而fork后就是结束进程,会刷新缓冲区到操作系统中,所以父子进程的缓冲区都会刷新,从而有两份代码!!
我们就可以证明C语言自带缓冲区。而且缓冲区要支持输入输出的格式化操作。
仿写缓冲区
我们直接通过封装系统调用来仿写一个C语言的缓冲区代码:
mybuffer.h
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#include <string.h>
#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
typedef struct IO_FILE{
int fileno;
int flag;
//char inbuffer[SIZE];
//int in_pos;
char outbuffer[SIZE]; // 用一下这个
int out_pos;
}_FILE;
_FILE * _fopen(const char*filename, const char *flag);
int _fwrite(_FILE *fp, const char *s, int len);
void _fclose(_FILE *fp);
#endif
mybuffer.c
#include "mybuffer.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#define FILE_MODE 0666
// "w", "a", "r"
_FILE * _fopen(const char*filename, const char *flag)
{
assert(filename);
assert(flag);
int f = 0;
int fd = -1;
if(strcmp(flag, "w") == 0) {
f = (O_CREAT|O_WRONLY|O_TRUNC);
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "a") == 0) {
f = (O_CREAT|O_WRONLY|O_APPEND);
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "r") == 0) {
f = O_RDONLY;
fd = open(filename, f);
}
else
return NULL;
if(fd == -1) return NULL;
_FILE *fp = (_FILE*)malloc(sizeof(_FILE));
if(fp == NULL) return NULL;
fp->fileno = fd;
//fp->flag = FLUSH_LINE;
fp->flag = FLUSH_ALL;
fp->out_pos = 0;
return fp;
}
int _fwrite(_FILE *fp, const char *s, int len)
{
// "abcd\n"
memcpy(&fp->outbuffer[fp->out_pos], s, len); // 没有做异常处理, 也不考虑局部问题
fp->out_pos += len;
if(fp->flag&FLUSH_NOW)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
else if(fp->flag&FLUSH_LINE)
{
if(fp->outbuffer[fp->out_pos-1] == '\n'){ // 不考虑其他情况
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_ALL)
{
if(fp->out_pos == SIZE){
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
return len;
}
void _fflush(_FILE *fp)
{
if(fp->out_pos > 0){
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
void _fclose(_FILE *fp)
{
if(fp == NULL) return;
_fflush(fp);
close(fp->fileno);
free(fp);
}
main.c
#include "mybuffer.h"
#include <unistd.h>
#define myfile "test.txt"
int main()
{
_FILE *fp = _fopen(myfile, "a");
if(fp == NULL) return 1;
const char *msg = "hello world\n";
int cnt = 10;
while(cnt){
_fwrite(fp, msg, strlen(msg));
// fflush(fp);
sleep(1);
cnt--;
}
_fclose(fp);
return 0;
}
main.c是用来测试的代码,如果有别的代码也可以进行替换使用。
以上就是这次的全部内容,我们将已经打开的文件基本已经讲述完毕,下一篇博客我们来系统说说没有被打开的文件,感谢大家观看。