目录
在c语言中我们学习了fopen,fread,fwrite接口,用于进行文件相关的操作,在之前我们学习了计算机的相关结构,由下往上依次为硬件层,驱动层,操作系统层,系统调用层,用户层,c语言中的接口是处于用户层的,用户层是不能直接跨过操作系统从而对对应的硬件设备进行读写操作,必须从上往下贯穿整个操作系统才能进行读写操作,本期我们将详细学习这一部分知识的基本原理。
库函数文件操作
写文件
#include<stdio.h>
#include<string.h>
int main()
{
FILE* fp=fopen("./log.txt","w");
if(!fp)
{
printf("open fail\n");
}
else
{
const char* msg="hello world\n";
//fwrite为实际写了多少字节的数据
fwrite(msg,strlen(msg),1,fp);
}
fclose(fp);
return 0;
}
fopen打开文件之后,返回的是一个文件指针,文件指针指向了一个文件结构体,文件结构体里包含了打开的文件相关信息。
读文件
int main()
{
FILE* fp=fopen("./log.txt","r");
if(!fp)
{
printf("open fail\n");
}
else{
char buf[1024];
const char* msg="hello yjd\n";
//一次读取一个字节,msg的一个元素一个字节,总共读取strlen(msg)个字节,返回值为实际读到的字节的个数。
ssize_t s= fread(buf,1,strlen(msg),fp);
if(s)
{
printf("%s\n",buf);
}
}
fclose(fp);
return 0;
}
log.txt中的文件。
代码中的ptintf想必大家都很熟悉,就是将要打印的文件打印到标准输出上,那么什么是标准输出呢?下来我们一一进行解答。
在C中我们有三个输出流,标准输入标准输出和标准错误,分别对应了键盘文件,显示器文件和显示器文件。
不难发现这三个流的返回类型也是FILE*,我们发现打开文件的返回类型也是FILE*,这个FILE*究竟是何种神圣呢?
其实,流中的操作其实都可以理解为对流文件的读写操作,标准输入流就是从键盘文件中进行读取,标准输出流和标准错误流都是往显示器文件中进行写入操作。所有的读取和写入的前提都是先打开文件。既然进程要打开文件,那么操作系统为了进行管理就必须创建打开文件对应的数据结构,从而对打开的文件进行管理,也即我们一直所讲述的先描述后组织。所以fopen和三个流的返回值类型为FILE*也就可以理解了,在C++中也一样,stdin,stdout和stderr分别对应cin,cout和cerr。
上述的对文件的读写操作都是站在用户的角度对文件进行读写的,实质上是对系统调用文件读写接口进行了封装,下来我们将学习系统调用文件读写接口。
系统调用文件操作
上一个标题我们使用的是库函数中的文件接口进行文件操作,实际上库函数接口的实现往往是基于系统调用接口。
写文件
#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("./log.txt",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
printf("open err\n");
return 1;
}
else{
const char* msg="hello YJD\n";
int len=strlen(msg);
//write函数的返回值是实际写了多少字节的数据
write(fd,msg,len);
}
close(fd);
return 0;
}
代码中的fd表示的是文件描述符,至于什么是文件描述符,我们下面会讲到。
读文件
#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("./log.txt",O_RDONLY);
if(fd<0)
{
printf("open err\n");
return 1;
}
else{
char buff[1024];
const char* msg="hello YJD\n";
int len=strlen(msg);
//read函数的返回值是实际读了多少字节的数据
ssize_t s = read(fd,buff,len);
if(s<0)
{
printf("read err\n");
return 1;
}
else{
printf("%s",buff);
}
}
close(fd);
return 0;
}
以上便是系统调用文件操作的相关接口。
文件描述符fd
在上个标题我们提出了文件描述符的概念,那么究竟什么是文件描述符呢?
在一个文件没有被打开时,这个文件是在磁盘上的,当一个文件被进程打开时,就被加载到了内存中,我们知道操作系统是系统软件和硬件的管理者,当一个文件被打开时,也就意味着一个软件被打开,所以操作系统要管理被打开的文件,就得请出我们的六字真言“先描述,在组织”,所以当一个文件被打开加载到内存时,操作系统就得为这些打开的文件创建对应的数据结构,从而方便进行管理。被打开的文件的数据结构我们用 struct file来表示,每打开一个文件,就为这个文件创建对应的struct file结构体对象,这个数据结构中存储的是对应的打开的文件的相关属性数据。以图示为大家进一步解释。
进程相关的属性和数据存储在进程控制块中。进程再打开文件时,一次可以打开多个文件,那么进程如何知道自己打开的多个文件是什么文件呢,所以在进程控制块中有一个struct files_struct*的结构体指针,指向了struct files_struct结构体,在这个结构体里面有一个指针数组,数组的每一个元素都是一个struct file*类型的结构体指针,分别指向了该进程在内存中打开的文件。所以进程通过这样一个结构体指针的方式能够清晰的知道自己打开了哪些文件。
在图示中我们会发现一个struct file* fd_arry[fd]的指针数组,我们上述所讨论的文件描述符就是这个数组的下标,所以当进程在内存中打开一个文件,对应就会在指针数组中分配一个下标,让该下标所对应的元素指向被打开的文件。
阅读下面的代码。
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("./log.txt",O_RDONLY);
printf("%d\n",fd);
return 0;
}
通过运行截图我们不难看出,这个fd的值竟然是3,为什么是3呢?下标难道不应该是从0开始的吗?
我们之前提到了标准输入stdin(键盘文件),标准输出stdout(显示器文件),标准错误stderr(显示器文件)。其实,fd就是从0开始的,但是我们所有的进程都是1号进程bash的子进程,bash是终端进程,终端就意味着要输入命令,要输出结果,要输出错误。所以bash终端进程就默认会打开键盘文件和显示器文件,所以操作系统也就会为bash进程默认分配0,1,2这个文件描述符,对应打开的键盘文件,显示器文件和显示器文件。这里还有一个知识点,就是所有的子进程都会继承父进程的数据结构,所以父子进程的 struct files_struct结构体也是两份相同的数据结构,所以对应的指针数组也是相同的,因为所有进程都是bash的子进程,所以这些进程的指针数组和bash进程的指针数组也是相同的,也就意味着所有的进程都会默认打开三个对应的文件,都会默认占用0,1,2这三个文件描述符。
深刻理解linux下一切皆文件
图示如下。
上层通过open接口打开文件,通过write和read接口进行文件的读写操作,但是实际上这个write和read接口有很多种,因为文件的种类有很多种。struct file是打开的文件的数据结构,这些数据结构中对应了多种文件,键盘文件,显示器文件,磁盘文件,显卡文件,其它文件。上层open打开一个文件就会为这个打开的文件分配一个文件描述符,后期通过文件描述符找到对应打开的文件,然后调用write和read方法进行读写,write方法和read方法对于每种文件是不一样的,通过虚拟文件层的函数指针找到具体种类文件的读写方法,这些读写方法存在于操作系统中的驱动层。所以上层的read和write方法就类似于后期我们学习的C++语法中的多态的基本概念,每种文件的读写方法对系统调用的读写方法进行了重写,在调用读写方法时,可以根据不同的文件种类调用不同文件种类的读写方法。
综上,操作系统是不考虑打开的文件是哪种类型文件的,在虚拟文件层都把不同种类的文件统一用struct file类型的结构体进行了描述,基于此,我们称在linux操作系统下,一切皆文件。
重定向原理
阅读下面代码。
int main()
{
close(1);
int fd = open("./log.txt",O_WRONLY|O_CREAT,0644);
printf("hello yjd\n");
return 0;
}
运行上述代码,我们发现再运行了可执行程序之后,终端并没有打印hello yjd,但是我们惊奇的发现在log.txt中,竟然出现了hello yjd这个字符串,这究竟是为什么呢?
其实,整个过程就是重定向的原理,将在显示器上打印的数据写入了文件中。整个过程的原理如图所示。
上图展示的是输出重定向的全过程。
printf函数是向标准输出文件(显示器文件)写入数据,从而在显示器上进行显示。标准输出文件对应的文件描述符为1,对于上述代码,当我们关闭1号文件描述符,即让显示器文件与1号文件描述符对应的结构体指针取消链接关系,然后我们打开了log.txt文件,然后让给其分配了1号文件描述符。这便是问题所在,与其说printf函数是向标准输出文件写入数据,不如说,printf是向1号文件描述符所对应的结构体指针指向的文件写入数据,因为此时1号文件描述符对应的结构体指针指向的文件时log.txt,所以此时printf就会向log.txt中写入数据。
这便是我们打印字符串,在显示器上看不见字符串,但是在log.txt中可以看到对应字符串现象的原理解释。输入重定向的原理也是类似的。
除了close描述符,还有dup2函数。代码如下。
int main()
{
int fd = open("./log.txt",O_WRONLY|O_CREAT,0644);
dup2(fd,1);
printf("hello yjd\n");
return 0;
}
dup2函数的含义为,让第二个参数(文件描述符)指向第一个参数(文件描述符)对应的结构体指针所指向的文件。
以上便是本期的所有内容。
本期内容到此结束^_^