Linux文件——Ext2文件系统(2)_文件系统的理解

文章目录

Ext2文件系统(2)_文件系统的理解

我们在文章:Linux文件------文件系统Ext2(1)_理解硬件中,谈到了对于存储文件的一个重要的外设,也知道了使用层面和物理层面的差别。

本篇文章将以磁盘为主体,重点理解在Linux下的一种文件系统Ext2的各类处理方法。

引入文件系统

首先,我们还需要做一些准备工作,才能更好地对后续的文件系统进行学习。

块概念

首先,我们需要引入一个叫做"块"的概念。这个概念是操作系统层面上的。

我们知道,操作系统做IO操作的时候,是需要把内容加载到内存上,在内存上修改/操作完后再重新输出到外设上的。

我们前面又知道,磁盘上的物理结构是通过CHS定位法来实现定位的。在操作系统层面而言,把每个扇区当成一个LBA地址空间来进行使用。

但是,这里要说的是:

操作系统觉得这种方式效率太低了!操作系统认为一次对一个扇区(512字节)操作效率太低。如果要修改分散在某些扇区位置的数据,需要一个扇区一个扇区地把内容加载到内存上操作,那耗费的成本还是蛮大的。

所以,操作系统为了能够高效地进行一系列的IO操作,会一次性从磁盘中读取指定字节数的内容,加载到内存上。进行一系列操作后再输出回外设当中。哪怕这么多字节数内部只有一个比特位要修改。

这个东西,就是块!一般来说大小为4KB。也就是说,操作系统会一次性读取4KB的内容到内存中,哪怕这4KB只有那么几个字节需要修改或者读取。这是为了提效!

使用指令stat就可以查看到当前文件系统下对应的块大小。

4096字节,即4 * 1024字节,即4KB大小。


同样的,操作系统会把所有的LBA地址转化为对块的编号(从0开始编号):

一个扇区的大小是512字节,就是0.5KB。所以一个内存块 = 8个扇区。

所以,还是一样的方法,可以让LBA地址和扇区之间进行转化。

LBA / 8就可以得到当前的块号。块号 * 8可以得到当前块在LBA的起始地址。然后只需要根据0 ~ 7 的偏移量就可以得到LBA地址号了。

分区概念

但是,操作系统觉得,管理整个磁盘还是太难了。因为很多工作都是具有重复性的。因为对于磁盘的操作就是一些增删查改。所以操作系统会对磁盘空间进行分区。

在Windows下来看,分区起始就是我们所谓的C\D\E盘等一系列的盘符。这些盘符不是说在当前计算机内有这么多个硬盘/磁盘,而是把磁盘空间进行分区了!

但是,Linux系统和Windows系统是不一样的。我们知道,Linux系统下,是把所有东西都抽象成文件来看的。那么,Linux系统如何进行分区呢?

假设现在有一个磁盘,h被分为了N个柱面,我们直接给出结论:

在Linux系统下,对磁盘的分区的最小单位是柱面!所以,Linux对于磁盘空间的分区,其实就是对磁盘中的柱面进行区域划分。

区域划分我们曾经在Linux的程序地址空间内容部分讲解过:

区域的划分,其实本质上只需要记录划分区域的起始位置即可


而且,每个分区的管理、操作方法都是一样的!所以,操作系统要管理整个磁盘,只需要管理其中的一个分区就好了。其它的分区管理也很简答,只需要把管理其中一个分区的方法复制一下,其他分区使用这套复制的方法就可以了。

在接下来的讲解中,我们主要还是会围绕着某个分区来进行。讲完分区的操作后,再来回过头有谈谈对于整个磁盘中所有分区的管理。

分组概念

管理整个分区对于操作系统来说,还是太大了。所以,操作系统会在分区内进行分组管理。

从此,后续所有讲的内容,都是基于Linux系统下Ext2文件系统对于磁盘中文件的管理方式。

每个分区会被划分为若干个组。组里面存储的,就是当前文件系统下所需要存储的一些相关信息,文件内容和属性等。

inode概念

我们曾经讲过,文件 = 属性 + 内容。

我们也会常常认为,Linux对于文件的存储,会把内容和属性的相关信息存放在一起。但其实不然,在Linux系统下:文件的内容和属性是分开存储的!

至于文件的内容,其实我们可以很清晰的知道,就是存在于块里面的。因为内存块本质就是多个扇区合并成的一个空间。但是,如果文件的内容和属性需要进行分开存储,那么也就是另外需要一块空间来存储相关属性。

而且文件的属性是有很多通性的:如文件大小、文件类型、文件的acm时间等。每个文件的属性也是需要被管理的,所以,依然是先组织后描述:

Linux系统下,文件的一些相关属性都是存储在一个叫做struct inode的结构体内:

本文主要探讨的是在Ext2文件系统下的inode结构:

c 复制代码
struct ext2_inode {
	__le16 i_mode; /* File mode */
	__le16 i_uid; /* Low 16 bits of Owner Uid */
	__le32 i_size; /* Size in bytes */
	__le32 i_atime; /* Access time */
	__le32 i_ctime; /* Creation time */
	__le32 i_mtime; /* Modification time */
	__le32 i_dtime; /* Deletion Time */
	__le16 i_gid; /* Low 16 bits of Group Id */
	__le16 i_links_count; /* Links count */
	__le32 i_blocks; /* Blocks count */
	__le32 i_flags; /* File flags */
	union {
		struct {
			__le32 l_i_reserved1;
		} linux1;
		struct {
			__le32 h_i_translator;
		} hurd1;
		struct {
			__le32 m_i_reserved1;
		} masix1;
	} osd1; /* OS dependent 1 */
		__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
		__le32 i_generation; /* File version (for NFS) */
		__le32 i_file_acl; /* File ACL */
		__le32 i_dir_acl; /* Directory ACL */
		__le32 i_faddr; /* Fragment address */
	union {
		struct {
			__u8 l_i_frag; /* Fragment number */
			__u8 l_i_fsize; /* Fragment size */
			__u16 i_pad1;
			__le16 l_i_uid_high; /* these 2 fields */
			__le16 l_i_gid_high; /* were reserved2[0] */
			__u32 l_i_reserved2;
		} linux2;
		struct {
			__u8 h_i_frag; /* Fragment number */
			__u8 h_i_fsize; /* Fragment size */
			__le16 h_i_mode_high;
			__le16 h_i_uid_high;
			__le16 h_i_gid_high;
			__le32 h_i_author;
		} hurd2;
		struct {
			__u8 m_i_frag; /* Fragment number */
			__u8 m_i_fsize; /* Fragment size */
			__u16 m_pad1;
			__u32 m_i_reserved2[2];
		} masix2;
	} osd2; /* OS dependent 2 */
};
/*
* Constants relative to the data blocks
*/
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
//备注:EXT2_N_BLOCKS = 15

当然,为了识别每个文件对应的数形结合inode,每个inode是需要有一个唯一标识的:
即inode编号!

Tips:

虽然inode结构体记录的是文件的相关属性,但是,文件名并不会存储在inode集合中

inode的大小一般是128字节或者256,我们后面讲解的时候统一128字节

文件的内容大小会不一样,但是对应的属性集合大小一定是一样的

这里就稍微解释一下为什么inode内部不存储文件名:

从结果论来看,因为操作系统要保证每个文件对应的属性集合大小是一样的。这样确实是有一定的好处,大小都一样是方便进行管理的。

文件名需要使用字符数组来存储。这个是最没有办法保证大小的。总不能每个文件对应的名字长度要完完全全相同吧。所以,为了保证集合属性的大小一致性,文件名不会放在inode内。


接下来就面临着一系列的问题:

1.如何使用内存块来进行存储文件?如何确定文件要使用哪些内存块?使用了哪些?

2.磁盘的分区,分组是如何操作的,分组里面是什么?

3.操作系统如何将文件的属性和内容相联系?

4.文件名作为文件的属性却不在inode属性集合内,那么会在哪里呢?

5.inode是如何受操作系统管理的呢?

...

这里会有一系列的问题,这涉及到操作系统如何让文件系统对这些相关内容进行统一协调管理。文件系统的工作就是用来管理和组织这些。

文件系统Ext

接下来,我们将深入Linux系统下的文件系统进行讲解和学习------Ext系统。

但是要说明的是:

Linux下的文件系统会有很多种:而文件系统的载体是分区之上的!所以我们可能会看见不同的分区对应的文件系统是不一样的,如Ext2 Ext3 Ext4 xfs…

但是本次讲解重点放在文件系统Ext2在分区之上的一些操作,而Ext系列的文件系统主体的逻辑都是相通的,只是在个别方面有一些区别。

宏观的认识

首先,我们需要对Ext系列的文件系统排布有一个宏观的认识。本节采用Ext2系统。

我们想要在硬盘上储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。

因为文件系统的工作就是用来组织和管理硬盘中的文件,需要有特定的格式和排布。

上图就是Ext2文件系统的格式/排布。

操作系统会把整个硬盘分为若干个分区(Partition),每个分区的区域内有一个Boot Sector的启动块,大小为1KB,用来存储磁盘分区信息和启动信息。而具体到每个分区内又会被分为若干个块组(Block Group)。我们现在需要来看看,在每个分组下,操作系统是如何对这些硬盘上的文件进行管理的。

一旦我们明白了对一个分组的管理,就能明白对所有分组的管理。就能明白对一个分区的管理...从而我们就能知道,操作系统是如何对当前磁盘的空间进行管理的!

块组内结构认识

接下来,我们重点了解块组内的结构。

inode Table & Data Blocks

首先我们就来看属于块组的两个部分:inode TableData Blocks

首先,Data Blocks就是用来存储文件内容的地方。里面是一个个大小为4KB的块。操作系统就是在这个区域内取出大小为4KB的内容加载到内存上进行操作的。

其实这个正好对应的内存中的内存块。内存中也是存在着很多的内存块的,一般来说大小也是为4KB。这样子两边大小匹配的情况下,是方便操作系统进行IO操作的。
所以,在后续的学习中,我们都要明白一个概念:操作系统对于内容的处理的最小单位是块!

我们知道,文件的属性是用一个叫做struct inode来进行描述的,那么,操作系统如何通过这个文件系统来管理这个inode的呢?
答案是:通过inode Table来进行管理的。

inode Table是一个表,里面记录的是inode结构体和一些文件的重要信息:如权限。

我们要知道的是:在Linux操作系统下,所有的存储内容的最小单位都是数据块!内容都是放在数据块内的,一个数据块大小为4KB,假设inode大小为128字节,那么一个数据块可以存储32个inode结构体。

同时,每个文件对应一个inode结构体,每个inode结构体在文件系统中的唯一标识是inode号!

我们可以通过指令ls-i选项来查看对应的inode号:

当然,还可以使用ls -i来查看详细信息,第一列就是该文件对应的inode号。

这里只是对inode TableData Blocks进行初步认识。等到我们后面讲完其它的部分的作用再回过头来再次理解。

Block bitmap(块位图)

现在面临着第一个问题,即在读取Data Block内数据块里面的内容的时候,如何判断该数据块内是否有内容的存储?即我们怎么知道这个数据块能不能用?

这个很简单,存储内容的数据块不是有个编号吗?一个块组里面会有很多的内容数据块。所以是不可能用多个整型变量或者其他变量的数组来进行记录每个块号对应的状态的。这种情况,数据大量,只需要记录状态,最合适的就是------位图。

所以,Block bitmap就是一个位图,从低位开始编号为0的比特位。那么对于不同的块号是否给占用,只需要在Block bitmap上对应的比特位进行置1即可(置1认为是被占用了)。


但是,这里有几个问题:

首先,这个块一般来说,是只能存储一个文件的内容的,除非个别特殊情况。因为操作系统要尽可能的保证文件的独立性。否则两份文件共存在一个数据块里面是很麻烦的(如被其他文件覆盖内容,拆分数据块不方便...)

其次,假设每个Block group里面有N个Data Block,那么,对应的编号是每组内部独立,还是说分区内多个分组中唯一呢?
答案是:分区内多个分组中是唯一的!即分区内数据块号唯一,不能跨区!

第三个问题:

既然数据块号分区内唯一,那么编号是否满足一定的规律?

答案:文件系统对于数据块的编号是满足一定的规律的:

如第一个Block Group中的Data Block编号可能是0 ~ N - 1,那么第二个就是N ~ 2N - 1...
也就是说,在 ​​ext2/ext3/ext4​​ 文件系统中,​​每个Block Group内的块编号确实是集中且连续的​​,但不同块组之间的块编号是全局唯一的。

第四个问题:

既然每个Block Group内的块编号是集中且连续的,那么Block bitmap是多个组共享一份还是每个组内单独有一份呢?

答案是:每个Block Group都有一份单独的Block bitmap,用于记录该组内数据块的使用情况。

那么,在每个组内Block bitmap唯一的情况下,Block bitmap的记录状态得多进行一步操作。

假设Block bitmap是全区共一份,那么就直接在对应的bit位上进行操作即可。

但是,现在是每个组内单独一个Block bitmap,编号又是全局唯一,每个组内的Block bitmap是一样大的数组,这如何映射?

很简单,我们只需要做一步映射操作即可,假设每个组内有N个数据块
比如Group1内块号0映射到比特位0,Group2内块号N映射到比特位0,Group3内块号2N映射到比特位0,这样子后续的其他位也按照这样映射。其实就是使用取模N的方式进行映射即可。 这样,就可以让每个组内有单独的Block bitmap,位图大小还相同,也能正确地标识对应的数据块的占用情况!

inode bitmap(inode位图)

inode bitmap是用来记录当前文件系统下某个inode号是否能够使用的。也是一个位图。

在文件系统内部,一个文件的属性(除了文件名)是存放在inode结构内部的。文件系统通过inode Table进行管理。一个文件对应着一个inode号!

但是,当磁盘中创建了一个新的文件的时候,必然需要记录这个文件的相关信息,内容写入磁盘。最重要的是,分配一个inode号进行管理!这是文件的标识!

现在问题就来了,如何判断一个inode号是否被使用了呢?仍然是位图。

如果需要删除文件呢?真的需要把对应数据块中存储的内容全部清空吗?
其实不需要,只需要在对应的两个bitmap中调整一下相关状态即可(把对应数据块和inode置为可用状态)。

我们学习了各种STL的容器,我们会发现,在一些容器里,删除操作其实不需要释放空间。只需要以某种状态的标记就可以完成删除了。如vector,stack等。这些容器我们在做删除操作的时候,是不需要删除的,直接标记状态。如果下一次要使用该位置直接覆盖即可。

这里的存储数据的数据块也是一样的,我们没办法做到说直接对磁盘中的空间进行开辟和释放。因为磁盘的存储空间是固定的!所以,我们其实也可以明白,磁盘所谓的删除操作,只是需要把对应的位置进行标记状态即可。


对于inode的管理和Data Block是类似的:

每个组内各有一个对应的inode bitmap,组内的inode号也是比较集中的。

inode号也是全区唯一的!即可以跨组编号,但不能跨区编号!

所以,对于位图的处理也是一样的,也是需要经过映射才能在位图上记录状态。

inode Table

在每个块组内,inode bitmap是用来记录某个inode号的使用状态的。但是,inode结构体还是需要被组织起来并且被管理的。

在文件系统内,每个块组中存在着一个inode表,即inode Table,用来组织管理inode结构体。

但是,这里组织inode的方式并不是和组织PCB一样的。

inode Table采用的是静态的连续空间进行存储inode结构体的。每个inode固定占用128/256个字节。我们可以通过inode编号的地址来计算相对于首地址的便宜量,从而对一个个的inode结构体进行操作和管理。

GDT------Group Descriptor Table

在块组中,存在着一个部分叫做GDT,这个也成为块组描述符表。

因为当前每个块组都被分为了指定部分,如位图,数据块,inode Table...

但是,我们现在需要知道的是,每个块组内的各个部分在当前块组的什么位置?块组占用的空间是多少,如何划分空间?还有多少个空的数据块/inode?...

这些问题都会被记录到GDT这个部分当中。

其实,GDT就是用来描述当前这个块组的一些相关信息的。每个块组内都有一份单独的:

c 复制代码
// 磁盘级blockgroup的数据结构
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__le32 bg_inode_table; /* Inodes table block*/
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};

同时,块组描述符在每个块组的开头都有一份拷贝,用于备份。

Super Block

还有最后一个部分没有讲到,即处于块组中的Super Block部分。

Super中存储的是整个文件系统下的相关信息:

bolck 和 inode 的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。

这个Super其实并不像是其它的部分,每个组内独有的。因为Super Block主要的是记录当前整个文件系统下的一些相关信息。Linux下,只对第一个块组的开头要求有这个部分。

其余的块组并不是每个块组都会有这个Super Block部分的(个别有)。

因为Super Block指向的是整个文件系统的信息,一旦遭破坏、丢失,那整个文件系统都会被破坏掉,所以是需要后面部分块组前面添加这个部分的,用作备份。

格式化的本质

上面我们对块组内的一些相关结构进行了宏观的了解,现在我们其实可以大概知道,文件系统的格式化究竟是什么。

对磁盘的格式化,其实就是把文件系统管理磁盘时所需要的相关信息进行初始化!

比如上述的文件系统结构图所示:

初始化工作,其实就是把bitmap的状态全部置为可用状态,将当前组内的区域划分的相关信息导入到GDT中,将整个文件系统的信息放置Super Block中,并做好备份...

做完上述的操作后,此时就能按照特定文件系统的规则,将管理磁盘所需的元数据结构和存储区域初始化为一个"可被该文件系统识别和操作"的标准化状态​​。磁盘使用前,必须要经过格式化的操作,否则会触发意想不到的效果。

文件系统的工作流程

这里输出一个结论:
根据格式化的要求,和上述对于inode和Data Block的编号方式(本质也是格式化),可知:
在后序的对于文件系统的操作来说,只需要使用inode,就可以找到对应的文件内容!

后序的内容,我们都将围绕着这一个主题来讲。

文件系统中的目录

首先我们需要提出第一个问题:

既然说文件系统下是通过inode来找到文件的相关属性和状态,内容,这个是系统层面的,我们能够理解。但是,在上层使用的时候,我们从来没用过inode编号啊,这是怎么回事呢?

第二个问题:

前面说到,inode结构中不存储文件名这个属性,那么它会存放在哪里呢?

接下来,我们需要重点解决这两个问题。


要解决上面的两个问题,我们首先得知道Linux文件系统下对于目录的处理。

我们知道,Linux系统下一切皆文件,所以目录也必然是一个文件!所以,目录在文件系统下的存储方式其实和普通文件一样,也是有对应的Data Block的使用和inode。

但是,目录里面存储的是什么呢?
直接说答案:Linux文件系统下,目录里面存的是文件名和inode编号的映射关系!

文件名和inode编号的映射关系,本质上,也是数据!所以,存储方式和普通文件是一样的。

下面,我们可以通过一段代码来验证上面的结论:

c 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    DIR *dir = opendir(argv[1]); // 系统调⽤,⾃⾏查阅
    if (!dir) {
        perror("opendir");
        exit(EXIT_FAILURE);
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) { // 系统调⽤,⾃⾏查阅
        // Skip the "." and ".." directory entries
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
    }

    closedir(dir);
    return 0;
}

这个代码就是打开目录,然后把目录里面的内容进行读取,读取到一个struct dirent变量内,然后使用readdir取出dir目录里面的内容。

将dir对应目录下除了.之外的所有文件和inode打印出来。

我们来尝试看一下运行结果:

确实如此,所以在这里验证得到:
Linux目录内存储的内容就是文件名和inode编号的映射关系!


至此我们就能够解决上面提出的两个问题了。文件名这个属性不会存放在该文件的inode结构下,这是其所在目录文件的数据,目录会存储当前目录下文件名和inode编号的映射关系!

所以,当使用文件名进行查找文件的时候,是需要使用路径 + 文件名来查找对应的文件的,找到这个文件所处的目录,根据目录下的映射关系,就能够找到该文件对应的inode编号

找到文件的inode编号后,就可以根据inode编号查找当前inode状态。如果状态为未使用就不需要到inode Table中查找了。如果inode状态显示存在,就可在inode Table中找到对应的文件!

inode结构中也会有指向使用内容数据块的指针,这个先不讲,目前知道可以通过inode找到当前文件所属的内容即可!

所以,这里就应证了上述的结论!
文件系统可以通过inode,找到关于文件的一切信息!

路径解析

但是,上面的讲解还是会让人提出一个问题:

既然说找一个文件是需要找到它对应的目录,找到映射关系。但是,要找到文件所处的目录,这个目录也是一个文件名呀!它也要被找到对应的映射关系!

如果某个文件/目录存储比较深,按照前面讲的查找规则:

那么该路径上所有的目录/文件其实都是要被找到映射关系的!


还记得在进程相关内容部分的时候讲过,进程运行的时:

是会维护一个叫**cwd(current work director)**的当前工作区路径!有了这个路径后,假设我们要直接在在进程/代码中使用某个文件hello.txt时,是需要把cwd对应的路径拼接到文件的前面的:cwd对应路径/hello.txt,这样子才能找到对应的文件。
而根目录/,是系统能够找得到的,在系统启动的时候就默认打开的!

其实,上述也就带来了一个结论:
Linux下的所有文件,都是需要从根目录下开始查找,也就是从根目录开始做路径的解析!

所以,在使用相对路径的时候,其实也是一样的:

../../xxx文件的时候,就是把cwd路径拼接到前面,操作系统/文件系统能够识别到..是上一级路径,这样子,就能成功的找到xxx文件了!


但是,上面会有一个问题,即谁提供这个前面的路径呢?从哪来呢?

答案很简单,进程提供的。

因为上层用户对文件的使用,都是通过进程这个载体来是实现的!但是,操作文件IO和文件系统是不可以绕过操作系统直接操作的。

所以进程可以使用接口getcwd实时获取进程对应的当前工作路径,然后把这个路径交给操作系统。操作系统在控制文件系统的时候,如果碰到了要查找文件的情况,会把这个得到的当前路径交给文件系统,做路径的解析。

如果当前工作路径发生更改,那么进程会自动维护,操作系统也会重新进行获取!

路径缓存

至此,我们终于知道了文件系统是如何通过文件名来找到对应的inode的,从而找到关于该文件的所有信息。

但是,如果所有的文件都需要进行从根目录下的路径解析,这是很影响效率的!

因为查找文件,是需要把所有的文件都加载到内存上的!也就是做了大量的IO操作。如果仅仅是为了查找某个文件,就需要耗费这么大的成本来进行查找,这就效率太低了!

所以,Linux对于这种情况,使用了一种方法来进行解决:

即使用缓存方式,把历史搜寻过的文件缓存在内存当中!如果以后再操作某个文件,回去缓存的历史路径去找,如果找到了就直接使用对应的路径!如果找不到,那就重新进行路径解析,成功了就再进行缓存!

那么,这些路径上所有的文件在内存中缓存,也是需要受到操作系统的管理的!

那么,如何描述这些路径上的文件呢?

Linux系统下采用的是一个叫做struct dentry的结构体来描述:

c 复制代码
struct dentry {
	atomic_t d_count;
	unsigned int d_flags;		/* protected by d_lock */
	spinlock_t d_lock;		/* per dentry lock */
	struct inode *d_inode;		/* Where the name belongs to - NULL is negative */
	/*
	 * The next three fields are touched by __d_lookup. Place them here
	 * so they all fit in a cache line.
	 */
	struct hlist_node d_hash;	/* lookup hash list */
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name;
	struct list_head d_lru;		/* LRU list */
	/*
	 * d_child and d_rcu can share memory
	 */
	union {
		struct list_head d_child;	/* child of parent list */
		struct rcu_head d_rcu;
	} d_u;
	struct list_head d_subdirs;	/* our children */
	struct list_head d_alias;	/* inode alias list */
	unsigned long d_time;		/* used by d_revalidate */
	struct dentry_operations *d_op;
	struct super_block *d_sb;	/* The root of the dentry tree */
	void *d_fsdata;			/* fs-specific data */
#ifdef CONFIG_PROFILING
	struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
	int d_mounted;
	unsigned char d_iname[DNAME_INLINE_LEN_MIN];	/* small names */
};

该结构体内存储着指向inode的指针,文件的引用计数等相关信息。其实,在Linux文件系统下,就是通过dentry结构体来进行描述文的。然后通过inode指针就可以在文件系统下找到一切关于该文件的相关信息!

那么,操作系统如何对这些文件结构进行组织呢?我们会发现有三种结构:
d_hash,d_lru,还有一个不太好看的出来:其实是树形结构!


d_hash其实就是一个哈希表,哈希表最大的用处就是用作查找。

即查找某个文件是否存在。

d_lru是一个链表节点,是用来进行缓存操作的。因为内存吃紧的情况下,操作系统是没有办法缓存过多的历史查询路径的,所以会有相应的算法,把缓存路径中最近最少用的给删除。而使用链表进行删除操作是最方便的!

至于树形结构,这就是我们说的Linux系统下的文件存储结构!
------以根目录为根节点的一个文件树!


但是,这里我要说明一点:Linux这个文件树并不是我们想象的那样,一旦启动系统就会存在于磁盘中或者内存中的!这个文件树是动态加载的!

文件都是用结构体dentry来描述的。当系统中操作/查找了某些文件,如果缓存中存在,那么就直接使用缓存中的路径即可。

如果缓存中没有,会进行路径解析!如果解析成功了,又会把路径上所有的文件以dentry的描述方式缓存在内存当中。只不过,在Linux操作系统下,dentry的组织方式有多种,其中一种就是以属性结构进行维护!

所以,Linux的"文件树"在内存中的表现形式(即用户感知的目录层级结构)本质上是通过 dentry cache 和 inode cache 动态构建的逻辑树​​,而非一个预先完整加载的全局数据结构。

挂载分区

我们已经能够根据inode号在指定分区找文件了,也已经能根据目录文件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。可是:

inode编号不能跨区!那如果拿到一个文件名,如何进行查找对应分区呢?


答案是通过挂载分区来实现!

磁盘在进行分区,格式化后,其实还是不能直接使用的!如果想要使用磁盘,那还需要把分区和一个指定的目录进行关联。以后在访问的时候,就是进入这个目录进行访问。进入这个目录,就相当于进入了一个分区!
上面的这种操作,就叫做挂载分区!

文件系统会根据路径 从根目录/开始逐级解析,在每一步检查当前路径是否被挂载到某个分区,优先匹配最长的挂载路径,最终定位到文件所在的物理分区。

接下来,我们来做一个实验看看:

bash 复制代码
$ dd if=/dev/zero of=./disk.img bs=1M count=5 #制作一个大的磁盘块,就当做一个分区
$ mkfs.ext4 disk.img # 格式化写⼊文件系统
$ sudo mkdir /mnt/mydisk # 建⽴空目录
$ df -h # 查看可以使用的分区

//当前可使用分区
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        909M     0  909M   0% /dev
tmpfs           919M     0  919M   0% /dev/shm
tmpfs           919M   97M  822M  11% /run
tmpfs           919M     0  919M   0% /sys/fs/cgroup
/dev/vda1        40G  4.4G   34G  12% /
tmpfs           184M     0  184M   0% /run/user/0
tmpfs           184M     0  184M   0% /run/user/1000
//

$ sudo mount -t ext4 ./disk.img /mnt/mydisk/ # 将分区挂载到指定的⽬录
$ df -h # 查看可以使用的分区

//
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        909M     0  909M   0% /dev
tmpfs           919M     0  919M   0% /dev/shm
tmpfs           919M   97M  822M  11% /run
tmpfs           919M     0  919M   0% /sys/fs/cgroup
/dev/vda1        40G  4.4G   34G  12% /
tmpfs           184M     0  184M   0% /run/user/0
tmpfs           184M     0  184M   0% /run/user/1000
/dev/loop0      3.9M   53K  3.5M   2% /mnt/mydisk
//

$ sudo umount /mnt/mydisk # 卸载分区
$ df -h

//
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        909M     0  909M   0% /dev
tmpfs           919M     0  919M   0% /dev/shm
tmpfs           919M   97M  822M  11% /run
tmpfs           919M     0  919M   0% /sys/fs/cgroup
/dev/vda1        40G  4.4G   34G  12% /
tmpfs           184M     0  184M   0% /run/user/0
tmpfs           184M     0  184M   0% /run/user/1000
//

上面的这个过程,就是在命令行上自行进行磁盘的格式化和挂载分区。我们可以发现,不同的分区可能会出现不同的文件系统。但是都是需要把分区进行挂载到指定目录下的。

至此,当操作系统拿到一个文件名的时候,就会把文件路径交给操作系统,从根目录开始做目录解析。通过检查每一级路径是否挂载来进行锁定分区!

inode和Data Block的映射

在前面的讲解中,我们只输出了一个结论:

即文件系统只要能获得inode,那就能得到文件属性和内容。

文件的属性很好办,就在inode结构体内。文件名也是存放在了当前所在的目录中(文件名和inode编号的映射关系)。但是,我们没有说明,inode是如何找到文件内容的。


其实,在inode结构中,存在着一个指向文件所用数据块的指针数组__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
这个EXT2_N_BLOCKS为15

也就是说,inode可以通过这个数组来找到对应的数据块。但是,这里感觉不太对劲:

因为一个数据块如果是4KB,那么这整个数组如果都指向一个数据块,那么只能表示60KB大小以内的文件啊。所以,内部结构不可能就是简单地指针指向数据块!


其实,这个数组是通过分级指向来进行操作索引数据块的!

上图为inode结构和各级数据块的索引表示意图。

索引表中,有15个指针。其中,十二个指针是直接指向数据块的。也就是直接通过地址索引出对应的数据块,从而寻得内容。

剩余的三个,是间接索引数据块的。

比如,一级简介块索引表指针,其实就是指向了一个数据块。这个数据块不进行内容的存储,而是存储多个指向数据块的指针。假设32位系统下,数据块大小为4KB,那么就可与存储4096 / 4 = 1024个指向数据块的指针。即一个数据块索引表能指向1024个块。

那么,二级间接快索引表指针,其实就是在一级的情况下,套多了一层数据块索引表!那么,指向的数据块就可以变成1024 * 1024个。

以此类推,我们轻松得知:三级间接块索引表指针,可以指向10243个数据块。

这样子一来,一个文件能够存储的内容空间就大的多了,但是文件的存储容量也是有上限的,一般也不会使用这么大的单个文件!

文件系统的总结

最后,我们来对上述讲的文件系统做一个简单地总结。

先来看下面这张图,即操作系统对于系统中文件的管理:

操作文件的人是用户,用户需要通过进程来调用系统接口从而操作文件。

操作系统对于打开的文件是一套管理方案(通过文件描述符索引对应文件,找出读写方法 ),对于磁盘/永久性存储的外设中的文件是使用文件系统来进行管理。但是最后,它们都是文件!

文件都会被一个叫做dentry的方式描述,并且通过一定的形式进行组织!如Linux系统下,会使用哈希表来快速索引,会使用链表来进行缓存的增删。会使用树形结构体现出逻辑结构,方便用户使用!

更加细节的图如上所示。