已挂载文件系统上的缓存

概述

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

CacheFiles 使用用户空间守护进程来执行一些缓存管理 - 例如回收过时的节点和剔除。这被称为 cachefilesd,位于 /sbin 中。

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

CacheFiles 创建一个 misc 字符设备 - “/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’d 到掩码中以收集各种信息

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

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

用户空间守护进程扫描缓存以构建可剔除对象的表。然后以最近最少使用顺序剔除这些。一旦在表中腾出空间,就会启动对缓存的新扫描。如果它们的 atimes 已更改,或者内核模块说它仍在 使用它们,则会跳过对象。

缓存结构

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 和参与者安全标签) - 而不会在进程成为其他进程执行的操作的目标时更改进程的安全上下文(因此信号传递等仍然可以正常工作)。

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

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

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

    cachefilesd_t
    

    并要求 LSM 提供一个安全 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 仅赋予其非常有限的权限:它可以扫描目录、统计文件以及擦除文件和目录。它可能无法读取或写入缓存中的文件,因此它被排除访问其中缓存的数据;它也不允许在缓存中创建新文件。

以下位置提供了策略源文件

和更高版本。在该 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 ops。因此,VFS 和 LSM 可能会拒绝 CacheFiles 访问缓存数据,因为在某些情况下,缓存代码在对 netfs 发出原始 syscall 的任何进程的安全上下文中运行。

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

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

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

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

存在 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 jiffies 和 HZ-1 jiffies 之间的每个时间段内的次数的细分。列如下

时间测量

查找

在后备 fs 上执行查找所需的时间

MKDIRS

在后备 fs 上执行 mkdir 所需的时间

CREATES

在后备 fs 上执行创建所需的时间

每行显示花费特定时间范围内的事件数。每个步骤的大小为 1 jiffy。JIFS 列指示涵盖的特定 jiffy 范围,SECS 字段指示等效的秒数。

调试

如果启用了 CONFIG_CACHEFILES_DEBUG,可以通过调整以下位置中的值来启用 CacheFiles 设施的运行时调试

/sys/module/cachefiles/parameters/debug

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

BIT

VALUE

STREAM

POINT

0

1

常规

函数入口跟踪

1

2

函数退出跟踪

2

4

常规

应将适当的值集 OR’d 在一起,并将结果写入控制文件。例如

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

将打开所有函数入口调试。

按需读取

当在其原始模式下工作时,CacheFiles 充当远程联网 fs 的本地缓存 - 而在按需读取模式下,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,用户守护进程可以对缓存文件执行 write/llseek 文件操作。

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

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

在实现用户守护进程时,请注意 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 字段匹配。