深度剖析 Linux cp 命令的秘密( 四 )


举个例子,假设我们现在有一个 6k 的文件,那么只需要 2 个 block 就可以存下了,假设现在数据就存储在编号为 3 和 101 这两个 block 上,那么如下图:

深度剖析 Linux cp 命令的秘密

文章插图
 
i_block[15] 第一个元素存的是 3,第二个存储的是 101,其他槽位没用用到,由于 inode 的内存是置零分配的,所以里面的值为 0,表示没有在使用 . 我们通过 [3, 101] 这两个 block 就能拼装出完整的用户数据了 。用户的 6k 文件组成如下:
  1. 第一个 4k 数据在 [3*4K, 4*4K] 范围;
  2. 第二个 2k 数据在 [ 101*4K, 101*4K+2K] 范围;
好,现在我们知道了每个定长 block 都有唯一编号,我们的 i_block[15] 数组 通过有序存储这个编号找到文件数据所在的位置,并且拼装出完整文件 。
思考问题:区分文件的切分成 4k 块的编号和 磁盘上物理 4k 块的编号的区别 。
举个栗子,一个文件 12K 的大小,那么按照 4K 切分会存储到 3 个 物理 block 上 。
文件第 0 个 4k 存储到了 101 这个物理 block 上;文件第 1 个 4k 存储到了 30 这个物理 block 上;文件第 2 个 4k 存储到了 11 这个物理 block 上;
文件逻辑空间上的编号是从 0 开始,到 2 结束,对应存储的物理块编号分别是 101,30,11。
思考问题:这么一个 inode 结构能够表示多大的文件?
我们看到 inode->i_block[15] 是一个一维数组,里面能存 15 个元素 。也就是能存 15 个 block 的编号,那么如果直接存储文件的 block 编号最大能表示 60K (15*4K) 的文件 。换句话说,如果我拿着 15 个槽位全部用来存储文件的编号,这个文件系统支撑的最大文件却就是 60K 。惊呆了?(注意:ext2 文件系统是可以创建 4T 以内的文件的!!)
那我们自然会思考,怎么解决呢?怎么才能支撑更大的文件?
最直接思考就是用更大的数组,把 inode->i_block 数组变得更大 。比如,如果你想要支持 100G 的文件:
那么,需要 i_block 数组大小为 26214400 (计算公式:100*1024*1024/4),也就是要分配一个 i_block[26214400] 的数组 。
每个编号占用 4 字节,这个数组就占用 100M 的空间(计算公式:(26214400*4)/1024/1024) 。100M !这里就有点夸张了,注意到 i_block 只是一个 inode 内部的字段,是一个静态分配的数组,也就是说,这个文件系统为了支持最大 100G 的文件存入,每一个 inode 都要占用 100M 的内存,就算你是一个 1K 的文件,inode 也会占用这么大的内存空间 。并且,这种方案扩展性差,支持的文件 size 越大,i_block[N] 消耗内存情况越严重 。这是无法接受的 。
思考问题:怎么才能让你既能表示更大的文件,又能不浪费占用空间?
我们仔细分析这个问题,你会发现,这里有 2 个核心问题:
  1. 第一点,核心在于浪费内存空间(关键点是要保证 inode 内存结构的稳定,无论文件怎么变,inode 结构本身不能变);
  2. 第二点,仔细思考你会发现,无论是什么神仙方案,如果你要存储一个按照 4k 切分的 100G 文件,都是需要 100M 的空间来存储索引( block 编号),但是 99.99% 的文件可能都没有这么大;
我们前面用一个大数组来一把存储 block 编号的方案固然简单,但是问题在于太过死板 。核心问题在于存储 block 编号的数组是预分配的,为了还没有发生并且 99% 场景都不会发生的事情(文件大小达到 100G),却不管三七二十一,提前准备好了完整的 block 索引数组,预分配就是浪费的根源 。
那么知道了这两个问题,下一步分析下一个个解决:
 
索引存磁盘 
问题一的解决:索引存磁盘:
既然问题在于浪费内存,inode 内存分配不灵活,那就可以看把 inode->i_block 下放到磁盘 。
为什么?
因为磁盘的空间比内存大了不止一个量级 。100M 对内存来说很大,对磁盘来说很小 。换句话说,用把用户数据所在的 block 编号存到磁盘上去,这个也需要物理空间,使用的也是 block 来存储,只不过这种 block 存储的是 block 编号信息,而不是用户数据 。
那么我们怎么通过 inode 找到用户数据呢?
因为这个 block 本身也有编号,我们则需要把这个存储用户 block 编号的 block 所在块的编号存储在 inode->i_block[15] 里,当读数据的时候,我们需要先找到这个存储编号的 block,然后再通过里面存储的用户数据所在的 block 编号找到用户所在的 block ,去读数据 。


推荐阅读