【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 结构体保存了指向缓冲区的地址,这样就能找到并操作缓冲区!
文件缓冲区(内核级)
内核级缓冲区存放在内存中的,对应在虚拟地址空间中的内核空间。