当前位置: 首页 > news >正文

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

前言:

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

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

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


理解文件

狭义理解

        文件存储在磁盘中

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

        磁盘是外设

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

广义理解

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

文件基本认知

        文件 = 内容 + 属性

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

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

系统角度

        磁盘的管理者是操作系统

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

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


C文件接口

fopen:打开文件

#include <stdio.h>//以w方式(write)打开文件
int main()
{FILE* fp= fopen("testfile","w");if(!fp){   printf("打开失败\n");}   else{   printf("打开成功\n");                                                                                                                                            }   
}w:若文件存在,则会清空文件内容若文件不存在,这会新建一个文件fopen:若打开成功,返回FILE类型的指针若不成功,返回NULL
补充:
#include <stdio.h>
int fclose(FILE *stream);表示关闭对应的文件

演示:

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

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

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

        “ a ”方式下(append):

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

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

        “ r ”方式打开(read):

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

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

fwrite:写文件

#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);ptr:数据的指针
size:数据的大小
conut:要写入的数据项个数
stream:要写入数据的文件的指针返回值:若全部成功写入,返回写入的个数若中途出现错误或达到文件末尾,返回写入的个数或0
#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:读文件

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);ptr:指向存储数据的内存缓冲区的指针
size:单个数据项的字节大小(每次读取的个数)
nmemb:期望读取的数据项数量
stream:文件指针(由 fopen 打开)返回值:若全部成功读取,返回读取了多少个size若中途出现错误或达到文件末尾,返回读取的个数或0
#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返回值类型,⽂件指针

#include <stdio.h>extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

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

#include <stdio.h>int main()
{const char* msg="hello yuzuriha\n";fprintf(stdout,msg);
}


系统文件IO

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

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

标志位

认识一下:

必选标志位:  O_RDONLY :只读(r)O_WRONLY :只写(w)O_RDWR : 读写(r+)可选标志位: O_CREAT : 新建O_APPEND : 追加O_TRUNC : 清空

什么是标志位:

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

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

标志位的原理如下:

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

#include <stdio.h>
#define ONE   1  //0000 0000 0000 0001
#define TWO   2  //0000 0000 0000 0010
#define THREE 4  //0000 0000 0000 0100void 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:打开文件

返回值:成功返回文件描述符(file descriptor)失败返回 -1

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

#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"打开方式!

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

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,给定新文件的权限。

#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:关闭文件

#include <unistd.h>
int close(int fd);  // fd 为 open 函数返回的文件描述符返回值:成功返回 0,失败返回 -1

write:写文件

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);fd:文件描述符(由 open 函数返回,标识已打开的资源)
buf:指向内存中待写入数据的缓冲区(如字符串、字节数组)
count:请求写入的字节数成功:返回实际写入的字节数(可能小于 count,需循环处理)
失败:返回 -1(需通过 errno 查看错误原因,如资源关闭、权限不足)
#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:读文件

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);fd:文件描述符(由 open 函数返回,标识已打开的资源,如文件、socket)
buf:指向内存中用于接收数据的缓冲区(需提前分配空间,如字符数组)
count:期望读取的最大字节数(受缓冲区大小限制)返回值:
成功:返回实际读取的字节数(可能小于 count,如资源中剩余数据不足或被信号中断)
到达末尾:返回 0(如文件读取到末尾,无更多数据)
失败:返回 -1(需通过 errno 查看错误原因,如资源关闭、权限不足)
#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:

#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。

#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=1int fd=open("testfile",O_WRONLY|O_CREAT,0666);
}

dup2接口

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

#include <unistd.h>
int dup2(int oldfd, int newfd);核心作用是将新的文件描述符 newfd 指向旧的文件描述符 oldfd 所关联的文件
使得两个描述符最终指向同一个文件返回值
成功:返回新的文件描述符 newfd
失败:返回 -1,并设置全局变量 errno 以指示错误原因
输出重定向
#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文件中

输入重定向
#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");elsebuffer[n]=0; //添加\0printf("%s\n",buffer);
}

标准错误

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

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

#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的文件无效。

其完整写法为:./sysIO 1>log.txt

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

法一:
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下一切皆文件!


缓冲区

什么是缓冲区?

        内存中的一段空间。

为什么要引入缓冲区?

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

代码一:
#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 结构体保存了指向缓冲区的地址,这样就能找到并操作缓冲区!

文件缓冲区(内核级)

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

http://www.lryc.cn/news/613789.html

相关文章:

  • Android 系统的安全 和 三星安全的区别
  • 华为USG防火墙双机,但ISP只给了1个IP, 怎么办?
  • 5. 缓存-Redis
  • 【Android笔记】Android 自定义 TextView 实现垂直渐变字体颜色(支持 XML 配置)
  • 考研复习-计算机组成原理-第四章-指令系统
  • wstool和git submodule优劣势对比
  • WinForm 对话框的 Show 与 ShowDialog:阻塞与非阻塞的抉择
  • qt中实现QListWidget列表
  • GUI:QT简介
  • C# GUI程序中的异步操作:解决界面卡顿的关键技术
  • 频谱图学习笔记
  • HTTP 请求返回状态码和具体含义?200、400、403、404、502、503、504等
  • Docker搭建Jenkins实现自动部署:快速高效的持续集成之道!
  • 五十五、【Linux系统nginx服务】nginx安装、用户认证、https实现
  • 芯伯乐XBL6019 60V/5A DC-DC升压芯片的优质选择
  • 查看泰山派 ov5695研究(1)
  • 【重磅发布】flutter_chen_keyboard -专注于键盘相关功能
  • MFC扩展库BCGControlBar Pro v36.2:MSAA和CodedUI测试升级
  • Kotlin 数据容器 - MutableList(MutableList 概述、MutableList 增删改查、MutableList 遍历元素)
  • 【Qt开发】常用控件(二) -> enabled
  • 日本站群服务器与普通日本服务器对比
  • 服务器硬件电路设计之I2C问答(一):为什么I2C总线要加上拉电阻?
  • 汉明码:从原理到实现的深度解析
  • UniApp Vue3 TypeScript项目中使用xgplayer播放m3u8视频的显示问题
  • Emacs 折腾日记(二十九)—— 打造C++ IDE
  • 机柜内部除了服务器还有哪些组件?
  • 微软发布Project Ire项目:可自主检测恶意软件的人工智能系统
  • 微软公布Windows 2030,要彻底淘汰鼠标、键盘
  • 【概率论】均匀分布的伪随机数
  • WebForms 实例