1.理解"文件"
1.1狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设(既是输出设备也是输入设备)
- 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出 简称IO。
1.2广义理解
Linux下一切皆文件(键盘、显示器、网卡、磁盘......这些都是抽象化的过程)
1.3文件操作的归类认知
- 对应0KB的空文件是占用磁盘空间的,因为它要存储文件名、大小、创建时间、权限等属性信息
- 文件时文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)
- 所有的文件操作本质是文件内容操作和文件属性操作
1.4系统角度
- 对文件的操作本质是进程对文件的操作
- 磁盘的管理者是操作系统
- 文件的读写本质不是通过C语言/C++的库函数来操作的,而是通过文件相关的系统调用接口来实现的
2.进程与文件的关系
核心结论:本质是进程在操作文件
- 文件必须先被打开才能访问,而打开文件时程序运行时的动作
- 仅在代码中写了fopen但不运行程序,文件不会被打开。
- 只有程序加载到内存成为进程,执行到open函数,文件才真正被打开
一个进程可以同时打开多个文件(系统默认打开三个)
- 操作系统需要管理所有被打开的文件
- 内核会为每个被打开的文件创建对应的内核数据结构(文件对象)
- 进程与被打开文件的关系,本质是两种内核数据结构之间的关联关系
2.1系统调用与库函数的关系
核心结论:所有语言的文件操作接口,底层都封装了操作系统的系统调用
- 磁盘是硬件,只有操作系统才有资格直接访问和控制硬件
- 操作系统向上提供统一的文件系统调用接口(如open、close、read、write)
- C语言的fopen、fclose、fread、fwrite等都是库函数,它们底层封装了系统调用。
- 封装的意义:提高代码的可移植性
● 基于标准库写的文件操作代码,在Linux和Windows上无需修改即可运行(基于标准库的文件操作代码可跨平台编译,但需针对路径、换行符等差异做适配。)
● 不同操作系统的系统调用不同,但标准库屏蔽了这些差异
3.回顾C文件接口
3.1 打开文件

cpp
4 int main()
5 {
6 FILE *fp = fopen("log.txt", "w");
7 if(!fp)
8 {
9 printf("fopen error!\n");
10 }
11 fclose(fp);
12 return 0;
13 }
~
3.2 写文件

cpp
4 int main()
5 {
6 FILE* fp = fopen("log.txt", "w");
7 if(!fp)
8 {
9 printf("fopen error!\n");
10 }
11
12 const char* msg = "hello bit!\n";
13 int count = 5;
14 while(count--)
15 {
16 fwrite(msg, strlen(msg), 1, fp);
17 }
18 return 0;
19 }

3.3读文件
cpp
4 int main()
5 {
6 FILE* fp = fopen("log.txt", "r");
7 if(!fp)
8 {
9 printf("fopen error!\n");
10 return 1;
11 }
12
13 char buf[1024];
14 const char* msg = "hello bit!\n";
15
16 while(1)
17 {
18 size_t s = fread(buf, 1, strlen(msg), fp);
19 if(s > 0)
20 {
21 buf[s] = 0;
22 printf("%s", buf);
23 }
24 if(feof(fp))
25 {
26 break;
27 }
28 }
29 fclose(fp);
30 return 0;
31 }
3.4模拟实现cat
cpp
4 int main(int argc, char* argv[])
5 {
6 if(argc != 2)
7 {
8 printf("argv error!\n");
9 return 1;
10 }
11 FILE *fp = fopen(argv[1], "r");
12 if(!fp)
13 {
14 printf("fopen error!\n");
15 return 2;
16 }
17 while(1)
18 {
19 char buf[108];
20 memset(buf, 0, sizeof(buf));
21 int n = fread(buf, 1, sizeof(buf)-1, fp);
22 if(n > 0)
23 {
24 printf("%s", buf);
25 }
26 if(feof(fp))
27 break;
28 }
29 return 0;
30 }

3.5 输出信息到显示器有那些方法
cpp
4 int main()
5 {
6 const char* msg = "hello fwrite\n";
7 fwrite(msg, strlen(msg), 1, stdout);
8
9 printf("hello printf\n");
10 fprintf(stdout, "hello fprintf\n");
11 return 0;
12 }
3.6 stdin&stdout&stderr
- C默认会打开三个输入输出流,分别是stdin,stdout,stderr
- 这三个流的类型都是FILE*,fopen返回值类型,文件指针
|--------|------|-------|----------|
| 流名 | 类型 | 对应设备 | 作用 |
| stdin | 标准输入 | 键盘文件 | 程序获取输入数据 |
| stdout | 标准输出 | 显示器文件 | 程序输出正常结果 |
| stderr | 标准错误 | 显示器文件 | 程序输出错误信息 |
默认打开的原因:
- 程序的核心都是数据处理,需要默认的数据源(输入)和数据出口(输出)
- 避免用户每次都手动打开设备文件,简化编程
3.7 文件的打开方式
|----|----|---------------------------------|
| 模式 | 含义 | 特点 |
| r | 只读 | 文件必须存在,否则打开失败 |
| w | 只写 | 文件不存在则创建;文件存在则先清空内容 |
| a | 追加 | 文件不存在则创建;文件存在则在文件末尾追加写入 |
| r+ | 读写 | 文件必须存在;读写都从文件开头开始 |
| w+ | 读写 | 文件不存在则创建;文件存在则先清空内容 |
| a+ | 读写 | 文件不存在则创建;写入总是在文件末尾,读取从开头开始 |
- 输出重定向(>):本质是以w方式打开目标文件,先清空再写入
- 追加重定向(>>):本质是以a方式打开目标文件,在末尾追加写入
3.8 文件操作细节
读写位置指针:
- 文件内部有一个读写位置指针,读写操作都会移动这个指针
- 以读写方式打开文件时,写完内容后直接读不到刚写的数据,需要先移动指针
- 使用fseek(fp,0,SEEK_SET)将指针移动到文件开头
字符串写入注意事项
- 不要以字符串结束符'\0'写入文件
- '\0'是C语言的规定,不是文件的规定,写入后会变成不可见字符(乱码)
读写难度差异:
- 写文件容易,读文件难,因为读取时需要解析文件格式
- 解决方案:将数据以结构体二进制方式写入,读取时也已结构体方式读取(序列化/反序列化)
4.系统文件I/O
4.1 open接口介绍
open是Linux操作系统提供的底层文件打开系统调用

- pathname:要打开或创建的目标文件
- mode:新建文件的权限,仅当使用O_CREAT时需要传入
- flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags。每个标志位是一个宏,只有一个比特位为1,且互不重复
参数:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读写打开
- O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND:追加写
- O_TRUNC:清空文件内容
返回值:
成功:新打开的文件描述符
失败:-1并设置errno
4.2 位图传参原理
操作系统使用一个整数的不同比特位来表示不同的标志,通过按位或组合多个标志
cpp
4 #define ONE (1<<0)
5 #define TWO (1<<1)
6 #define THREE (1<<2)
7 #define FOUR (1<<3)
8
9 void func(int flags)
10 {
11 if(flags & ONE) printf("flags has ONE!");
12 if(flags & TWO) printf("flags has TWO!");
13 if(flags & THREE) printf("flags has THREE!");
14 if(flags & FOUR) printf("flags has FOUR!");
15 printf("\n");
16 }
17
18 int main()
19 {
20 func(ONE);
21 func(ONE | TWO);
22 func(ONE | TWO | THREE);
23 func(FOUR);
24 return 0;
25 }

4.3文件操作基本接口
- fopen,fclose,fread,fwrite都是C标准库当中的函数,我们称之为库函数。
- 而open,close,read,write,lseek都属于系统提供的接口,称之为系统调用接口。
//关闭文件
int close(int fd);
//写入文件
ssize_t write(int fd, const void *buf, szie_t count);
//返回值:实际写入的字节数,失败返回-1.
ssize_t read(int fd, void *buf, size_t count);
//返回值:实际读取的字节数,0表示到达文件末尾,失败返回-1
重要结论:
- 系统层面不区分文本写入和二进制写入,所有写入都是二进制流。
- 文本写入时语言层提供的概念,本质是将数据格式化为字符串后再写入
- printf、fprintf等格式化函数在底层都会调用write系统调用
4.4文件描述符详解
4.4.1什么是文件描述符
文件描述符是一个非负整数,是进程访问文件的"句柄"
每个进程默认打开3个文件描述符:
- 0:标准输入(stdin),对应键盘
- 1:标准输出(stdout),对应显示器
- 2:标准错误(stderr),对应显示器
新打开的文件会从3开始分配文件描述符
4.4.2文件描述符的本质
文件描述符是进程文件描述符表的数组下标
内核数据结构关系:
- 每个进程都有一个进程控制块(PCB)
- PCB中包含一个指向文件描述符表的指针
- 文件描述符表是一个struct file*类型的指针数组
- 数组中的每个元素指向内核中一个被打开的文件对象(struct file)
- struct file包含文件的属性、读写位置、缓冲区、操作方法等信息
文件描述符的分配规则:分配最小的未被使用的文件描述符。
cpp
20 int main()
21 {
22 int fd = open("myfile", O_RDONLY);
23 if(fd < 0)
24 {
25 return 1;
26 }
27 printf("fd:%d\n", fd);
28 close(fd);
29 return 0;
30 }

这个时候fd为3,关闭0或者2
cpp
22 int main()
23 {
24 close(0);
25 int fd = open("myfile", O_RDONLY);
26 if(fd < 0)
27 {
28 return 1;
29 }
30 printf("fd:%d\n", fd);
31 close(fd);
32 return 0;
33 }

那如果关闭1呢?
cpp
22 int main()
23 {
24 close(1);
25 int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
26 if(fd < 0)
27 {
28 return 1;
29 }
30 printf("fd:%d\n", fd);
31 fflush(stdout);//关闭文件时,缓冲区里的数据还没来得及写进去,直接丢了!需要刷新缓冲区
32 close(fd);
33 return 0;
34 }

此时,我们发现,本来应该输出到显示器上的内容,输出到了log.txt文件中,这种现象我们叫做输出重定向。常见的重定向有:>,>>,<。
4.4.3重定向原理
重定向的本质:修改文件描述符表中对应下标的指针指向。

上层代码始终使用固定的文件描述符(0,1,2),但底层指针指向的文件对象发生了变化,上层完全透明。
4.4.4使用dup2系统调用
#include<unistd.h>
int dup2(int oldfd, int newfd)
功能:将newfd指向oldfd指向的文件,必要时先关闭newfd原来指向的文件。
参数:
- oldfd:源文件描述符
- newfd:目标文件描述符
- 返回值:成功返回newfd,失败返回-1。
cpp
22 int main()
23 {
24 int fd = open("log.txt", O_CREAT | O_RDWR, 0666);
25 if(fd < 0)
26 {
27 return 1;
28 }
29 close(0);
30 dup2(fd, 0);
31 while(1)
32 {
33 char buffer[64];
34 if(!fgets(buffer, sizeof(buffer), stdin)) break;
35
36 printf("%s",buffer);
37 }
38 return 0;
39 }

不同类型的重定向:
- 输出重定向(>):以清空方式打开文件,dup2(fd,1)
- 追加重定向(>>):以追加方式打开文件,dup2(fd,1)
- 输入重定向(<):以只读方式打开文件,dup2(fd,0)
进程替换不会影响重定向结构:进程替换值替换代码和数据,不会改变内核中的文件描述符表。
4.5语言层封装与跨平台性
- C语言的FILE*是一个结构体指针,封装了文件描述符
- FILE结构体中包含了文件描述符、缓冲区、读写位置等信息
语言层封装的意义
- 屏蔽平台差异:不同操作系统的系统调用接口不同,语言层封装后提供统一接口。
- 提高可移植性:使用语言层接口编写的代码可以在不同平台上编译运行
- 提供更高级的功能:如:格式化输入输出,按行读写。
文件引用计数:
- 内核中的每个struct file对象都有一个引用计数
- 当多个文件描述符指向同一个文件对象时,引用计数加1
- 调用close时,引用计数减1,当引用计数为0时,文件对象才会被释放。
缓冲区问题:
- 系统调用read和write是不带缓冲区的
- C语言函数fread、fwrite、printf等是带缓冲区的
- 缓冲区的存在可以减少系统调用的次数,提高I/O效率。
4.6标准错误重定向
cpp
#include<cstdio>
#include<unistd.h>
#include<iostream>
using namespace std;
int main()
{
printf("hello printf\n");
cout << "hello cout" << endl;
cerr << "hello cerr" << endl;
fprintf(stderr, "hello stderr\n");
return 0;
}
普通输出重定向 >log.txt只重定向标准输出,标准错误仍会打印到显示器。( > 等价于 1>,只重定向标准输出(stdout))

分别将标准输出和标准错误重定向到同一个文件

./a.out > log.txt 2>&1
原理:先将1重定向到log.txt,再将2的内容拷贝到1中,使两者指向同一个文件。

标准错误存在的意义:
- 将常规输出消息和错误消息分离,方便日志排查
- 调试时可以只关注错误信息,避免与正常输出混淆
- 所有编程语言都提供标准错误,就是为了利用重定向能力实现日志分级
5.理解"一切皆文件"
在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,可以使用访问文件的方法访问它们获得信息;甚至管道也是文件。
这样做最明显的好处是,**开发者仅需要使用一套API和开发工具,即可调用Linux系统中绝大部分的资源。**Linux中几乎所有读的操作都可以用read函数来进行;几乎所有更改的操作都可以使用write函数来进行。
当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体。
cpp
struct file {
...
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向它,就会增加f_count的值。
unsigned int f_flags; // 表⽰打开⽂件的权限
fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义
loff_t f_pos; // 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK
*/
struct file中的f_op指针指向了一个file_operations结构体,这个结构体中的成员除了struct module* owner其余都是函数指针。该结构和struct file都在fs.h下
cpp
struct file_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负,
返回值代表成功写的字节数.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned
long, loff_t);
//初始化设备上的⼀个异步写.
int (*readdir) (struct file *, void *, filldir_t);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤
返回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//⽤⼾调⽤来刷新任何挂着的数据.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实
现它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t
*, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应这一个系统调用。读取file_operation中相应的函数指针,接着把控制权交给函数,从而完成了Linux设备驱动程序的工作。

内核缓冲区:操作系统将磁盘文件的内容加载到内存的缓冲区中,所有文件读写操作都先操作缓冲区,再由操作系统决定何时刷新磁盘。
内存管理:操作系统将内存划分为4KB大小的页,通过struct_page结构体管理,文件缓冲区就是由这些内存页组成的
5.1虚拟文件系统(VFS)
VFS是内核中的一个软件抽象层,向上提供统一的文件操作接口,向下适配不同的文件系统和硬件设备
核心机制:函数指针实现多态
- struct file中的f_op指针指向不同设备的具体操作方法
- 例如:磁盘的read/write方法和显示器的read/write方法实现完全不同,但上层调用的接口完全一致。
- 操作系统通过函数指针自动调用对应设备的驱动程序,屏蔽了底层硬件差异。
6.缓冲区
6.1什么是缓冲区
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
6.2为什么要引入缓冲区机制
读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此文件,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,一次从文件中读出大量的数据到缓冲区,以后对着部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相对的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低俗的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放CPU,使其能够高效率工作。
6.3 缓冲类型
标准I/O提供了3种类型的缓冲区。
- 全缓冲区:这种缓冲方式要求填满整个缓冲区才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
- 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时,使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
- 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流strerr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:
- 缓冲区满时;
- 执行flush语句;
- 进程结束;
cpp
1 #include<cstdio>
2 #include<cstring>
3 #include<unistd.h>
4 #include<iostream>
5 #include<sys/wait.h>
6 using namespace std;
7
8 int main()
9 {
10 printf("hello printf\n");
11 fprintf(stdout, "hello fprintf\n");
12 const char* s = "hello fwrite\n";
13 fwrite(s, strlen(s),1 , stdout);
14
15 const char* ss = "hello write\n";
16 write(1, ss,strlen(ss));
17
18 fork();
19 return 0;
20 }

现象:
直接运行(输出到显示器):打印4条消息
重定向到文件:打印7条消息(系统调用write只打印1次,3个库函数各打印2次)
原因:
- 输出到显示器时采用行缓冲,\n会立即刷新缓冲区,fork时缓冲区已空
- 重定向到文件时采用全缓冲,fork时数据仍在用户级缓冲区中
- fork会创建子进程,复制父进程的地址空间(包括缓冲区)
- 父子进程退出时各自刷新缓冲区,导致库函数输出重复
- 系统调用write直接写入内核缓冲区,不存在用户级缓冲区,所以不会重复。
用户级缓冲区(语言级缓冲区):由C标准库提供,存在与FILE结构体中
内核级缓冲区:由操作系统内核维护,存在于struct file对应的内存页中。
6.4 简易C标准库封装
cpp
#pragma once
#include<stdio.h>
#define MAX 1024
#define NONE_FLUSH (1<<0)//无缓冲区
#define LINE_FLUSH (1<<1)//行缓冲区
#define FULL_FULSH (1<<2)//全缓冲区
typedef struct IO_FILE
{
int fileno;//文件操作符
int flag;//
char outbuffer[MAX];//缓冲区
int bufferlen;//缓冲区已用长度
int flush_method;//缓冲区的刷新方式
}MyFile;
MyFile *MyFopen(const char* path, const char* mode);//打开文件
void MyFclose(MyFile* );//关闭文件
int MyFwrite(MyFile*, void *str, int len);//写文件
void MyFFlush(MyFile*);//刷新缓冲区
cpp
#include"mystdio.h"
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
static MyFile* BuyFile(int fd, int flag)
{
MyFile* f = (MyFile*)malloc(sizeof(MyFile));
if(f == NULL) return NULL;
f->bufferlen = 0;
f->fileno = fd;
f->flag = flag;
f->flush_method = LINE_FLUSH;//默认行缓冲
memset(f->outbuffer, 0, sizeof(f->outbuffer));
return f;
}
MyFile* MyFopen(const char* path, const char *mode)//mode是打开方式
{
int fd = -1;
int flag = 0;
if(strcmp(mode, "w") == 0)
{
flag = O_CREAT | O_WRONLY | O_TRUNC;
fd = open(path, flag, 0666);
}
else if(strcmp(mode, "a") == 0)
{
flag = O_CREAT | O_WRONLY | O_APPEND;
fd = open(path, flag, 0666);
}
else if(strcmp(mode, "r") == 0)
{
flag = O_RDONLY;
fd = open(path, flag);
}
if(fd < 0) return NULL;
return BuyFile(fd, flag);
}
void Myclose(MyFile* file)
{
if(file->fileno < 0) return ;
MyFFlush(file);//关闭前刷新缓冲区
close(file->fileno);
free(file);
}
int MyFwrite(MyFile* file, void *str, int len)
{
//拷贝往缓冲区里写
memcpy(file->outbuffer+file->bufferlen, str, len);
file->bufferlen += len;
//判断是否满足刷新条件
if((file->flush_method == LINE_FLUSH) && file->outbuffer[file->bufferlen - 1] == '\n')
{
MyFFlush(file);
}
return 0;
}
void MyFFlush(MyFile* file)
{
if(file->bufferlen <= 0) return ;
//把数据从用户拷贝到内核文件缓冲区中
int n = write(file->fileno, file->outbuffer, file->bufferlen);
(void)n;//消除编译器警告
fsync(file->fileno);
file->bufferlen = 0;
}
- write只是将数据从用户空间拷贝到内核级缓冲区,并未真正写入磁盘。若要强制将内核缓冲区数据刷新到磁盘,需调用系统调用fsync(fd)。
- 数据库落盘原理:所有数据库本质都是进程,对数据的增删查改先在内存中运行,最终通过fsycn将数据持久化到磁盘文件,这个过程称为落盘
cpp
#include"mystdio.h"
#include<string.h>
#include<unistd.h>
int main()
{
MyFile *file = MyFopen("./log.txt", "a");
if(file == NULL)
{
printf("fopen error!\n");
return 1;
}
int cnt = 10;
while(cnt--)
{
char* msg = (char*)"hello myfile";
MyFwrite(file, msg, strlen(msg));
MyFFlush(file);
printf("buffer:%s\n", file->outbuffer);
sleep(1);
}
MyFclose(file);
return 0;
}