1. 缓冲区
1.1 什么是缓冲区
缓冲区其实就是一段内存空间,打个比方:
假如你要给好朋友送一个礼物,但你们在两个不同的城市,由你亲自送要走一个星期,耗费了大量时间。
于是你将礼物交给了楼下的快递驿站,这样你就不需要亲自去送,转而让快递员将礼物送到朋友手里。
快递驿站就扮演了缓存的角色,而礼物就是数据,快递发出就是刷新缓冲区的数据。
但是快递驿站不会为了发你这一件快递就浪费人力资源,所以他会攒到一定数量再发走。
所以:数据允许在缓冲区中积压,这样一次就可以刷新多次数据,变相的减少了IO的次数。
缓存最大的意义是提高使用缓存的进程的效率,允许进程在单位时间内做更多的工作,也就变相的提高了使用者(用户)的效率。
1.2 缓冲类型
在语言层面来看,决定什么时候发快递(决定使用什么样的刷新策略):
- 无缓冲,立即刷新。(直接调⽤系统调⽤,标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来)
- 有缓冲,行刷新。(一般在显示器中使用)
- 有缓冲,缓冲区满则刷新。(普通文件一般采用这种方式)
还有两种特殊情况:
- 执行fflush语句。
- 进程结束。
下面来看一段代码:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
// 关闭标准输出(文件描述符1)
close(1);
// 打开或创建文件"log.txt"
// O_WRONLY: 只写方式
// O_CREAT: 如果文件不存在则创建
// O_TRUNC: 如果文件存在则清空内容
// 0666: 文件权限(所有用户可读写)
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 检查文件是否成功打开
if (fd < 0) {
perror("open"); // 打印错误信息
return 0; // 程序结束
}
// 打印信息到标准输出(现在指向log.txt文件)
printf("hello world: %d\n", fd);
// 关闭文件
close(fd);
return 0; // 程序正常结束
}
这段代码本意是想使用重定向,让本该打印在显示器上的内容写到“log.txt”文件中,但实际运行后会发现,文件中并无内容。
这是因为代码中将1号文件描述符重定向到磁盘文件后,刷新的方式变为了全缓冲。而写入的内容并没有填满缓冲区,所以就不会将缓冲区的内容刷新到磁盘文件中,那么可以怎么做呢?可以使用fflush强制刷新缓冲区。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
// 关闭标准输出(文件描述符1)
close(1);
// 打开或创建文件"log.txt"
// O_WRONLY: 只写方式
// O_CREAT: 如果文件不存在则创建
// O_TRUNC: 如果文件存在则清空内容
// 0666: 文件权限(所有用户可读写)
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 检查文件是否成功打开
if (fd < 0) {
perror("open"); // 打印错误信息
return 0; // 程序结束
}
// 打印信息到标准输出(现在指向log.txt文件)
printf("hello world: %d\n", fd);
// 强制刷新
fflush(stdout);
// 关闭文件
close(fd);
return 0; // 程序正常结束
}
2. FILE
上面谈完了缓冲区,那么缓冲区到底在哪里呢?先说结论,在FILE中。也就是调用fopen函数的返回值的类型。
在操作系统操作文件的时候只认识文件描述符,而fopen之类的函数封装了系统调用函数open,所以fopen的返回值(FILE结构体)中必定包含了文件描述符。
/*
* FILE结构体是C标准库中表示文件流的数据结构
* 它封装了文件描述符和缓冲区,提供更高效的I/O操作
*
* 当我们使用 fopen() 打开文件时,返回的 FILE* 指针就指向一个FILE结构体
*/
typedef struct _IO_FILE FILE; // 通常的typedef定义
FILE是一个结构体,而在这个结构体内部会维护一块语言级的缓冲区空间。所以当我们调用printf之类的函数时,是将输出格式化成字符串写入到FILE结构体的缓冲区,再按照刷新规则刷新到输出设备上。下面是FILE结构体内部的一部分:
struct _IO_FILE {
int _fileno; // 文件描述符(底层open返回的fd)
char* _IO_read_ptr; // 读缓冲区当前位置
char* _IO_read_end; // 读缓冲区结束位置
char* _IO_read_base; // 读缓冲区起始位置
char* _IO_write_ptr; // 写缓冲区当前位置
char* _IO_write_end; // 写缓冲区结束位置
char* _IO_write_base; // 写缓冲区起始位置
char* _IO_buf_base; // 缓冲区起始位置
char* _IO_buf_end; // 缓冲区结束位置
int _flags; // 文件状态标志
// ... 还有其他字段
};
- 所以在学习例如C语言的时候,所说的缓冲区实际上是语言级缓冲区,和内核没有关系!
- 缓冲区在哪?FILE内部!
- 为什么要有语言级别缓冲区?因为调用系统调用是有成本的(浪费时间),所以可以减少系统调用次数。
3. 模拟文件操作
下面通过对系统调用封装,模拟实现一下C标准库的文件操作接口。
3.1 my_stdio.h
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#define FLUSH_NONE 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4
#define SIZE 4096
#define UMASK 0666
#define FORCE 1
#define NORMAL 2
typedef struct _MY_IO_FILE
{
int fileno; //文件描述符
int flag; //刷新方式
char outbuffer[SIZE]; //缓冲区
int curr; //当前缓冲区字符数量
int cap; //当前缓冲区总容量
}MYFILE;
MYFILE* my_fopen(const char* filename, const char* mode);
void my_fclose(MYFILE* fp);
int my_fwrite(const char* s, int size, MYFILE* fp);
void my_fflush(MYFILE* fp);
#endif
3.2 my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
// 打开文件
MYFILE *my_fopen(const char *filename, const char *mode)
{
int fd = -1;
if (strcmp(mode, "w") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, UMASK);
}
else if (strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if (strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, UMASK);
}
else if (strcmp(mode, "a+") == 0)
{
fd = open(filename, O_CREAT | O_RDONLY | O_APPEND, UMASK);
}
if (fd < 0)
return NULL;
MYFILE *fp = (MYFILE *)malloc(sizeof(MYFILE));
if (fp == NULL)
return NULL;
fp->fileno = fd;
fp->flag = FLUSH_LINE;
fp->curr = 0;
fp->cap = SIZE;
fp->outbuffer[0] = 0;
return fp;
}
static void my_fflush_core(MYFILE *fp, int force)
{
if (fp->curr <= 0)
return;
if (force == FORCE)
{
write(fp->fileno, fp->outbuffer, fp->curr);
fp->curr = 0;
}
else
{
// 判断刷新条件
if ((fp->flag & FLUSH_LINE) && (fp->outbuffer[fp->curr - 1] == '\n'))
{
// 行刷新
write(fp->fileno, fp->outbuffer, fp->curr);
fp->curr = 0;
}
else if ((fp->flag & FLUSH_FULL) && (fp->curr == fp->cap))
{
// 满刷新
write(fp->fileno, fp->outbuffer, fp->curr);
fp->curr = 0;
}
else
{
write(fp->fileno, fp->outbuffer, fp->curr);
fp->curr = 0;
}
}
}
int my_fwrite(const char *s, int size, MYFILE *fp)
{
memcpy(fp->outbuffer + fp->curr, s, size);
fp->curr += size;
my_fflush_core(fp, NORMAL);
return size;
}
void my_fflush(MYFILE *fp)
{
my_fflush_core(fp, FORCE);
}
void my_fclose(MYFILE *fp)
{
if (fp->fileno >= 0)
{
my_fflush(fp);
fsync(fp->fileno);
close(fp->fileno);
free(fp);
}
}