[译] OverlayFS Document

  • 2018-07-10
  • 194
  • 0

translate by reposkeeper
点击查看原文

Written by: Neil Brown
Please see MAINTAINERS file for where to send questions.

译注: 下面 stat 的结构体

struct stat {
   dev_t     st_dev;     /* ID of device containing file */
   ino_t     st_ino;     /* inode number */
   mode_t    st_mode;    /* protection */
   nlink_t   st_nlink;   /* number of hard links */
   uid_t     st_uid;     /* user ID of owner */
   gid_t     st_gid;     /* group ID of owner */
   dev_t     st_rdev;    /* device ID (if special file) */
   off_t     st_size;    /* total size, in bytes */
   blksize_t st_blksize; /* blocksize for file system I/O */
   blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
   time_t    st_atime;   /* time of last access */
   time_t    st_mtime;   /* time of last modification */
   time_t    st_ctime;   /* time of last status change */
};

堆叠文件系统

本文描述关于Linux上,使用一个新方式来提供堆叠文件系统功能(有时也指union文件系统)的原型。一个堆叠文件系统试图呈现一个文件系统,其结果堆叠在其他文件系统的上面。

因为一些技术原因,将其看作一个普通的文件系统一定是不行的。希望通过一些例子来忽略这其中的差别。

堆叠对象

堆叠文件系统是“混合的”,因为文件系统中的对象也许并不属于这个文件系统。很多情况下,无法区分联合文件系统访问对象和在原始文件系统中访问相应的对象。从 stat(2) 的 'st_dev' 字段可以看出来。

当目录汇报一个堆叠文件系统的 st_dev 时,非目录对象会汇报一个提供此对象的下层文件系统或者上层文件系统 的 'st_dev'。同样的,st_ino 只有在和 st_dev 绑定时才是唯一的,而且两者都可以在非目录对象上改变生命周期。许多应用和工具会忽略这些值,也没有影响。

当所有的堆叠层都在同一个底层文件系统这个特殊情况下,所有的对象报告的 st_dev 来自堆叠文件系统,st_ino 来自底层文件系统。这将使堆叠挂载更兼容的文件系统扫描器,并且将堆叠对象和原始文件系统中的相应对象区分开来。

在64位系统上,尽管不是所有的堆叠层都在同一个底层文件系统,也可以实现一个同样的兼容行为包括 xino 特性。xino 特性通过真实对象的 st_ino 和 底层 fsid 索引 组成一个唯一的对象识别符。如果所有的底层文件系统都支持使用32位inode号编码(比如:ext4)的NFS文件操作和导出操作,堆叠文件系统将使用inode号高位作为 fsid。甚至当底层文件系统使用 64位 inode号,用户依然可以使用 xino特性,通过mount参数-o xino=on。这用于使用像 xfs、tmpfs 这些64位inode号的底层操作系统,不过不太可能使用高位inode号。

上层 和 下层

堆叠文件系统结合两个文件系统,一个上层文件系统,一个下层文件系统,当一个名字存在于两个文件系统,上层文件系统的可见,下层文件系统隐藏,如果是目录时,则与上层目录合并。

对于上层和下层,用目录树会比文件系统更准确一点,因为两个目录树在同一个文件系统是完全没问题的,而且,对于每一个上层或下层,并不要求提供文件系统的根。

下层文件系统可以使任何Linux支持的文件系统,且不需要可写权限。下层文件系统甚至可以是其他overlayfs。上层文件系统需要可写,必须支持创建可信的 .* 扩展,而且必须提供在 readdir 返回中合法的 d_type,所以,NFS是不适合的。

一个基于只读文件系统的只读的堆叠,则可以使用任意文件系统类型。

目录

堆叠主要涉及目录。如果一个目录存在于上层和下层文件系统中,指向非目录对象也一样,那么下层对象被隐藏,名字仅指向上层对象。

当上层和下层对象都是目录时,会形成一个合并的目录。

挂载时,有挂载选项 “lowerdir” 和 “upperdir” 的两个目录会结合成一个合并目录:

mount -t overlay overlay -olowerdir=/lower,upperdir=/upper,\
workdir=/work /merged

"workdir" 需要是空目录,且和上层目录在同一个文件系统上。

然后,只要在这样的合并目录中请求查找,就会在每个实际目录中执行查找,并将结果缓存在堆叠文件系统的目录项中。如果两个实际查找都找到目录,则创建一个合并目录。如果只有一个存储:如果上层有,则用上层,否则使用下层。

只有目录的名字列表被合并。其他的内容,例如元数据,扩展属性只报告上层目录。下层目录的这些属性会被隐藏。

whiteouts 和 opaque 目录

为了达到不改变下层文件系统就能支持支持 rm 和 rmdir,堆叠文件系统需要在上层文件系统记录文件已经被删除。这是通过 whiteoutsopaque 目录完成的(非目录对象是不透明的)。

whiteout 被创建为一个具有 0/0 设备号的字符设备。当在合并目录上部发现 whiteout ,将忽略低级别中的任何匹配名称,whiteout 本身也是隐藏的。

通过设置 xattr 中的 "trusted.overlay.opaque" 为 y 来是目录变为不透明。上层文件系统包含一个叫 opaque 的目录,则忽略下层文件系统中具有相同名字的任何目录。

readdir

当在合并目录请求 'readdir', 上层和下层目录分别读取,并以一个简单的方法合并名称列表(上层首先读取,然后是下层,如果已经存在,则不再重新添加)。合并的名称列表缓存在 'struct file' 并在文件打开期间一直存在。如果目录被两个进程同时打开读取,则他们有各自的缓存。 用 seekdir到目录开头(offset 0)紧接着 readdir 会导致缓存失效并重建。

这意味着,读取目录不会显示对合并目录的更改,很多程序不太会注意到这一点。

当读取目录时,寻找偏移被顺序分配。因此,如果

  • 读取目录一部分
  • 记录偏移位置,并关闭目录
  • 隔一会重新打开目录
  • 定位到记录的偏移位置

文件名列表的旧位置和新位置之间可能没什么联系,尤其是如果目录中有任何更改。

在没有合并的目录中使用 readdir 会直接被底层目录处理(上层或下层)。

重命名目录

当重命名一个在下层或被合并的目录(目录并不是在上层被开始创建)overlayfs可以用两种方式处理:

  1. 返回 EXDEV 错误:这个错误是 rename(2) 试图跨文件系统边界移动文件或者目录时返回。因此应用通常准备好处理这个错误(mv(1) 例如 递归复制目录树)。这是个默认的行为。

  2. 如果 "重定向目录" 特性被启用,则会复制目录(不是内容)。然后 "trusted.overlay.redirect" 扩展属性设置为相对于堆叠层根的原始位置。最后目录将移动到新位置。

这里有几种方法来调节 "redirect_dir" 特性。

内核配置选项:

  • OVERLAY_FS_REDIRECT_DIR
    如果启用,那么 redirect_dir 会默认开启
  • OVERLAY_FS_REDIRECT_ALWAYS_FOLLOW
    如果启用,那么默认情况下始终遵循重定向。开启会导致只有少量安全配置。仅当考虑与具有 redirect_dir的内核的向后兼容性时才启用。即使关闭,也会遵循重定向。

模块参数(也可以通过 /sys/module/overlay/parameters/* 调整)

  • "redirect_dir=BOOL":
    参见 上面的内核配置 OVERLAY_FS_REDIRECT_DIR
  • "redirect_always_follow=BOOL":
    参见 上面的内核配置 OVERLAY_FS_REDIRECT_ALWAYS_FOLLOW
  • "redirect_max=NUM":
    重定向绝对路径的最大字符数(默认 256)

挂载参数

  • "redirect_dir=on":
    启用重定向
  • "redirect_dir=follow":
    不创建重定向,只遵循
  • "redirect_dir=off":
    如果内核参数/模块参数打开了 redirect_always_follow ,不创建重定向只遵循
  • "redirect_dir=nofollow":
    不创建,也不遵循重定向(如果没有打开 redirect_always_follow 相当于 "redirect_dir=off" )

当启用NFS导出时,所有复制目录由下层inode文件句柄编制索引,而且上层目录的文件句柄被存储在 "trusted.overlay.upper" 的索引项扩展属性中。查找合并目录时,如果上层目录与存储在索引中的文件句柄不匹配,这表示有多个上层目录可能被重定向到了同一个下层目录中。在这种情况下,查找返回可能不一致的相关错误和警告。

由于下层重定向不能通过索引验证,因此没有上层的堆叠文件系统中启用NFS导出支持时,需要关闭重定向跟随("redirect_dir=nofollow")

非目录对象

不是目录的对象(文件,链接,设备文件等等)是由上层或下层文件系统来呈现。当对一个下层文件系统文件做写请求时,比如,打开并写入,变更元数据等,文件首先从下层文件系统复制到上层文件系统(复制动作)。注意,创建一个硬链接也需要复制动作,软链接则不需要。

复制动作有可能失败,比如文件被打开准备写入,但数据还没有改变。

复制动作首先保证目录存在于上层文件系统,如果不存在就创建。然后,基于相同的元数据创建对象(所有人,权限,变更时间,软链接目标 等),如果对象是文件,从下层文件系统复制数据到上层文件系统。最后,复制所有扩展属性。

一旦复制动作完成,堆叠文件系统就提供直接访问上层文件系统的文件,后续文件操作几乎不会被堆叠文件系统注意到(但名称操作,比如 重命名,取消链接 会被处理)。

多个下层文件系统

多个下层可以通过":"作为分隔符来提供,比如:

  mount -t overlay overlay -olowerdir=/lower1:/lower2:/lower3 /merged

上面的例子中,"upperdir=" 和 "workdir=" 省略掉了,这种情况下,堆叠会变为只读。

指定的下层目录会从右向左入栈堆叠。在上面的例子中,lower1在最顶层,lower2在中间,lower3在最下层。

分享和复制层

下层可能会在多个堆叠挂载上,这是一个常见的用法。一个堆叠挂载可能和另一个使用同样的下层,而且有可能使用其他下层路径的父级或子级的路径作为下层路径。

不允许使用已由另一个堆叠挂载使用的上层路径或 workdir 路径,可能会因EBUSY失败。不可以使用重叠路径的一部分,但不会因为EBUSY失败。如果从两个共享或堆叠上层或workdir路径的 overlayfs挂载访问文件,那么,覆盖行为是未定义的,不过不会导致崩溃或者死锁。

除非启用了inode索引功能,否则允许使用上层路径挂载堆叠层,此上层路径之前已由另一个已挂载堆叠层组合不同的下层路径使用。

对于inodes索引功能,在第一次挂载时,一个下层根目录的NFS文件句柄连同下层文件系统的UUID一起编码存储在上层根目录的 "trusted.overlay.origin" 中。随后的挂载中,下层根目录句柄和下层文件系统UUID会同已经存储在上层根目录的原始数据对比。验证失败的话,mount会因 ESTALE 失败。如果下层文件系统不支持 NFS导出,或者下层文件系统没有合法UUID,或者上层文件系统不支持扩展属性,开启inodes索引的overlayfs挂载时会因为 EOPNOTSUPP失败。

复制堆叠层到在相同/不同的下层文件系统上的不同的目录树,,甚至是不同机器上是常用的做法。使用inodes索引功能,尝试挂载一个复制层就会无法验证下层文件句柄。

非标准行为

复制动作必须创建一个新的相同文件,并将其重命名为旧的名字,引用这个inode的任何打开的文件都将访问旧数据。

新文件可能在不同的文件系统,所以真实文件的 st_dev 和 st_ino 都可能改变。为了防止值在复制动作时被更改,stat(2) 返回的堆叠对象的 st_dev 和 st_ino 的值通常不同于真实文件 stat(2) 的返回。

除非 "xino" 特性被启用,否则当堆叠层不在同一个底层文件系统时,对于同一堆叠文件系统中的两个非目录对象,st_dev 的值可能不同,并且目录对象的 st_ino 可能不是持久的,甚至在任何已挂载时仍可能改变。

除非 "inode索引" 特性被启用,如果一个有多个硬链接的文件被复制,这回破坏链接。变更不会扩散给其他指向同一个inode的名称。

除非 "redirect_dir" 特性被启用,在下层或合并目录进行 rename(2) 会因为 EXDEV 失败。

对底层文件系统的更改

当堆叠没有挂载时,离线变更允许修改上层和下层树。

不允许变更已挂载的堆叠文件系统的部分底层文件系统。如果变更底层文件系统,堆叠的行为是未定义的,但不会崩溃或死锁。

当堆叠的 NFS导出功能启用,堆叠文件系统的底层下层离线变更时的行为不同于禁用时。

在一个复制行为中,下层inode的NFS文件句柄连同下层文件系统的UUID会编码存在上层inode的扩展属性 "trusted.overlay.origin" 中。

当 NFS导出功能启用,查找合并目录,在查找路径或者在"trusted.overlay.redirect"扩展属性中找到一个下层目录时,将会验证发现的下层目录文件句柄和下层文件系统UUID是否匹配拷贝时存放的原始文件句柄。如果不匹配,目录不会被合并到上层目录。

NFS导出

当底层文件系统支持 NFS导出,且 "nfs_export" 特性启用时,堆叠文件系统可以被导出到 NFS。

使用 "nfs_export" 功能,进行任何下层对象的复制动作时,会在index目录下创建一个index项。index项的名字是复制原始文件句柄的十六进制表示。对于非目录对象,index项是一个硬链接指向上层inode。对于目录对象,index项有一个扩展属性 "trusted.overlay.upper" 存放编码后的上层目录inode文件句柄。

当对一个堆叠文件系统对象的文件句柄编码时,会应用下面的规则:

  1. 对于非上层对象,从下层inode编码一个下层文件句柄
  2. 对于索引对象,从复制源编码一个下层文件句柄
  3. 对于纯上层对象和无索引上层对象,从上层inode编码一个上层文件句柄

编码的堆叠文件句柄包括:

  • Header,包括路径类型信息(上层/下层)
  • 底层文件系统的 UUID
  • 底层inode的底层文件系统编码

这种编码格式和存储在扩展属性 "trusted.overlay.origin" 中的文件句柄的编码格式一样。

当解码一个堆叠文件句柄是,遵循下面的步骤:

  1. 通过UUID找到底层和路径类型信息。
  2. 解码底层文件系统句柄给底层目录项。
  3. 对于下层文件句柄,通过名称在index目录查找句柄。
  4. 如果 whiteout 在index中,返回 ESTALE。这代表文件堆叠对象在其文件句柄编码后被删除。
  5. 对于非目录,如果能找到,从解码的底层目录项中实例化一个断开连接的堆叠目录项(路径类型和索引inode)。
  6. 对于目录,通过连接的底层解码的目录项(路径类型和索引)查找已连接的堆叠目录项。

解码一个非目录文件句柄可能返回一个断开连接的目录项。该断开连接目录项的复制动作会创建一个没有上层别名的上层索引项。

当堆叠文件系统有多个下层时,一个中间层目录可能有到下层目录的 "重定向" 。由于中间层"重定向"没有编制索引,因此一个从"重定向"原始目录编码的下层文件句柄不能用来查找中间或上层目录。类似的,从"重定向"原始目录的后代编码的一个下层文件句柄,不能用来重建已连接的堆叠路径。为了减少无法从下层文件句柄解码的目录的情况,这些目录在编码时被复制并编码为上层文件句柄。在一个没有上层覆盖的堆叠文件系统上,不能用此缓解。此设置中的NFS导出需要关闭重定向跟随("redirect_dir=nofollow")。

堆叠文件系统不支持非目录可连接文件句柄,所以用 'subtree_check' exportfs配置导出会导致通过NFS查找文件失败。

当启用NFS导出功能,所有的目录索引项会在挂载时确认上层文件句柄没有过期。这个验证会在某些情况下会导致很大的开销。

测试套件

有一个测试套件最初由David Howells开发的,目前由Amir Goldstein维护:

  https://github.com/amir73il/unionmount-testsuite.git

以root身份运行:

# cd unionmount-testsuite
# ./run --ov --verify

作者和出处(reposkeeper) 授权分享 By CC BY-SA 4.0 Creative Commons License

关注微信公众号,获取新文章的推送!
qrcode

评论

还没有任何评论,你来说两句吧

发表评论

*

code