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,后者将检查数据是否有效,并且可能会启动一个向上调用以获取新数据。如果条目为负或需要进行向上调用但不可能,则 cache_check 将返回 -ENOENT;如果向上调用挂起,则返回 -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”的工作方式有点像数据报套接字。每次“写入”都会作为一个整体传递给缓存进行解析和解释。每个缓存可以不同地处理写入请求,但预计写入的消息将包含

  • 一个键

  • 一个过期时间

  • 一个内容。

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

从通道读取更有趣一些。当缓存查找失败时,或者当查找成功但找到可能很快过期的条目时,会为该缓存项注册一个请求,以便用户空间进行更新。这些请求会出现在通道文件中。

连续读取将返回连续请求。如果没有更多要返回的请求,则读取将返回 EOF,但是选择或轮询读取将阻塞,等待添加另一个请求。

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

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

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

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

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

注意

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

请求/响应格式

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

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

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