☰
Linux 6.6内核内存管理深度解析(六):GUP / pin_user_pages — 怎么把用户态内存“钉“给设备 DMA
2026/10/15 2:42:44 网站建设 项目流程

〇、全景:设备要 DMA 用户态内存,得先把页"钉"住

前面 #3 讲了缺页把虚拟地址映射到物理页,#5 讲了 page cache。但还有一个关键问题:设备(GPU、网卡、NVMe)要做 DMA 访问用户态内存时,它拿的是物理地址、且要在一段时间内持续访问。可用户态内存是"按需分配、随时可能被换出/迁移/写时复制"的——如果设备 DMA 到一半,页被换出去了或 COW 改了,就出数据损坏。

GUP(Get User Pages)就是解决这个:把用户态的虚拟地址走页表翻成物理页,并且**“钉住”(pin)**它,保证它在 DMA 期间稳定。

用户态虚拟地址
(进程地址空间)

GUP
走页表 vaddr → struct page

物理页
被 pin 住,不被换出/迁移/COW

dma_map_page
(IOMMU #7)物理页 → 设备 IOVA

设备 DMA
读/写这段内存

get_user_pages
FOLL_GET:临时引用

pin_user_pages
FOLL_PIN:长期 pin

一句话主线:GUP 把用户态虚拟地址走页表翻译成物理页并"钉"住,供设备 DMA;有两个入口——get_user_pages(FOLL_GET,临时引用、页仍可能被 COW/迁移)和pin_user_pages(FOLL_PIN,长期 pin、页不能被 COW/迁移);pin 的核心是"pin 计数独立于 ref 计数"(单页 folio 用refcount += 1024的 bias 编码、大 folio 用_pincount精确计数),让内核能检测"这页被 DMA 钉住了"从而跳过 COW/迁移;两条路径——_fast快路径不持锁直接遍历页表,慢路径__get_user_pages持 mmap_lock 还能faultin_page触发缺页。


一、为什么要有 pin,而不是只get_page加个引用

设备 DMA 用户态内存,需要两样东西:① 物理页地址(设备只认物理地址);② 这个页在 DMA 期间稳定(不被换出到 swap、不被内存回收迁移、不被写时复制改内容)。

如果只用get_page(page->_refcount+1),只满足"页不被释放",满足不了"稳定"——内核无法区分"这个 refcount 是普通页表映射"还是"某个设备正在 DMA 它"。于是写这页时,内核不知道它正被设备 DMA,会按普通 COW 把它原地改写(复用同一物理页改内容),而设备还在读写旧页 →CPU 看到新数据、设备写到旧页,数据错乱。

所以内核需要单独的 pin 计数,把"设备 DMA 引用"和"普通页表引用"区分开,这就是FOLL_PIN。源码注释(include/linux/mm_types.h:1301)写得清楚:

FOLL_PIN indicates that a special kind of tracking (not just page->_refcount)… potentially changing the pages’ data. In contrast to FOLL_GET (whose pages are released via put_page())…


二、两个入口:get_user_pagesvspin_user_pages

// mm/gup.c (v6.6, line 2367)longget_user_pages(unsignedlongstart,unsignedlongnr_pages,unsignedintgup_flags,structpage**pages){// is_valid_gup_args 里补 FOLL_TOUCHreturn__get_user_pages_locked(current->mm,start,nr_pages,pages,&locked,gup_flags);}
// mm/gup.c (v6.6, line 3370)longpin_user_pages(unsignedlongstart,unsignedlongnr_pages,unsignedintgup_flags,structpage**pages){// is_valid_gup_args 里补 FOLL_PINreturn__gup_longterm_locked(current->mm,start,nr_pages,pages,&locked,gup_flags);}

两者接口一模一样,差别全在flag上:

入口flag语义释放典型场景
get_user_pages*FOLL_GET临时引用(refcount+1),页仍可能被 COW/迁移put_pageO_DIRECT 读写的瞬时访问、process_vm_readv
pin_user_pages*FOLL_PIN(调用者通常再传FOLL_LONGTERM)长期 pin,页不能被 COW/迁移/ksmunpin_user_page*RDMA、vfio、GPU 直通等长期 DMA

FOLL_PIN是内部 flag(mm/internal.h:960,FOLL_PIN = 1<<19),调用者不能自己传,由pin_user_pages*内部帮你设置——这就是两个 API 分开的原因:不让调用者乱设 flag,用 API 名字就定死了"get 还是 pin"。此外pin_user_pages还会走__gup_longterm_locked,隐含"长期持有"的语义(见 §五)。

每个入口还有几个变体:_fast(不持锁的快路径)、_unlocked(内部自管锁)、_remote(操作别的进程 mm)。


三、核心机制:pin 计数独立于 ref 计数

"把 pin 和普通引用区分开"落到struct folio上,是try_grab_folio(gup.c:126)干的:

// mm/gup.c (v6.6, line 126,节选)if(flags&FOLL_GET)returntry_get_folio(page,refs);// ① 普通 get:refcount + refs// FOLL_PIN 分支:if(is_zero_page(page))returnpage_folio(page);// ② 零页不 pin(共享、永不消失)folio=try_get_folio(page,refs);// ③ 先正常 refcount + refs// …if(folio_test_large(folio))atomic_add(refs,&folio->_pincount);// ④a 大 folio:独立 pincount 字段elsefolio_ref_add(folio,refs*(GUP_PIN_COUNTING_BIAS-1));// ④b 单页 folio:refcount 按 1024 步进

三种情况,核心是**“pin 数怎么单独记账”**:

  • FOLL_GET(①):就是try_get_folio给_refcount+1,没有任何 pin 记账。页被谁 get 了内核不知道,所以仍可能被 COW/迁移。
  • 大 folio(④a):struct folio里有独立的_pincount字段,直接精确atomic_add,同时_refcount也至少 +1(保证"确实被引用")。
  • 单页 folio(④b):struct page没有多余字段放 pincount,于是复用 refcount 的高位——pin 一次就把_refcount加GUP_PIN_COUNTING_BIAS(1024)。这样"pin 数 ≈ refcount / 1024",内核只要看 refcount 是否 ≥ 1024 就知道"这页被 pin 了"。
// include/linux/mm.h (v6.6, line 1577)#defineGUP_PIN_COUNTING_BIAS(1U<<10)// 1024

释放端gup_put_folio(gup.c:189)对称:FOLL_PIN 大 folio 减_pincount、单页 folio 减 1024;FOLL_GET 就普通folio_put_refs。

为什么单页用 bias 而不是独立字段:struct page已经塞得满满当当(union 复用),没空间给每个页再开一个 pincount 计数器。用 1024 的 bias 复用 refcount,是"用一点计数精度(refcount 上限被 pin 数压缩 1024 倍)换零额外内存"的折中;大 folio 本来就是多个页合成一个、folio结构宽裕,才开得起独立_pincount。


四、两条路径:_fast无锁快路径 vs 慢路径

4.1 慢路径__get_user_pages:持锁 + 能触发缺页

get_user_pages/pin_user_pages默认走慢路径__get_user_pages(gup.c:1184),它持着mmap_lock,逐页:

// mm/gup.c (v6.6, line 1184,节选)do{// ① 找到覆盖这个地址的 vma(gup_vma_lookup)+ 权限检查 check_vma_flagsif(!vma||start>=vma->vm_end){vma=gup_vma_lookup(mm,start);// …ret=check_vma_flags(vma,gup_flags);}// ② 走页表找物理页page=follow_page_mask(vma,start,foll_flags,&ctx);// ③ 页没映射 → 触发缺页(faultin_page)→ 重试if(!page||PTR_ERR(page)==-EMLINK){ret=faultin_page(vma,start,&foll_flags,…);// …gotoretry;}// ④ try_grab_folio 把页"抓"住(get 或 pin)// …}while(…);
  • ① vma + 权限:和 #3 缺页一样先查 vma(gup_vma_lookup)、检查访问权限(check_vma_flags);
  • ②follow_page_mask(gup.c:809):沿 PGD→P4D→PUD→PMD→PTE 逐级走页表,找到目标物理页;大页 vma 走hugetlb_follow_page_mask;
  • ③faultin_page:页还没映射(没物理页)就触发一次缺页,把页按需分配/读进来(复用 #3 的handle_mm_fault),再 retry;
  • ④try_grab_folio:真正把页"抓"住(§三)。

所以慢路径 = “走页表 + 缺页兜底”,功能最全,代价是要持mmap_lock(读锁),并发上有限制。

4.2 快路径gup_pte_range:无锁直接读 pte

get_user_pages_fast(gup.c:3270)/pin_user_pages_fast(gup.c:3304)不持mmap_lock,直接遍历页表:

// mm/gup.c (v6.6, line 2555,节选)staticintgup_pte_range(pmd_tpmd,pmd_t*pmdp,unsignedlongaddr,unsignedlongend,unsignedintflags,…){ptem=ptep=pte_offset_map(&pmd,addr);do{pte_tpte=ptep_get_lockless(ptep);// 无锁原子读 pte// …if(pte_protnone(pte))gotopte_unmap;// 复杂情况 → 退回慢路径if(!pte_access_permitted(pte,flags&FOLL_WRITE))gotopte_unmap;// …folio=try_grab_folio(page,1,flags);// 读完之后 recheck pte 没被并发改过,改过就放弃if(pmd_val(pmd)!=pmd_val(*pmdp)||pte_val(pte)!=pte_val(ptep_get(ptep)))gotopte_unmap;// …}while(…);}

快路径的 trick:

  • 无锁读 pte:ptep_get_lockless原子读,不碰锁;
  • 安全靠"关中断 + RCU + IPI":页表页被释放前,TLB 刷新的 IPI 会阻塞在关中断的 CPU 上,加上 RCU 延迟释放页表页,保证遍历期间页表不被脚下释放(见get_user_pages_fast上的大段注释,gup.c:2409);
  • recheck:读完页后再比对 pmd/pte 没被并发改动,改了(说明有 race,可能被 split/换出)就放弃、退回慢路径;
  • 只做"已映射"的页:遇到pte_protnone、无权限、THP split、special pte 等复杂情况一律goto pte_unmap退回慢路径——快路径不触发缺页,页必须已经映射好了。

所以快路径 = “页已映射的前提下,无锁快速翻页表”,适合高频、页基本都在的场合;一旦遇到它搞不定的,就 fallback 到慢路径。


五、FOLL_LONGTERM:长期 pin 要先把 movable 区的页迁走

pin_user_pages走的是__gup_longterm_locked(gup.c:2187)。注意pin_user_pages只自动帮你设FOLL_PIN,FOLL_LONGTERM是调用者额外传的(RDMA/vfio 等长期 DMA 都会传)。只有设了FOLL_LONGTERM,才会多出下面这步迁移:

// mm/gup.c (v6.6, line 2187,节选)staticlong__gup_longterm_locked(…){if(!(gup_flags&FOLL_LONGTERM))return__get_user_pages_locked(…);// 非 longterm,跟普通 get 一样flags=memalloc_pin_save();do{nr_pinned_pages=__get_user_pages_locked(…);// …/* FOLL_LONGTERM implies FOLL_PIN */rc=check_and_migrate_movable_pages(nr_pinned_pages,pages);}while(rc==-EAGAIN);memalloc_pin_restore(flags);returnrc?rc:nr_pinned_pages;}

check_and_migrate_movable_pages(gup.c:2161)会检查 pin 住的页有没有落在movable zone(比如 CMA 区),有就把它们迁移到 non-movable zone。

为什么要迁:movable zone(CMA 尤其)的设计前提是"这些页可以随便迁移,腾出来给连续分配用"(见后续 CMA 篇)。而长期 pin 的页不能被迁移——它留在 movable 区就会卡死内存规整/CMA 分配。所以长期 pin 前先把它们迁走,别占着可迁移的地盘。try_grab_folio里对应的检查是folio_is_longterm_pinnable(gup.c:158):fast 路径遇到不可长期 pin 的页直接失败退回慢路径,交给慢路径去迁移。


六、与 DMA / IOMMU 的关系:GUP 是"零拷贝"的前半段

用户态零拷贝 DMA 的完整链路是两段拼接:

  1. GUP(本篇):用户态虚拟地址 → 物理页,并把页 pin 住;
  2. DMA API(IOMMU #7):物理页 → 设备能访问的IOVA,dma_map_page/dma_map_single建 IOMMU 页表。

驱动先pin_user_pages拿到物理页数组(struct page **),再对每个页dma_map_page得到设备地址,然后告诉设备"去这个 IOVA 读/写"。设备 DMA 完,反向dma_unmap_page+unpin_user_page释放。GUP 管"CPU 视角的页稳不稳定",DMA API 管"设备视角的地址翻不翻译"——两者缺一不可,这就是 GPU 显存拷贝(cuMem)、RDMA、NVMe 直通这些"用户态零拷贝"的共同底座。


七、为什么这样设计(Why 层)

7.1 为什么需要单独的 pin 计数,而不是复用 refcount

因为**“被普通引用"和"被设备 DMA 钉住"是两种不同的约束**:普通引用只要求"别释放”,DMA pin 还要求"别换出、别迁移、别 COW 改内容"。复用 refcount 会让内核分不清这两种情况——它不知道一个 refcount 里混着"DMA 引用",于是照常 COW/迁移,导致"设备写旧页、CPU 看新页"的数据错乱。pin 计数独立出来后,folio_maybe_dma_pinned(mm.h:1919)能检测"这页被 DMA 钉住了";COW 时page_needs_cow_for_dma(mm.h:1947,fork 时用)会提前 break COW 复制一份、而不是原地改,保住设备正在用的那份。

7.2 为什么单页用 bias=1024、大 folio 用独立_pincount

struct page字段已满,塞不下独立计数器;用 1024 的 bias 复用 refcount 高位,是"零额外内存"换"计数精度压缩 1024 倍"的折中。大 folio 是多个页合成、struct folio有空间,才开独立字段做精确计数。这是"内存开销 vs 计数精度"的取舍,按 folio 大小分两档。

7.3 为什么有 fast 和 slow 两条路径

因为 GUP 是热路径(每笔 DMA、每次 RDMA 都调),而持mmap_lock的慢路径有锁竞争、还要能触发缺页。fast 路径"关中断 + 无锁原子读 pte + recheck",把"页已经映射好了"的常见情况做到最快;遇到复杂情况(页没映射、THP split、PROT_NONE)就退回慢路径兜底。“快路径覆盖常见 case、慢路径覆盖完整语义”,和 #5 缺页的 fast/slow 是同一套思想。

7.4 为什么长期 pin 要把 movable 区的页迁走

movable zone(CMA)的页"本来就是要被迁移腾地方的";长期 pin 的页不能被迁移,留在里面就成了"钉子户",卡死 compaction/CMA 连续分配。所以 pin 前先把它们迁到 non-movable zone,别让"长期钉死的页"占着"随时要搬走的区"。

7.5 为什么零页不 pin

零页是全局共享、永不回收、永不迁移的特殊页,pin 它既没必要(它本来就不会变)也浪费 pin 计数。所以try_grab_folio对is_zero_page直接返回、不记账。


八、与已有博客的交叉引用

已有博客本篇覆盖的关系
内存 #3 页表缺页GUP 慢路径的follow_page_mask就是"走同一套多级页表";faultin_page复用 #3 的handle_mm_fault缺页机制。GUP 是缺页机制的一个"只读查询 + pin"的变体
内存 #5 page cacheO_DIRECT绕过 page cache 后,驱动用 GUP 拿到用户缓冲区物理页直接 DMA;page cache 是"文件缓存",GUP 是"用户内存 pin",两者是 DMA 的两条不同数据来源
IOMMU #7 DMA API本篇 §六:GUP 拿物理页(vaddr→page),dma_map_page再映射成 IOVA(page→设备地址),两段拼成用户态零拷贝 DMA。GUP 是 DMA API 的"上游"
内存 #4 大页GUP 的gup_huge_pmd/hugetlb_follow_page_mask能直接 pin 大页,THP/HugeTLB 的大页也可被 pin 给设备

附:本篇关键源码索引

符号位置
get_user_pages/get_user_pages_unlockedmm/gup.c:2367/:2395
pin_user_pages/pin_user_pages_unlockedmm/gup.c:3370/:3390
get_user_pages_fast/pin_user_pages_fastmm/gup.c:3270/:3304
__get_user_pages(慢路径)mm/gup.c:1184
__gup_longterm_locked(pin 慢路径)mm/gup.c:2187
follow_page_mask(慢路径页表遍历)mm/gup.c:809
gup_pte_range(快路径 pte 遍历)mm/gup.c:2555
try_grab_folio(get/pin 核心)mm/gup.c:126
try_get_folio(FOLL_GET)mm/gup.c:71
gup_put_folio(释放)mm/gup.c:189
check_and_migrate_movable_pages(迁出 movable 区)mm/gup.c:2161
GUP_PIN_COUNTING_BIAS(1024)include/linux/mm.h:1577
FOLL_WRITE/FOLL_GET/FOLL_LONGTERMinclude/linux/mm_types.h:1332/:1334/:1355
FOLL_PIN(内部 flag)mm/internal.h:960

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询