深入理解 Linux 内核—Ex2 和 Ex3 文件系统

释放双眼,带上耳机,听听看~!

Ext2 的一般特征:引入 Ext2。
Ext2 磁盘数据结构:说明把 Ext2 存放在磁盘上的数据结构。
Ext2 的内存数据结构:说明如果把磁盘上的数据结构复制到内存中。
创建 Ext2 文件系统:讨论如何在磁盘分区创建 Ext2。
接着描述使用磁盘时内核所执行的操作,大部分是为索引节点和数据块分配磁盘空间。
最后,对 Ext3 文件系统进行简短描述。

Ext2 的一般特征

以下特点有助于 Ext2 的效率:

  • 当创建 Ext2 文件系统时,系统管理员可根据预期的文件平均长度选择最佳块大小(1024B ~ 4096B)。
  • 当创建 Ext2 文件系统时,系统管理员可根据在给定大小的分区上预计存放的文件数来选择给该分区分配多少个索引节点。
  • 文件系统把磁盘块分为组。
  • 磁盘数据块被实际使用前,文件系统就把这些块预分配给普通文件。
  • 支持快速符号链接。

另外,Ext2 还包含了一些使它既健壮又灵活的特点:

  • 文件更新策略的谨慎实现将系统崩溃的影响减到最少。
  • 在启动时支持对文件系统的状态进行自动的一致性检查。
  • 支持不可变的文件和仅追加的文件。
  • 既与 Unix System V Release 4(SVR 4)兼容,也与新文件的用户组 ID 的 BSD 语义兼容。

但 Ext2 中没有日志功能,Ext3 中有。

Ext2 磁盘数据结构

任何 Ext2 分区中的第一个块从不受 Ext2 文件系统的管理,因为这一块是为分区的引导扇区所保留。
Ext2 的其余部分分成块组,每个块组的分布如图 18-1 所示。
在 Ext2 文件系统中的所有块组大小相同并被顺序存放,因此,内核可以从块组的正数索引很容易地得到磁盘中一个块组的位置。
深入理解 Linux 内核---Ex2 和 Ex3 文件系统
由于内核尽可能地把属于一个文件的数据块存放在同一块组中,所以块组减少了文件碎片。
块组中的每个块包含下列信息之一:

  • 文件系统的超级块的一个拷贝。
  • 一组块组描述符的拷贝。
  • 一个数据块位图。
  • 一个索引节点位图。
  • 一个索引节点表。
  • 属于文件的一大块数据,即数据块。

如果一个块中不包含任何有意义的信息,就说这个块是空闲的。

从图 18-1 可看出,超级块与组描述符被复制到每个块组中。
只有块组 0 中所包含的超级块和组描述符才由内核使用,而其余的超级块和组描述符保持不变。
当 e2fsck 程序对 Ext2 文件系统的状态执行一致性检查时,就引用存放在块组 0 中的超级块和组描述符,然后把它们拷贝到其它所有的块组中。
如果出现数据损坏,并且块组 0 中的主超级块和主组描述符变为无效,则系统管理员就可以命令 e2fsck 引用存放在某个块组(除了第一个块组)中的超级块和组描述符的旧拷贝。

块组的数量主要限制于块位图,因为块位图必须存放在一个单独的块中。
块位图用来标识一个组中块的占用和空闲状况。
所以,每组中至多有 8b 个块,b 是以字节为单位的块大小。

超级块

Ext2 在磁盘上的超级块存放在一个 ext2_super_block 结构中,

s_indoes_count 字段存索引节点的个数。
s_blocks_count 字段存放 Ext2 文件系统的块的个数。

s_log_block_size 字段以 2 的幂次方表示块的大小,用 1024 字节作为单位。

s_blocks_per_group、s_frags_per_group 与 s_inodes_per_group 字段分别存放每个块组中的块数、片数及索引节点数。

s_mnt_count、s_max_mnt_count、s_lastcheck 及 s_checkinterval 字段使系统启动时自动地检查 Ext2 文件系统。

组描述符和位图

每个块组都由自己的组描述符,它是一个 ext2_group_desc 结构。

当分配新索引节点和数据块时,会用到 bg_free_blocks_count、bg_free_inodes_count 和 bg_used_dirs_count 字段。
这些字段确定在最合适的块中给每个数据结构进行分配。

位图是位的序列,其中值 0 表示对应的索引节点块或数据块是空闲的,1 表示占用。
一个单独的位图描述 8192、16384 或 32768 个块的状态。

索引节点表

索引节点表由一连串连续的块组成,其中每一块包含索引节点的一个预定义号。
索引节点表第一个块的块号存放在组描述符的 bg_inode_table 字段中。

所有索引节点的大小相同,即 128 字节。
一个 1024 字节的块可以包含 8 个索引节点,一个 4096 字节的块可以包含 32 个索引节点。
为了计算出索引节点表占用了多少块,用一个组中的索引节点总数(超级块的 s_inodes_per_group 字段)除以每块中的索引节点数。

每个 Ext2 索引节点为 ext2_innode 结构。

i_size 字段存放以字节为单位的文件的有效长度。
i_blocks 字段存放已分配给文件的数据块数(以 512 字节为单位)。

i_size 和 i_blocks 的值没有必然的联系。
因为一个文件总是存放在整数块中,一个非空文件至少接受一个数据块且 i_size 可能小于 512 $*$ i_blocks。
如果一个文件中包含空洞,i_size 可能大于 512 * i_blocks。

i_blocks 字段是具有 EXT2_N_BLOCKS(通常是 15)个指针元素的一个数组,每个元素指向分配给文件的数据块。

留给 i_size 字段的 32 位把文件的大小限制到 4GB。
又因为 i_size 字段的最高位有使用,因此,文件的最大长度限制为 2GB。
i_dir_acl 字段(普通文件没有使用)表示 i_size 字段的 32 位扩展。
因此,文件的大小作为 64 位整数存放在索引节点中。
在 32 位体系结构上访问大文件时,需以 O_LARGEFILE 标志打开文件。

索引节点的增强属性

引入增强属性的原因:如果要给索引节点的 128 个字符空间中充满了信息,增加新字段时,将索引节点的长度增加到 256 有些浪费。

增强属性存放在索引节点之外的磁盘块中。
索引节点的 i_file_acl 字段指向一个存放增强属性的块。
具有同样增强属性的不同索引节点可共享同一个块。

每个增强属性有一个名称和值。
两者都编码位变长字符数组,并由 ext2_xattr_entry 描述符确定。
每个属性分成两部分:在块首部的是 ext2_xattr_entry 描述符与属性名,而属性值则在块尾部。
块前面的表项按照属性名称排序,而值的位置是固定的,因为它们是由属性的分配次序决定的。
深入理解 Linux 内核---Ex2 和 Ex3 文件系统

访问控制列表

访问控制列表(access control list, ACL)可以与每个文件关联。
有了这种列表,用户可以为他的文件线段可以访问的用户(或用户组)名称及相应的权限。

Linux 2.6 通过索引节点的增强属性完整实现 ACL。

各种文件类型如何使用磁盘块

Ext2 所认可的文件类型(普通文件、管道文件等)以不同的方式使用数据块。

普通文件

普通文件是最常见的情况,但只有在开始有数据时才需要数据块。
普通文件在刚创建时是空的;也可以用 truncate() 或 open() 清空它。

目录

Ext2 以一种特殊的文件实现了目录,这种文件的数据块把文件名和相应的索引节点号存放在一起。
这样的数据块包含了类型为 ext2_dir_entry_2 的数据结构。
该结构最后一个 name 字段是最大为 EXT2_NAME_LEN(通常是 255)个字符的变长数组,因此该结构的长度是可变的。
此外,因为效率的原因,目录项的长度总是 4 的倍数,必要时以 null 字符(\0)填充。
name_len 字段存放实际的文件名长度。

file_type 字段存放指定文件类型的值。
rec_len 字段可被解释为指向下一个有效目录的指针:它是偏移量,与目录项的起始地址相加就得到下一个有效的目录项的起始地址。
为了删除一个目录项,把它的 inode 字段置为 0 并适当地增加前一个有效目录项 rec_len 字段的值即可。

深入理解 Linux 内核---Ex2 和 Ex3 文件系统

符号链接

如果符号链接的路径名小于等于 60 个字符,就把它存放在索引节点的 i_blocks 字段,该字段是由 15 个 4 字节整数组成的数组,因此无需数据块。
但是,如果路径名大于 60 个字符,就需要一个单独的数据块。

设备文件、管道和套接字

这些类型的文件不需要数据块。
所有必要的信息都存放在索引节点中。

Ext2 的内存数据结构

为提高效率,安装 Ext2 文件系统时,存放在 Ext2 分区的磁盘数据结构中的大部分信息被拷贝到 RAM 中,从而使内核避免了后来的很多读操作。

因为所有的 Ext2 磁盘数据结构都存放在 Ext2 分区的块中,因此,内核利用页高速缓存来保持它们最新。

索引节点与块位图并不永久保存在内存里,而是需要时从磁盘读。

Ext2 的超级块对象

VFS 超级块的 s_fs_info 字段指向一个包含文件系统信息的数据结构。
对于 Ext2,该字段指向 ext2_sb_info 类型的结构,它包含如下信息:

  • 磁盘超级块中的大部分字段。
  • s_sbh 指针,指向包含磁盘超级块的缓冲区的缓冲区首部。
  • s_es 指针,指向磁盘超级块所在的缓冲区。
  • 组描述符的个数 s_desc_per_block,可以放在一个块中。
  • s_group_desc 指针,指向一个缓冲区(包含组描述符的缓冲区)首部数组。
  • 其它与安装状态、安装选项等有关的数据。

当内核安装 Ext2 文件系统时,它调用 ext2_fill_super() 为数据结构分配空间,并写入从磁盘读取的数据。
这里只强调缓冲区与描述符的内存分配。

  1. 分配一个 ext2_sb_info 描述符,将其地址当作参数传递并存放在超级块的 s_fs_info 字段。
  2. 调用 __bread() 在缓冲区页中分配一个缓冲区和缓冲区首部。

然后从磁盘读入超级块存放在缓冲区中。
如果一个块在页高速缓存的缓冲区页而且是最新的,那么无需再分配。
将缓冲区首部地址存放在 Ext2 超级块对象的 s_sbh 字段。

  1. 分配一个字节数组,每组一个字节,把它的地址存放在 ext2_sb_info 描述符的 s_debts 字段。
  2. 分配一个数组用于存放缓冲区首部指针,每个组描述符一个,把该数组地址存放在 ext2_sb_info 的 s_group_desc 字段。
  3. 重复调用 __bread() 分配缓冲区,从磁盘读入包含 Ext2 组描述 符的块。

把缓冲区首部地址存放在上一步得到的 s_group_desc 数组中。

  1. 为根目录分配一个索引节点和目录项对象,为超级块建立相应的字段,从而能够从磁盘读入根索引节点对象。

ext_fill_super() 返回后,分配的所有数据结构都保存在内存里,只有当 Ext2 文件系统卸载时才会被释放。
当内核必须修改 Ext2 超级块的字段时,它只要把新值写入相应缓冲区内的相应位置然后将该缓冲区标记为脏即可。

Ext2 的索引节点对象

对于不在目录项高速缓存内的路径名元素,会创建一个新的目录项对象和索引节点对象。
当 VFS 访问一个 Ext2 磁盘索引节点时,它会创建一个 ext2_inode_info 类型的索引节点描述符。
该描述符包含以下信息:

  • 存放在 vfs_inode 字段的整个 VFS 索引节点对象。
  • 磁盘索引节点对象结构中的大部分字段(不保存在 VFS 索引节点中)。
  • 索引节点对应的 i_block_group 块组索引。
  • i_next_alloc_block 和 i_next_alloc_goal 字段,分别存放着最近为文件分配的磁盘块的逻辑块号和物理块号。
  • i_prealloc_block 和 i_prealloc_count 字段,用于数据块预分配。
  • xattr_sem 字段,一个读写信号量,允许增强属性与文件数据同时读入。
  • i_acl 和 i_default_acl 字段,指向文件的 ACL。

当处理 Ext2 文件时,alloc_inode 超级块方法是由 ext2_alloc_inode() 实现的。
它首先从 ext2_inode_cachep slab 分配器高速缓存得到一个 ext2_inode_info 描述符,然后返回在这个 ext2_inode_info 描述符中的索引节点对象的地址。

创建 Ext2 文件系统

在磁盘上创建一个文件系统通常有两个阶段。
第一步格式化磁盘,以使磁盘驱动程序可以读和写磁盘上的块。
硬磁盘已由厂家预先格式化,因此不需要重新格式化。
Linux 上可使用 superformat 或 fdformat 等使用程序对软盘格式化。
第二步才涉及创建文件系统。

Ext2 文件系统是由实际程序 mke2fs 创建的。
mke2fs 采用下列缺省选项,用户可以用命令行的标志修改这些选项:

  • 块大小:1024 字节。
  • 片大小:块的大小。
  • 所分配的索引节点个数:每 8192 字节的组分配一个索引节点。
  • 保留块的百分比:5%

mke2fs 程序执行下列操作:

  1. 初始化超级块和组描述符。
  2. 作为选择,检查分区释放包含有缺陷的块;如果有,就创建一个有缺陷块的链表。
  3. 对于每个块组,保留存放超级块、组描述符、索引节点表及两个位图所需的所有磁盘块。
  4. 把索引节点位图和每个块组的数据映射位图都初始化为 0。
  5. 初始化每个块组的索引节点表。
  6. 创建 /root 目录。
  7. 创建 lost+found 目录,由 e2fsck 使用该目录把丢失和找到的缺陷块连接起来。
  8. 在前两个已经创建的目录所在的块组中,更新块组中的索引节点位图和数据块位图。
  9. 把有缺陷的块(如果存在)组织起来放在 lost+found 目录中。

表 18-7 总结了按缺省选项如何在软盘上建立 Ext2 文件系统。
深入理解 Linux 内核---Ex2 和 Ex3 文件系统

Ext2 的方法

Ext2 超级块的操作

超级块方法的地址存放在 ext2_sops 指针数组中。

Ext2 索引节点的操作

一些 VFS 索引节点的操作在 Ext2 中都由具体的实现,这取决于索引节点所指的文件类型。

普通文件与目录的 Ext2 方法的地址分别存放在 ext2_file_inode_operations 和 ext2_dir_inode_operations 表中。

有两种符号链接:快速符号链接(路径名全部存放在索引节点内)与普通符号链接(较长的路径名)。
因此,有两套索引节点操作,分别存放在 ext2_fast_symlink_inode_operations 和 ext2_symlink_inode_operations 表中。

如果索引节点指的是一个字符设备文件、块设备文件或命名管道,那么这种索引节点的操作不依赖于文件系统,其操作分别位于 chrdev_inode_operations、blkdev_inode_operations 和 fifo_inode_operations 表中。

Ext2 的文件操作

一些 VFS 方式是由很多文件系统共用的通用函数实现的,这些方法的地址存放在 ext2_file_operations 表中。

Ext2 的 read 和 write 方法分别通过 generic_file_read() 和 generic_file_write() 实现。

管理 Ext2 磁盘空间

文件在磁盘的存储不同于程序员所看到的文件,表现在两方面:

  • 块可以分散在磁盘上;
  • 程序员看到的文件似乎比实际的文件大,因为文件中可包含空洞。

在分配和释放索引节点和数据块方面有两个主要的问题必须考虑:

  • 空间管理必须尽力避免文件碎片。
  • 空间管理必须考虑效率,即内核应该能从文件的偏移量快速导出 Ext2 分区上相应的逻辑块号。

创建索引节点

ext2_new_inode() 创建 Ext2 磁盘的索引节点,返回相应的索引节点对象的地址。
该函数谨慎地选择存放在该新索引节点的块组;它将无联系的目录散放在不同的组,而且同时把文件存放在父目录的同一组。
为了平衡普通文件数与块组中的目录数,Ext2 为每一个块组引入“债”参数。

该函数的两个参数:

  • dir,一个目录对应的索引节点对象的地址,新创建的索引节点必须插入到该目录中。
  • mode,要创建的索引节点的类型。还包含一个 MS_SYNCHRONOUS 标志,该标志请求当前进程一直挂起,直到索引节点被分配。

该函数执行如下操作

  1. 调用 new_inode() 分配一个新的 VFS 索引节点对象,并把它的 i_sb 字段初始化为存放在 dir->i_sb 中的超级块地址。

然后把它追加到正在用的索引节点链表与超级块链表中。

  1. 如果新的索引节点是一个目录,函数就调用 find_group_orlov() 为目录找到一个合适的块组。该函数执行如下试探:

a. 以文件系统根 root 为父目录的目录应该分散在各个组。
这样,函数在这些块组中查找一个组,它的空闲索引节点数和空闲块数比平均值高。如果没有这样的组则跳到第 2c 步。
b. 如果满足下列条件,嵌套目录(父目录不是文件系统根 root)就应该存放到父目录组:

  • 该组没有包含太多的目录。
  • 该组有足够多的空闲索引节点。
  • 该组有一点小“债”

如果父目录组不满足这些条件,则选择第一个满足条件的组。
如果没有满足条件的组,则跳到第 2c 步。
c. 这是一个“退一步”原则,当找不到合适的组时使用。
函数从包含父目录的块组开始选择第一个满足条件的块组,该条件为:
它的空闲索引节点数比每块组空闲索引节点数的平均值大。

  1. 如果新索引节点不是个目录,则调用 find_group_other(),在有空闲索引节点的块组中给它分配一个。

该函数从包含父目录的组开始往下找,具体如下:
a. 从包含父目录 dir 的块组开始,执行快速的对数查找。
这种算法要查找 log(n) 个块组,这里 n 是块组总数。
该算法一直向前查找直到找到一个可用的块组,具体如下:如果我们把开始的块组称为 i,那么,该算法要查找的块组为 i mod(n),i+1 mod(n),i+1+2 mod(n),i+1+2+4 mod(n),等等 。
b. 如果该算法没有找到含有空闲索引节点的块组,就从包含父目录 dir 的块组开始执行彻底的线性查找。

  1. 调用 read_inode_bitmap() 得到所选块组的索引节点位图,并从中寻找第一个空位,这样就得到了第一个空闲磁盘索引节点号。
  2. 分配磁盘索引节点:把索引节点位图中的相应置位,并把含有这个位图的缓冲区标记为脏。

此外,如果文件系统安装时指定了 MS_SYNCHRONOUS 标志,则调用 sync_dirty_buffer() 开始 I/O 写操作并等待,直到写操作终止。

  1. 减少组描述符的 bg_free_inodes_count 字段。

如果新的索引节点是一个目录,则增加 bg_used_dirs_count 字段,并把含有这个组描述符的缓冲区标记为脏。

  1. 依据索引节点指向的是普通文件或目录,相应增减超级块内 s_debts 数组中的组计数器。
  2. 减少 ext2_sb_info 数据结构中的 s_freeinodes_counter 字段;而且如果新索引节点是目录,则增大 ext2_sb_info 数据结构的 s_dirs_counter 字段。
  3. 将超级块的 s_dirt 标志置 1,并把包含它的缓冲区标记为脏。
  4. 把 VFS 超级块对象的 s_dirt 字段置 1。
  5. 初始化这个索引节点对象的字段。

特别是,设置索引节点号 i_no ,并把 xtime.tv_sec 值拷贝到 i_atime、i_mtime 及 i_ctime。
把这个块组的索引赋给 ext2_inode_info 结构的 i_block_group 字段。

  1. 初始化该索引节点对象的访问控制列表(ACL)。
  2. 将新索引节点对象插入散列表 inode_hashtable,调用 mark_inode_dirty() 把该索引节点对象移进超级块脏索引节点链表。
  3. 调用 ext2_preread_inode() 从磁盘读入包含该索引节点的块,将它存入页高速缓存。

进行这种预读是因为最近创建的索引节点可能会被很快写入。

  1. 返回新索引节点对象的地址。

总结:分配 VFS 索引节点对象;根据新索引节点是目录还是普通文件找到一个合适的块组;得到索引节点位图;从位图中找到空位,分配磁盘索引节点;更新相关计数器;初始化索引节点对象;将新索引节点插入散列表、存入页高速缓存;返回新索引对象地址。

删除索引节点

用 ext2_free_inode() 删除一个磁盘索引节点,把磁盘索引节点表示为索引节点对象,其地址作为参数来传递。
内核在进行一系列的清除操作后调用该函数。
具体来说,它在下列操作完成后才执行:
索引节点对象已经从散列表中删除,执行该索引节点的最后一个硬链接已经从适当的目录中删除,文件的长度截为 0 以回收它的所有数据块。
函数执行下列操作:

  1. 调用 clear_inode(),它依次执行如下步骤:

a. 删除与索引节点关联的“间接”脏缓冲区。
它们都存放在一个链表中,该链表的首部在 address_space 对象 inode->i_data 的 private_list 字段。
b. 如果索引节点的 I_LOCK 标志置位,则说明索引节点中的某些缓冲区正处于 I/O 数据传送中;
于是,函数挂起当前进程,直到这些 I/O 数据传送结束。
c. 调用超级块对象的 clear_inode 方法,但 Ext2 没有定义该方法。
d. 如果索引节点指向一个设备文件,则从设备的索引节点链表中删除索引节点对象,该链表要么在 cdev 字符设备描述符的 cdev 字段,要么在 block_device 块设备描述符的 bd_inodes 字段。
e. 把索引节点的状态置为 I_CLEAR(索引节点对象的内容不再有意义)。

  1. 从每个块组的索引节点号和索引节点数计算包含这个磁盘索引节点的块组的索引。
  2. 调用 read_inode_bitmap() 得到索引节点位图。
  3. 增加组描述符的 bg_free_inodes_count 字段。

如果删除的索引节点是一个目录,那么也要减小 bg_used_dirs_count 字段。
把这个组描述符所在的缓冲区标记为脏。

  1. 如果删除的索引节点是一个目录,就减小 ext2_sb_info 结构的 s_dirs_counter 字段,把超级块的 s_dirt 标志置 1,并把它所在的缓冲区标记为脏。
  2. 清除索引节点位图中这个磁盘索引节点对应的位,并把包含这个位图的缓冲区标记为脏。

此外,如果文件系统以 MS_SYNCHRONIZE 标志安装,则调用 sync_dirty_buffer() 并等待,直到在位图缓冲区上的写操作终止。

总结:删除索引节点缓冲区;获取块组索引;获取索引节点位图;更新相关计数器、状态;清除索引节点位图中相应;写回。

数据块寻址

每个非空的普通文件都由一组数据块组成。
这些块或者由文件内的相对位置(它们的文件块号)标识,或者由磁盘分区内的位置(它们的逻辑块号)来标识。

从文件内的偏移量 f 导出相应数据块的逻辑块号需要两个步骤:

  1. 从偏移量 f 导出文件的块号,即在偏移量 f 处的字符所在的块索引。
  2. 把文件的块号转化为相应的逻辑块号。

因为 Unix 文件不包含任何控制字符,因此,导出文件的第 f 个字符所在的文件块号的方式为,用 f 除以文件系统块的大小,并取整即可。

但是,由于 Ext2 文件的数据块在磁盘上不必是相邻的,因此不能直接把文件的块号转化为相应的逻辑块号。
因此,Ext2 文件系统在索引节点内部实现了一种映射,可以在磁盘上建立每个文件块号与相应逻辑块号之间的关系。
这种映射也涉及一些包含额外指针的专用块,这些块用来处理大型文件的索引节点的扩展。

磁盘索引节点的 i_block 字段是一个有 EXT2_N_BLOCKS 个元素且包含逻辑块号的数组。
如图 18-5 所示,假定 EXT2_N_BLOCKS = 15,数组中的元素有 4 种不同的类型。
深入理解 Linux 内核---Ex2 和 Ex3 文件系统

  • 最初的 12 个元素产生的逻辑块号与文件最初的 12 个块对应,即对应的文件块号为 0 ~ 11。
  • 下标 12 中的元素包含一个块的逻辑块号(叫做间接块),这个块表示逻辑块号的一个二级数组。

该数组的元素对应的文件块号从 12 ~ b/4 + 11,这里 b 是文件系统的块大小(每个逻辑块号占 4 个字节)。
因此,内核为了查找指向一个块的指针必须先访问该元素,然后,在这个块中找到另一个指向最终块(包含文件内容)的指针。

  • 下标 13 中的元素包含一个间接块的逻辑块号,而这个包含逻辑块号的一个二级数组,这个二级数组的数组项依次指向三级数组,这个三级数组存放的才是

文件块号对应的逻辑块号,范围从 b/4 + 12 ~ (b/4)$^2$ + (b/4) + 11。

  • 最后,下标 14 中的元素使用三级间接索引,第四级数组中存放的采释文件块号对应的逻辑块号,范围从(b/4)$^2$ + (b/4) + 12 ~ (b/4)$^3$ + (b/4)$^2$ + (b/4)+ 11。

如果文件需要的数据块小于 12,则两次磁盘访问就可以检索到任何数据:一次是读磁盘索引节点 i_block 数组的一个元素,另一次是读所需要的数据块。
对于打文件,可能需要三四次的磁盘访问才能找到需要的块。
实际上,因为目录项、索引节点、页高速缓存都有助于极大减少实际访问磁盘的次数。

文件的洞

文件的洞是普通文件的一部分,它是一些空字符但没有存放在磁盘的任何数据块中。
因为文件的洞是为了避免磁盘空间的浪费。

文件洞在 Ext2 中的实现是基于动态数据块的分配的:只有当进程需要向一个块写数据时,才真正把这个块分配给文件。
每个索引节点的 i_size 字段定义程序所看到的文件大小,包括洞,而 i_blocks 字段存放分配给文件有效的数据块数(以 512 字节为单位)。

分配数据块

当内核要分配一个数据块来保存 Ext2 普通文件的数据时,就调用 ext2_get_block()。
如果块不存在,该函数就自动为文件分配块。
每当内核在 Ext2 普通文件上执行读或写操作时就调用该函数。
该函数只有在页高速缓存内没有相应的块时才被调用。

ext2_get_bloc() 在必要时调用 ext2_alloc_block() 在 Ext2 分区真正搜索一个空闲块。
如果需要,还为间接寻址分配相应的块。

为了减少文件的碎片,Ext2 文件系统尽力在已分配给文件的最后一个块附近找到一个新块分配给该文件。
如果失败,Ext2 文件系统又在包含这个文件索引节点的块组中搜寻一个新的块。
作为最后一个办法,可以从其它一个块组中获得空闲块。

Ext2 文件系统使用数据块的预分配策略。
文件并不仅仅获得所需的块,而是获得一组多达 8 个邻接的块。
ext2_inode_info 结构的 i_prealloc_count 字段存放预分配给某一个文件但还没有使用的数据块数,而 i_prealloc_block 字段存放下一次要使用的预分配块的逻辑块号。

下列情况发生时,释放预分配而一直没有使用的块:当文件被关闭时,当文件被缩短时,或者当一个写操作相对于引发预分配的写操作不是顺序时。

ext2_alloc_block() 参数为指向索引节点对象的指针、目标和存放错误码的变量地址。
目标是一个逻辑块号,表示新块的首选位置。
ext2_getblk() 根据下列的试探法设置目标参数:

  1. 如果正被分配的块与前面已分配的块有连续的文件块号,则目标就是前一块的逻辑块号加 1。
  2. 如果第一条规则不适用,并且至少给文件已分配了一个块,那么目标就是这些块的逻辑块号的一个。

更确切的说,目标是已分配的逻辑块号,位于文件中待分配块之前。

  1. 如果前面的规则都不适用,则目标就是文件索引节点所在的块组中第一个块的逻辑块号。

ext2_alloc_block() 检查目标是否指向文件的预分配块中的一块。
如果是,就分配相应的块并返回它的逻辑块号;
否则,丢弃所有剩余的预分配块并调用 ext2_new_block()。

ext2_new_block() 用下列策略在 Ext2 分区内搜寻一个空闲块:

  1. 如果传递给 ext2_alloc_block() 的首选块(目标块)是空闲的,就分配它。
  2. 如果目标为忙,就检查首选块后的其余块之中是否有空闲的块。
  3. 如果在首先块附近没有找到空闲块,就从包含目标的块组开始,查找所有的块组,对每个块组:

a. 寻址至少有 8 个相邻空闲块的一个块组。
b. 如果没有找到这样的一组块,就寻找一个单独的空闲块。

只要找到一个空闲块,搜索就结束。
在结束前,ext2_new_block() 还尽力在找到的空闲块附近的块中找 8 个空闲块进行预分配,并把磁盘索引节点的 i_prealloc_block 和 i_prealloc_count 字段设置为适当的块位置及块数。

释放数据块

当进程删除一个文件或把它的长度截为 0 时,ext2_truncate() 将其所有数据块回收。
该函数扫描磁盘索引节点的 i_block 数组,以确定所有数据块的位置和间接寻址用的块的位置。
然后反复调用 ext2_free_blocks() 释放这些块。

ext2_free_blocks() 释放一组含有一个或多个相邻块的数据块。
除 ext2_truncate() 调用它外,当丢弃文件的预分配块时也主要调用它。
参数:

  • inode,文件的索引节点对象的地址。
  • block,要释放的第一个块的逻辑块号。
  • count,要释放的相邻块数。

该函数对每个要释放的块执行下列操作:

  1. 获得要释放块所在块组的块位图。
  2. 把块位图中要释放的块的对应位清 0,并把位图所在的缓冲区标记为脏。
  3. 增加块组描述符的 bg_free_blocks_count 字段,并把相应的缓冲区标记为脏。
  4. 增加磁盘超级块的 s_free_blocks_count 字段,并把相应的缓冲区标记为脏,把超级块对象的 s_dirt 标记置位。
  5. 如果 Ext2 文件系统安装时设置了 MS_SYNCHRONOUS 标志,则调用 sync_dirty_buffer() 并等待,直到对这个位图缓冲区的写操作终止。

Ext3 文件系统

Ext3 文件夹系统设计时秉持两个简单的概念:

  • 成为一个日志文件系统。
  • 尽可能与原来的 Ext2 文件系统兼容。

日志文件系统

日志文件系统的目标是避免对整个文件系统进行耗时的一致性检查,这是通过查看一个特殊的磁盘区达到的,因为这种磁盘区包含日志的最新磁盘写操作。
系统出现故障后,安装日志文件系统只需要几秒钟。

Ext3 日志文件系统

Ext3 日志所隐含的思想就是对文件系统进行的任何高级修改都分两步进行。
首先,把待写块的一个副本存放在日志中;其次,当发往日志的 I/O 数据传送完成时,块就被写入文件系统。
当发往文件系统的 I/O 数据传送终止时,日志中的块副本就被丢弃。

当从系统故障中恢复时,e2fsck 程序区分下列两种情况:
提交到日志之前系统故障发生。 与高级修改相关的块副本或者从日志中丢失,或者是不完整的;这两种情况下,e2fsck 都忽略它们。
提交到日志之后的系统故障发生。 块的副本是有效的,且 e2fsck 把它们写入文件系统。

第一种情况下,对文件系统的高级修改被丢失,但文件系统的状态还是一致的。
第二种情况下,e2fsck 应用于整个高级修改,因此,修正由于把未完成的 I/O 数据传送到文件系统而造成的任何不一致。

日志系统通常不把所有的块都拷贝到日志中。
事实上,每个文件系统都由两种块组成:包含元数据的块和包含普通数据的块。
在 Ext2 和 Ext3 中,有六种元数据:超级块、块组描述符、索引节点、用于间接寻址的块(间接块)、数据位图块和索引节点位图块。

很多日志文件系统都限定自己把影响元数据的操作写入日志。
事实上,元数据的日志记录足以恢复磁盘文件系统数据结构的一致性。
然而,因为文件的数据块不记入日志,因此就无法防止系统故障造成的文件内容的损坏。

不过,可以把 Ext3 文件系统配置为把影响文件系统元数据的操作和影响文件数据块的操作都记入日志。
因为把每种些操作都记入日志会导致极大的性能损失,因此,Ext3 让系统管理员决定应当把什么记入日志;
具体来说,它提供三种不同的日志模式:

  • 日志,文件系统所有数据和元数据的改变都被记入日志。
  • 预定,只有对文件系统元数据的改变才被记入日志。是 Ext3 缺省的日志模式。
  • 写回,只有对文件系统元数据的改变才被记入日志;这是在其它日志文件系统中发现的方法,也是最快的模式。

日志块设备层

Ext3 日志通常存放在名为 .journal 的隐藏文件中,该文件位于文件系统的根目录。

Ext3 文件系统本身不处理日志,而是利用所谓日志块设备(Journaling Block Device, JBD)的通用内核层。
现在,只有 Ext3 使用 JDB 层,而其它文件系统可能在将来才使用它。

JDB 层是相当复杂的软件部分。
Ext3 文件系统调用 JDB 例程,以确保在系统万一出现故障时它的后续操作不会损坏磁盘数据结构。
然后,JDB 典型地使用同一磁盘来把 Ext3 文件系统所做的改变记入日志,因此,它与 Ext3 一样易受系统故障的影响。
换言之,JDB 也必须保护自己免受任何系统故障引起的日志损坏。

因此,Ext3 与 JDB 之间的交互本质上基于三个基本单元:

  • 日志记录,描述日志文件系统一个磁盘块的一次更新。
  • 原子操作处理,包括文件系统的一次高级修改对应的日志记录;一般来说,修改文件系统的每个系统调用都引起一次单独的原子操作处理。
  • 事务,包括几个原子操作处理,同时,原子操作处理的日志记录对 e2fsck 标记为有效。

日志记录

日志记录本质上是文件系统将要发出的一个低级操作的描述。
在某些日志文件系统中,日志记录只包括操作所修改的字节范围及字节在文件系统中的起始位置。
然而,JDB 层使用的日志记录由低级操作所修改的整个缓冲区组成。
这种方式可能浪费很多日志空间,但它还是相当快的,因为 JBD 层直接对缓冲区和缓冲区首部进行操作。

因此,日志记录在日志内部表示为普通的数据块(或元数据)。
但是,每个这样的块都是与类型为 journal_block_tag_t 的小标签相关联的,这种小标签存放在文件系统中的逻辑块和几个状态标志。

随后,只要一个缓冲区得到 JBD 的关注,或者因为它属于日志记录,或者因为它是一个数据块,该数据块应当在相应的元数据之前刷新到磁盘,那么,内核把 journal_head 数据结构加入到缓冲区首部。
这种情况下,缓冲区首部的 b_private 字段存放 journal_head 数据结构的地址,并把 BH_JBD 标志置位。

原子操作处理

修改文件文件系统的任一系统调用通常都被划分为操纵磁盘数据结构的一系列低级操作。

为防止数据损坏,Ext3 文件系统必须确保每个系统调用以原子的方式进行处理。
原子操作处理是对磁盘数据结构的一组低级操作,这组低级操作对应一个单独的高级操作。
当系统故障恢复时,文件系统确保要么整个高级操作起作用,要么没有一个低级操作起作用。

任何原子操作处理都用类型为 handle_t 的描述符表示。
为了开始一个原子操作,Ext3 文件系统调用 journal_start() JBD 函数,该函数在必要时分配一个新的原子操作处理并把它插入到当前事务中。
因为对磁盘的任何低级操作都可能挂起进程,因此,活动原子操作处理的地址存放在进程描述符的 journal_info 字段。
为了通知原子操作已经完成,Ext3 文件系统调用 journal_stop()。

事务

出于效率的原因,JBD 层对日志的处理采用分组的方法,即把属于几个原子操作处理的日志记录分组放在一个单独的事务中。
此外,与一个处理相关的所有日志记录都必须包含在同一个事务中。

一个事务的所有日志记录存放在日志的连续块中。
JBD 层把每个事务作为整体来处理。

事务一旦被创建,它就能接受新处理的日志记录。
当下列情况之一发生时,事务就停止接受新处理:

  • 固定的时间已经过去,典型情况为 5s。
  • 日志中没有空闲块留给新处理。

事务是由类型为 transaction_t 的描述符来表示。
其最重要的字段为 t_state,该字段描述事务的当前状态。

从本质上上,事务可以是:

  • 完成的。包含在事务中的所有日志记录都已经从物理上写入日志。

当从系统故障恢复时,e2fsck 考虑日志中每个完成的事务,并把相应的块写入文件系统。
在这种情况下, t_state 字段存放值 T_FINISHED。

  • 未完成的。包含在事务中的日志记录至少还有一个没有从物理上写入日志,或者新的日志记录还在追加到事务中。

在系统故障的情况下,存放在日志中的事务映像可能不是最新的。
因此,当从系统故障中恢复时,e2fsck 不信任日志中未完成的事务,并跳过它们。
这种情况下,i_state 存放下列值之一:

  • T_RUNNING,还在接受新的原子操作处理。
  • T_LOCKED,不接受新的原子操作,但其中的一些还没有完成。
  • T_FLUSH,所有的原子操作处理都完成,但一些日志记录还正在写入日志。
  • T_COMMIT,原子操作处理的所有日志记录都已经写入磁盘,但在日志中,事务仍然被标记为完成。

在任何时刻,日志可能包含多个事务,但其中只有一个处于 T_RUNNNIG 状态,即它是活动事务。
所谓活动事务就是正在接受由 Ext3 文件系统发出的新原子操作处理的请求。

日志中的几个事务可能是未完成的,因为包含相关日志记录的缓冲区还没有写入日志。

如果事务完成,说明所有日志记录已被写入日志,但是一部分相应的缓冲区还没有写入文件系统。
只有当 JBD 层确认日志记录描述的所有缓冲区都已成功写入 Ext3 文件系统时,一个完成的事务才能从日志中删除。

日志如何工作

  1. write() 系统调用服务例程触发与 Ext3 普通文件相关的文件对象的 write 方法。

对于 Ext3 来说,该方法由 generic_file_write() 实现。

  1. generic_file_write() 几次调用 address_space 对象的 prepare_write 方法,写方法涉及的每个数据页都调用一次。

对 Ext3 来说,该方法由 ext3_prepare_write() 实现的。

  1. ext3_prepare_write() 调用 journal_start() JBD 函数开始一个新的原子操作。该原子操作处理被加到活动事务中。

实际上,原子操作处理是第一次调用 journal_start() 创建的。
后续的调用确认进程描述符的 journal_info 字段已经被置位,并使用这个处理。

  1. ext3_prepare_write() 调用 block_prepare_write(),参数为 ext3_get_block() 的地址。

block_prepare_write() 负责准备文件页的缓冲区和缓冲区首部。

  1. 当内核必须确定 Ext3 文件系统的逻辑块号时,就执行 ext3_get_block()。

该函数实际上类似于 ext2_get_block(),但有一个差异在于 Ext3 文件系统调用 JDB 层的函数确保低级操作记入日志:

  • 在对 Ext3 文件系统的元数据块发出低级写操作之前,该函数调用 journal_get_write_access()。

后一个函数主要把元数据缓冲区加入到活动事务链表中。
但是,它也必须检查元数据是否包含在日志的一个较老的未完成的事务中;这种情况下,它把缓冲区复制一份以确保老的事务以老的内容提交。

  • 在更新元数据块所在的缓冲区后,Ext3 文件系统调用 journal_dirty_metadata() 把元数据缓冲区移到活动事务的适当脏链表中,并在日志中记录这一操作。

注意,由 JDB 层处理的元数据缓冲区通常并不包含在索引节点的缓冲区的脏链表中,因此,这些缓冲区并不由正常磁盘高速缓存的刷新机制写入磁盘。

  1. 如果 Ext3 文件系统已经以“日志”模式安装,则 ext3_prepare_write() 在写操作触及的每个缓冲区上也调用 journal_get_write_access()。
  2. 控制权回到 generic_file_write(),该函数用存放在用户态地址空间的数据更新页,并调用 address_space 对象的 commit_write 方法。

对于 Ext3,函数如何实现该方法取决于 Ext3 文件系统的安装方式:

  • 如果 Ext3 文件系统已经以“日志”模式安装,那么 commit_write 方法是由 ext3_journalled_commit_write() 实现的,它对页中的每个数据缓冲区调用 journal_dirty_metdata()。

这样,缓冲区就包含在活动事务的适当脏链表中,但不包含在拥有者索引节点的脏链表中;此外,相应的日志记录写入日志。
最后,ext3_journalled_commit_write() 调用 journal_stop 通知 JBD 层原子操作处理已关闭。

  • 如果 Ext3 文件系统已经以“预定”模式安装,那么 commit_write 方法是由 ext3_ordered_commit_write() 实现,它对页中的每个数据缓冲区调用 journal_dirty_data() 以把缓冲区插入到活动事务的适当链表中。

JDB 层确保在事务中的元数据缓冲区写入之前这个链表中的所有缓冲区写入磁盘。没有日志记录写入日志。
然后,ext3_ordered_commit_write() 执行 generic_commit_write(),将数据缓冲区插入拥有者索引节点的脏缓冲区链表中。
然后,ext3_writeback_commit_write() 调用 journal_stop() 通知 JBD 层原子操作处理已关闭。

  • 如果 Ext3 文件系统以“写回”模式安装,那么 commit_write 方法由 ext3_writeback_commit_write() 实现,它执行 generic_commit_write() 把数据缓冲区插入拥有者索引节点的脏缓冲区链表中。

然后,ext3_writeback_commit_write() 调用 journal_stop() 通知 JBD 层原子操作已关闭。

  1. write() 的服务例程到此结束。

但是,JDB 层还没有完成它的工作,当事务的所有日志记录都物理地写入日志时,我们的事务才完成。
然后,执行 journal_commit_transaction()。

  1. 如果 Ext3 文件系统以“预定”模式安装,则 journal_commit_transaction() 为事务链表包含的所有数据缓冲区激活 I/O 数据传送,并等待直到数据传送终止。
  2. journal_commit_transaction() 为包含在事务中的所有元数据缓冲区激活 I/O 数据传送。
  3. 内核周期性地为日志中每个完成的事务激活检查活动。

检查点主要验证由 journal_commit_transaction() 触发的 I/O 数据传送是否已经成功结束。
如果是,则从日志中删除事务。

总结:write() 开始;开始一个新的原子操作;确定逻辑块号,将元数据缓冲区加入到活动事务链表;commit_write:把缓冲区写入磁盘,原子操作关闭;write() 结束;JDB 事务中的元数据缓冲区激活 I/O 数据传送;周期性为每个完成事务激活检查活动。

只有当系统发生故障时,e2fsck 使用程序才扫描存放在文件系统中的日志,并重新安排完成的事务中的日志记录所描述的所有写操作。

给TA打赏
共{{data.count}}人
人已打赏
安全运维

WordPress网站专用docker容器环境带Waf

2020-7-18 20:04:44

安全运维

运维安全-Gitlab管理员权限安全思考

2021-9-19 9:16:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索