上个月学校考试,进行课程复习,一直没有更新博客,现考试结束,继续保持更新,欢迎大家关注!
目录
- 1 模仿C库自主封装简单的文件接口
- 2 对缓冲区的理解
- 2.1 数据刷新到磁盘的过程分析
- 2.2 如何强制刷新内核
1 模仿C库自主封装简单的文件接口
目的:简单理解C语言是如何封装各个文件操作接口的。
1.接口实现:
#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <assert.h>
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{
//其他模式
}
//2.打开文件
mode_t m = 0666;
int fd = 0;
if(flag & O_CREAT) fd = open(path, flag, m);
else fd = open(path, flag);
if(fd < 0) return NULL;
//3.构建MY_FILE结构体对象
MY_FILE* mf = (MY_FILE*)malloc(sizeof(MY_FILE));
if(mf == NULL)
{
close(fd);
return NULL;
}
//4.初始化MY_FILE对象
mf->fd = fd;
mf->flags = 0;
mf->flags |= BUFF_LINE;
memset(mf->outputbuffer, '\0', sizeof(mf->outputbuffer));
mf->current = 0;
//5.返回打开的文件(MY_FILE)
return mf;
}
int my_fflush(MY_FILE *fp)
{
assert(fp);
write(fp->fd, fp->outputbuffer, fp->current);
fp->current = 0;
return 0;
}
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
//1.缓冲区满了,就直接刷新
if(stream->current == NUM) my_fflush(stream);
//2.缓冲区没满,把数据拷贝进缓冲区
size_t user_size = size * nmemb;
size_t my_size = NUM - stream->current;
size_t writen = 0;//实际写入的字节数
if(my_size >= user_size)
{
memcpy(stream->outputbuffer + stream->current, ptr, user_size);
//3.更新计数器字段
stream->current += user_size;
writen = user_size;
}
else
{
memcpy(stream->outputbuffer + stream->current, 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;
}
//关闭文件的时候,C会帮助我们冲刷缓冲区
int my_fclose(MY_FILE *fp)
{
assert(fp);
//1.冲刷缓冲区
if(fp->current > 0) my_fflush(fp);
//2.关闭文件
close(fp->fd);
//3.释放堆空间
free(fp);
//4.指针NULL
fp = NULL;
return 0;
}
2.测试代码:
#include "mystdio.h"
#include <string.h>
#include <unistd.h>
#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--);
snprintf(buffer, sizeof(buffer), "%s: %d", str, cnt--);
size_t size = my_fwrite(buffer, strlen(buffer), 1, fp);
sleep(1);
printf("当前成功写入:%lu个字节\n", size);
if(cnt % 5 == 0)
{
my_fwrite("\n", strlen("\n"), 1, fp);
}
}
my_fclose(fp);
return 0;
}
注:测试代码分为两种情况:
- 写入数据时不添加\n,写5次手动在缓冲区写入一个\n,来完成一次行刷新。
- 写入数据时添加\n,写一次刷新一次数据。
观察运行结果,思考:
写入时存在缓冲区,先将数据写入到缓冲区中,然后再刷新到显示器,那为什么不直接将数据刷新到显示器呢?为什么要有缓冲区?
——因为可以先将数据存在缓冲区中,不发生刷新,不进行写入,也就是不进行IO,不进行系统调用,所以函数会调用的很快,数据会暂存在缓冲区中。在缓冲区中可以积压多份数据,最后统一刷新。
其本质就是一次IO可以IO多份数据,从而提高IO的的效率。
2 对缓冲区的理解
以前我们所说的的缓冲区,是指用户级别的缓冲区,由语言提供。
下面要理解一个完整的缓冲区——用户层+内核,进而引入强制刷新内核。
2.1 数据刷新到磁盘的过程分析
我们要把数据保存到磁盘文件中,至少要进行3次拷贝,下面进行解析:
当我们每次访问一个文件的时候,其实就是通过进程在访问文件,所以进程拥有对应的tesk_struct
,拥有对应的文件描述符表struct file* fd_array[]
(在files_struct
中),拥有被打开的文件struct file
,并且被打开的文件拥有自己的文件缓冲区。
并且我们平时使用的C语言,存在C标准库libc.a/so
,库当中为我们提供了对应的方法(比如fwrite/fput等),并且提供的这些所有的接口,都拥有FILE*参数,这个FILE结构体对象是在我们使用open打开文件的时候库在内部为我们创建好的,我们可以理解成在库里面或者在自己的进程中(我们理解为放在库中),FILE中存在一个缓冲区,所以我们在使用库提供的接口(fwrite/fput等)时,其实是先将数据存放在这个缓冲区中,所以我们使用的比如fwrite/fput这些接口其实本质是拷贝函数,它们将我们的数据拷贝到了FILE结构体的缓冲区中,然后再结合函数的刷新策略(通过打开文件的类型以及缓冲区是否满了判断),结合OS提供的比如write系统调用接口(write中有文件描述符fd、要写入的文件缓冲区的地址),定期的将数据冲刷到文件缓冲区(前面说的被打开的文件拥有自己的缓冲区)中,所以系统调用接口write的本质也是拷贝函数!
那么C库提供的 “拷贝函数” 与系统调用接口 “拷贝函数” 有什么不同呢?
- C库提供的拷贝函数是从用户——>语言
- 系统调用拷贝函数是从语言——>内核
当数据到达文件缓冲区,OS就要有自己的刷新策略(所以刷新策略不止全缓冲、行缓冲、无缓冲)!OS可以将数据暂存在这个缓冲区中,以时间为单位刷新到磁盘;也可以等缓冲区满了再冲刷到磁盘;也可以判断当内存使用很紧张的时候,将数据刷新到磁盘中;也可以为了提高刷新速度,积累一定量的数据后再一次刷新。(我们平时写word,ctrl+s保存就是手动将保存在缓冲区中的数据刷新到磁盘,方式断电缓冲区中的数据丢失)
实际上OS的刷新策略要比上层用户的刷新策略复杂得多,因为OS要综合考虑的因素很多。但是总体的思想还是一样的——通过一次IO刷新更多的数据来提升刷新速度。
注:OS的刷新策略我们是不可见的。
总结: 要将数据刷新到硬件上至少要有三次拷贝:
- 第一次拷贝:先将数据刷新到语言库的缓冲区上
- 第二次拷贝:通过系统调用将数据拷贝到对应的内核当中
- 第三次拷贝:将数据从内存拷贝到外设
上面所讲的刷新过程图解:
2.2 如何强制刷新内核
例如我们在使用word的时候,ctrl+s强制刷新到外设,防止数据丢失。
所以系统也要提供接口供用户自主将数据刷新到外设。
系统提供的接口:
功能:通过文件描述符fd,将缓冲区数据刷新到外设。
接口:int fsync(int fd);
所以在实现fflush的时候,调用fsync来将数据刷新到外设:
int my_fflush(MY_FILE *fp)
{
assert(fp);
write(fp->fd, fp->outputbuffer, fp->current);
fp->current = 0;
fsync(fp->fd);
return 0;
}
补充理解:
综上理解,当我们调用printf打印例如1245这样的数字时,其实我们打印的是字符1 2 3 4 5,即打印的是字符串,只是这些字符是连在一起的,在我们看来打印的是一串数字。但是在C程序中,12345是一个整数,但是显示的时候却打印成了字符串,所以肯定出现了一次数据格式转化,数据格式转化是printf做的(所以printf叫格式控制),它将内存的数据转换成了字符串数据。
那么printf是如何进行格式控制的呢?
- 先获取对应的变量x
- 定义缓冲区,将x转换成字符串
- 将字符串拷贝到stdout->buffer
- 集合具体的刷新策略刷新显示即可
int my_printf(const char *format, ...)
{
//1. 先获取对应的变量x
//2. 定义缓冲区,将x转换成字符串
//3. 将字符串拷贝到stdout->buffer
//4. 集合具体的刷新策略刷新显示即可
}
再比如scanf是如何进行格式控制的呢?
- 首先获取数据读取到stdin->buffer中
- 对buffer的内容进行格式化,写入到对应的变量当中
写一段伪代码进行解释:
int a, b;
scanf(“%d %d”, &a, &b); //假设输入123 456
其本质就是:
- 将数据读取到缓冲区中:read(0, stdin->buffer, num);
- 本质输入的是123 456 字符串
- 扫描字符串,碰到空格,字符串就被分为2个子串
- 分别对两个字串进行格式转换后写入即可:*ap = atoi(str1); *bp = atoi(str2);
总结:
我们在学习比如C、C++等语言的时候,所说的缓冲区概念都是由该语言提供的。