目录
- 一、预备知识
- 二、复习常见C语言的文件接口
-
- [2.1 文件接口的说明](#2.1 文件接口的说明)
-
- [2.1.1 fopen函数](#2.1.1 fopen函数)
- [2.1.2 fputs函数](#2.1.2 fputs函数)
- [2.1.3 fclose函数](#2.1.3 fclose函数)
- [2.2 文件接口的使用](#2.2 文件接口的使用)
- 三、认识操作文件的系统调用
-
- [3.1 系统调用的说明](#3.1 系统调用的说明)
-
- [3.1.1 open函数](#3.1.1 open函数)
-
- [3.1.1.1 Linux中常用的传参方法](#3.1.1.1 Linux中常用的传参方法)
- [3.1.2 write函数](#3.1.2 write函数)
- [3.1.3 close函数](#3.1.3 close函数)
- [3.2 系统调用的使用](#3.2 系统调用的使用)
- 四、C语言文件接口与文件系统接口的关系
- 五、文件描述符(fd)
-
- [5.1 fd](#5.1 fd)
- [5.2 fd为0、1、2的文件](#5.2 fd为0、1、2的文件)
- [5.3 如何理解操作系统下一切皆文件](#5.3 如何理解操作系统下一切皆文件)
- [5.4 C语言中的FILE](#5.4 C语言中的FILE)
- [六、理解struct file内核对象](#六、理解struct file内核对象)
- 七、fd的分配规则
- 八、重定向
-
- [8.1 输出重定向](#8.1 输出重定向)
- [8.2 dup2函数](#8.2 dup2函数)
- [8.3 追加重定向](#8.3 追加重定向)
- [8.4 输入重定向](#8.4 输入重定向)
- [8.5 重定向的本质](#8.5 重定向的本质)
- [8.6 重定向与进程替换之间会不会互相影响](#8.6 重定向与进程替换之间会不会互相影响)
- [8.7 使用重定向来讲述标准错误存在的原因](#8.7 使用重定向来讲述标准错误存在的原因)
- 九、缓冲区
-
- [9.1 缓冲区的简单理解](#9.1 缓冲区的简单理解)
- [9.2 缓冲区的样例](#9.2 缓冲区的样例)
- [9.3 理解样例](#9.3 理解样例)
- [9.4 缓冲区的位置](#9.4 缓冲区的位置)
- [9.5 stdio.h的模拟实现(原理)](#9.5 stdio.h的模拟实现(原理))
- 结尾
一、预备知识
通过之前的学习我们知道:文件=内容+属性
-
所以对文件的操作都分为以下两种
- 对文件的内容进行操作
- 对文件的属性进行操作
-
文件的内容是数据,文件的属性也是数据,存储文件就必须既存储文件的内容,也存储文件的属性,文件默认都是在磁盘中的。
-
进程要访问一个文件之前,就必须先将这个文件打开,文件打开之前 这个文件就是普通的磁盘文件,进程打开文件的目的就是为了读、写、访问文件,我们是通过CPU执行进程中打开文件的函数来访问文件的,就是CPU通过我们的代码来访问文件,由于CPU只能与内存进行交互,那么我们就要将需要打开的文件加载到内存中,所以文件打开之后这个文件就是在内存之中的。
加载磁盘上的文件,一定涉及到访问磁盘设备,而这个操作需要操作系统来完成。
-
一个进程可以打开多文件,被加载到内存中的文件也可能会存在多个,进程与被打开文件的数量比为1:n。操作系统在运行的时候,可能会打开多个文件,操作系统就需要管理这些被打开的文件,与管理进程相同,先描述,再组织,当一个文件加载到内存时,操作系统会为其创建一个文件结构体对象,这个结构体中有描述这个文件的属性,结构体中还有一个字段为结构体指针,当继续打开文件时,创建结构体对象,并将这些结构体对象通过指针的方式连接起来,形成一个链表,操作系统对文件的管理就转换为对链表的管理。上面提到了文件=内容+属性,结构体对象中的属性就是从文件中的属性中获取来的。
cppstruct file { // 文件属性 // ... // 指针 struct file* next; }
-
文件按照是否被打开分为以下两种
- 被打开的文件(在内存中)
- 未被打开的文件(在磁盘中)
-
这里我们讲述的文件操作本质上是进程与被打开文件的关系。
二、复习常见C语言的文件接口
2.1 文件接口的说明
2.1.1 fopen函数
cpp
FILE *fopen(const char *path, const char *mode);
功能:fopen 函数是 C 标准库中用于打开文件的函数。
参数:
- path:被打开文件的路径和文件名,若只有文件名则默认在当前工作目录搜索这个文件。
- mode:被打开为文件以什么样的模式打开。
2.1.2 fputs函数
cpp
int fputs(const char *s, FILE *stream);
功能:fputs函数是C 标准库中用于将字符串写入到指定的文件流中的函数。
参数:
- s:向文件流中写入的字符串。
- stream:指向目标文件流,字符串就是写入到stream指向的文件流。
2.1.3 fclose函数
cpp
int fclose(FILE *fp);
功能:fclose函数是C标准库中用于关闭一个打卡文件的函数。
参数:
- fp:就是指向要关闭的文件的FILE指针。
2.2 文件接口的使用
cpp
FILE* fp = fopen("fortest.txt","w");
将fortest.txt
文件以w(只写)的方式打开,若文件不存在则在当前工作目录下创建该文件,通过代码的测试我们发现确实可以创建文件。
使用w的方式下打开文件,会先将文件中所有的内容清空。在下面的代码中,我们先向文件中写入一段数据,通过下图可以看到这段数据确实写入文件中了,再将文件以w的方式打开什么都不做,再将文件关闭,我们可以发现文件中的内容消失了,这里可以证明以w的方式打开文件,会先将文件中所有的内容清空。
cpp
FILE* fp = fopen("fortest.txt","a");
使用a(追加)方式打开文件,不会清空文件中的内容,会从文件的结尾处开始写入。下面的代码中,我们向文件中写入一段数据,通过下图可以看到这段数据确实写入文件中了,向文件中多写入几段数据,也是写入到了文件中,再将文件以a的方式打开什么都不做,再将文件关闭,我们可以发现文件中的内容并没有消失了,这里可以证明以a的方式打开文件,不会将文件中的内容清空,并且会在文件中的结尾处开始写入。
三、认识操作文件的系统调用
3.1 系统调用的说明
3.1.1 open函数
cpp
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:open函数是系统调用中用于打开/创建文件的函数
参数:
-
pathname:被打开文件的路径和文件名,若只有文件名则默认在当前工作目录搜索这个文件。
-
flags:用于指定文件访问类型的标志。这些标志可以通过按位或运算符(|)组合在一起。常用的标志包括:
- O_RDONLY:以只读方式打开文件。
- O_WRONLY:以只写方式打开文件。
- O_RDWR:以读写方式打开文件。
- O_CREAT:如果文件不存在,则创建它。
- O_TRUNC:如果文件已存在且以写方式打开,则将其长度截断为 0。
- O_APPEND:以追加方式打开文件。写入的数据会被添加到文件末尾。
- O_EXCL:与 O_CREAT 一起使用时,如果文件已存在,则调用失败。
-
mode:(仅在创建文件时使用)用来设置文件权限,这个参数是八进制数,我们看到函数open有两个版本,有一个版本是没有参数mode的,若使用没有mode的函数创建文件会导致文件的权限出现乱码。有mode参数的接口通常用来创建文件,没有mode参数的接口通常用于打开文件。
返回值:
- open 函数返回一个文件描述符(一个非负整数),用于后续的文件操作(如读、写)。
- 如果失败,返回 -1 并设置 errno 以指示错误类型。
3.1.1.1 Linux中常用的传参方法
这里讲述一下关系函数传入标志位的技巧----Linux中常用的传参方法。
我们看到函数open中有一个参数flags,它可以将多个标志组合起来,传个函数,它的原理就是flags是int类型,有32位,它的每一位都能够作为一个标志(这里可能用不到32位),将每一个标志都定义为一个宏对应于32位中的一位。按我们的需要将标志按位或组合起来传个函数,函数中则可以通过与宏按位与得到标志,从而决定是否执行对应标志下的操作。
下面我写了一段代码,定义四个宏,分别对应int类型的低四位,定义一个函数Print,这个函数的内部就是用来获取每一个位上是否存在标记,存在则执行对应的操作,并且这些标志可以互相组合,函数能够将这些标志全部区别开并执行对应的操作。
cpp
#include <stdio.h>
#define Print1 1 // 0001
#define Print2 (1<<1) // 0010
#define Print3 (1<<2) // 0100
#define Print4 (1<<3) // 1000
void Print(int flags)
{
if(flags&Print1)
printf("Print1\n");
if(flags&Print2)
printf("Print2\n");
if(flags&Print3)
printf("Print3\n");
if(flags&Print4)
printf("Print4\n");
}
int main()
{
Print(Print1);
Print(Print1|Print2);
Print(Print1|Print2|Print3);
Print(Print3|Print4);
Print(Print4);
return 0;
}
3.1.2 write函数
cpp
ssize_t write(int fd, const void *buf, size_t count);
功能:write函数是系统调用中用来向文件中输入的函数。
参数:
- fd:指定要写入文件的文件描述符。
- buf:指向要写入文件的数据的指针。
- count:代表写入文件数据的字节数。
3.1.3 close函数
cpp
int close(int fd);
功能:close函数是系统调用中用于关闭一个打卡文件的函数
参数:
- fd:指定要关闭的文件的文件描述符。
3.2 系统调用的使用
cpp
int fd = open("fortest.txt",O_WRONLY|O_CREAT);
将文件以只写的方式打开,若文件不存在则创建文件的方式创建文件,我们发现没有加上创建文件的权限,会导致创建出来的文件权限是乱码,所以若是打开文件不存在需要创建,需要向函数中传入文件权限。
向函数中传入文件权限,可以发现文件确实被创建出来了,但是我们传入文件的权限是0666,但创建出来的文件的权限确实0664,这是因为存在权限掩码,会将传入的文件权限进行过滤,若不想使用系统中的权限掩码,可以使用umask函数在该进程中设置一个权限掩码,那么创建出来的文件的权限只受进程的权限掩码的影响。
cpp
int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
以只写的方式打开文件,若文件不存在则创建文件,打卡文件时将文件内的内容全部清空。我们在此文件中写入一段字符串,并将字符串中的'\0'也写入到文件中,当运行进程后打开文件,我们发现除了我们写入的字符串外还有一个^@
,这实际上就是'\0',由此我们可以知道'\0'是C语言的标准,并不是文件的标准,所以在向文件中写入字符串时,不需要将'\0'写入到文件中。当我们再一次打开文件后,什么都不做就关闭文件,我们发现文件中的内容消失了,这也就证明了以标志O_TRUNC打开文件时,会将文件内所有的内容清除。
cpp
int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
以追加的方式打开文件,若文件不存在则创建文件。我们在此文件中写入一段字符串,我们发现确实向文件中写入了一段字符串。当我们多次向文件中写入字符,可以发现文件中的内容越来越多,可见这时候我们打开文件时,并没有清空文件的内容,而是在文件的结尾处开始继续写入字符串。这也就证明了以标志O_APPEND打开文件时,不会将文件内容清除,而是在为文件结尾处继续写入。
四、C语言文件接口与文件系统接口的关系
通过上面文件接口的使用和系统调用的使用,我们发现下面C标准库中的fopen的功能和系统调用中的open功能一样,我们知道在语言层面上是无法访问磁盘的,所以C标准库中的文件操作函数实际上底层是封装了系统调用的文件操作接口的。
cpp
// C标准库
FILE* fp = fopen("fortest.txt","w");
cpp
// 系统调用
int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
cpp
// C标准库
FILE* fp = fopen("fortest.txt","a");
cpp
// 系统调用
int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
五、文件描述符(fd)
5.1 fd
通过对open函数多次的使用,我们知道fd是文件描述符,那么接下来我们来看看fd中存储的是什么,在下面这段代码中多打开几个文件并输出他们对应的文件描述符,我们发现这些文件描述符是从3开始的小整数,看着与我们学习过的数组下标很像但却没有0、1、2。
当一个进程运行时,操作系统会为其创建一个task_struct,操作系统还会为每个进程创建一个files_struct,进程的task_struct中有一个字段为struct files_struct*file
是用来指向它的files_struct的,files_struct中有一个进程文件描述符表,文件描述符表中有一个数组用来存储被打开文件的地址,当进程使用open函数打开一个文件时,操作系统会为这个文件创建一个结构体对象,并将这个对象的地址存储到进程文件描述符表中,我们看到的fd(文件描述符)实际上就是指向这个文件对象的数组下标,最后将fd返回给上层。
小结:
- 文件描述符的本质就是数组的下标
- 操作系统不希望进程和文件的耦合度太高了,所以为进程创建了一个文件描述符表,这个表中可以存储文件结构体对象的地址,通过文件描述符表将进程与文件联系起来。
5.2 fd为0、1、2的文件
在我们对文件描述符进行了测试后,发现被打开的文件的文件描述符是从3开始的,并没有从0开始,因为在进程被打开时,操作系统就将stdin(0),stdout(1),stderr(2)打开了,我们之前讲到过操作系统下一切皆文件,这些硬件设备也可以被视为文件。
设备 | 标识符 | 文件描述符 | |
---|---|---|---|
标准输入 | 键盘 | stdin | 0 |
标准输出 | 显示屏 | stdout | 1 |
标准错误 | 显示屏 | stderr | 2 |
下面使用代码验证一下在我们没有打打开标准输入/输出/错误的情况下,能不能对这几个文件进行操作,并验证文件描述符是否与之对应。
通过对下面代码的运行我们发现在我们没有打打开标准输入/输出/错误的情况下,可以对这几个文件进行操作,并且文件描述符与上述的一 一对应,再打开一个文件并输出它的文件描述符也确实是从3开始的。
操作系统在运行进程后,默认将标准输入/输出/错误打开,是为了让程序员默认进行输入输出代码的编写。
5.3 如何理解操作系统下一切皆文件
在硬件层中,每一个硬件至少会有读/写的方法,并且每个硬件的读写方法都不可能相同,在操作系统打开硬件的时候,会为其创建一个结构体对象,这个对象中存在两个函数指针,用于指向硬件的读写方法,当我们想调用硬件的读写方法时,可以通过函数指针来调用,而不是通过底层的读写方式来调用,那么就完成了对硬件读写操作的统一。
学习过面向对象语言的同学看下面这幅图会有什么想法呢?
大家可能会想到多态,Linux操作系统底层使用C语言编写的,C语言本身并不直接支持面向对象编程中的继承和多态,但是可以通过结构体和函数指针模拟了继承和多态的行为。
5.4 C语言中的FILE
我们看到C标准库中的fopen函数的返回值是*FILE,FILE是C语言定义的一个结构体类型,由于操作系统打开文件只认fd文件描述符,所以我们知道FILE结构体中必定封装了文件描述符。
六、理解struct file内核对象
当一个进程运行时,操作系统会为其创建一个task_struct,操作系统还会为每个进程创建一个files_struct,files_struct中有一个进程文件描述符表,文件描述符表中有一个数组用来存储被打开文件的结构体对象的地址,结构体对象中有一个字段是用来指向文件所有属性的,有一个字段是用来指向文件的操作方法集的,还有个字段是用来指向文件缓冲区的。
从文件中读取数据时,用户通常会定义一个字符串作为用户的缓冲区,当上层通过read/fread函数向文件中读取内容时,本质上是通过找到调用函数进程的task_struct,task_struct中有一个指针指向files_struct,files_struct中有一个进程文件描述符表,通过函数参数中的fd找到文件描述符表中下标对应的文件结构体对象,对象中有一个指针指向文件缓冲区,然后文件缓冲区中的数据将拷贝到用户缓冲区中,我们就可以得到文件中的内容了。
向文件中写入数据时,用户通常会定义一个字符串作为用户的缓冲区,当上层通过write/fwrite函数向文件中写入内容时,本质上是通过找到调用函数进程的task_struct,task_struct中有一个指针指向files_struct,files_struct中有一个进程文件描述符表,通过函数参数中的fd找到文件描述符表中下标对应的文件结构体对象,对象中有一个指针指向文件缓冲区,然后用户缓冲区中的数据将拷贝到文件缓冲区中,最终将文件缓冲区上的内容写入到文件中。
若进程打开文件是为了从文件中读取数据,若文件不在文件缓冲区中,操作系统会发生缺页中断,将文件中的数据加载到文件缓冲区中,最后将文件缓冲区中的数据拷贝到用户缓冲区中。若进程打开文件是为了向文件中写入数据,增加/修改/删除数据都是向文件中写入数据,操作系统要修改文件不能在磁盘上修改,所以要进行这些操作,就需要先将文件加载到文件缓冲区中,然后将用户缓冲区中的内容拷贝到文件缓冲区中,最后将文件缓冲区中的内容写入到文件中。所以无论是向文件中读取还是写入数据都需要将文件的内容加载到文件缓冲区中。
所以我们在应用层上进行数据读写的本质就是将内核缓冲区中的数据进行来回拷贝。
七、fd的分配规则
- 操作系统默认在进程运行时,将文件描述符为0、1、2的文件打开,我们可以直接使用0、1、2进行数据的访问
- 文件描述符的规则就是遍历文件描述符表中的数组,寻找数组中下标最小且该位置没有被使用的位置,用来分配给指定的被打开文件。
下面的代码中,我们分别将文件标识符为0和2的文件关闭后,在新打开一个文件,我们发现新打开文件的文件描述符确实分别为0和2,符合fd的分配规则。
八、重定向
重定向通常指的是改变数据输入或输出的流向,从默认的设备(如键盘或屏幕)改为文件或其他设备。
在上面讲述fd的分配规则时,我们分别将文件标识符为0和2的文件关闭后,在新打开一个文件,这里我们将表示文件标识符为1的文件关闭后,然后再新打开一个文件,最后输出一段数据,运行进程观察现象,我们发现本应该显示在显示屏上的数据,最后却写入到了这个被新打开的文件中?
这是因为printf只认stdout,stdout的描述符又为1,与其说printf只认stdout,不如说printf只认文件描述符为1的文件,printf并不会管文件描述符表中的数组内容是如何变化的,它只会将数据写入到文件描述符为1的文件中。这里我们将标准输入关闭,新打开的文件文件描述符就是1,所以printf将数据写入到新文件中,并且新打开文件的文件描述符为1,符合fd的分配规则。
8.1 输出重定向
这里我们以只写并且打开文件会清空文件内容的方式打开文件,我们将默认的输出设备(显示屏)修改为了这个新打开的文件,这样本该输出到显示屏的数据却写入到了文件中,由于这个文件被打开时会被清空内容,所以这里的重定向我们称之为输出重定向。
上面我们是先将文件标识符为1的文件先关闭后,再打开一个新的文件后,才让这个文件的文件标识符变为1,而系统调用中有一个函数dup,dup函数能够使文件标识符表中为fd1为下标的内容直接覆盖到另一个文件标识符表中为fd1为下标的内容。
8.2 dup2函数
cpp
int dup2(int oldfd, int newfd);
参数oldfd为要复制的文件描述符。
参数newfd为目标文件描述符。如果 newfd 已经打开,则它会被关闭,除非 newfd 和 oldfd 相同。
8.3 追加重定向
这里我们以只写并且向文件中写入数据时会从文件的尾部开始写入的方式打开文件,我们将默认的输出设备(显示屏)修改为了这个新打开的文件,这样本该输出到显示屏的数据却写入到了文件中,由于向文件中写入内容时是向文件中的尾部开始写入,所以这里的重定向我们称之为追加重定向。
8.4 输入重定向
既然能够覆盖stdout让printf将数据写入被打开文件,那么我们也能够覆盖stdin让scanf/fgets函数从文件中读取数据。以下面的代码为例,fread本应该从键盘中读取数据,因为被文件覆盖了,导致最终从文件中读取数据,我们称之为输入重定向。
8.5 重定向的本质
重定向的本质是修改文件描述符表中特殊下标的指向,使其指向其他的文件或资源。当上层读写函数使用这些特殊文件描述符时,它们就会读写重定向后的文件或资源。
8.6 重定向与进程替换之间会不会互相影响
首先给出结论重定向与进程替换之间是不会互相影响的,重定向影响的是进程的文件描述符表和文件结构体对象,而进程替换影响的是进程的进程地址空间、进程的页表和物理内存,所以重定向与进程替换之间是不会互相影响的。
8.7 使用重定向来讲述标准错误存在的原因
在以前的学习过程中,我们会常常使用到标准输入和标准输出,很少使用到标准错误,并且标准输出和标准错误的设备都是显示屏,接下来我就为大家讲解这样做的原因。
观察下面的代码,我们分别向标准输出和标准错误中输出一条信息,运行程序我们发现打印了两条信息,当我们使用输出重定向将运行结果写入到文件中时,我们发现输出到标准错误中的信息输出到了显示屏上,输出到标准错误中的信息写入到了文件中,因为标准重定向是向fd为1的文件中写入。
接下来我们使用./myprocess > fortest.txt 2>&1
将标准输入和标准错误都重定向到文件中,查看文件发现确实都写进去了,一个命令就可以是标准输入和标准输出都重定向到一个文件中,那么我们也可以使用一个命令将标准输入和标准输出分别重定向到两个不同的文件中,通过下图发现也确实分别将信息写入到了两个文件中,到这里就能就是标准错误存在的原因了,标准错误的存在就可以将标准信息和错误信息分别写入到两个文件中,这样可以方便我们查看错误信息,方便排查代码错误。
九、缓冲区
9.1 缓冲区的简单理解
我们理解的缓冲区其实就是一小部分内存,那么为什么会有缓冲区这个概念呢?下面就举个例子来帮助大家理解。
假设你在新疆,而你的朋友在北京,并且这时候并没有快递这个概念,如果说你要给你的朋友送一下特产,你就需要自己花很长时间千里迢迢的将这个特产送过去,当特产送到了你朋友的手上你才会认为东西已经送了。而过了一两年后,突然有了快递这个行业,而你和你朋友的周边就正好有一家快递站,这时你又想给你的朋友送点特产,那么你就可以把东西准备好,让后放到快递站让快递员给你送过去,当你把快递放到了快递站的时候,你就会认为这个东西已经送了,在这个例子中,快递站就可以被理解为缓冲区,没有缓冲区的时候,就需要从一个设备中将数据直接拷贝到另一个设备中,而有了缓冲区后,只要将数据写到缓冲区中,那么这个数据就会由操作系统来帮你写入。缓冲区的主要作用就是提高使用者的效率。
那么继续,快递站并不会因为只收到了你的东西,就直接将东西快马加鞭的送过去,这样做的成本会非常的高,快递站能够存储东西,所以快递站会根据东西的情况来决定是否发送,可以是一个货架满了再发送,也可以是整个快递站满了之后再发送,还有特殊的发货条件就是,若是很多天都没有货导致一直没有发送,你也可以打电话投诉让他直接发送,也可能是快递站要倒闭了,在倒闭之前讲东西全部发送出去,这几种情况就分别对应了缓冲区的刷新方式:
-
一般情况:
- 无缓冲(立即刷新)
- 行缓冲(行刷新)
- 全缓冲(全刷新)
-
特殊情况:
- 强制刷新
- 进程退出时,一般要刷新缓冲区
一般对于显示器文件等,通常默认为行缓冲(行刷新)
一般对于磁盘上的文件,通常默认为全缓冲(全刷新)
9.2 缓冲区的样例
在下面这段代码中,我们使用三个C标准库中的函数向标准输出中写入三段数据,再用一个系统调用向标准输出写入一段数据,运行代码观察结果,我们发现这四条数据确实被打印出来了,当我们使用输出重定向将写入标准输出的数据写入到文件中,观察文件内容,发现四条消息都被写入了,但是顺序有些不同。
还是刚刚那段代码,但是在结尾处加入一个fork函数用来创建子进程,运行代码观察结果,我们发现这四条数据也被打印出来了,当我们使用输出重定向将写入标准输出的数据写入到文件中,观察文件内容,我们发现系统调用向文件中只写入了一条数据,而每个C标准库中的函数却向文件中写入了两条数据。
9.3 理解样例
- 当我们向显示器打印时,显示器文件默认的刷新方式为行刷新,而我们代码中每个输出的字符串后面都带了一个'\n',所以在fork之前,缓冲区中的内容都已经被刷新了。
- 当我们重定向到fortest.txt时,本质上就是向文件中写入了,文件的默认刷新方式从行刷新变为了全刷新。
- 全刷新意味着缓冲区变大,我们这里写的简单数据不足以将缓冲区写满,fork执行时,数据还在缓冲区中。
- 通过上面的现象我们知道了fork的执行并没有影响系统调用,而C标准库中的IO接口底层都是封装了系统调用的IO接口,所以我们目前所谈的"缓冲区"与操作系统没有关系。
- C/C++中的缓冲区中保存的是用户的数据,属于我们当前运行进程的数据,当我们将数据写入到操作系统中时,那么这些数据就不再属于进程了,而是属于操作系统了。
- 当我们退出进程时,一般需要进行刷新缓冲区的操作,这个操作属于清空或是"写入"操作,即便这时候的数据没有满足刷新条件,fork调用以后,任意一个进程退出以后,就要发生写时拷贝,将缓冲区中的内容拷贝给另一个进程,另一个进程退出以后,还会刷新缓冲区,所以我们看到了数据被打印了两次。
- 系统调用在C语言之下,不能使用C语言的缓冲区,而是直接将数据写入给了操作系统,不会发生写时拷贝,所以系统调用只写入了一条数据。
上面我们一直提到刷新,刷新就是将C语言缓冲区中是数据写入到操作系统的文件缓冲区中,操作系统会负责将这些数据最终写入到磁盘上的文件中或发送到其他设备。
9.4 缓冲区的位置
任何情况下,我们进行输入输出的操作时,都有一个FILE,FILE是一个结构体,里面包含了fd(文件描述符),还提供了一段缓冲区。在C标准库中,有一些IO操作函数,它们在设计上默认操作标准输入(stdin)、标准输出(stdout)或标准错误(stderr)。这些函数在调用时无需显式传递FILE类型的指针,因为它们在内部已经预定义了这些标准流。
接下来我们在C标准库中查找一下FILE,是否符合上面的内容。
9.5 stdio.h的模拟实现(原理)
这里的模拟实现只代表了原理上的说明,与真实库中的头文件相差甚远。
cpp
// mystdio.h
#pragma once
#define SIZE 1024
typedef struct _myFILE
{
int fileno;
char buffer[SIZE];
int flag;
int end;
}myFILE;
myFILE* myfopen(const char* path , const char* mode);
int myfwrite(const void *ptr,int size,int num,myFILE* stream);
int myfflush(myFILE* stream);
int myfclose(myFILE* fp);
cpp
// mystdio.c
#include "mystdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#define DEL_MODE 0666
#define FLUSHNONE 1
#define FLUSHLINE (1<<1)
#define FLUSHALL (1<<2)
myFILE* myfopen(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_WRONLY | O_CREAT | O_TRUNC);
}
else if(strcmp(mode,"a") == 0)
{
flag |= (O_WRONLY | O_CREAT | O_APPEND);
}
else
{
// 这里只写三种情况
// do nothing
}
if(flag & O_CREAT)
{
fd = open(path,flag,DEL_MODE);
}
else
{
fd = open(path,flag);
}
if(fd < 0)
{
errno = 2; // 设置错误码
return NULL;
}
myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
if(fp == NULL)
{
errno = 3;
return NULL;
}
fp->fileno = fd;
fp->end = 0;
fp->flag = FLUSHLINE;
return fp;
}
int myfwrite(const void *ptr,int size, int num,myFILE* stream)
{
memcpy(stream->buffer+stream->end,ptr,num*size);
stream->end+=num;
if(stream->flag == FLUSHLINE && stream->end > 0 && stream->buffer[stream->end-1] == '\n')
{
myfflush(stream);
}
return 0;
}
int myfflush(myFILE* stream)
{
if(stream->end > 0)
{
write(stream->fileno,stream->buffer,stream->end);
}
stream->end = 0;
return 0;
}
int myfclose(myFILE* fp)
{
myfflush(fp);
return close(fp->fileno);
}
接下来就用代码来测试一下,在下面这段代码中,我们向文件中每隔一秒写入一段字符串,并且在这段字符串的结尾有一个'\n',一边运行程序,一边查看文件内容,我们发现每隔一秒确实向文件中写入的一段字符串。
下面这段代码与上面的代码一样,只是字符串后面没有了'\n',一边运行程序,一边查看文件内容,这时候我们发现文件中并没有写入字符串,而是过了一段时间程序结束后,将所有的字符串一下子全部写入到文件中。
根据上面两段代码的运行结果,我们就能够发现缓冲区的存在了。
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹