目录
- [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-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 字节说明。
- 任何文件的内容大小可以不同,但是属性大小一定是相同的。
到目前为止,相信大家还有两个问题:
在进入文件系统之前,还需要回答两个问题:
- 硬盘是典型的块设备,操作系统读取硬盘数据的基本单位是"块"。块位于分区中,它们如何排布,又该怎样被找到?
- 保存文件属性的 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 个操作:
-
存储属性
内核先找到一个空闲的 inode(这里是 263466),把文件信息记录到其中。
-
存储数据
假设该文件需要三个磁盘块,内核找到三个空闲块 300、500、800,将内核缓冲区中的数据依次复制到这些块中。
-
记录分配情况
文件内容按 300、500、800 的顺序存放,内核在 inode 的磁盘分布区中记录这组块号。
-
把文件名添加到目录
新文件名为
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。要得到它,似乎又要先打开上级目录;上级目录本身也是目录,这个过程怎样终止?
- 可以继续打开当前工作目录的上级目录,但上级目录仍然会遇到同样的问题。
- 因此需要像递归一样解析路径中的所有目录,递归出口是根目录
/。 - 实际上任何文件都有路径。访问目标文件,例如
/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 软硬连接的用途
硬链接:
.和..是硬链接。- 可用于文件备份。
软链接:
- 类似快捷方式。