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*() 和其他)设置这些标志的正确组合,并检查问题。
另一方面,允许在 gup 调用点设置 FOLL_LONGTERM。这是为了避免创建大量包装器函数来覆盖 get*()、pin*()、FOLL_LONGTERM 以及更多的所有组合。此外,pin_user_pages*() API 明显不同于 get_user_pages*() API,因此这是一个自然的分界线,也是进行单独包装调用的好地方。换句话说,使用 pin_user_pages*() 进行 DMA 固定页面,使用 get_user_pages*() 用于其他情况。本文档稍后将描述五个案例,以进一步阐明该概念。
对于给定的 gup 调用,FOLL_PIN 和 FOLL_GET 是互斥的。但是,多个线程和调用点可以通过 FOLL_PIN 和 FOLL_GET 自由地固定相同的 struct page。只需要调用点选择其中一个,而不是 struct page(s)。
FOLL_PIN 的实现与 FOLL_GET 几乎相同,只是 FOLL_PIN 使用了不同的引用计数技术。
FOLL_PIN 是 FOLL_LONGTERM 的前提条件。另一种说法是,FOLL_LONGTERM 是 FOLL_PIN 的一个特定情况,更严格的情况。
每个包装器设置的标志¶
对于这些 pin_user_pages*() 函数,FOLL_PIN 与调用者提供的任何 gup 标志进行 OR 运算。调用者必须传入一个非空的 struct pages* 数组,然后该函数通过将每个页面递增一个特殊值来固定页面:GUP_PIN_COUNTING_BIAS。
对于大型 folio,不使用 GUP_PIN_COUNTING_BIAS 方案。相反,struct folio
中可用的额外空间用于直接存储 pincount。
这种针对大型 folio 的方法避免了下面讨论的计数上限问题。这些限制会被巨型页面严重加剧,因为每个尾页都会向头页添加一个 refcount。实际上,测试表明,如果没有单独的 pincount 字段,在一些巨型页面的压力测试中会看到 refcount 溢出。
这也意味着巨型页面和大型 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,并通过将每个页面的 refcount 递增 +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 的大小不能增加,并且所有字段都已使用。
鉴于以上所述,我们可以通过使用该字段中的高位来重载 page->\_refcount 字段,用于 dma 固定计数。 “某种程度上” 的意思是,与其将 page->\_refcount 分成位字段,我们只是将一个中等大小的值(GUP_PIN_COUNTING_BIAS,最初选择为 1024:10 位)添加到 page->\_refcount。这提供模糊的行为:如果一个页面被调用 get_page() 1024 次,那么它似乎只有一个 dma 固定计数。再次强调,这是可以接受的。
这也导致了限制:只有 31-10==21 位可用于以每次递增 10 位的方式递增的计数器。
由于该限制,在使用 FOLL_PIN 时,会对零页面应用特殊处理。我们只是假装固定一个零页面 - 我们根本不会更改它的 refcount 或 pincount(它是永久的,所以没有必要)。取消固定函数也不会对零页面做任何事情。这对调用者是透明的。
调用者必须明确请求 “页面的 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 通知程序回调。然后,在收到通知程序 “使范围无效” 回调时,停止设备使用该范围,并取消固定页面。可能有其他可能的方案,例如,显式地与待处理的 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 时做出明智的决策。
在这些情况下该做什么是一个历时多年的讨论和辩论的主题(请参阅本文档末尾的参考资料)。这是一个 TODO 项目:一旦解决,请填写详细信息。同时,可以肯定地说,有了这个可用性
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() 已略微增强,以处理这些新的计数字段,并更好地报告一般的大型页帧。具体来说,对于大型页帧,会报告确切的锁定计数。
参考资料¶
John Hubbard, 2019 年 10 月