目录
一、理解文件
1.1、狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上是的存储是永久的
- 磁盘是外设(即是输出设备也是输入设备)
- 对磁盘上文件的所有操作,本质上都是对外设的IO(输入和输出)操作
1.2、广义理解
Linux下一切皆是文件(磁盘、网卡、显示器、键盘……等等,这些都是抽象化的过程)
1.3、文件的归类认知
- 对于0kb的空文件也是占用磁盘空间的
- 文件是文件内容和文件属性(元数据)的集合(文件=内容+属性(元数据))
- 所有对文件的操作本质上是对文件内容和文件属性的操作
1.4、系统角度
- 对文件的操作本质上是进程对文件的操作(进程打开文件!)
- 磁盘的管理者是操作系统
- 文件读写的本质不是通过C/C++库函数实现的,这些库函数只是为了方便使用;实际上是通过与文件相关的系统调用接口来实现的
- 操作系统管理磁盘上的文件是通过先描述再组织的方式,也就是文件会有一个与之对应的内核数据结构,又因为对文件的操作本质上是进程对文件的操作,而进程在操作系统层面也是通过内核数据结构管理的;那么对文件的操作实际上可以理解为对两个内核数据结构之间的操作
二、回顾C语言文件接口
2.1、hello.c打开文件
1 #include<stdio.h>
2
3 int main()
4 {
5 FILE * fp = fopen("myfile" , "w");
6 if(fp == NULL)
7 {
8 perror("open failed!\n");
9 return 1;
10 }
11 printf("open secceed!\n");
12 fclose(fp);
13 return 0;
14 }

成功以写的打开文件不存在的文件会默认先生成这个文件;生成的位置为当前的路径;

这是因为cwd记录着当前进程的路径,这个进程打开文件时,也会把文件拼接在这个路径之后:

2.2、写文件
介绍两个函数:fwrite、fread
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
相关说明:
The function fwrite() writes nmemb elements of data, each size bytes long, to the stream pointed to by stream, obtaining them from the location given by ptr.将ptr里面的内容写入stream里面,写入nmemb个单位,每个nmemb单位长度是size个字节
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
相关说明:
The function fread() reads nmemb elements of data, each size bytes long, from the stream pointed to by stream, storing them at the location given by ptr.
将stream中的数据读取到ptr里面,读取nmemb个单位,每个单位size个字节
返回值:
On success, fread() and fwrite() return the number of items read or written. This number equals the number of bytes transferred only when size is 1. If an error occurs, or the end of the file is reached, the return value is a short item count (or zero).
成功后,fread() 和 fwrite() 返回读取或写入的项目数。此数字等于仅当 size 为 1 时传输的字节数。如果发生错误或到达文件末尾,则返回值为较短的项目计数(或零)。
写文件:将msg和cnt格式化写入buffer,再使用fwrite函数将buffer的内容写入myfile文件
5 int main()
6 {
7 FILE * fp = fopen("myfile", "w");
8 if(fp == NULL)
9 {
10 perror("open failed!\n");
11 return 1;
12 }
13 const char* msg = "hello linux !:";
14 int cnt = 5;
15 while(cnt)
16 {
17 char buffer[1024];
18 snprintf(buffer, sizeof(buffer), "%s%d\n" , msg , cnt--);
19 fwrite(buffer , strlen(buffer) , 1 , fp);
20 }
21 fclose(fp);
22 return 0;
23 }

2.3、读文件
7 int main()
8 {
9 FILE * fp = fopen("myfile" , "r");
10 if(fp == NULL)
11 {
12 perror("open failed\n");
13 return 1;
14 }
15 const char* msg = "hello linux !:";//用来计算每次读取的字节数
16 while(1)
17 {
18 char* buffer[1024];
19 size_t s = fread(buffer , strlen(msg)+2 , 1 , fp);//写入文件时有一个数字和一个'\n'
20 if(s > 0)
21 {
W> 22 printf("%s" , buffer);
23 }
24 if(feof(fp))
25 {
26 break;
27 }
28 }
29 return 0;
30 }

feof判断文件指针是否指向文件末尾,若是返回值非0,否则返回0
2.4、实现一个简单的cat命令
5 //基于文件读操作实现一个简单的cat
6 int main(int argc , char* argv[])
7 {
8 if(argc != 2)
9 {
10 printf("argc error!\n");
11 return 1;
12 }
13 FILE * fp = fopen(argv[1] , "r");
14 if(fp == NULL)
15 {
16 perror("open failed\n");
17 return 2;
18 }
19 const char* msg = "hello linux !:";//用来计算每次读取的字节数
20 while(1)
21 {
22 char* buffer[1024];
23 size_t s = fread(buffer , strlen(msg)+2 , 1 , fp);//写入文件时有一个数字和一个'\n'
24 if(s > 0)
25 {
W> 26 printf("%s" , buffer);
27 }
28 if(feof(fp))
29 {
30 break;
31 }
32 }
33 return 0;
34 }

2.5、将信息输出到显示器
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
5 //输出信息到显示器
6 int main()
7 {
8 const char* msg = "hello file!\n";
9 printf("%s:%s\n", "printf", msg);
10 fprintf(stdout, "%s:%s\n", "fprintf", msg);
11 char buffer[128];
12 snprintf(buffer, sizeof(buffer), "%s:%s\n", "fwrite", msg);
13 fwrite(buffer, strlen(buffer), 1, stdout);
14
15 return 0;
16 }

2.6、stdin、stdout、stderr
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
stderr作为标准错误流,程序在运行期间遇到错误时,可借助它输出错误信息。将错误信息和正常输出分开,能让用户更清晰地区分哪些是程序正常运行的结果,哪些是程序出错的提示
- C默认会打开三个输入输出流,分别是stdin、stdout、stderr
- 这三个文件流需要频繁使用,默认打开
2.7、文件打开的方式
r Open text file for reading. The stream is positioned at the beginning of the file.
r+ Open for reading and writing. The stream is positioned at the beginning of the file.
w Truncate file to zero length or create text file for writing(文件存在先清空不存在先新建此文件). The stream is positioned at the beginning of the file.
w+ Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file). The file is created if it does not exist. The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.
看看"w"和"a"的对于一个不为空文件的区别
"w"打开文件,存在则清空


echo >重定向操作和其类似

"a"打开文件,写入时是追加的方式不会清空
先清空文件,再用"a"打开文件写入


这个打开方式和echo >> 类似,写入是以追加的方式

三、系统IO
3.1、一种传递标志位的方法
按照位图的方式,这种方式在open的标志位中使用
8 void Func(int flag)
9 {
10 if(flag & ONE_FLAG)
11 {
12 printf("flag one\n");
13 }
14 else if(flag & TWO_FLAG)
15 {
16 printf("flag two\n");
17 }
18 else if(flag & THREE_FLAG)
19 {
20 printf("flag three\n");
21 }
22 else
23 {
24 printf("flag four\n");
25 }
26 }
27 int main()
28 {
29 Func(ONE_FLAG);
30 Func(TWO_FLAG);
31 Func(THREE_FLAG);
32 Func(FOUR_FLAG);
33 return 0;
34 }

看看open系统调用接口中常用的几种标志位,它们都是定义的宏对应有具体的值
3.2、写文件
先看看open打开文件的系统调用接口
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- pathname就是我们要打开的文件,这个可以只指定文件名,因为进程会记录当前路径;
- flags就是标志位,有只读、只写、新建、追加、清空;
- mode权限位:当新建一个文件时,用于指定这个文件的权限,比如传0666一类的,当然此后文件的具体权限还和umask掩码有关
- open返回的是打开的这个文件的文件描述符,用来标识这个文件;要是打开失败就返回-1
用一个例子引入
先看看write系统调用接口
头文件
#include <unistd.h>
接口使用
ssize_t write(int fd, const void *buf, size_t count);
返回值:要是写入成功就返回成功写入的字节;反之返回-1
On success, the number of bytes written is returned (zero indicates nothing was written). On error, -1 is returned, and errno is set appropriately.
打开也要关闭 系统调用接口close
#include <unistd.h>
int close(int fd);
close() returns zero on success. On error, -1 is returned, and errno is set appropriately.
7 int main()
8 {
9 int fd = open("log.txt", O_CREAT | O_WRONLY , 0666);
10 if(fd < 0)
11 {
12 perror("open failed!\n");
13 return 1;
14 }
15 const char* msg = "open a file\n";
16 int cnt = 5;
17 while(cnt)
18 {
19 write(fd, msg, strlen(msg));
20 cnt--;
21 }
22 close(fd);
23 return 0;
24 }

首先看看flags
这里使用了创建和只写,我们若是向此时的文件再写一串abcd呢
此时会发现会覆盖并且也没有清空,这是因为我们没有设置对应的标志位,若是想要清空写就加上O_TRUNC;若是想要追加写就加上O_APPEND
再C语言中fwrite中w就会自带清空,这是因为这个函数接口封装了系统调用接口;在我们使用系统调用接口时要自己设置
![]()

使用O_TRUNC,清空写


添加标志位O_APPNED追加写
追加写肯定不能和清空写一起出现


看看权限位mode
权限为设置的是0666,但是显示的是这样的,也就是0664,这是因为权限掩码umask的存在![]()
可以设置权限掩码:

系统默认的
在我们的代码中设置


关于写文件是否关心写进的内容的形式
在C语言阶段,有文本写入和二进制写入,但是在系统层面没有这个概念就是说,系统写文件时不关心写的内容的格式,从write的参数就可以得知,第二个参数是void*
实验一下,之前写入的都是字符串,现在写一个整形变量看看:


在写入字符串时,系统使用ASCLL码表将对应字符的字节写入文件,打印时就是ASCLL码表中对应的字符;但是在写入整形变量时,会将整形变量对应的二进制数据原封不动的写入文件,cat 命令并不知道这些数据代表的是整数,它只是简单地把文件中的字节当作 ASCII 码来尝试显示,由于二进制数据并不是有效的 ASCII 码,所以显示出来的就是乱码或者看似二进制的数据。
但是这个不是最重要的,重要的是我们使用系统调用接口写入文件时,不关系写入内容的形式
而所谓的文本写入或者是二进制写入是语言层面关心的;所以语言层面才会封装一系列接口去写入文件
3.3、读文件
读文件时打开文件使用open但是此时标志位只需要传O_RDONLY,权限位也不用传,因为写文件时创建的文件权限位已经设置好了,这也是为什么open会有两个接口
先看看read接口
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值,正常情况返回大于0的数表示读取到的字节数;返回0,则是读取到文件的末尾;返回-1表示读取出现错误
8 int main()
9 {
10 int fd = open("log.txt", O_RDONLY);
11 if(fd < 0)
12 {
13 perror("open failed!\n");
14 return 1;
15 }
16 char buffer[128];
17 read(fd, buffer, sizeof(buffer));
18 printf("%s\n", buffer);
19 return 0;
20 }

3.4、接口相关知识
3.5、文件描述符fd
打开几个文件,查看它们的返回值文件描述符fd;发现没有0、1、2;这是因为这三个文件描述符代表的是三个标准文件流,stdin、stdout、stderr


打印0、1、2文件描述符


fd就是数组下标,那么是什么数组下标呢?解析下图:

首先文件是存储在磁盘上的,对于每个存储在磁盘上的文件在操作系统中都存在一个struct file的内核数据结构用来存储这个打开的文件的相关信息(注意是打开的文件才有,因为只有打开的文件才会在操作系统中被交互);并且每个打开的文件都有一个文件缓冲区,这个文件缓冲区中就是磁盘上文件内容的拷贝 ,对于文件的任何操作都需要先把文件内容拷贝到文件缓冲区,对文件缓冲区操作之后再把文件缓冲区刷新到磁盘上的文件中。
其次这个数组就是操作系统中的一个指针数组,这个数组中存放的指针类型是struct file*的,就是存放的地址就是打开文件的组织形式即struct file的地址;
那么我们对文件的操作具体是怎么实现的呢?就拿写文件来说,首先,通过write中传的fd(充当下标)和打开这个文件的进程中struct file*从文件描述符指针中找到要写的文件的地址,再通过这个地址找到对应的struct file以及文件缓冲区;将内容写道文件缓冲区中再刷新缓冲区到磁盘。同样的读文件也是,先通过相同操作找到对应文件,再将文件缓冲区的内容拷贝到buffer里面,所以read本质上是系统到用户空间的拷贝函数
打开的文件也是先描述再组织
重定向操作
首先来看一组现象


关闭文件描述符0代表的文件stdin之后,再打开一个新的文件发现这个新打开的文件的文件描述符是0;
由此得出一个结论:文件描述符分配规则就是最小的还没有被使用的文件描述符分配给新打开的文件
那么将fd为2的文件描述符代表的文件stdout关掉,再打开一个新文件,那么输出就默认输出到这个新文件里面了;本质上是指针的指向的改变
看代码:


###dup2系统调用接口的操作:
int dup2(int oldfd, int newfd);
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary;
RETURN VALUE
On success, these system calls return the new descriptor. On error, -1 is returned, and errno is set appropriately.
分析一下oldfd和newfd,所谓的oldfd就是打开这个文件本身应该的fd,newfd就是指定要的fd;也就说,打开这个文件时分配的fd是oldfd,但是我们自己想修改分配的fd,指定为newfd;
在底层对应的变化newfd在文件描述符表中的指针被oldfd在文件描述符表中的指针所覆盖了,也就是newfd处的指针是oldfd处的指针的一份拷贝,那么因为指针指向没变,那么newfd处的指针就会指向oldfd处指针指向的struct file了
不过此时oldfd处的指针还是指向的原来的struct file,也就是说此时的新打开的文件的struct file在文件描述符表中被两个指针同时指向
看一段代码:


从结果可以知道此时的fd还是3,但是呢fd为3对应的文件已经被当作stdout了 ;这是因为dup2的作用,此时fd为1处的指针已经是指向log.txt了,而不是stdout。
追加重定向就是将打开方式中的清空换为追加
输入重定向:
本来应该从stdin键盘文件读取的,但是此时从log.txt读取了,这是因为输入重定向


另一种写法:
将log.txt当作命令行参数传进去实现输入重定向操作


myshell实现重定向操作






添加一个点:
在我们使用dup2进行重定向时,要是0、1,2指向的文件某一个假设就是stdin,被重定向之后,其他的进程使用stdin时会不会受到影响;答案是不会
首先需要知道,文件是可以被多个进程共享的,这才是提出这个问题的前提(从我们之前父子进程打印pid时打印到同一个stdout可知)
那么是怎么消除影响的呢?在stdin里面存在一个计数器,这个计数器记录了自己被多少个进程的文件描述符所指向,当有一个进程的stdin被dup时,计数器减减,只要不到0,其他的进程还是可以使用stdin这个标准文件流。
重谈重定向(标准错误)
重定向的实际写法:



指定文件描述符,就是将log.txt1的地址覆盖到1所在文件描述符表的位置,打印时寻找的是1所在的文件描述符表中的文件地址,而此时1指向的是log.txt1
看看stderr:


虽然stderr和stdout的文件描述符fd分别是2、1;但是它们指向的都是标准输出,上图只是为了方标理解;从运行结果我们也能知道,向stderr打印其实就是向显示器打印
使用重定向:
发现只有stdout的被重定向了,而stderr还是向显示器打印;这是因为这样写只是把1给重定向了,而2还没有被重定向,依旧向显示器打印

分别使用重定向:

为什么会有这种现象?这是因为为了我们可以使用重定向操作将常规信息和错误信息分离,方便日志的形成
若是想要把这两个打印到同一个文件呢?
这种写法只有标准错误测试信息,因为>打开时会先清空文件

可以使用>>,这样是可以的

还有一种比较好的写法:

解释:&1是一个语法,由解释器解释,我们不用关心;重要的是:首先将fd为3的指针覆盖到1,此时打印文件就是向log.txt里面打印,之后2>&1,这是将1中的指针覆盖到2,此时2指向的就也是log.txt
四、理解“一切皆文件”

在OS中硬件的相关信息都被多个内核数据结构struct_device相连成的链表组织描述管理起来了;值得注意的是,我们需要对这些硬件进行读写一类的操作,但是肯定的是,每个设备的配有的read和write的具体实现不一样,读写不同硬件的操作肯定是具有很大差别的;
而Linux下“一切皆文件”就体现在这个位置:OS中进程打开文件,这个进程具有一张文件描述符表,并且通过这张文件描述符表关联了很多struct file的内核数据结构;在我们对硬件进行读写时首先对文件缓冲区进行内容修改,之后是怎么读写到硬件的呢?由于不同硬件的读写差别很大,所以这种操作并不简单,在struct file里面是不能定义函数的,为了解决这个问题,struct file里面具有很多的函数指针,这些函数指针的命名参数都一样,和底层硬件的读写方法相关联;
于是可以通过函数指针,屏蔽底层的差异,而只是简单地将文件缓冲区里面的内容通过函数指针关联不同硬件的读写接口从不同的硬件进行读写;类似于多态思想;
这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤ read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函 数来进⾏。
再来理解“一切皆文件”:就是屏蔽掉底层不同设备的差异,只让进程对VFS(虚拟文件系统,就是一个一个struct file)做出相关操作就能实现对硬件的操作,这样看来硬件也就是一个一个的文件了;所以“一切皆文件”这种说法实际上是站在进程的角度来说的。


五、缓冲区
先看一组现象:
关闭stdout之后,打开一个新的文件,这个文件就是新的stdout,此时打印数据,打印之后不关闭fd:


关闭fd:
之后发现只有系统调用的write能将数据写进文件

解释:

在我们使用fprintf、printf、fputs、fwrite这些库函数时,并不是直接将数据写到文件内核缓冲区的;而是先写到语言层面的缓冲器中;这个缓冲区的维护靠的就是我们常见到的FILE*,这是一个struct,里面包含文件的各种属性,其中就有维护这个FILE对应文件的相关缓冲区信息以及fd;使用fopen打开文件时,这个文件类型是FILE* 的,底层会为这个文件申请一段内存空间也就是struct FILE的,之后再对这个对象填充属性,每个文件都有对应的缓冲区
在达到某种条件之后操作系统会使用系统调用接口例如write和fd将语言层面缓冲区中的数据刷新到文件内核缓冲区
这个语言层面的缓冲区刷新到文件内核缓冲区的方式:1、用户强制刷新,使用fflush直接刷新到stdout;2、满足刷新的条件(比如使用写透模式WT相当于写入语言层面的缓冲区之后直接刷新到内核缓冲区,或者FILE中维护的缓冲区满了这个针对普通文件,或者按行刷新这个主要是方便用户读的即针对显示器);3、进程退出,刷新。
为什么要存在语言层面的缓冲区?这是因为系统调用是具有成本的。所以语言层面的缓冲区的存在是为了提高效率,先将一定量数据写到这个缓冲区,达到一定限度或者满足一定条件之后刷新减少系统调用的次数来提高效率,这也是为什么stl中容器的扩容都是按照接近2倍的方式去扩容的,原因就是扩容需要系统调用,而一次扩容多点就能减少系统调用提高效率
再者库函数或者系统调用对文件的操作都是数据的拷贝,所以计算机内部数据的流动方式本质上都是数据的拷贝
演示强制刷新:
就算最后close但是在前面使用fflush强制刷新缓冲区到文件内核缓冲区,这样就能交付给磁盘上文件了


源代码中的FILE
来一个例子加深对缓冲区的理解:


解释为什么出现这种不同的现象:
首先显示器是行刷新,而上述代码打印的都是带了\n的,也就是每一行放到库缓冲区之后都会刷新到文件内核缓冲区;当代码走完库函数的打印之后此时库缓冲区里面已经没有数据了,系统调用不用说,直接写到文件内核缓冲区;那么再创建子进程时继承父进程的一些数据结构,那么此时的子进程的缓冲区也就没有数据
而重定向打印到普通文件时,库缓冲区是满了之后才会刷新,那么此时父进程库函数将数据写到库缓冲区,不满不会刷新到文件内核缓冲区,也就是说,库缓冲区中的数据会一直存在;而系统调用直接将数据写道文件内核缓冲区再写到磁盘(只要系统把数据交给OS,就相当于把数据交给了硬件设备)。那么在创建子进程之前,父进程库缓冲区中有一份库函数写的数据,而write写的数据已经写到了硬件;创建子进程,子进程的库缓冲区中也会有一份这样的数据,最后进程结束时父进程和子进程都会将自己的库缓存区中的数据写到文件内核缓冲区中,这样就呈现出上述结果。
简单glibc库的实现
头文件:
1 #pragma once
2
3 #include<stdio.h>
4
5 #define MAX 1024
6
7 #define NONE_FLUSH (1<<0)
8 #define LINE_FLUSH (1<<1)
9 #define FULL_FLUSH (1<<2)
10
11 typedef struct MyFile
12 {
13 int fileno;//文件描述符
14 char outbuffer[MAX];//文件内容
15 int bufferlen;//文件实际内容长度
16 int flag;//文件打开方式
17 int flush_method;//向文件内核缓冲区的刷新方式
18 }MyFile;
19
20 MyFile * MyFopen(const char* path, const char* mode);
21 void MyFwrite(const void* ptr, size_t len, MyFile* file);
22 void MyFclose(MyFile* file);
23 void MyFflush(MyFile* file);
源文件:
1 #include"mystdio.h"
2 #include<string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include<unistd.h>
7 #include<stdlib.h>
8 //初始化MyFile
9 MyFile* BuyFile(int fd, int flag)
10 {
11 MyFile* f = (MyFile*)malloc(sizeof(MyFile));
12 if(f == NULL)
13 {
14 perror("malloc fail\n");
15 return NULL;
16 }
17 f->bufferlen = 0;
18 f->fileno = fd;
19 f->flag = flag;
20 memset(f->outbuffer, 0, sizeof(f->outbuffer));
21 f->flush_method = LINE_FLUSH;
22 return f;
23 }
24 MyFile* MyFopen(const char* path, const char* mode)
25 {
26 int fd = -1;
27 int flag = 0;
28 if(strcmp(mode,"w") == 0)
29 {
30 flag = O_CREAT | O_WRONLY | O_TRUNC;
31 fd = open(path, flag, 0666);
32 }
33 else if(strcmp(mode,"a") == 0)
34 {
35 flag = O_CREAT | O_WRONLY | O_APPEND;
36 fd = open(path, flag, 0666);
37 }
38 else if(strcmp(mode,"r") == 0)
39 {
40 flag = O_RDWR;
41 fd = open(path, flag);
42 }
43 else
44 {}
45 if(fd < 0)
46 {
47 perror("open fail\n");
48 return NULL;
49 }
50 //打开文件之后需要申请MyFile
51 return BuyFile(fd, flag);
52 }
53
54 void MyFclose(MyFile* file)
55 {
56 //关闭文件要进行刷新
57 if(file->fileno < 0) return;
58 MyFflush(file);
59 close(file->fileno);
60 free(file);
61 }
62 void MyFflush(MyFile* file)
63 {
64 //刷新实际上是将库缓冲区的内容写到文件内核缓冲区
65 if(file->fileno < 0) return;
66 write(file->fileno, file->outbuffer, file->bufferlen);
67 file->bufferlen = 0;//刷新之后file就没了,已经拷贝了
68 //刷新到文件内核缓冲区之后,应该直接再将数据拷贝到磁盘
69 fsync(file->fileno);
70 }
71 void MyFwrite(const void* ptr, size_t len, MyFile* file)
72 {
73 if(file == NULL) return;
74 //写入实际上是拷贝
75 memcpy(file->outbuffer + file->bufferlen, ptr, len);
76 file->bufferlen += len;
77 //写入之后要考虑要不要刷新的问题
78 if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
79 {
80 //行刷新
81 MyFflush(file);
82 }
83 }
fopen打开的文件返回一个FILE*的对象,这个FILE对象里面存在fd、管理缓冲区的属性,那么我们模拟时也要添加,MyFile就是这样设计的;可以得知的是,库中fopen也是对系统调用open的封装,那么模拟时使用open需要知道打开的文件以及方式,open返回一个fd,这个fd用来构造MyFile;
fwrite写入是将数据拷贝到FILE结构体里面的缓冲区,若是缓冲区满了就要刷新;这里模拟的是行刷新,刷新方式定义在头文件,只有当写入的字符换末尾有\n并且刷新方式是行刷新才会进行刷新;
刷新就是把库缓冲区的数据写入到文件内核缓冲区,调用write;刷新之后库缓冲区就没数据了;一般来说,刷新之后就直接写到硬件,那么模拟时调用fsync系统调用,这个接口的作用是将文件内核缓冲区的数据写到硬件上;
fclose将打开的文件关闭,那么此时会将没有刷新的数据刷新,之后释放FILE申请的资源;并且因为打开时调用了open,那么关闭时也对应的要调用close
###测试观察现象,加深对缓冲区的理解
1、简单的写入:


2、若是去掉写入字符串的\n,会发生什么现象?将写入执行十次,观察库缓冲区和硬件上log.txt内容的区别。结果应该是不会写一行刷新一行,每次写入我们只能看到库缓冲区也就是模拟的outbuffer里面的内容增加,但是log.txt里面没有内容增加,因为此时不满足刷新条件;只有当文件写完,调用MyFclose时才会刷新

从结果可以知道,每次filep里面的outbuffer都有数据写入,但是log.txt不会按行刷新,因为写入filep的字符串不是以'\n'结尾的,所以log.txt前四次没有数据,只有第五次之后调用的MyFclose中调用了MyFflush函数,会刷新数据,所以只有第五次log.txt里面才会存在数据。
3、接着2,但是写入一次调用一下自己实现的MyFflush,观察:


从以上实验回想在使用printf函数打印字符串时,若是没有带\n,那么数据是不会刷新到磁盘的,而是先在库缓冲区中,所以不带\n,每次打印时才会使用fflush函数

被折叠的 条评论
为什么被折叠?



