学习参考:《linux文件系统分析 - 欧进利 20815007》
文章目录
主要介绍:fs结构; fs的挂载和文件的查找
文件系统分析
2.1 文件系统的结构
文件系统的结构主要是文件系统的磁盘结构,包括磁盘的组织、数据存储结构、文件和目录在磁盘上的表示。文件系统的磁盘组织决定了fs能管理的磁盘大小以及fs的强壮性、可扩展性(预留)。fs中数据的存储结构、文件及目录的存储结构决定了文件及目录的查找、创建、读写的方式和效率。
2.1.1 文件系统的磁盘组织
ext2首先将逻辑磁盘分成固定大小的逻辑磁盘块(block),然后再将一定数量的块组合成逻辑磁盘组(group),每个块在fs中有一个全局的唯一的编号,知道编号就可以读取对应块的数据或索引节点。
ext2的磁盘布局(逻辑磁盘布局)经历了四个发展阶段:
.
图2.2 只有组号为0及3、5、7的幂次方的组含有超级块和组描述符的备份信息,但bitmap和inode table位置不连续
ext2文件系统的磁盘布局中,基本单位为逻辑磁盘块,块的大小通常为4KB。每个组内所有块的分配情况由一个块大小的位图来表示,因此组的大小为:组大小 = 块大小 * 8 * 块大小 = 128M.
一个块的组描述符所能表示的大小为:块大小/组描述符的大小 * 组大小 = 4096 / 32 * 128M = 16G.
演变原因:
图2.1,在磁盘容量不大的时候,每个组中均有超级块和组描述符备份,浪费的空间也不多。但随着磁盘容量的增加,会浪费不少空间。所以后续只在组号为3、5、7的幂次方中有。
图2.2,应该是设计上的一种考虑不周,导致图2.2所示磁盘布局的罪魁祸首是图2.5所示代码,稍加修改即可得到图2.3所示磁盘布局。
图2.4中预留中若干块存放组描述符,这样当fs磁盘空间不足时,可以在线扩展fs的大小。
2.1.2 文件系统的数据存储结构
VFS角度,同一文件的数据存储在相邻页中;文件系统角度,同一文件的数据在逻辑上是相邻的。逻辑相邻的数据可以存储在物理上也相邻的位置。但文件的连续存储会带来文件的扩展和磁盘碎片问题。
为了支持文件的扩展,ext2采用索引的方式存储。索引是连续存储的,但索引对应的数据块不一定连续。为了在管理小文件上取得较好的性能,同时支持大文件,ext2文件系统采用三级索引。
struct ext2_inode{
__lel6 i_mode; //文件模式
i_uid; //owner uid的低16 bits
i_size; //大小(字节)
i_atime; //访问时间
i_ctime; //创建时间
i_mtime; //修改时间
i_dtime; //删除时间
i_gid; //group id的低16 bits
i_links_count; //链接数
i_blocks; //blocks count
__le32 i_block[EXT2_N_BLOCKS]; //指向数据块的指针
}
EXT2文件系统中索引的分配
#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)
12个直接索引,第13个为一重间接索引,第14个为二重间接索引,第15个为三重间接索引
32位机器上标识一个位置需要32位(4B),4KB块,一个索引块可以索引1024个逻辑磁盘块,那么表示索引块内索引的位置只需要10位(210 = 1024),三级索引共需30位,剩下2位作其他用途。故ext2所支持的单个文件的最大的公式为:
(直接索引 + 一重间接索引 + 二重间接索引 + 三重间接索引) * 块大小 = (12 + 1024 + 1024 * 1024 + 1024 3) * 4KB = 4TB
如何根据索引查找对应的逻辑磁盘块?
创建fs时会对其所在的逻辑磁盘按固定大小进行分块,且根据其位置进行全局编号,fs不需要记录块的编号,块的编号由其位置唯一决定。fs将逻辑磁盘块分配给某一文件后,逻辑磁盘块在文件也有一个唯一的编号,由其在文件中的逻辑位置决定。
(1)逻辑磁盘号
(2)文件中的逻辑编号
假设需要读取文件第2048579个索引对应的逻辑磁盘块,即文件中编号为2048579的数据块。2048579 - (12 + 1024 + 1024 * 1024) = 998967,为三重索引中第998967个索引,998967/1024/1024 = 0,998967/1024 = 975, 998967&(1024-1) = 567,所以要读取的数据块为索引块A中下标为0,索引块B中下标为975,索引块C中下标为567的索引对应的数据块。
完成如上文件中数据块编号到文件系统中逻辑磁盘编号转换过程的代码是:
ext2_block_to_path(struct inode *inode, long i_block, int offsets[4], int *boundary)
其中i_block为文件中数据块的编号,计算得到的下标通过数组offsets返回。
static int ext2_block_to_path(struct inode *inode,
long i_block, int offsets[4], int *boundary)
{
int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb);
int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
const long direct_blocks = EXT2_NDIR_BLOCKS,
indirect_blocks = ptrs,
double_blocks = (1 << (ptrs_bits * 2));
int n = 0;
int final = 0;
if (i_block < 0) {
ext2_msg(inode->i_sb, KERN_WARNING,
"warning: %s: block < 0", __func__);
} else if (i_block < direct_blocks) {
offsets[n++] = i_block;
final = direct_blocks;
} else if ( (i_block -= direct_blocks) < indirect_blocks) {
offsets[n++] = EXT2_IND_BLOCK;
offsets[n++] = i_block;
final = ptrs;
} else if ((i_block -= indirect_blocks) < double_blocks) {
offsets[n++] = EXT2_DIND_BLOCK;
offsets[n++] = i_block >> ptrs_bits;
offsets[n++] = i_block & (ptrs - 1);
final = ptrs;
} else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) {
offsets[n++] = EXT2_TIND_BLOCK;
offsets[n++] = i_block >> (ptrs_bits * 2);
offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1);
offsets[n++] = i_block & (ptrs - 1);
final = ptrs;
} else {
ext2_msg(inode->i_sb, KERN_WARNING,
"warning: %s: block is too big", __func__);
}
if (boundary)
*boundary = final - 1 - (i_block & (ptrs - 1));
return n;
}
2.1.3 文件系统的文件及目录结构
ext2中文件和目录是通过标志位来进行区分。ext2中文件和目录均以目录项和索引节点来表示。(ps:跟我之前理解的不太一样,应该是,所有目录和文件都会有目录项这个东西,而不仅仅是目录有)
目录项结构:(ps:好像是文件和目录都有的一个结构)
struct ext2_dir_entry_2 {
__le32 inode; // Inode number
__le16 rec_len; // 目录项长度,Directory entry length
__u8 name_len; // 文件名或目录名长度,Name length
__u8 file_type; //决定目录对应的是文件还是目录
char name[]; //文件名,file name, up to EXT2_NAME_LEN
};
目录项中name是按4字节对齐的,所以name_len是4的倍数。
磁盘中目录项只用于文件或目录的查找,查找到目录项后,根据其中的inode值读取索引节点,不同目录项的inode值可以指向同一个索引节点。如此一来文件和目录可以使用统一的接口进行操作,区别在于:如果目录项对应的是目录,其inode指向的索引节点的数据块中存储的是目录项;如果对应的是文件,其inode指向的索引节点的数据块中存储的是文件数据。
文件和目录在磁盘上和在内存中均有目录项及其对应的inode表示,只不过磁盘上和内存上的结构不同。目录项在磁盘和内存中分别由struct ext2_dir_entry_2和struct dentry表示,inode则分别由struct ext2_inode和struct inode表示。
为了提高系统性能,应减少磁盘的I/O操作,struct dentry和struct inode在内核中均有一个缓冲队列,使用slab分配器进行分配,根据LRU算法进行回收。
在包含目录项的逻辑磁盘中,最后一个目录项的rec_len不一定是该目录项的长度,因为要保证一个块中所有目录项的rec_len值相加等于块大小,所以需要通过计算得到目录项的准确长度。
debugfs可以查看到文件系统的结构信息
dd if=/dev/sdb of=/tmp/dir_entry count=1 bs=4096 skip=1539,注意DD是按照磁盘块号进行读取
2.2 文件系统挂载
fs的挂载过程就是读取文件系统结构信息的过程,不涉及文件或目录的读取,所有此时需要读取结构信息为文件系统在磁盘上的结构。
ext2的挂载过程,就是读取文件系统超级块和组描述符的过程。
ext2超级块:
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_first_ino; // First non-reserved inode
__le16 s_inode_size; // size of inode structure
__le16 s_block_group_nr; / block group # of this 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 /
__u8 s_prealloc_blocks; / Nr of blocks to try to preallocate*/
__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
__u16 s_padding1;
__u32 s_reserved[190]; // Padding to the end of the block
};
s_first_data_block:表示fs中第一个逻辑磁盘块的编号,挂载时就是从该块读取super block。s_first_data_block的值与fs中逻辑磁盘块的大小有关。因为os可以安装在任何一个有足够空间的分区上,所以每个分区必须预留前1024个字节用作引导块。根据介绍数据缓冲区的分析,fs逻辑磁盘块的大小只能取1024、2048、4096。如果fs的逻辑磁盘块大小等于1024,那么super block将存储在第2个数据块中,此时s_first_data_block的值为1,如果逻辑磁盘块大小不为1024,则super block将存储在第1个数据块起始位置为1024的空间中,此时s_first_data_block的值为0。
s_first_ino表示开始分配给文件或目录索引节点号。ext2中值为11,编号为2的inode对应fs的根目录,此外其他小于s_first_ino的值目前没有使用。
s_inode_size为文件系统分配给每一个inode的空间,其值大于inode实际占用的空间,这样比较好兼容以前的版本。
挂载过程
注意,挂载fs时,系统起初不知道ext2的逻辑磁盘块的大小,为了读取超级块,系统必须假定一个逻辑磁盘块的大小(BLOCK_SIZE宏),因为每个分区的前1024个字节预留作为引导块,所以super block的起始地址为1024,且占用一个块。挂载fs时,系统假定逻辑磁盘块的大小为1024,所以sb存储在第2块即编号1的块中,系统先将编号为1的逻辑磁盘块读入数据缓冲区,然后在bh中读取sb中的一些标志,在进行必要的检查后,接着读取逻辑磁盘块的大小,即s_log_block_size(乘上1024即为逻辑磁盘块大小),知道fs准确的逻辑磁盘块大小后,系统再次将超级块所在的逻辑磁盘块读入数据缓冲区,然后在bh中读取sb的信息,接着读取之后的组描述符。
挂载点切换过程
读取fs结构信息,并将fs挂载到指定的目录后,就可以访问fs中的文件和目录。struct vfsmount表示文件系统的挂载信息。
其中特别注意:mnt_mountpoint表示fs的挂载点(/root/test),而mnt_root表示文件系统本身的根目录,即inode号为2节点所对应的目录。linux中文件系统的根目录和系统的根目录是不一样的,系统根目录为"/",而fs的根目录并没有目录名。其次系统根目录只存在于内存中,而fs的根目录存在与磁盘中。
没有目录名的目录在linux中如何访问?将多个fs同时挂载到相同目录下是如何实现的?如何确定访问的是哪个fs?(mnt_mountpoint和mnt_root实现)
ext2中有一个特殊的目录,即根目录,其inode号为2,且没有目录名,该目录是fs中第一个创建的目录,初始化好fs的磁盘结构后创建,创建过程与目录一样(create_root_dir)。文件系统挂载后,mnt_root所指向的就是文件系统的根目录,在挂载fs的过程中会创建一个struct vfsmount变量(假设为mnt),当将fs挂载到某个目录后(/mnt/sd),目录/mnt/sd就成为一个挂载点,同时mnt中成员mnt_mountpoint指向目录/mnt/sd,成员mnt_root指向被挂载文件系统的根目录,最后将结构变量mnt通过其中的mnt_hash连接到目录/mnt/sd所对应的struct dentry结构中的由d_mounted指向的链表。所以目录所对应的struct dentry中由d_mounted指向链表中保存了挂载到该目录文件系统。
当访问目录/mnt/sd时,由于目录/mnt/sd是一个挂载点(其d_mounted指向链表不为空),系统会进入挂载到目录/mnt/sd下的文件系统的根目录中,所以执行命令cd /mnt/sd后,当前目录为/mnt/sd所对应的struct dentry结构中的由d_mounted指向struct vfsmount类型的变量(mnt)中的mnt_root所指向的目录。如此一来间接访问fs的根目录。
struct vfsmount(mnt变量)
| — mnt_mountpoint成员指向目录/mnt/sd的dentry
| — mnt_root成员指向fs的根目录dentry
| — mnt_hash:连接到/mnt/sd的dentry中d_mounted所指向的链表。即/mnt/sd的dentry的d_mounted指向的链表保存了挂载到该目录的文件系统
有了前面的分析,同时将多个文件系统挂载到相同目录也容易理解。实际上是将后面一次挂载的文件系统挂载到前一次挂载的文件系统的根目录下,如:
mount /dev/sdb /mnt/sd
mount /dev/sdc /mnt/sd
假设设备/dev/sdb和/dev/sdc上分别存在的文件系统为fsb和fsc.执行完第一个命令后,访问/mnt/sd就是访问fsb的根目录,执行第二个命令,实际上就是将文件系统fsc挂载到fsb的根目录上。因此,真正挂载在目录/mnt/sd上的只有文件系统fsb。
详细挂载过程见sys_mount函数。
2.3 文件查找
os中,文件和目录按目录树进行组织。文件查找的实际是根据文件的路径查找对应的目录项,再根据目录项中索引节点的位置,读取inode,最后在内存中建立与文件操作所需要的数据结构如struct dentry和struct file等。
简要回顾ext2文件系统的磁盘结构。ext2首先将逻辑磁盘分成固定大小的逻辑磁盘块,相邻固定数量的逻辑磁盘块组成一个组。每个块根据其在磁盘上的物理位置都有一个唯一的编号。
fs中每个组由备份块、inode alloc bitmap、data alloc bitmap、inode table、数据块组成,且由组描述符描述。ext2中组描述符主要跟踪该组中所有逻辑磁盘块和索引节点的位置及其分配情况。
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];
};
bg_block_bitmap:该组中所有逻辑磁盘块分配情况位图
bg_inode_bitmap:该组中所有inode分配情况的位图
bg_inode_table:该组中存储inode的逻辑磁盘块的起始块号,此后若干连续的逻辑磁盘块均被用来存储索引节点。存储inode的逻辑磁盘块的数量由该组中inode的数量、inode大小和逻辑磁盘块大小决定。inode块后面为数据块,存储文件和目录中的数据。
文件查找例子
可阅读path_init和path_walk函数
例:在当前目录/home/oujinli下查找program/helloc
查找过程如下:
- 读取当前进程中的当前目录作为查找的父目录
- 提取路径中下一个文件名或目录名(program)作为当前目录项的名称。如果当前目录项是路径中最后一项,则步骤4完成后,返回创建的struct dentry或失败
- 根据父目录的inode将父目录中存储的目录项读入数据缓冲区,直至查找到当前目录项或遍历父目录中所有目录项
- 如果父目录中找到当前目录项,则读取所对应的inode,创建与之关联的struct dentry结构,否则结束查找,返回失败。
- 将创建的struct dentry结构对应的目录作为当前目录。如果当前目录是挂载点,则将挂载到当前目录的fs的根目录作为当前目录,直到当前目录不是挂载点
- 将当前目录作为父目录,继续步骤2.
2.4 文件系统中的虚拟化
虚拟化,一方面指虚拟出没有的设备;另一方面还包括将真实存在的设备隐藏起来。
文件系统中的虚拟化主要有以下两个方面:将多个、甚至无限个存储设备虚拟成一个存储设备,从而提供统一无限的存储空间,如图2.25中lvm逻辑卷,图2.26中zfs的存储池;甚至将多个文件系统虚拟为一个文件系统,从而管理多台计算机上的存储设备,这就是分布式文件系统。