【Linux篇】- Ext系列文件系统
📌 个人主页: 孙同学_
🔧 文章专栏:Liunx
💡 关注我,分享经验,助你少走弯路!
文章目录
- 1. 磁盘
- 1.1 理解硬件 - 磁盘
- 1.2 磁盘的物理结构
- 1.3 磁盘的存储结构
- 1.4 磁盘的逻辑结构
- 1.4.1 理解过程
- 1.4.2 真实过程
- 1.5 CHS && LBA 地址
- 2. 引入文件系统
- 2.1 引入“块”概念
- 2.2 引入“分区”概念
- 2.3 引入“inode”概念
- 3. ext2文件系统
- 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.3.7 Inode和DataBlock的映射
- 3.4 目录与文件名
- 3.5 路径解析
- 3.6 路径缓存
- 3.7 挂载分区
- 3.8 文件系统总结
1. 磁盘
1.1 理解硬件 - 磁盘
我们这里所说的磁盘并非我们现在电脑里的SSD(固态硬盘),磁盘在划分上可以分为企业级磁盘和用户级磁盘。磁盘是既能被读取,也能被写入的。
1.2 磁盘的物理结构
1.3 磁盘的存储结构
扇区是磁盘存储数据的基本单位,扇区的大小通常是512
字节
![]() | ![]() |
![]() | ![]() |
如何定位一个扇区呢?
答案是先定位磁头(header)确定磁头要访问哪一个柱面(磁道)(cylinder)再定位一个扇区(sector)这叫做CHS地址定位。
- 扇区: 扇区是从磁盘读取和写入的基本单位,大小是
512
字节 - 磁头(head)数: 每个盘片都有上下两面,每面都有一个磁头,一个盘面共两个磁头
- 磁道(track)数: 磁道是从磁盘的外圈向内编号,0磁道,1磁道…,靠近主轴的同心圆用于停靠磁头,不存储数据。
- 柱面(cylinder)数: 磁道构成柱面,数量上等于磁道数
- 扇区(sector)数: 每个磁道被分成很多个扇形区域,每个磁道的扇形区域数量相同。
- 圆盘(platter)数: 就是盘片的数量
- 磁盘容量 = 磁头数 × 磁道数/柱面数 × 每道扇区数 × 每扇区的字节数
- 细节: 传动臂上面的磁头是共进退的
柱面(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位(寻址)方式之一,CHS寻址方式。
📌CHS寻址
对早期的磁盘非常有效,知道用哪个磁头,读取哪个柱面上的第几扇区就可以读到数据了。但是CHS模式支持的硬盘容量有限,因为系统用8bit来存储磁头地址,用10bit来存储柱面地址,用6bit来存储扇区地址,而⼀个扇区共有512Byte,这样使用CHS寻址⼀块硬盘最大容量为256 * 1024 * 63 * 512B = 8064 MB(1MB = 1048576B)(若按1MB=1000000B来算就是8.4GB)
1.4 磁盘的逻辑结构
1.4.1 理解过程
上图所述的物件大家肯定都见过吧!磁带上面可以存储数据,我们可以把磁带“拉直”,形成线性结构
那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在一起的磁带,那么磁盘的逻辑存储结构我们也可以类似于:
这样的话每一个扇区,就有了一个线性地址(本质上就是数组下标),这种地址叫做LBA
1.4.2 真实过程
细节: 传动臂上面的磁头是共进退的
柱面其实是每一个面上,相同半径的磁道逻辑上构成柱面。所以磁盘物理上分了很多面,但在我们看来,逻辑上,磁盘整体是由“柱面”卷起来的。
所以,磁盘的真实情况是:
磁道:
某一盘面的某一磁道展开:
即:一维数组
柱面:
整个磁盘的所有盘面的同一磁道,即柱面展开:
- 柱面上的每个磁道,扇区个数都是一样的,这不就是二维数组吗?
整盘:
所以整个磁盘不就是多张的二维数组吗?(整体不就是三维数组)
所以,寻址一个扇区:先找到哪一个柱面(Cylinder),在确定柱面内的哪一个磁道(其实就是磁头的位置,Head),再确定扇区(Sector),所以就有了CHS
所以,每一个扇区都有一个下标,我们叫做LBA
(Logical Block Address)地址,其实就是线性地址。所以怎么计算得到这个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)。
硬盘的每一个分区是被划分成一个一个的“块”。一个“块”的大小是由格式化的时候确定的,并且不可以更改,最常见的是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”概念
我们一直在说文件 = 文件内容 + 文件属性
文件是存储在“块”中的,那么我们还必须找到一个地方存储文件的元信息(属性信息),比如文件的创建者,文件的创建时间,文件大小等等。这种存储文件元信息的区域叫做inode
(索引结点)。
每个文件都有一个inode,里面包含了与该文件有关的信息。
注意:
- Linux下的文件存储是属性和 内容分离存储的
Liunx下,保存文件属性的集合叫做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)
注意:
- 文件名属性并未纳入到inode数据结构内部
- 任何文件的内容大小可以不同,但是属性大小一定是相同的
3. ext2文件系统
我们想在硬盘上面存储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬件中的文件。
ext2
文件系统将整个分区划分为若干个同样大小的快组(Block Group),如下图所示。只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。
启动块(Boot Block/Sector)的大小是确定的,为1kb。用来存储磁盘分区信息和启动信息,任何文件系统都崩修改启动块。启动块之后才是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信息在这种情况下还能继续访问。所以一个文件系统的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.3.7 Inode和DataBlock的映射
下面我们通过touch一个新的文件来看看如何工作
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
创建一个新文件分为4步:
- 存储属性
内核先找到一个空闲节点inode(这里是263466)。内核把文件信息记录在其中。 - 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,一次类推。 - 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录
新的文件名abc。内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件的内容和属性连接起来。
3.4 目录与文件名
访问文件,必须先打开当前目录,根据文件名,获取对应的Inode号,然后进行文件访问。
目录的内容:inode与文件名的映射关系
3.5 路径解析
访问任何文件或者目录都要从根目录开始,一次打开每一个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到我们要访问的目录或者目标文件。这个过程叫做linux路径解析。
注意:
所以访问文件必须要有 目录 + 文件名 = 路径
根目录的文件名和inode号都是固定的,无需查找,系统开机就知道
路径是由谁提供的?
我们访问文件,都是指令/工具访问,本质上都是进程访问文件,进程有CWD
(当前工作目录),所以进程提供路径
我们在新创建目录文件,都是在我们或者系统指定的目录下创建的,这不就自然而然的形成路径了吗?所以系统+用户共同构建Linux文件结构。
3.6 路径缓存
- Linux磁盘中不存在真正的目录,只有文件。只保存文件属性+文件内容
- 原则上访问任何目录都是从根目录开始的,但是这样访问文件太慢了,为了提高访问效率,Linux会缓存历史结构路径。
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.7 挂载分区
到目前为止我们已经能够根据inode找到指定分区的文件,也能根据目录文件内容,找到指定的inode了。
我们分区写入文件系统无法直接使用,需要和指定的目录关联,挂载才能使用。
所以可以根据访问目标文件的路径前缀准确判断我在哪一个分区。
3.8 文件系统总结
👍 如果对你有帮助,欢迎:
- 点赞 ⭐️
- 收藏 📌
- 关注 🔔