基础IO

1.理解"文件"

1.1狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设(既是输出设备也是输入设备)
  • 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出 简称IO。

1.2广义理解

Linux下一切皆文件(键盘、显示器、网卡、磁盘......这些都是抽象化的过程)

1.3文件操作的归类认知

  • 对应0KB的空文件是占用磁盘空间的,因为它要存储文件名、大小、创建时间、权限等属性信息
  • 文件时文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)
  • 所有的文件操作本质是文件内容操作和文件属性操作

1.4系统角度

  • 对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读写本质不是通过C语言/C++的库函数来操作的,而是通过文件相关的系统调用接口来实现的

2.进程与文件的关系

核心结论:本质是进程在操作文件

  1. 文件必须先被打开才能访问,而打开文件时程序运行时的动作
  • 仅在代码中写了fopen但不运行程序,文件不会被打开。
  • 只有程序加载到内存成为进程,执行到open函数,文件才真正被打开

一个进程可以同时打开多个文件(系统默认打开三个)

  • 操作系统需要管理所有被打开的文件
  • 内核会为每个被打开的文件创建对应的内核数据结构(文件对象)
  • 进程与被打开文件的关系,本质是两种内核数据结构之间的关联关系

2.1系统调用与库函数的关系

核心结论:所有语言的文件操作接口,底层都封装了操作系统的系统调用

  1. 磁盘是硬件,只有操作系统才有资格直接访问和控制硬件
  2. 操作系统向上提供统一的文件系统调用接口(如open、close、read、write)
  3. C语言的fopen、fclose、fread、fwrite等都是库函数,它们底层封装了系统调用。
  4. 封装的意义:提高代码的可移植性
    ● 基于标准库写的文件操作代码,在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文件描述符的本质

文件描述符是进程文件描述符表的数组下标

内核数据结构关系:

  1. 每个进程都有一个进程控制块(PCB)
  2. PCB中包含一个指向文件描述符表的指针
  3. 文件描述符表是一个struct file*类型的指针数组
  4. 数组中的每个元素指向内核中一个被打开的文件对象(struct file)
  5. 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中,使两者指向同一个文件。

标准错误存在的意义:

  1. 将常规输出消息和错误消息分离,方便日志排查
  2. 调试时可以只关注错误信息,避免与正常输出混淆
  3. 所有编程语言都提供标准错误,就是为了利用重定向能力实现日志分级

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通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;
  3. 进程结束;
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;
}
相关推荐
呉師傅1 小时前
EPSON爱普生 L3118打印头【喷头】清洗方法
运维·服务器·网络·学习·电脑
JeJe同学2 小时前
LabelImg 标签字体大小修改教程
linux·人工智能·python
小鸡毛程序员2 小时前
从零搭建 Linux 开发服务器:VMware NAT 静态网络 + Docker + MySQL + Redis + 云服务器迁移
linux·服务器·网络
我命由我123452 小时前
Excel - Excel 单元格格式同时设置日期时间
运维·学习·职场和发展·excel·求职招聘·职场发展·学习方法
RSTJ_16252 小时前
PYTHON+AI LLM DAY SIXTY-SIX
服务器·开发语言·python
Cx330❀2 小时前
【Linux网络】一文吃透 TCP Socket 编程
linux·运维·服务器·开发语言·网络·tcp/ip
zizle_lin2 小时前
WSL初始化Ubuntu的使用
linux·运维·ubuntu·wsl
志栋智能2 小时前
轻量级 vs. 重平台:巡检超自动化的两种路径选择
运维·网络·人工智能·自动化
衫水2 小时前
项目后端服务 Docker 部署SOP (2026-06-04)
运维·docker·容器