文章目录
- 📕 看现象
- 📕 理解本质
- 📕 模拟文件接口
- mystdio.h
- mystdio.c
📕 看现象
如下代码,运行结果如图。
1 #include<sys/types.h>
2 #include<sys/stat.h>
3 #include<fcntl.h>
4 #include<stdio.h>
5 #include<unistd.h>
6 #define LOG "log.txt"
7
8 int main()
9 {
10 fprintf(stdout,"hello fprintf\n");
11 const char* msg="hello write\n";
12 write(1,msg,sizeof msg);
13 return 0;
14 }
在 return 之前,调用 fork() 创建一个子进程,再尝试同样的操作,结果如下。
1 #include<sys/types.h>
2 #include<sys/stat.h>
3 #include<fcntl.h>
4 #include<stdio.h>
5 #include<unistd.h>
6 #include<string.h>
7 #define LOG "log.txt"
8
9 int main()
10 {
11 fprintf(stdout,"hello fprintf\n");
12 const char* msg="hello write\n";
13 write(1,msg,strlen(msg));
14
15 fork();
16 return 0;
17 }
从打印的次数来看,直接执行的都一样。但是重定向的里面, hello fprintf 很明显是打印了两次。
📕 理解本质
如下,对于 C 语言的函数库,调用 fprintf() 函数,要依赖于 FILE 对象,而 FILE 对象是由 struct FILE 结构体构建的,这个结构体里面必然要有对应文件的文件描述符。
与此同时,struct FILE 里面也有自己的缓冲区,当我们调用 fprintf() 向某个 FILE 对象写入数据的时候,实际上是将数据复制到 FILE 对象自己的缓冲区。
数据放到 FILE 对象的缓冲区之后,C库会结合一定的刷新策略,把缓冲区里面的数据写入操作系统(可以通过 FILE 对象里面的 fd 找到),由操作系统交给文件的缓冲区。刷新策略包括:
- 无缓冲:不提供缓冲,例如调用 fprintf 的时候,直接就把数据写给操作系统,不写给 C库自己的缓冲区。
- 行缓冲:数据写到C库自己的缓冲区,只要遇到 ‘\n’ ,就把 ‘\n’ 以及之前的数据,全部写到操作系统里面。
- 全缓冲:只有把 FILE 对象的缓冲区写满了,才会刷新到操作系统里面。
一般而言,显示器的刷新策略是行缓冲。普通文件的刷新策略是全缓冲。
在这里,我们要区分两个概念,struct FILE 的缓冲区 和 文件的缓冲区,前者是我们使用 C 语言的 fwrite、fread 函数会用到的,后者是使用系统调用 write、read 的时候用到的。
所以,这就能理解上面的现象了,首先对于write() 而言,它是系统调用,所以没有C库的缓冲区刷新策略。
而 fprintf 是 C 语言封装的库函数,有上面提到的三种刷新策略。
1 #include<sys/types.h>
2 #include<sys/stat.h>
3 #include<fcntl.h>
4 #include<stdio.h>
5 #include<unistd.h>
6 #include<string.h>
7 #define LOG "log.txt"
8
9 int main()
10 {
11 fprintf(stdout,"hello fprintf\n");
12 const char* msg="hello write\n";
13 write(1,msg,strlen(msg));
14
15 fork();
16 return 0;
17 }
对于 ./test ,这是打印到显示器,fprintf 的刷新策略是行缓冲,所以 fork 之前,FILE 对象里缓冲区的数据就已经刷新到文件的缓冲区,然后刷新到到显示器里面了。而 write 是系统调用,直接刷新到文件缓冲区,再刷新到显示器,所以在 fork 之前,文件缓冲区里也没有数据。那么,就会只打印一份。
但是,./test > log.txt 是重定向到文件,fprintf 的刷新策略就变成了全缓冲,可是字符串无法把缓冲区写满,那么 fork 之前,数据被写到 FILE 对象的缓冲区里面,还存在那里,没有被刷新到文件里面。fork 之后,子进程继承了父进程的数据,两个进程都要刷新,谁先刷新就发生写时拷贝,另一个进程继续刷新,这样子就刷新了两次!! write 依然是直接把数据写到文件的缓冲区, 然后写入文件。
📕 模拟文件接口
mystdio.h
#include<stdio.h>
#define NUM 1024
// 定义缓冲方式
#define BUF_NONE 0x1
#define BUF_LINE 0x2
#define BUF_ALL 0x4
typedef struct _MY_FILE
{
int fd;
char BUFFER[NUM]; // 缓冲区
int current; // 写到BUFFER的哪个位置了
int flag; // 什么方式缓冲
}MY_FILE;
int my_fclose(MY_FILE *fp);
int my_fflush(MY_FILE *stream);
MY_FILE *my_fopen(const char *path, const char *mode);
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb,MY_FILE *stream);
mystdio.c
如下,我们调用 write 方法,实际上就是把 struct FILE 缓冲区里面的内容,拷贝到文件的缓冲区,至于数据什么时候从文件中的缓冲区,刷新到磁盘,是由操作系统调度的。
而C语言中的 fwrite 等等,实际上就是对操作系统接口的封装,本质上是将数据写入自己 struct FILE 中的缓冲区,然后按照缓冲区的策略来刷新到文件的缓冲区。
#include"mystdio.h"
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<malloc.h>
#include<assert.h>
#include<stdlib.h>
int my_fclose(MY_FILE *fp)
{
assert(fp);
// 冲刷缓冲区,这是语言层面上的
if(fp->current > 0) my_fflush(fp);
// 关闭文件,close
close(fp->fd);
// free 开辟的 fp
free(fp);
fp=NULL;
return 0;
}
int my_fflush(MY_FILE *stream)
{
// 把缓冲区的数据,冲刷给操作系统,让操作系统决定刷新策略
write(stream->fd,stream->BUFFER,stream->current-1);
stream->current=0;
return 0;
}
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,"x") == 0) flag |= O_WRONLY | O_CREAT | O_APPEND;
else{ // r+ w+ x+ ……
}
// 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){
return NULL; // 也是打开文件失败
}
mf->fd=fd;
mf->current=0;
mf->flag=BUF_LINE; // 行缓冲
memset(mf->BUFFER,'\0',sizeof(mf->BUFFER));
// 4.返回
return mf;
}
// 返回写了多少个字节
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb,MY_FILE *stream)
{
assert(ptr);
// 缓冲区满了,直接刷新
if(stream->current == NUM) my_fflush(stream);
// 缓冲区没有满,根据缓冲区情况拷贝即可
size_t freesize=NUM-stream->current; // 剩余字节数
size_t writesize=size*nmemb; // 写入字节数
size_t ret=0;
if(freesize >= writesize) {
memcpy(stream->BUFFER+stream->current,ptr,writesize);
stream->current+=writesize;
ret+=writesize;
}
else{ //写入能写的
memcpy(stream->BUFFER+stream->current,ptr,freesize);
stream->current=NUM;
ret+=freesize;
}
// 计划刷新
if(stream->flag & BUF_ALL){
if(stream->current == NUM) my_fflush(stream);
}
else if(stream->flag & BUF_LINE){
if(stream->BUFFER[stream->current -1 ] == '\0') my_fflush(stream);
}
else{
}
return ret;
}