pin_user_pages() 及相关调用¶
概述¶
本文档描述了以下函数
pin_user_pages()
pin_user_pages_fast()
pin_user_pages_remote()
FOLL_PIN 的基本描述¶
FOLL_PIN 和 FOLL_LONGTERM 是可以传递给 get_user_pages*()(“gup”)系列函数的标志。FOLL_PIN 与 FOLL_LONGTERM 之间存在重要的交互和相互依赖关系,因此本文档同时涵盖两者。
FOLL_PIN 是 gup 内部的,这意味着它不应出现在 gup 调用点。这使得相关的封装函数(pin_user_pages*() 及其他)能够设置这些标志的正确组合,并检查问题。
另一方面,FOLL_LONGTERM *可以*在 gup 调用点设置。这是为了避免创建大量的封装函数来涵盖 get*()、pin*()、FOLL_LONGTERM 等所有组合。此外,pin_user_pages*() API 与 get_user_pages*() API 明显不同,因此这是一个自然的划分界限,也是进行单独封装调用的一个好地方。换句话说,对于 DMA 锁定的页面,使用 pin_user_pages*();对于其他情况,使用 get_user_pages*()。本文档后面描述了五种情况,以进一步阐明这一概念。
FOLL_PIN 和 FOLL_GET 在给定的 gup 调用中是互斥的。然而,多个线程和调用点可以自由地通过 FOLL_PIN 和 FOLL_GET 锁定相同的 struct pages。只需要调用点选择其中一个,而不是 struct page(s)。
FOLL_PIN 的实现与 FOLL_GET 几乎相同,只是 FOLL_PIN 使用了不同的引用计数技术。
FOLL_PIN 是 FOLL_LONGTERM 的先决条件。换句话说,FOLL_LONGTERM 是 FOLL_PIN 的一个更具体、更严格的案例。
每个封装函数设置的标志¶
对于这些 pin_user_pages*() 函数,FOLL_PIN 会与调用者提供的任何 gup 标志进行或运算。调用者需要传入一个非空的 struct pages* 数组,然后函数通过将每个页面增加一个特殊值:GUP_PIN_COUNTING_BIAS 来锁定页面。
对于大型 folio,不使用 GUP_PIN_COUNTING_BIAS 方案。取而代之的是,利用 struct folio
中可用的额外空间直接存储 pincount。
这种针对大型 folio 的方法避免了下面讨论的计数上限问题。这些限制会因巨页(huge pages)而严重加剧,因为每个尾页(tail page)都会给头页(head page)增加一个引用计数。事实上,测试表明,如果没有独立的 pincount 字段,在一些巨页压力测试中会出现引用计数溢出。
这也意味着巨页和大型 folio 不会遇到下面提到的误报问题。
Function
--------
pin_user_pages FOLL_PIN is always set internally by this function.
pin_user_pages_fast FOLL_PIN is always set internally by this function.
pin_user_pages_remote FOLL_PIN is always set internally by this function.
对于这些 get_user_pages*() 函数,FOLL_GET 可能甚至没有被指定。行为比上面稍微复杂一些。如果 *未*指定 FOLL_GET,但调用者传入了一个非空的 struct pages* 数组,则函数会为您设置 FOLL_GET,并通过将每个页面的引用计数增加 +1 来锁定页面。
Function
--------
get_user_pages FOLL_GET is sometimes set internally by this function.
get_user_pages_fast FOLL_GET is sometimes set internally by this function.
get_user_pages_remote FOLL_GET is sometimes set internally by this function.
跟踪 DMA 锁定页¶
跟踪 DMA 锁定页的一些关键设计约束和解决方案
需要为每个 struct page 提供一个实际的引用计数。这是因为多个进程可能会锁定和解除锁定一个页面。
误报(报告页面被 DMA 锁定,但实际上没有)是可以接受的,但漏报则不允许。
为此,struct page 的大小不能增加,并且所有字段都已使用。
鉴于上述情况,我们可以通过使用该字段的某种上部位来存储 DMA 锁定计数,从而重载 page->_refcount 字段。“某种”意味着,我们不是将 page->_refcount 划分为位字段,而是简单地将一个中等大小的值(GUP_PIN_COUNTING_BIAS,最初选择为 1024:10 位)添加到 page->_refcount。这会提供模糊的行为:如果一个页面被调用 get_page() 1024 次,那么它将显示有一个 DMA 锁定计数。这同样是可以接受的。
这也导致了限制:对于每次递增 10 位的计数器,只有 31-10=21 位可用。
由于这一限制,在使用 FOLL_PIN 时,对零页(zero pages)进行了特殊处理。我们只是假装锁定一个零页——我们根本不改变它的引用计数或锁定计数(它是永久的,所以没有必要)。解除锁定函数也不会对零页做任何事情。这对调用者来说是透明的。
调用者必须明确请求“页面的 DMA 锁定跟踪”。换句话说,仅仅调用 get_user_pages() 是不够的;必须使用一组新的函数,pin_user_page() 及相关函数。
FOLL_PIN, FOLL_GET, FOLL_LONGTERM: 何时使用哪个标志¶
感谢 Jan Kara、Vlastimil Babka 和其他一些 -mm 人员描述了这些类别
案例 1: 直接 IO (DIO)¶
存在对用作 DIO 缓冲区的页面的 GUP 引用。这些缓冲区只需要相对较短的时间(因此它们不是“长期”的)。未提供与 folio_mkclean() 或 munmap() 的特殊同步。因此,在调用点设置的标志是
FOLL_PIN
...但调用点不应直接设置 FOLL_PIN,而应使用设置 FOLL_PIN 的 pin_user_pages*() 例程之一。
案例 2: RDMA¶
存在对用作 DMA 缓冲区的页面的 GUP 引用。这些缓冲区需要很长时间(“长期”)。未提供与 folio_mkclean() 或 munmap() 的特殊同步。因此,在调用点设置的标志是
FOLL_PIN | FOLL_LONGTERM
注意:某些页面,例如 DAX 页面,不能用长期锁定来锁定。这是因为 DAX 页面没有单独的页面缓存,因此“锁定”意味着锁定文件系统块,这尚未(或暂时)以这种方式支持。
案例 3: MMU 通知器注册,无论是否有页错误硬件¶
设备驱动程序可以通过 get_user_pages*() 锁定页面,并注册内存范围的 MMU 通知器回调。然后,在收到通知器“invalidate range”回调后,停止设备使用该范围,并解除页面锁定。可能还有其他方案,例如明确地与挂起的 IO 进行同步,也能达到大致相同的目的。
或者,如果硬件支持可重播页错误,那么设备驱动程序可以完全避免锁定(这是理想情况),如下所示:如上所述注册 MMU 通知器回调,但不是在回调中停止设备和解除锁定,而是简单地从设备的页表中移除该范围。
无论哪种方式,只要驱动程序在 MMU 通知器回调时解除页面锁定,就能与文件系统和 MM(folio_mkclean()、munmap() 等)进行适当的同步。因此,无需设置任何标志。
案例 4: 仅用于 struct page 操作的锁定¶
如果只影响 struct page 数据(而不是页面跟踪的实际内存内容),则正常的 GUP 调用就足够了,并且不需要设置任何标志。
案例 5: 为写入页面内数据而锁定¶
即使不涉及 DMA 或直接 IO,仅仅一个简单的“锁定、写入页面数据、解除锁定”的案例也可能导致问题。案例 5 可以被视为案例 1 和案例 2 的超集,以及任何调用该模式的情况。换句话说,如果代码既不是案例 1 也不是案例 2,它仍然可能需要 FOLL_PIN,例如以下模式:
- 正确(使用 FOLL_PIN 调用)
pin_user_pages() 写入页面内数据 unpin_user_pages()
- 不正确(使用 FOLL_GET 调用)
get_user_pages() 写入页面内数据 put_page()
folio_maybe_dma_pinned(): 锁定的全部意义¶
将 folio 标记为“DMA 锁定”或“gup 锁定”的全部意义在于能够查询“此 folio 是否被 DMA 锁定?”这使得诸如 folio_mkclean()(以及一般的文件系统回写代码)之类的代码能够做出明智的决定,当 folio 由于此类锁定而无法解除映射时该怎么做。
在这些情况下该怎么做是长达数年的讨论和辩论的主题(请参阅本文档末尾的参考文献)。这是一个待办事项:一旦确定了细节,请在此处填写。同时,可以肯定地说,拥有此功能是
static inline bool folio_maybe_dma_pinned(struct folio *folio)
...是解决长期存在的 gup+DMA 问题的先决条件。
关于 FOLL_GET, FOLL_PIN 和 FOLL_LONGTERM 的另一种思考方式¶
另一种思考这些标志的方式是将其视为一系列限制的递进:FOLL_GET 用于 struct page 操作,而不影响 struct page 引用的数据。FOLL_PIN 是 FOLL_GET 的*替代*,用于对数据*将*被访问的页面进行短期锁定。因此,FOLL_PIN 是一种“更严格”的锁定形式。最后,FOLL_LONGTERM 是一个更具限制性的案例,它以 FOLL_PIN 为前提:这适用于将长期锁定且其数据将被访问的页面。
单元测试¶
此文件
tools/testing/selftests/mm/gup_test.c
包含以下新的调用来测试新的 pin*() 封装函数
PIN_FAST_BENCHMARK (./gup_test -a)
PIN_BASIC_TEST (./gup_test -b)
您可以通过两个新的 /proc/vmstat 条目监控系统启动以来总共获取和释放了多少 DMA 锁定页面
/proc/vmstat/nr_foll_pin_acquired
/proc/vmstat/nr_foll_pin_released
在正常情况下,除非有任何长期 [R]DMA 锁定存在,或在锁定/解除锁定转换期间,这两个值将相等。
nr_foll_pin_acquired: 这是系统通电以来获取的逻辑锁定数量。对于巨页,巨页中的每个页面(头页和每个尾页)都会锁定头页一次。这与 get_user_pages() 对巨页使用的行为类似:当 get_user_pages() 应用于巨页时,巨页中的每个尾页或头页都会使头页的引用计数增加一次。
nr_foll_pin_released: 这是系统通电以来已释放的逻辑锁定数量。请注意,即使最初的锁定应用于巨页,页面也会以 PAGE_SIZE 粒度释放(解除锁定)。由于上面“nr_foll_pin_acquired”中描述的锁定计数行为,会计核算会平衡,因此在执行此操作后
pin_user_pages(huge_page); for (each page in huge_page) unpin_user_page(page);
...预计会发生以下情况
nr_foll_pin_released == nr_foll_pin_acquired
(...除非由于存在长期 RDMA 锁定而导致其已经失衡。)
其他诊断¶
dump_page() 已略微增强,以处理这些新的计数字段,并更好地报告大型 folio。具体来说,对于大型 folio,会报告精确的锁定计数 (pincount)。
参考文献¶
John Hubbard,2019 年 10 月