【Linux系统】详解Ext2,文件系统

发布于:2025-08-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言:

        上文我们讲到了文件IO【Linux系统】万字解析,文件IO-CSDN博客

        文件IO是文件被打开了之后的操作,那么在文件没有被打开前系统是如何管理文件的呢?那么下面我们来讲一讲,系统是如何对文件进行管理的。

           点个关注!            


硬件理解:

机械磁盘

        机械磁盘是计算机中唯⼀的⼀个机械设备。

        磁盘是外设。

        读写速度慢,但是容量⼤,价格便宜。

磁盘物理结构

磁盘存储结构

        磁头:每个盘片一般有上下两面,分别对应1个磁头,共2个磁头。

         磁盘由多个盘片组成。

        一个盘片有多个磁道组成。

        磁道并不是连续的,而是由多个扇区间隔组合而成的。

        扇区:512字节,是磁盘存储的基本单位!

        柱面:磁道构成柱面,数量上等同于磁道个数

        细节:传动臂上的磁头是共进退的!

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

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

磁盘的逻辑结构

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

        那么磁盘本质上虽然是硬质的,但是我们可以逻辑上想象其可以拉直,那么磁盘的逻辑存储结构我们也可以类似于:

        这样每个扇区,就都有了一个标记(数组下标),这种地址叫做:LBA

真实过程

        柱面:是一个逻辑概念,本质是由每一面相同半径的磁道构成的。

        所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘是由“柱面”卷起来的!

        

所以磁盘的真实逻辑结构

磁道展开:

(一维数组?)

柱面展开:既多个半径相同的磁道!

(二维数组?)

整个磁盘展开:既多个柱面展开!(三维数组)

        所以,寻找一个扇区,先找到柱面(Cylinder),再确定是柱面的那一个磁道(其实就是磁头的位置,Head),最后再确定扇区,所以就有了CHS

        在逻辑上,每个扇区都有一个下标,我们叫做LBA(Logical Block Address)地址,其实就是线性地址!

        OS只需要知道知道LBA地址即可!因为LBA可以和CHS之间相互转换!

LBA & CHS

        LBA与CHS的转化由磁盘自己来做!(固件:硬件电路,伺服系统)

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使用磁盘时,就可以用一个数字访问磁盘扇区了!


文件系统概念

“块”

        其实硬盘是一个典型的 “块” 设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区的读取的,这样效率太低了!

        而是一次性读取多个扇区,一次性读取的多个扇区我们称作“块”

        硬盘的每个分区是被划分为一个个的“块”,一个“块”的大小是由格式化的时候确定的,并且不可修改!其最常见的“块”大小为4KB,既:8个连续的扇区!
        “块”是文件存取的最小单位。

注:

        磁盘在逻辑上,我们将其看作一个一维数组,数组的下标就是LBA,元素是扇区。

        每个扇区都有对应的LBA,8个扇区为一个“块”,那么我们也可以算出“块”的下标

        块号 = LBA / 8

        LAB = 块号 * 8 + n(n是块内第几个扇区)

“分区”

        其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备都是以文件形式存在,那是怎么分区的呢?

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

“inode”

        我们知道,文件 = 属性 + 内容

        当我们使用:stat指令时可以查看到文件的一系列属性信息

        文件都是存储在“块”中,那么文件的属性也自然存储在这里面。其中,存储文件信息的区域就叫做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字节!

        一个块,存储32个inode结构体。

        但值得一提的是,文件名并没有保存在inode里


Ext2文件系统

        如图,一个磁盘(Disk)会划分为多个分区(Partition)

        而一个分区(Partition)又会以块组(Block Group)为单位划分为多个块组(Block Group)

        而一个块组划分为:超级块(Super Block)、GDT、块位图、inode位图、节点表(inode Table)、数据块

        理解:就像为了管理一个地区,先划分几个省,再将省划分为若干个市,再将区划分为若干个区...... ,便于管理。

        补充:文件系统的载体是分区!

超级块(Super Block)

        存放文件系统本身的结构信息,描述整个分区的文件系统信息。记录的信息主要有:bolck和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了

        超级块在每个块组的开头都有⼀份拷贝(第⼀个块组必须有,后⾯的块组可以没有)。但为了保证在磁盘部分扇区出现了问题时可以正常工作,超级块(super block)会在多个分组里面进行备份!这些超级块的数据都是一模一样的。

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];
};

块位图 & inode位图

        块位图(Block Bitmap):其中用一个bit位记录Date Block中一个数据块是否被占用。

        inode位图(Inode Bitmap):与块位图一样,都是用一个bit位表示一个inode是否可用。

        补充:格式化的本质是通过初始化超级块、块组描述符表、块位图、inode 位图、inode 表

等文件系统的管理信息。

        

Ionde Table

        存放文件的属性,如:文件大小、所有者、acm时间等

        是当前分组中所有文件属性的集合!

        inode编号是跨组编号的,但不跨分区(既:在一个分区中inode编号是唯一的,分区与分区之间编号是重复的。比如分区一的inode编号:0~2000,分区二的编号也是:0~2000)

        inode编号是存放在inode内部的!

struct inode
{
    int type;
    int size;    
    int UID;
    ....
    int inode_number;
}

Date Block

        存放文件的内容!

        对于普通文件,文件的内容就存放在数据块中

        对于目录,目录下的所有文件名、目录名以以及各个文件目录对应的inode编号,存在数据块中        

        数据块的编号与inode编号一样,跨组但不跨区!

        块是文件系统和存储设备交互的基本单位,操作系统读写数据都是按块来进行的。

Ionde与Date Block的映射

        inode内部存在,__le32 i_block[EXT2_N_BLOCKS];​  ​​​​​EXT2_N_BLOCKS=15。其就是一个数组。

                如图,前12个块编号直接指向存储数据的块。

                第13个块编号指向一个块,这个数据块里面存放块编号,再通过这个给块指向更多的存储文件内容的数据块

                同理,第14个块编号、第15个块编号通过间接的方式,指向更多的数据块。

于是往后,我们只需要拿到一个文件的inode,就可以找到文件的所有内容了

但是这里有两个问题,值得我们思考:

1.系统是如何知道这个文件在那个分组的呢?

        这个很简单,因为inode码在一个分区里面是唯一的!所以可以直接定位到具体的分组!通过对inode 取余和模运算 组的大小,就可以得到inode所在的分组,以及组里面的位置了!

2.系统是如何知道这个文件在那个分区的呢?

        这个我们通过inode无法判断,下面将会为大家讲解

目录与文件名

        我们说到文件的定位是通过文件的标识符inode实现的,但是在我们正常的使用过程中从来没有用过inode啊,我们都是通过文件名定位的!
        这是怎么回事呢?

        首先,在磁盘上没有目录这一概念,只有:文件属性+文件内容的概念。

        所以对于目录来说,目录的属性不用多说,而目录的内容则是保存的文件名与inode的映射关系。(回应上面说到了inode中并不存储文件名)

        我们知道想要访问文件,就必须先打开目录!(Linux所有文件都在目录中,即使什么都没有做,都会在根目录下!)。

        而我们通过文件名,就可以获得文件名对应的inode码,然后进行文件的查找访问!

        而想要打开对应目录找到对应的文件,其前提就是必须要有对应的路径!

关于路径

        想要打开当前目录,就先到得到当前目录的inode,再找到当前目录的位置,再打开。而如何得到当前目录的inode呢?只能通过当前目录的上级目录中保存的映射关系找到!

        以此类推!最终的出口是:根目录!(根目录固定文件名,inode号,无需查找,系统开机之后就必须知道)

        所以想要打开任何目录任何文件,都只能从根目录开始往下找!

        但这不就意味这我们找一个目录或文件时,会一直与磁盘进行IO交互吗?那效率也太低了

        实际上,在Linux中OS会将历史上访问过路径全部保存,形成一棵树!叫做: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_PROFILING
	struct dcookie_struct* d_cookie; /* cookie, if any */
#endif
	int d_mounted;
	unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};

        所以此后,想要访问任何目录或文件,都会先在这颗树里面查找!找到了则返回对应的inode、内容,没找到就去磁盘中加载路径,更新到树里面!这极大了提高了效率!

挂载分区

        回到上一个问题:系统是如何知道这个文件在那个分区的呢?

做一个实验:

$ 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 # 卸载分区 
hyc@hyc-alicold:/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

hyc@hyc-alicold$ 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

结论:

        分区后写入文件系统,无法使用,必须要和指定的目录关联,进行挂在才可以使用。

        所以,根据访问目标文件的路径,我们就可以知道这个文件是属于那个分区的!


文件系统总结

        1.打开文件:使用语言接口、系统调用执行打开文件操作。

        2.通过接口传递的路径,OS先去struct dentry中查找,若找到返回inode。若没找到,通过路径确定目标所在的分区、通过上级目录中inode码与文件名的映射关系得到inode码、通过inode码确定目标所在分组,最终返回inode。

        3.inode加载到内存中。

        4.通过inode中的 i_block[EXT2_N_BLOCKS] 找到文件内容。

        5.创建struct file 并为其分配fd(文件描述符),struct file内部指针指向inode,并初始化其他文件属性信息(打开模式,引用计数等等)。

        读写文件时,文件内容才会加载到缓冲区中!


软硬链接

软链接

ln -s  目标文件 文件

文件 软链接至 目标文件
hyc@hyc-alicloud:~/linux/软硬链接$ ln -s test.c abc.c
hyc@hyc-alicloud:~/linux/软硬链接$ ls -li
total 4
425348 lrwxrwxrwx 1 hyc hyc  6 Aug 12 17:49 abc.c -> test.c
425386 -rw-rw-r-- 1 hyc hyc 79 Aug 12 17:47 test.c

        软连接:abc.c是一个独立的文件!因为它拥有与test.c不同的inode码!

        软连接:保存了目标文件的路径!相当于windows的快捷图标!

hyc@hyc-alicloud:~/linux/软硬链接$ cat test.c
#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

hyc@hyc-alicloud:~/linux/软硬链接$ cat abc.c
#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

硬链接

ln 目标文件 文件

文件 硬链接至 目标文件
hyc@hyc-alicloud:~/linux/软硬链接$ ln test.c abc.c
hyc@hyc-alicloud:~/linux/软硬链接$ ls -li
total 8
425386 -rw-rw-r-- 2 hyc hyc 79 Aug 12 17:47 abc.c
425386 -rw-rw-r-- 2 hyc hyc 79 Aug 12 17:47 test.c

        硬链接:abc.c不是一个独立的文件!因为其inode码是一样的!

        其本质是新的文件名与inode建立了映射关系!

        作用:对文件进行备份

hyc@hyc-alicloud:~/linux/软硬链接$ ls -li
total 8
425386 -rw-rw-r-- 2 hyc hyc 79 Aug 12 17:47 abc.c
425386 -rw-rw-r-- 2 hyc hyc 79 Aug 12 17:47 test.c
hyc@hyc-alicloud:~/linux/软硬链接$ rm test.c
hyc@hyc-alicloud:~/linux/软硬链接$ ls -li
total 4
425386 -rw-rw-r-- 1 hyc hyc 79 Aug 12 17:47 abc.c
hyc@hyc-alicloud:~/linux/软硬链接$ cat abc.c
#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}


网站公告

今日签到

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