ToB企服应用市场:ToB评测及商务社交产业平台
标题:
linux文件——用户缓冲区——概念深度探索、IO模仿实现
[打印本页]
作者:
乌市泽哥
时间:
2024-8-24 13:05
标题:
linux文件——用户缓冲区——概念深度探索、IO模仿实现
媒介:
本篇文章主要讲授文件缓冲区。 讲授的方式是通过抛出题目, 然后通过分析题目, 将缓冲区的概念与原理一步一步地讲授。同时, 本节内容在末了一部分还会带友友们模仿实现一下c语言的printf, fprintf接口, 加深友友们对于缓冲区的理解。
ps: 本节内容适合相识linux历程和linux文件重定向的友友们进行观看。
目次
缓冲区概念——使用close引出
exit和用户缓冲区
缓冲区刷新方案
为什么要有用户缓冲区
FILE
fork和缓冲区读写
模仿实现c语言标准库
只包含fd的接口——不带缓冲区
c语言的跨平台性
带缓冲区的c语言接口
fflush
缓冲区概念——使用close引出
讲述缓冲区, 我们需要先看一下下面的接口:
对于上面的接口, 我们可以预测一下——就是打印4行内容, 分别是
hello printf
、
hello fprintf
、
hello fwrite
、
hello write
:
如图,代码运行效果符合我们的预期。对于上面的代码我们知道, 对于c语言的接口, stdoutFILE文件流内里就封装了1号文件描述符。 他们的底层肯定是调用了write函数。
对于上面的函数,我们重定向到文件是这样, 和直接在显示器上面输出是一样的。
但是, 如果加上close(1), 将显示器文件关闭, 那么情况就有些不同了, 下面是有\n的:
下面是没有\n的:
如果没有了\n, 对于c语言的打印接口, 就不能输出了。
如下图, 只输出了系统调用write:
我们知道对于上面printf、fprintf、fwrite来说, 这三个是c语言接口, 他们的底层肯定是封装的write接口。 这就有题目了,
为什么printf、fprintf、fwrite的底层是write, 而write可以大概打印数据, printf、fprintf、fwrite都不能打印数据呢?
友友们如果学习过进度条, 那么就会知道一个缓冲区的概念。 这个缓冲区遇到\n大概缓冲区满了之后就会刷新自己。
一样平常情况下, 我们使用printf、fprintf、fwrite不是直接写向显示器, 而是先写向缓冲区
。 比及遇到\n大概缓冲区满了之后再刷新缓冲区,将数据刷新到缓冲区。
也就是说, 对于上面的这个代码来说, 当程序运行到close(1)前一行,
那么数据就已经写到缓冲区了。 只是这个缓冲区肯定不在操作系统内部, 这个缓冲区不是系统级别的缓冲区
!!!
详细的底层逻辑关系如下:
我们使用的c语言接口的时候, 如果是将数据直接拷贝到了系统级别的缓冲区中, 那么比及close(1)的时候, 这个close(1)就能找到对应的文件struct file, 然后找到对应的系统级别的文件缓冲区, 将缓冲区内容刷新到磁盘内里。 也就是说, 如果c语言接口是将数据只拷贝到系统级别的缓冲区, 那么我们应该也能看到c语言接口的效果, 而事实上并没有。 而write是系统调用接口, 是直接写到系统级别的文件缓冲区, 那么当close(1)的时候, 就能直接将数据刷新到磁盘内里。
我们平时口中的缓冲区, 不是这个系统级别的缓冲区, 而是语言提供的, 这里也就是c语言提供的一个缓冲区, 这个缓冲区是一个用户级别的缓冲区
。 也就是说,
c语言的printf, fprintf等接口是将数据写到语言级别的缓冲区中, 然后比及某一个时机就调用write接口, 将数据刷新到系统级别的缓冲区中,末了再刷新到磁盘中
。
所以, 当我们printf、fprintf等等的时候, 这些数据其实是保存在语言级别的缓冲区, 他们还没有进入系统级别的缓冲区, 当close(1)的时候。1号文件struct file, 系统文件缓冲区都被关掉了。 就无法再向磁盘写入数据了, 也就没有显示效果。
显示器对应的刷新方案是行刷新, 当我们在c语言接口要打印的数据中加上\n的时候, 如下图, 那么即便有close(1), 但是对于用户缓冲区来说, 每一行都会被刷新到系统级别的文件缓冲区。 所以可以打印出来。 用户缓冲区刷新的本质就是将数据通过1 + write写到内核中。
exit和用户缓冲区
我们之前说过exit和_exit, exit是封装的上层接口, 而_exit是系统调用。 两者的区别是_exit退出前不会刷新缓冲区(我们口中的缓冲区, 都是用户级缓冲区), 而exit会刷新缓冲区(我们口中的缓冲区, 都是用户级缓冲区)。 这是由于_exit身为系统调用, 它在用户层的下层, 它根本看不到用户级缓冲区。 也就没办法刷新它, 而exit处在用户层, 它底层肯定是先刷新缓冲区flush, 然后再调用系统调用_exit。
从上面我的叙述中就可以发现, 其实当数据到达内核的时候, 那么这个数据就可以到达硬件了。 ——这也是我们现在认为的, 只要是将数据刷新到了内核, 数据就可以到达硬件了!!!
缓冲区刷新方案
缓冲区刷新题目:——这里的缓冲区思量的是语言层, 也就是用户层缓冲区。
无缓冲: 直接刷新, 不管printf, fprintf等接口打印的数据是什么, 直接刷新。
行缓冲: 不刷新,直到遇到\n再刷新。
全缓冲: 缓冲区满了再刷新, 不满的话, 无论怎样都不刷新。
所以整个的刷新流程如下图:
在向显示器中写入的时候, 采用的是行缓冲。
在向普通文件中写入的时候, 采用的是全缓冲。
为什么要有用户缓冲区
历程退出的时候也会刷新缓冲区, 为什么要有这个缓冲区呢?
首先就是效率层面——这里可以使用一个例子来说明这件事, 就好比我们给朋友送东西, 如果没有快递公司, 我们就必须亲自去送东西给朋友, 但是现在有了快递公司, 快递公司就可以帮助我们去送快递, 我们节流的这些时间就可以去做自己的事情。——这内里的快递公司就是缓冲区, 我们就是用户。 所以, 缓冲区解决的是谁的效率题目?——其实就是用户的效率题目。 (也就是使用c语言的人的效率题目)
其次是语言设计层面——拥有缓冲区的目的是
为了配合c语言接口:printf,fprintf的输出格式化。
——这里我们需要想一个题目, 就是我们在显示器上面打印的789, 这样的数字, 打印的是字符呢? 打印的还是数字呢? ——答案是打印的字符789。 也就是说, 将789转化成字符7, 字符8, 字符9, 然后再打印到显示器上面。 当数据打印的时候, 先将数据打印到用户缓冲区。 当某个时机的时候, 就将用户缓冲区的数据打印到硬件内里。 由于这种数据进入, 数据流出, 很像一条河流, 所以用户缓冲区也被叫做流。
FILE
请问, FILE是属于用户呢? 还是属于操作系统呢? 答案是用户, 语言都是属于用户的。
知道这个之后, 我们就可以知道, 对于fopen来说
如果使用fopen打开了一个文件, 那么就会获取这个文件的文件描述符fd。 然后再malloc一块内存空间保存在FILE, 这个fd和malloc的内存空间都保存在FILE内里!!也就是下面这张图:
fork和缓冲区读写
现在,我这里有另外一个关于fork函数的题目。 我们看下面这串代码:
我们天生程序后, 将数据重定向到文件中。 如果按照我们以往的经验的话, 这里我们预测会打印四行内容, 分别是
hello printf、hello fprintf、hello fwrite、hello write
。
但是, 我们实际运行的效果如下:
如上图, 我们可以发现c语言接口的打印都被打印了两次, 只有write系统调用被打印了一次。
这是由于当使用重定向, 重定向到了文件之后, 缓冲区的刷新方案就变成了全缓冲, 遇到\n不再刷新, 而是等缓冲区被写满之后才刷新。
那么我们下载来证明一下重定向到文件是全缓冲的
首先我们写下面这个程序;
这个程序如果按照我们的猜想。 前三秒不会打印任何数据, 这些数据都会被打印到了缓冲区之中。 当三秒过后, write会被打印。然后再五秒所有数据才会全部被打印。
我们先打开监视窗口进行观察:
然后运行这个程序, 就会得到:
那么这是为什么呢?——首先我们知道, 当我们三个c语言接口执行完的时候, 数据都被写到了缓冲区中。 而缓冲区也是在历程内里的, 所以
当fork创建子历程后, 再刷新缓冲区, 而刷新缓冲区就相当于数据的修改, 需要进行写时拷贝
。 所以就有两份缓冲区, 两个缓冲区的数据都被刷新后就产生了上面的征象!!!
模仿实现c语言标准库
为了模仿实现c语言的标准库, 我们创建三个文件。 这里的_my_func.h就相当于stdio.h这样的标准库, 而_my_func.c就是用来模仿实现c标准库接口。
我们打开_my_func.h, 首先做好准备工作。 先让该头文件不可重复包含, 也就是下面的
ifdef, define, endif
。 然后再包含一个头文件string.h。 我们先来简单的实现以下三个函数,
_fopen, _fwrite, _fclose
。
只包含fd的接口——不带缓冲区
下面是代码:
#include<string.h>
//定义缓冲区最大长度
#define SIZE 1024
//flag用来表示刷新方案
#define FLUSH_NOw 0
#define FLUSH_LINE 1
#define FLUSH_ALL 2
typedef struct IO_FILE
{
int fileno; //文件fd
int flag;
//输入缓冲区
//char inbuffer[SIZE];
//int in_pos;
//输出缓冲区
char outbuffer[SIZE];
int out_pos;
}_FILE;
_FILE* _fopen(const char* filename, const char* mode);
int _fwrite(_FILE* fp, const char* s, int len);
void _fclose(_FILE* fp);
void _fflush(_FILE* fp);
#endif
复制代码
然后我们定义FILE范例的结构体。 但是为了区分标准库。 我们这里改成_FILE(以后的函数等等为了区分都会加上"_")。
对于这个结构体内里的内容, 根据我们前面讲到的只是可以知道, 这内里肯定有两个字段一个是文件描述符fd, 一个是文件缓冲区。 这里我们先将fd包含进来。如下图:
然后我们就可以先简单的实现一下fopen, fwrite, fclose函数了。
fopen
如下为代码:
_FILE* _fopen(const char* filename, const char* mode)
{
//先打开文件
//open函数打开文件, 返回文件描述符。第二个参数是打开方式
//先判断打开文件的方式。确定第二个参数
int f = 0;
int fd = -1; //一开始令fd 为-1
if (strcmp(mode, "w") == 0) f = (O_CREAT|O_WRONLY|O_TRUNC);
else if (strcmp(mode, "a") == 0) f = (O_CREAT|O_WRONLY|O_APPEND);
else if (strcmp(mode, "r") == 0) f = (O_RDONLY);
else return NULL;
//打开文件, 默认权限是FILEMODE
fd = open(filename, f, FILEMODE);
//如果fd是-1, 那么直接return。 否则就是正常打开文件。 正常进行操作
if (fd == -1) return NULL;
//else
_FILE* fp = (_FILE*)malloc(sizeof(_FILE));
if (fp == NULL) return NULL;//如果fp == NULL说明空间不够了。
//else
fp->fileno = fd;//让将fd赋值给fp的fd。
return fp;
}
复制代码
fwrite
如下为代码:
int _fwrite(_FILE* fp, const char* s, int len)
{
//fd, 要写入的字符串, 字节长度
return write(fp->fileno, s, len);
}
复制代码
fclose
如下为代码:
//关闭文件
void _fclose(_FILE* fp)
{
if (fp == NULL) return;
close(fp->fileno);
free(fp);
}
复制代码
我们自己实现一串代码进行测试, 打开的方式是清空写:
运行效果如下:
其实从上面的代码我们就能看到封装的好处。 有了fopen, fwrite这些封装之后, 以后再向文件中写内容, 我们想要修改写入的方式, 就不需要再使用O_RONLY、O_WRONLY这些标志了, 直接使用w, a, r, 然后程序就会自动帮助我们判断怎样打开文件!!!
c语言的跨平台性
我们在上面实现的接口是符合linux环境的接口。 如果我们再windows下, 想实现同样的接口, 那么为了可以大概在windows正常工作,就要实现一份适合在windows下面跑的代码。 同样的macos也是一样的。 这三份代码我们可以使用if else if, endif条件编译。 如果在linux下面就是用linux下面的代码。 如果在windows下面就是用windows下面的代码。 如果在macos下面, 就是用macos下的代码。 这个就叫做c语言的跨平台性!!!
带缓冲区的c语言接口
我们在我们的接口内里加入缓冲区, 下图是引入缓冲区的方法。图中的in_pos和out_pos是为了标志缓冲区的使用情况。 它指向缓冲区已经使用的末了一个位置。 这个位置的左边是已经使用的, 右边是没有使用的。而且由于我们此次模仿实现的接口用不到输入缓冲区, 所以将输入缓冲区解释掉。
接下来我们修改我们的代码:
fopen
首先修改fopen, 要初始化缓冲区, 那么就是将标志缓冲区使用情况的out_pos置为0. 就代表缓冲区没有被使用过。 并且我们要初始化缓冲区的刷新方案。也就是_FILE结构体内里的flag。 我们这里默认初始化为行刷新, 下图的黄色代码就是初始化缓冲区。(只需要在原本的fopen下面添加黄色框框的代码就可以实现)
fwrite
如下图是写方法的含有缓冲区的实现方式。
就是先将数据拷贝到缓冲区中, 然后判断刷新方式, 是直接刷新, 行刷新还是缓冲区满才刷新。刷新后标志位out_pos置为0.
下面是代码:
//写方法
int _fwrite(_FILE* fp, const char* s, int len)
{
//使用memcpy将数据拷贝到输出缓冲区中。
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;
}
//遇到反斜杠n刷新
else if ((fp->flag == FLUSH_LINE) && (fp->outbuffer[fp->out_pos - 1] == ' \n'))
{
//只要遇到反斜杠n就行刷新
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
else if (fp->flag == FLUSH_ALL && fp->out_pos == SIZE)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
else return len;
return len;
}
复制代码
fclose
关闭文件和之前没有什么区别
测试:
我们可以使用下面这个监控脚本, 以及我们要测试的代码, 来测试一下我们模仿实现的是否精确:
#include"_my_func.h"
#include<unistd.h>
int main()
{
_FILE* fp = _fopen("mytest.txt", "a");
if (fp == NULL) return 1;
int cnt = 10;
const char* mes = "hello linux\n";
while (cnt)
{
_fwrite(fp, mes, strlen(mes));
sleep(1);
cnt--;
}
_fflush(fp);
_fclose(fp);
return 0;
}
复制代码
运行效果如下, 由于我们是以行刷新, 并且每一秒都会追加一行。 所以会出现下图的情况。 验证效果我们的代码是精确的。
fflush
fflush这里我们也要模仿一下, 博主模仿是为了应对全刷新的情况(并不是说fflush只是为了应对全刷新而存在的)。由于我们使用全刷新的时候, 写到缓冲区的内容不轻易被刷新出来。就如同下图我们已经运行了程序, 但是仍然刷新不出东西, 也就是没有向文件写入:
那么实现fflush, 我们要怎么实现呢?代码如下图:
代码:
void _fflush(_FILE* fp)
{
if (fp->out_pos > 0)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
//关闭文件
E>void _fclose(_FILE* fp)
{
if (fp == NULL) return;
_fflush(fp);//因为关闭文件要将缓冲区中的东西都放出来。
close(fp->fileno);
free(fp);
}
复制代码
我们在运行就能在10秒胡历程退出的时候打出来了。下图就是测试——前十秒没有打印任何东西, 但是后面打印了一串数据。 这是由于历程退出的时候刷新了缓冲区。
以上就是本节的全部内容。 下面是博主的条记:
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4