🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹:【Linux笔记】理解文件系统(上)
🔖流水不争,争的是滔滔不
一、ext2文件系统宏观认识
特点
- 可靠性高:采用了先进的日志技术,能够记录文件系统的元数据和数据块的更改操作,在系统崩溃或突然断电等情况下,可以通过日志快速恢复文件系统的一致性,减少数据丢失和损坏的风险。
- 支持大文件和大分区:理论上,ext2 文件系统支持的最大文件大小为 2TB,最大分区大小为 32TB,能够满足大多数用户对存储大容量文件和管理大规模数据的需求。
- 高效的文件访问:采用了索引节点(inode)和数据块的结构来组织文件数据,通过 inode 可以快速定位文件的元数据和数据块位置,提高了文件的访问效率。
结构 - 超级块(Super Block):存储了文件系统的基本信息,如文件系统的大小、块大小、inode 数量、空闲块和 inode 的数量等,是文件系统的核心控制结构。
- 组描述符表(Group Descriptor Table):包含了文件系统中每个块组的描述信息,如块组中 inode 表的起始位置、数据块的起始位置、空闲块和 inode 的位图等。
- inode 表:每个文件和目录在文件系统中都有一个对应的 inode,inode 中存储了文件的元数据,如文件的所有者、权限、大小、创建时间、修改时间等,以及指向文件数据块的指针。
- 数据块:用于实际存储文件的数据内容,文件的数据被分散存储在多个数据块中,通过 inode 中的指针来链接这些数据块,从而构成完整的文件数据。
工作原理 - 文件创建:当用户在 ext2 文件系统中创建一个新文件时,系统会从空闲的 inode 中分配一个 inode,并在 inode 中填写文件的元数据信息,同时为文件分配数据块,并将数据块的指针记录在 inode 中。
- 文件读写:在读取文件时,系统根据文件名找到对应的 inode,从 inode 中获取文件的数据块指针,然后按照指针顺序读取数据块中的数据;写入文件时,系统先根据 inode 找到要写入的数据块,如果数据块空间不足,则分配新的数据块,并更新 inode 中的数据块指针和文件大小等元数据。
- 文件删除:删除文件时,系统会将 inode 标记为空闲,并释放文件占用的数据块,将这些数据块标记为可用,以便后续文件使用。
应用场景
- 服务器环境:由于 ext2 文件系统具有较高的稳定性和可靠性,被广泛应用于 Linux 服务器中,用于存储系统文件、用户数据、应用程序等。
- 嵌入式系统:在一些对存储容量和性能要求不是特别高的嵌入式设备中,ext2 文件系统因其相对简单的结构和较好的兼容性,也常被用作默认的文件系统。
我们想要在硬盘上储存文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。
ext2文件系统将整个分区划分成若干个同样大小的块组 (Block Group)。只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件
注意 :分完区还要分组
上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
二、Block Group
ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
2.1 Data Block
文件=内容+属性。文件的内容就存放在Date Block区域。
数据区:存放文件件内容,也就是⼀个⼀个的Block。根据不同的文件类型有以下几种情况:
- 对于普通文件,文件的数据存储在数据块中。
- 对于目录,该目录下的所有文件名和目录名存储在所在目录的数据块中,除了文化名外,ls -l命令看到的其它信息保存在该文件的inode中。
- Block 号按照分区划分,不可跨分区
2.2 i节点表(Inode Table) 和 inode
存放文件属性 ,如文件大小,所有者,最近修改时间等
当前分组所有Inode属性的集合
inde编号是跨组编号的。inode编号以分区为单位,整体划分,不可跨分区 。所以在同一分区内,inode编号是唯一的。
inode存储文件的属性。文件的属性的大小是一样的。innode是一个结构体,存储文件的各种属性。
struct ext2_inode {
__le16 i_mode; // 文件模式,如文件类型和权限
__le16 i_uid; // 文件所有者的用户 ID 低 16 位
__le32 i_size; // 文件大小(字节)
__le32 i_atime; // 最后访问时间
__le32 i_ctime; // 创建时间
__le32 i_mtime; // 最后修改时间
__le32 i_dtime; // 删除时间
__le16 i_gid; // 文件所属组 ID 低 16 位
__le16 i_links_count; // 硬链接计数
__le32 i_blocks; // 文件占用的数据块数量
__le32 i_flags; // 文件标志
__le32 i_block[EXT2_N_BLOCKS]; // 指向数据块的指针数组
// 其他字段...
};
文件名不会作为文件的属性保存在文件的inode里。
inode Table也就是数据块大小为4kb,一个inode为128字节。也就是说一个数据块中存储32个inode。操作系统和文件系统交互的时候,这些数据都会读进去操作系统按需读取。
结构体中有一个指向数据块的指针数组,通过指针找到inode对应的内容。
2.3 块位图(Block Bitmap)
Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
2.4 inode位图(Inode Bitmap)
每个bit表示⼀个inode是否空闲可用。
2.5 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];
};
2.6 超级块
存放文件系统本身的结构信息,描述整个分区的文件系统信息。记录的信息主要有:bolck 和 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 */
};
文件系统是在哪运行的呢 ?文件系统与操作系统的交互
分区信息处理:
描述:通过读取分区表获取分区的起始扇区、大小等基本信息。
组织:将这些信息存储在自定义的数据结构中。
加载:把数据结构加载到内存。
块组信息处理:
描述:读取超级块和组描述符表,获取块组的 inode 表位置、数据块位置等管理信息。
组织:使用相应的数据结构存储块组信息。
加载:将块组信息数据结构加载到内存。
三、目录与文件名
磁盘上没有目录的概念,只有文件属性+文件内容的概念,目录也是文件。
目录的内容保存的是,文件名和inode的映射关系,磁盘只认inode所以打开文件就是通过这个文件所映射的inode打开的。
我们平常通过路径+文件名打开文件是通过,先打开我所在的路径,读取对应的目录里的数据内容得到文件名和inode的映射关系,以此来进行文件的查找。
访问文件必须要知道当前⼯作目录,本质是必须能打开当前工作目录文件,查看目录文件的内容!
:~/learn/test_12_1/code.c
要访问code.c就必须打开test_12_1(当前工作目录),以获取test.c对应的inode进而对文件进行访问。以此类推要从最开始一层一层的往下剥。所以,找到任何Linux文件,都必须从跟目录开始,进行路径解析直到找到对应的文件。
四、路径解析
何文件,都有路径,访问目标文件,如/home/whb/code/test/test/test.c,都要从根目录开始,依次打开每⼀个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到test.c。这个过程叫做Linux路径解析。
访问文件,都是指令/工具访问,本质是进程访问,进程有CWD!进程提供路径。open文件,就提供了路径。
上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系统指定的目录下新建,这不就是天然就有路径了嘛!系统+用户共同构建Linux路径结构。
五、路径缓存
上面聊了,我们访问任何文件都要从跟目录进行路径解析。可是这样太慢了,所以Linux会进行缓存历史路径结构。Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
在Linux中,进行路径解析的时候,会把我们历史访问的所以的目录(路径)形成一颗多叉树,进行保存。
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* 目录项标志,用于表示目录项的状态和属性 */
seqcount_t d_seq; /* 顺序计数器,用于 RCU(Read-Copy Update)同步机制 */
struct hlist_bl_node d_hash; /* 哈希链表节点,用于将目录项插入到哈希表中 */
struct dentry *d_parent; /* 指向父目录项的指针,用于构建目录树结构 */
struct qstr d_name; /* 目录项的名称,以 `struct qstr` 结构体表示 */
struct inode *d_inode; /* 指向关联的 inode 的指针,inode 存储文件元数据 */
unsigned char d_iname[DNAME_INLINE_LEN]; /* 内联存储的目录项名称,提高性能 */
/* Ref lookup touched fields */
struct lockref d_lockref; /* 用于引用计数的锁,控制目录项的生命周期 */
const struct dentry_operations *d_op; /* 指向目录项操作函数集的指针,包含如 d_hash、d_compare 等操作 */
struct super_block *d_sb; /* 指向关联的超级块的指针,超级块存储文件系统的全局信息 */
unsigned long d_time; /* 目录项的时间戳,用于缓存管理 */
/* d_lru list */
struct list_head d_lru; /* 链表节点,用于将目录项插入到最近最少使用(LRU)链表中 */
/* d_child and d_rcu can share memory */
union {
struct list_head d_child; /* 链表节点,用于将目录项插入到父目录的子目录项链表中 */
struct rcu_head d_rcu; /* RCU 头,用于 RCU 机制下的延迟释放 */
};
/* d_subdirs and d_alias can share memory */
union {
struct list_head d_subdirs; /* 链表头,用于管理子目录项链表 */
struct hlist_node d_alias; /* 哈希链表节点,用于将目录项插入到 inode 的别名链表中 */
};
/* 私有数据,由文件系统特定使用 */
void *d_fsdata;
};
- 每个文件其实都要有对应的dentry结构,包括普通文件。这样所有被打开的⽂件,就可以在内存中形成整个树形结构
- 整个树形节点也同时会隶属于LRU(Least Recently Used,最近最少使用)结构中,进行节点淘汰
- 整个树形节点也同时会隶属于Hash,方便快速查找
- 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都在先在这棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径
六、挂载分区
Linux 等操作系统中,“挂载” 是一种将存储设备或文件系统连接到操作系统的目录树中的操作,使得该存储设备或文件系统中的内容可以通过操作系统的文件系统接口进行访问。
分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。 所以,可以根据访问目标文件的"路径前缀"准确判断我在哪⼀个分区。
文件的基础io和文件系统的联系
当进程调用open()、creat()等系统调用打开一个文件时,文件系统会在磁盘上找到该文件的 inode(索引节点),inode 存储了文件的元数据和数据块指针。同时,内核会为这个打开的文件分配一个新的文件描述符,并在文件描述符表中记录该文件描述符与 inode 的关联。后续进程对文件的操作(如read()、write())就通过文件描述符来定位到具体的文件,进而通过 inode 访问文件的数据。例如,read(fd, buffer, size)这个系统调用中,fd就是文件描述符,内核根据它找到对应的 inode,再从 inode 指向的数据块中读取数据。
当进程向文件写入数据时,数据首先被写入到用户空间缓冲区。当缓冲区满或者满足特定条件(如调用fflush())时,数据会被传输到内核空间缓冲区。内核会在合适的时机将内核空间缓冲区中的数据写入到磁盘上的文件中。文件系统负责管理磁盘上的数据存储,它会根据 inode 中的信息找到文件的数据块,并将数据写入相应的位置。在读取文件时,文件系统会将磁盘上的数据读取到内核空间缓冲区,然后再从内核空间缓冲区复制到用户空间缓冲区,供进程使用。
写入操作流程:进程调用write()系统调用,将数据写入到用户空间缓冲区,此时数据还未真正写入磁盘。write()返回的是写入到用户空间缓冲区的数据量。当用户空间缓冲区满或者进程调用fflush()时,数据被发送到内核空间缓冲区。内核根据文件描述符找到对应的 inode,再根据 inode 中的信息确定数据要写入的磁盘位置,最后将内核空间缓冲区的数据写入磁盘。
读取操作流程:进程调用read()系统调用,内核首先检查内核空间缓冲区中是否有进程所需的数据。如果有,则直接从内核空间缓冲区将数据复制到用户空间缓冲区;如果没有,内核会根据文件描述符找到对应的 inode,从磁盘上读取数据到内核空间缓冲区,再将数据复制到用户空间缓冲区。
七、软硬链接
7.1 软链接
链接是通过inode引用另外⼀个文件,软链接是通过名字引用另外⼀个文件,但实际上,新的文件和被引用的文件的inode不同,应用常见上可以想象成⼀个快捷方式。
[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
7.2 硬链接
我们看到,真正找到磁盘上文件的并不是文件名,而是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的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
硬链接本质不是一个独立的文件,因为它没有独立的inode,本质是一组新的文件名和目标inode的映射关系。
用于文件备份,还有我们Linux打开目录时,.和…就是硬链接