文章目录
- 前言
- 什么是文件
- 文件操作
- C语言文件操作接口
- 文件操作系统调用接口
- 文件描述符
- 如何理解FILE和文件描述符的关系
前言
本文主要介绍的是Linux系统下的文件操作和Linux的文件系统的机制,希望能够通过本文来增加读者对于文件操作的基本使用和理解。以及了解Linux系统对文件管理的一套独有的优秀的机制
什么是文件
接下来,我们就要来进入我们的主题。在介绍相关操作和Linux文件 系统之前,我们要先弄明白什么是文件 或许我们对于文件既陌生又熟悉。熟悉是因为我们几乎每天都在读文件和写文件,陌生是因为我们没法能够准确描述出这个文件这个概念。 接下来我们就来理解理解什么是文件。
文件=文件内容+文件属性!
在Linux系统下,即使创建一个空内容的文件!这个文件也是要占据磁盘空间的!因为操作系统需要记录这个文件的属性!比如大小,创建时间,问价类型等等属性,而这些东西同样是数据,既然是数据,需要存储就要存储空间!
文件操作
既然文件=文件内容+文件属性。那么毫无疑问地,对文件的操作=对文件内容得操作+对文件属性的操作! 最典型的就是,假如我往一个文件里面写入东西,改变的就是文件的内容。同时由于内容增多,对应的文件的体积变大,也就是文件的大小也变大,所以此时文件的属性也发生了改变! 既然谈到了文件操作,许多高级语言也都为我们提供了一系列的和文件相关操作的接口函数,下面我们以C语言为例,来使用一下C语言的文件接口。
C语言文件操作接口
我们来回顾一下C语言的相关的文件接口:C语言有很多文件的读写操作,这里我们只用fwrite进行演示
FILE* fopen(const char* name,const char* model);
打开文件的接口,如果成功返回对应的文件指针,失败则返回NULL,model参数有如下的几种:
“r” ---->只读方式 ,“w”---->写入方式
r+ /w±–>读写方式
fclose(FILE* pf);---->关闭文件
fwirte---->向指定文件写入
下面我们就来简单得使用一下这几个接口
#include<stdio.h>
#include<string.h>
/*
* 使用C语言相关的文件接口
* fopen()--->打开文件
* fclose()---->关闭文件
* fwrie()---->写 fprintf--->格式话写
* fread()--->读 fscanf---->格式化读取
*
* */
int main()
{
FILE* pf=fopen("test.txt","w+");
//打开失败
if(!pf)
{
perror("打开失败了,不玩了\n");
return 1;
}
//打开成功,使用fwrite写入I like Linux!
const char* msg="I like Linux!\n";
fwrite(msg,sizeof(char),strlen(msg),pf);
return 0;
}
程序运行完以后,我们确实看到了在当前的目录下多了一个test.txt的文件,接下来我们打开对应的文件查看是否是否把对应得内容写入到了对应得文件里面。
可以看到,对应内容确实已经被写入到文件里面了。接下来我们在来多在原来的基础上再写入一条信息到文件里。
#include<stdio.h>
#include<string.h>
int main()
{
const char* msg="I love Linux";
FILE* pf=fopen("test.txt","w");
if(!pf)
{
perror("哥们打开文件失败了,跑路把\n");
return 1;
}
//写入文件
fwrite(msg,sizeof(char),strlen(msg),pf);
fclose(pf);
return 0;
}
程序一运行,打开对应的文件查看内容:
完蛋!我之前写入的内容咋就不见了呢?问题就出现在这个w模式上!w模式打开文件进行写入的时候,都会将原来的文件的内容清空!而如果我们要追加式写入,那么我们就要使用“a”选项!具体的相关细节操作和w模式一模一样,读者可以自行下来尝试使用。
文件操作系统调用接口
但是,这里有一个问题。我们知道文件是存放在磁盘的。磁盘是何物?磁盘是一个没有感情,冷冰冰的硬件! 换言之,我们对文件进行写入操作,本质就是对磁盘进行了写入操作!就是对硬件进行了操作! 但问题是,你只是一个编程语言,你哪里来的权力能够操作硬件?归根结底,写入磁盘的工作必须是操作系统完成的!毕竟操作系统是硬件的管理者。 但是操作系统这货又不相信用户,不直接暴露自己给用户,所以对应的操作系统必然会提供一系列的系统调用接口来给我们提供文件操作! 那么接下来来在深入理解这些系统接口之前,我们先来看一看这些接口。
首先,系统接口的命名和前面讲的fopen,fwrite,fread,fclose非常接近。 系统接口的名字就是把这些C语言接口的名字去掉一个f就可以了。
先来看第一个函数:open() ->顾名思义,就是打开一个文件,具体的函数声明如下:
int open(const char pathname, int flags);
int open(const char pathname, int flags, mode_t mode)
第一个参数是对应你要打开的文件名,第二个参数则是你要以什么模式打开。而文件打开又有如下的选项参数:
O_RONLY:只读模式打开
O_WRONLY:只写方式打开
O_RDONLY;读写方式打开
还有对应的一些附加选项;
O_CREAT:文件不存在就创建
O_TRUNC:每次打开时清空文件内容
O_APPEND:不清空文件,往文件末尾继续追加内容
介绍完了打开,我们在介绍一下往文件写入的接口函数
ssize_t write(int fd, const void *buf, size_t count);
第一个参数是open的返回值(后面我们会详谈这个fd具体是什么)
第二个参数就是指向我们要写入文件的内容
第三个参数就是我们要写多少字节的内容
write函数返回我们实际写入文件的内容的字节大小,出错就返回-1
//使用系统调用对文件进行操作
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
//屏蔽系统默认掩码的影响
umask(0);
//如果不存在要创建,就要用三个参数版本的
int fd=open("test.txt",O_WRONLY | O_CREAT,0666);
//中间进行读写操作,这里先以写入操作为例
const char* msg="I like Linux!\n";
//不需要带上'\0',文件不认识这个东西
write(fd,msg,strlen(msg));
close(fd);
return 0;
}
可以看到,这里确实生成了一个文件,并且这个文件被写入了I like Linux! 这就是write系统调用,而相对应的读取就使用的是read操作。
//使用系统调用对文件进行操作
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
//之前文件已经创建好了,接下来读取文件内容
char message[128];
memset(message,'\0',sizeof(message));
int fd=open("test.txt",O_RDONLY);
if(fd<0)
{
perror("打开失败了,直接溜吧\n");
return 1;
}
//当作字符串处理
ssize_t s=read(fd,message,sizeof(message)-1);
if(s<0)
{
perror("兄弟读取失误了啊\n");
return 2;
}
printf("读取到的内容是%s",message);
return 0;
}
这就是Linux系统里面文件的相关读写操作。接下来,我们对这些接口中一些参数进行解读,并结合一些内核的数据结构来理解这些操作的含义。
文件描述符
首先,在对于文件读写操作之前,我们做的第一件事情就是打开文件。对应的系统调用就是open,而这个open的第一个参数就是打开的文件名,而耐人寻味的就是这个文件的返回值。我们先来看看手册里面对于这个返回值的解读
open 和 creat 都 返回 一个 新的 文件描述符 (若是 有 错误 发生 返回 -1 ,并在 errno 设置 错误 信息).
这里提到了一个叫做文件描述符的东西。接下来我们就来深入探究一下这个文件描述符究竟是何物,但在谈及文件描述符之前,我们必须要理解Linux操作系统的一个设计的理念---->Linux下一切皆文件!
那对应诸如键盘,显示器,网卡等等这些硬件,Linux也是把它们看成一个个文件吗?---->是的!Linux依旧把这些设备看成文件!那么这是怎么做到的呢?
不知道读者是否还记得一个东西叫做进程地址空间,我们知道内存是无法保护自己不被非法访问。而进程地址空间不仅提供给了进程统一的视角看待内存,更是给物理内存提供了一层访问保护(软件层的保护),同样,类似于进程地址空间,只要我能够提供一套特定的软件层,负责做这些硬件相关的读写方法,这样从内核的角度,我就可以使用统一的视角来看待这些硬件设备! 而实际上,Linux内核也是这么去完成。内核中的文件的结构被称为struct file,对应的一部分的源代码如下
//文件的内核数据结构,struct file
struct file {
struct list_head f_list;
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
struct file_operations *f_op;
//....
}
//而这个file_opreations的定义如下
struct file_operations {
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
//...
};
那么对应的,在Linux内核里面,对于硬件的处理方式可以用如下的方式解决
到这里,我们就可以得到一个重要的结论:绝大多数硬件问题,都可以通过添加一层软件驱动层的方式,使得上层能够以统一的视角看待下层,屏蔽底层硬件的差异!
理解了Linux能够把一切东西抽象成文件以后,我们再来进一步认识文件。我们必须明确一个概念,我们当前所有的文件相关操作都是内存级别的!也就是说打开文件都是由进程打开的。一个进程可以打开多个文件,那么就意味着这个进程必须要把对应的打开的文件给维护起来,那么在内核数据结构层面如何体现这种维护关系呢?
struct task_struct{
//...其他属性
struct files_struct *files;
//..其他属性
};
//接下来我们跳到task_struct的对应定义
struct files_struct {
atomic_t count;
spinlock_t file_lock; /* Protects all the below members. Nests inside tsk->alloc_lock */
//...
//这个才是最重要的结构
struct file * fd_array[NR_OPEN_DEFAULT];
};
到这里,我们可以得到一个结论了:这个文件描述符fd—>其实就是用来维护一个进程和对应打开的文件的数量的关系。更准确的讲,这个fd其实就是一个文件指针数组的下标!
那么对应一个进程和它打开文件的对应关系就是下面这个样子
当我们调用open函数打开文件的时候,内核创建了对应的数据结构,对应的在进程的file_struct找到一个尚未被分配的fd也就是对应的数组下标 建立对应的映射维护关系,并且把这个下标返回给用户。
那么我们来看一看这个系统给我们创建文件以后,给我们分配的文件fd是多少
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
//打开文件test1.txt文件,查看文件
int fd1=open("test1.txt",O_WRONLY | O_CREAT,0666);
int fd2=open("test2.txt",O_WRONLY | O_CREAT,0666);
printf("test1文件的fd是%d\n",fd1);
printf("test2文件的fd是%d\n",fd2);
return 0;
}
运行结果如下:
奇怪,为什么打开的文件是从三开始的?0,1,2去了哪里了呢?难道,已经被别人给占了吗? 是的!0,1,2这三个确实是已经被占。而这三个就是我们C语言阶段接触过的三个文件流.
stdin:标准输入
stdout:标准输出
stderr:标准错误
但是这里有一个很大的问题:这三个玩意是C语言提供的,类型是FILE*,但是为什么它会把文件fd这样一个下标给占据了呢?
如何理解FILE和文件描述符的关系
结合我们前面的理解,我们知道,C语言内部所有的文件操作函数。最终都会被转化成朴实无华的操作系统对于文件的一系列的操作。那么对应的,因为操作系统只认识fd,所以对应的FILE结构体必然是封装了对应的fd文件描述,我们可以简单瞅一瞅对应的FILE类型的源代码
struct _IO_FILE {
//...很多属性
//_fileno--->就是对应封装的文件fd
int _fileno;
为了进一步验证我们的观点,我们可以尝试把C语言给我提供的三个文件流的_fileno打印出来看一看是不是0,1,2
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
printf("stdin->fd=%d\n",stdin->_fileno);
printf("stdout->fd=%d\n",stdout->_fileno);
printf("stderr->fd=%d\n",stderr->_fileno);
return 0;
}
如果这个现象还不够明显,我们不妨试一试用把stdout的file_no作为参数传递给write函数,看一看效果是什么样的。
//使用系统调用对文件进行操作
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
const char* msg="hello world\n";
write(stdout->_fileno,msg,strlen(msg));
return 0;
}
运行结果如下:
结果应证了我们先前的结论。也就是FILE封装了fd,而stdin,stdout,stderr对应封装的fd是0,1,2。
以上就是本篇文章的主要内容,如果有不足的地方,希望可以指出。