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()。后面将进一步描述五种情况,以进一步澄清这个概念。
对于给定的gup调用,FOLL_PIN和FOLL_GET是互斥的。然而,多个线程和调用点可以通过FOLL_PIN和FOLL_GET来固定相同的结构页面。只是调用点需要选择其中一个,而不是结构页面。
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)来固定页面。
对于大的foliage,不使用GUP_PIN_COUNTING_BIAS方案。相反,使用struct folio中可用的额外空间直接存储固定计数。
这种大foliage的方法避免了下面讨论的计数上限问题。这些限制将因为巨大页面而严重恶化,因为每个尾页都会向头页添加一个引用计数。实际测试表明,在某些巨大页面压力测试中,如果没有单独的固定计数字段,会出现引用计数溢出问题。
这也意味着巨大页面和大foliage不会受到下面提到的假阳性问题的影响。
函数
- pin_user_pages:此函数内部始终设置FOLL_PIN。
- pin_user_pages_fast:此函数内部始终设置FOLL_PIN。
- pin_user_pages_remote:此函数内部始终设置FOLL_PIN。
对于这些get_user_pages()函数,FOLL_GET甚至可能不会被指定。行为比上面稍微复杂一些。如果没有指定FOLL_GET,但调用者传递了一个非空的struct pages数组,则函数会为您设置FOLL_GET,并通过将每个页面的引用计数增加1来固定页面。
函数
- get_user_pages:此函数有时会在内部设置FOLL_GET。
- get_user_pages_fast:此函数有时会在内部设置FOLL_GET。
- get_user_pages_remote:此函数有时会在内部设置FOLL_GET。
跟踪DMA固定页面
跟踪DMA固定页面的一些关键设计约束和解决方案:
- 需要每个struct page的实际引用计数,因为多个进程可能固定和取消固定页面。
- 假阳性(报告页面被DMA固定,而实际上并没有)是可以接受的,但假阴性是不可以接受的。
- struct page可能不会增加尺寸,而且所有字段已经被使用。
- 鉴于上述情况,我们可以通过使用page->_refcount字段的上位比特来重载该字段,以实现一种中等大值(GUP_PIN_COUNTING_BIAS,最初选择为1024:10位)的DMA固定计数。这提供了模糊行为:如果一个页面被调用了1024次get_page(),那么它将看起来有一个单独的DMA固定计数。这也导致了一些限制:只有31-10==21位可用于每次增加10位的计数器。
- 由于这种限制,在使用FOLL_PIN时,对零页面应用特殊处理。我们只是假装固定一个零页面 - 我们根本不改变它的引用计数或固定计数(它是永久的,所以没有必要)。取消固定函数也不会对零页面执行任何操作。这对调用者是透明的。
- 调用者必须明确请求“跟踪页面的DMA固定”。换句话说,仅仅调用get_user_pages()是不够的;必须使用一组新的函数,如pin_user_page()等。
FOLL_PIN、FOLL_GET、FOLL_LONGTERM:何时使用哪些标志
感谢Jan Kara、Vlastimil Babka和其他一些-mm人员,对这些类别进行了描述:
CASE 1:直接IO(DIO)
存在对用作DIO缓冲区的页面的GUP引用。这些缓冲区只需要相对较短的时间(因此它们不是“长期”)。不提供与page_mkclean()或munmap()的特殊同步。因此,在调用点设置的标志是:
- FOLL_PIN
...但是,调用点应该使用设置FOLL_PIN的pin_user_pages*()例程,而不是直接设置FOLL_PIN。
CASE 2:RDMA
存在对用作DMA缓冲区的页面的GUP引用。这些缓冲区需要长时间(“长期”)。不提供与page_mkclean()或munmap()的特殊同步。因此,在调用点设置的标志是:
- FOLL_PIN | FOLL_LONGTERM
注意:某些页面,如DAX页面,不能使用长期固定。这是因为DAX页面没有单独的页面缓存,因此“固定”意味着锁定文件系统块,目前还不支持这种方式。
CASE 3:MMU通知器注册,带或不带页面错误硬件
设备驱动程序可以通过get_user_pages*()固定页面,并为内存范围注册mmu通知器回调。然后,在接收到通知器“使范围无效”回调时,停止设备使用该范围,并取消固定页面。可能还有其他可能的方案,例如显式地与待处理IO同步,以实现大致相同的功能。
或者,如果硬件支持可重放的页面错误,那么设备驱动程序可以完全避免固定(这是理想的),方法是:注册mmu通知器回调,如上所述,但在回调中,而不是在回调中停止设备并取消固定,只需从设备的页表中删除该范围。
无论哪种方式,只要驱动程序在mmu通知器回调时取消固定页面,就可以与文件系统和mm(page_mkclean()、munmap()等)进行适当的同步,因此不需要设置任何标志。
CASE 4:仅用于struct page操作的固定
如果只影响struct page数据(而不是页面实际跟踪的内存内容),则普通的GUP调用就足够了,不需要设置任何标志。
CASE 5:固定以便写入页面内的数据
即使不涉及DMA或直接IO,只是简单的“固定、写入页面数据、取消固定”也可能会导致问题。Case 5可以被认为是Case 1的超集,再加上Case 2,再加上任何调用这种模式的模式。换句话说,如果代码既不是Case 1也不是Case 2,可能仍然需要FOLL_PIN,用于这种模式:
正确的(使用FOLL_PIN调用):
- pin_user_pages() 写入页面内的数据 unpin_user_pages()
不正确的(使用FOLL_GET调用):
- get_user_pages() 写入页面内的数据 put_page()
page_maybe_dma_pinned()
: 固定页面的整个目的
将页面标记为“DMA-pinned”或“gup-pinned”的整个目的是为了能够查询“这个页面是否被DMA固定?”这允许诸如page_mkclean()
(以及文件系统回写代码)这样的代码在由于这些固定而无法取消映射时做出知情决策。
在这些情况下该做什么是多年来一系列讨论和辩论的主题(请参见本文档末尾的参考资料)。这是这里的一个待办事项:一旦解决了这个问题,填写详细信息。与此同时,可以肯定地说,拥有这个:
static inline bool page_maybe_dma_pinned(struct page *page)
…是解决长期存在的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
粒度被释放(unpinned)。由于上述“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,将报告确切的固定计数。
参考资料
- 一些关于get_user_pages()的缓慢进展(2019年4月2日)
- DMA和get_user_pages()(LPC:2018年12月12日)
- get_user_pages()的问题(2018年4月30日)
- LWN内核索引:get_user_pages()
约翰·哈伯德,2019年10月