位于/系统编程/文件系统
本部分主要以ext2文件系统为例。
学完本部分内容,应该能解决的问题是,如何找到/home/gyl/test.c
总体存储布局
一个磁盘可以划分成多个分区,每个分区必须先用格式化工具(例如某种 mkfs 命令)格式化成某种格式的文件系统,然后才能存储文件,格式化的过程会在磁盘上写一些管理存储布局的信息。
下图就是一个磁盘分区格式化成ext2文件系统后的存储布局。
文件系统中存储的最小单位是块(Block),一个块究竟多大是在格式化时确定的,例如mke2fs的-b选项可以设定块大小为1024、2048或4096字节,linux系统默认情况下是4096,即4K。使用stat filename命令可以查看每个Block的大小,如下图所示:
上图中,IO Block指的就是每个Block的大小,为4096字节;注意左侧的Blocks指的并不是IO Block的个数,而是main.c这个文件所占据的物理磁盘扇区的个数;其中,每个磁盘扇区大小为512字节,详情以及其它参数后续再说。
ext2文件系统的存储布局中启动块(Boot Block)的大小是确定的,就是1KB,启动块是由PC标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划成若干个同样大小的块组(Block Group),每个块组都由以下几大部分组成
一个块组中的块是这样利用的:数据块(Data Block)存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块(Super Block)、块组描述符表(GDT)、块位图(Block bitmap)、inode位图(inode bitmap)、inode表(inode table)这几部分存储该块组的描述信息。为了更好的展开讲解,GDT放到最后说。
Super Block(超级块)
本身占一个块,超级块描述的是整个分区的文件系统信息,例如每个块的大小、文件系统版本号、上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。每份拷贝的信息完全一样。一个块组出问题,可以用其它块组的超级块来恢复。
Block bitmap(块位图)
通过前面的学习,已经知道一个块组中块是怎麽利用的了,那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。因此,如果一个Block为4096字节,那么可以表示4096×8bit;即每个组块,最多可以表示4096*8个块的占用情况,换句话说,一个含有m个Block的分区,最少有m/(4096×8)个块组。
为什么用 df 命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即可,而不需要搜遍整个分区。相反,用 du 命令查看一个较大目录的已用空间就非常慢,因为不可避免地要搜遍整个目录的所有文件。
与此相联系的另一个问题是:在格式化一个分区时究竟会划出多少个块组呢?主要的限制在于块位图本身必须只占一个块。用mke2fs 格式化时默认块大小是 1024 字节,可以用-b 参数指定块大小,格式化时可以用-g 参数指定一个块组有多少个块,但是通常不需要手动指定,mke2fs 工具会计算出最优的数值。
Inode bitmap(inode位图)
和块位图类似,本身占一个块,其中每个 bit 表示一个 inode 是否空闲可用。
inode 表(inode Table)
一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是 ls -l 命令看到的那些信息,这些信息存在 inode中而不是数据块中。每个文件都有一个 inode,一个块组中的所有 inode组成了inode 表。每个inode占128位。所以一个Block(4096)最多可以保存4096/128 = 32个inode;32个inode明显不够用,因此inode表占多个块。
inode表占多个块,到底占多少个块在格式化分区时就要决定并写入块组描述符(GDT)中, 默认情况下,每8KB分配一个inode,因此mke2fs 格式化工具的策略是一个块组有多少个8KB 就分配多少个 inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个8KB就分配多少个 inode。例如:大小1MB的文件系统,对应1024K/8K = 128个inode;100个inode对应100*8k = 800KB空间。
如果平均每个文件的大小是 8KB,当分区存满的时候 inode 表会得到比较充分的利用,数据块也不浪费。
如果这个分区的都是很大的文件(比如电影),则数据块用完的时候 inode 会有剩余,这就造成了一定的浪费。
如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完 inode 就已经用完了,数据块可能有很大的浪费。
如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用 mke2fs 的-i 参数手动指定每多少个字节分配一个 inode。
小提示
使用df -i可以查看inode空闲情况
使用df -l可以查看磁盘文件系统空闲情况
如果出现无法创建文件或者目录的情况,确认非权限问题的前提下,通过这两个命令,就可以检查到哪个环节出问题。
数据块(Data Block)
数据块占据了Block Group绝大部分的Block,根据不同的文件类型有以下几种情况
1.对于常规文件,文件的数据存储在数据块中。
2.对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件
名之外,ls -l 命令看到的其它信息都保存在该文件的 inode 中。注意这个概念:目录也是一种文件,是一种特殊类
型的文件。
3.对于符号链接,如果目标路径名较短则直接保存在 inode 中以便更快地查找,如果目标路径名较长则分配一个数据
块来保存。
4.设备文件、FIFO 和 socket 等特殊文件没有数据块,设备文件的主设备号和次设备号保存在 inode 中。
现在cd到根目录下,执行ls -l。如下图所示:
会发现,目录大小都是4096的整数倍。原因是这个分区的块大小是 4096,目录的大小总是数据块的整数倍。为什么有的目录大有的目录小?因为目录的数据块保存着它下边所有文件和目录的名字,如果一个目录中的文件很多,那么一个块就装不下这么多文件名了,此时就可能分配更多的数据块给这个目录。因此就出现目录有大有小的情况。
GDT(Group Descriptor Table,块组描述符表)
块组描述符表由很多块组描述符组成,占多个Block。整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,比如从那里开始是数据块,空闲的inode和数据块各还有多少等信息都保存在GDT里。
跟超级块类似,块组描述符表在每个块组的开头都有一个拷贝,每个拷贝的信息完全一样。也就是说每个Block Group里的块组描述符表不只含有当前Group的Block信息,还包括当前分区其它所有Group内Block的信息。
超级块和快组描述符表非常重要,如果超级块和GDT损坏就会丢失整个块组的数据,此时的多分拷贝就起到作用了,一般情况下,内核只用到第0个块组中的拷贝,当执行e2fsck检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第 0 个块组(第0个lock Group)的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失,这就是多份拷贝的用处。
寻址
来自文心一言
如果你希望手动解析ext2文件系统,理解其数据结构,并一步步提取文件名和文件内容,你需要深入了解ext2文件系统的内部结构和各个组件,包括超级块(superblock)、组描述符(group descriptor)、inode表、数据块(data blocks)等。
以下是一个简化的步骤,你可以通过编程或使用命令行工具手动进行:
-
读取超级块:
- 使用
dd
或hexdump
等工具,从ext2文件系统的开头读取超级块。超级块位于文件系统的开始位置,通常占用1KB(1024字节)。 - 分析超级块中的关键信息,如inode大小、inode表的位置、数据块大小、块组描述符表的位置等。
- 使用
-
读取组描述符:
- 根据超级块中的信息,计算组描述符表的位置。
- 读取每个组描述符,了解块组中的inode表和数据块的位置。
-
读取inode表:
- 根据组描述符中的信息,计算inode表的位置。
- 遍历inode表,每个inode记录了文件的元数据,如文件大小、文件类型、链接数、数据块的位置等。
- 找到你要查找的文件对应的inode。
-
读取文件内容:
- 根据inode中记录的数据块位置,读取文件的数据块。
- 将数据块按照文件系统中定义的块大小进行组合,还原出文件内容。
-
解析文件名:
- ext2文件系统中的文件名存储在目录项(dentry)中,目录项是inode的一种特殊形式。
- 找到根目录对应的inode(通常在超级块中有指示)。
- 读取根目录inode对应的数据块,解析目录项,找到你要查找的文件名对应的目录项。
- 从目录项中提取文件名。
这个过程涉及大量的底层细节,并且需要对二进制数据格式和文件系统结构有深入的了解。通常,这些步骤会通过编写C语言程序或使用专门的工具(如debugfs
)来完成,因为这些工具能够直接处理二进制数据,并提供解析文件系统结构的接口。
如果你打算手动进行这个过程,你可能需要编写一个C语言程序或使用像hexdump
这样的工具来查看二进制数据,并手动解析出文件系统的组件。这会是一个复杂且耗时的过程,但对于深入理解文件系统的工作原理非常有帮助。
请注意,这个过程并不适合初学者,并且很容易出错。如果你没有丰富的底层编程经验和文件系统知识,使用现成的工具通常是更好的选择。如果你确实想手动解析ext2文件系统,我建议你先阅读相关的文档和教程,了解ext2文件系统的详细结构和组件,然后再逐步进行。
ext2文件系统的超级块(superblock)是文件系统的核心部分,它包含了文件系统的全局参数和状态信息。以下是ext2超级块的主要数据结构定义(基于Linux内核源代码中的ext2_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 the filesystem.
*/
__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 */
/*
* Additional fields in new revisions of the superblock
*/
__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_algo_bitmap; /* Precomputed bitmap of file system */
/*
* Performance hints. Directory preallocation should only
* happen if the EXT2_FEATURE_COMPAT_DIR_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 */
__le16 s_reserved_gdt_blocks;
要使用dd
或hexdump
等工具来读取并解析ext2文件系统的超级块,你需要先确定文件系统映像文件的位置。这通常是一个文件,如/dev/sda1
(代表第一个分区上的ext2文件系统),或者是一个文件系统的映像文件。
以下是一个基本的步骤指南,展示如何使用这些工具来读取和解析超级块:
使用dd读取超级块
-
确定超级块的偏移量:
在ext2文件系统中,超级块通常位于文件系统的开始位置,偏移量为0。 -
使用dd命令读取超级块:
使用dd
命令以二进制格式读取超级块。由于超级块的大小通常为1024字节(1KB),你可以使用以下命令:dd if=/dev/sda1 of=superblock.bin bs=1K count=1
这里,
if
参数指定输入文件(即文件系统设备或文件),of
参数指定输出文件,bs
参数设置块大小(这里是1K),count
参数设置块的数量(这里是1)。
使用hexdump解析超级块
接下来,你可以使用hexdump
或xxd
等工具来查看超级块的十六进制表示,并手动解析它。
-
使用hexdump查看超级块内容:
使用hexdump
命令以十六进制格式显示超级块的内容:hexdump -C superblock.bin
-C
选项使输出更易读,它将每行显示为16字节的十六进制数据,后跟相应的ASCII字符(如果是可打印的)。 -
手动解析超级块:
根据ext2超级块的数据结构定义,你可以手动解析hexdump
的输出。例如,超级块的第一个字段s_inodes_count
是一个32位无符号整数,表示文件系统中inode的总数。在hexdump
的输出中,这个值将占据连续的4个字节。你需要根据ext2的数据结构定义,逐个字段地解析十六进制数据,将它们转换成相应的数据类型,并理解每个字段的含义。
注意事项
- 确保你有足够的权限来访问文件系统设备文件(如
/dev/sda1
),通常需要使用sudo
。 - 在进行任何操作之前,最好先对文件系统做一个备份,以防止数据丢失或损坏。
- 如果你是在一个实际运行的文件系统上操作,确保文件系统处于只读模式或卸载状态,以避免对文件系统造成损坏。
- 解析文件系统结构是一个复杂的过程,需要对二进制数据格式和文件系统结构有深入的了解。如果你是初学者,建议从阅读相关文档和教程开始,并逐步进行实践。
要从超级块中解析出块组描述符表(block group descriptor table),你需要首先找到超级块中的s_first_data_block
字段,它表示第一个数据块的块号。块组描述符表紧跟在超级块之后,每个描述符的大小由s_desc_size
字段定义。
以下是一个简化的步骤,指导你如何只解析块组描述符表:
-
确定超级块的大小:
通常,ext2/ext3/ext4文件系统的超级块大小为1024字节(1KB)。 -
读取超级块:
使用dd
命令读取超级块。dd if=/dev/sda1 of=superblock.bin bs=1K count=1
-
查看超级块内容:
使用hexdump
或xxd
查看超级块内容。hexdump -C superblock.bin
-
找到
s_first_data_block
和s_desc_size
字段:
在hexdump
的输出中,找到s_first_data_block
和s_desc_size
字段的值。s_first_data_block
通常位于偏移量0x10(16进制)处,而s_desc_size
位于偏移量0x24(16进制)处。这些值将告诉你块组描述符表的位置和大小。 -
计算块组描述符表的偏移量:
块组描述符表的偏移量可以通过将超级块大小(通常为1024字节)加上s_first_data_block
的值来计算。offset=$(printf "0x%x + 1024\n" $(od -An -N4 -i superblock.bin | head -n 1 | tail -n 1))
这个命令使用
od
工具从超级块文件中提取s_first_data_block
字段的值,并加上1024来计算偏移量。 -
读取块组描述符表:
使用dd
命令读取块组描述符表。你需要知道每个描述符的大小(s_desc_size
)以及块组数量(s_blocks_per_group
/s_desc_size
)。desc_size=$(printf "0x%x\n" $(od -An -N2 -i superblock.bin | head -n 2 | tail -n 1)) block_groups=$(printf "%d\n" $(od -An -N4 -i superblock.bin | head -n 3 | tail -n 1) / $desc_size) dd if=/dev/sda1 of=bg_descriptors.bin bs=$desc_size count=$block_groups skip=$offset
这里,
od
命令被用来从超级块文件中提取s_desc_size
和s_blocks_per_group
字段的值。然后,我们使用这些信息来计算块组描述符的数量,并使用dd
命令读取这些描述符。 -
查看和分析块组描述符表:
最后,你可以使用hexdump
或xxd
来查看和分析块组描述符表的内容。hexdump -C bg_descriptors.bin
现在,你可以根据ext2文件系统的数据结构定义来手动解析
bg_descriptors.bin
文件,以获取每个块组的信息。
请注意,上述步骤中的命令和偏移量可能需要根据你的实际文件系统和超级块结构进行调整。务必小心操作,确保你理解每个步骤的含义,并在对实际文件系统进行操作之前备份重要数据。