你好,我是LMOS。
上一节课,我们一起了解了什么是文件和文件系统。接下来的两节课,我们继续深入学习Linux上的一个具体的文件系统——Ext3,搞清楚了文件究竟是如何存放的。
这节课我会带你建立一个虚拟硬盘,并在上面建立一个文件系统。对照代码实例,相信你会对Ext3的结构有一个更深入的认识。课程配套代码,你可以从这里下载。话不多说,我们开始吧。
要想建立文件系统就得先有硬盘,我们直接用真正的物理硬盘非常危险,搞不好数据就会丢失。所以,这里我们选择虚拟硬盘,在这个虚拟硬盘上操作,这样怎么折腾都不会有事。
其实我们是用Linux下的一个文件来模拟硬盘的,写入硬盘的数据只是写入了这个文件中。所以建立虚拟硬盘,就相当于生成一个对应的文件。比如,我们要建立一个 100MB 的硬盘,就意味着我们要生成 100MB 的大文件。
下面我们用 Linux 下的 dd 命令(用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换)生成 100MB 的纯二进制的文件(就是向 1~100M 字节的文件里面填充为 0 ),代码如下所示:
dd bs=512 if=/dev/zero of=hd.img count=204800
;bs:表示块大小,这里是512字节
;if:表示输入文件,/dev/zero就是Linux下专门返回0数据的设备文件,读取它就返回0
;of:表示输出文件,即我们的硬盘文件
;count:表示输出多少块
下面我们就要在虚拟硬盘上建立文件系统了,所谓建立文件系统就是对虚拟硬盘放进行格式化。可是,问题来了——虚拟硬盘毕竟是个文件,如何让 Linux 在一个文件上建立文件系统呢?
这个问题我们要分成两步来解决。
第一步,把虚拟硬盘文件变成 Linux 下的回环设备,让 Linux 以为这是个设备。下面我们用 losetup 命令,将 hd.img 这个文件变成 Linux 的回环设备,代码如下:
sudo losetup /dev/loop0 hd.img
第二步,由于回环设备就是 Linux 下的块设备,用户可以将其看作是硬盘、光驱或软驱等设备,并且可以用mount命令把该回环设备挂载到特定目录下。这样我们就可以用 Linux 下的 mkfs.ext3 命令,把这个 /dev/loop0 回环块设备格式化,进而格式化hd.img文件,在里面建立 Ext3 文件系统。
sudo mkfs.ext3 -q /dev/loop0
需要注意的是,loop0可能已经被占用了,我们可以使用loop1、loop2等,你需要根据自己电脑的情况处理。
我们可以用 mount 命令将hd.img挂载到特定的目录下,如果命令执行成功,就能验证我们虚拟硬盘上的文件系统成功建立。命令如下所示:
sudo mount -o loop ./hd.img ./hdisk/ ;挂载硬盘文件
这行代码的作用是,将hd.img这个文件使用 loop 模式挂载在 ./hdisk/目录之下,通过这个hdisk目录,就能访问到hd.img虚拟硬盘了。并且,我们还可以用常用的mkdir、touch命令在这个虚拟硬盘中建立目录和文件。
我们建好了硬盘,对其进行了格式化,也在上面建立了Ext3文件系统。下面我们就来研究一下Ext3文件系统的结构。
Ext3文件系统的全称是Third extended file system,已经有20多年的历史了,是一种古老而成熟的文件系统。Ext3在Ext2基础上加入了日志机制,也算是对Ext2文件系统的扩展,并且也能兼容Ext2。Ext3是在发布Linux2.4.x版本时加入的,支持保存上TB的文件,保存的文件数量由硬盘容量决定,还支持高达255字节的文件名。
Ext3的内部结构是怎样的呢?Ext3将一个硬盘分区分为大小相同的储存块,每个储存块可以是2个扇区、4个扇区、8个扇区,分别对应大小为1KB、2KB、4KB。
所有的储存块又被划分为若干个块组,每个块组中的储存块数量相同。每个块组前面若干个储存块中,依次放着:超级块、块组描述表、块位图、inode节点位图、inode节点表、数据块区。需要注意的是,超级块和块组描述表是全局性的,在每个块组中它们的数据是相同的。
我再帮你画一个逻辑结构图,你就容易理解了,如下所示:
上图中,第1个储存块是用于安装引导程序,或者也可以保留不使用的。超级块占用一个储存块,在第2个储存块中,即储存块1,储存块的块号是针对整个分区编码的,从0开始。其中的块组描述符表、块位图、inode节点位图、inode节点表的占用大小,是根据块组多少以及块组的大小动态计算的。
下面我们分别讨论这些重要结构。
我们首先要探讨的是Ext3文件系统的超级块,它描述了Ext3的整体信息,例如有多少个inode节点、多少个储存块、储存块大小、第一个数据块号是多少,每个块组多少个储存块等。
Ext3文件系统的超级块存放在该文件系统所在分区的2号扇区,占用两个扇区。当储存块的大小不同时,超级块所在块号是不同的。
比如说,当储存块大小为1KB时,0号块是引导程序或者保留储存块,超级块起始于1号块储存;当块大小为2KB时,超级块起始于0号储存块,其位于0号储存块的后1KB,前1KB是引导程序或者保留;当储存块大小为4KB时,超级块也起始于0号储存块,其位于0号块的1KB处。总之,超级块位于相对于分区的2号~3号扇区,这一点是固定的。
下面我们看一看用C语言定义的超级块,代码如下所示:
struct ext3_super_block {
__le32 s_inodes_count; //inode节点总数
__le32 s_blocks_count; // 储存块总数
__le32 s_r_blocks_count; // 保留的储存块数
__le32 s_free_blocks_count;// 空闲的储存块数
__le32 s_free_inodes_count;// 空闲的inode节点数
__le32 s_first_data_block; // 第一个数据储存块号
__le32 s_log_block_size; // 储存块大小
__le32 s_log_frag_size; // 碎片大小
__le32 s_blocks_per_group; // 每块组包含的储存块数
__le32 s_frags_per_group; // 每块组包含的碎片
__le32 s_inodes_per_group; // 每块组包含的inode节点数
__le32 s_mtime; // 最后挂载时间
__le32 s_wtime; // 最后写入时间
__le16 s_mnt_count; // 挂载次数
__le16 s_max_mnt_count; // 最大挂载次数
__le16 s_magic; // 魔数
__le16 s_state; // 文件系统状态
__le16 s_errors; // 错误处理方式
__le16 s_minor_rev_level; // 次版本号
__le32 s_lastcheck; // 最后检查时间
__le32 s_checkinterval; // 强迫一致性检查的最大间隔时间
__le32 s_creator_os; // 建立文件系统的操作系统
__le32 s_rev_level; // 主版本号
__le16 s_def_resuid; // 默认用户保留储存块
__le16 s_def_resgid; // 默认用户组保留储存块
__le32 s_first_ino; // 第一个非保留inode节点号
__le16 s_inode_size; // inode节点大小
__le16 s_block_group_nr; // 当前超级块所在块组
__le32 s_feature_compat; // 兼容功能集
__le32 s_feature_incompat; // 非兼容功能集
__le32 s_feature_ro_compat;// 只读兼容功能集
__u8 s_uuid[16]; // 卷的UUID(全局ID)
char s_volume_name[16]; // 卷名
char s_last_mounted[64]; // 文件系统最后挂载路径
__le32 s_algorithm_usage_bitmap; // 位图算法
//省略了日志相关的字段
};
以上的代码中我省略了日志和预分配的相关字段,而__le16 __le32,在x86上就是u16、u32类型的数据。le表示以小端字节序储存数据,定义成这样,是为了大小端不同的CPU可以使用相同文件系统,或者已经存在的文件系统的前提下,方便进行数据转换。
接着我们来看看Ext3文件系统的块组描述符,里面存放着用来描述块组中的位图块起始块号、inode节点表起始块号、空闲inode节点数、空闲储存块数等信息,文件系统中每个块组都有这样的一个块组描述符与之对应。所有的块组描述符集中存放,就形成了块组描述符表。
块组描述符表的起始块号位于超级块所在块号的下一个块,在整个文件系统中,存有很多块组描述符表的备份,存在的方式与超级块相同。
下面我们看一看用C语言定义的单个块组描述符结构,如下所示:
struct ext3_group_desc
{
__le32 bg_block_bitmap; // 该块组位图块起始块号
__le32 bg_inode_bitmap; // 该块组inode节点位图块起始块号
__le32 bg_inode_table; // 该块组inode节点表起始块号
__le16 bg_free_blocks_count; // 该块组的空闲块
__le16 bg_free_inodes_count; // 该块组的空闲inode节点数
__le16 bg_used_dirs_count; // 该块组的目录计数
__u16 bg_pad; // 填充
__le32 bg_reserved[3]; // 保留未用
};
对照上述代码,我们可以看到,多个ext3_group_desc结构就形成了块组描述符表,而__le16 __le32类型和超级块中的相同。如果想知道文件系统中有多少个块组描述符,可以通过超级块中总块数和每个块组的块数来进行计算。
接下来要说的是Ext3文件系统的位图块,它非常简单,每个块组中有两种位图块:一种用来描述块组内每个储存块的分配状态,另一种用于描述inode节点的分配状态。
位图块中没有什么结构,就是位图数据,即块中的每个字节都有八个位。每个位表示一个相应对象的分配状态,该位为0时,表示相应对象为空闲可用状态,为1时则表示相应对象是占用状态。例如位图块中第一个字节,表示块组0~7号储存块的分配状态;第二个字节,表示块组8~15号储存块的分配状态 ……依次类推。位图块的块号可以从块组描述符中得到。
接下来,我们再深入研究一下inode节点。上节课我们提过,inode节点用来存放跟文件相关的所有信息,但是文件名称却不在inode节点之中,文件名称保存在文件目录项中。
inode节点中包含了文件模式、文件链接数、文件大小、文件占用扇区数、文件的访问和修改的时间信息、文件的用户ID、文件的用户组ID、文件数据内容的储存块号等,这些重要信息也被称为文件的元数据。
那么,用C语言如何定义单个inode节点结构呢?代码如下所示:
struct ext3_inode {
__le16 i_mode; // 文件模式
__le16 i_uid; // 建立文件的用户
__le32 i_size; // 文件大小
__le32 i_atime; // 文件访问时间
__le32 i_ctime; // 文件建立时间
__le32 i_mtime; // 文件修改时间
__le32 i_dtime; // 文件删除时间
__le16 i_gid; // 建立文件的用户组
__le16 i_links_count; // 文件的链接数
__le32 i_blocks; // 文件占用的储存块 */
__le32 i_flags; // 文件标志
union {
struct {
__u32 l_i_reserved1;
} linux1;
struct {
__u32 h_i_translator;
} hurd1;
struct {
__u32 m_i_reserved1;
} masix1;
} osd1; //操作系统依赖1
__le32 i_block[EXT3_N_BLOCKS];// 直接块地址
__le32 i_generation; // 文件版本
__le32 i_file_acl; // 文件扩展属性块
__le32 i_dir_acl; // 目录扩展属性块
__le32 i_faddr; // 段地址
union {
struct {
__u8 l_i_frag; //段号
__u8 l_i_fsize; //段大小
__u16 i_pad1;
__le16 l_i_uid_high;
__le16 l_i_gid_high;
__u32 l_i_reserved2;
} linux2;
struct {
__u8 h_i_frag; //段号
__u8 h_i_fsize; //段大小
__u16 h_i_mode_high;
__u16 h_i_uid_high;
__u16 h_i_gid_high;
__u32 h_i_author;
} hurd2;
struct {
__u8 m_i_frag; //段号
__u8 m_i_fsize; //段大小
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; //操作系统依赖2
__le16 i_extra_isize;
__le16 i_pad1;
};
这就是inode节点,它包含文件的所有信息。文件的数据内容的储存块号保存在i_block中,这个i_block数组前十二元素保存的是1~12这12个储存块号,第十三个元素开始保存的是一级间接储存块块号、二级间接储存块块号、三级间接储存块块号。
那问题来了,什么是间接储存块?我给你画幅图,你就明白了。
由上图可知,一个inode节点中有11个直接储存块,其中存放的是块号,能直接索引11个储存块。
如果每个储存块大小是1KB的话,可以保存11KB的文件数据;当文件内容大于11KB时,就要用到一级间接储存块。
这时,一级间接储存块里的块号索引的储存块中不是文件数据,而是储存的指向储存块的块号,它可以储存1024/4个块号,即可索引1024/4个储存块。二级、三级间接块则依次类推,只不过级别更深,保存的块号就更多,能索引的储存块就更多,储存文件的数据量就更大。
讲到这里,我们已经对Ext3文件系统若干结构都做了梳理,现在你应该对Ext3文件系统如何储存文件有了一定认识。
可是文件系统中还有许多文件目录,文件目录是怎么处理的呢?
Ext3文件系统把目录当成了一种特殊的文件,即目录文件,目录文件有自己的inode节点,能读取其中数据。在目录文件的数据中,保存的是一系列目录项,目录项用来存放文件或者目录的inode节点号、目录项的长度、文件名等信息。
下面我们看一看,用C语言定义的单个目录项结构长什么样:
#define EXT3_NAME_LEN 255
struct ext3_dir_entry {
__le32 inode; // 对应的inode节点号
__le16 rec_len; // 目录项长度
__u8 name_len; // 文件名称长度
__u8 file_type; // 文件类型:文件、目录、符号链接
char name[EXT3_NAME_LEN];// 文件名
};
目录项结构大小不是固定不变的,这是由于每个文件或者目录的名称,不一定是255个字符,一般情况下是少于255个字符,这就导致name数组不必占用完整的空间。所以目录项是动态变化,需要结构中的rec_len字段,才能知道目录项的真实大小。
今天的课程我们就结束了,我们一起回顾一下学习的重点。
首先为了体验一下怎么建立文件系统,同时为了避免我们在物理硬盘的误操作导致丢失数据,所以我们用文件方式建立了一个虚拟硬盘,并在上面格式化了Ext3文件系统。
接着我们从逻辑上了解Ext3文件系统,重点了解了它的几个重要结构:超级块用于保存文件系统全局信息了;块组描述符用于表示硬盘的一个个块组;位图用于分配储存块和inode节点;inode节点用于保存文件的元数据,还有文件数据块的块号;最后还有目录结构,用来存放者文件或者目录的inode节点号、目录项的长度、文件名等信息。
这节课的导图如下所示,供你参考:
下节课我们继续聊聊怎么读取文件系统的文件,敬请期待。
请问Ext3文件系统的超级块放在硬盘分区的第几个扇区中。
欢迎你在留言区记录自己的收获,或者向我提问。如果觉得这节课还不错,别忘了分享给身边的朋友。