• Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统


    注:本分类下文章大多整理自《深入分析linux内核源代码》一书,另有参考其他一些资料如《linux内核完全剖析》、《linux c 编程一站式学习》等,只是为了更好地理清系统编程和网络编程中的一些概念性问题,并没有深入地阅读分析源码,我也是草草翻过这本书,请有兴趣的朋友自己参考相关资料。此书出版较早,分析的版本为2.4.16,故出现的一些概念可能跟最新版本内核不同。

    此书已经开源,阅读地址 http://www.kerneltravel.net


    一、Ext2 文件系统

    (一)、文件系统布局



    文件系统中存储的最小单位是块( Block),一个块究竟多大是在格式化时确定的,例如 mke2fs的 -b选项可以设定块大小为 1024、 2048或 4096字节。而上图中引导块/自举块( Boot Block)的大小是确定的,就是 1KB,引导块是由 PC标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是 ext2文件系统的开始, ext2文件系统将整个分区划成若干个同样大小的块组( Block Group),每个块组都由以下部分组成。

    超级块(Super Block

    描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次 mount的时间等等。超级块在每个块组的开头都有一份拷贝。

    块组描述符表(GDT, Group Descriptor Table

    由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符( Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是 inode表,从哪里开始是数据块,空闲的 inode和数据块还有多少个等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此它们都有多份拷贝。通常内核只用到第个块组中的拷贝,当执行 e2fsck检查文件系统一致性时,第 0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第 0个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。

    块位图(Block Bitmap

    一个块组中的块是这样利用的:数据块存储所有文件的数据,比如某个分区的块大小是 1024字节,某个文件是 2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块、块组描述符表、块位图、 inode位图、 inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个 bit代表本块组中的一个块,这个 bit为 1表示该块已用,这个 bit为 0表示该块空闲可用。

    为什么用df命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即可,而不需要搜遍整个分区。相反,用 du命令查看一个较大目录的已用空间就非常慢,因为不可避免地要搜遍整个目录的所有文件。

    与此相联系的另一个问题是:在格式化一个分区时究竟会划出多少个块组呢?主要的限制在于块位图本身必须只占一个块。用 mke2fs格式化时默认块大小是 1024字节,可以用 -b参数指定块大小,现在设块大小指定为 b字节,那么一个块可以有 8b个 bit,这样大小的一个块位图就可以表示 8b个块的占用情况,因此一个块组最多可以有 8b个块,如果整个分区有 s个块,那么就可以有 s/(8b)个块组。格式化时可以用 -g参数指定一个块组有多少个块,但是通常不需要手动指定, mke2fs工具会计算出最优的数值。 

    inode 位图(inode Bitmap

    和块位图类似,本身占一个块,其中每个 bit表示一个 inode是否空闲可用。

    inode 表(inode Table

    我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建 /修改 /访问时间等,也就是 ls -l命令看到的那些信息,这些信息存在 inode中而不是数据块中。每个文件都有一个 inode,一个块组中的所有 inode组成了 inode表。

    inode表占多少个块在格式化时就要决定并写入块组描述符中, mke2fs格式化工具的默认策略是一个块组有多少个 8KB就分配多少个 inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个 8KB就分配多少个 inode,换句话说,如果平均每个文件的大小是 8KB,当分区存满的时候 inode表会得到比较充分的利用,数据块也不浪费。如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候 inode会有一些浪费,如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完 inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用 mke2fs的 -i参数手动指定每多少个字节分配一个 inode

    数据块(Data Block

    根据不同的文件类型有以下几种情况:


    对于常规文件,文件的数据存储在数据块中。

    对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外, ls -l命令看到的其它信息都保存在该文件inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。

    对于符号链接,如果目标路径名较短则直接保存在 inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。

    设备文件、FIFOsocket 等特殊文件没有数据块,即文件大小为0,设备文件的主设备号和次设备号保存在 inode中。


    Ext2 文件系统加上日志支持的下一个版本是 ext3 文件系统,它和 ext2 文件系统在硬盘布局上是一样的,其差别仅仅是 ext3 文件系统在硬盘上多出了一个特殊的 inode(可以理解为一个特殊文件),用来记录文件系统的日志,也即所谓的 journal。
    Ext4 支持更大的文件和无限量的子目录。

    On an ext3 file-system 31,998 subdirectories is the limit. Each subdirectory is considered a “link” by the system and this is where we get the somewhat cryptic “too many links” message.

    By the way, there is technically a limit of 32,000 subdirectories but each directory always includes two links – one to reference itself and another to reference the parent directory – that leaves us with 31,998 to work with.

    consider bucketing your directories into a multi-level hashed structure, to reduce the number of directories you need to store at a particular "layer".In the case of hashes, a similar technique can be used, take the first 4 character of the file "ABCDEFG.txt" and put it in "AB/CD/ABCDEFG.txt"

    (二)、数据块寻址

    如果一个文件有多个数据块,这些数据块很可能不是连续存放的,应该如何寻址到每个块呢?事实上,每个文件的inode的索引项一共有 15个,从 Blocks[0]到 Blocks[14],每个索引项占 4字节。前 12个索引项都表示块编号,例如若Blocks[0]字段保存着 24,就表示第 24个块是该文件的数据块,如果块大小是 1KB,这样可以表示从 0字节到 12KB的文件。如果剩下的三个索引项 Blocks[12]到 Blocks[14]也是这么用的,就只能表示最大 15KB的文件了,这是远远不够的,事实上,剩下的三个索引项都是间接索引。

    索引项Blocks[12]所指向的块并非数据块,而是称为间接寻址块( Indirect Block),其中存放的都是类似 Blocks[0]这种索引项,再由索引项指向数据块。设块大小是 b,那么一个间接寻址块中可以存放 b/4个索引项,指向 b/4个数据块。所以如果把 Blocks[0]到 Blocks[12]都用上,最多可以表示 b/4+12个数据块,对于块大小是 1K的情况,最大可表示 268K的文件。如下图所示,注意文件的数据块编号是从 0开始的, Blocks[0]指向第 0个数据块, Blocks[11]指向第 11个数据块, Blocks[12]所指向的间接寻址块的第一个索引项指向第 12个数据块,依此类推。


    从上图可以看出,索引项 Blocks[13]指向两级的间接寻址块,总共最多可表示 (b/4)^2 +b/4+12个数据块,对于 1K的块大小最大可表示 64.26MB的文件。索引项 Blocks[14]指向三级的间接寻址块,总共最多可表示 (b/4)^3 +(b/4)^+b/4+12个数据块,对于 1K的块大小最大可表示 16.06GB的文件。

    可见,这种寻址方式对于访问不超过 12个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读 inode(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作: inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的 inode(索引节点高速缓存)和数据块(块高速缓存)往往已经被内核缓存了,读大文件的效率也不会太低。 
      

     

     二、VFS 虚拟文件系统

    Linux支持各种各样的文件系统格式,如 ext2、 ext3、 reiserfs、 FAT、 NTFS、 iso9660等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以 mount到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用 ls命令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢? Linux内核在各种不同的文件系统格式之上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来用起来都一样,这个抽象层称为虚拟文件系统(VFSVirtual File System)。

    VFS 是应用程序和具体的文件系统之间的一个层。不过,在某些情况下,一个文件操作可能由VFS 本身去执行,无需调用下一层程序。例如,当某个进程关闭一个打开的文件时,并不需要涉及磁盘上的相应文件,因此,VFS 只需释放对应的文件对象。类似地,如果系统调用lseek()修改一个文件指针,而这个文件指针指向有关打开的文件与进程交互的一个属性,那么VFS 只需修改对应的文件对象,而不必访问磁盘上的文件,因此,无需调用具体的文件系统子程序。从某种意义上说,可以把VFS 看成“通用”文件系统,它在必要时依赖某种具体的文件系统。



    每个进程在PCBProcess Control Block)中都保存着一个指向文件描述符表的指针(struct files_struct* files),文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用 file结构体表示,文件描述符表中的指针指向 file结构体。

    file结构体中维护 File Status Flag( file结构体的成员 f_flags)和当前读写位置( file结构体的成员 f_pos)。在上图中,进程 1和进程 2都打开同一文件,但是对应不同的 file结构体,因此可以有不同的 File Status Flag和读写位置。 file结构体中比较重要的成员还有 f_count,表示引用计数( Reference Count),dup、 fork等系统调用会导致多个文件描述符指向同一个 file结构体,例如有 fd1和 fd2都引用同一个 file结构体,那么它的引用计数就是 2,当 close(fd1)时并不会释放 file结构体,而只是把引用计数减到 1,如果再 close(fd2),引用计数就会减到 0同时释放 file结构体,这才真的关闭了文件。

    每个file结构体都指向一个 file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中 read一个文件描述符, read通过系统调用进入内核,然后找到这个文件描述符所指向的 file结构体,找到 file结构体所指向的 file_operations结构体,调用它的 read成员所指向的内核函数以完成用户请求。在用户程序中调
    lseekreadwriteioctlopen等函数,最终都由内核调用 file_operations的各成员所指向的内核函数完成用户请求。 file_operations结构体中的 release成员用于完成用户程序的 close请求,之所以叫 release而不叫 close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到 0才关闭文件。对于同一个文件系统上打开的常规文件来说, read、 write等文件操作的步骤和方法应该是一样的,调用的函数应该是相同的,所以图中的三个打开文件的 file结构体指向同一个 file_operations结构体。如果打开一个字符设备文件,那么它的 read、 write操作肯定和常规文件不一样,不是读写磁盘的数据块而是读写硬件设备,所以 file结构体应该指向不同的 file_operations结构体,其中的各种文件操作函数由该设备的驱动程序实现。

    每个file结构体都有一个指向 dentry结构体的指针, “dentry”是 directory entry(目录项)的缩写。我们传给 open、 stat等函数的参数的是一个路径,例如 /home/akaedu/a,需要根据路径找到文件的 inode。为了减少读盘次数,内核缓存了目录的树状结构,称为 dentry cache(目录高速缓存),其中每个节点是一个 dentry结构体,只要沿着路径各部分的 dentry搜索即可,从根目录 /找到 home目录,然后找到 akaedu目录,然后找到文件 a。 dentry cache只保存最近访问过的目录项,如果要找的目录项在 cache中没有,就要从磁盘读到内存中。

    每个dentry结构体都有一个指针指向 inode结构体。 inode结构体保存着从磁盘 inode读上来的信息。在上图的例子中,有两个 dentry,分别表示 /home/akaedu/a和 /home/akaedu/b,它们都指向同一个 inode,说明这两个文件互为硬链接。 inode结构体中保存着从磁盘分区的 inode读上来信息,例如所有者、文件大小、文件类型和权限位等。每个 inode结构体都有一个指向 inode_operations结构体的指针,后者也是一组函数指针指向一些完成文件目录操作的内核函数。和 file_operations不同, inode_operations所指向的不是针对某一个文件进行操作的函数,而是影响文件和目录布局的函数,例如添加删除文件和目录、跟踪符号链接等等,属于同一文件系统的各 inode结构体可以指向同一个 inode_operations结构体。

    inode结构体有一个指向 super_block结构体的指针。 super_block结构体保存着从磁盘分区的超级块读上来的信息,例如文件系统类型、块大小等。 super_block结构体的 s_root成员是一个指向 dentry的指针,表示这个文件系统的根目录被 mount到哪里,在上图的例子中这个分区被 mount到 /home目录下。

    file、 dentry、 inode、 super_block这几个结构体组成了 VFS的核心概念。对于 ext2文件系统来说,在磁盘存储布局上也有 inode和超级块的概念,所以很容易和 VFS中的概念建立对应关系。而另外一些文件系统格式来自非 UNIX系统(例如 Windows的 FAT32、 NTFS),可能没有 inode或超级块这样的概念,但为了能 mount到 Linux系统,也只好在驱动程序中硬凑一下,在 Linux下看 FAT32和 NTFS分区会发现权限位是错的,所有文件都是 rwxrwxrwx,因为它们本来就没有 inode和权限位的概念,这是硬凑出来的。
  • 相关阅读:
    Java接口的真正意义
    从机械自动化专业转行到Java工程师的学习之路
    程序员必备的17个软件开发工具
    【Java练手项目】Java外卖点餐系统
    Java接口的其他使用语法
    【Java练手项目】Java在线考试系统
    IDEA最新版本中maven配置默认选项
    Java 企业级项目需求文档
    Elasticsearch 搜索数量不能超过10000的解决方案
    Java中TreeSet怎么实现?(详解)
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8472105.html
Copyright © 2020-2023  润新知