太久没有写文章了,有时候是因为懒,有时候是觉得理解的不够透彻,更多的是因为感觉文笔太差。。

今天就来聊聊文件系统,这也是个庞然大物,先说说现在流行的分布式存储把,现在分布式存储主要分为对象存储,块存储,和文件存储,这些存储其实单机本来就有的,不过是云计算厂商在云上提供了这些服务,把单机的存储转到了云上,准确的说其实应该叫云上的分布式对象存储、块存储、文件存储。

块存储要从Linux系统设备说起,这是Unix系统遗留下来的传统,也是一种优秀的设计思想,把所有的东西都看成文件,设备当然也是一种文件咯,根据读写方式的不同分为:块设备(按照Block为单位读写),字符设备(可以按照offset以字节为单位读写),以及其他设备。在虚拟文件系统里,下一层是就是块设备层,块设备层会调用驱动程序对硬盘进行读写,而对VFS提供的这个块设备的服务抽出来,拿到云上去做就叫块存储。

对象存储其实就是KV存储,因为在比如Java这种语言里,value存储的往往是一个对象的序列化后的数据,所以叫对象存储(理解的不对请告诉我!)。

文件存储指的是提供符合POSIX标准的存储服务,在Linux用户态下,想要对文件进行读写操作,必须通过系统提供的接口,比如read,write,open等,而POSIX标准就是一个针对操作系统提供的接口的标准,也就说说能够提供read,write,open等接口的存储服务就是文件存储。

文件系统

其实文件系统茫茫多,ext,ext2,ext3,ext4等等等,看看linux/fs下面的模块就知道了,为了统一这些模块,linux抽象出了虚拟文件系统VFS,VFS把文件抽象成了inode,每个inode都会关联一组函数指针,这一组函数指针就是不用的文件系统具体的读写操作。

Linux启动时会挂载一个根目录文件系统,这个由你启动参数指定,启动过程中还有一些特殊的文件系统,比如sockfs,tmpfs,procfs等等,这些文件系统都会被挂载到一个挂载树上,挂载树的根就是/,他是这个系统这次启动的根目录,你可以在这棵树上继续挂载新的节点,每个节点都会针对一个具体的文件系统。

当你做一个操作时,比如open(“/foo/bar”),VFS首先会在挂载树中查找挂载点,然后从挂载点找到对应的inode,从这个inode开始对剩余的路径继续进行相应的操作。

EXT2文件系统

EXT2和EXT3是兼容的是个简单的文件系统,很适合来学习,下面我们来剖析一下EXT2文件系统。

上面说到了,块设备就是按照块来读写的设备,那么EXT2就是决定如何将数据存储到对应的块设备上去的系统。

文件系统其实很简单,只有文件和目录,目录里可能会包含目录和文件,是一个树状的结构,而文件可能会存储数据,数据从几KB到几GB不等,所以要选择合适的文件系统,就首先要确定你的存储场景。而这么多的文件系统其实就是针对不同的场景经过权衡取舍的结果。

EXT2文件系统将许多Blocks分成多个Block Group,每个Block Group都有一个Group Descriptor,这些Group Descriptor都在一起,位于SuperBlock的后面,而每个Group Descriptor都含有block bitmap,inode bitmap,inode table等。

SuperBlock

对于EXT2文件系统来说,首先就是SuperBlock,SuperBlock是一个特殊的块,它记载着本文件系统全局的属性信息,位于磁盘起始的1024字节处,假如BlockSize是1024字节,它就位于第二个Block,假如BlockSize是4096字节,那它位于第一个Block,BlockSize在创建具体的文件系统时指定。

/*
 * Structure of the super block
 */
struct ext2_super_block {
	unsigned long  s_inodes_count;	/* Inodes count */
	unsigned long  s_blocks_count;	/* Blocks count */
	unsigned long  s_r_blocks_count;/* Reserved blocks count */
	unsigned long  s_free_blocks_count;/* Free blocks count */
	unsigned long  s_free_inodes_count;/* Free inodes count */
	unsigned long  s_first_data_block;/* First Data Block */
	unsigned long  s_log_block_size;/* Block size */
	long           s_log_frag_size;	/* Fragment size */
	unsigned long  s_blocks_per_group;/* # Blocks per group */
	unsigned long  s_frags_per_group;/* # Fragments per group */
	unsigned long  s_inodes_per_group;/* # Inodes per group */
	unsigned long  s_mtime;		/* Mount time */
	unsigned long  s_wtime;		/* Write time */
	unsigned short s_mnt_count;	/* Mount count */
	short          s_max_mnt_count;	/* Maximal mount count */
	unsigned short s_magic;		/* Magic signature */
	unsigned short s_state;		/* File system state */
	unsigned short s_errors;	/* Behaviour when detecting errors */
	unsigned short s_pad;
	unsigned long  s_lastcheck;	/* time of last check */
	unsigned long  s_checkinterval;	/* max. time between checks */
	unsigned long  s_reserved[238];	/* Padding to the end of the block */
};

比较关键的有s_blocks_per_group和s_log_block_size,这就确定了磁盘分区上有多少个block group。

BlockDescriptor

Block Descriptor紧跟SuperBlock,Block Descripotr的数量可以通过公式 (s_blocks_count - s_first_data_block - 1) / s_blocks_per_group + 1算出,而且每个BlockDescriptor的大小必须不能超过BlockSize,所以我们可能一下将所有的Block Descriptor读出来。

struct ext2_group_desc
{
	unsigned long  bg_block_bitmap;		/* Blocks bitmap block */
	unsigned long  bg_inode_bitmap;		/* Inodes bitmap block */
	unsigned long  bg_inode_table;		/* Inodes table block */
	unsigned short bg_free_blocks_count;	/* Free blocks count */
	unsigned short bg_free_inodes_count;	/* Free inodes count */
	unsigned short bg_used_dirs_count;	/* Directories count */
	unsigned short bg_pad;
	unsigned long  bg_reserved[3];
};

Inode

虚拟文件系统我们就提到Inode了,这里的Inode其实像是虚拟文件系统的Inode序列化后存放到磁盘中的Inode,每一个目录或者文件都会对应一个Inode。

根目录的Inode是固定的,从根目录开始对于特定Inode的读取,首先要知道Inode的inode号,然后用inode号除以SuperBlock中的s_inodes_per_group得到在哪个Block Descriptor,通过余数得到在这个BlockDescriptor里的哪个inode,从inode table中读取对应的inode。

/*
 * Structure of an inode on the disk
 */
struct ext2_inode {
	unsigned short i_mode;		/* File mode */
	unsigned short i_uid;		/* Owner Uid */
	unsigned long  i_size;		/* Size in bytes */
	unsigned long  i_atime;		/* Access time */
	unsigned long  i_ctime;		/* Creation time */
	unsigned long  i_mtime;		/* Modification time */
	unsigned long  i_dtime;		/* Deletion Time */
	unsigned short i_gid;		/* Group Id */
	unsigned short i_links_count;	/* Links count */
	unsigned long  i_blocks;	/* Blocks count */
	unsigned long  i_flags;		/* File flags */
	unsigned long  i_reserved1;
	unsigned long  i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
	unsigned long  i_version;	/* File version (for NFS) */
	unsigned long  i_file_acl;	/* File ACL */
	unsigned long  i_dir_acl;	/* Directory ACL */
	unsigned long  i_faddr;		/* Fragment address */
	unsigned char  i_frag;		/* Fragment number */
	unsigned char  i_fsize;		/* Fragment size */
	unsigned short i_pad1;
	unsigned long  i_reserved2[2];
};

对文件的读写其实就是对于inode的读写,inode是如何存放连续的数据的呢,答案就在i_block里。

首先每个inode都有15个block指针,前12个指针是direct pointer,直接指向一个具体的block。

假设这些block用完了之后,第13个指针是indirect pointer,它指向的是block的指针。

第十四个指针是double indirect pointer,它指向的是block指针的指针。

第十五个指针是triple indirect pointer,它指向的是block指针的指针的指针。

通过这样多级指针的设计我们可以把某个文件扩展的很大,而在文件较小时也不影响它的性能。

direntry

direntry是一种固定大小的存储格式,它代表一个具体的目录,它的inode内容就是它所包含的子目录或者文件。

struct ext2_dir_entry {
	unsigned long  inode;			/* Inode number */
	unsigned short rec_len;			/* Directory entry length */
	unsigned short name_len;		/* Name length */
	char           name[EXT2_NAME_LEN];	/* File name */
};