目录
一、C语言缓冲区
1.1 什么是缓冲区
缓冲区本质是一块C语言提供的内存空间,这块内存区用来缓存待处理的数据。
缓冲区的刷新策略:
- 立即刷新。(IO次数多,效率低)
- 行刷新 (\n)。
- 满刷新(全缓冲),即写满缓冲区再刷新。(效率最高)
但是还有一些特殊的刷新策略:
- 用户强制刷新( fflush )
- 进程退出
对于不同文件有着不同的刷新策略
显示器对应的刷新策略为行缓冲 (为了兼顾用户体验)
而磁盘文件倾向为全缓冲的刷新策略。
其实所有的设备都倾向于全缓冲,因为全缓冲这种刷新策略可以减少IO的次数,也就是减少外设的访问次数,这种方式效率是最高的。
1.2 缓冲区刷新
关于为什么上篇文章中实现重定向功能,最后要使用fflush进行刷新缓冲区这个问题我们进行讲解,我们先来看看下面这段代码,分析了下面这段代码自然就理解了这个没有解决的问题 了。
但是,如果使用重定向操作,奇怪的现象就出现了。
C语言提供的打印函数都执行了两次,而系统调用(write)就输出了一次。
这必然和上面我们所说的缓冲区对显示器和磁盘文件的刷新策略不同导致的。
具体原因解释如下:
1.如果我们向显示器打印,则刷新策略则是行刷新,那么最后执行fork时——函数已经执行结束,内容已经被刷新至系统。(此时 fork 无意义)
2.如果将该程序进行重定向至文件进行打印,则让原本向显示器进行的行刷新变为了向磁盘文件的满刷新,此时\n无意义。
执行到fork时,函数执行结束,数据仍存在于对应进程的C标准库缓冲区中,没有被刷新至系统。
3.此时缓冲区的数据属于父进程的数据,fork创建子进程,子进程暂时使用父进程内的数据。
4.当执行至return 0时,父子进程准备相继退出时进行缓冲区内容的刷新,因为刷新数据属于一种写入操作,所以发生写时拷贝,父子进程相继将缓冲区中的数据刷新至系统,系统再输入至磁盘文件中。
其实很好证明我们上面的观点,我们只要在fork之前进行fflush,刷新缓冲区,就可以做到无论输出到文件还是显示器,结果都一样。
结果如下:
此时又引申一个问题,我们fflush函数中只是传入了stdout,就刷新了缓冲区?
原因如下:
在文件 /usr/include/libio.h 中,我们可以找到 FILE 结构体的定义
1.3 问题解释
再昨天讲述输出重定向的原理时,留下的一个问题就是什么一定要在关闭文件前进行进行缓冲区的刷新。
有了上面的讲解其实这个问题也就很好解释了。
因为我们关闭了 stdout 对应的文件描述符1,此时log.txt对应的文件描述符 fd 就是1。然后我们调用printf 向 stdout( fd==1 ) 的缓冲区中存放数据,如果不调用fflush刷新stdout的缓冲区,则该文件直接被close关闭,缓冲区中的数据也就丢失了。
1.4 stdout与stderr
首先我们来看一段代码
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
myperror("open");
return 1;
}
//stdout -> 1
printf("hello printf 1\n");
fprintf(stdout,"hello fprintf 1\n");
// stderr -> 2
perror("hello perror 2"); //stderr
const char *s1 = "hello write 1\n";
write(1, s1, strlen(s1));
const char *s2 = "hello write 2\n";
write(2, s2, strlen(s2));
// cout -> 1
std::cout << "hello cout 1" << std::endl;
// cerr -> 2
std::cerr << "hello cerr 2" << std::endl;
return 0;
}
执行结果如下:
然后我们进行重定向(就会出现差别了):
进行重定向时,默认将fd为1处的指向进行了覆盖,在C语言中就是覆盖了stdout。
本质上,1和2对应的都是显示器文件,但是他们两个是不同的,可以认为stdout和stderr是同一个显示器文件被打开了两次。
这样的好处就在于,我们可以将stdout的内容存放至运行日志,将stderr的内容存放至错误日志,有利于程序的排错与测试。向下图这样:
当然我们可以将stdout、stderr的内容都重定向到一个文本中。
这里的原理是,结合文件描述中dup2的理解,先将fd为1处的指向改为指向log.txt,然后取出1处的数据再放入到fd为2处,这样fd为1、fd为2处都指向了log.txt。本质其实还是重定向。
1.5 perror的实现
perror 不同于printf,perror是向2号文件描述符写入,printf是向1号文件描述符写入。
perror 本质就是在读取程序中的 errno 的值,然后打印处对应的错误信息。
我们可以引入头文件<errno.h>来修改程序的错误码,让perror打印对应的错误信息,这就是perror的原理。
接下来就模拟实现一下
我们要使用strerror函数,这个函数的功能就是输入程序的错误码从而打印对应的错误信息。
实现与结果如下:
二、模拟实现缓冲区
2.1 接口展示
为了加深我们对C标准库中缓冲区的理解,我们打算模拟实现一下缓冲区。
接下来是我们打算实现的接口以及测试的方式:
//缓冲区的大小
#define NUM 1024
typedef struct MyFILE {
int fd;
char buffer[NUM];
int end;
}MyFILE;
MyFILE* _fopen(const char* pathname, const char* mode)
{}
void _fputs(const char* message, MyFILE* fp)
{}
void _fflush(MyFILE* fp)
{}
void _fclose(MyFILE* fp)
{}
int main()
{
MyFILE* fp = _fopen("log.txt", "w");
if (fp == NULL)
{
printf("open file error");
return 0;
}
_fputs("hello world1\n", fp);
_fputs("hello world2\n", fp);
_fputs("hello world3\n", fp);
_fflush(fp);
_fclose(fp);
return 0;
}
我们在主函数中的测试代码其功能为:
使用我们的_fopen函数以写的方式打开文件log.txt,用MyFILE指针接受其返回值,用fputs往fp指向的文件中输入信息,因为是往磁盘文件进行写入文件,所以我们使用_fflush刷新缓冲区,将内容输入至内核中,再由操作系统写入至log.txt中,最后使用_fclose关闭文件。
2.2 函数实现
1._fopen
判断参数正确性后,接着我们要判断用户是以什么方式打开文件(使用strcmp进行判断)。
然后调用open函数打开文件,传入对应的选项。如果文件打开成功,申请MyFILE结构体,返回该结构体的地址即可后序进行访问文件。
MyFILE* _fopen(const char* pathname, const char* mode)
{
assert(pathname && mode);
MyFILE* fp=NULL;
//因为stdin的fd为0.
int fd = -1;
if (strcmp(mode, "r") == 0)
{
fd = open(pathname,O_RDONLY);
}
else if (strcmp(mode, "w") == 0)
{
//打开文件,传入3个选项:只读、创建、清空,设置权限为0666
int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
}
else if (strcmp(mode, "a") == 0)
{
fd = open(pathname, O_WRONLY | O_APPEND | O_CREAT, 0666);
}
if (fd >= 0)
{
//申请一个FILE结构体
fp = (MyFILE*)calloc(1, sizeof(MyFILE));
fp->fd = fd;
}
return fp;
}
2._fputs
判断参数正确性后,我们将数据拷贝到我们的缓冲区中,并调整对应 fp 中的数据个数(end)。
注意:向C语言默认打开的3个文件流(stdin、stdout、stderr)中进行打印时,要进行特殊的缓冲区刷新处理。比如向显示器中打印时刷新策略会变为行刷新,例如语句:_fputs(stdout,"hello\n");
void _fputs(const char* message, MyFILE* fp)
{
assert(message && fp);
//将字符串写入到缓冲区中
strcpy(fp->buffer + fp->end, message); //adcde\0
fp->end += strlen(message);
//如果是标准输入输出
if (fp->fd == 0) //暂不实现
{}
else if (fp->fd == 1)
{
//输出到显示器 刷新策略改变为行刷新
if (fp->buffer[fp->end - 1] == '\n')
{
write(fp->fd, fp->buffer, fp->end);
fp->end = 0;
}
}
else if (fp->fd == 2) {} //暂不实现
else {} //暂不实现
}
3._fflush
_fflush的其实就是将C语言中缓冲区的数据调用write接口传入系统内核的缓冲区中,所以当我们检测到如果C缓冲区不为空,则调用write进行刷新。
注意:此时调用write接口只是将数据传入系统中,这里我们还可以将内核中的数据直接刷新到磁盘中。调用的接口是syncfs,直接将fd传入即可。
void _fflush(MyFILE* fp)
{
assert(fp);
if (fp->end != 0)
{
//暂且将数据写入至内核
write(fp->fd, fp->buffer, fp->end);
//将数据从内核刷新至外设
syncfs(fp->fd);
fp->end = 0;
}
}
4._close
_close的实现就是调用close接口即可,只不过在调用之前要先进行缓冲区的刷新。
void _fclose(MyFILE* fp)
{
//close之前,先进行刷新
assert(fp);
_fflush(fp);
close(fp->fd);
free(fp);
}
2.3 测试与源代码
测试代码如下:
执行结果:
全部代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
//缓冲区的大小
#define NUM 1024
typedef struct MyFILE {
int fd;
char buffer[NUM];
int end;
}MyFILE;
MyFILE* _fopen(const char* pathname, const char* mode)
{
assert(pathname && mode);
MyFILE* fp=NULL;
int fd = -1;
if (strcmp(mode, "r") == 0)
{
fd = open(pathname,O_RDONLY);
}
else if (strcmp(mode, "w") == 0)
{
//打开文件,传入3个选项:只读、创建、清空,设置权限为0666
int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
}
else if (strcmp(mode, "a") == 0)
{
fd = open(pathname, O_WRONLY | O_APPEND | O_CREAT, 0666);
}
if (fd >= 0)
{
//申请一个FILE结构体
fp = (MyFILE*)calloc(1, sizeof(MyFILE));
fp->fd = fd;
}
return fp;
}
void _fputs(const char* message, MyFILE* fp)
{
assert(message && fp);
//将字符串写入到缓冲区中
strcpy(fp->buffer + fp->end, message); //adcde\0
fp->end += strlen(message);
//如果是标准输入输出
if (fp->fd == 0) //暂不实现
{}
else if (fp->fd == 1)
{
//输出到显示器 刷新策略改变为行刷新
if (fp->buffer[fp->end - 1] == '\n')
{
write(fp->fd, fp->buffer, fp->end);
fp->end = 0;
}
}
else if (fp->fd == 2) {} //暂不实现
else {} //暂不实现
}
void _fflush(MyFILE* fp)
{
assert(fp);
if (fp->end != 0)
{
//暂且将数据写入至内核
write(fp->fd, fp->buffer, fp->end);
//将数据从内核刷新至外设
syncfs(fp->fd);
fp->end = 0;
}
}
void _fclose(MyFILE* fp)
{
//close之前,先进行刷新
assert(fp);
_fflush(fp);
close(fp->fd);
free(fp);
}
int main()
{
MyFILE* fp = _fopen("log.txt", "w");
if (fp == NULL)
{
printf("open file error");
return 0;
}
_fputs("hello world1\n", fp);
_fputs("hello world2\n", fp);
_fputs("hello world3\n", fp);
//直接往屏幕上输出(用于测试)
_fflush(fp);
_fclose(fp);
return 0;
}
最后就是模拟向显示器打印,从而改变刷新策略从满刷新到行刷新的测试。
结果如下: