RPC 缓存

本文档简要介绍了 sunrpc 层中的缓存机制,该机制专门用于 NFS 身份验证。

缓存

缓存取代了旧的导出表,并允许缓存各种各样的值。

存在许多结构相似的缓存,尽管它们的内容和用途可能大相径庭。 有一组用于管理这些缓存的通用代码。

可能需要的缓存示例包括

  • 从 IP 地址到客户端名称的映射

  • 从客户端名称和文件系统到导出选项的映射

  • 从 UID 到 GID 列表的映射,以解决 NFS 16 个 gid 的限制。

  • 对于没有统一 uid 分配的站点,在本地 UID/GID 和远程 UID/GID 之间进行映射

  • 从网络标识到加密身份验证的公钥的映射。

通用代码处理以下事项,例如

  • 使用正确的锁定进行常规缓存查找

  • 支持“NEGATIVE”以及肯定条目

  • 允许缓存项具有 EXPIRED 时间,并在缓存项过期且不再使用时将其删除。

  • 向用户空间发出请求以填写缓存条目

  • 允许用户空间直接设置缓存中的条目

  • 延迟依赖于尚未完成的缓存条目的 RPC 请求,并在缓存条目完成时重放这些请求。

  • 在旧条目过期时清除它们。

创建缓存

  • 缓存需要存储数据。 这是一个结构定义的形式,该定义必须包含 struct cache_head 作为一个元素,通常是第一个。它还将包含一个键和一些内容。每个缓存元素都经过引用计数,并包含用于缓存管理的过期和更新时间。

  • 缓存需要一个描述缓存的“cache_detail”结构。 这存储哈希表、一些用于缓存管理的参数以及一些详细说明如何处理特定缓存项的操作。

    这些操作是

    struct cache_head *alloc(void)

    这只是分配适当的内存并返回指向嵌入在结构中的 cache_detail 的指针

    void cache_put(struct kref *)

    当对某个项目的最后一个引用被删除时,将调用此函数。 传递的指针是指向 cache_head 中的“ref”字段。 cache_put 应该释放由“cache_init”创建的任何引用,如果设置了 CACHE_VALID,则释放由 cache_update 创建的任何引用。 然后它应该释放由“alloc”分配的内存。

    int match(struct cache_head *orig, struct cache_head *new)

    测试两个结构中的键是否匹配。 如果匹配,则返回 1,如果不匹配,则返回 0。

    void init(struct cache_head *orig, struct cache_head *new)

    从“orig”设置“new”中的“key”字段。 这可能包括引用共享对象。

    void update(struct cache_head *orig, struct cache_head *new)

    从“orig”设置“new”中的“content”字段。

    int cache_show(struct seq_file *m, struct cache_detail *cd, struct cache_head *h)

    可选。 用于提供一个 /proc 文件,其中列出了缓存的内容。 这应该只显示一项,通常只在一行上。

    int cache_request(struct cache_detail *cd, struct cache_head *h, char **bpp, int *blen)

    格式化要发送到用户空间的请求,以便实例化一个项目。 *bpp 是大小为 *blen 的缓冲区。 bpp 应该在编码的消息上向前移动,并且 *blen 应该减少以显示剩余多少可用空间。 成功时返回 0,如果空间不足或其他问题,则返回 <0。

    int cache_parse(struct cache_detail *cd, char *buf, int len)

    来自用户空间的消息已到达以填写缓存条目。 它位于长度为“len”的“buf”中。 cache_parse 应该解析它,使用 sunrpc_cache_lookup_rcu 查找缓存中的项目,并使用 sunrpc_cache_update 更新该项目。

  • 需要使用 cache_register() 注册缓存。 这会将其包含在定期清除以丢弃旧数据的缓存列表中。

使用缓存

要在缓存中查找值,请调用 sunrpc_cache_lookup_rcu,传递一个指向样本项目中 cache_head 的指针,其中填充了“key”字段。 这将被传递到 ->match 以识别目标条目。 如果未找到任何条目,将创建一个新条目,添加到缓存中,并标记为不包含有效数据。

返回的条目通常传递给 cache_check,后者将检查数据是否有效,并且可能会启动一个 up-call 以获取新数据。 如果条目为负,或者需要但无法进行 upcall,cache_check 将返回 -ENOENT,如果 upcall 挂起,则返回 -EAGAIN,如果数据有效,则返回 0;

可以向 cache_check 传递一个“struct cache_req*”。 此结构通常嵌入在实际请求中,可用于创建请求的延迟副本 (struct cache_deferred_req)。 当找到的缓存条目不是最新的,但有理由相信用户空间可能会很快提供信息时,就会这样做。 当缓存条目变为有效时,将重新访问请求的延迟副本 (->revisit)。 预计此方法将重新安排请求以进行处理。

sunrpc_cache_lookup_rcu 返回的值也可以传递给 sunrpc_cache_update 以设置该项目的内容。 将传递第二个项目,该项目应保存内容。 如果 _lookup 找到的项目具有有效数据,则将其丢弃并创建一个新项目。 这样可以避免任何项目用户担心在检查项目时内容发生更改。 如果 _lookup 找到的项目不包含有效数据,则会将内容复制过来并设置 CACHE_VALID。

填充缓存

每个缓存都有一个名称,并且在注册缓存时,将在 /proc/net/rpc 中创建一个具有该名称的目录

此目录包含一个名为“channel”的文件,该文件是用于在内核和用户之间进行通信以填充缓存的通道。 此目录稍后可能包含其他用于与缓存交互的文件。

“channel”的工作方式有点像数据报套接字。 每个“write”都作为一个整体传递到缓存以进行解析和解释。 每个缓存可以以不同的方式处理写入请求,但预计写入的消息将包含

  • 一个键

  • 一个过期时间

  • 一个内容。

目的是在缓存中创建一个具有给定键的项目或更新为具有给定内容,并且应在该项目上设置过期时间。

从通道读取更有趣。 当缓存查找失败,或者当它成功但发现一个可能很快过期的条目时,会提交一个请求,要求用户空间更新该缓存项。 这些请求将显示在通道文件中。

连续读取将返回连续请求。 如果没有更多请求要返回,read 将返回 EOF,但用于读取的 select 或 poll 将阻塞,等待添加另一个请求。

因此,用户空间助手可能会

open the channel.
  select for readable
  read a request
  write a response
loop.

如果它死亡并且需要重新启动,任何尚未应答的请求仍将显示在该文件中,并将被助手的新实例读取。

每个缓存都应该定义一个“cache_parse”方法,该方法接受从用户空间写入的消息并对其进行处理。 它应该返回一个错误(该错误会传播回写入 syscall)或 0。

每个缓存还应该定义一个“cache_request”方法,该方法接受一个缓存项并将请求编码到提供的缓冲区中。

注意

如果缓存在通道上没有活动的读取器,并且在超过 60 秒内没有活动读取器,则不会将进一步的请求添加到通道,而是所有未找到有效条目的查找都会失败。 这部分是为了向后兼容:先前的 nfs 导出表被认为是权威的,并且失败的查找意味着明确的“否”。

请求/响应格式

虽然每个缓存都可以自由地使用自己的格式来通过通道发送请求和响应,但建议采用以下格式,并且支持例程可用于提供帮助:每个请求或响应记录应该是可打印的 ASCII,并且只有精确的一个换行符,该换行符应该位于末尾。 记录中的字段应该用空格分隔,通常是一个。 如果字段中需要空格、换行符或 nul 字符,则必须引用它们。 有两种机制可用

  • 如果字段以“x”开头,则它必须包含偶数个十六进制数字,并且这些数字对提供字段中的字节。

  • 否则,字段中的 a 必须后跟 3 个八进制数字,这些数字给出字节的代码。 其他字符被视为它们本身。 至少,空格、换行符、nul 和“'”必须以这种方式引用。