网络文件系统辅助库

概述

网络文件系统辅助库是一组旨在帮助网络文件系统实现 VM/VFS 操作的函数。目前,它仅包括将各种 VM 缓冲读取操作转换为从服务器读取的请求。但是,辅助库还可以介入其他服务,例如本地缓存或本地数据加密。

请注意,库模块不直接链接本地缓存,因此必须由 netfs 提供访问权限。

每个 Inode 上下文

网络文件系统辅助库需要一个地方来存储其在每个要管理的 netfs inode 上使用的一小部分状态。为此,定义了一个上下文结构

struct netfs_inode {
        struct inode inode;
        const struct netfs_request_ops *ops;
        struct fscache_cookie *cache;
};

想要使用 netfs lib 的网络文件系统必须将其放置在其 inode 包装器结构中,而不是 VFS struct inode。这可以通过类似于以下方式完成

struct my_inode {
        struct netfs_inode netfs; /* Netfslib context and vfs inode */
        ...
};

这允许 netfslib 通过使用 inode 指针的 container_of() 找到其状态,从而允许 VFS/VM 操作表直接指向 netfslib 辅助函数。

该结构包含以下字段

  • inode

    VFS inode 结构。

  • ops

    网络文件系统提供给 netfslib 的操作集。

  • cache

    本地缓存 cookie,如果未启用缓存,则为 NULL。如果禁用 fscache,则此字段不存在。

Inode 上下文辅助函数

为了帮助处理每个 inode 的上下文,提供了一些辅助函数。首先,一个函数对上下文执行基本初始化并设置操作表指针

void netfs_inode_init(struct netfs_inode *ctx,
                      const struct netfs_request_ops *ops);

然后是一个从 VFS inode 结构转换为 netfs 上下文的函数

struct netfs_inode *netfs_node(struct inode *inode);

最后,一个从附加到 inode 的上下文中获取缓存 cookie 指针的函数(如果禁用 fscache,则为 NULL)

struct fscache_cookie *netfs_i_cookie(struct netfs_inode *ctx);

缓冲读取辅助函数

该库提供了一组读取辅助函数,用于处理 ->read_folio()、->readahead() 和大部分 ->write_begin() VM 操作,并将它们转换为通用调用框架。

提供以下服务

  • 处理跨越多个页面的 folio。

  • 使 netfs 免受 VM 接口更改的影响。

  • 允许 netfs 将读取任意拆分为多个片段,即使这些片段与 folio 大小或 folio 对齐方式不匹配,并且可能会跨越 folio。

  • 允许 netfs 在两个方向上扩展预读请求以满足其需求。

  • 允许 netfs 部分满足读取请求,然后将重新提交。

  • 处理本地缓存,允许缓存数据和服务器读取的数据为单个请求交织在一起。

  • 处理清除不在服务器上的缓冲数据。

  • 处理失败的读取重试,并在必要时将读取从缓存切换到服务器。

  • 将来,这是可以执行其他服务的地方,例如对要远程存储或在缓存中存储的数据进行本地加密。

从网络文件系统来看,辅助函数需要一个操作表。这包括一个用于发出读取操作的强制方法以及许多可选方法。

读取辅助函数

提供三个读取辅助函数

void netfs_readahead(struct readahead_control *ractl);
int netfs_read_folio(struct file *file,
                     struct folio *folio);
int netfs_write_begin(struct netfs_inode *ctx,
                      struct file *file,
                      struct address_space *mapping,
                      loff_t pos,
                      unsigned int len,
                      struct folio **_folio,
                      void **_fsdata);

每个函数都对应一个 VM 地址空间操作。这些操作使用每个 inode 上下文中的状态。

对于 ->readahead() 和 ->read_folio(),网络文件系统直接指向相应的读取辅助函数;而对于 ->write_begin(),它可能稍微复杂一些,因为网络文件系统可能希望刷新冲突的写入或跟踪脏数据,并且如果调用辅助函数后发生错误,则需要放置获取的 folio。

辅助函数管理读取请求,通过提供的操作表回调到网络文件系统。在为同步辅助函数返回之前,将根据需要执行等待。

如果发生错误,将调用 ->free_request() 来清理分配的 netfs_io_request 结构。如果在发生错误时请求的某些部分正在进行中,如果读取了足够的数据,则将部分完成该请求。

此外,还有

* void netfs_subreq_terminated(struct netfs_io_subrequest *subreq,
                               ssize_t transferred_or_error,
                               bool was_async);

应调用该函数来完成读取子请求。该函数给定传输的字节数或负错误代码,以及一个标志,指示操作是否是异步的(即,鉴于这可能涉及休眠,后续处理是否可以在当前上下文中完成)。

读取辅助结构

读取辅助函数使用几个结构来维护读取的状态。第一个是管理整个读取请求的结构

struct netfs_io_request {
        struct inode            *inode;
        struct address_space    *mapping;
        struct netfs_cache_resources cache_resources;
        void                    *netfs_priv;
        loff_t                  start;
        size_t                  len;
        loff_t                  i_size;
        const struct netfs_request_ops *netfs_ops;
        unsigned int            debug_id;
        ...
};

以上字段是 netfs 可以使用的字段。它们是

  • inode

  • mapping

    正在读取的文件的 inode 和地址空间。mapping 可能指向也可能不指向 inode->i_data。

  • cache_resources

    本地缓存使用的资源(如果存在)。

  • netfs_priv

    网络文件系统的私有数据。可以将此值传递给辅助函数或在请求期间设置。

  • start

  • len

    读取请求的起始文件位置和长度。这些可能会被 ->expand_readahead() 操作更改。

  • i_size

    请求开始时文件的大小。

  • netfs_ops

    指向操作表的指针。此值将传递给辅助函数。

  • debug_id

    分配给此操作的数字,可以在跟踪行中显示以供参考。

第二个结构用于管理整个读取请求的各个切片

struct netfs_io_subrequest {
        struct netfs_io_request *rreq;
        loff_t                  start;
        size_t                  len;
        size_t                  transferred;
        unsigned long           flags;
        unsigned short          debug_index;
        ...
};

每个子请求都应访问单个源,但辅助函数将处理从一种源类型回退到另一种源类型。成员是

  • rreq

    指向读取请求的指针。

  • start

  • len

    此读取请求切片的起始文件位置和长度。

  • transferred

    到目前为止,此切片的长度已传输的数据量。网络文件系统或缓存应在此切片中的位置开始操作。如果发生短读取,辅助函数将再次调用,并在更新此值以反映到目前为止已读取的数量。

  • flags

    与读取相关的标志。文件系统或缓存需要关注以下两个标志

    • NETFS_SREQ_CLEAR_TAIL

      可以设置此标志以指示应清除切片的其余部分(从传输到 len)。

    • NETFS_SREQ_SEEK_DATA_READ

      这是对缓存的提示,提示它可能希望尝试跳到下一个数据(即使用 SEEK_DATA)。

  • debug_index

    分配给此切片的数字,可以在跟踪行中显示以供参考。

读取辅助操作

网络文件系统必须向读取辅助函数提供一个操作表,通过该表它可以发出请求并协商

struct netfs_request_ops {
        void (*init_request)(struct netfs_io_request *rreq, struct file *file);
        void (*free_request)(struct netfs_io_request *rreq);
        void (*expand_readahead)(struct netfs_io_request *rreq);
        bool (*clamp_length)(struct netfs_io_subrequest *subreq);
        void (*issue_read)(struct netfs_io_subrequest *subreq);
        bool (*is_still_valid)(struct netfs_io_request *rreq);
        int (*check_write_begin)(struct file *file, loff_t pos, unsigned len,
                                 struct folio **foliop, void **_fsdata);
        void (*done)(struct netfs_io_request *rreq);
};

操作如下

  • init_request()

    [可选] 调用此函数来初始化请求结构。它会给出要引用的文件。

  • free_request()

    [可选] 在释放请求时调用此函数,以便文件系统可以清理其附加的任何状态。

  • expand_readahead()

    [可选] 调用此函数以允许文件系统扩展预读读取请求的大小。文件系统可以在两个方向上扩展请求,但它不允许减少请求,因为这些数字可能表示已经进行的分配。如果启用了本地缓存,它将首先扩展请求。

    通过更改请求结构中的 ->start 和 ->len 来传达扩展。请注意,如果进行任何更改,则 ->len 必须至少增加与 ->start 减少的量相同。

  • clamp_length()

    [可选] 调用此函数以允许文件系统减小子请求的大小。例如,文件系统可以使用此函数来拆分必须跨多个服务器拆分的请求,或放入多个正在进行的读取。

    成功时应返回 0,错误时返回错误代码。

  • issue_read()

    [必需] 辅助函数使用此函数将子请求分派到服务器进行读取。在子请求中,->start、->len 和 ->transferred 指示应从服务器读取哪些数据。

    没有返回值;应调用 netfs_subreq_terminated() 函数以指示操作是否成功以及传输了多少数据。文件系统也不应处理设置 folios uptodate、解锁它们或删除它们的引用 - 辅助函数需要处理此问题,因为它们必须与复制到本地缓存的操作协调。

    请注意,辅助函数已锁定但未固定 folios。可以使用 ITER_XARRAY iov 迭代器来引用正在操作的 inode 范围,而无需分配大型 bvec 表。

  • is_still_valid()

    [可选] 调用此函数以查找刚刚从本地缓存读取的数据是否仍然有效。如果仍然有效,则应返回 true,否则应返回 false。如果它仍然无效,它将从服务器重新读取。

  • check_write_begin()

    [可选] 在 netfs_write_begin() 辅助函数分配/获取要修改的 folio 后调用此函数,以允许文件系统在允许修改之前刷新冲突状态。

    它可以解锁并丢弃给它的 folio,并将调用者的 folio 指针设置为 NULL。如果一切正常(*foliop 保持设置),则应返回 0,或者应重试操作(清除 *foliop),并返回任何其他错误代码来中止操作。

  • done

    [可选] 在请求中的所有 folios 都被解锁(并在适用时标记为 uptodate)后调用此函数。

读取辅助过程

读取辅助函数按以下一般过程工作

  • 设置请求。

  • 对于预读,允许本地缓存和网络文件系统建议扩展读取请求。然后将其提供给虚拟机。如果虚拟机无法完全执行扩展,则将执行部分扩展的读取,但这可能不会完整写入缓存。

  • 循环遍历,从请求中切分出块以形成子请求。

    • 如果存在本地缓存,则由它进行切分;否则,助手只会尝试生成最大切片。

    • 如果网络文件系统是数据源,则可以限制每个切片的大小。这样可以实现 rsize 和分块。

    • 助手从缓存或服务器发出读取请求,或者根据需要清除切片。

    • 下一个切片从上一个切片的末尾开始。

    • 当切片读取完成时,它们会终止。

  • 当所有子请求都终止后,会评估子请求,并且重新发出任何短请求或失败的请求。

    • 失败的缓存请求将改为向服务器发出请求。

    • 失败的服务器请求只会失败。

    • 如果从任一来源读取的数据不足,并且传输了更多数据,则会向该来源重新发出请求。

      • 缓存可能需要跳过无法执行 DIO 的空洞。

      • 如果设置了 NETFS_SREQ_CLEAR_TAIL,则短读取将被清除到切片的末尾,而不是重新发出请求。

  • 读取数据后,已完全读取/清除的页框

    • 将被标记为 uptodate。

    • 如果存在缓存,则会被标记为 PG_fscache。

    • 已解锁

  • 任何需要写入缓存的页框都将发出 DIO 写入。

  • 同步操作将等待读取完成。

  • 写入缓存的操作将异步进行,并且当写入完成时,页框的 PG_fscache 标记将被删除。

  • 当所有操作都完成后,请求结构将被清理。

读取助手缓存 API

在实现供读取助手使用的本地缓存时,需要两件事:一种是网络文件系统初始化读取请求的缓存的方法,另一种是助手调用的操作表。

要开始在 fscache 对象上进行缓存操作,请调用以下函数

int fscache_begin_read_operation(struct netfs_io_request *rreq,
                                 struct fscache_cookie *cookie);

传入请求指针和与文件对应的 cookie。这将填充下面提到的缓存资源。

netfs_io_request 对象包含一个缓存挂起其状态的位置

struct netfs_cache_resources {
        const struct netfs_cache_ops    *ops;
        void                            *cache_priv;
        void                            *cache_priv2;
};

其中包含一个操作表指针和两个私有指针。操作表如下所示

struct netfs_cache_ops {
        void (*end_operation)(struct netfs_cache_resources *cres);

        void (*expand_readahead)(struct netfs_cache_resources *cres,
                                 loff_t *_start, size_t *_len, loff_t i_size);

        enum netfs_io_source (*prepare_read)(struct netfs_io_subrequest *subreq,
                                               loff_t i_size);

        int (*read)(struct netfs_cache_resources *cres,
                    loff_t start_pos,
                    struct iov_iter *iter,
                    bool seek_data,
                    netfs_io_terminated_t term_func,
                    void *term_func_priv);

        int (*prepare_write)(struct netfs_cache_resources *cres,
                             loff_t *_start, size_t *_len, loff_t i_size,
                             bool no_space_allocated_yet);

        int (*write)(struct netfs_cache_resources *cres,
                     loff_t start_pos,
                     struct iov_iter *iter,
                     netfs_io_terminated_t term_func,
                     void *term_func_priv);

        int (*query_occupancy)(struct netfs_cache_resources *cres,
                               loff_t start, size_t len, size_t granularity,
                               loff_t *_data_start, size_t *_data_len);
};

带有一个终止处理程序函数指针

typedef void (*netfs_io_terminated_t)(void *priv,
                                      ssize_t transferred_or_error,
                                      bool was_async);

表中定义的方法包括

  • end_operation()

    [必需] 在读取请求结束时调用以清理资源。

  • expand_readahead()

    [可选] 在 netfs_readahead() 操作开始时调用,以允许缓存在任一方向上扩展请求。这允许缓存根据缓存粒度调整请求大小。

    该函数在其参数中传递指向起始位置和长度的指针,以及文件的大小作为参考,并适当地调整起始位置和长度。它应该返回以下值之一

    • NETFS_FILL_WITH_ZEROES

    • NETFS_DOWNLOAD_FROM_SERVER

    • NETFS_READ_FROM_CACHE

    • NETFS_INVALID_READ

    以指示是否应该仅清除切片,是否应该从服务器下载或从缓存读取,或者是否应该在当前点放弃切片。

  • prepare_read()

    [必需] 调用以配置请求的下一个切片。子请求中的 ->start 和 ->len 指示下一个切片的位置和大小;缓存可以减小长度以匹配其粒度要求。

  • read()

    [必需] 调用以从缓存读取。给出起始文件偏移量以及要读取到的迭代器,该迭代器也给出了长度。可以给出一个提示,请求它从该起始位置向前查找数据。

    还提供了指向终止处理程序函数的指针和传递给该函数的私有数据。应使用传输的字节数或错误代码以及一个标志(指示终止是否肯定在调用者的上下文中发生)来调用终止函数。

  • prepare_write()

    [必需] 调用以准备进行写入缓存的操作。这包括检查缓存是否有足够的空间来满足写入需求。*_start*_len 指示要写入的区域;可以根据需要缩小该区域或将其扩展到页面边界,以对齐直接 I/O。i_size 保存对象的大小并提供参考。如果调用者确定尚未将任何数据写入该区域,则将 no_space_allocated_yet 设置为 true,例如,如果它尝试从该区域读取数据。

  • write()

    [必需] 调用以写入缓存。给出起始文件偏移量以及要写入的迭代器,该迭代器也给出了长度。

    还提供了指向终止处理程序函数的指针和传递给该函数的私有数据。应使用传输的字节数或错误代码以及一个标志(指示终止是否肯定在调用者的上下文中发生)来调用终止函数。

  • query_occupancy()

    [必需] 调用以查找缓存特定区域内下一个数据块的位置。传入要查询的区域的起始位置和长度,以及答案需要对齐的粒度。该函数会返回该区域内可用的数据的起始位置和长度(如果有)。请注意,前面可能存在一个空洞。

    如果在该区域中找到一些数据,则返回 0;如果该区域中没有可用的数据,则返回 -ENODATA;如果此文件上没有缓存,则返回 -ENOBUFS。

请注意,这些方法会传递指向缓存资源结构的指针,而不是读取请求结构的指针,因为它们也可以在其他没有读取请求结构的情况下使用,例如将脏数据写入缓存。

API 函数参考

void folio_start_private_2(struct folio *folio)

在页框上启动 fscache 写入。[已弃用]

参数

struct folio *folio

页框。

说明

在将页框写入本地缓存之前调用此函数。不允许在第一个写入完成之前启动第二个写入。

请注意,这应该不再使用。

struct netfs_inode *netfs_inode(struct inode *inode)

从 inode 获取 netfs inode 上下文

参数

struct inode *inode

要查询的 inode

说明

从网络文件系统的 inode 获取 netfs lib inode 上下文。上下文结构应直接跟在 VFS inode 结构之后。

void netfs_inode_init(struct netfs_inode *ctx, const struct netfs_request_ops *ops, bool use_zero_point)

初始化 netfslib inode 上下文

参数

struct netfs_inode *ctx

要初始化的 netfs inode

const struct netfs_request_ops *ops

netfs 的操作列表

bool use_zero_point

如果为 true,则使用 zero_point 读取优化

说明

初始化 netfs 库上下文结构。它应直接跟在 VFS inode 结构之后。

void netfs_resize_file(struct netfs_inode *ctx, loff_t new_i_size, bool changed_on_server)

请注意,文件已调整大小

参数

struct netfs_inode *ctx

正在调整大小的 netfs inode

loff_t new_i_size

新的文件大小

bool changed_on_server

更改已应用于服务器

说明

通知 netfs lib 文件已调整大小,以便它可以调整其状态。

从 inode 获取缓存 cookie

参数

struct netfs_inode *ctx

要查询的 netfs inode

说明

从网络文件系统的 inode 获取缓存 cookie(如果已启用)。

void netfs_wait_for_outstanding_io(struct inode *inode)

等待未完成的 I/O 完成

参数

struct inode *inode

要等待的 netfs inode

说明

等待任何类型的未完成 I/O 请求完成。这旨在从 inode 驱逐例程中调用。这确保在允许 inode 被清理之前,这些请求持有的任何资源都被清理干净。

void netfs_readahead(struct readahead_control *ractl)

用于管理读取请求的助手

参数

struct readahead_control *ractl

预读请求的描述

说明

如果可能,从缓存中提取数据,否则从 netfs 中提取数据,以满足预读请求。超出 EOF 的空间将填充为零。来自不同来源的多个 I/O 请求将混合在一起。如有必要,可以向任一方向扩展预读窗口,以便更方便地进行 RPC 对齐,或使缓存在缓存中可行。

调用 netfs 必须在调用此函数之前初始化与 vfs inode 相邻的 netfs 上下文。

无论是否启用缓存,此函数都可用。

int netfs_read_folio(struct file *file, struct folio *folio)

用于管理 read_folio 请求的助手

参数

struct file *file

要读取的文件

struct folio *folio

要读取的 folio

说明

如果可能,从缓存中提取数据,否则从 netfs 中提取数据,以满足 read_folio 请求。超出 EOF 的空间将填充为零。来自不同来源的多个 I/O 请求将混合在一起。

调用 netfs 必须在调用此函数之前初始化与 vfs inode 相邻的 netfs 上下文。

无论是否启用缓存,此函数都可用。

int netfs_write_begin(struct netfs_inode *ctx, struct file *file, struct address_space *mapping, loff_t pos, unsigned int len, struct folio **_folio, void **_fsdata)

用于准备写入的助手 [已弃用]

参数

struct netfs_inode *ctx

netfs 上下文

struct file *file

要读取的文件

struct address_space *mapping

要读取的映射

loff_t pos

写入开始的文件位置

unsigned int len

写入的长度(可能会超出所选 folio 的末尾)

struct folio **_folio

放置结果 folio 的位置

void **_fsdata

供 netfs 存储 cookie 的位置

说明

通过从缓存中提取数据(如果可能),或者从 netfs 中提取数据(如果不可能),预读取数据以进行写开始请求。超出 EOF 的空间将填充为零。来自不同来源的多个 I/O 请求将混合在一起。

调用 netfs 必须提供一个操作表,其中只有一个 issue_read 是强制性的。

可以提供 check_write_begin() 操作来检查和刷新冲突的写入,一旦 folio 被获取并锁定。它被传递一个指向 fsdata cookie 的指针,该指针被返回到 VM 以传递给 write_end。它允许睡眠。如果请求应该继续进行,则应返回 0,或者可以返回错误。它还可以解锁并放置 folio,前提是将 *foliop 设置为 NULL,在这种情况下,返回 0 将导致 folio 被重新获取并且该过程被重试。

调用 netfs 必须在调用此函数之前初始化与 vfs inode 相邻的 netfs 上下文。

无论是否启用缓存,此函数都可用。

请注意,这应该被认为是已弃用的,而应使用 netfs_perform_write()。

ssize_t netfs_buffered_read_iter(struct kiocb *iocb, struct iov_iter *iter)

文件系统缓冲 I/O 读取例程

参数

struct kiocb *iocb

内核 I/O 控制块

struct iov_iter *iter

读取数据的目标

说明

这是所有可以直接使用页面缓存的文件系统的 ->read_iter() 例程。

iocb->ki_flags 中的 IOCB_NOWAIT 标志表示,当无法在不等待 I/O 请求完成的情况下读取数据时,应返回 -EAGAIN;它不会阻止预读。

iocb->ki_flags 中的 IOCB_NOIO 标志表示,不应为读取或预读生成新的 I/O 请求。当无法读取数据时,应返回 -EAGAIN。当会触发预读时,应返回部分(可能为空)的读取。

返回值

  • 复制的字节数,即使是部分读取

  • 如果未读取任何内容,则返回负错误代码(如果 IOCB_NOIO,则返回 0)

ssize_t netfs_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)

通用文件系统读取例程

参数

struct kiocb *iocb

内核 I/O 控制块

struct iov_iter *iter

读取数据的目标

说明

这是所有可以直接使用页面缓存的文件系统的 ->read_iter() 例程。

iocb->ki_flags 中的 IOCB_NOWAIT 标志表示,当无法在不等待 I/O 请求完成的情况下读取数据时,应返回 -EAGAIN;它不会阻止预读。

iocb->ki_flags 中的 IOCB_NOIO 标志表示,不应为读取或预读生成新的 I/O 请求。当无法读取数据时,应返回 -EAGAIN。当会触发预读时,应返回部分(可能为空)的读取。

返回值

  • 复制的字节数,即使是部分读取

  • 如果未读取任何内容,则返回负错误代码(如果 IOCB_NOIO,则返回 0)