【Linux系统】万字解析,文件IO

前言:

上文我们讲到了进程的控制,主要包括了进程的创建、进程的终止、进程的等待以及进程的程序替换......【Linux系统】详解,进程控制-CSDN博客

本文我们来讲讲Linux中下一个重难点:文件的IO

++点点关注吧佬~ (:з」∠)++


理解文件

狭义理解

文件存储在磁盘中

磁盘的永久性存储介质,因此文件在磁盘上的存储的永久的

磁盘是外设

对文件的操作本质上都是对外设的输入输出,简称IO

广义理解

Linux下,一切皆文件(键盘、显示器、磁盘、网卡.....都是文件,下面会详细介绍)

文件基本认知

文件 = 内容 + 属性

对于0KB的空文件,也是要占据空间的,因为有属性

所有文件操作的本质都是对文件内容的操作、文件属性的操作

系统角度

磁盘的管理者是操作系统

文件操作的本质是进程对文件的操作

文件读写不是通过库函数,而是通过文件相关的系统调用实现的,库函数只是封装了系统调用(方便用户使用,以及保证了可移植性)


C文件接口

fopen:打开文件

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

//以w方式(write)打开文件
int main()
{
   FILE* fp= fopen("testfile","w");
   if(!fp)
   {   
       printf("打开失败\n");
   }   
   else
   {   
       printf("打开成功\n");                                                                                                                                            
   }   
}

w:若文件存在,则会清空文件内容
   若文件不存在,这会新建一个文件

fopen:若打开成功,返回FILE类型的指针
       若不成功,返回NULL
cpp 复制代码
补充:
#include <stdio.h>
int fclose(FILE *stream);

表示关闭对应的文件

演示:

运行之前并没有文件,执行进程后发现新建了文件。

向新建的文件写入一些文本保存并退出,再运行进程。

我们发现,写出的文本信息被清空了!

" a "方式下(append):

若文件存在,并不会清空文件,写出信息的时候是采用追加的方式写入。

若文件不存在,则会新建文件

" r "方式打开(read):

若文件存在,则直接打开,不采取任何措施

若文件不存在,则打开失败,返回NULL

fwrite:写文件

cpp 复制代码
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);


ptr:数据的指针
size:数据的大小
conut:要写入的数据项个数
stream:要写入数据的文件的指针

返回值:若全部成功写入,返回写入的个数
        若中途出现错误或达到文件末尾,返回写入的个数或0
cpp 复制代码
#include <stdio.h>
#include <string.h>                                                                                                                                                    
//写文件
int main()
{
        //一切对文件内容的操作,都必须先打开文件
        FILE* fp=fopen("testfile","w");
        if(!fp)
        {   
             printf("打开失败\n");    
        }   
        else
        {   
           //写文件
           const char* msg="Yuzuriha\n";
           fwrite(msg,strlen(msg),1,fp);
           //写完之后关闭文件                                                                                                                                          
           fclose(fp);
        }    
}

注意:

向文件写入文本时,我们不能写入' \0 '。

因为此符号是语言字符串特有的,文件并不认识,写入'\0'会变成乱码

fread:读文件

cpp 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

ptr:指向存储数据的内存缓冲区的指针
size:单个数据项的字节大小(每次读取的个数)
nmemb:期望读取的数据项数量
stream:文件指针(由 fopen 打开)

返回值:若全部成功读取,返回读取了多少个size
        若中途出现错误或达到文件末尾,返回读取的个数或0
cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp=fopen("testfile","r");
    if(!fp)
    {   
        printf("打开失败");
        return 1;
    }   
    const char* msg="Yuzuriha\n";
    char buffer[20];
    //     读取到buffer中  一次读取元素的大小  读取几次   从fp中读取
    size_t s=fread(buffer,1,strlen(msg),fp);
    if(s>0)
    {   
        //添加'\0'
        buffer[s]=0;
        printf("%s",buffer);
    }   
    //检查文件是否到达了文件末尾
    else if(feof(fp))
             printf("到达文件末尾");

    fclose(fp);
}

可以看到,我们成功的从文件中读取到了字符串。

stdin&stdout&stderr

在C语言中,会默认开启三个输入输出流:stdin、stdout、stderr

分别代表标准输入、标准输出、标准错误

仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,⽂件指针

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

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

上面我们讲到了,文件的写入,那么假设我们想要在显示器上打印文本,就不只一种方法了

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

int main()
{
    const char* msg="hello yuzuriha\n";
    fprintf(stdout,msg);
}

系统文件IO

文件IO不仅仅只有fopen、fwrite等语言提供的接口,系统也有对应的系统调用,并且系统调用是语言接口的根本

下面我们就来看看,文件IO的系统调用

标志位

认识一下:

cpp 复制代码
必选标志位:  O_RDONLY :只读(r)
             O_WRONLY :只写(w)
             O_RDWR : 读写(r+)

可选标志位: O_CREAT : 新建
            O_APPEND : 追加
            O_TRUNC : 清空

什么是标志位:

标志位是用于指定文件操作的方式、权限、以及一些特殊行为的。

标志位的本质是整型常数,通过宏来封装,每一个标志位对应一个唯一的二进制位。

标志位的原理如下:

通过宏封装,不同标志位代表不同的功能,不冲突的标志位可以混用,使其同时使用多个功能。

cpp 复制代码
#include <stdio.h>
#define ONE   1  //0000 0000 0000 0001
#define TWO   2  //0000 0000 0000 0010
#define THREE 4  //0000 0000 0000 0100

void func(int f)
{
    if(f&ONE)
        printf("ONE");
    if(f&TWO)
        printf("TWO");
    if(f&THREE)
        printf("THREE");                                                                                                                                               
    printf("\n");
}


int main()
{
    func(ONE);
    func(ONE|TWO);
    func(ONE|TWO|THREE);
}

open:打开文件

cpp 复制代码
返回值:成功返回文件描述符(file descriptor)
        失败返回 -1

与fopen是使用区别不是很大,第一个参数是一样的,第二个参数用标志位代替即可

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

int main()
{
   int fd=open("testfile",O_WRONLY|O_CREAT|O_TRUNC);
   if(fd<0)                                                                                                                                                            
   {   
       perror("open");
       return 1;
   }   

   printf("打开成功\n");
   close(fd);
}

我们可以看到打开成功了,并且使用了3标志位,含义是:只读、新建、清空。

其实就相当与fopen的"w"打开方式!

这里我们打开的是之前就以及存在的文件,那我们再打开不存在的文件看看效果:

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

int main()
{
   int fd=open("newfile",O_WRONLY|O_CREAT|O_TRUNC);                                                                                                                    
   if(fd<0)
   {   
       perror("open");
       return 1;
   }   

   printf("打开成功\n");
   close(fd);
}

很好我们打开成功了,也同时新建了一个新文件。

但是我们发现,这个新建文件的权限是不对的,我们从没有见过S权限。

于是这时,我们就需要传递第三个参数了:mode,给定新文件的权限。

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

int main()
{
   int fd=open("newfile",O_WRONLY|O_CREAT|O_TRUNC,0666);                                                                                                               
   if(fd<0)
   {   
       perror("open");
       return 1;
   }   

   printf("打开成功\n");
   close(fd);
}

这下可以看到权限是正常的了。

有细心的同学可能发现了,文件权限并不是我们给定的666。这是因为系统中存在权限掩码umask。

感兴趣的同学可以看看这篇文章:【Linux】权限相关指令_linux 权限展示-CSDN博客

在:目录权限问题 -> 3.缺省权限。

close:关闭文件

cpp 复制代码
#include <unistd.h>
int close(int fd);  // fd 为 open 函数返回的文件描述符

返回值:成功返回 0,失败返回 -1

write:写文件

cpp 复制代码
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

fd:文件描述符(由 open 函数返回,标识已打开的资源)
buf:指向内存中待写入数据的缓冲区(如字符串、字节数组)
count:请求写入的字节数

成功:返回实际写入的字节数(可能小于 count,需循环处理)
失败:返回 -1(需通过 errno 查看错误原因,如资源关闭、权限不足)
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
   int fd=open("newfile",O_WRONLY|O_CREAT|O_TRUNC,0666);
   if(fd<0)
   {   
       perror("open");
       return 1;
   }   

   const char* msg="hello yuzuriha\n";
   write(fd,msg,strlen(msg));                                                                                                                                          
   close(fd);
}

read:读文件

cpp 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

fd:文件描述符(由 open 函数返回,标识已打开的资源,如文件、socket)
buf:指向内存中用于接收数据的缓冲区(需提前分配空间,如字符数组)
count:期望读取的最大字节数(受缓冲区大小限制)

返回值:
成功:返回实际读取的字节数(可能小于 count,如资源中剩余数据不足或被信号中断)
到达末尾:返回 0(如文件读取到末尾,无更多数据)
失败:返回 -1(需通过 errno 查看错误原因,如资源关闭、权限不足)
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    //读取不需要新建、也不需要清空
   int fd=open("newfile",O_RDNOLY);
   if(fd<0)
   {   
       perror("open");
       return 1;
   }   

   const char* msg="hello yuzuriha\n";
   char buffer[20];
   read(fd,buffer,strlen(msg));
   printf("%s",buffer);                                                                                                                                                
   // write(fd,msg,strlen(msg));
   close(fd);
}

我们知道C语言的文件IO接口是返回FILE* 类型的指针,而系统调用的接口是返回fd。

语言层的接口底层是一定封装了系统调用的,所以FILE中一定是封装了fd了的。

fd:文件描述符

系统调用接口open会返回fd,write与read也依靠fd来定位文件。、

那么fd到底是个什么东西?

我们先多看看几个文件的fd:

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


int main()
{

    int fd1=open("h1",O_WRONLY|O_REAET|,0666);
    int fd2=open("h2",O_WRONLY|O_REAET|,0666);
    int fd3=open("h3",O_WRONLY|O_REAET|,0666); 
    int fd4=open("h4",O_WRONLY|O_REAET|,0666);
        
    printf("fd1:%d\n",fd1);                                                                                                                                            
    printf("fd2:%d\n",fd2);
    printf("fd3:%d\n",fd3);
    printf("fd4:%d\n",fd4);

}

看到连续递增的数,不知道大家会联想到什么?数组下标?

对没错,就是数组下标。

fd的本质就是数组下标!

对文件的操作,本质是进程对文件的操作。

进程的PCB中,有一个指针:struct files_struct* files,指向一个结构体:struct file_struct。而这个结构体中有指针数组:fd_array[ ],用于保存不同文件属性的结构体地址。我们所讲的fd其实就是这个数组的下标。

我们知道文件=属性+内容。属性由结构体struct file保存,而内容要加载到文件缓冲区中。

补充:系统会默认打开3个输出流:标准输入、标准输出、标准错误,分别占用fd:0、1、2。所以我们上面的看到的文件fd是从3开始的。

fd的分配规则

分配规则为:分配没有被占用的最小的fd

验证:

关闭了fd=0的位置,我们可以发现之前新打开的文件就占用了fd=0的位置。

重定向

在我们之前学习Linux指令的时候,就已经了解过了重定向,下面我们来看看重定向是如何实现的【Linux】初见,基础指令-CSDN博客

重定向的本质是:

让其他文件占用输入输出,让其他文件代替stdin、stdout。

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);
    //h文件获得fd=1
    int fd=open("testfile",O_WRONLY|O_CREAT,0666);
}
dup2接口

使用dup2接口,我们就可以一键完成上面的操作,不用关闭、打开....这些繁琐的步骤!

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

核心作用是将新的文件描述符 newfd 指向旧的文件描述符 oldfd 所关联的文件
使得两个描述符最终指向同一个文件

返回值
成功:返回新的文件描述符 newfd
失败:返回 -1,并设置全局变量 errno 以指示错误原因
输出重定向
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

//输出重定向:打印信息不在显示器,而在其他文件
int main()
{
    int fd=open("myfile",O_WRONLY|O_CREAT,0666);
    if(fd<0)
    {   
        perror("open");                                                                                                                                                
        return 1;
    }   

    dup2(fd,1);//让1指向fd关联的文件
    printf("%s","你好世界\n");
}

我们可以看到,信息打印到了myfile文件中

输入重定向
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

//输入重定向
int main()
{
      int fd=open("myfile", O_RDONLY);                                                                                                                                 
      if(fd<0)
      {   
          perror("open");
          return 1;
      }   

      char buffer[20];
      dup2(fd,0);//让0指向fd的关联文件
      int n=read(0,buffer,sizeof(buffer)-1); //从0读取信息,读取到buffer中
      if(n==-1)
          perror("read failed");
      else
          buffer[n]=0; //添加\0
      printf("%s\n",buffer);
}
标准错误

错误信息与输出信息,其实都是打印在显示器上的,这也就意味这它们都指向同一个文件

打印错误、打印信息是不同的函数:perror、printf。这是因为使用了重定向,把常规信息与错误信息进行了分离!

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

int main()
{
    printf("hello\n");                                                                                                                                                 
    perror("erro");
}

如上图,直接运行我们可以看到信息都打印出来了。

但是当我将输出重定向到log.txt文件中,发现错误信息并没有重定向,而是打印了出来,这是为什么?

因为输出重定向,是针对文件描述符为1的文件,所以对文件描述符为2的文件无效。

cpp 复制代码
其完整写法为:

./sysIO 1>log.txt

想要都写入log.txt中有两种方法:

bash 复制代码
法一:
hyc@hyc-alicloud:~/linux/文件IO$ ./sysIO 1>log.txt 2>>log.txt
hyc@hyc-alicloud:~/linux/文件IO$ cat log.txt
hello
Success


法二:推荐!
hyc@hyc-alicloud:~/linux/文件IO$ ./sysIO 1>log.txt 2>&1
(&1 是 Shell 语法的一部分,用于 引用文件描述符)
hyc@hyc-alicloud:~/linux/文件IO$ cat log.txt
erro: Success
hello

理解"一切皆文件"

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

像进程、磁盘、显示器、键盘这样的硬件设备,是通过驱动程序(struct device)管理的,而指向驱动程序的指针是存放在struct file中的。

而对于struct file,我们上面讲到了如下关系:

上图中的外设,每个设备都可以有自己的read、write,但⼀定是对应着不同的操作方法!但通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取Linux系统中绝大部分的资源!这便是"linux下一切皆文件"的核心理解。

Linux下一切皆文件!


缓冲区

什么是缓冲区?

内存中的一段空间。

为什么要引入缓冲区?

提高效率:提高使用者的效率。

cpp 复制代码
代码一:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
                                                                                                                                                                       
int main()
{
        close(1);
        int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0)
        {   
              perror("open");
              return 1;
        }   


        printf("fd:%d",fd);
        printf("hello Yuzuriha\n");
        printf("hello Yuzuriha\n");
        printf("hello Yuzuriha\n");

        const char* msg="你好\n";
        write(fd,msg,strlen(msg));
}


代码二:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
                                                                                                                                                                       
int main()
{
        close(1);
        int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0)
        {   
              perror("open");
              return 1;
        }   


        printf("fd:%d",fd);
        printf("hello Yuzuriha\n");
        printf("hello Yuzuriha\n");
        printf("hello Yuzuriha\n");

        const char* msg="你好\n";
        write(fd,msg,strlen(msg));
        close(fd);
}

代码一:

代码二:

我们发现代码二,其实就比代码一,在结尾多了一个close的函数 。为什么库函数打印的信息没有了

如下图:

实际上我们存在两种缓冲区:语言层缓冲区(用户级)、文件缓冲区(内核级)。

而我们使用语言接口的话,数据会先加载到语言层的缓冲区,满足条件后才会刷新到文件缓冲区(内核级)。

我们使用系统接口的话,数据则会直接加载到文件缓冲区(内核级)。

再看我们上面的代码,我们会发现,在进程还没有退出的时候,文件就已经关闭了。当进程退出,想要通过文件描述符(fd)找到对应的struct_file、文件缓冲区时,发现已经找不到了!于是数据没能成功刷新到文件缓冲区!

补:其实不论是加载、刷新或是其他的数据流动,其本质都是拷贝!不要想复杂了!

计算机数据流动的本质都是:拷贝!

C语言库的刷新规则如图,其中强制刷新使用:fflush函数

当然,文件缓冲区(内核级)也有对应的刷新规则 ,但我们并不关心,由OS自主决定!

另外,我们常说的缓冲区都是说的是:语言层的缓冲区!

缓冲区在哪里?

语言层缓冲区(用户级):

我们都知道C语言的文件管理是有一个FILE的,那么FILE是什么呢?

其实FILE是一个由C语言提供的结构体,C语言的缓冲区具体存放位置不单一,但FILE 结构体保存了指向缓冲区的地址,这样就能找到并操作缓冲区!

文件缓冲区(内核级)

内核级缓冲区存放在内存中的,对应在虚拟地址空间中的内核空间。