前置知识的简单了解
- 文件包括了文件内容和文件属性两个部分(文件内容顾名思义就是文件里面的数据等内容),而文件属性就是文件的相关信息
以上这些信息都是属于文件属性的范畴 - 要访问一个文件,必须先将文件打开,不管以什么形式打开文件(不论是编程语言的文件操作还是鼠标双击打开),都是将文件先加载到内存上的一个操作(也就是说在打开文件之前,访问文件的程序就已经运行起来了)
- 不论是以什么形式打开一个文件,文件是被进程打开的
- 一个进程可以打开多个文件
- 在一个时间段内,系统中会存在多个进程,那么也就意味这可能会存在更多的被打开的文件,作为操作系统,肯定是要管理这些被打开的文件。如何管理?(先描述,再组织),也就是说操作系统会去为每一个被打开的文件创建一个结构体,结构体中成员就是文件的各种信息即操作
- 不是所有的文件都被进程给打开了,没有被打开的文件被放到了磁盘当中
- 被打开的文件通常称为内存文件
简单回顾C语言的文件操作
如果让你用C语言输出信息到显示器,你有哪些方法?
#include<stdio.h>
#include<string.h>
//分别用不同的方法向屏幕输出内容
int main()
{
printf("printf\n");
fprintf(stdout,"fprintf\n");
fputs("fputs\n",stdout);
const char*str="fwrite\n";
fwrite(str,strlen(str),1,stdout);
return 0;
}
以上程序输出结果为
由此可见,虽然使用的不同的函数,但是通过不同的方法,同样可以往显示器上输出数据,就拿printf
和fprintf
来说,fprintf
相较于printf
只多了一个参数stdin
,但是它们最后执行的结果都是一样的,那么这个stdin
就值得我们去研究
stdin&stdout&stderr
在C语言中,程序会默认打开三个流,分别是stdin(标准输入),stdout(标准输出)和stderr(标准错误),我们可以通过man手册来查阅这三个东西究竟是什么
不难看出,这三个东西都是类型为FILE*的结构体指针,同时也是fopen的返回值类型,也就是文件指针,关于这个结构体里究竟有什么,接下来再慢慢分析
系统文件IO
操作文件,除了C接口之外(当然其他的编程语言也有对应的文件操作),我们还可以使用系统调用接口来进行文件访问
首先要认识到的一个系统调用接口就是open
pathname:要打开或者创建的目标文件
flags:打开文件时,可以传入多个参数选项
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
以上三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC:若文件已存在,且打开文件是为了写,那么使用该选项就会将文件之前内容清空
这些选项实质上类似于就是C语言中宏,所有的宏都是只有一个bit位上为1,其他bit位上为0,并且它们为1的bit位都是相互错位的
这么说可能有些抽象,我在这里举个例子
这里面的用#define定义的都是宏,当往Print
中传参数的时候,通过按位或实现虽然只占用了已给参数的位置但是可以通过选项使执行的结构不同
返回值:
成功:返回新打开的文件描述符
失败:返回-1(将错误码放到errno中)
mode:权限位(只有在需要创建文件时才会用到)
这就是通过系统调用使用只读方式打开一个文件,并且使用系统调用close
关闭文件,运行以上代码
发现确实是在当前目录下创建了log,txt
文件
使用了以上系统调用,我们发现用以上open方式打开一个文件就和C语言的以只读方式打开文件非常的相似(系统调用的其他接口比如write、read、lseek等等都可以在C语言中找到类似的接口,这里就不再一一列举)
fopen=("log.txt","r");
通过以上我们就可以大胆推测,C语言的文件操作有关的函数中是一定封装了系统调用接口。那么这个fd什么东西呢?不妨将这个fd打印以下看看
为了方便观察,可以一次性打开多个文件观察fd有什么特点
我们发现打印的fd从3开始逐渐递增,这是要认识到fd到底有什么含义
open函数的返回值
在认识返回值之前,先要明确两个概念:系统调用和库函数。之前所提到的open和close就是系统提供的接口,我们称之为系统调用接口,而C语言的fwrite、fopen、fclose等等都是属于C标准库中的函数,我们称之为库函数
当需要打开一个文件的时候,必定要先从磁盘中找到这个文件,然后将这个文件的文件内容和文件属性加载到内存当中。当我们向屏幕或者是其他地方打印字符串的时候,必定会需要访问硬件资源,比如我们使用到的printf函数向屏幕写入数据,显示器是硬件吗?答案是肯定的,所以你向显示器中写入数据本质上是向显示器这个硬件单元中写入数据;
我们发现只要是发生了文件IO,不管是什么语言,最后一定是要和硬件产生关联。根据计算机系统的层状结构,最底层是各种硬件,然后是操作系统,接着是系统调用接口,最后才是用户层
当一个C程序通过pritnf这样的库函数去访问硬件资源的时候,由于上面所述,C程序是不能直接去访问硬件资源的,它必须要通过操作系统来间接访问。因为操作系统是所有软硬件的管理者,它不允许应用层直接绕过操作系统去访问硬件,所以决定了在文件操作的时候,整个操作必须贯穿整个计算机体系的层状结构。
站在操作系统的角度上,它必须提供一系列的系统调用为用户层服务(换句话说,操作系统要为我们提供一系列与文件相关的系统调用才有了C语言的一系列的文件操作)
一句话总结:可以认为f#系列的函数,都是对系统调用的封装,方便二次开发
文件描述符fd
通过之前的代码,我们可以知道,fd就是一个整数,但是不知道这个整数具体有什么含义
打开文件背后的操作
当我们打开一个文件的时候,操作系统要在内存中创建相应的数据结构来描述,这个数据结构在linux中是一个结构体,叫做struct file
,表示一个已经打开的文件对象(这个结构体对象中保存了文件相关的信息例如:inode、属性、方法集、权限等等,当然操作系统中会有多个文件被打开,自然内存中会有多个文件结构体,操作系统为了管理这些结构体,会将这些结构体用指针的方式连接起来,用链表的方式管理)
在进程的task_struct当中存在一个类型为struct files*
的结构体指针files
,指向类型为struct files_struct
的结构体,这个结构体中存在一个指针数组struct file* fd_array[]
,数组里面存的就是该进程所打开的文件所对应的文件对象,而文件描述符就是对应的数组下标
文件描述符的分配规则
当我们直接打开一个文件
当我们关闭0或者2(这里以关闭0为例)
再次运行发现结果变为了0
文件描述符分配规则:在ffile_struct数组中,找到当前没有被使用的最小的下标,作为新的文件描述符