Linux从入门到精通——基础IO(简洁清晰版)

一. 深入理解文件和语言级文件操作

1.1 文件理解

① 文件 = 文件内容 + 文件属性(元数据),对文件的操作就是对文件的属性和对文件的内容进行操作这两类操作。

② 要访问一个文件,首先必须打开这个文件,打开这个文件是由对应的程序运行起来变成进程之后动态打开的;而打开这个文件之前,必须先找到这个文件。我们访问任何文件,必须通过路径,要么用户自己提供路径,要么使用进程的 cwd ,进程会动态维持自己当前的工作路径 cwd 。

③ 打开一个文件,本质是把这个文件加载到内存中;对文件的操作,本质是进程通过CPU访问内存中的文件。

磁盘是外设(既是输出设备也是输⼊设备)。

④ Linux 系统中存在大量的文件,这些文件分为已经打开的文件和为被打开的文件;这些文件从位置上也分为内存中被打开的文件和磁盘上的文件。磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的。

⑤ 在Linux系统中,可以同时存在许多被打开的文件。操作系统作为计算机软硬件资源的管理者,必须对这些文件进行管理------先描述,再组织,即将文件用结构体描述起来,再用对应的数据结构把这些结构体进行管理。

⑥ ⽂件的所有操作本质都是对外设(磁盘)的输入和输出,简称 IO。

1.2 语言级文件操作

1.2.1 文件基本操作

① "r": 以读的方式打开文件,默认从起始位置开始读。

② "w": 以写的方式打开文件,若文件不存在就新建,若存在则清空文件再写,默认从起始位置开始写。

③ "a": 以在文件尾部追加的方式打开文件,若文件不存在就新建。

④ "r+": 以读写的方式打开文件,默认从起始位置开始读写。

⑤ "w+": 以读写的方式打开文件,若文件不存在就新建,存在则清空文件再读写,默认从起始位置开始读写。

⑥ "a": 以读写的方式打开文件,若文件不存在就新建,写入时会追加写入。

1.2.2 文件基本操作示例

cpp 复制代码
#include <stdio.h>                                                                                                       
#include <unistd.h>
   
int main(int argc,char* argv[])
{
     if(argc!=2)
     {   
         printf("Usage:%s filename\n",argv[0]);
         return 1;
     }
     chdir("..");
     FILE* fp=fopen("log.txt","r");
     FILE* fp=fopen("log.txt","w");
     FILE* fp=fopen(argv[1],"a");
     if(!fp)
     {   
         perror("fopen");
         return 1;
     }
     //printf("这是一个进程:%d\n",getpid());

     int cnt=5;
     const char* msg="hello files\n";
     while(cnt--)
     {
         fputs(msg,fp);
     }
    
     while(1)
     {
         char buffer[128];
         if(!fgets(buffer,sizeof(buffer),fp))
         {
              break;
         }
 
         printf("from file fp:%s",buffer);
     }
     // while(1)
    // {
    //     sleep(1);        
     //}

     fclose(fp);
     return 0;
}

1.2.3 文件读写位置

cpp 复制代码
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);

以上三个接口都是和文件的读写位置有关。文件 = 文件内容 + 文件属性,文件内容类似一个 char类型的一维数组,而文件的读写位置则是这个数组的下标,即本质是一个整数。

二. 深入理解操作系统的文件IO

2.1 函数标志位

打开⽂件的方式不仅仅是 fopen,ifstream 等方式,这些方式都是语言层的打开文件方式,其实系统才是打开文件最底层的方式。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统⽂件IO接口中

会使用。

示例:

cpp 复制代码
#define ONE (1<<0) //0001
#define TWO (1<<1) //0010
#define THREE (1<<2) //0100
#define FOUR (1<<3) //1000
 
void Print(int flag)
{
     if(ONE & flag)
     {
         printf("ONE\n");
     }
     if(TWO & flag)
     {
         printf("TWO\n");
     }
     if(THREE & flag)
     {
         printf("THREE\n");
     }
     if(FOUR & flag)
     {
         printf("FOUR\n");
     }
     printf("\n");
}

int main()                                                                                                               
{
     //通过传递不同的标志位,能让函数执行不同的功能
     Print(ONE);
     Print(ONE|TWO);
     Print(ONE|TWO|THREE);
     Print(ONE|FOUR);
}

2.2 系统文件操作的接口

cpp 复制代码
#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);

2.2.1 分析:

① pathname: 要打开或创建的目标文件。

② flags: 打开文件时,可以传入多个参数选项,用下面的⼀个或者多个常量进行"或"运算,构成标志位 flags。

参数:

O_RDONLY: 只读打开

O_WRONLY: 只写打开

O_RDWR : 读写打开

这三个常量,必须指定⼀个且只能指定⼀个。

③ O_CREAT : 若文件不存在,则创建它。默认创建的文件权限是随机的,可以使用mode选项,来指明新文件的访问权限。

④ O_APPEND: 指明打开文件后在写入时是追加写入。

⑤ 返回值:open成功,返回新打开文件的⽂件描述符;open失败,返回 -1 。

2.3 文件描述符

2.3.1 分析

①操作系统中,访问任何打开的文件,操作系统只"认识"文件描述符。对文件进行任何操作,都必须先把文件加载到内存中。所以当我们打开任何一个⽂件时,操作系统在内存中都要创建相应的数据结构来描述这个目标文件。于是就有了file结构体,这一个file 结构体表示⼀个已经打开的文件对象。这个file结构体直接或间接包含了文件的大部分属性。

② 而我们打开文件本质是通过进程打开的,而进程打开文件需要执行open系统调用,且进程和打开的文件是一对多的关系,所以必须让进程和⽂件关联起来。

③ 每个进程的进程控制块task_struct 中都有⼀个指针 files_struct* files, 指向⼀张表files_struct,该表中包含⼀个struct file* fd_array[]的指针数组,每个元素都是⼀个指向打开文件的指针。所以,本质上,文件描述符就是从0开始的整数,就是该数组的下标。所以,我们通过文件描述符,就可以找到对应的⽂件。

④ 同时每个file文件对象中都有一段文件缓冲区,当我们想对文件进行写入时,进程会通过CPU调用系统调用write对文件进行写入,操作系统会通过文件描述符找到这个文件,再把内容拷贝到这个文件对应的缓冲区里,然后再刷新到磁盘上。

2.3.2 文件描述符的分配规则

当我们使用进程打开一个新的文件时,操作系统会给该文件分配一个值最小且没有被使用的文件描述符,这就是文件描述符的分配规则

2.3.3 文件描述符0、1、2

① 在操作系统内核视角,Linux系统会默认打开文件描述符0、1、2,这三个文件描述符分别对应的硬件设备依次是:键盘、显示器、显示器。

② 在C语言视角,我们在编写程序时操作系统也会默认给我们打开这三个流:标准输入 stdin 、标准输出 stdout 和 标准错误 stderr 。它们对应的就是键盘、显示器、显示器,它们的类型是 FILE * ,其中 FILE 本质就是一个结构体。 因为操作系统在对文件进行操作时只 "认识" 文件描述符,所以这里的FILE 中必定封装了文件描述符。

cpp 复制代码
#include <stdio.h>

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

2.3.4 问题:为什么C/C++要封装系统调用?

首先,直接使用系统调用不具备很好的跨平台性,即不同平台上的程序只能在自己的平台上面执行。一门语言创造出来之后最重要的事情就是让更多的人使用,而每个不同的平台对应的都是成千上万的用户,所以C/C++为了让自己能被更多的人使用,所以在各个平台上面对这些系统调用进行了封装,从而提高它自身的跨平台性。

2.4 系统IO示例

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 
int main()                                                                                                               
{
     printf("stdin->%d\n",stdin->_fileno);
     printf("stdout->%d\n",stdout->_fileno);
     printf("stderr->%d\n",stderr->_fileno);
     umask(0);
     int op=open("log.txt",O_WRONLY);  //打开不存在的文件,打开失败
     int op=open("log.txt",O_WRONLY|O_CREAT); //打开不存在的文件会新建
     int fd=open("log.txt",O_WRONLY|O_CREAT,0666); //打开不存在的文件会新建,并设置文件权限
     int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); //打开不存在的文件会新建,并设置文件权限,且会先清空文件
     int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //打开不存在的文件会新建,并设置文件权限,且会追加文件
     //int fda=open("loga.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); 
     //int fdb=open("logb.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
     //int fdc=open("logc.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); 
  
     if(fd<0)
     {
         perror("open");
         return 1;
     }
     //const char* msg="abcdefg";
      const char* msg="123\n";
     write(fd,msg,strlen(msg));  //这里的strlen不要+1,+1是C语言的规定,和系统无关
     //write(1,msg,strlen(msg)); //向文件描述符1------显示器写入 
     //fputs(msg,stdout);  //向标准输出流------显示器写入 
 
     //printf("fd:%d\n",fd);
     //printf("fda:%d\n",fda);
     //printf("fdb:%d\n",fdb);
     //printf("fdc:%d\n",fdc);
    
     close(fd);  //关闭文件
     return 0;
}

2.5 重定向

2.5.1 示例:

cpp 复制代码
#include <stdio.h>                                                                                                       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{   
     //close(1);
     close(0);
     int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);  //写入|创建|清空
     //int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);  //写入|创建|追加
     //int fd=open("log.txt",O_RDONLY);  
      printf("fd:%d\n",fd);
      //dup2(fd,1); 
      dup2(fd,0);
      
      printf("sssssssss\n");
      printf("sdddgjkrogssss\n");
      printf("yosdnms\n");

      char buffer[128];
      fgets(buffer,sizeof(buffer),stdin);//OS只"认识"fd,stdin封装了0,输入/输出/追加重定向
      printf("buffer:%s\n",buffer);
      
      const char* msg="files\n";
      write(fd,msg,strlen(msg));
  
      int fda=open("loga.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
      printf("fda:%d\n",fda);
 
      int fdb=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);  
      printf("fdb:%d\n",fdb);
      
      close(fd);
  
      return 0;
}

2.5.2 理解重定向

① 引言:当我们直接打开一个文件时,该文件的文件描述符fd=3,因为操作系统默认会打开0(stdin)、1(stdout)、2(stderr)。

输出重定向>:从以上代码中,我们能看到如果在打开一个文件之前关闭了文件描述符1对应的文件之后,发现本来应该输出到显示器上的内容,输出到了文件 log,txt 当中,且文件log,txt的文件描述符fd变成了1。这种现象叫做输出重定向。

追加重定向>>:追加重定向和输出重定向类似,只不过追加重定向在打开文件时是添加了追加的选项。

输入重定向<: 我们默认获取输入都是从键盘获取的,键盘对应的是stdin,它的文件描述符是0。当操作系统把文件描述符表中下标为0的位置的内容改成指向其他文件时,由于语言层并不知道,语言层只认识 stdin, stdin封装了文件描述符0,所以语言层还是会去文件描述符表数组的0下标中查找,从而在获取输入时就变成了从指定的文件获取输入了。这种现象叫做输入重定向。

dup2(): 当我们不想使用先关闭文件描述符0、1、2在打开文件的方式来实现重定向时,我们也能使用系统调用 dup2() ,它会用户的指定文件进行覆盖文件描述符表数组特定下标中的内容,从而实现重定向。

重定向的本质: 更改文件描述符表数组特定下标中的内容。

重定向在shell中的实现

cpp 复制代码
void ParseRedir(char commandline[])
{
     redir_type=0;
     filename=NULL;
     char* start=commandline;
     char* end=commandline+strlen(commandline);
     while(start<end)  //abcdefg
     {
         if(*start=='>')
         {
             if(*(start+1)=='>')
             {
                 //追加重定向
                 *start='\0';
                 ++start;
                 *start='\0';
                 ++start;
                 Trimspace(start);
                 redir_type=AppendRedir;
                 filename=start;
                 break;
             }
             //输出重定向
             *start='\0';
             ++start;
             Trimspace(start);
             redir_type=OutputRedir;
             filename=start;
             break;
         }
         else if(*start=='<')  //cat < sss
         {
             //输入重定向
             *start='\0';
             ++start;
             Trimspace(start);
             redir_type=InputRedir;
             filename=start;
             break;
         }
         else    
         {
             ++start;
         }
     }
} 

从以上代码也能看出,不管是父进程还是子进程,它们进程程序替换都不会影响已经打开的文件。

三. 初步理解"一切皆文件"

1. 在windows中是文件的东西,它们在linux中也是文件;其次⼀些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样的硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们;甚至管道,也是文件。

2. 操作系统作为软硬件资源的管理者,当用户通过进程打开文件时,操作系统会为每个文件都创建一个 struct file结构体,而struct file 中的 f_op 指针指向了⼀个 file_operations 的结构体,这个结构体中的成员大部分都是函数指针。

3. Linux中几乎所有文件的读写IO操作都能用read()和write()函数来进行,由于在底层每个硬件设备的IO方法实现都不一样,这些函数指针都指向各个文件设备的读写方法,从而用户在上层创建进程,进程再通过struct file中的这些函数指针访问对应的文件方法,从而让用户仅仅通过⼀套 文件API接口和开发工具,即可访问Linux系统中绝⼤部分的资源。

4. 上述的叙述中的文件struct file结构体是上层,类似C++中的基类,不同的读写方法对应的不同文件设备是下层,类似C++中的派生类。这在某种程度上也体现了C++多态的思想。

四. 缓冲区

1. 文件打开和读写的基本过程

1.1 首先我们打开文件是用户通过进程来打开的,所以在打开文件之前,用户必须先创建进程。有了进程之后,在进程的PCB中有一个files指针,这个指针指向该进程的文件描述符表。当我们要打开一个文件时,操作系统首先会为这个文件创建一个struct file文件对象,然后在文件描述符标中为该文件分配一个值最小且没有被占用的文件描述符并且把这个文件描述符返回给用户。

1.2 在这个文件所对应的文件对象struct file中,直接或间接包含这个文件的属性,操作表以及操作系统为文件所分配的一段文件内核级缓冲区。因为文件在磁盘上,磁盘是硬件,操作系统是软硬件资源的管理者,所以操作系统会参与文件的读写操作。在对文件读写时,操作系统会首先根据文件路径在磁盘中找到这个文件,找到之后,因为文件 = 文件内容 + 文件属性,所以首先操作系统会先把文件的属性加载到内存中的struct file文件对象中。

1.3 读操作: 当我们调用系统调用 read(fd,buffer,sizeof(buffer)) 来读文件时,操作系统会先根据fd找到对应文件的struct file 对象,再根据struct file文件对象中的操作表把文件的内容加载到内存中,即加载到 struct file 文件对象所对应的文件内核级缓冲区中,然后再把文件内核级缓冲区中的内容拷贝到用户级缓冲区buffer中。当我们在read时,如果文件内核级缓冲区中没有内容,操作系统就会让进程阻塞住,等到操作系统把文件内容加载到文件内核级缓冲区之后,操作系统再把进程唤醒,继续执行 read 操作。这种情况类似C语言中我们使用scanf时,如果我们不输入数据,进程会一直阻塞在那里等待用户输入。只有用户输入数据并按回车之后,进程才能继续执行。

1.4 写操作: 当我们调用系统调用 write(fd,buffer,strlen(buffer)) 来写入文件时,操作系统同样会先根据fd找到对应文件的struct file 对象,再根据struct file文件对象中的操作表把文件的内容加载到内存中,即加载到 struct file 文件对象所对应的文件内核级缓冲区中,然后再把buffer中的内容拷贝到文件内核级缓冲区中,然后操作系统再根据struct file文件对象中的操作表在某个时刻把文件内核级缓冲区中的内容在自动刷新到磁盘上(如果用户不是立即强制刷新)。

cpp 复制代码
int main()                                                                                                               
{
     int fd=open("log.txt",O_RDONLY);
     if(fd<0)
     {
         perror("open");
         return 1;
     }
    
     char buffer[128];
     int n=read(fd,buffer,sizeof(buffer)-1);
     if(n>0)
     {
         buffer[n]=0; 
         printf("buffer:%s\n",buffer);
     }
 
     return 0;
}

1.5 在文件IO中,read和write操作本质都是拷贝函数,且进行拷贝操作都是内存级的。大部分情况下都是操作系统先进行文件的加载,再对文件进行读写操作,最后操作系统再进行刷新操作。 但是如果用户在读写时添加了TRUNC选项,那么可能操作系统直接先把文件内容置空,再在文件内核级缓冲区中进行读写,然后再把内容直接刷新到磁盘上。

1.5 但是我们调用系统调用是有成本的,即它的效率太慢,所以我们需要减少系统调用的次数。例如,我们之前在使用malloc或者在new的时候会调用系统调用在虚拟地址空间空间中的用户空间(即用户能直接通过指针访问的空间)申请堆空间,再申请物理内存。如果内存不够时,操作系统就会把内存中的阻塞进程的代码和数据交换到磁盘的swap分区中来腾出空间。这个过程是需要时间的。所以类似C++中的容器在空间不够时一般都是二倍扩容,这样就能避免多次调用系统调用来申请内存这样的情况了。于是为了解决这样的问题C/C++就开始登上了历史舞台。

2. 缓冲区的引入

2.1 在C语言中当我们在使用 fgets(buffer,size,stream)读取文件或者 fputs(buffer,stream)写入文件时,其中buffer是用户缓冲区,即定义的局部或全局数组。以fputs为例,每一个文件都有一个输入缓冲区和一个输出缓冲区,在fputs写入文件时,实际上都是先把buffer拷贝到输出缓冲区中,然后等到这个进程结束的时候再调用一次系统调用write把输出缓冲区中的内容刷新到文件内核级缓冲区中,这样的话就减少了系统调用的次数,从而提高了C语言的IO函数的效率。

2.2 我们曾经所说的缓冲区不是文件内核级缓冲区,而是上述的输入缓冲区和输出缓冲区,即语言级缓冲区。在语言层面我们使用 fopen(path,mode)来打开文件, fopen()函数的返回类型是 FILE* 文件指针,其中FILE是一个结构体,在fopen内部创建,在FILE这个结构体中,除了包含默认打开的stdin、stdout和stderr之外,还包含了文件描述符fd以及输入缓冲区和输出缓冲区buffer。当我们fclose(fd)时,操作系统会关闭文件描述符fd、刷新缓冲区和释放文件对象struct file。

cpp 复制代码
struct FILE
{
		FILE* stdin;
		FILE* stdout;
		FILE* stderr;
		int fd;
		char InputBuffer[];
		char OutputBuffer[];
		//...
};

3. 三种刷新缓冲区的现象

3.1 语言级缓冲区的刷新问题

① 刷新的本质:把语言级缓冲区对应的内容通过系统调用write()拷贝到内核级文件缓冲区。即把用户数据交给操作系统,但是不一定会写到磁盘上。

② 进程在结束时,会自动刷新缓冲区。

③ 如果目标文件是显示器,则会进行行刷新,即行缓冲。

④ 普通文件一般是全缓冲,即缓冲区写满了才会刷新。

3.1 重定向与缓冲区

cpp 复制代码
int main()
{
     close(1);  //关闭stdout
     int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
     if(fd<0)
     {
         perror("open");
         return 1;
     }
     printf("fd is %d\n",fd); //fd=1-->stdout                                                                             
     close(fd);
 
     return 0;
}

通过以上代码我们能看到,在close(1)之后,新建的log.txt文件的文件描述符fd就是1,由于stdout封装了1,但是此时1下标里对应的文件不再是显示器文件了,而是log.txt,所以在打印时就先打印到了log.txt对应的缓冲区中,但是由于普通文件是只有缓冲区被写满或者进程结束时才把缓冲区中的内容刷新到内核文件缓冲区中,且由于在进程结束之前又执行了close(fd),所以在进程结束时找不到1对应的文件,所以看不到缓冲区刷新了。

3.2 进程终止与缓冲区

cpp 复制代码
int main()
{
     printf("12345678");
     sleep(1);
     //exit(0);
     _exit(0);
      
     return 0;
}

通过以上代码我们能看到,我们调用exit()时它会刷新语言级缓冲区然后再终止进程,而_exit()则是一个系统调用,它不会刷新语言级缓冲区。

3.3 子进程与缓冲区

cpp 复制代码
int main()
{
     //向显示器打印的几种方法
     printf("hello printf\n");
     fprintf(stdout,"hello fprintf\n");
 
     const char * str="hello fputs\n";
     fputs(str,stdout);
 
     char buffer[]="hello write\n";
     write(1,buffer,strlen(buffer));
 
     fork();
     return 0;
}

从以上代码能看出,当我们直接运行时,它会默认向显示器文件上面打印,由于显示器是行刷新策略且几种打印方式都加了换行,所以在打印时遇到换行都会刷新到文件内核级缓冲区中。且由于write()是系统调用,所以它会直接把数据拷贝到文件内核级缓冲区中,而其他几种打印都需要先把数据拷贝到语言级输出缓冲区中,遇到换行才刷新到文件内核级缓冲区中。

当我们要输出重定向到log.txt这个普通文件时,普通文件是全缓冲的刷新策略,所以在fork()之前会把数据拷贝到log.txt文件所对应的输出缓冲区中。在fork()创建了子进程之后,父子进程会共享代码和数据,之后在return进程结束时,父子进程都会把默认打开的stdout关闭,然后父子都会刷新输出缓冲区。不管是父子哪个进程先刷新,刷新就是修改输出缓冲区的内容,类似会发生写时拷贝,所以我们能看到文件log.txt中的前三条内容打印了两遍,而由于write系统调用是直接把数据拷贝到文件内核级缓冲区中,没有经过输出缓冲区,所以只打印了一次。

3.4 文件内核级缓冲区

① 只要把数据从用户缓冲区拷贝到了文件内核级缓冲区,就相当于把数据交给了硬件。客观上,就是把数据写给了struct file文件对象对应的文件内核级缓冲区,然后操作系统会自动把数据刷新到硬件上。

② 操作系统的刷新策略:立即刷新;当操作系统空闲时再自动刷新。如果用户想要操作系统立即刷新,可以调用系统调用fsync(fd)。

3.5 理解标准错误

① 我们已经知道当我们启动进程时,基本上就是为了完成某种任务,。在完成任务过程中,我们可能需要输入数据,然后数据经过计算机处理后可能需要给我们用户返回结果,所以操作系统会在进程启动时默认为我们打开标准输入stdin、标准输出stdout。同时这些数据被处理完之后数据的处理结果可能有正确信息和错误信息,所以为了加以区分和debug代码,所以操作系统又默认为我们打开了标准错误stderr,以便分离错误信息。

② 示例

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    printf("这是一个正确的信息\n");
    fprintf(stdout,"这是一个正确的日志信息\n");
    const char* truemsg="这是正确的信息,write\n";
    write(1,truemsg,strlen(truemsg));

    fprintf(stderr,"这是一个错误的日志信息\n");
    const char* errmsg="这是错误的信息,write\n";
    write(2,errmsg,strlen(errmsg));
    perror("write:info");
    return 0;
}




4. C文件库的简单封装

cpp 复制代码
#pragma once

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

#define MAXSIZE 1024

typedef struct MyFILE
{
    int fd;  //文件描述符
    int flags; //文件打开模式
    int flush_mode; //文件刷新模式
    char OutBuffer[MAXSIZE]; //文件输出缓冲区
    int pos;  //文件读写位置
    int capacity; //文件大小
}MyFILE;

MyFILE* MyFopen(const char* pathname,const char* mode);

int MyFputs(const char* str,MyFILE* fp);

void MyFlushCore(MyFILE* fp,int flag);

int MyFclose(MyFILE* fp);
cpp 复制代码
#include "mystdio.h"

//缓冲区刷新方式
#define NoneBuffer 1
#define LineBuffer 2
#define FullBuffer 4

//默认权限
#define void_mode 0666

//刷新策略
#define TRY_FLUSH 1  //尝试刷新 0001
#define MUST_FLUSH 2 //强制刷新 0010

MyFILE* MyFopen(const char* pathname,const char* mode)
{
    int fd=-1;
    int flags=0;
    if(strcmp(mode,"r")==0)
    {
        flags=O_RDONLY;
        fd=open(pathname,flags);
    }
    else if(strcmp(mode,"w")==0)
    {
        flags=O_WRONLY|O_CREAT|O_TRUNC;
        fd=open(pathname,flags,void_mode);
    }
    else if(strcmp(mode,"a")==0)
    {
        flags=O_WRONLY|O_CREAT|O_APPEND;
        fd=open(pathname,flags,void_mode);
    }
    else
    {}

    if(fd<0)
    {
        printf("open errror\n");
        return NULL;
    }

    MyFILE* myfile=(MyFILE*)malloc(sizeof(MyFILE));
    if(myfile==NULL)
    {
        printf("malloc fail\n");
        return NULL;
    }
    myfile->fd=fd;
    myfile->flags=flags;
    myfile->flush_mode=LineBuffer;
    myfile->pos=0;
    myfile->capacity=MAXSIZE;

    return myfile;
}
 
void MyFFlushCore(MyFILE* fp,int flag)
{
    if(fp->pos==0)
    {
        return;
    }

    if((fp->flush_mode & LineBuffer)||(flag & MUST_FLUSH))
    {
        //行刷新条件满足-->把数据刷新到内存
        if((fp->OutBuffer[fp->pos-1]=='\n')||(flag & MUST_FLUSH))
        {
            write(fp->fd,fp->OutBuffer,fp->pos);
            fp->pos=0;
        }
    }
    else if(fp->flush_mode & FullBuffer)
    {}
    else if(fp->flush_mode & NoneBuffer)
    {}
}

int MyFputs(const char* str,MyFILE* fp)
{
    if(strlen(str)==0)
    {
        return 0;
    }
    //把数据从用户缓冲区拷贝到语言级文件缓冲区
    memcpy(fp->OutBuffer + fp->pos,str,strlen(str));
    fp->pos+=strlen(str);

    //如果条件允许,可以刷新缓冲区
    MyFFlushCore(fp,TRY_FLUSH);

    return strlen(str);
}

void MyFFlush(MyFILE* fp)
{
    MyFFlushCore(fp,MUST_FLUSH);
}

int MyFclose(MyFILE* fp)
{
    //1.强制刷新缓冲区到内核中
    MyFFlush(fp);

    //1.2 调用系统调用强制刷新文件内核级缓冲区到磁盘
    fsync(fp->fd);

    //2.关闭文件fd
    close(fp->fd);

    //3.释放文件对象
    free(fp);
}
cpp 复制代码
#include "mystdio.h"

int main()
{
    MyFILE* fp=MyFopen("log.txt","a");
    if(fp==NULL)
    {
        printf("MyFopen fail\n");
        return 1;
    }

    const char* msg="hello world";
    int cnt=10;
    while(cnt--)
    {
        MyFputs(msg,fp);
        printf("Debug:OutBuffer->%s,pos:%d\n",fp->OutBuffer,fp->pos);   
        sleep(1);
    }

    MyFclose(fp);
    printf("write file finish\n");
    return 0;
}

在使用文件操作时,我们的最佳实践是使用语言级函数!!!

相关推荐
节点小宝2 小时前
一站式部署:支持Windows、macOS、Linux三端的统一方案
linux·运维·macos
乌鸦9442 小时前
《库制作与原理》
linux·动态库
北亚数据恢复2 小时前
服务器数据恢复—昆腾StorNext文件系统双盘离线故障数据恢复案例
运维·服务器
ZhengEnCi2 小时前
L1D-Linux系统Node.js部署Claude Code完全指南 🚀
linux·ai编程·claude
希望永不加班2 小时前
SpringBoot 内置服务器(Tomcat/Jetty/Undertow)切换
服务器·spring boot·后端·tomcat·jetty
服务器专卖店2 小时前
单台服务器52块硬盘
服务器
hnxaoli2 小时前
统信小程序(十一)快捷地址栏
linux·python·小程序
黄昏晓x2 小时前
Linux----网络
linux·网络·arm开发
小比特_蓝光3 小时前
Linux开发工具
linux·运维·服务器