轻松Linux-7.Ext系列文件系统

发布于:2025-09-08 ⋅ 阅读:(33) ⋅ 点赞:(0)

天朗气清,惠风和煦,今日无事,遂来更新。


1.概述

总所周知,我们存的数据都是在一个叫硬盘的东西里面,这个硬盘又像个黑盒,这章就来简单解析一下Linux中文件系统。

现在我们用的大都是固态硬盘,除了服务器级别的主机在用机械硬盘,已经很少有人用机械硬盘啦,我们今天就要从机械硬盘讲起。因为Linux中的Ext3、Ext4基本是Ext2文件系统的增强版,所以本章以Ext2文件系统为例。

硬盘里有多层的磁盘和多个磁头,早期的磁盘使用的是CHS(磁道(柱面)、磁头、扇面)定位寻址,而系统需要将CHS地址转换为LBA(Local Block Adress)地址来使用,硬盘就可以看作是一个三维数组,CHS转LBA,就可以看作是将三维数组转化为一维数组

系统读取硬盘的基本单位一般是“块(block)”,块是又n个扇区组成的,系统可以给硬盘分区,分区的最小单位是柱面,柱面内包含各个扇区,即包含了各个块。操作系统为了管理这些分区,引入Ext文件系统,Ext文件系统将各个分区以组为单位分为了n个块组(Block Group),每个块组内记录着本区域内的各种数据,其中就包括了每个存储在此区域文件的inode编号,通过inode以及其中各种位图就可以找到存储文件的属性以及数据,其中包括有各种目录与文件

系统启动时,会自动挂载各个已经预先准备好的分区,只有挂载后分区才可用

Linux中的目录也是文件,属性(元数据)与一般文件区别不大,数据存储的是目录名与目录下存储的各种文件的inode的映射关系,可以将目录看作一个一维数组,里面可能存有别的目录也可能是文件。

访问文件时,其实是打开当前工作目录通过文件名找到对应的inode,去查找文件内容,因此系统要解析文件路径,通过在内核中维护的树状路径结构的结构体struct dentry查找文件,如果找到了就会返回文件的属性inode和内容,如果没找到就会将文件的struct dentry添加到内核缓冲中(每个文件都有对应的dentry结构)

Linux中还有软链接和硬链接,软链接有点像Windows中的快捷访问方式,硬链接相当备份,原理是引用计数。


2.硬盘与CHS和LBA

这里用一张简单的硬盘剖面图来展开:

【计算机组成原理】快到碗里来,轻松图解硬盘结构~_磁盘结构-CSDN博客

硬盘内有磁头(Head)、磁道(track)、扇区(sector),硬盘通过先定位磁头,再确认访问某个磁道(柱面),最后定位扇区来进行定位。扇区从磁盘中读出和写入信息的最小单位一般是512字节。

我们知道文件 = 属性 + 内容,对于硬盘而言就是多定位几个扇区的事。

硬盘的各种属性:

磁头数:一般每个磁盘的上下两面都会有一个磁头。(传动臂上的磁头是共进退的)

磁道数:从0开始编号,从外往内计数,最靠进轴心和最靠外用于停放磁头的磁道不用作存储数据。

柱面数:由同一柱面上的磁道构成,数量上与一个磁盘盘面的磁道数相同。

扇区数:每个磁道被切分为多个扇区,每个磁道的扇区相同。

磁盘数:盘数。

磁盘容量 = 磁头数 x 磁道(柱面)数 x 每磁道扇区数 x 每扇区字节数。

 硬盘是:由多个柱面构成

2.1CHS与LBA的转换

所以整张硬盘就像是一个三维数组(蛋卷-不是),在寻找某个扇区时,就要先寻找某个柱面,再确定在哪个磁道(其实是磁头),最后再确定扇区,由此CHS寻址就产生了。

我们在C/C++中学过数组,二维数组的下标通过算法转换其实是可以转换成一维数组的,所以CHS转换成LBA其实同理,Belike:

这样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。

2.2有关硬盘的各个属性

2.2.1“块”

操作系统在读取硬盘(典型的“块”设备)数据的时候,并不会一个一个扇区的读取,而是一次读取多个扇区,就是以“块”为单位进行读取,硬盘的分区被分为各个块,块的大小是在格式化的时候设置的,并非不可改变的,通常来说由8个扇区组成一个块,即一个块 = 4096字节(4KB)

硬盘我们可以看作是一个三维数组,把他转换为一维数组后数组下标就是LBA,每一个扇区都LBA。

已知LBA:块号 = LBA / 8 (整除)。

已知块:LBA = 块号 * 8 + n(块内第几个扇区)。

那么块是怎么划分的呢?我们后面讲。

2.2.2分区

硬盘的分区,其实就是给硬盘进行格式化,柱面是分区的最小单位(为了保证物理连续性,减少碎片化)。

2.2.3“inode”

我们都知道文件 = 属性 + 内容,并且我们目前还知道文件的内容存在块中,为什么还要有inode呢?

其实inode叫做“索引节点”,用于指向文件的属性信息,inode属性信息大小都是一样,通常为128字节或256字节。并且在Linux中inode作为文件的唯一标识。我们来看看inode长什么样↓

指令为 ls -li

第一个属性就是inode,第二个为模式,第三个为硬链接数,第四个为文件所有者,第五个为用户组,第六个为文件大小(字节),第七个为最后修改的日期,最后的为文件名。

下面为源码:

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)

3.Ext2文件系统

我们上述的文件内容,基本都是由文件系统来管理的,要用硬盘存储数据的话,首先要先把硬盘格式化成某个文件系统的格式,然后才能使用,文件系统存在的目的就是组织和管理硬盘中的数据和文件

在Linux中最常见的文件系统就是Ext2文件系统,Ext3和Ext4基本是Ext2文件系统的增强版,其核心基本没有变化。

Ext文件把整个分区分为n个大小相同的块组,如下图↓

EXT2 File System一起的是Boot Sector(启动块),其大小是确定的1KB,根据PC标准规定,它储存着磁盘分区信息和启动信息,任何文件系统都是无法修改启动块的。

3.1Block Group

我们可以看到Block Group内部分为 Super Block、GDT、Block Bitmap、inode Bitmap、inode Table、Data Blocks。

3.1.1Super Block

Super Block内存储着文件系统本身的结构和信息,描述了整个分区的文件系统信息。记录的信息主要有:bolck和inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写 ⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。如果Super Block被破坏了,整个文件系统就被破坏了。

Super Block在每个组块内都有备份(也有可能是第一个块组有,其他块组可以没有)。系统对Super Block有多个备份,就是为了磁盘部分出现物理问题时文件系统也能正常工作,保证文件系统的Super Block信息可以正常访问。而且每个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.1.2GDT

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

struct ext2_group_desc
{
	__le32	bg_block_bitmap;		/* Blocks bitmap block */
	__le32	bg_inode_bitmap;		/* Inodes bitmap block */
	__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.1.3Block Bitmap

块位图,记录着Data Block中每个块的占用信息,可以知道哪个块被占用。

3.1.4Inode Bitmap

inode位图中,每个bit表示inode是否空闲可用。

3.1.5Inode Table

inode表,当前组的所有inode集合,存放着文件属性,最近修改日期,所属者等等。inode标号以分区为单位整体划分,不可越区访问。

3.1.6Data Block

数据区,存放着一个个Block:存放⽂件内容。

根据不同的⽂件类型有以下⼏种情况:

对于普通⽂件,⽂件的数据存储在数据块中。

对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls -l命令 看到的其它信息保存在该⽂件的inode中。

3.2inode与Data Block的(简化)映射

inode内部存在
__le32  i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ 
EXT2_N_BLOCKS = 15 ,就是⽤来进⾏inode和block映射的

因此创建⼀个新⽂件主要有以下4个操作:(以图为例)----找的网图

1.存储属性:内核先找到一个空闲的inode节点,内核把文件属性记录到inode中,这里的inode为263466。

2.存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

3.记录分配情况:文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表(GDT)。

4. 添加文件名到目录:新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

3.3目录与路径解析

我们都说,一切皆文件,那么目录是文件吗,怎么理解目录。

实际上目录也是文件,它也是属性 + 内容,不过他的内容存放的是目录下的目录以及文件的映射关系,也就是各个文件名与inode的映射关系

简单抽一段代码看看↓

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <dirent.h>
  5 #include <sys/types.h>
  6 #include <unistd.h>
  7 int main(int argc, char *argv[]) {                                               
  8     if (argc != 2) {
  9         fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
 10         exit(EXIT_FAILURE);
 11     } 
 12     DIR *dir = opendir(argv[1]); // ϵͳµ÷Óã¬×ÔÐвéÔÄ
 13     if (!dir) { 
 14         perror("opendir");
 15         exit(EXIT_FAILURE);
 16     }
 17     struct dirent *entry;
 18     while ((entry = readdir(dir)) != NULL) { // ϵͳµ÷Óã¬×ÔÐвéÔÄ
 19         if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")== 0)
 20         {
 21             continue; 
 22         } 
 23         printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned
 24             long)entry->d_ino);
 25     }
 26     closedir(dir);
 27     return 0; 
 28 }

我们可以看到,打开目录就要获得对应的文件名,找到对应的inode号,然后访问目录(文件),本质上是我们必须能打开当前工作目录文件,查看目录文件内容。

这时我们不禁要问,当前我们所在的目录,不是工作目录吗?当前的目录不就打开文件名进来吗,访问不是需要inode吗?我们怎么打开。

Linux中要打开我们当前工作目录,就要有一个层层“递归”的过程,我们要递归到最上层,也就是 "/",我们也叫它根目录,每次访问文件,我们都要从根目录开始,依次打开每一个文件目录,知道找到我们要打开文件的文件名。这个过程就是路径解析。(访问文件: 目录 + 文件名 = 路径)

并且在Linux中访问文件,本质上是由进程进行访问,而进程PCB有CWD(进程的当前工作目录),而我们每次进入目录,打开文件都会被记录,也就有了路径。

根目录是Linux系统提供的目录文件,所以Linux的路径结构是由系统和用户一起构建的

那么问题来了,每次都由根目录打开是否过于慢、低效了。

3.4路径缓存

如果每次都从根目录开始,那确实太低速了。

在Linux系统中,OS会自己缓存历史路径结构,OS会自动维护近期打开的路径结构

Linux在内核中维护的树状路径结构结构体为:struct dentry

struct dentry {
	/* RCU lookup touched fields */
	unsigned int d_flags;		/* protected by d_lock */
	seqcount_t d_seq;		/* per dentry seqlock */
	struct hlist_bl_node d_hash;	/* lookup hash list */
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name;
	struct inode *d_inode;		/* Where the name belongs to - NULL is
					 * negative */
	unsigned char d_iname[DNAME_INLINE_LEN];	/* small names */

	/* Ref lookup also touches following */
	struct lockref d_lockref;	/* per-dentry lock and refcount */
	const struct dentry_operations *d_op;
	struct super_block *d_sb;	/* The root of the dentry tree */
	unsigned long d_time;		/* used by d_revalidate */
	void *d_fsdata;			/* fs-specific data */

	union {
		struct list_head d_lru;		/* LRU list */
		wait_queue_head_t *d_wait;	/* in-lookup ones only */
	};
	struct list_head d_child;	/* child of parent list */
	struct list_head d_subdirs;	/* our children */
	/*
	 * d_alias and d_rcu can share memory
	 */
	union {
		struct hlist_node d_alias;	/* inode alias list */
		struct hlist_bl_node d_in_lookup_hash;	/* only for in-lookup ones */
	 	struct rcu_head d_rcu;
	} d_u;
} __randomize_layout;

注:

每个⽂件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中 形成整个树形结构

整个树形节点也同时会⾪属于LRU(LeastRecentlyUsed,最近最少使⽤)结构中,进⾏节点淘汰

整个树形节点也同时会⾪属于Hash,⽅便快速查找

更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这 棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry 结构,缓存新路径。

3.5小结

分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block Bitmap、InodeBitmap等管理信息,这些管理信息统称:⽂件系统。

只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定 是哪⼀个inode,进而⽂件属性和内容就全部都有了。

放几个网图帮组理解:


4. 软硬连接

硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,但实际上,新的文件和被引用的文件的inode不同,应用常见上可以想象成一个类似windows的快捷方式

1.硬链接

指令:ln 源文件 链接创建的文件名

我们可以看到硬链接文件的属性几乎是一至的。

. 和 .. 都是硬链接

2.软链接

指令:ln -s 源文件 链接创建的文件名

我们可以看到软链接文件有一个指向性的箭头。


祝大家技术更加精进,文章若有错误请指出。


网站公告

今日签到

点亮在社区的每一天
去签到