目录
前置知识
在学习下面的知识之前,我希望大家先在脑海中想象出一个冯诺依曼体系结构:
1. 什么是缓冲区?
根据我们目前的知识,所理解的缓冲区:一部分内存。
2. 为什么需要缓冲区?
为了帮助大家理解,我为大家举个例子,某大学生张三要给在异地的朋友送一个礼物,当然不需要自己亲自跑过去送,他只需要到菜鸟驿站,填好信息后,由菜鸟驿站帮他把货物发出去。虽然不是自己亲自去送礼物,但因为张三相信菜鸟驿站能帮他把礼物寄到朋友那里,所以张三就可以认为礼物将会递达朋友那里。
对于缓冲区,我们可以理解为它相当于做了菜鸟驿站的工作,能够提高效率,那么提高的是谁的效率呢?就算张三不用亲自把礼物送过去,还是要有人去做这件事,是快递公司负责把礼物送到他朋友家。所以这提高了张三的效率,类似的,缓冲区提高了使用者的效率。
例子到这还没完,菜鸟驿站在收了张三的货物后,也不会马上寄出,而是先放着,等有了一批货物后再统一寄出,如此一来,效率就提升了。类似的,缓冲区也提高了数据发送的效率。
所以我们就可以认为:缓冲区提高了整体的效率。
3. 缓冲方式都有哪些?
既然系统中存在缓冲区,那么就有一定的常规刷新方式:
1. 无缓冲(立即刷新) 2. 行缓冲(按行刷新) 3. 全缓冲(一块缓冲区满了,再刷新)
既然说到常规刷新方式,就有特殊情况:
1. 强制刷新 2. 进程退出时,往往会对缓冲区进行刷新
(一般而言,显示器文件采取的是行缓冲,磁盘上的文件采取的是全缓冲)
一个“奇怪”的样例
接下来请大家看这一段代码:
先来看看直接运行的结果:
和我们的预期一致,这几个函数无论是库函数还是系统调用函数都向显示器文件写入了对应的信息,但是当我们把运行结果重定向到文本文件中时,结果就有些奇怪了:
库函数向文件中写入了两次,而系统调用函数只写入了一次!
以我们对这几个函数的理解,出现此现象肯定和最后调用的fork函数相关,可是我们在创建子进程之前,这些打印操作都已经进行完了呀,为什么有的函数会打印两次呢?
要解释这个现象,就得和缓冲方式结合起来看了,当我们直接向显示器打印信息时,显示器的刷新机制是行刷新,而我们打印的信息都带有"\n",也就是说:这种情况下,在执行fork函数之前,信息就已经打印完了,因此最后调用fork函数也不会有什么变化;但当把要打印的信息重定向到文件中时,系统的刷新机制是全刷新,缓冲区满了才进行刷新,所以在执行fork函数之前,信息还没有被写入到文件中,而是暂时保存在缓冲区中,由于创建了子进程,而两个进程在有一方退出时,刷新了缓冲区,对进程数据进行了修改,也就需要发生写时拷贝,所以缓冲区中又多了 一份信息,这样就解释了为什么文件中C语言函数打印的信息有两次。
那么为什么系统调用函数的信息只有一次呢?就是因为系统调用函数打印的信息并没有存到这个缓冲区中,也就是说这个缓冲区应该不是系统提供的,而是C语言为我们提供的一块缓冲区!这样,这个“奇怪”的现象就说得通了,因为系统调用函数打印的信息不需要先存到缓冲区里,所以尽管是最后调用的,但会被最先写入到文件中;因为系统调用函数的打印在执行fork函数之前已经执行完毕,所以进程一方退出时不会发生写时拷贝,文件中写入的信息也就只有一份!
用户级缓冲区 vs 内核缓冲区
现在,我们明白了一件事:C语言内部提供了一个缓冲区,而printf, fprintf, fputs这些函数并不直接把打印内容写入到指定流,而是先写入到缓冲区中,在某一时刻调用内部封装的write系统调用,向指定位置进行写入。而从C缓冲区写入到操作系统内部的工作就叫刷新
大家可能会觉得奇怪,难道直接写入到操作系统不行呢,为什么还先写到缓冲区中呢?还记得我前面说过的么,缓冲区就像驿站一样,是为了提高使用的的效率而存在的,根据冯诺依曼体系,如果频繁地进行把数据从用户拷贝到操作系统中的操作,CPU的效率势必会被拖累,所以缓冲区存满了之后再统一拷贝是更加高效的!
类似的,操作系统也存在内核级缓冲区,并且存在一定刷新策略,在符合条件后,将缓冲区中的信息写入到文件中。
进一步C语言缓冲区
在前面我们知道了,C语言内部会为用户提供一段内存空间作为缓冲区,这能提高用户I/O的效率,那么我们这段内存空间在哪里呢?又凭什么说它存在呢?
大家是否看过C语言文件I/O操作相关库函数的声明呢?
在任何情况下,进行输入输出操作时,必须有个FILE类型的结构体,在上篇文章中我们证明了FILE结构体内必须封装有文件描述符,现在看来,FILE内部还应该要为用户提供有缓冲区,既然C语言输入输出函数对任何一个文件进行操作时,都要有个FILE结构体,那么对任何一个文件操作时,都有对应的缓冲区用以暂时保存信息,这就是我们在学习C语言时接触到的输入缓冲区、输出缓冲区!
这就是C语言FILE类型中表示缓冲区的相关地址变量,至于光标所指向的,其实就是前面提到的文件描述符。
当我们通过printf()向显示器打印整数时,其实打印的是一个个字符;当我们通过scanf()从键盘中输入一个整数时,其实输入的也是字符,这串字符被暂时保存在缓冲区中,在刷新时,进行格式化输入,以整数类型的形式存到内存中
编写一个简单的C语言IO库
接下来为了深化对用户级别缓冲区的理解,我们来试着编写一个简单的C语言IO库!
mystdio.h
#pragma once
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#define SIZE 4096
#define FLUSH_NONE 1
#define FLUSH_LINE (1<<1)
#define FLUSH_ALL (1<<2)
typedef struct _myFILE
{
int fileno;
int flag; // 刷新策略
char buffer[1024]; // 缓冲区
int end; // 缓冲区的指针
}myFILE;
extern myFILE *my_fopen(const char *path, const char *mode);
extern int my_fwrite(const char *s, int num, myFILE *stream);
extern int my_fflush(myFILE *stream);
extern int my_fclose(myFILE *stream);
mystdio.c
#include "mystdio.h"
// 默认创建文件时用户权限
#define DFL_MODE 0666
// 支持r, w, a三种方式打开文件
myFILE *my_fopen(const char *path, const char *mode)
{
int fd = 0;
int flag = 0;
if (strcmp(mode, "r") == 0)
{
flag |= O_RDONLY;
}
else if (strcmp(mode, "w") == 0)
{
flag |= (O_CREAT | O_TRUNC | O_WRONLY);
}
else if (strcmp(mode, "a") == 0)
{
flag |= (O_CREAT | O_APPEND | O_WRONLY);
}
else
{
// do nothing
}
if(flag & O_CREAT)
{
fd = open(path, flag, DFL_MODE);
}
else
{
fd = open(path, flag);
}
if(fd < 0)
{
errno = 2;
return NULL;
}
myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
if(!fp)
{
errno = 3;
return NULL;
}
// 将刷新方式设置为行刷新
fp->flag = FLUSH_LINE;
fp->end = 0;
fp->fileno = fd;
return fp;
}
int my_fwrite(const char *s, int num, myFILE *stream)
{
memcpy(stream->buffer+stream->end, s, num);
stream->end += num;
// 判断是否需要行刷新
// 不支持"abcd\nefgh"
if(stream->flag & FLUSH_LINE & stream->buffer[stream->end-1] == '\n')
{
my_fflush(stream);
}
return num;
}
int my_fflush(myFILE *stream)
{
if(stream->end > 0)
{
write(stream->fileno, stream->buffer, stream->end);
}
return 0;
}
int my_fclose(myFILE *stream)
{
my_fflush(stream);
return close(stream->fileno);
}
试着使用一下这批接口:
makefile
test:main.c mystdio.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f test
main.cc
#include "mystdio.h"
int main()
{
myFILE *fp = my_fopen("./log.txt", "w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
int cnt = 10;
const char *msg = "this is my stdio lib\n";
while(cnt--)
{
my_fwrite(msg, strlen(msg), fp);
sleep(1);
}
my_fclose(fp);
return 0;
}
运行结果:
简单总结
简单总结一下,通过今天的学习,我们对于缓冲区有了更深入的理解,而对于文件操作的接口,尽管我们还并不是特别熟悉,但我们不会感到困惑,因为我们知道了,这些接口的底层肯定是通过文件描述符来操作文件的,并且为了提高效率,内部应该也是封装了缓冲区的。