11.【Linux系统编程】文件系统详解——从磁盘硬件到文件系统
目录
- 1.磁盘理解
- 1.1 磁盘、服务器、机柜、机房
- 1.2 关于磁盘 --- 磁铁(磁盘存储方式)
- 1.3 磁盘物理结构
- 1.4 磁盘的存储结构
- 1.4.1 存储结构
- 1.4.2 存储位置(如何定位扇区)
- 1.5 磁盘的逻辑结构
- 1.5.1 理解 --- 想象磁带存储
- 1.5.2 真实存储过程
- 1.6 CHS地址和LBA地址相互转换
- 2. 文件系统
- 2.1 引入“块”概念
- 2.2 分区
- 2.3 引入"inode"概念
- 2.4 遗留的两个问题
- 3.ext2 文件系统
- 3.1 宏观认识
- 3.2 Block Group
- 3.3 块组内部构成(三级标题可不细看)
- 3.3.1 超级块(Super Block)
- 3.3.2 GDT(Group Descriptor Table)
- 3.3.3 块位图(Block Bitmap)
- 3.3.4 inode位图(Inode Bitmap)
- 3.3.5 i节点表(Inode Table)
- 3.3.6 Data Block
- 3.4 inode和Data block映射(弱化)
- 3.5 目录与文件名
- 3.6 路径解析(文件路径从哪来)
- 3.7 路径缓存(放在内存,以便后续查找)
- 3.8 挂载分区(怎么知道文件在哪一个分区)
- 3.8.1 一个实验:
- 3.8.2 结论
- 3.9 文件系统总结
- 4. 软硬连接
- 4.1 硬链接(文件备份)
- 4.2 软连接(快捷方式)
- 4.3 软硬连接对比
- 4.4 软硬连接的用途:
1.磁盘理解
1.1 磁盘、服务器、机柜、机房
- 特点:机械磁盘是计算机中唯一的一个机械设备
- 角色:磁盘— 外设
- 存储速度:慢
- 优点:容量大,价格便宜
![]() | ![]() | ![]() | ![]() | ![]() |
|---|---|---|---|---|
| 磁盘 | 磁盘 | 服务器 | 机柜 | 机房 |
1.2 关于磁盘 — 磁铁(磁盘存储方式)
磁盘的每个 bit 位是通过磁性材料中磁畴的两种相反磁化方向表示 0 和 1,“磁性正负极” 是对这一原理的通俗简化。
磁存储的核心是磁盘表面的磁性涂层,其内部有无数微小磁畴(类似小磁铁)。未存储数据时磁畴方向杂乱,存储时通过磁头改变特定区域磁畴的整体方向,让它们统一指向 “顺时针 / 逆时针” 或 “左 / 右” 等两种相反方向,这两种方向分别对应二进制的 0 和 1。
1.3 磁盘物理结构

1.4 磁盘的存储结构
1.4.1 存储结构
![]() | ![]() |
|---|---|
| 1.磁道和间隙 | 2.盘面&柱面 |
![]() | ![]() |
| 3.磁头和磁头臂(传动臂) |
解释:
-
磁盘有许多同心圆的磁道,(磁道从盘片外圈往内圈编号0磁道,1磁道…,靠近主轴的同心圆用于停靠磁
头,不存储数据)。磁道由扇区和间隙组成,每个磁道上扇区的数量相同。(传统扇区可以存储512字节数据,512 字节 = 512 × 8 = 4096 位,即理论上需要
4096个磁畴)。 -
磁盘并不是只有一个盘,而是有多个串在同一个轴上的盘片,每个盘片的两面称为盘面,每个盘面都可以存储数据。(见图2)
每个盘面相同垂直位置的磁道构成柱面。
-
磁盘数据的修改,即磁畴磁性的修改是由磁头完成的,磁头由磁头臂和一块永磁铁连接。传动臂上的磁头是共进退的(这点比较重要,后面会说明)。
同样每个盘面都有一个磁头,可以修改盘面的磁畴。
- 磁盘容量 = 磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数
![]() | ![]() |
|---|
1.4.2 存储位置(如何定位扇区)
- 先定位柱面(cylinder)— 摆动磁头臂
- 再确定磁头(header)(不一定每个盘面的相同垂直位置的扇区都要写数据)— 确定磁头
- 定位一个扇区(sector) — 主轴旋转使磁头定位到扇区
以上即为CHS地址定位,摆动磁头臂 —> 确定磁头 —> 旋转主轴定位扇区
对早期的磁盘非常有效,知道用哪个磁头,读取哪个柱面上的第几扇区就可以读到数据了。但是CHS模式支持的硬盘容量有限,因为系统用8bit来存储磁头地址,用10bit来存储柱面地址,用6bit来存储扇区地址,而一个扇区共有512Byte,这样使用CHS寻址一块硬盘最大容量为256 * 1024 * 64 * 512B = 8064 MB(1MB = 1048576B)(若按1MB=1000000B来算就是8.4GB)
1.5 磁盘的逻辑结构
磁盘的存储方式:每次读取一个扇区(512字节)的内容进行修改,修改后再写入对应的扇区
1.5.1 理解 — 想象磁带存储
![]() | ![]() |
|---|
磁带上面可以存储数据,我们可以把磁带“拉直”,形成线性结构

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

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

1.5.2 真实存储过程
记住:传动臂上的磁头是共进退的
柱面是一个逻辑上的概念,其实就是每一面上,相同半径的磁道逻辑上构成柱面。
所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由“柱面”卷起来的。
所以,磁盘的真实情况是:
- 磁道:某一盘面的某一个磁道展开:即:一维数组

-
柱面:整个磁盘所有盘面的同一个磁道,即柱面展开
柱面上的每个磁道,扇区个数是一样的(这不就是二维数组吗)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
-
整盘:
整个磁盘不就是多张二维的扇区数组表(三维数组)
所以,寻址一个扇区:先找到哪一个柱面(Cylinder) ,再确定柱面内哪一个磁道(其实就是磁头位置,Head),在确定扇区(Sector),所以就有了
CHS。我们之前学过C/C++的数组,在我们看来,其实全部都是一维数组:


所以,每一个扇区都有一个下标,我们叫做LBA(Logical Block Address) 地址,其实就是线性地址。
LBA地址怎么获取?由于磁盘中盘片数量、磁道数量、扇区数量都是确定的,计算机很容易计算得到每个扇区的LBA地址。

OS只需要使用LBA就可以了!!LBA地址转成CHS地址,CHS如何转换成为LBA地址。谁做啊??磁盘自己来做!固件(硬件电路,伺服系统)
1.6 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使用磁盘,就可以用一个数字访问磁盘扇区了。
磁盘的存储方式:每次读取一个扇区(512字节)的内容进行修改,修改后再写入对应的扇区
2. 文件系统
2.1 引入“块”概念
其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”(block)。
硬盘的每个分区是被划分为一个个的”块”。一个”块”的大小是由格式化的时候确定的,并且不可以更改,最常见的是4KB,即连续八个扇区组成一个”块”。”块”是文件存取的最小单位。

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

2.2 分区
其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有一块磁盘并且将它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的一种格式化。
但是Linux的设备都是以文件形式存在,那是怎么分区的呢?
柱面是分区的最小单位,我们可以利用参考柱面号码的方式来进行分区,其本质就是设置每个区的起始柱面和结束柱面号码。 此时我们可以将硬盘上的柱面(分区)进行平铺,将其想象成一个大的平面,如下图所示:


柱面大小一致,扇区个位一致,那么其实只要知道每个分区的起始和结束柱面号,知道每一个柱面多少个扇区,那么该分区多大,其实和解释
LBA是多少也就清楚了.
2.3 引入"inode"概念
之前我们说过文件=数据+属性,我们使用 ls -l 的时候看到的除了看到文件名,还能看到文件元数据(属性)。
[gyy@csdn lesson22]$ ll
total 16
-rwxrwxr-x 1 gyy gyy 8360 "Oct 16 11:23" test
-rw-rw-r-- 1 gyy gyy 77 "Oct 16 11:23" test.c
每行包含7列:模式、硬链接数、文件所有者、组、大小、最后修改时间、文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来

- 其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
[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,我们需要是深入了解一下文件系统。
注意:
- Linux下文件的存储是属性和内容分离存储的
- Linux下,保存文件属性的集合叫做
inode,一个文件,一个inode,inode内有一个唯一的标识符,叫做inode号
文件的属性inode结构体:
/*
* 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
再次注意:
- 文件名属性并未纳入到
inode数据结构内部inode的大小一般是128字节或者256,我们后面统一128字节- 任何文件的内容大小可以不同,但是属性大小一定是相同的
2.4 遗留的两个问题
-
我们已经知道硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,读取的基本单位是”块”。“块”又是硬盘的每个分区下的结构,难道“块”是随意的在分区上排布的吗?那要怎么找到“块”呢?
-
还有就是上面提到的存储文件属性的
inode,又是如何放置的呢?文件系统就是为了组织管理这些的!!
3.ext2 文件系统
3.1 宏观认识
所有的准备工作都已经做完,是时候认识下文件系统了。我们想要在硬盘上储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在Linux 系统中,最常见的是ext2 系列的文件系统。其早期版本为 ext2,后来又发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进行了增强,但是其核心设计并没有发生变化,我们仍是以较老的 ext2 作为演示对象。
ext2文件系统将整个分区划分成若干个同样大小的块组 (Block Group),如下图所示。只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。

上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
3.2 Block Group
ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
3.3 块组内部构成(三级标题可不细看)
磁盘→分区→分组,组内成员如下:
- Super Block:描述整个分区的文件系统信息,和当前组的关联不大。个别组内有Super Block,每个Super Block都是相同的,即备份多份以防意外发生。
- GDT(Group Descriptor Table):块组描述符表,存储自己组内成员的描述信息,如在这个块组中从哪里开始是
inode Table,从哪里开始是Data Blocks,空闲的inode和数据块还有多少个等等。 - Block Bitmap:块位图,每个bit位表示1个
Data Block中的数据块是否被占用。 - inode Bitmap:inode位图,每个bit位表示位图中的1个
inode节点是否被占用。(这里的inode节点不是以块为单位的,一个数据块中可以有好多inode节点,节点不想管) - inode Table:存放文件属性,
4KB为单位,每个struct inode是128Byte,则一个数据块会保存32个struct inode。每次访问要读取一个数据块4KB的内容,而看到的只是其中一个。 - Data Blocks:数据块,以
4KB为单位。在组内占据绝对空间。
3.3.1 超级块(Super Block)
存放文件系统本身的结构信息,描述整个分区的文件系统信息,如果Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。记录的信息主要有:
bolck和inode的总量,- 未使用的
block和inode的数量, - 一个
block和inode的大小,最近一次挂载的时间, - 最近一次写入数据的时间,
- 最近一次检验磁盘的时间等其他文件系统的相关信息。
超级块在每个块组的开头都有一份拷贝(第一个块组必须有,后面的块组可以没有)。 为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作,就必须保证文件系统的super block信息在这种情况下也能正常访问。所以一个文件系统的super block会在多个block group中进行备份,这些super block区域的数据保持一致。
/*
* 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和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。
// 磁盘级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];
};
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映射的 -
这样文件=内容+属性,就都能找到了。

/*
* 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 字节
思考:
- 请解释:知道
inode号的情况下,在指定分区怎样确定分组- 请解释:对文件进行增、删、查、改是在做什么?
结论:
- 分区之后的格式化操作,就是对分区进行分组,在每个分组中写入
SB、GDT、Block Bitmap、Inode Bitmap等管理信息,这些管理信息统称: 文件系统- 只要知道文件的
inode号,就能在指定分区中确定是哪一个分组,进而在哪一个分组确定是哪一个inode- 拿到
inode文件属性和内容就全部都有了
举例:下面,通过touch一个新文件来看看如何工作。
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
为了说明问题我们将上图简化:

创建一个新文件主要有以下4个操作:
- 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。- 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。- 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。- 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
3.5 目录与文件名
问题:
- 我们访问文件,都是用的文件名,没用过
inode号啊? - 目录是文件吗?如何理解?
答案:
- 目录也是文件,但是磁盘上没有目录的概念,只有文件属性+文件内容的概念。
- 目录的属性不用多说,内容保存的是:文件名和
inode号的映射关系
// 验证说明代码,课堂不写了,直接复制粘贴即可
// 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 entriesif (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")== 0) {continue;}printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsignedlong)entry->d_ino);}closedir(dir);return 0;
}
whb@bite:~/code/test/test$ ./readdir /
Filename: mnt, Inode: 1048577
Filename: tmp, Inode: 1179650
Filename: sys, Inode: 917506
Filename: libx32, Inode: 17
......
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
......
- 所以,访问文件,必须打开当前目录,根据文件名,获得对应的
inode号,然后进行文件访问 - 所以,访问文件必须要知道当前工作目录,本质是必须能打开当前工作目录文件,查看目录文件的
内容!
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/gyy/code/test/test/test.c都要从根目录开始,依次打开每一个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到test.c。这个过程叫做Linux路径解析。
注意:
• 所以,我们知道了:访问文件必须要有 目录+文件名=路径 的原因
• 根目录固定文件名,inode号,无需查找,系统开机之后就必须知道
可是路径谁提供?
- 进程提供路径:你访问文件,都是指令/工具访问,本质是进程访问,进程有
cwd!进程提供路径。
你open文件,提供了路径可是最开始的路径从哪里来?所以Linux为什么要有根目录, 根目录下为什么要有那么多缺省目录?你为什么要有家目录,你自己可以新建目录?
- 上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系统指定的目录下新建,这不就是天然就有路径了嘛!
- 系统+用户共同构建Linux路径结构.
3.7 路径缓存(放在内存,以便后续查找)
问题1:Linux磁盘中,存在真正的目录吗?
答案:不存在,只有文件。只保存文件属性+文件内容
问题2:访问任何文件,都要从/目录开始进行路径解析?
答案:原则上是,但是这样太慢,所以Linux会缓存历史路径结构
问题3:Linux目录的概念,怎么产生的?
答案:打开的文件是目录的话,由OS自己在内存中进行路径维护
Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
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_PROFILINGstruct dcookie_struct* d_cookie; /* cookie, if any */
#endifint d_mounted;unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
注意:
- 每个文件其实都要有对应的
dentry结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构 - 整个树形节点也同时会隶属于
LRU(Least Recently Used,最近最少使用)结构中,进行节点淘汰 - 整个树形节点也同时会隶属于
Hash,方便快速查找 - 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都先在这棵树下根据路径进行查找,找到就返回属性
inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径

3.8 挂载分区(怎么知道文件在哪一个分区)
我们已经能够根据inode号在指定分区找文件了,也已经能根据目录文件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。可是:
问题:inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪一个分区???
3.8.1 一个实验:
$ 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
注意:
/dev/loop0 在Linux系统中代表第一个循环设备(loop device)。循环设备,也被称为回环设备或者loop back设备,是一种伪设备(pseudo-device),它允许将文件作为块设备(block device)来使用。这种机制使得可以将文件(比如ISO镜像文件)挂载(mount)为文件系统,就像它们是物理硬盘分区或者外部存储设备一样。
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。
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -li abc def
263466 abc
263466 def
abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode263466 的硬连接数为2。- 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
4.2 软连接(快捷方式)
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,但实际上,新的文件和被引用的文件的inode不同,应用常见上可以想象成一个快捷方式。在shell中的做法
[root@localhost linux]# ln -s abc.s abc
[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

acm解释,文件的三个时间:
-
Access 最后访问时间
-
Modify 文件内容最后修改时间
-
Change 属性最后修改时间
4.3 软硬连接对比
-
软连接是独立文件
-
硬链接只是文件名和目标文件
inode的映射关系
4.4 软硬连接的用途:
硬链接
- 文件备份(. 和… 就是硬链接)
软链接
- 类似快捷方式













