目录
在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;
}
读文件
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* 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下一切皆文件
图示如下。
综上,操作系统是不考虑打开的文件是哪种类型文件的,在虚拟文件层都把不同种类的文件统一用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这个字符串,这究竟是为什么呢?
其实,整个过程就是重定向的原理,将在显示器上打印的数据写入了文件中。整个过程的原理如图所示。
上图展示的是输出重定向的全过程。
这便是我们打印字符串,在显示器上看不见字符串,但是在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函数的含义为,让第二个参数(文件描述符)指向第一个参数(文件描述符)对应的结构体指针所指向的文件。
以上便是本期的所有内容。
本期内容到此结束^_^