已挂载文件系统上的缓存

概述

CacheFiles 是一个缓存后端,旨在将本地类型的已挂载文件系统(如 Ext3)上的目录用作缓存。

CacheFiles 使用用户空间守护进程来完成一些缓存管理,例如回收过时的节点和剔除。这个守护进程叫做 cachefilesd,位于 /sbin 中。

缓存的文件系统和数据完整性仅与提供后备服务的文件的文件系统一样好。请注意,CacheFiles 不会尝试记录任何内容,因为各种文件系统的日志接口本质上非常具体。

CacheFiles 创建一个杂项字符设备 - “/dev/cachefiles” - 用于与守护进程通信。一次只能有一个程序打开它,并且当它打开时,缓存至少部分存在。守护进程会打开它并向下发送命令来控制缓存。

CacheFiles 目前仅限于单个缓存。

CacheFiles 尝试在文件系统上维护至少一定比例的可用空间,并在必要时通过剔除其包含的对象来缩小缓存,从而腾出空间 - 请参阅“缓存剔除”部分。这意味着它可以与一组活动数据放在同一介质上,并会扩展以利用可用空间,并在该组数据需要更多空间时自动收缩。

要求

CacheFiles 及其守护进程的使用要求系统和缓存文件系统中具备以下功能:

  • dnotify。

  • 扩展属性 (xattrs)。

  • openat() 及其相关函数。

  • 在文件系统中的文件上支持 bmap() (FIBMAP ioctl)。

  • 使用 bmap() 来检测文件末尾的部分页面。

强烈建议在用作缓存的 Ext3 文件系统上启用 “dir_index” 选项。

配置

缓存通过 /etc/cachefilesd.conf 中的脚本进行配置。这些命令设置好缓存以供使用。以下脚本命令可用:

brun <N>%, bcull <N>%, bstop <N>%, frun <N>%, fcull <N>%, fstop <N>%

配置剔除限制。可选。请参阅有关剔除的部分。默认值分别为 7%(run)、5%(cull)和 1%(stop)。

以 “b” 开头的命令是文件空间(块)限制,以 “f” 开头的命令是文件计数限制。

dir <path>

指定包含缓存根目录的目录。必填。

tag <name>

指定一个标签,供 FS-Cache 用于区分多个缓存。可选。默认值为 “CacheFiles”。

debug <mask>

指定一个数字位掩码来控制内核模块中的调试。可选。默认值为零(全部关闭)。可以将以下值 OR 运算到掩码中以收集各种信息:

1

打开函数入口的跟踪(_enter() 宏)

2

打开函数退出的跟踪(_leave() 宏)

4

打开内部调试点的跟踪 (_debug())

此掩码也可以通过 sysfs 设置,例如

echo 5 > /sys/module/cachefiles/parameters/debug

启动缓存

通过运行守护进程来启动缓存。守护进程打开缓存设备,配置缓存,并告诉它开始缓存。此时,缓存绑定到 fscache,缓存变为活动状态。

守护进程的运行方式如下:

/sbin/cachefilesd [-d]* [-s] [-n] [-f <configfile>]

标志为:

-d

增加调试级别。可以多次指定此选项,并且它会累积增加。

-s

将消息发送到 stderr 而不是 syslog。

-n

不守护进程化,也不进入后台。

-f <configfile>

使用替代配置文件而不是默认配置文件。

应避免的事项

请勿在缓存中挂载其他内容,这会导致问题。内核模块包含其自己的精简路径遍历工具,它会忽略挂载点,但守护进程无法避免这些挂载点。

请勿在缓存处于活动状态时在缓存中创建、重命名或取消链接文件和目录,这可能会导致状态变得不确定。

重命名缓存中的文件可能会使对象看起来像其他对象(文件名是查找键的一部分)。

请勿更改或删除缓存附加到缓存文件的扩展属性,因为这会导致缓存状态管理混乱。

请勿在缓存中创建文件或目录,以免缓存混淆或提供不正确的数据。

请勿更改缓存中文件的 chmod。模块会使用最小权限创建内容,以防止随机用户直接访问它们。

缓存剔除

缓存可能偶尔需要剔除以腾出空间。这涉及从缓存中丢弃比其他任何对象使用频率都低的对象。剔除基于数据对象的访问时间。空闲目录如果未使用则会被剔除。

缓存剔除是根据底层文件系统中可用的块百分比和可用文件百分比完成的。有六个“限制”:

brun,frun

如果缓存中的可用空间量和可用文件数量都高于这两个限制,则关闭剔除。

bcull,fcull

如果缓存中的可用空间量或可用文件数量低于任一限制,则开始剔除。

bstop,fstop

如果缓存中的可用空间量或可用文件数量低于任一限制,则不允许再分配磁盘空间或文件,直到剔除操作再次将它们提升到这些限制之上。

必须按如下方式配置它们:

0 <= bstop < bcull < brun < 100
0 <= fstop < fcull < frun < 100

请注意,这些是可用空间和可用文件的百分比,并且_不_显示为 “df” 程序显示的百分比减去 100。

用户空间守护进程扫描缓存以建立一个可剔除对象的表。然后,按照最近最少使用的顺序剔除这些对象。一旦表中腾出空间,就会开始对缓存进行新的扫描。如果对象的 atime 已更改或内核模块表示它仍在被使用,则会跳过这些对象。

缓存结构

CacheFiles 模块将在给定的目录中创建两个目录:

  • cache/

  • graveyard/

活动缓存对象都驻留在第一个目录中。CacheFiles 内核模块将任何已停用或剔除的、无法简单取消链接的对象移动到废弃目录,守护进程将从该目录中实际删除它们。

守护进程使用 dnotify 来监视废弃目录,并将删除其中出现的任何内容。

该模块将索引对象表示为文件名 “I...” 或 “J...” 的目录。请注意,“cache/” 目录本身就是一个特殊的索引。

如果数据对象没有子对象,则将其表示为文件,如果有子对象,则表示为目录。它们的文件名都以 “D...” 或 “E...” 开头。如果表示为目录,则数据对象将在目录中有一个名为 “data” 的文件,该文件实际保存数据。

特殊对象与数据对象类似,但它们的文件名以 “S...” 或 “T...” 开头。

如果一个对象有子对象,那么它将被表示为一个目录。在表示目录中,会立即出现一组以子对象键的哈希值命名的目录,并在前面加上 “@”。如果可能,子对象的表示形式将放置在此目录中。

 /INDEX    /INDEX     /INDEX                            /DATA FILES
/=========/==========/=================================/================
cache/@4a/I03nfs/@30/Ji000000000000000--fHg8hi8400
cache/@4a/I03nfs/@30/Ji000000000000000--fHg8hi8400/@75/Es0g000w...DB1ry
cache/@4a/I03nfs/@30/Ji000000000000000--fHg8hi8400/@75/Es0g000w...N22ry
cache/@4a/I03nfs/@30/Ji000000000000000--fHg8hi8400/@75/Es0g000w...FP1ry

如果键太长,以至于在添加装饰后超出了 NAME_MAX,那么它将被切成碎片,前几个碎片将用于创建一个目录嵌套,最后一个碎片将是最后一个目录中的对象。中间目录的名称将以 “+” 开头。

J1223/@23/+xy...z/+kl...m/Epqr

请注意,键是原始数据,不仅可能超出 NAME_MAX 大小,还可能包含 “/” 和 NUL 字符之类的东西,因此它们可能不适合直接转换为文件名。

为了处理这个问题,CacheFiles 将直接使用适合打印的文件名,并对不直接适合的文件名进行 “base-64” 编码。对象文件名的两个版本指示了编码:

对象类型

可打印

已编码

索引

“I...”

“J...”

数据

“D...”

“E...”

特殊

“S...”

“T...”

中间目录始终是 “@” 或 “+”,视情况而定。

缓存中的每个对象都有一个扩展属性标签,该标签保存对象类型 ID(区分特殊对象所必需)和来自 netfs 的辅助数据。后者用于检测缓存中的过时对象并更新或停用它们。

请注意,CacheFiles 会从缓存中删除任何无法识别的文件或类型不正确的文件(例如 FIFO 文件或设备文件)。

安全模型和 SELinux

CacheFiles 的实现旨在正确处理 Linux 内核的 LSM 安全特性和 SELinux 功能。

CacheFiles 面临的问题之一是,它通常代表进程行事,并在该进程的上下文中运行,这包括不适合访问缓存的安全上下文 - 要么是因为缓存中的文件无法被该进程访问,要么是因为如果该进程在缓存中创建文件,则该文件可能无法被其他进程访问。

CacheFiles 的工作方式是临时更改进程的行为安全上下文(fsuid、fsgid 和 actor 安全标签) - 而不更改该进程作为其他进程执行的操作的目标时的安全上下文(因此信号和其他类似操作仍然可以正常工作)。

当要求 CacheFiles 模块绑定到其缓存时,它会

  1. 查找附加到根缓存目录的安全标签,并将其用作创建文件的安全标签。默认情况下,这是

    cachefiles_var_t
    
  2. 查找发出绑定请求的进程(假定为 cachefilesd 守护进程)的安全标签,默认情况下将是

    cachefilesd_t
    

    并要求 LSM 提供一个安全 ID,它应该根据守护进程的标签作为该 ID 行事。默认情况下,这将是

    cachefiles_kernel_t
    

    SELinux 根据策略中的此形式的规则将守护进程的安全 ID 转换为模块的安全 ID

    type_transition <daemon's-ID> kernel_t : process <module's-ID>;
    

    例如

    type_transition cachefilesd_t kernel_t : process cachefiles_kernel_t;
    

模块的安全 ID 使其有权在缓存中创建、移动和删除文件和目录,查找和访问缓存中的目录和文件,设置和访问缓存对象上的扩展属性,以及读取和写入缓存中的文件。

守护进程的安全 ID 只赋予它非常有限的权限:它可以扫描目录、stat 文件以及擦除文件和目录。它不能读取或写入缓存中的文件,因此它不能访问其中缓存的数据;也不允许在缓存中创建新文件。

策略源文件位于

以及更高版本中。在该 tarball 中,请参阅文件

cachefilesd.te
cachefilesd.fc
cachefilesd.if

它们由 RPM 直接构建和安装。

如果正在使用基于非 RPM 的系统,则将上述文件复制到它们自己的目录并运行

make -f /usr/share/selinux/devel/Makefile
semodule -i cachefilesd.pp

在构建之前,您需要安装 checkpolicy 和 selinux-policy-devel。

默认情况下,缓存位于 /var/fscache 中,但如果希望将其放在其他位置,则必须更改上述策略文件,或者必须安装辅助策略来标记缓存的备用位置。

有关如何在 SELinux 处于强制模式时添加辅助策略以使缓存能够位于其他位置的说明,请参阅

/usr/share/doc/cachefilesd-*/move-cache.txt

安装 cachefilesd rpm 时;或者,该文档可以在源代码中找到。

关于安全性的说明

CacheFiles 使用 task_struct 中的拆分安全性。它会分配自己的 task_security 结构,并在代表另一个进程(在该进程的上下文中)行事时,将 current->cred 重定向到该结构。

这样做的原因是它调用 vfs_mkdir() 和其他类似函数,而不是绕过安全性和直接调用 inode 操作。因此,VFS 和 LSM 可能会拒绝 CacheFiles 访问缓存数据,因为在某些情况下,缓存代码是在发出 netfs 上原始 syscall 的任何进程的安全上下文中运行的。

此外,如果 CacheFiles 创建文件或目录,则该对象创建的安全参数(UID、GID、安全标签)将从发出系统调用的进程派生,从而可能阻止其他进程(包括 CacheFiles 的缓存管理守护进程 (cachefilesd))访问缓存。

需要的是临时覆盖发出系统调用的进程的安全性。但是,我们不能仅仅就地更改安全数据,因为这会影响进程作为对象,而不仅仅是作为主体。这意味着它可能会丢失信号或 ptrace 事件,例如,并影响进程在 /proc 中的显示方式。

因此,CacheFiles 利用了目标安全性 (task->real_cred) 和主观安全性 (task->cred) 之间安全性的逻辑拆分。目标安全性保存进程的内在安全属性,并且永远不会被覆盖。这是在 /proc 中显示的内容,并且是当进程成为其他进程执行的操作(例如 SIGKILL)的目标时使用的内容。

主观安全性保存进程的活动安全属性,并且可能会被覆盖。这在外部不可见,并且当进程对另一个对象执行操作时使用,例如 SIGKILL 另一个进程或打开文件。

存在 LSM 钩子,允许 SELinux(或 Smack 或其他)拒绝 CacheFiles 在特定安全标签上下文中运行的请求,或使用另一个安全标签创建文件和目录的请求。

统计信息

如果 FS-Cache 是使用以下选项启用的方式编译的

CONFIG_CACHEFILES_HISTOGRAM=y

那么它将收集某些统计信息并通过 proc 文件显示它们。

/proc/fs/cachefiles/histogram

cat /proc/fs/cachefiles/histogram
JIFS  SECS  LOOKUPS   MKDIRS    CREATES
===== ===== ========= ========= =========

这显示了各种任务在 0 个节拍和 HZ-1 个节拍之间运行所花费的时间次数的细分。列如下

时间测量

查找

在后备文件系统上执行查找所需的时间长度

MKDIRS

在后备文件系统上执行 mkdir 所需的时间长度

CREATES

在后备文件系统上执行 create 所需的时间长度

每行显示在特定时间范围内发生的事件数。每个步骤的大小为 1 个节拍。JIFS 列指示所涵盖的特定节拍范围,SECS 字段指示等效的秒数。

调试

如果启用了 CONFIG_CACHEFILES_DEBUG,则可以通过调整以下值来启用 CacheFiles 功能的运行时调试

/sys/module/cachefiles/parameters/debug

这是要启用的调试流的位掩码

0

1

常规

函数入口跟踪

1

2

函数出口跟踪

2

4

常规

应将适当的一组值进行“或”运算,并将结果写入控制文件。例如

echo $((1|4|8)) >/sys/module/cachefiles/parameters/debug

将启用所有函数入口调试。

按需读取

当以其原始模式工作时,CacheFiles 充当远程网络文件系统的本地缓存 - 而在按需读取模式下,CacheFiles 可以提升需要按需读取语义的场景,例如容器映像分发。

这两种模式之间的本质区别在于发生缓存未命中时:在原始模式下,netfs 将从远程服务器获取数据,然后将其写入缓存文件;在按需读取模式下,获取数据并将其写入缓存的任务委托给用户守护进程。

应启用 CONFIG_CACHEFILES_ONDEMAND 以支持按需读取模式。

协议通信

按需读取模式使用一个简单的协议在内核和用户守护进程之间进行通信。该协议可以建模为

kernel --[request]--> user daemon --[reply]--> kernel

CacheFiles 会在需要时向用户守护进程发送请求。用户守护进程应轮询 devnode(“/dev/cachefiles”)以检查是否有待处理的请求需要处理。当有待处理的请求时,将返回一个 POLLIN 事件。

然后,用户守护进程会读取 devnode 以获取要处理的请求。应该注意的是,每次读取只能获得一个请求。当它完成处理请求时,用户守护进程应将答复写入 devnode。

每个请求都以以下形式的消息头开始

struct cachefiles_msg {
        __u32 msg_id;
        __u32 opcode;
        __u32 len;
        __u32 object_id;
        __u8  data[];
};

其中

  • msg_id 是一个唯一的 ID,用于标识所有待处理请求中的此请求。

  • opcode 指示此请求的类型。

  • object_id 是一个唯一的 ID,用于标识操作的缓存文件。

  • data 指示此请求的有效负载。

  • len 指示此请求的整个长度,包括标头和后面的特定于类型的有效负载。

开启按需模式

一个可选参数可用于“bind”命令

bind [ondemand]

当“bind”命令未提供任何参数时,它默认为原始模式。当它提供“ondemand”参数时,即“bind ondemand”,将启用按需读取模式。

OPEN 请求

当 netfs 第一次打开缓存文件时,将向用户守护进程发送一个带有 CACHEFILES_OP_OPEN 操作码的请求,也称为 OPEN 请求。有效负载格式如下

struct cachefiles_open {
        __u32 volume_key_size;
        __u32 cookie_key_size;
        __u32 fd;
        __u32 flags;
        __u8  data[];
};

其中

  • data 包含 volume_key,紧接着是 cookie_key。卷密钥是一个以 NUL 结尾的字符串;cookie 密钥是二进制数据。

  • volume_key_size 指示卷密钥的大小(以字节为单位)。

  • cookie_key_size 指示 cookie 密钥的大小(以字节为单位)。

  • fd 指示一个匿名 fd,引用缓存文件,用户守护进程可以通过该 fd 对缓存文件执行写入/llseek 文件操作。

用户守护进程可以使用给定的 (volume_key, cookie_key) 对来区分所请求的缓存文件。通过给定的匿名 fd,用户守护进程可以在后台获取数据并将其写入缓存文件,即使内核尚未触发缓存未命中。

请注意,每个缓存文件都有一个唯一的 object_id,但它可能有多个匿名 fd。用户守护进程可能会通过 dup() 从 @fd 字段指示的初始匿名 fd 复制匿名 fd。因此,每个 object_id 可以映射到多个匿名 fd,而用户守护进程本身需要维护这种映射。

在实现用户守护进程时,请注意 RLIMIT_NOFILE、/proc/sys/fs/nr_open/proc/sys/fs/file-max。通常这些不需要很大,因为它们与打开的设备 blob 的数量有关,而不是每个文件系统打开的文件数量。

用户守护进程应该通过在 devnode 上发出 “copen”(完成打开)命令来回复 OPEN 请求。

copen <msg_id>,<cache_size>

其中

  • msg_id 必须与 OPEN 请求的 msg_id 字段匹配。

  • 当 >= 0 时,cache_size 指示缓存文件的大小;当 < 0 时,cache_size 指示用户守护进程遇到的任何错误代码。

CLOSE 请求

当 cookie 被撤回时,将向用户守护进程发送 CLOSE 请求(操作码为 CACHEFILES_OP_CLOSE)。这告诉用户守护进程关闭与给定 object_id 关联的所有匿名 fd。CLOSE 请求没有额外的负载,不应被回复。

READ 请求

当按需读取模式下遇到缓存未命中时,CacheFiles 将向用户守护进程发送 READ 请求(操作码为 CACHEFILES_OP_READ)。这告诉用户守护进程获取请求的文件范围的内容。有效负载的形式如下:

struct cachefiles_read {
        __u64 off;
        __u64 len;
};

其中

  • off 指示请求的文件范围的起始偏移量。

  • len 指示请求的文件范围的长度。

当收到 READ 请求时,用户守护进程应获取请求的数据并将其写入由 object_id 标识的缓存文件。

当它完成处理 READ 请求时,用户守护进程应通过对与 READ 请求中给定的 object_id 关联的其中一个匿名 fd 使用 CACHEFILES_IOC_READ_COMPLETE ioctl 来回复。ioctl 的形式如下:

ioctl(fd, CACHEFILES_IOC_READ_COMPLETE, msg_id);

其中

  • fd 是与给定的 object_id 关联的其中一个匿名 fd。

  • msg_id 必须与 READ 请求的 msg_id 字段匹配。