Linux文件系统(二)磁盘文件系统

Linux文件系统

Linux文件系统(一)文件系统概述

Linux文件系统(二)磁盘文件系统

Linux文件系统(三)虚拟文件系统

Linux文件系统(四)文件缓存

Linux文件系统(二)磁盘文件系统


我们常见的磁盘长下面这样子,左边中间圆是磁盘的盘片,右边是抽象出来的图

img

每一层有多个磁道,每个磁道有多个扇区,每个扇区大小为512个字节

文件系统是安装在磁盘之上的,本文将讲解Linux主流的文件系统 —— ext系列的文件系统格式

一、inode与块的存储

为了方便管理,我们将硬盘分为大小相同的单元,称之为,每个块的大小为扇区的整数倍,常见为4K。在格式化的时候,这个值是可以设置的

扫描二维码关注公众号,回复: 9213074 查看本文章

一大块硬盘被分为一个一个的块,这样子,我们存放文件的时候,就不需要分配一段连续的空间,可以将其分为一块一块存储,然后记录好相应的位置,方便添加、删除和插入数据

为了维护这些文件的存储信息,我们需要建立一个索引区域,来方便查找文件的存储位置。另外,文件还有元数据部分,例如名字、权限等信息,这些信息存放在inode结构中

什么是inode?i 表示 index,“索引”,inode 就表示索引节点,每个文件都会对应一个 inode,一个文件夹也是一个文件,所以也对应一个 inode

inode 在内核中的定义如下

struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Inode Change 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_lo;	/* Blocks count */
	__le32	i_flags;	/* File flags */
......
	__le32	i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
	__le32	i_generation;	/* File version (for NFS) */
	__le32	i_file_acl_lo;	/* File ACL */
	__le32	i_size_high;
......
};

  • i_mode:文件的读写权限
  • i_uid:属于哪个用户
  • i_gid:属于哪个组
  • i_size_lo:文件大小
  • i_blocks_lo:文件占了多少块

ls命令获取文件信息,就是从这里面取出来的

  • i_atime:Access time 最近一次访问的时间
  • i_ctime:最近一个修改文件的时间(修改文家属性或者内容都会改变)
  • i_mtime:最近一次修改文件内容的时间

上面说的,“每个文件会被分为几块,这些块的位置在哪里“,这些都保存在 inode 中的 i_block 中

具体是如何保存的?先看 EXT4_N_BLOCKS 的定义,总共有 15 项

#define	EXT4_NDIR_BLOCKS		12
#define	EXT4_IND_BLOCK			EXT4_NDIR_BLOCKS
#define	EXT4_DIND_BLOCK			(EXT4_IND_BLOCK + 1)
#define	EXT4_TIND_BLOCK			(EXT4_DIND_BLOCK + 1)
#define	EXT4_N_BLOCKS			(EXT4_TIND_BLOCK + 1)

在 ext2 和 ext3 中,前12项直接保存文件存储的块的位置,也就是直接通过 i_block[0-11] 就可以直接得到存储文件内容块的位置

img

一个块的大小默认是 4K,显然,如果文件太大,12块是放不下的。当我们放在 i_block[12] 的时候,就不能直接存放数据块的位置了,这时候我们将其指向一个块,但是这个块不存放文件内容,而是存放其它数据块的位置,这个块我们称之为 间接块

接下俩的 i_block[13] 和 i_block[14] 也是一样的道理,分别是 二次间接三次间接,通过这样的方式,一个 inode 可以指向的数据块空间就非常大了

但是这里有一个显著的缺点,对于大文件,因为间接块的缘故,需要多次读取磁盘,才能找到相应的块,这样子访问速度就比较慢

这样子,ext4 做了一定的改进,引入了一个新概念,叫做 Extents

下面来解释以下 Extents

假设一个文件有128M,那么就需要 32K 个块。如果按照 ext2 和 ext3 这样的方式存放,那么数据块会非常分散,数量非常多。但是 Extents 可以存放连续的块,也就是我们可以在128M的连续空间记录到一个 Extents里面。这样子,文件的读写性能就提高了,文件碎片就减少了

Extents 是如何保存数据块的,它起始存储成一棵树

img

树有一个一个的节点,有分支节点,也有叶子节点。每个节点都有一个头部,叫做 ext4_extent_header,定义如下

struct ext4_extent_header {
	__le16	eh_magic;	/* probably will support different formats */
	__le16	eh_entries;	/* number of valid entries */
	__le16	eh_max;		/* capacity of store in entries */
	__le16	eh_depth;	/* has tree real underlying blocks? */
	__le32	eh_generation;	/* generation of the tree */
};

eh_entries 表示这个节点里面有多少项

这里的项分为两种,如果是叶子节点,那么节点里的项会指向硬盘上的连续块的地址,我们称为数据节点 ext4_extent;如果是分支节点,那么节点里的项就会指向分支节点或者叶子节点,我们称之为索引节点 ext4_extent_idx。这两种类型的项大小都是12字节,定义如下

/*
 * This is the extent on-disk structure.
 * It's used at the bottom of the tree.
 */
struct ext4_extent {
	__le32	ee_block;	/* first logical block extent covers */
	__le16	ee_len;		/* number of blocks covered by extent */
	__le16	ee_start_hi;	/* high 16 bits of physical block */
	__le32	ee_start_lo;	/* low 32 bits of physical block */
};
/*
 * This is index on-disk structure.
 * It's used at all the levels except the bottom.
 */
struct ext4_extent_idx {
	__le32	ei_block;	/* index covers logical blocks from 'block' */
	__le32	ei_leaf_lo;	/* pointer to the physical block of the next *
				 * level. leaf or next index could be there */
	__le16	ei_leaf_hi;	/* high 16 bits of physical block */
	__u16	ei_unused;
};

其中一个 ext4_extent 就可以指向128MB连续的数据块

在 ext4_inode 中,i_block 是一个15个32位元素的数据,总共有60个字节

而 ext4_extent_header 大小为12字节,ext4_extent 的大小为12字节

所以 单纯靠 inode 中的 i_block 就可以存储1个 ext4_extent_header 和4个 ext4_extent

而1个 ext4_extent 就可以指向一大片连续的块,所以对于文件不大,这样子完全足够;如果文件太大,没有足够的连续块空间,那么就需要裂变成一棵树

除了根节点,其他节点都存放在一个块 4K 里面,4K 扣除 ext4_extent_header 的12个字节,还能存储340项,每个 ext4_extent 可以指向 128MB 连续的数据块,所以340个 extent 能够表达的最大小为 42.5 G,这已经非常大了

二、inode 位图和块位图

到这里,我们知道了,磁盘中肯定有一系列的 inode 和一系列的块排列起来

接下来的问题是,如果要保存一个 inode 或者一个数据块,应该存放到磁盘上的哪个位置?拿到要将所有的 inode 列表和块列表扫面一遍,然后找个空位放吗?

显然,这样子的效率是非常低的。所以在文件系统中,我们需要专门弄一块来保存 inode 的位图。这这4k里面,每一位对应一个 inode,如果为1,就表示这个 inode 被占用,如果为0,就表示没被用。同样的,也弄了一块来保存 block 的位图

接下来看位图在Linux内核中是如何起作用的。上一篇文章讲过,如果创建一个新文件,通过调用 open,然后指定参数 O_CREAT。这表示当文件找不到的时候,就创建一个。open 是一个系统调用,对应内核的 sys_open,定义如下

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;


	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

这里重点看对于 inode 的操作

调用链为:do_sys_open -> do_filp_open -> path_openat -> do_last -> lookup_open,这个调用链的逻辑是,要打开一个文件,先根据路径找文件夹,如果发现文件夹下面没有这个文件,同时又设置了 O_CREAT,就说明我们需要在这个文件夹下面创建一个文件,这就需要一个新的 inode

static int lookup_open(struct nameidata *nd, struct path *path,
			struct file *file,
			const struct open_flags *op,
			bool got_write, int *opened)
{
......
	if (!dentry->d_inode && (open_flag & O_CREAT)) {
......
		error = dir_inode->i_op->create(dir_inode, dentry, mode,
						open_flag & O_EXCL);
......
	}
......
}

创建新的 inode,需要调用 dir_inode->i_op->create。它的定义如下

const struct inode_operations ext4_dir_inode_operations = {
	.create		= ext4_create,
	.lookup		= ext4_lookup,
	.link		= ext4_link,
	.unlink		= ext4_unlink,
	.symlink	= ext4_symlink,
	.mkdir		= ext4_mkdir,
	.rmdir		= ext4_rmdir,
	.mknod		= ext4_mknod,
	.tmpfile	= ext4_tmpfile,
	.rename		= ext4_rename2,
	.setattr	= ext4_setattr,
	.getattr	= ext4_getattr,
	.listxattr	= ext4_listxattr,
	.get_acl	= ext4_get_acl,
	.set_acl	= ext4_set_acl,
	.fiemap         = ext4_fiemap,
};

这里定义了文件夹 inode 的操作集,create 对应 ext4_create

接下来的调用链是:ext4_create -> ext4_new_inode_start_handle -> __ext4_new_node。在 __ext4_new_node 函数中,会创建新的 inode

struct inode *__ext4_new_inode(handle_t *handle, struct inode *dir,
			       umode_t mode, const struct qstr *qstr,
			       __u32 goal, uid_t *owner, __u32 i_flags,
			       int handle_type, unsigned int line_no,
			       int nblocks)
{
......
inode_bitmap_bh = ext4_read_inode_bitmap(sb, group);
......
ino = ext4_find_next_zero_bit((unsigned long *)
					      inode_bitmap_bh->b_data,
					      EXT4_INODES_PER_GROUP(sb), ino);
......
}

这里的逻辑是,从文件系统读取 inode 位图,找到下一个0,也就下一个空闲的 inode

对于 block 位图,在写入文件的时候,也有这个过程

三、文件系统的格式

看起来,我们可以 inode 位图和 block 位图来创建文件了。但是如果仔细算一算,还是有问题

数据块的位图存放在一个块里面,也就是4K大小,总共 4*1024*8 = 2^15 个位。一个数据块大小为 4K,所以可以表示 2^15 * 4K = 128M,也就是一个块的位图最多只能表示128M大小的连续数据块

这是否太小了,所以又分为一个一个的块组,每个块组里面有 一个块的位图 + 一系列的块 外加 一个块的 inode 位图 + 一系列的inode结构,大小最大为128M

对于块组,我们需要一个数据结构来表示(ext4_group_desc)。这里面有 inode 位图(bg_inode_bitmap_lo)、块位图(bg_block_bitmap_lo)、inode 列表(bg_inode_table_lo)

这样一个一个的块组,就基本构成了整个文件系统的结构。因为块组有多个,所以块组描述符也组成了一个列表,我们称之为块组描述符

当然,我们还需要一个数据结构,对整个文件系统进行描述,这个就是超级块(ext4_super_block)。这里里面有整个文件系统一共有多少个 inode(s_inodes_count);一共有多少块(s_block_count_lo);每个块组有多少个 inode(s_inodes_pre_group);每个块组有多少块(s_blocks_per_group)等。这些都是全局信息

如果是一个启动盘,我们需要预留一块区域作为引导区,所以第一个块组前面要预留 1K,用于启动引导区

最终,整个文件系统就变成下面这样

img

这里需要注意的是,超级块和块组描述符都是比较重要的全局信息,如果这些信息损坏,那比损坏一个文件严重多了。所以对于这两部分数据,需要做好备份,但是采用不同的备份策略

默认情况下,超级块和块组都有副本保存在每一个块组里面

如果开启 sparse_super 特性,超级块和块组描述符的副本只会保存在块组索引为 0、3、5、7 的整数幂里

对于超级块来说,超级块不是很大,所以备份多也没关系

但是对于块组描述符表来讲,如果每个块组里面都保存一份块组描述符表,一方面是浪费空间;一方面是由于一个块组最大128M,而块组描述符有多少项,就限制了有多少个块组,整个文件系统的大小 = 128M * 块组总数,这样子,文件系统的大小就被限制住了

我们改进的思路就是引入 Meta Block Groups 特性

首先,块组描述符表不会保存所有块组的描述符了,而是讲块组分为多个组,我们称为元块组(Meta Block Groups)。一个元块组包含64个块组,也就是一个元块组的块组描述符表仅仅包含64个块组描述符

假设一共有256个块组,原来是一整个块组描述符表有256项,现在是分为4个元块组,每个元块组有64个块组,所以每个元块组的块组描述符表就有64个块组描述符,这就小得多了,而且这四个元块组是自己备份自己的

img

图中,每个元块组包含64个块组,其中有其对应的块组描述符表,分别在该元块组的第一项和第二项以及最后一项备份

这样可以发挥 ext4 的48位块寻址的优势了,在超级块 ext4_super_block 的定义中,我们可以看到块寻址的高位和低位,均为32位,其中有用的是48位,2^48个块是 1EB

struct ext4_super_block {
......
	__le32	s_blocks_count_lo;	/* Blocks count */
	__le32	s_r_blocks_count_lo;	/* Reserved blocks count */
	__le32	s_free_blocks_count_lo;	/* Free blocks count */
......
	__le32	s_blocks_count_hi;	/* Blocks count */
	__le32	s_r_blocks_count_hi;	/* Reserved blocks count */
	__le32	s_free_blocks_count_hi;	/* Free blocks count */
......
}

四、目录的存储形式

通过前面的描述,我们知道一个普通文件是如何存储的。有一类特殊的文件 —— 目录,它是如何保存的呢?

起始目录也是个文件,也有 inode,inode 也指向一些块。和普通文件不同的是,普通文件的块保存的是文件的信息,而目录文件的块保存的是一项一项的文件信息。这些信息我们称为 ext4_dir_entry

从代码看,有两个版本,在成员变量几乎没有差别,只不过第二个版本 ext4_dir_entry_2 是讲一个 16位的 name_len,变成了一个8位的 name_len 和 8位的 file_type

struct ext4_dir_entry {
	__le32	inode;			/* Inode number */
	__le16	rec_len;		/* Directory entry length */
	__le16	name_len;		/* Name length */
	char	name[EXT4_NAME_LEN];	/* File name */
};
struct ext4_dir_entry_2 {
	__le32	inode;			/* Inode number */
	__le16	rec_len;		/* Directory entry length */
	__u8	name_len;		/* Name length */
	__u8	file_type;
	char	name[EXT4_NAME_LEN];	/* File name */
};

在目录文件的块中,最简单的保存格式就是列表,就是一项一项地讲 ext4_dir_entry_2 列在那里

每一项都保存这文件名 和 inode 编号,通过这个 inode 编号就可以找到文件对应的真正的 inode,然后就可以查看文件了

第一项为 “.”,表示当前目录;第二项为 “…”,表示上一级目录

有时候目录下的文件太多,一项一项去查找太慢了,于是我们添加了索引模式

如果 inode 中设置了 EXT4_INDEX_FL 标志,则目录文件的块的组织形式就会发生变化,如下定义

struct dx_root
{
	struct fake_dirent dot;
	char dot_name[4];
	struct fake_dirent dotdot;
	char dotdot_name[4];
	struct dx_root_info
	{
		__le32 reserved_zero;
		u8 hash_version;
		u8 info_length; /* 8 */
		u8 indirect_levels;
		u8 unused_flags;
	}
	info;
	struct dx_entry	entries[0];
};

首先出现的还是差不多,第一项是 ”.“,表示当前目录,第二项是 ”…“,表示上一级目录,这两个是不变的。接下来开始发生变化,是一个 dx_root_info 的结构,其中重要的成员是 indirect_levels,表示间接索引的层次

接下来就是索引项 dx_entry,定义如下

struct dx_entry
{
	__le32 hash;
	__le32 block;
};

查找文件时,首先通过名称获取哈希值,如果哈希能够匹配上,表明这个文件的信息在相应的块中,然后打开这个块,如果里面不在是索引,而是索引树的叶子节点的话,那么这里面就是一项一项的 ext4_dir_entry_2,只需要按照文件名来查找就行

img

五、软链接和硬链接的存储格式

所谓的链接,可以理解为文件的别名,链接有两种,软链接和硬链接,可以通过下面命令创建

 ln [参数][源文件或目录][目标文件或目录]

ln -s 是创建软链接,不带 -s 是创建硬链接,它们有什么区别呢?

首先查找文件首先是通过目录文件的文件列表进行查找的

对于硬链接,只是在目录文件的文件列表中增加一项,而这一项的 inode 指向原文件的 inode,并没有创建新的inode,这种方式是无法跨文件系统的

对于软链接,在目录文件的文件列表中增添一项,然后再创建一个新的 inode,inode 的数据块里的内容是指向另一个文件,这种方式可以跨文件系统,并且原文件被删除,那么软链接文件还存在

img

六、总结

对于我们最常用到的两个概念是,一个是 inode,一个是数据块

无论是文件还是目录,都是一个文件,它们都有自己的 inode,每个 inode 指向相应的数据块

普通文件的数据块存放着文件信息

目录文件的数据块存放的是一项一项的文件信息,每一项都包含 文件名 和 inode 编号,用于找到文件对应的 inode

在这里插入图片描述

发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102531706