一、文件学习的简单铺垫
我们都知道,文件等于文件内容加上文件属性。访问文件之前都得先通过进程才能打开相应的文件,一个进程可以打开多个文件。修改文件,都是通过执行代码的方式完成修改。要对文件进行修改(或其他操作),文件就必须事先被加载到内存当中。从前面的叙述我们可以知道,在一定的时间段内,系统可能呢存在多个进程,也一定会存在多个被打开的文件,操作系统通过先描述再组织的方式对这些文件进行统一的管理。所以,在操作系统内部,一定会存在描述被打开文件的结构体,并用其定义对象。没有被打开的文件就存储在磁盘当中。
二、对文件路径产生与由来的理解
从上面的铺垫可以看出,文件与进程一定是密切相关的,下面我启动一个进程,在这个进程中创建了一个文件并向其中写入内容:
并获得该进程的pid:
在proc目录中找到该进程对应的所有属性,其中就可以看到该进程所处的目录。进程在打开时,会自动记录它当前所处的目录,所以在该进程中创建的文件如果不带路径,那么自然而然地就被创建在了进程所在的目录中。这就是文件路径的产生与由来。
三、文件的系统调用接口
我们都知道,上层的语言是无法直接访问底层的硬件的,要访问硬件必须通过操作系统,所以操作系统就必须提供所谓的系统调用函数来供用户使用。那为什么我们看不到系统调用函数呢?原因是因为在语言层面上已经封装了系统调用函数。就比如说fopen函数就封装了open函数。下面我会以open函数对系统调用函数做一些简单的介绍。
open函数
我们可以看到open函数中有flags这个参数,其实flags这个参数你可以把它看成32个比特位,其中每一个比特位代表不同的权限(类似于位图),所以我们就可以通过与或操作对flags设置不同的值,进而设置文件的读写权限。
mode是权限的意思,如果一个文件事先还没有被创建出来,就必须在创建的时候通过设置mode指明它的权限。
O_WRONLY表示只写权限位(注意这里的只写与C语言中的w权限不一样,O_WRONLY并不会将原有的文件内容清空再写入,而是直接在文件的开头进行覆盖式写入,如果要将原有的文件内容清空再写入需要再或上O_TRUNC),O_CREAT表示文件不存在就创建权限位,0666表示我设置的权限位(也会受umask掩码的约束),如果一个文件本来就已经存在了,在打开的时候就不用传递权限了。这样也可以创建出一个文件。
四、文件描述符详解
文件描述符就是指的上面代码中open函数返回的fd这个int类型的值。在C语言中,描述文件的FILE类型其实就是一个结构体,该结构体中封装了特定的fd。在系统当中,一定会存在多个被打开的文件,操作系统通过先描述再组织的方式(创建出struct file结构体)对这些文件进行统一的管理,而一个进程也可能会打开多个文件,所以在描述进程的task_struct结构体中就存在一个指针struct files_struct* files,这个指针指向一个结构体,在进程创建的时候这个结构体就被创建出来了,在这个结构体中包含了很多字段,其中就有一个struct file* fd_array数组(也叫做文件描述符表),保存了该进程所打开的所有文件的地址,这样,该数组的数组下标就和文件的地址产生了一一对应的关系,而这里的数组下标就是fd。
所以我们打开文件的本质就是先在磁盘当中找到这个文件,然后将其内容和属性加载到内存当中,创建出struct file结构体,将结构体内的属性、方法、缓冲区全部初始化,再将struct file结构体链入到操作系统管理被打开的struct file结构体链表中,再将struct file结构体的地址填入到对应进程中的struct file* fd_array数组,最后将数组下标返回给用户。最后我们总结一下:其实文件描述符就是数组下标。正如下面这个图所示:
所以现在我们就可以大致说一下,向文件中写入内容(write函数),本质就是通过fd找到对应的struct file结构体,将数据写入struct file结构体对应的缓冲区中,再由操作系统帮我们把数据写入磁盘当中。从文件中读内容(read函数),本质就是通过fd找到对应的struct file结构体,从该结构体的缓冲区中读取数据,如果缓冲区中没有对应的数据,操作系统就会从磁盘中加载对应的数据到缓冲区中,再将数据根据上层给的地址拷贝到内存当中,文件的内容就被读到了。上面所述内容在操作系统源代码中也是可以得到验证的。
五、总结:理解一切皆文件
各个硬件在底层的读写方法肯定都是不一样的,在创建struct file结构体时,需要创建read和write两个函数指针,在调用不同的硬件时,只需要让函数指针指向不同的函数,返回的是都是read或write方法,从而屏蔽了底层硬件上的差异,使得linux下一切都可以当成文件来看待。如图所示: