云计算百科
云计算领域专业知识百科平台

【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 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:打开文件

返回值:成功返回文件描述符(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=1
int 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");
else
buffer[n]=0; //添加\\0
printf("%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 结构体保存了指向缓冲区的地址,这样就能找到并操作缓冲区!

文件缓冲区(内核级)

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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Linux系统】万字解析,文件IO
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!