Ext系列文件系统(下)

一. ext2⽂件系统

1.1 宏观认识

  • 所有的准备⼯作都已经做完,是时候认识下⽂件系统了。我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。在Linux 系统中,最常⻅的是 ext2 系列的⽂件系统。其早期版本为 ext2,后来⼜发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进⾏了增强,但是其核⼼设计并没有发⽣变化,我们仍是以较⽼的 ext2 作为演⽰对象。
  • ext2⽂件系统将整个分区划分成若⼲个同样⼤⼩的块组 (Block Group),如下图所⽰。只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘⽂件。

上图中启动块(Boot Block/Sector)的⼤⼩是确定的,为1KB,由PC标准规定,⽤来存储磁盘分区信息和启动信息,任何⽂件系统都不能修改启动块。启动块之后才是ext2⽂件系统的开始。

1.2 Block Group

ext2⽂件系统会根据分区的⼤⼩划分为数个Block Group。⽽每个Block Group都有着相同的结构组成。政府管理各区的例⼦。我们接下来只需要理解,并管理好一个组,就可以实现方法的公用。

1.3 块组内部构成

接下来,我将会对块组内部构成部分进行一一讲解。

在我们所有的分组当中,基本单位是块哦,是4KB!!!

1.3.1 Data Block

数据区:存放⽂件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下⼏种情况:

  • 对于普通⽂件,⽂件的数据存储在数据块中。
  • 对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls -l命令看到的其它信息保存在该⽂件的inode中。
  • Block 号按照分区划分,不可跨分区

数据区和数据块:

  • 数据区:数据区是文件系统中用于存储文件实际内容的区域。数据区被划分为固定大小的数据块(通常为 4KB 或 8KB,具体取决于文件系统类型和配置)。

  • 数据块:数据块是数据区的基本单位,用于存储文件的实际数据。每个数据块的大小是固定的,文件系统通过块号来标识和管理这些数据块。

普通文件的数据存储在数据块中。文件的元数据(如文件类型、权限、所有者、大小、时间戳等)存储在 inode 中。inode 中包含指向数据块的指针,通过这些指针,文件系统可以找到存储文件内容的数据块。

目录文件是用于组织文件和子目录的特殊文件。目录文件的内容存储在数据块中,这些内容包括目录下的所有文件名和子目录名,以及每个文件名或子目录名对应的 inode 号。目录文件的元数据(如目录的权限、所有者、大小等)存储在目录文件的 inode 中。当你使用 ls -l 命令时,看到的文件名以外的其他信息(如权限、所有者、大小等)实际上是存储在文件的 inode 中的。

块号是文件系统分配给每个数据块的唯一标识符。块号是按照分区划分的,不可跨分区。每个分区的块号是独立的,不同分区的数据块不会共享块号。

假设你有一个文件系统,数据块大小为 4KB,分区大小为 1GB。文件系统会将这 1GB 的空间划分为 256,000 个数据块(1GB / 4KB = 256,000 个数据块)。每个数据块都有一个唯一的块号,从 0 开始编号。

  • 如果你创建一个大小为 8KB 的普通文件,文件系统会分配两个数据块(块号可能是 123 和 124),并将文件内容存储在这两个数据块中。文件的 inode 会包含指向这两个数据块的指针。

  • 如果你创建一个目录,目录文件的内容(文件名和对应的 inode 号)会存储在数据块中。目录文件的 inode 会包含指向这些数据块的指针。

1.3.2 i节点表(Inode Table)

inode table(inode节点表)是文件系统分配给inode的存储空间。它本身并不直接存储文件属性,而是存储了所有inode的集合。因为在我们的分组当中,所有的东西都是以4KB为单位的数据块,所以inode Table和Data Blocks一样,都是单位为4KB的数据块。

所以我们实际上,在我们磁盘上保存的对应inode,本质就和保存数据来说,是没有区别的,所以创建一个内容为0的文件,是要占磁盘空间的,因为在inode当中还要保存属性,inode到存在inode Table当中。

可是单位是4KB的数据块,我们的inode只有128字节:4096÷128=32;

也就是说,一个数据块,会保存32个inode,即32个文件,但是我们文件系统(filesystem)和磁盘(IO)交互的时候,不是以4KB为单位吗?如果以4KB为单位,那么今天我想打开或者访问一个文件,获得属性的时候,那么我们一定是首先是要一次就把32个inode所对应的数据块(4kb)全部都读取到内存里了吗?

答案是:是的,这也是一种局部性原理的体现,从概率上讲,两个文件的属性在同一数据块,那么对应的两个文件是在短时间内同时创建的,在同数据块的两文件就不需要再加载一次了,这跟我们的1.5倍扩容是同样的作用的。

那我们怎么区分文件是哪一个呢?所以每一个文件的inode都要有自己的inode编号:

我们可以使用:

ls -li

来查看文件对应inode的编号:

我们根目录的inode编号是2,明显是建得比较早得。 

我们inode Table横向的数据是inode结构体当中的内容,是文件内的所有属性集。

现在我们知道:

  • 文件内容保存在Data Blocks;
  • 文件属性保存在inode Table。

可是这样是完全不够的,这个道理就好比一个高校里只有学生的话是不够的,还要又老师,辅导员,校长等等,因为学生还需要管理,所以同样的,除了将文件的内容和属性保存到对应的inode Table和Data Blocks当中,还需要有一些能够管理数据的,讲保存的文件进行管理。

接下来我们来看看inode Bitmap和Block Bitmap:

1.3.3 块位图(Block Bitmap)

我们所有的文件内容全都保存在Data Blocks里,比如说该DataBlocks里面有10万个4KB,将来如果我要新建一个文件的时候,我会在这Data Blocks里面选择没有被占用的若干个数据块给我用,可是这么多数据块里,我怎么知道哪些被用了,哪些没有被用呢?所以我们就有了Block Bitmap。

Block Bitmap中记录着Data Block中哪个数据块已经被占⽤,哪个数据块没有被占⽤。 

假设文件系统将数据区划分为 10 万个 4KB 的数据块。这意味着整个数据区的大小为:

100,000×4KB=400,000KB=400MB

每个数据块对应一个位(bit),因此块位图的大小为:

100,000 bits

由于 1 字节(Byte)= 8 位(bit),因此块位图的大小为:

\frac{100,000 \text{ bits}}{8 \text{ bits/Byte}} = 12,500 \text{ Bytes}

12,500 Bytes=12.5 KB

所以,块位图的大小为 12.5 KB。就是占了4个数据块(4KB为单位)

块位图是一个由 10 万个位 组成的数组,每个位对应一个数据块。

  • 如果块位图的第 12345 位0,表示第 12345 个数据块 是空闲的。

  • 如果块位图的第 12345 位1,表示第 12345 个数据块 已被占用。

通过块位图,文件系统可以快速找到空闲数据块并进行分配,同时也能高效地管理数据块的释放。

所以我们申请一个数据块来保存对应的数据的话,所谓的申请,本质就是对Block Bitmap对应的比特位进行置1!释放一个数据块,我们此时只需要对Block Bitmap对应的比特位进行清0!

1.3.4 inode位图(Inode Bitmap) 

一样的,Data Blocks有10万个数据块,可能有一万个文件,有一万个文件的话,对应就会有一万个inode,有一万个inode,inode Table当中不是所有的inode都是被有效的写入的,有的是没有被使用的,所以这么多的inode里面,那些是被占用,哪些是没有被占用的呢?这时候,我们需要一个数据结构:inode Bitmap。

inode Bitmap中每个bit表⽰⼀个inode是否空闲可⽤。 

所以我们未来申请一个文件,那么要申请inode,还有数据块,其实是通过修改这两个位图。

在日常生活当中,如果我们往电脑上拷贝一部3~4GB的高清电影,我们拷贝的时候可能要花费2,3分钟,但是我们删除的时候,可能1秒就删了。在我们拷贝电影的时候:

  1. 将文件的属性形成,写到对应的inode里;
  2. 要讲内容拷贝到Data Blocks里,是真实的写入3~4GB的数据的。

所以拷贝是比较耗时的。当写入完成的时候,inode Bitmap里只有一个比特位被置1,代表申请的inode,填入属性有效(占用)了;而Block Bitmap当中可能有多个比特位被置1。可是我们在删的时候,我们根本就奴需要把文件的属性和内容做删除,我们只需要把Bitmap位图做清空(置0)就可以了。因为对于我们的磁盘来讲,只要块或者inode Table没有用,那么对应的他就是乱码,还是电影,他是什么都不重要,所以他的数据压根就没有动他只要讲位图清除(置0),管理信息删掉就可以了。(这也就是我们删一个文件这么快的原因,也是为什么我们误删文件,有时还可以恢复的原因:恢复只要将曾经占用的比特位从0置1就行了)

所以在我们文件误删后:

  • 文件恢复的成功率与恢复操作的时机密切相关。越早尝试恢复,文件被覆盖的可能性越小。

  • 建议在发现文件误删后,立即停止对磁盘的写操作,避免新的数据写入覆盖了被删除文件的数据块。

1.3.5 GDT(Group Descriptor Table)

我们现在就存在一个问题:

在我们的整个分组里面,一个组的位置,从哪个位置到那个位置是哪个对应的模块?也就是还有一部分信息通过以上4个模块是无法直接体现出来的,我们就需要一个比较关键的东西:GDT:

块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。

GDT(Group Descriptor Table,块组描述符表)并不是一个块组的内容,而是用于管理整个文件系统中所有块组的元数据的集合。GDT 是一个数组,每个元素是一个块组描述符(ext2_group_desc),它记录了每个块组的详细信息,包括块位图、inode位图、inode表的位置,以及该块组内空闲的块数和inode数。

// 磁盘级块组(Block Group)的数据结构
/*
* 块组描述符结构
*/
struct ext2_group_desc
{
    __le32 bg_block_bitmap;       // 块位图(Block Bitmap)所在的块号
                                  // 该位图记录了本块组内所有数据块的使用情况(0表示空闲,1表示已占用)

    __le32 bg_inode_bitmap;       // inode位图(Inode Bitmap)所在的块号
                                  // 该位图记录了本块组内所有inode的使用情况(0表示空闲,1表示已占用)

    __le32 bg_inode_table;        // inode表(Inode Table)所在的块号
                                  // 该表存储了本块组内所有inode的详细信息

    __le16 bg_free_blocks_count;  // 本块组内空闲的数据块数量
                                  // 用于快速统计和管理空闲块

    __le16 bg_free_inodes_count;  // 本块组内空闲的inode数量
                                  // 用于快速统计和管理空闲inode

    __le16 bg_used_dirs_count;    // 本块组内已使用的目录数量
                                  // 用于统计目录数量,有助于文件系统的管理和优化

    __le16 bg_pad;                // 填充字段,用于对齐
                                  // 确保结构体大小为32字节的倍数,便于访问和存储

    __le32 bg_reserved[3];        // 保留字段,供未来扩展使用
                                  // 目前未使用,但为未来可能的功能扩展预留空间
};
  • 管理块组的元数据:GDT 提供了每个块组的详细信息,使得文件系统可以快速定位和管理块组。

  • 支持文件系统的扩展和可靠性:GDT 的设计使得文件系统可以方便地扩展,同时通过保留的 GDT 块(Reserved GDT Blocks)增强了文件系统的可靠性。

1.3.6 超级块(Super Block)

我们有了上面的5个模式,好像可以将该分组勉勉强强管理起来了,这个Super Block又是干嘛的?

超级块是文件系统的核心元数据结构,它存储了整个文件系统的全局信息,包括文件系统的大小、块大小、inode数量等关键信息。(整个文件系统的全局信息!!!

/*
* Structure of the super block
*/
struct ext2_super_block {
    __le32 s_inodes_count;         // 文件系统中的inode总数
    __le32 s_blocks_count;         // 文件系统中的数据块总数
    __le32 s_r_blocks_count;       // 预留块的数量(通常用于超级用户)
    __le32 s_free_blocks_count;    // 当前空闲的数据块数量
    __le32 s_free_inodes_count;    // 当前空闲的inode数量
    __le32 s_first_data_block;     // 第一个数据块的块号(通常是1)
    __le32 s_log_block_size;       // 数据块大小的对数(2的幂,如10表示1024字节)
    __le32 s_log_frag_size;        // 片段大小的对数(通常与数据块大小相同)
    __le32 s_blocks_per_group;     // 每个块组中的数据块数量
    __le32 s_frags_per_group;      // 每个块组中的片段数量
    __le32 s_inodes_per_group;     // 每个块组中的inode数量
    __le32 s_mtime;                // 文件系统最后一次挂载的时间
    __le32 s_wtime;                // 文件系统最后一次写操作的时间
    __le16 s_mnt_count;            // 文件系统自上次检查以来的挂载次数
    __le16 s_max_mnt_count;        // 文件系统允许的最大挂载次数
    __le16 s_magic;                // 文件系统的魔数(用于标识文件系统类型)
    __le16 s_state;                // 文件系统的状态(如干净或有错误)
    __le16 s_errors;               // 发现错误时的行为(如继续、只读或挂起)
    __le16 s_minor_rev_level;      // 文件系统的次要修订版本
    __le32 s_lastcheck;            // 文件系统最后一次检查的时间
    __le32 s_checkinterval;        // 文件系统两次检查之间的最大时间间隔
    __le32 s_creator_os;           // 创建文件系统的操作系统
    __le32 s_rev_level;            // 文件系统的修订版本
    __le16 s_def_resuid;           // 预留块的默认用户ID
    __le16 s_def_resgid;           // 预留块的默认组ID

    /*
    * These fields are for EXT2_DYNAMIC_REV superblocks only.
    *
    * Note: the difference between the compatible feature set and
    * the incompatible feature set is that if there is a bit set
    * in the incompatible feature set that the kernel doesn't
    * know about, it should refuse to mount the filesystem.
    *
    * e2fsck's requirements are more strict; if it doesn't know
    * about a feature in either the compatible or incompatible
    * feature set, it must abort and not try to meddle with
    * things it doesn't understand...
    */
    __le32 s_first_ino;            // 第一个非保留inode的编号
    __le16 s_inode_size;           // inode结构的大小
    __le16 s_block_group_nr;       // 包含这个超级块的块组编号
    __le32 s_feature_compat;       // 兼容特性集(文件系统支持的特性)
    __le32 s_feature_incompat;     // 不兼容特性集(文件系统支持但可能需要特殊处理的特性)
    __le32 s_feature_ro_compat;    // 只读兼容特性集(文件系统支持但只能以只读方式挂载的特性)
    __u8 s_uuid[16];               // 文件系统的128位UUID
    char s_volume_name[16];        // 文件系统的卷标名称
    char s_last_mounted[64];       // 文件系统最后一次挂载的目录路径
    __le32 s_algorithm_usage_bitmap; // 压缩算法的使用情况

    /*
    * Performance hints. Directory preallocation should only
    * happen if the EXT2_COMPAT_PREALLOC flag is on.
    */
    __u8 s_prealloc_blocks;        // 尝试预分配的块数量
    __u8 s_prealloc_dir_blocks;    // 尝试为目录预分配的块数量
    __u16 s_padding1;              // 填充字段

    /*
    * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
    */
    __u8 s_journal_uuid[16];       // 日志文件的UUID
    __u32 s_journal_inum;          // 日志文件的inode编号
    __u32 s_journal_dev;           // 日志文件的设备编号
    __u32 s_last_orphan;           // 待删除inode链表的起始位置
    __u32 s_hash_seed[4];          // HTREE哈希种子
    __u8 s_def_hash_version;       // 默认哈希版本
    __u8 s_reserved_char_pad;      // 保留的字符填充字段
    __u16 s_reserved_word_pad;     // 保留的字填充字段
    __le32 s_default_mount_opts;   // 默认挂载选项
    __le32 s_first_meta_bg;        // 第一个元数据块组的编号
    __u32 s_reserved[190];         // 保留字段,用于未来的扩展
};

文件系统格式化的过程是将存储设备划分为逻辑结构并初始化元数据的过程。首先,格式化工具会在存储设备的特定位置创建超级块,存储文件系统的全局信息,如总大小、块大小等。接着,它会创建块组描述符表(GDT),记录每个块组的详细信息,包括块位图、inode位图和inode表的位置。然后,存储设备被划分为多个块组,每个块组包含一定数量的数据块和inode。格式化工具会初始化每个块组的块位图和inode位图,将所有位设置为0,表示初始状态下所有资源都是空闲的。同时,为每个块组创建inode表,用于存储inode的详细信息。最后,格式化工具会创建根目录,设置默认配置,并将文件系统标记为干净状态,使其可以被操作系统挂载和使用。

所以格式化的本质是写入文件系统的管理信息!!! 

超级块(Superblock)描述的是整个文件系统的信息,那么为什么不在文件系统的开头单独开辟一段空间来放置超级块呢?是因为不是所有的块组都需要包含超级块吗?可能在10个块组中,只有3个块组包含超级块,而且这些超级块的信息完全一样。这是为什么呢?

我们有时在Windows启动时,如果检测到文件系统可能存在问题,如上次关机异常或文件系统损坏,系统会自动提示“正在检查磁盘”或“正在修复文件系统错误”,并可能显示进度条,表明系统正在对磁盘进行检查和修复操作,这个过程可能会持续一段时间,然后就开机了。

inode的丢失会导致个别文件或目录无法访问,而GDT的丢失会影响块组内的资源管理,最大影响的也是整个块组。但Super Block的丢失通常是最严重的,因为它包含了整个文件系统的全局信息。一旦Super Block出问题,整个文件系统将会挂掉!

所以为了提高可靠性,超级块有多个备份,这些备份存储在文件系统的不同位置。 


超级块:提供文件系统的全局信息,是文件系统管理的顶层结构。存储全局信息,减少了对全局信息的重复查询。

GDT:提供每个块组的详细信息(数组元素:块组描述符),是文件系统管理的中层结构。存储局部信息,使得文件系统可以快速定位和管理每个块组的资源,减少了对整个文件系统的扫描。

块位图和inode位图:提供每个块组内具体的数据块和inode的使用情况,是文件系统管理的底层结构。


inode和数据块都是跨组的编号的;组1【0~100】;组2【100~200】

inode和数据块编号是不能跨分区的:区1【0~1000】;区2【0~1000】

一个分区是一套完整的文件系统。

所以,在同一分区内部,inode编号和块号都是唯一的!!!并且由于每个组的大小等信息都是唯一的,我们可以通过组号和bitmap的信息确定编号:每组【0~1000】【1000~2000】【2000~3000】,那么编号1001就是:

1001/1000=1:组1

1001%1000=1:bitmap第一个比特位

编号的全局带来的是我们文件inode在一个组中,但是数据块可能会跨组存放,因为假设文件内容太大,以至于一个组的所有数据块放不下!!! 


系统操作在开机管理磁盘时,磁盘和操作系统不是脱离的,操作系统需要把磁盘上面的管理信息加载到内存当中,就好比内存中有Super Block对象,将分区上的Super Block内容拷贝进来,磁盘上有10个分区,就将其链接起来(先描述再组织),也将GDT的信息加载进来,再描述再组织。我们的申请,删除文件等操作,全部都必须把Bitmap管理信息加载管理信息到内存,进行比特位置换,等合适的时候再将其刷新到磁盘上:

当操作系统启动时,它需要将磁盘上的管理信息加载到内存中,以便高效地管理和操作文件系统。具体过程如下:

  1. 加载超级块(Super Block)

    • 操作系统首先读取每个分区的超级块,将超级块的内容加载到内存中。超级块包含了文件系统的全局信息,如总块数、inode总数、块大小等。

    • 如果磁盘上有10个分区,操作系统会为每个分区加载一个超级块,并将这些超级块链接起来,形成一个全局的文件系统视图。

  2. 加载块组描述符表(GDT)

    • 在加载超级块之后,操作系统会读取每个分区的块组描述符表(GDT),并将GDT的内容加载到内存中。GDT记录了每个块组的详细信息,如块位图、inode位图和inode表的位置。

    • 操作系统会为每个分区加载一个GDT,并将其组织起来,以便快速访问每个块组的元数据。

  3. 加载位图(Bitmap)

    • 操作系统会将每个块组的块位图和inode位图加载到内存中。这些位图记录了数据块和inode的使用情况。

    • 当用户进行文件操作(如创建、删除文件)时,操作系统会在内存中的位图上进行操作(如设置或清除位),并在合适的时候将这些更改刷新到磁盘上。

  • 文件创建

    • 操作系统在内存中的位图上查找空闲的inode和数据块。

    • 分配inode和数据块后,更新位图,并将文件的元数据写入inode表。

    • 在合适的时候,将位图和inode表的更改刷新到磁盘上。

  • 文件删除

    • 操作系统在内存中的位图上标记inode和数据块为未使用。

    • 更新位图,并在合适的时候将这些更改刷新到磁盘上。

操作系统在启动时将磁盘上的管理信息(如超级块、GDT、位图等)加载到内存中,并在内存中进行高效的管理和操作。当用户进行文件操作时,操作系统会在内存中的位图上进行操作,并在合适的时候将更改刷新到磁盘上,以确保文件系统的完整性和一致性。(所有的操作都是在内存当中进行的,我们之前的文件操作,对文件内容进行读写时,也必须得把文件内容从磁盘加载到内存,在内存中操作完,再写回磁盘)

1.4 inode和datablock映射(弱化)

/*
 * Structure of an inode on the disk
 */
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 */
};

文件系统中的每个文件都有一个唯一的inode,inode存储了文件的属性信息,如文件类型、权限、大小等。inode内部存在一个__le32 i_block[EXT2_N_BLOCKS];数组,其中EXT2_N_BLOCKS = 15,这个数组用来进行inode和数据块的映射,数组内容是该文件所对应的数据块编号。(我们将i_block[]看成Blocks[]:下图)

具体来说,i_block数组的前12个元素直接指向文件的前12个数据块,如果文件较小,这12个数据块足以存储文件内容。(前面12个直接块指针,后面的就是一级/二级/三级间接块索引表指针)如果文件较大,超出这12个直接块的大小,i_block数组的第13个元素会指向一个间接块,这个间接块中包含了更多数据块的指针。如果文件更大,第14个元素会指向一个双重间接块,双重间接块中包含了指向间接块的指针,这些间接块又指向实际的数据块。第15个元素则指向一个三重间接块,三重间接块中包含了指向双重间接块的指针,以此类推,形成了一个多级间接映射结构。

  • 一级间接块索引表指针:指向一个索引表,该表中的每个条目指向文件的一个数据块。这种方式可以管理更多的数据块,因为一个索引表可以包含多个指针。

  • 二级间接块索引表指针:指向一个索引表,该表中的每个条目指向一个一级间接块索引表。这种方式可以管理更多的数据块,因为每个一级间接块索引表可以包含多个一级间接块指针。

  • 三级间接块索引表指针:指向一个索引表,该表中的每个条目指向一个二级间接块索引表。这种方式可以管理极其庞大的文件,因为每个二级间接块索引表可以包含多个一级间接块索引表,从而可以管理大量的数据块。

通过这种映射机制,我们拿着一个文件的inode,就可以通过i_block数组找到对应文件的所有内容。这样,文件的内容和属性就都能找到了,文件系统就能够高效地管理和访问文件数据。

1.5 目录与文件名

但是我们的inode编号是跨组的,所以我们在一个分区中可以确定该文件所对应的组号,但是inode只能在一个分区内有效,那我们是如何确定我们一个文件是在哪一个分区的?

解决这个问题,我们先要扫除下面的障碍:

Linux下如何看待目录?

上篇知道,文件名不会作为属性,保存在文件的inode当中,那保存在了哪里?(我们没有用过inode找文件啊,我们用的都是文件名!!!)

其实目录也是按照上面所说文件的方式进行保存的。也就是目录也有自己的inode:

我们平时用的是文件名,但是我们在默认显示一个文件的时候,有一个潜台词:当前一定处在了某个路径下:

所以我们文件的查询本质是通过路径+文件名。既然目录有inode,那么他指向的对应的内容是什么呢?

目录的inode指向的内容是目录项(Directory Entries),这些目录项记录了目录中每个文件和子目录的名称及其对应的inode编号。具体来说:

每个目录项包含两个主要信息:

  1. 文件名或子目录名:这是目录中一个文件或子目录的名称。

  2. inode编号:这是该文件或子目录的inode编号,通过这个编号可以找到文件或子目录的详细元数据。

所以文件名不作为属性保存在inode当中,而是保存在所属的目录的数据内容当中! 

自此,在磁盘中进行文件保存的时候就没有了目录的概念了,在磁盘当中保存的无非就是inode和数据。 

下面这段代码是一个简单的C程序,用于列出指定目录中的文件和目录,同时打印出每个条目的文件名和inode编号:

// 包含标准输入输出头文件,用于printf和fprintf函数
#include <stdio.h>
// 包含字符串操作头文件,用于strcmp函数
#include <string.h>
// 包含标准库函数头文件,用于exit函数
#include <stdlib.h>
// 包含目录项操作头文件,用于读取目录内容
#include <dirent.h>
// 包含系统数据类型头文件,用于定义文件类型相关的数据类型
#include <sys/types.h>
// 包含Unix标准函数头文件,用于访问目录和文件
#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);
    }

    // 定义一个指向dirent结构的指针,用于存储读取的目录项
    struct dirent *entry;
    // 循环读取目录中的每个条目
    while ((entry = readdir(dir)) != NULL) {
        // 跳过当前目录(".")和父目录("..")的条目
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        // 打印当前条目的文件名和inode编号
        printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
    }

    // 关闭目录流,释放系统资源
    closedir(dir);

    // 程序正常退出,返回0
    return 0;
}

 

1.6 路径解析

问题: 打开当前工作目录文件,查看当前工作目录文件的内容?当前工作目录不也是文件吗?我们访问当前工作目录不也是只知道当前工作目录的文件名吗?要访问它,不也得知道当前工作目录的inode吗?

答案1: 所以也要打开:当前工作目录的上级目录,额....,上级目录不也是目录吗??不还是上面的问题吗?

答案2: 所以类似“递归”,需要把路径中所有的目录全部解析,出口是“/”根目录。

最终答案3: 而实际上,任何文件,都有路径,访问目标文件,比如:/home/whb/code/test/test/test.c,都要从根目录开始,依次打开每一个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到test.c这个过程叫做Linux路径解析。

所以,找到任何Linux文件,都必须从 / 目录开始,进行路径分析,直到找到对应的文件。

💡 注意:

  • 所以,我们知道了:访问文件必须要有目录+文件名=路径的原因。

  • 根目录固定文件名,inode号,无需查找,系统开机之后就必须知道。

可是路径谁提供?

  • 你访问文件,都是指令/工具访问,本质是进程访问,进程有CWD!进程提供路径。

  • 你open文件,提供了路径。

可是最开始的路径从哪里来?

  • 所以Linux为什么要有根目录,根目录下为什么要有那么多缺省目录?

  • 你为什么要有家目录,你自己可以新建目录?

  • 上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系统指定的目录下新建,这不就是天然就有路径了嘛!

  • 系统+用户共同构建Linux路径结构。

1.7路径缓存

访问目标文件,比如:/home/whb/code/test/test/test.c,都要从根目录开始,依次打开每一个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到test.c。(本质就是一直在做磁盘IO,要不断地将文件加载到内存)这个过程叫做Linux路径解析。那我们如果要多次访问code目录下的文件,不就每次都要进行IO操作,这样效率也太低了,Linux是怎么做的?

Linux当中,操作系统在进行路径解析的时候,会把我们历史访问的所有目录(路径)形成一颗多叉树,进行保存!这个多叉树就是Linux系统的树状目录结构:

磁盘当中不存在所谓的目录,目录在磁盘里保存,是统一按照普通文件的方式:inode+数据块统一存的但是在Linux系统中,当我们需要多次访问/home/whb/code目录下的文件时,系统会利用内存中的路径缓存(dentry cache)来优化这个过程,避免每次都进行完整的磁盘IO操作。这个缓存机制实际上是在内存中构建了一个路径的多叉树结构,其中每个节点代表路径中的一个目录。当系统解析路径时,它会首先在内存中搜索这个多叉树,如果找到对应的节点,就直接使用该节点,而不需要重新从磁盘加载信息。这样,随着系统运行,访问过的路径会被缓存起来,形成一个内存级的路径结构,从而提高后续访问相同路径的效率。

我们可以使用:

find ~ -name myshell.cc//这是我的路径下的文件

来找myshell.cc所在的一个或多个路径,第一次可能会慢一点,但是接着就很快了。

问题1:Linux磁盘中,存在真正的⽬录吗?
答案:不存在,只有⽂件。只保存⽂件属性+⽂件内容
问题2:访问任何⽂件,都要从/⽬录开始进⾏路径解析?
答案:原则上是,但是这样太慢,所以Linux会缓存历史路径结构
问题3:Linux⽬录的概念,怎么产⽣的?
答案:打开的⽂件是⽬录的话,由OS⾃⼰在内存中进⾏路径维护
Linux中,在内核中维护树状路径结构的内核结构体(和task_struct一样的内存级结构体)叫做: struct dentry
struct dentry {
    atomic_t d_count;         /* 引用计数,原子操作,用于跟踪引用该dentry的数量 */
    unsigned int d_flags;     /* 保护标志,由d_lock保护 */
    spinlock_t d_lock;        /* 每个dentry的锁,用于同步访问 */

    struct inode *d_inode;    /* 指向inode结构,如果文件存在则非NULL,否则为NULL(负dentry) */

    /* 以下字段由__d_lookup操作,放置在一起以适应缓存行 */
    struct hlist_node d_hash; /* 哈希表节点,用于快速查找 */
    struct dentry *d_parent; /* 父目录的dentry指针 */
    struct qstr d_name;      /* 目录项名称 */

    struct list_head d_lru;   /* LRU列表头,用于缓存管理 */

    /* d_child和d_rcu共享内存 */
    union {
        struct list_head d_child; /* 子目录列表 */
        struct rcu_head d_rcu;    /* RCU头,用于读-复制-更新机制 */
    } d_u;

    struct list_head d_subdirs; /* 子目录列表 */
    struct list_head d_alias;   /* inode别名列表,多个dentry可能指向同一个inode */
    unsigned long d_time;       /* 时间戳,用于d_revalidate函数 */
    struct dentry_operations *d_op; /* 指向dentry操作的函数指针表 */

    struct super_block *d_sb;   /* 指向超级块,表示文件系统的根 */
    void *d_fsdata;             /* 文件系统特定数据 */

#ifdef CONFIG_PROFILING
    struct dcookie_struct *d_cookie; /* 用于性能分析的cookie */
#endif

    int d_mounted;             /* 是否有文件系统挂载到此dentry */
    unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 用于存储短名称的缓冲区 */
};

在 Linux 内核中,dentry(目录项)结构体所形成的结构既包含链表(list)的元素,也具有多叉树(multi-way tree)的特征。这种设计是为了优化路径的查找和缓存,提高文件系统的性能。

  1. 使用多叉树结构存储 dentry 可以高效地进行路径查找,因为它允许快速定位文件系统中的目录项,通过哈希表减少冲突,并迅速访问目录层次结构。

  2. 当内存中缓存的 dentry 数量过多时,LRU(最近最少使用)列表确保了最少使用的 dentry 可以被优先淘汰,从而优化内存使用并保持缓存效率。

dentry 结构与进程控制块(PCB)在某种程度上具有相似性。它们都可以同时作为不同数据结构的一部分,以满足操作系统中不同管理需求:

dentry 结构

  • 作为树状结构的一部分:dentry 通过 d_parentd_subdirs 指针形成树状结构,表示文件系统中的目录层次。

  • 作为链表的一部分:dentry 通过 d_lru 列表头形成链表,用于实现最近最少使用(LRU)缓存策略,以优化内存使用。

进程控制块(PCB)

  • 作为链表的一部分:PCB 通常包含一个链表指针,用于将所有PCB链接起来,形成进程控制块链表,便于操作系统管理所有进程。

  • 作为其他数据结构的一部分:例如,PCB 可能被组织成数组或哈希表的一部分,以支持快速查找特定进程。

这种设计使得 dentry 和 PCB 都可以灵活地参与到不同的数据结构中,以满足文件系统和进程管理的不同需求。通过这种方式,操作系统可以更高效地组织和访问关键数据,提高系统的整体性能和可靠性。

dentry 和 PCB(进程控制块)都是操作系统中重要的数据结构,但它们负责不同的功能和数据管理。dentry 主要用于文件系统中,将文件名映射到文件的具体位置(通常是 inode),并缓存文件路径以加快文件路径的解析速度。而 PCB 用于管理和控制进程,记录进程的所有相关信息。尽管 dentry 和 PCB 看起来相似,因为它们都可以同时存在于不同的数据结构中,但它们之间没有直接的联系。一个 struct dentry 可以属于多叉树,又可以属于某个管理的链表,正如一个进程PCB可以属于全局的链表,也可以属于某个队列中。 

注意以下几点关于Linux文件系统的路径缓存机制:

  • 每个文件,包括普通文件,都对应一个 dentry 结构。这使得所有被打开的文件可以在内存中构建成一个完整的树形结构。

  • 这个树形结构中的每个节点同时也属于LRU(Least Recently Used,最近最少使用)结构,该结构负责管理节点的淘汰,以优化内存使用。

  • 树形结构中的节点同样会加入到哈希表中,这样做可以加快查找速度,使得路径解析更为高效。

  • 关键的是,这个树形结构整体上构成了Linux的路径缓存。当访问任何文件时,系统首先会在这个路径缓存树中根据路径进行查找。如果找到了对应的 dentry,则直接返回文件的属性(通过inode)和内容;如果没有找到,则从磁盘加载相应的路径信息,创建新的 dentry 结构,并将其添加到路径缓存中。

这个机制显著提高了文件访问的速度,因为它减少了需要从磁盘读取数据的次数,并且快速地在内存中定位文件路径。

我们是如何确定我们一个文件是在哪一个分区的?我们最重要的还没解决呢?

1.8 挂载分区

我们已经能够根据inode号在指定分区找⽂件了,也已经能根据⽬录⽂件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。可是:
问题:inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪⼀个分区???

我们的分区,一定是要很一个特定的目录进行关联,往后我们通过进入这个目录就相当于进入这个分区,我们称为挂载:

在计算机系统中,挂载是指将一个文件系统(例如硬盘分区、外部存储设备、网络文件系统等)与一个目录(称为挂载点)关联起来,从而使得用户可以通过访问这个目录来访问存储在该文件系统中的数据。简单来说,挂载就是把一个存储设备的内容“映射”到一个目录上。

在计算机系统中,分区是磁盘上划分的独立存储区域,用于组织和管理数据。然而,这些分区本身是作为块设备存在的,例如 /dev/sda1/dev/sda2 等,它们本身并不是可以直接访问的目录路径。我们无法直接像访问普通文件夹那样进入这些分区,因为它们是磁盘的物理划分,而不是文件系统中的目录结构。

为了能够方便地访问这些分区中的数据,我们需要将它们与一个特定的目录路径关联起来,这个过程就被称为挂载(Mounting)。通过挂载,我们可以将一个分区映射到一个目录(称为挂载点),这样我们就可以通过访问这个目录来间接访问分区中的内容。例如,我们可以将分区 /dev/sda1 挂载到 /mnt/mydisk,之后通过 cd /mnt/mydisk 命令进入该目录,就可以像操作普通文件夹一样访问分区中的文件和数据了。

如果系统中有多个分区(比如10个分区),我们可以通过挂载操作将每个分区分别关联到不同的目录路径上。这样,每个分区都有一个对应的挂载点目录,用户可以通过这些目录方便地访问各个分区中的数据,而无需记住复杂的设备文件路径。挂载操作不仅使得分区的访问更加直观和方便,还能够通过挂载点目录来统一管理不同分区的文件系统,提高数据访问的灵活性和安全性。

由于有点抽象,我们来通过一个实验理解:

这段操作展示了如何在Linux系统中创建一个虚拟磁盘分区、格式化它、挂载到一个目录,并进行访问,最后卸载。以下是对每个步骤的详细解释:

1. 创建虚拟磁盘文件

dd if=/dev/zero of=./disk.img bs=1M count=5
  • dd:这是一个用于数据复制的工具,可以将数据从一个文件复制到另一个文件。

  • if=/dev/zero:指定输入文件为/dev/zero,这是一个特殊的设备文件,会输出无限的零(0)。

  • of=./disk.img:指定输出文件为当前目录下的disk.img,这个文件将被创建并写入数据。

  • bs=1M:指定每次读写的数据块大小为1MB。

  • count=5:指定总共写入5个数据块。

结果:创建了一个大小为5MB的文件disk.img,内容全部是零。这个文件可以被当作一个虚拟的磁盘分区来使用。

2. 格式化虚拟磁盘

mkfs.ext4 disk.img
  • mkfs.ext4:这是一个用于创建ext4文件系统的工具。

  • disk.img:指定要格式化的文件(虚拟磁盘)。

结果:将disk.img格式化为一个ext4文件系统。现在,这个文件可以被挂载并作为文件系统使用。

3. 创建挂载点目录

mkdir dir
  • mkdir:创建一个新目录。

  • dir:指定要创建的目录路径。

结果:在当前目录下创建了一个名为dir的空目录,这个目录将用作挂载点。(我们已经创建)

4. 查看当前挂载的文件系统

df -h
  • df -h:显示文件系统的磁盘使用情况,-h选项表示以易读的格式显示。

结果:列出当前系统中所有挂载的文件系统及其使用情况。此时,disk.img还没有挂载,所以不会出现在列表中。

5. 挂载虚拟磁盘

sudo mount -t ext4 ./disk.img /mnt/mydisk/
  • sudo:以超级用户权限执行命令。

  • mount:挂载文件系统。

  • -t ext4:指定文件系统的类型为ext4。

  • ./disk.img:指定要挂载的文件(虚拟磁盘)。

  • ./dir:指定挂载点目录。

结果:将disk.img挂载到./dir目录。现在,可以通过访问./dir来访问disk.img中的内容。

6. 再次查看挂载的文件系统

df -h

结果:此时,disk.img已经挂载到./dir,会在df -h的输出中显示为/dev/loop4(Linux使用循环设备来挂载文件作为块设备)。可以看到它的大小、已用空间、可用空间等信息。

7. 卸载虚拟磁盘

sudo umount ./dir
  • sudo:以超级用户权限执行命令。

  • umount:卸载文件系统。

  • ./dir:指定要卸载的挂载点目录。

结果:将disk.img./dir卸载。卸载后,./dir目录仍然存在,但不再与disk.img关联。

8. 再次查看挂载的文件系统

df -h

 

结果disk.img已经从挂载列表中移除,/mnt/mydisk目录不再显示为挂载点。

通过上述步骤,我们完成了一个虚拟磁盘分区的创建、格式化、挂载和卸载。这个过程模拟了真实磁盘分区的管理操作,展示了如何在Linux系统中灵活地使用文件系统。

我们就可以解决上面遗留的问题:

但是我们的inode编号是跨组的,所以我们在一个分区中可以确定该文件所对应的组号,但是inode只能在一个分区内有效,那我们是如何确定我们一个文件是在哪一个分区的?

答:在Linux系统中,虽然inode编号在不同分区之间是独立的,但文件系统通过挂载点的目录结构来确定文件属于哪个分区。当访问一个文件时,系统会根据文件的路径,从根目录开始逐级查找,直到找到其所在的挂载点,从而确定该文件属于哪个分区。

假设你有以下挂载情况:

  • /dev/sda1挂载到/(根目录)

  • /dev/sdb1挂载到/mnt/data

  • /dev/sdc1挂载到/home/user

当你访问文件/home/user/documents/file.txt时:

  • 系统从根目录/开始。

  • 找到/home,再找到/home/user,这里/home/user是一个挂载点,关联到分区/dev/sdc1

  • 在分区/dev/sdc1中查找documents/file.txt

所以我们在进行fopen()函数调用的时候,我们打开文件的时候没有指定路径,操作系统会为我们添加对应的CWD,所以当我们在fopen()的时候,操作系统在内核当中就要帮我们根据路径找到这个文件,如果需要,就要查找struct dentry,帮我们进行路径搜索,把所有的对应文件上的节点全部打开,找到对应的文件名和inode的映射关系。

1.9 文件系统总结

这些图描述了在Linux操作系统中,文件从进程打开到挂载在文件系统上的整个流程。首先,每个进程(如进程A和进程B)都有一个task_struct结构,其中包含一个files_struct结构,用于跟踪该进程打开的所有文件。每个打开的文件由file结构表示,该结构包含文件描述符、文件状态信息以及指向inodedentry的指针。

inode结构代表硬盘文件系统上的索引节点,包含文件的元数据,如权限、所有者、大小和时间戳等。dentry(目录项)结构代表文件系统中的文件名和它对应的inode的关联。dentry通过其d_inode字段与inode关联。

文件路径由path结构表示,它包含指向dentryvfsmount(虚拟文件系统挂载)的指针。vfsmount结构表示一个挂载的文件系统,包含指向其super_block的指针。super_block结构代表文件系统的超级块,包含文件系统类型和状态等信息。

当一个文件被打开时,系统会根据文件路径找到对应的dentryinode,然后创建或使用现有的file结构来表示该文件的打开状态。file结构中的f_op字段指向一组文件操作函数,这些函数定义了如何读写文件等。

文件系统类型由file_system_type结构定义,它包含文件系统的名称和各种操作函数,如挂载(mount)、卸载(kill_sb)等。super_operations结构定义了超级块的操作,如分配和销毁inode、同步文件系统等。

总的来说,这些图展示了Linux内核如何通过一系列的数据结构和操作函数来管理文件、目录项、索引节点和文件系统,从而实现文件的打开、读写和挂载等操作。

二.软硬链接

2.1 软链接

软链接(Symbolic Link),也称为符号链接,是Unix和类Unix操作系统(如Linux和macOS)中的一种特殊类型的文件,它提供了对另一个文件或目录的引用。软链接类似于Windows中的快捷方式。

假设你在Windows中有以下文件结构:

C:\Projects\
├── ProjectA\
│   └── source\
│       └── main.cpp
└── ProjectB\
    └── docs\
        └── readme.md

现在,你想在ProjectB/docs目录下创建一个指向ProjectA/source/main.cpp的快捷方式,以便快速访问这个文件。在Windows中,你可以这样做:

  1. 右键点击ProjectA/source/main.cpp文件。

  2. 选择“创建快捷方式”。

  3. 将快捷方式拖动到ProjectB/docs目录下。

现在,在ProjectB/docs目录下会有一个指向ProjectA/source/main.cpp的快捷方式。你可以双击这个快捷方式来打开main.cpp文件。

对于Linux,我们先来实验一下:

在Linux中,可以使用ln -s命令创建软链接。命令格式如下:

ln -s <目标文件或目录> <软链接名称>

例如,创建一个指向code.c文件的软链接code-soft

ln -s code.c code-soft
lfz@HUAWEI:~/lesson/lesson21$ ll
total 12
drwxrwxr-x  2 lfz lfz 4096 Feb 10 16:43 ./
drwxrwxr-x 19 lfz lfz 4096 Feb  9 23:15 ../
-rw-rw-r--  1 lfz lfz    0 Feb  9 23:15 code.c
-rw-rw-r--  1 lfz lfz 1781 Feb 10 16:42 readdir.c
lfz@HUAWEI:~/lesson/lesson21$ ln -s code.c code-soft
lfz@HUAWEI:~/lesson/lesson21$ ls -l
total 4
-rw-rw-r-- 1 lfz lfz    0 Feb  9 23:15 code.c
lrwxrwxrwx 1 lfz lfz    6 Feb 10 16:44 code-soft -> code.c
-rw-rw-r-- 1 lfz lfz 1781 Feb 10 16:42 readdir.c
lfz@HUAWEI:~/lesson/lesson21$ echo "hello code.c" >> code.c
lfz@HUAWEI:~/lesson/lesson21$ cat code-soft
hello code.c

我们看到:

cat code-soft命令读取并显示软链接code-soft指向的文件内容。由于code-soft是指向code.c的软链接,所以显示的是code.c的内容,包括刚刚追加的"hello code.c"字符串。

我们看到软链接有自己的inode,所以软链接是一个独立的文件,因为他有独立的inode number!!!还有以 l 开头,说明是链接文件。

既是文件,那就离不开文件的内容和属性,还有,软链接能干吗?

其实和Windows下的快捷操作一样,就是为了方便:

所以我们软链接文件内容就是保存目标文件的路径!!!

我们可以通过 unlink 软链接文件 或者 rm 软连接文件 进行删除。

2.2 硬链接

硬链接(Hard Link)是Unix和类Unix操作系统中文件系统中的一种文件类型,它提供了一种将多个文件名指向同一数据的方法。 

在Linux中,可以使用ln命令创建硬链接,命令格式如下:

ln <目标文件> <硬链接名称>

例如,创建一个名为code-hard的硬链接,指向code.c

ln code.c code-hard

我们发现code-hard对应的inode和code.c对应的inode的编号竟然是一样的,所以硬链接本质不是一个独立的文件,因为他没有独立的inode,那是什么?

我们来看看软硬连接后的变化:

所以他本质就是一组新的文件名和目标文件inode number的映射关系! 

这个2就相当于多了一个新的文件名指向目标文件,这个2就是硬链接数。有两个文件名指向相同的inode,因为我们inode结构体中,存在一个引用计数的概念:

那这玩意儿有什么用?

第一,假设你有一个重要的配置文件,你希望在不占用额外磁盘空间的情况下创建一个备份。你可以创建一个硬链接到该文件:

ln /etc/config.txt /etc/config_backup.txt

这样,config_backup.txt就是config.txt的硬链接,它们指向相同的inode和数据块。如果config.txt被意外修改或删除,你仍然可以通过config_backup.txt访问原始数据。所以可以达到文件备份,避免数据丢失的目的!!!

第二,我们新建目录的时候,硬链接数是2,因为我们cd目录,发现还会有 "."文件:

所以我们"."文件就是一个硬链接,我们"."就表示当前目录!!!

第三,我们在dir目录下再创建一个hello目录,发现dir的硬链接数多了1,因为hello目录里有一个".."文件,表示上级目录:

所以我们".."文件就是一个硬链接,我们".."就表示上级目录!!!

我们可以用硬链接数-2得到该目录下有几个目录!!!

2.3 软硬链接对比

硬链接(Hard Link)和软链接(Symbolic Link)是Linux系统中用于创建文件或目录引用的两种不同机制。下面是一个详细的对比:

创建方式:

硬链接

ln 源文件 硬链接文件

使用ln命令创建,不带-s选项。

软链接:

ln -s 源文件 软链接文件

使用ln命令创建,带-s选项。

存储结构:

硬链接:硬链接实际上是文件系统中的另一个名字,指向相同的inode。不占用额外的磁盘空间,因为它们只是文件系统中的另一个名字。

软链接:软链接是一个独立的文件,包含对目标文件路径的引用。占用少量的磁盘空间来存储目标文件的路径。

文件删除:

硬链接:删除硬链接不会影响原始文件,只有当所有指向同一inode的硬链接都被删除后,文件数据才会被删除。

软链接:删除软链接不会影响它所指向的原始文件,因为软链接本身是一个独立的文件。

文件系统限制:

硬链接:不能跨文件系统创建。不能指向目录(因为可能导致目录结构的循环引用)。

软链接:可以跨文件系统创建。可以指向目录。

下面详细解释为什么硬链接不能跨文件系统创建以及为什么不能指向目录:

1. 不能跨文件系统创建

硬链接直接指向文件系统中的inode(索引节点),inode包含了文件的元数据和数据块的位置信息。不同文件系统有不同的inode结构和inode号空间:

  • 不同的inode号空间:每个文件系统维护自己的inode号空间,这意味着一个文件系统中的inode号在另一个文件系统中可能是无效的或指向不同的inode。

  • 文件系统独立性:文件系统设计为独立的数据管理单元,跨文件系统创建硬链接会破坏这种独立性,可能导致数据不一致和管理上的复杂性。

2. 不能指向目录(避免目录结构的循环引用)

目录在文件系统中是特殊的文件,它们包含文件和子目录的名称和inode号的映射。如果允许硬链接指向目录,可能会引发以下问题:

  • 循环引用:假设目录A有一个硬链接B,B又有一个硬链接指向A,这样就形成了一个循环引用。在遍历目录时,系统会不断在A和B之间跳转,导致无限循环,无法正常访问文件系统。

  • 目录树结构破坏:文件系统的目录结构是一个树形结构,硬链接目录可能导致目录树结构的逻辑混乱,使得文件系统的遍历和管理变得复杂。

  • 一致性问题:目录的硬链接可能导致目录内容的一致性问题。例如,如果通过一个硬链接删除目录,可能会导致其他硬链接指向的目录内容丢失或不一致。

假设有以下目录结构:

/a
└── /b

如果允许硬链接目录,可能会创建如下结构:

/a -> /b
/b -> /a

现在,如果尝试遍历目录/a,系统会进入无限循环:/a -> /b -> /a -> /b -> ...,这会导致系统无法正常工作。

硬链接的限制主要是为了维护文件系统的稳定性和一致性,避免产生复杂的文件系统结构问题。相比之下,软链接通过存储目标文件的路径来避免这些问题,因此可以跨文件系统创建,也可以指向目录。

那我们上面的.和..的本质是对目录的硬链接,这不就是“只许州官放火,不许百姓点灯”吗?!

你别说,就是这么双标!!!

更新和修改:

硬链接:对任何一个硬链接的修改都会反映到所有其他硬链接上,因为它们指向的是同一个inode。

软链接:指向的文件被修改时,软链接本身不受影响,它仍然指向原始文件。

访问权限:

硬链接:不能改变,因为它们是同一个文件的不同名字。

软链接:可以有自己的访问权限,这可能会影响用户能否通过软链接访问目标文件。

系统资源:

硬链接:增加硬链接会增加文件系统中的inode使用,但不会增加磁盘空间的使用。

软链接:虽然占用的磁盘空间很小,但每个软链接都是文件系统中的一个独立实体。 

硬链接和软链接各有优势和适用场景。硬链接适用于需要多个名称指向同一数据的场景,而软链接提供了一种灵活的方式来引用文件,特别是在需要跨文件系统链接或创建目录链接时。然而,由于硬链接的限制(如不能跨文件系统创建、不能链接目录等),在某些场景下可能需要使用软链接作为替代方案。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值