第6讲:Ext 系列文件系统

目录

  • [1. 理解硬件](#1. 理解硬件)
    • [1-1 磁盘、服务器、机柜、机房](#1-1 磁盘、服务器、机柜、机房)
    • [1-2 磁盘物理结构](#1-2 磁盘物理结构)
    • [1-3 磁盘的存储结构](#1-3 磁盘的存储结构)
    • [1-4 磁盘的逻辑结构](#1-4 磁盘的逻辑结构)
    • [1-5 CHS & LBA 地址](#1-5 CHS & LBA 地址)
  • [2. 引入文件系统](#2. 引入文件系统)
    • [2-1 引入"块"概念](#2-1 引入“块”概念)
    • [2-2 引入"分区"概念](#2-2 引入“分区”概念)
    • [2-3 引入"inode"概念](#2-3 引入“inode”概念)
  • [3. ext2 文件系统](#3. ext2 文件系统)
    • [3-1 宏观认识](#3-1 宏观认识)
    • [3-2 Block Group](#3-2 Block Group)
    • [3-3 块组内部构成](#3-3 块组内部构成)
    • [3-4 inode 和 data block 映射(弱化)](#3-4 inode 和 data block 映射(弱化))
    • [3-5 目录与文件名](#3-5 目录与文件名)
    • [3-6 路径解析](#3-6 路径解析)
    • [3-7 路径缓存](#3-7 路径缓存)
    • [3-8 挂载分区](#3-8 挂载分区)
    • [3-9 文件系统总结](#3-9 文件系统总结)
  • [4. 软硬连接](#4. 软硬连接)
    • [4-1 硬链接](#4-1 硬链接)
    • [4-2 软链接](#4-2 软链接)
    • [4-3 软硬连接对比](#4-3 软硬连接对比)
    • [4-4 软硬连接的用途](#4-4 软硬连接的用途)

本节重点

  • 理解磁盘物理结构
  • 掌握 CHS 和 LBA 地址
  • 掌握 Ext 系列文件系统原理
  • 理解分区、格式化、路径解析、挂载等过程和操作
  • 理解软硬连接的使用和用途

1. 理解硬件

1-1 磁盘、服务器、机柜、机房

  • 机械磁盘是计算机中唯一的一个机械设备
  • 磁盘 - 外设
  • 慢
  • 容量大,价格便宜

题外话:

  1. 关于机房
  2. 关于磁盘 - 磁铁

1-2 磁盘物理结构

1-3 磁盘的存储结构

扇区:是磁盘存储数据的基本单位,512 字节,块设备。

如何定位一个扇区呢?

  • 可以先定位磁头(head)
  • 确定磁头要访问哪一个柱面(磁道)(cylinder)
  • 定位一个扇区(sector)
  • CHS 地址定位

文件 = 内容 + 属性,都是数据,无非就是占据哪几个扇区的问题!能定位一个扇区了,能不能定位多个扇区呢?

  • 扇区是从磁盘读出和写入信息的最小单位,通常大小为 512 字节。
  • 磁头(head)数:每个盘片一般有上下两面,分别对应 1 个磁头,共 2 个磁头。
  • 磁道(track)数:磁道是从盘片外圈往内圈编号 0 磁道、1 磁道......靠近主轴的同心圆用于停靠磁头,不存储数据。
  • 柱面(cylinder)数:磁道构成柱面,数量上等同于磁道个数。
  • 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同。
  • 圆盘(platter)数:就是盘片的数量。
  • 磁盘容量 = 磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数。
  • 细节:传动臂上的磁头是共进退的(这点比较重要,后面会说明)。

柱面(cylinder)、磁头(head)、扇区(sector),显然可以定位数据了,这就是数据定位(寻址)方式之一:CHS 寻址方式。

!warning CHS 寻址

对早期的磁盘非常有效,知道用哪个磁头,读取哪个柱面上的第几扇区就可以读到数据了。

但是 CHS 模式支持的硬盘容量有限,因为系统用 8 bit 来存储磁头地址,用 10 bit 来存储柱面地址,用 6 bit 来存储扇区地址,而一个扇区共有 512 Byte。这样,使用 CHS 寻址的一块硬盘最大容量为 256 * 1024 * 63 * 512 B = 8064 MB(1 MB = 1048576 B);若按 1 MB = 1000000 B 计算,就是 8.4 GB。

1-4 磁盘的逻辑结构

1-4-1 理解过程

磁带上面可以存储数据,我们可以把磁带"拉直",形成线性结构。

那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在一起的磁带,那么磁盘的逻辑存储结构也可以类似于:

这样每一个扇区就有了一个线性地址(其实就是数组下标),这种地址叫做 LBA。

1-4-2 真实过程

一个细节:传动臂上的磁头是共进退的。

柱面是一个逻辑上的概念,其实就是每一面上,相同半径的磁道在逻辑上构成柱面。

所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由"柱面"卷起来的。

所以,磁盘的真实情况是:

磁道:

某一盘面的某一个磁道展开:

即:一维数组。

柱面:

整个磁盘所有盘面的同一个磁道,即柱面展开:

  • 柱面上的每个磁道,扇区个数是一样的。
  • 这不就是二维数组吗?

整盘:

整个磁盘不就是多张二维的扇区数组表(三维数组?)

所以,寻址一个扇区:先找到哪一个柱面(Cylinder),再确定柱面内哪一个磁道(其实就是磁头位置,Head),再确定扇区(Sector),所以就有了 CHS。

我们之前学过 C/C++ 的数组,在我们看来,其实全部都是一维数组:

所以,每一个扇区都有一个下标,我们叫做 LBA(Logical Block Address)地址,其实就是线性地址。所以,怎么计算得到这个 LBA 地址呢?

LBA,1000,CHS 必须要!!LBA 地址转成 CHS 地址,CHS 如何转换成为 LBA 地址。

OS 只需要使用 LBA 就可以了!!LBA 地址转成 CHS 地址、CHS 如何转换成为 LBA 地址,谁来做?磁盘自己来做!由固件(硬件电路、伺服系统)完成。

1-5 CHS && LBA 地址

CHS 转成 LBA:

  • 磁头数 * 每磁道扇区数 = 单个柱面的扇区总数
  • LBA = 柱面号 C * 单个柱面的扇区总数 + 磁头号 H * 每磁道扇区数 + 扇区号 S - 1
  • 即:LBA = 柱面号 C * (磁头数 * 每磁道扇区数) + 磁头号 H * 每磁道扇区数 + 扇区号 S - 1
  • 扇区号通常是从 1 开始的,而在 LBA 中,地址是从 0 开始的。
  • 柱面和磁道都是从 0 开始编号的。
  • 总柱面、磁道个数、扇区总数等信息,在磁盘内部会自动维护,上层开机的时候会获取到这些参数。

LBA 转成 CHS:

  • 柱面号 C = LBA // (磁头数 * 每磁道扇区数)(除数就是单个柱面的扇区总数)
  • 磁头号 H = (LBA % (磁头数 * 每磁道扇区数)) // 每磁道扇区数
  • 扇区号 S = (LBA % 每磁道扇区数) + 1
  • // 表示整除。

所以,从此往后,在磁盘使用者看来,根本就不关心 CHS 地址,而是直接使用 LBA 地址,由磁盘内部自己转换。

从现在开始,磁盘就是一个元素为扇区的一维数组,数组的下标就是每一个扇区的 LBA 地址。OS 使用磁盘,就可以用一个数字访问磁盘扇区了。

2. 引入文件系统

2-1 引入"块"概念

其实硬盘是典型的"块"设备。操作系统读取硬盘数据的时候,其实不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。

硬盘的每个分区被划分为一个个的"块"。一个"块"的大小由格式化的时候确定,并且不可以更改,最常见的是 4 KB,即连续八个扇区组成一个"块"。"块"是文件存取的最小单位。

!warning 注意

  • 磁盘就是一个三维数组,我们把它看待成一个"一维数组",数组下标就是 LBA,每个元素都是扇区。
  • 每个扇区都有 LBA,那么 8 个扇区一个块,每一个块的地址我们也能算出来。
  • 知道 LBA:块号 = LBA / 8。
  • 知道块号:LBA = 块号 * 8 + n(n 是块内第几个扇区)。

2-2 引入"分区"概念

磁盘可以被划分成多个分区(partition)。以 Windows 为例,一块磁盘可能被划分为 C、D、E 盘,这些盘符分别对应不同的分区。分区是在磁盘上划定逻辑区域,格式化则是在分区中写入文件系统结构。Linux 的设备以文件形式存在,那么它是怎样进行分区的呢?

柱面是分区的最小单位,我们可以利用参考柱面号码的方式来进行分区,其本质就是设置每个区的起始柱面和结束柱面号码。此时我们可以将硬盘上的柱面(分区)进行平铺,将其想象成一个大的平面,如下图所示:

!warning 注意

柱面大小一致,扇区个数一致,那么其实只要知道每个分区的起始和结束柱面号,知道每一个柱面有多少个扇区,该分区多大,以及 LBA 是多少,也就清楚了。

2-3 引入"inode"概念

之前我们说过,文件 = 数据 + 属性 。我们使用 ls -l 的时候,除了看到文件名,还能看到文件元数据(属性)。

text 复制代码
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月  13 14:56" a.out
-rw-r--r--. 1 root root  654 "9月  13 14:56" test.c

每行包含 7 列:

  • 模式
  • 硬链接数
  • 文件所有者
  • 组
  • 大小
  • 最后修改时间
  • 文件名

ls -l 读取存储在磁盘上的文件信息,然后显示出来。

其实这个信息除了通过这种方式来读取,还有一个 stat 命令能够看到更多信息:

text 复制代码
[root@localhost linux]# stat test.c
  File: "test.c"
  Size: 654               Blocks: 8          IO Block: 4096   普通文件
Device: 802h/2050d        Inode: 263715      Links: 1
Access: (0644/-rw-r--r--) Uid: (    0/    root) Gid: (    0/    root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

到这里我们要思考一个问题:文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息(属性信息),比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做 inode,中文译名为"索引节点"。

每一个文件都有对应的 inode,里面包含了与该文件有关的一些信息。为了能解释清楚 inode,我们需要深入了解一下文件系统。

!warning 注意

  • Linux 下文件的属性和内容是分离存储的。
  • Linux 下,保存文件属性的集合叫做 inode。一个文件对应一个 inode,inode 内有一个唯一的标识符,叫做 inode 号。

所以,一个文件的属性 inode 长什么样子呢?

c 复制代码
/*
 * 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 */
};

/*
 * 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。

!warning 再次注意

  • 文件名属性并未纳入 inode 数据结构内部。
  • inode 的大小一般是 128 字节或者 256 字节,我们后面统一按 128 字节说明。
  • 任何文件的内容大小可以不同,但是属性大小一定是相同的。

到目前为止,相信大家还有两个问题:

在进入文件系统之前,还需要回答两个问题:

  1. 硬盘是典型的块设备,操作系统读取硬盘数据的基本单位是"块"。块位于分区中,它们如何排布,又该怎样被找到?
  2. 保存文件属性的 inode 又是如何放置和定位的?

文件系统就是用来组织和管理这些内容的。

3. ext2 文件系统

3-1 宏观认识

所有的准备工作都已经做完,是时候认识文件系统了。想要在硬盘上存储文件,必须先把硬盘格式化为某种文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。

在 Linux 系统中,常见的是 ext 系列文件系统。其早期版本为 ext2,后来又发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进行了增强,但是核心设计并没有发生变化,这里仍以较老的 ext2 作为演示对象。

ext2 文件系统将整个分区划分成若干个同样大小的块组(Block Group)。只要能管理一个块组,就能用相同方式管理所有块组,进而管理整个分区中的文件。

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

3-2 Block Group

ext2 文件系统会根据分区大小划分出若干个 Block Group,每个 Block Group 都由相同的结构组成。

3-3 块组内部构成

3-3-1 超级块(Super Block)

超级块存放文件系统本身的结构信息,用来描述整个分区的文件系统。记录的信息主要包括:block 和 inode 的总量、未使用的 block 和 inode 数量、block 和 inode 的大小、最近一次挂载时间、最近一次写入数据的时间、最近一次检查磁盘的时间,以及其他与文件系统有关的信息。Super Block 中的信息一旦被破坏,整个文件系统的结构都可能随之损坏。

超级块在每个块组的开头可以有一份副本,第一个块组必须有,后面的块组可以没有。为了让文件系统在磁盘局部发生物理故障时仍能工作,Super Block 会在多个 Block Group 中备份,并保持这些副本的数据一致。

c 复制代码
/*
 * Structure of the super block
 */
struct ext2_super_block {
    __le32 s_inodes_count;          /* Inodes count */
    __le32 s_blocks_count;          /* Blocks count */
    __le32 s_r_blocks_count;        /* Reserved blocks count */
    __le32 s_free_blocks_count;     /* Free blocks count */
    __le32 s_free_inodes_count;     /* Free inodes count */
    __le32 s_first_data_block;      /* First Data Block */
    __le32 s_log_block_size;        /* Block size */
    __le32 s_log_frag_size;         /* Fragment size */
    __le32 s_blocks_per_group;      /* # Blocks per group */
    __le32 s_frags_per_group;       /* # Fragments per group */
    __le32 s_inodes_per_group;      /* # Inodes per group */
    __le32 s_mtime;                 /* Mount time */
    __le32 s_wtime;                 /* Write time */
    __le16 s_mnt_count;             /* Mount count */
    __le16 s_max_mnt_count;         /* Maximal mount count */
    __le16 s_magic;                 /* Magic signature */
    __le16 s_state;                 /* File system state */
    __le16 s_errors;                /* Behaviour when detecting errors */
    __le16 s_minor_rev_level;       /* minor revision level */
    __le32 s_lastcheck;             /* time of last check */
    __le32 s_checkinterval;         /* max. time between checks */
    __le32 s_creator_os;            /* OS */
    __le32 s_rev_level;             /* Revision level */
    __le16 s_def_resuid;            /* Default uid for reserved blocks */
    __le16 s_def_resgid;            /* Default gid for reserved blocks */

    /*
     * 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;                 /* First non-reserved inode */
    __le16 s_inode_size;                /* size of inode structure */
    __le16 s_block_group_nr;            /* block group # of this superblock */
    __le32 s_feature_compat;            /* compatible feature set */
    __le32 s_feature_incompat;          /* incompatible feature set */
    __le32 s_feature_ro_compat;         /* readonly-compatible feature set */
    __u8   s_uuid[16];                  /* 128-bit uuid for volume */
    char   s_volume_name[16];           /* volume name */
    char   s_last_mounted[64];          /* directory where last mounted */
    __le32 s_algorithm_usage_bitmap;    /* For compression */

    /*
     * Performance hints. Directory preallocation should only
     * happen if the EXT2_COMPAT_PREALLOC flag is on.
     */
    __u8  s_prealloc_blocks;            /* Nr of blocks to try to preallocate */
    __u8  s_prealloc_dir_blocks;        /* Nr to preallocate for dirs */
    __u16 s_padding1;

    /*
     * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
     */
    __u8  s_journal_uuid[16];           /* uuid of journal superblock */
    __u32 s_journal_inum;               /* inode number of journal file */
    __u32 s_journal_dev;                /* device number of journal file */
    __u32 s_last_orphan;                /* start of list of inodes to delete */
    __u32 s_hash_seed[4];               /* HTREE hash seed */
    __u8  s_def_hash_version;           /* Default hash version to use */
    __u8  s_reserved_char_pad;
    __u16 s_reserved_word_pad;
    __le32 s_default_mount_opts;
    __le32 s_first_meta_bg;             /* First metablock block group */
    __u32 s_reserved[190];              /* Padding to the end of the block */
};
3-3-2 GDT(Group Descriptor Table)

块组描述符表用于描述块组的属性信息。整个分区被分成多少个块组,就对应有多少个块组描述符。每个块组描述符保存一个块组的描述信息,例如该块组中的 Inode Table 和 Data Blocks 从哪里开始,以及还剩多少空闲 inode 和数据块等。块组描述符在每个块组的开头都有一份副本。

c 复制代码
// 磁盘级 block group 的数据结构
/*
 * 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];
};
3-3-3 块位图(Block Bitmap)
  • Block Bitmap 记录 Data Block 中哪些数据块已经被占用,哪些数据块尚未被占用。
3-3-4 inode 位图(Inode Bitmap)
  • 每个 bit 表示一个 inode 是否空闲可用。
3-3-5 i 节点表(Inode Table)
  • 存放文件属性,例如文件大小、所有者和最近修改时间等。
  • 它是当前块组中所有 inode 属性的集合。
  • inode 编号以分区为单位整体划分,不能跨分区。
3-3-6 Data Block

数据区用于存放文件内容,也就是一个个 Block。根据文件类型不同,有以下几种情况:

  • 对于普通文件,文件数据存储在数据块中。
  • 对于目录,其数据块保存该目录下的文件名和目录名。除文件名外,ls -l 看到的其他信息保存在对应文件的 inode 中。
  • Block 号按分区划分,不能跨分区。

3-4 inode 和 data block 映射(弱化)

  • inode 内部有 __le32 i_block[EXT2_N_BLOCKS]; /* Pointers to blocks */。
  • EXT2_N_BLOCKS = 15,这组指针用于建立 inode 与 block 的映射。
  • 这样,文件的属性和内容就都能找到。
c 复制代码
/*
 * 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 */
};

#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)

// inode 的大小通常是 128 字节或 256 字节

!question 思考

已知 inode 号并指定分区时,对文件进行增、删、查、改,实际是在操作哪些文件系统结构?
!tip 结论

  • 分区后的格式化操作,就是对分区进行分组,并在每个分组中写入 Super Block、GDT、Block Bitmap、Inode Bitmap 等管理信息。这些管理信息统称为文件系统。
  • 只要知道文件的 inode 号,就能在指定分区中确定它属于哪个块组,并进一步在该块组中定位对应的 inode。
  • 取得 inode 后,就能继续找到文件的属性和内容。

下面通过 touch 一个新文件来观察工作过程:

text 复制代码
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc

为了说明问题,将相关结构简化如下:

创建一个新文件主要有以下 4 个操作:

  1. 存储属性

    内核先找到一个空闲的 inode(这里是 263466),把文件信息记录到其中。

  2. 存储数据

    假设该文件需要三个磁盘块,内核找到三个空闲块 300、500、800,将内核缓冲区中的数据依次复制到这些块中。

  3. 记录分配情况

    文件内容按 300、500、800 的顺序存放,内核在 inode 的磁盘分布区中记录这组块号。

  4. 把文件名添加到目录

    新文件名为 abc。Linux 如何在当前目录中记录这个文件?内核把目录项 (263466, abc) 添加到目录文件中。文件名与 inode 之间的对应关系把文件名同文件内容、属性连接起来。

3-5 目录与文件名

问题:

  • 访问文件时使用的是文件名,而不是 inode 号,这是为什么?
  • 目录是文件吗?应如何理解?

答案:

  • 目录也是文件。磁盘上没有独立的"目录"概念,仍然只有文件属性和文件内容。
  • 目录文件的属性与普通文件类似;它的内容保存文件名和 inode 号的映射关系。

下面的示例读取一个目录,并打印其中每个目录项的文件名和 inode 号:

c 复制代码
// 验证说明代码,课堂不写,直接复制粘贴即可
// readdir.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;
}

运行结果及与 ls -li / 的对照:

text 复制代码
whb@bite:~/code/test/test$ ./readdir /
Filename: mnt, Inode: 1048577
Filename: tmp, Inode: 1179650
Filename: sys, Inode: 917506
Filename: libx32, Inode: 17
Filename: srv, Inode: 786434
Filename: lib64, Inode: 16
Filename: sbin, Inode: 18
Filename: dev, Inode: 131073
Filename: swapfile, Inode: 12
Filename: run, Inode: 1048578
Filename: log.txt, Inode: 20
Filename: proc, Inode: 1179649
Filename: lost+found, Inode: 11
Filename: etc, Inode: 262145
Filename: lib, Inode: 14
Filename: opt, Inode: 917505
Filename: usr, Inode: 1179651
Filename: lib32, Inode: 15
Filename: boot, Inode: 655361
Filename: var, Inode: 655365
Filename: product-service-1.0-SNAPSHOT.jar, Inode: 19
Filename: bin, Inode: 13
Filename: media, Inode: 393217
Filename: home, Inode: 786433
Filename: root, Inode: 655362

whb@bite:~/code/test/test$ ls -li /
total 1014436
     13 lrwxrwxrwx   1 root root         7 Sep 14  2020 bin -> usr/bin
 655361 drwxr-xr-x   3 root root      4096 May  6 14:34 boot
      2 drwxr-xr-x  17 root root      3880 Jul 17 10:39 dev
 262145 drwxr-xr-x  98 root root      4096 Oct 27 14:56 etc
 786433 drwxr-xr-x   6 root root      4096 Sep  4 14:56 home
     14 lrwxrwxrwx   1 root root         7 Sep 14  2020 lib -> usr/lib
     15 lrwxrwxrwx   1 root root         9 Sep 14  2020 lib32 -> usr/lib32
     16 lrwxrwxrwx   1 root root         9 Sep 14  2020 lib64 -> usr/lib64
     17 lrwxrwxrwx   1 root root        10 Sep 14  2020 libx32 -> usr/libx32
     20 -rw-r--r--   1 root root       461 Jul 16 15:51 log.txt
     11 drwx------   2 root root     16384 Sep 14  2020 lost+found
 393217 drwxr-xr-x   4 root root      4096 Sep 14  2020 media
1048577 drwxr-xr-x   3 root root      4096 Oct 17 17:47 mnt
 917505 drwxr-xr-x   3 root root      4096 May  6 14:37 opt
      1 dr-xr-xr-x 169 root root         0 Jul 17 10:26 proc
     19 -rw-r--r--   1 root root  45457485 Feb  3  2024 product-service-1.0-SNAPSHOT.jar
 655362 drwx------  13 xjh  xjh       4096 Oct 27 09:36 root
      2 drwxr-xr-x  25 root root       780 Oct 28 20:36 run
     18 lrwxrwxrwx   1 root root         8 Sep 14  2020 sbin -> usr/sbin
 786434 drwxr-xr-x   2 root root      4096 Apr 23  2020 srv
     12 -rw-------   1 root root 993249280 Sep 14  2020 swapfile
      1 dr-xr-xr-x  13 root root         0 Jul 17 18:26 sys
1179650 drwxrwxrwt  20 root root      4096 Oct 28 20:39 tmp
1179651 drwxr-xr-x  14 root root      4096 Nov 10  2023 usr
 655365 drwxr-xr-x  12 root root      4096 Jun 16 16:40 var
  • 访问文件时,必须先打开当前目录,根据文件名取得对应的 inode 号,然后才能访问文件。
  • 因此,访问文件必须知道当前工作目录;其本质是能够打开当前工作目录文件,并查看目录文件的内容。
text 复制代码
whb@bite:~/code/test/test$ pwd
/home/whb/code/test/test
whb@bite:~/code/test/test$ ls -li
total 24
1596260 -rw-rw-r-- 1 whb whb 814 Oct 28 20:32 test.c

例如,要访问 test.c,必须先打开 test(当前工作目录),从目录内容中取得 test.c 对应的 inode,再访问该文件。

3-6 路径解析

问题: 要打开当前工作目录文件并查看其内容,仍然需要知道当前工作目录的 inode。要得到它,似乎又要先打开上级目录;上级目录本身也是目录,这个过程怎样终止?

  1. 可以继续打开当前工作目录的上级目录,但上级目录仍然会遇到同样的问题。
  2. 因此需要像递归一样解析路径中的所有目录,递归出口是根目录 /。
  3. 实际上任何文件都有路径。访问目标文件,例如 /home/whb/code/test/test/test.c,都要从根目录开始,依次打开路径上的每个目录,根据目录名访问其下的下一级目录,直到定位 test.c。这个过程叫作 Linux 路径解析。

!warning 注意

  • 这也说明了访问文件必须提供"目录 + 文件名",即路径的原因。
  • 根目录的文件名和 inode 号固定,无需查找;系统启动后必须掌握这些信息。

路径由谁提供?

  • 指令或工具访问文件,本质上是进程访问文件;进程有 CWD(当前工作目录),可以据此提供路径。
  • 调用 open 时会提供路径。

最初的路径从哪里来?

  • Linux 有根目录,根目录下有一组默认目录。
  • 用户有家目录,也可以创建新目录。
  • 这些行为本质上是在磁盘文件系统中创建目录文件。任何新文件都会在用户或系统指定的目录下创建,因此天然就有路径。
  • 系统和用户共同构建 Linux 的路径结构。

3-7 路径缓存

问题 1:Linux 磁盘中存在真正的目录吗?

不存在。磁盘上只有文件,保存的是文件属性和文件内容。

问题 2:访问任何文件都要从 / 目录开始路径解析吗?

原则上是,但这样太慢,所以 Linux 会缓存历史路径结构。

问题 3: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 */
};

!warning 注意

  • 每个文件,包括普通文件,都有对应的 dentry 结构。所有已经打开的文件可以在内存中形成一棵树。
  • 树中的节点同时属于 LRU(Least Recently Used,最近最少使用)结构,以便淘汰节点。
  • 节点还会加入哈希结构,便于快速查找。
  • 这棵树整体构成 Linux 的路径缓存。访问文件时,内核先按路径在树中查找;找到后返回 inode 和内容,找不到则从磁盘加载路径并添加新的 dentry 结构。

3-8 挂载分区

已经能够根据 inode 号在指定分区中查找文件,也能够根据目录文件内容查找指定 inode。但 inode 不能跨分区,Linux 又可以有多个分区,因此还需要判断正在访问哪一个分区。

3-8-1 一个实验
text 复制代码
$ dd if=/dev/zero of=./disk.img bs=1M count=5   # 制作一个大的磁盘块,视为一个分区
$ mkfs.ext4 disk.img                            # 格式化并写入文件系统
$ mkdir /mnt/mydisk                             # 建立空目录
$ df -h                                         # 查看可用分区
Filesystem      Size  Used Avail Use% Mounted on
udev            956M     0  956M   0% /dev
tmpfs           198M  724K  197M   1% /run
/dev/vda1        50G   20G   28G  42% /
tmpfs           986M     0  986M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           986M     0  986M   0% /sys/fs/cgroup
tmpfs           198M     0  198M   0% /run/user/0
tmpfs           198M     0  198M   0% /run/user/1002

$ sudo mount -t ext4 ./disk.img /mnt/mydisk/    # 将分区挂载到指定目录
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            956M     0  956M   0% /dev
tmpfs           198M  724K  197M   1% /run
/dev/vda1        50G   20G   28G  42% /
tmpfs           986M     0  986M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           986M     0  986M   0% /sys/fs/cgroup
tmpfs           198M     0  198M   0% /run/user/0
tmpfs           198M     0  198M   0% /run/user/1002
/dev/loop0      4.9M   24K  4.5M   1% /mnt/mydisk

$ sudo umount /mnt/mydisk                       # 卸载分区
whb@bite:/mnt$ df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            956M     0  956M   0% /dev
tmpfs           198M  724K  197M   1% /run
/dev/vda1        50G   20G   28G  42% /
tmpfs           986M     0  986M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           986M     0  986M   0% /sys/fs/cgroup
tmpfs           198M     0  198M   0% /run/user/0
tmpfs           198M     0  198M   0% /run/user/1002

!warning 注意

/dev/loop0 在 Linux 中代表第一个循环设备(loop device)。循环设备也称回环设备,是一种伪设备(pseudo-device),允许把普通文件作为块设备(block device)使用。这种机制可以把 ISO 镜像等文件挂载为文件系统,就像使用物理硬盘分区或外部存储设备一样。

text 复制代码
whb@bite:/mnt$ ls /dev/loop* -l
brw-rw---- 1 root disk  7,   0 Oct 17 18:24 /dev/loop0
brw-rw---- 1 root disk  7,   1 Jul 17 10:26 /dev/loop1
brw-rw---- 1 root disk  7,   2 Jul 17 10:26 /dev/loop2
brw-rw---- 1 root disk  7,   3 Jul 17 10:26 /dev/loop3
brw-rw---- 1 root disk  7,   4 Jul 17 10:26 /dev/loop4
brw-rw---- 1 root disk  7,   5 Jul 17 10:26 /dev/loop5
brw-rw---- 1 root disk  7,   6 Jul 17 10:26 /dev/loop6
brw-rw---- 1 root disk  7,   7 Jul 17 10:26 /dev/loop7
crw-rw---- 1 root disk 10, 237 Jul 17 10:26 /dev/loop-control
3-8-2 一个结论
  • 分区写入文件系统后不能直接使用,需要与指定目录关联并挂载后才能使用。
  • 可以根据目标文件的路径前缀判断它位于哪个分区。理解到这一层即可。

3-9 文件系统总结

下面用几张图从不同角度总结文件系统:

4. 软硬连接

4-1 硬链接

真正定位磁盘上文件的并不是文件名,而是 inode。在 Linux 中,可以让多个文件名对应同一个 inode。

text 复制代码
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -li abc def
263466 abc
263466 def
  • abc 和 def 的链接状态完全相同,它们称为指向同一文件的硬链接。内核记录链接数,此时 inode 263466 的硬链接数为 2。
  • 删除文件名时会做两件事:先删除目录中的对应记录,再把硬链接数减 1。只有硬链接数减为 0 时,才释放对应的磁盘空间。

4-2 软链接

硬链接通过 inode 引用同一个文件;软链接则通过名字引用另一个文件。软链接是一个独立的新文件,与目标文件的 inode 不同,使用上可以把它理解为快捷方式。

text 复制代码
[root@localhost linux]# ln -s abc abc.s
[root@localhost linux]# ls -li
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 def

文件的三个时间:

  • Access:最后访问时间。
  • Modify:文件内容最后修改时间。
  • Change:文件属性最后修改时间。

4-3 软硬连接对比

  • 软链接是独立文件。
  • 硬链接只是文件名与目标文件 inode 之间的映射关系。

4-4 软硬连接的用途

硬链接:

  • . 和 .. 是硬链接。
  • 可用于文件备份。

软链接:

  • 类似快捷方式。
相关推荐
爱吃香菜的初学者3 小时前
十九.Linux——TCP协议(1)
linux·网络·tcp/ip
Zevalin爱灰灰3 小时前
Linux入门第二部分(ARM裸机篇) 第三章 汇编LED驱动实验
linux·汇编·arm开发
EVEN-09133 小时前
Linux-IPC进程间通信
linux·运维·服务器
多弗朗皮卡丘3 小时前
Linux 进程信号
linux·运维·服务器
^乘风破浪^3 小时前
Kali配置i3wm教程1
linux·kali·i3wm
qeen873 小时前
【Linux】动静态库的链接与加载过程
linux·运维·系统编程
重生之小比特4 小时前
【Linux系统】17 库制作与原理(上)
linux·运维·服务器
wzg20164 小时前
ubuntu 离线安装 docker-compose
linux·ubuntu·docker
行走的陀螺仪4 小时前
Claude Code 从入门到实战(三):Hooks、Skills、SubAgents 高级扩展
linux·编辑器·vim