1.文件缓冲区的引出
如上现象,在学习完文件缓冲区然后即可解释
2.认识缓冲区
缓冲区的本质就是显存当中的一部份,这么是谁向显存申请的?是属于谁的?为何要存在缓冲区呢?
道理是这般,在之前的学习过程中我们也确实晓得操作系统中存在缓冲区的概念,并且我们在编撰代码时,并未执行将数据拷贝到缓冲区的代码呀,操作系统是如何将数据拷贝到缓冲区的呢?
–与其将fwrite函数理解成是写入到文件当中的函数,不如将其理解为fwrite是拷贝函数,将数据从进程拷贝到"缓冲区"或则外设当中
3.缓冲区的刷新策略
假如此时存在一些数据须要写入到外设(c盘文件)当中,是一次性写入效率高还是多次少量写入效率高?
答:其实是一次性写入效率高,数据写入到外设须要对外设进行恳求,对于CPU来说写入是十分高效的,并且对于外设响应CPU恳求特别低效,所以一次性写入就只须要恳求一次,而多次分批写势必效率更低
缓冲区一定会结合具体的设备来订制自己的刷新策略,一般有以下3种刷新策略:
立刻刷新–无缓冲行刷新–行缓冲缓冲区满–全缓冲
一般针对c盘文件而言,采用的是全缓冲(最高效)
对于显示器而言,采用的是行缓冲,由于显示器是给用户显示的。
若是全缓冲,一次性将数据全部刷新下来,用户无法阅读数据,而无缓冲又太低效了,所以采用的行缓冲(便捷用户)
同样的刷新策略也存在特殊情况:
当用户进行强制刷新时(调用fflush插口)进程退出–一般情况下都须要进行缓冲区刷新4.解释现象
最开始我们引出文件缓冲区是通过fork函数后将运行结果重定向到文件当中发觉调用C插口的数据会复印两遍,而调用系统插口的数据只复印一遍,该现象与缓冲区存在哪些关系呢?
首先,我们要明晰一点该现象一定与缓冲区有关linux操作系统版本,还有一点就是缓冲区一定不存在于内核当中,不然系统插口也应当复印两遍
我们之前所谈论的所有缓冲区都是用户级语言层面给我们提供的缓冲区
该缓冲区存在于stdout,stderr,stdin当中,这两者都被文件表针所指向(FILE*)而FILE结构体是经过C语言封装之后,其中包含fd(文件描述符)和一个缓冲区
所有我们想要立刻获取数据就要强制刷新(fflush(文件表针)),在关掉文件时也须要传入文件表针(fclose(文件表针))
4.1C语言库中的源代码
从源码来看,可以看出FILE结构体当中除了封装文件描述符,文件的打开方法还封装了缓冲区
基于以上认识linux模糊查询文件个数,我们就可以解释该现象了
在代码结束之前fork创建子进程
假如未进行重定向,只复印4行信息
stdout默认采用的是行刷新,
在进程fork之前就早已将数据进行复印输出到外设(显示器)上linux 关机命令,所以在FILE内部(或则称为进程内部)不存在对应的数据假如进行了重定向,写入文件不再是显示器而是普通文件,采用的刷新策略就是全缓冲
而之前的3条C复印函数其实结尾带上n,
然而**并不足以将stdout缓冲区写满,**这么数据也就不会被刷新
此时再执行fork函数,stdout是属于父进程的linux模糊查询文件个数,创建子进程,子进程会对父进程的代码和数据进行拷贝
fork以后紧接着就是退出,谁先退出就一定会进行缓冲区的刷新(也就是更改)
更改会造成写时拷贝,致使数据会显示两份write为何没有显示两份呢?
由于前面的过程都与write无关,write没有FILE结构体而是采用的文件描述符fd,也就不存在C提供的缓冲区啦!5.深刻理解缓冲区
缓冲区究竟应当如何理解呢?我们通过尝试自己将文件描述符,缓冲区封装成FILE来实现对缓冲区的深刻理解
5.1功能需求实现
先将文件描述符,缓冲区封装上去,再实现文件操作的基本功能
写入数据:_fwrite刷新数据:_fflush关掉文件:_fclose打开文件:_fopen
暂时就先实现这几个简单模块,主要还是针对缓冲区的理解5.2基本框架搭建
[hx@hx my_stdio]$ ll
total 4
-rw-rw-r-- 1 hx hx 78 Jun 7 18:33 Makefile
-rw-rw-r-- 1 hx hx 0 Jun 7 18:33 myStdio.c
-rw-rw-r-- 1 hx hx 0 Jun 7 18:33 myStdio.h
[hx@hx my_stdio]$ cat Makefile
main:main.c myStdio.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f main
5.3封装成_FILE
// 在myStdio.h文件当中定义
1 #pragma once
2
3 #include <stdio.h>
4
5 #define SIZE 1024
6
7 typedef struct _FILE
8 {
9 int flags; // 刷新方式:(无/行/全缓冲)
10 int fileno; // 文件描述符
11 int capacity; // buffer的总容量
12 int size; // buffer当前的使用量
13 char buffer[SIZE]; // SIZE字节的缓冲区
14 } _FILE;
15
16 // 文件打开需要文件打开路径和权限
17 _FILE * fopen_(const char* path_name,const char *mode);
18
19 // ptr是要写入文件的数据 num是数据字节数 _FILE* 文件指针
20 void fwrite_(const char* ptr,int num,_FILE* fp);
21
22 //传文件指针关闭文件
23 void fclose_(_FILE* fp);
24
25 //传文件指针强制刷新缓冲区
26 void fflush_(_FILE* fp);
5.4fopen_的实现
[hx@hx my_stdio]$ cat myStdio.c
#include "myStdio.h"
_FILE* fopen_(const char * path_name,const char *mode)
{
int flags = 0;
int defaultMode = 0666;
// 以只读的方式打开文件
if(strcmp(mode,"r") == 0)
{
flags |= O_RDONLY;
}
// 以只写的方式打开文件
else if(strcmp(mode,"w") == 0)
{
flags |= (O_WRONLY | O_CREAT | O_TRUNC);
}
else if(strcmp(mode,"a") == 0)
{
flags |= (O_WRONLY | O_CREAT | O_APPEND);
}
else
{
// 目前就简单实现这3种文件操作
}
// 文件描述符
int fd = 0;
// 以只读的方式打开文件
if(flags & O_RDONLY) fd = open(path_name,flags);
// 写入文件 若文件不存在 需要以defaultMode的权限创建
else fd = open(path_name,flags,defaultMode);
// 文件打开失败
if(fd < 0)
{
// 记录下错误信息
const char* err = strerror(errno);
// 将错误信息写入到标准错误(2/stderr)当中
write(2,err,strlen(err));
// 这也就是为啥文件打开失败要返回NULL(C语言底层就是这样实现的)
return NULL;
}
// 下面就是文件打开成功
// 在堆上申请空间
_FILE * fp = (_FILE*)malloc(sizeof(_FILE));
// 暴力检查 未申请成功直接报断言错误
assert(fp);
// 默认设置为行刷新
fp->flags = SYNC_LINE;
// 文件描述符置为fd
fp->fileno = fd;
fp->capacity = SIZE;
fp->size = 0;
// 将缓冲区数据置为0 保证后续往缓冲区写入数据正确
memset(fp->buffer,0,SIZE);
// 这也就是为啥打开文件要返回FILE*的指针(C语言底层的实现方式)
return fp;
}
5.5头文件的引用刷新方法的定义
[hx@hx my_stdio]$ cat myStdio.h
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024
// 无缓冲
#define SYNC_NOW 1
// 行缓冲
#define SYNC_LINE 2
// 全缓冲
#define SYNC_FULL 4
5.6fwrite_的实现
void fwrite_(const void * ptr,int num,_FILE *fp)
{
// 将数据写入到缓冲区
// 这里的fp->buffer+fp->size 若缓冲区当中存在数据 往后追加
// 这里不考虑缓冲区溢出的问题
memcpy(fp->buffer+fp->size,ptr,num);
// 缓冲区数据增加num个字节
fp->size += num;
// 判断刷新方式
// 无刷新
if(fp->flags & SYNC_NOW)
{
// 将缓冲区数据写入文件
write(fp->fileno,fp->buffer,fp->size);
// 将缓冲区置为0 惰性清空缓冲区
fp->size = 0;
}
// 全刷新
else if(fp->flags & SYNC_FULL)
{
//当缓冲区满了才刷新
if(fp->size == fp->capacity)
{
write(fp->fileno,fp->buffer,fp->size);
fp->size = 0;
}
}
// 行缓冲
else if(fp->flags & SYNC_LINE)
{
//当最后1个字符为n时,刷新数据
//这里不考虑 "abcdnefg" 这种情况
if(fp->buffer[fp->size-1] == 'n')
{
write(fp->fileno,fp->buffer,fp->size);
fp->size = 0;
}
}
else
{
// 不执行任何操作
}
}
5.7fclose_和fflush_的实现
void fflush_(_FILE *fp)
{
//若缓冲区内存在数据 将缓冲区的数据写入对应的文件描述符(可能是磁盘文件,也可能是显示器)
if(fp->size > 0)
write(fp->fileno,fp->buffer,fp->size);
}
void fclose_(_FILE *fp)
{
// 文件关闭前要进行数据的强制刷新
fflush_(fp);
// 关闭对应的文件描述符
// 文件描述指向的就是文件(关闭文件)
close(fp->fileno);
}
5.8实例测试1.情况1
2.情况2
3.情况3
6.理解文件刷新后的整个过程
用户在往文件当中写入"hellolinuxn",先调用的C语言插口fwrite,fwrite会1将数据先写入到C语言封装的FILE缓冲区当中,再采取对应的刷新策略进过write插口(底层插口)依据文件描述符将数据拷贝到内核缓冲区当中,最后由OS定期刷到外设(c盘)当中。
数据要写入到外设当中要经历3次拷贝,第一次拷贝到C语言的缓冲区当中,第二次拷贝到内核缓冲区当中,第三次拷贝到外设当中
(所以fwrite/write插口的实质虽然就是拷贝函数)
如何证明该过程呢?–无法证明,并且可以看见插口
用户调用fwrite将数据交到C语言的缓冲区当中,C语言调用操作系统底层的write插口将数据交给内核缓冲区,假如在这个过程当中OS宕机了如何办?
操作系统宕机也就意味着缓冲在内核缓冲区的数据还未刷新到外设当中,这么都会导致数据遗失,假如用户对数据遗失0容忍如何办(假定用户是农行机构,数据遗失影响重大),那该如何办?
操作系统当中存在插口fsync–强制刷新
7.对强制刷新的深刻理解
[hx@hx my_stdio]$ man 2 fsync
FSYNC(2) Linux Programmer's Manual FSYNC(2)
NAME
fsync, fdatasync - synchronize a file's in-core state with storage device
SYNOPSIS
#include
int fsync(int fd);
调用该插口告知操作系统别在根据自己的刷新策略刷新数据,只要领到数据就立刻刷新到外设当中
7.1实例
在fflush_当中强制刷新插口fsync才是真正的强制刷新fwrite只能算拷贝
void fflush_(_FILE *fp)
{
//若缓冲区内存在数据 将缓冲区的数据写入对应的文件描述符(可能是磁盘文件,也可能是显示器)
if(fp->size > 0)
write(fp->fileno,fp->buffer,fp->size);
// 强制要求操作系统对外设进行刷新
fsync(fp->fileno);
// 刷新完 将size置为0 表示此时缓冲区内无数据
fp->size = 0;
}