目录
- 前言
- 一、背景知识
- 1.文件的真面目
- 2.对文件进行的操作
- 3.所谓的打开文件究竟是在干啥?
- 4.文件可能存在的位置?
- 5.文件操作的幕后主使者
- 二、复习C语言中的相关文件操作
- 1. 源代码:形成一个file.txt的文件
- 2. 以"r"的方式打开文件
- 3. 以"w"的方式打开文件
- 4. 以"a"的方式打开文件
- 5.文件形成的路径
- 三、操作系统中的相关文件操作的系统调用接口
- 1. open
- 2.write
- 3.read
- 四、文件描述符
- 1. 查看open函数的返回值
- 2.实验:验证0,1,2和C语言中的stdin,stdout,stderr的关系
- 3.文件描述符的本质
- 4.文件描述符的分配规则
前言
在前面,我们学习C语言的时候已经学习过相关的对文件进行操作的函数接口,我们知道对文件进行的操作最常见的有打开文件,关闭文件,对文件进行读写操作,那么今天我们需要学习的是操作系统层面上的对文件进行操作的理解,会进一步加深对文件的理解和学习
一、背景知识
1.文件的真面目
一个文件是由其文件内容和文件属性共同构成的,在计算机中**,我们将一个文件的文件内容和文件属性统称为文件的数据**,一个文件一旦被创建,它存在的地方就有可能是磁盘和内存,当一个文件没有被打开的时候,这个文件是在磁盘中静静地躺着的,当一个文件被打开的时候,是会由相应的进程将这个文件加载到内存,此时,文件存在的位置就是内存
2.对文件进行的操作
由上面的介绍我们知道,一个文件是由文件内容和文件属性构成的,因此对文件的操作本质上就是对文件内容的操作和对文件属性的操作,我们需要注意的是,有时我们在修改文件的内容的时候,有可能也会影响到文件的属性数据,比如:当我们对一个文件的内容进行修改的时候,那么修改完成之后,这个文件的内容明显会发生变化,那么这个文件的修改时间和文件的大小等也会发生变化,这些数据就属于文件的属性数据
3.所谓的打开文件究竟是在干啥?
这个过程其实是在将文件的属性数据或者内容数据加载到内存,因为,当我们要执行这个文件的时候,明显是需要CPU来处理这个文件的,需要CPU读取相关的指令从而对这个文件的进行读写操作,而我们知道,由冯诺依曼体系可知**,CPU在数据层面上是不与外设直接接触的**,当一个文件没有被打开的时候,那么这个文件是存放在磁盘上的,磁盘属于一种外设。因此如果我们想要打开一个文件从而对其进行读写操作,首先肯定是需要将这个文件先加载到内存,然后才能上CPU运行
4.文件可能存在的位置?
一个文件被创建的时候,这个文件是处于关闭状态的,那么此时这个文件存在的位置是磁盘(磁盘文件),而由上面一条知识点我们知道,当我们在打开文件的时候,本质是操作系统正在将这个文件加载到内存,从而此时这个文件我们可以理解为正在被运行,此时这个文件存在的位置就是内存(打开的文件)。因此文件存在的位置可能是磁盘也可能是内存
5.文件操作的幕后主使者
我们知道,对文件进行的操作主要是打开文件,关闭文件,对文件进行读写操作,那么这些操作本质上到底是谁在执行的呢?我们平时写的C语言函数中使用到一些函数接口来进行这些操作,但是我们写完后发现这些文件操作并没有被执行,当我们将写的源代码编译形成可执行程序之后,对应的文件操作依旧没有被执行,当我们将可执行程序运行起来的时候相应的文件操作才能被执行起来,我们知道,当一个可执行程序运行起来的时候,本质上是在内存中创建了对应的进程,因此文件的操作本质上是进程在对文件进行操作
二、复习C语言中的相关文件操作
1. 源代码:形成一个file.txt的文件
这个源代码的作用是会在当前路径下形成一个叫file.txt
的文件,并且这个代码中打开文件的方式是"w",就是以写的形式打开,我们需要知道的是常见的打开文件的方式有三种,分别是"r","w","a"
,那么这三种打开文件的方式分别是以读的方式打开,以写的方式打开,以追加写的方式打开,区别在于第二个以写的方式打开的时候呢,如果文件不存在那么会创建对应的文件,并且如果文件存在,文件中存在内容,则首先会将该文件中的内容清空,再从文件的头部重新写入,第三个以写的方式打开,不会清空文件中的内容,而是会在文件的末尾继续追加内容到文件中
下面的实验中,对file.c源文件编译之后形成的可执行程序叫file,对file2.c编译之后形成的可执行程序叫做file2,对file1.c编译形成可执行程序叫file1
2. 以"r"的方式打开文件
- 源代码
- 运行结果
上面的实验中,我们首先使用file形成对应的文件,再使用file2中以读的方式继续打开该文件,实验现象是可以正常将文件的内容输出到显示屏上,并且不会将文件的内容清空
3. 以"w"的方式打开文件
- 源代码
- 实验现象
我们首先将file文件运行起来,这里首先会形成一个file.txt文件到当前路径下,但是当我们将file1运行起来的时候,我们再次打开这个file.txt文件的时候,我们发现观察不到任何的内容,此时证明这个文件被清空了
4. 以"a"的方式打开文件
-
源代码
-
实验结果
从最终的实验现象中我们可以看到外面再file3程序中为file.txt文件中写入的内容最终被写在了上次运行之后的file.txt中已经形成的文件之后
5.文件形成的路径
当我们文件的时候其实是需要指明这个文件存在的路径的,那么如果没有指明这个文件存在的路径,那么就会默认在当前路径下形成这个文件,当前路径指的就是当前进程所在的路径,这个我们可以使用ls /proc/pid -l
进行查看:
- cwd:指的是当前进程的路径,其实就是correct work directory,就是当前的工作路径
- exe:指的是当前可执行程序的路径
三、操作系统中的相关文件操作的系统调用接口
1. open
- 查看系统手册
通过这里的手册的介绍,我们可以知道,open这个接口的作用是打开文件,这个文件有三个头文件,然后手册中介绍了三个使用方法的声明方式,我们最常用的是其中的第二个声明方式,其中的第二个声明方式有三个参数,第一个参数是文件的名字(带路径),第二个参数是标记位(后面会重点介绍),第三个参数是设置文件的权限 - open中的flags详解
这个flags的作用是告诉操作系统我们要以什么方式打开文件,其中最常见的标记位有:O_RDONLY:只读
O_WRONLY:只写
O_RDWR:读写
O_APPEND:追加
O_CREAT:不存在就创建
O_TRUNC:清空
然后这个标记位是以位图的形式传参的,下面举一个例子来辅助理解 - 源代码
- 实验结果
在上面,我们定义了几个宏,其实分别可以表示一个整数中的几个比特位,1表示0001,2表示0010,4表示0100,8表示1000,其中如果要传一个,那么直接传入即可,如果要传多个参数,那么需要用按位或进行传参,才能够将多个逻辑对应起来
- 打开文件(读文件)
- 源代码
- 实验结果
能够在当前路径下形成了一个file.txt的文件
- 写文件
- 源代码
- 实验结果
上述实验成功将hello mywrite字符串写入到文件中,调用的是系统调用接口write - 查看write
- 注意:
我们会发现,上述的代码执行两次之后得到的是同样的结果,这是因为上述的代码再重新执行程序打开文件的时候会先将文件中的内容清空,再重新进行写入,我们可以稍微代码做修改,实验现象会更加明显
我们可以将向文件中写入数据的代码先注释起来,然后查看实验结果 - 实验结果
我们可以观察到,第二次打开文件的时候,发现文件中的数据已经被清空,这个代码的功能相当于C语言中的以"w"的方式打开文件
- 追加写文件
-
源代码
-
实验结果
通过上面的实验结果我们可以发现,第二次在执行可执行程序二次打开文件的时候,文件的内容新增了,原来的文件内容并没有被清空,上面的代码的作用相当于C语言中学习的以"a"的方式打开文件,就是不会清空原来文件中的数据,而是会重新向文件内容的末尾中追加写入数据
2.write
- 查看man手册
- 函数参数
- fd:代表向哪个文件写入,fd是一个文件描述符
- buf:代表写入的内容,一般情况下我们是将要写入文件中的内容先放在一个数组中的
- count:代表写入的内容的字符数
- 使用
- 源代码
- 实验结果
通过上述的实验结果,我们可以看到,我们确实成功在当前路径打开了一个文件,并且成功将我们的内容写入到文件中,其实本质上我们调用write的时候是将数据写入到系统内核的,不是直接写入到磁盘上的也就是所谓的文件,后面我们学习缓冲区的时候会具体说到
3.read
- 查看man手册
- 参数
- fd:指明从哪个文件读取内容
- buf:读取的内容存放的位置
- count:读取的内容的字符个数
- 使用
- 源代码
- file.txt中的 内容
- 实验结果
从上面的实验结果来看,我们确实成功将file.txt中的内容读取到并且将其输出到显示器上,源代码中我们需要注意的是,在读取文件内容的时候,我们首先需要准备一个数组来存放我们即将从文件中读取到的数据,然后读取的个数一定不能超过数组最大的空间,并且要预留出一个字节的空间来存放’\0’,read接口调用成功之后会返回实际读取的字节个数,因此那么数组中下标位返回值的位置存放的就是读取的最后一个元素的下一个位置,因此,当我们读取成功的时候,我们需要注意将最后一个元素的下一个位置设置成’\0’
四、文件描述符
1. 查看open函数的返回值
- 源代码
- 实验结果
实验结论:通过上面的实验结果,我们惊奇的发现open函数的返回值竟然是从3开始的,并且以一定的次序排列下去,这是为什么呢?0,1,2为什么不行呢?
其实0,1,2默认是被打开的,并且都是具有一定意义的,0代表的是标准输入,对应的硬件设备是键盘,1代表的是标准输出,对应的硬件设备是显示器,2代表的是标准错误,对应的硬件设备是显示器
2.实验:验证0,1,2和C语言中的stdin,stdout,stderr的关系
- 查看stdin,stdout,stderr
- 源代码
- 实验结果
3.文件描述符的本质
为什么文件描述符是一个个的自然数呢?为什么不能是其他类型的数据呢?文件描述符的本质是什么呢?想要回答这些问题就必须深入了解系统文件之间是怎么组织起来的
- 通过上面的实验,我们知道一个进程是可以打开多个文件的,也就是说系统中进程数与打开的文件数的比例是小于1的,也就是说系统中被打开的文件的数量是非常多的,那么通过前面的学习我们知道,系统中的进程数量也是非常多的,所以系统需要对进程进行管理,那么被打开的文件的数量比系统中的进程的数量还要多,那么需不需要进行管理呢??肯定是需要的,不然系统中的文件岂不是乱套了,那么如何进行管理呢??这个时候我们就需要类比我们在学习进程管理的时候的相关策略了,也就是先描述再组织,所谓的先描述就是系统需要为每一个打开的文件创建一个结构体,这个结构体中就会存放相应的被打开的文件的相关属性,其实这个就是FILE类型
- 我们需要来观察系统级别中进程与文件的关系
系统中进程与文件的对应关系就大概如上图所示,系统会为每一个打开的文件维护一个结构体是FILE类型的,每一个结构体就会有对应的地址,是FILE*类型的,那么这些结构体的地址会存放到一个结构体中的一个数组中,这个数组的下标是从0开始往下排列的,数组中存放对应文件指针的下标就是对应文件的文件描述符,从进程的角度来看,进程的task_struct中有一个指针可以找到这个结构体,那么就能够通过这个指针找到这个结构体中的存放文件指针的数组,那么就能够通过数组下标即文件描述符找到对应的打开的文件,那么打开文件中调用open函数接口返回的整数其实就是这个数组的下标,因此,我们后面再将这个返回值传给read函数或者write函数的时候,那么对应的系统调用不就可以通过这个整数(即文件描述符)找到对应的文件指针,从而找到对应的打开文件了吗?
4.文件描述符的分配规则
-
源代码(正常情况下)
-
实验结果
-
源代码(将文件描述符为0的文件关闭)
-
实验结果
-
源代码(将文件描述符为1的文件关闭)
这个代码需要注意一些东西,因为文件描述符为1 对应的文件是显示器,如果将1关闭,那么就不能向显示器中输出信息了
-
实验结果
那么此时printf输出的数据到哪里去了呢?分析过程是这样的,因为我们先将文件描述符为1的文件关闭了,所以在文件描述符数组中,1是最前面空闲的,所以此时会将1分配给新打开的文件,printf是向stdout输出的,但是从printf的角度看,printf函数是C 语言函数,它并不知道操作系统已经将stdout关闭了,它只知道文件描述符1,所以,它会继续向文件描述符为1的对应文件写入,而我们知道文件描述符1已经分配给了新打开的文件,因此,此时printf函数会向新打开的文件写入 -
源代码(将文件描述符为2的文件关闭)
-
实验结论:通过上面的实验结果我们可以看出,文件描述符并不一定是从3开始的,如果我们手动将0,1,2中的任意一个文件描述符对应的文件关闭的话,那么我们新打开的文件的文件描述符也可以是0,1,2所以文件描述符的分配规则是:从头遍历文件描述符数组,找到第一个未被使用的下标的位置,将其分配给新打开的文件