文章目录
- 前言
- [一、pin user pages in memory](#一、pin user pages in memory)
-
- [1.1 user pages](#1.1 user pages)
- [1.2 in memory](#1.2 in memory)
- 二、get_user_pages
-
- [2.1 函数简介](#2.1 函数简介)
- [2.2 demo](#2.2 demo)
- 三、get_user_pages_unlocked
- 四、get_user_pages_fast
- 五、pin_user_pages
前言
一、pin user pages in memory
1.1 user pages
在 Linux 中,用户态程序看到的地址空间(虚拟地址)并不直接对应物理内存,而是通过内核维护的一系列结构映射出来的。
每个进程有一个:
c
struct mm_struct *mm;
其中保存了该进程所有的内存映射(VMA,虚拟内存区域),包括:
c
程序代码段 .text
数据段 .data
堆(malloc())
栈
动态库(mmap())
文件映射、匿名页等
当你访问一个地址,比如:
c
int *p = malloc(4096);
*p = 123;
这个虚拟地址对应的页,会在页表(Page Table)中映射到一个物理页(struct page)。
这些被映射、可被访问的物理页,就是所谓的 user pages(用户页)。
1.2 in memory
"pin user pages in memory"(固定用户页面在内存中):固定用户页面是指将用户空间的内存页面锁定在物理内存中,防止它们被换出到磁盘或回收。
"in memory" 强调这些页面 当前已经被加载到 RAM 中,也就是说:
c
页表项(PTE)是有效的;
对应的 struct page 存在于内核的物理页缓存;
内核可以直接访问它;
不需要再触发缺页异常(page fault)来将其调入内存
当一个用户页面被访问时,如果它不在内存中,会触发 缺页中断(page fault),由内核将其加载进物理内存,此时才成为 "in memory"。
页面固定前:
c
用户进程虚拟地址空间
↓ (通过页表转换)
物理内存页面 ←→ 可能被换出到swap
页面固定后:
c
用户进程虚拟地址空间
↓ (通过页表转换)
物理内存页面 [PINNED] ← 不会被换出
↑
引用计数增加
pin memoy会增加页面引用计数,因此:
c
// 当页面被固定时,内存管理器会:
1. 跳过这些页面的页面回收扫描
2. 不允许将这些页面换出到swap
3. 在内存压力时,这些页面保持驻留
默认情况下,Linux 的内存是可以被换出的:
内核可以把长时间不用的页写入 swap 或文件,然后回收物理内存。
但某些内核子系统需要访问用户空间内存的实际物理页内容,例如:
场景 | 说明 |
---|---|
DMA / RDMA 驱动 | 硬件直接访问用户空间缓冲区,需要固定页以防被换出 |
零拷贝 I/O | 内核直接在用户缓冲区上读写 |
GPU 驱动 (OpenGL / Vulkan) | 用户显存映射 |
文件 I/O(AIO、splice) | 在内核中操作用户缓冲区 |
这些情况下,内核必须保证:
c
页不会被回收或移动;
页内容不会变化(COW);
可以直接得到物理地址。
pin memory的形式即使用mmap/malloc时提前将虚拟内存与对应物理内存锁定,以提高性能。
pin memory好处还有另外一个优势就是可以防止内存被swap out置换到存储器中,如果在进程切换时该物理内存被swap out磁盘中,下次读取还需要从磁盘加载到内存中,整个过程非常耗时,通过使用pin memory可以将一些主要常用的内存锁住,以防止被置换出去同时防止进行各种原因造成的页迁移,以提高程序性能。
pin memory最大坏处就是:如果每个程序都大量使用pin memory,那么最后将会导致没有物理内存可用,所以一般社区开发不建议在大量长期时候的内存使用pin memory类型内存。
二、get_user_pages
2.1 函数简介
c
// linux/v6.14/source/mm/gup.c
long get_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages)
{
int locked = 1;
if (!is_valid_gup_args(pages, NULL, &gup_flags, FOLL_TOUCH))
return -EINVAL;
return __get_user_pages_locked(current->mm, start, nr_pages, pages,
&locked, gup_flags);
}
EXPORT_SYMBOL(get_user_pages);
get_user_pages() 内存管理函数用于将用户态虚拟地址对应的物理页固定(pin)在内存中,以便内核或驱动程序能直接访问这些页(例如 DMA、零拷贝、或者 GPU/显卡映射用户缓冲区等场景)。
参数说明:
参数 | 类型 | 说明 |
---|---|---|
start |
unsigned long |
用户空间起始虚拟地址 |
nr_pages |
unsigned long |
需要固定的页数 |
gup_flags |
unsigned int |
控制行为的标志(如是否写入、是否触发缺页等) |
pages |
struct page ** |
用于返回物理页指针数组(可以为 NULL ) |
返回值为成功固定的页数(>= 0),或负错误码(< 0)。
get_user_pages():
遍历并"固定"用户空间的虚拟页(对应物理页引用计数 +1)。
确保这些页在被使用期间不会被换出到 swap。
备注:
函数调用:必须在调用时持有 mmap_lock(读或写锁)。
引用计数:每个固定的页面都会增加引用计数。
需要清理:完成后必须在所有固定页面上调用 put_page()。
所以当内核函数 get_user_pages() 被调用时,它实际上:
c
查找用户空间虚拟地址对应的 pte_t;
如果页不在内存,则触发缺页异常(fault in);
找到或分配对应的 struct page;
增加页引用计数,从而"固定"它在物理内存中;
返回 struct page * 数组给调用者。
调用 get_user_pages() 后:
c
每个页的引用计数 page->_refcount +1;
页不会被换出;
页内容被锁定在 RAM;
你可以通过 kmap() 映射到内核空间访问;
用完后必须调用 put_page() 释放引用,否则内存泄漏。
内核调用链:
c
get_user_pages()
-->__get_user_pages_locked()
-->__get_user_pages()
-->follow_page_mask()
-->follow_p4d_mask()
--> follow_pud_mask()
-->follow_pmd_mask()
-->follow_page_pte()
c
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags,
struct dev_pagemap **pgmap)
{
struct mm_struct *mm = vma->vm_mm;
struct folio *folio;
struct page *page;
spinlock_t *ptl;
pte_t *ptep, pte;
int ret;
/* FOLL_GET 和 FOLL_PIN 是互斥的,不能同时设置 */
if (WARN_ON_ONCE((flags & (FOLL_PIN | FOLL_GET)) ==
(FOLL_PIN | FOLL_GET)))
return ERR_PTR(-EINVAL);
/*
* 获取 PTE(页表项)指针,并加锁防止并发修改。
* 这是遍历页表的标准方式:从 PMD 找到对应的 PTE。
*/
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!ptep)
return no_page_table(vma, flags, address); // 没有有效的页表项
/*
* 读取 PTE 内容。
* 使用 ptep_get() 确保原子性读取,避免架构相关问题。
*/
pte = ptep_get(ptep);
/*
* 如果页面未驻留(不在物理内存中),比如被 swap 出去或尚未分配,
* 则无法 follow,跳转到 no_page 处理。
*/
if (!pte_present(pte))
goto no_page;
/*
* 如果该 PTE 标记为 PROT_NONE(无访问权限),
* 并且当前操作不允许访问此类页面(如非 dump 场景),则拒绝访问。
*/
if (pte_protnone(pte) && !gup_can_follow_protnone(vma, flags))
goto no_page;
/*
* 尝试从 VMA 和 PTE 中获取对应的 struct page。
* 对于普通匿名页或文件映射页,这会返回正确的 page 结构。
* 如果是特殊映射(如设备内存、zero page),可能返回 NULL。
*/
page = vm_normal_page(vma, address, pte);
/*
* 如果请求的是写访问(FOLL_WRITE),但 PTE 不允许写,
* 或者页面本身不支持写共享(如 COW 页面已被多个进程共享),
* 则不能 follow,返回失败。
*/
if ((flags & FOLL_WRITE) &&
!can_follow_write_pte(pte, page, vma, flags)) {
page = NULL;
goto out;
}
/*
* 如果 page 为 NULL,但这是一个设备映射页(device memory, devmap),
* 并且调用者需要获取引用(FOLL_GET 或 FOLL_PIN),
* 那么我们需要获取对应的 dev_pagemap 引用,以确保生命周期安全。
*/
if (!page && pte_devmap(pte) && (flags & (FOLL_GET | FOLL_PIN))) {
*pgmap = get_dev_pagemap(pte_pfn(pte), *pgmap);
if (*pgmap)
page = pte_page(pte); // 获取设备内存对应的 page 结构
else
goto no_page; // 获取失败,说明设备映射无效或不可访问
} else if (unlikely(!page)) {
/*
* 特殊情况处理:当 page == NULL 时,可能是:
* - zero page(全零页面,惰性分配)
* - 其他特殊映射(如 HUGETLB,但这里不处理)
*/
if (flags & FOLL_DUMP) {
/* 在 core dump 中避免包含特殊页面(如 zero page) */
page = ERR_PTR(-EFAULT);
goto out;
}
if (is_zero_pfn(pte_pfn(pte))) {
/* 映射的是全零页面(例如 bss 段未初始化部分) */
page = pte_page(pte);
} else {
/* 其他未知 PTE 类型,尝试 follow PFN(物理帧号) */
ret = follow_pfn_pte(vma, address, ptep, flags);
page = ERR_PTR(ret);
goto out;
}
}
/* 转换为 folio(新式内存管理单元,一个 folio 可能包含多个 page) */
folio = page_folio(page);
/*
* 检查是否需要写时分离(unshare)。
* 如果请求写访问,但 PTE 不可写,且页面是共享的(如 COW),
* 则不能直接 follow,应返回 -EMLINK 触发 unsharing。
*/
if (!pte_write(pte) && gup_must_unshare(vma, flags, page)) {
page = ERR_PTR(-EMLINK);
goto out;
}
/*
* 调试检查:如果使用 FOLL_PIN pin 了一个匿名页,
* 它必须是独占的(PageAnonExclusive),否则存在竞争风险。
*/
VM_BUG_ON_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
!PageAnonExclusive(page), page);
/*
* 尝试增加页面的引用计数(refcount)。
* 只有在设置了 FOLL_GET 或 FOLL_PIN 时才会真正增加。
* 如果失败(如页面正在释放),返回错误。
*/
ret = try_grab_folio(folio, 1, flags);
if (unlikely(ret)) {
page = ERR_PTR(ret);
goto out;
}
/*
* 如果是 FOLL_PIN 请求,需要确保页面内容对 CPU 可访问。
* 某些设备内存或加密内存可能默认不可访问,
* 必须显式调用 arch_make_folio_accessible() 来激活。
* 失败时需回滚:调用 unpin_user_page() 释放引用。
*/
if (flags & FOLL_PIN) {
ret = arch_make_folio_accessible(folio);
if (ret) {
unpin_user_page(page);
page = ERR_PTR(ret);
goto out;
}
}
/*
* 如果设置了 FOLL_TOUCH,表示要"访问"这个页面:
* - 若是写访问且页面未标记为 dirty,则标记为 dirty
* - 更新 accessed 位,防止被过早回收
*
* 注意:虽然 pte_mkyoung() 更精确,但 atomic 操作复杂,
* 因此使用 folio_mark_accessed() 更安全。
*/
if (flags & FOLL_TOUCH) {
if ((flags & FOLL_WRITE) &&
!pte_dirty(pte) && !folio_test_dirty(folio))
folio_mark_dirty(folio);
folio_mark_accessed(folio);
}
out:
/* 释放页表锁,并取消映射 PTE */
pte_unmap_unlock(ptep, ptl);
return page;
no_page:
/* 页面未驻留或不可访问 */
pte_unmap_unlock(ptep, ptl);
/* 如果 PTE 不为空(如 swap entry),返回 NULL 表示缺页 */
if (!pte_none(pte))
return NULL;
/* 否则可能是无效地址,调用 no_page_table 进一步处理 */
return no_page_table(vma, flags, address);
}
follow_page_pte() 的作用是:
在给定的虚拟地址空间(vma)中,通过页表项(PTE)查找并返回对应的 struct page *,同时根据标志位(flags)进行权限检查、引用计数增加、页面状态更新等操作。
常见 flag(gup_flags)解释:
标志 | 作用 |
---|---|
FOLL_WRITE |
以写入方式获取页(触发 COW 时复制页) |
FOLL_FORCE |
即使 VM 区不可访问也强制获取(例如 ptrace) |
FOLL_PIN |
将页固定在内存中,禁止换出(推荐代替 FOLL_GET) |
FOLL_GET |
仅增加引用计数(早期 API) |
FOLL_TOUCH |
标记页已被访问(更新 LRU) |
FOLL_UNLOCKABLE |
允许自动加锁/解锁 mmap_lock |
FOLL_NOWAIT |
不等待缺页中断,立即返回 |
2.2 demo
内核态代码:
c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/highmem.h>
#include <linux/mman.h>
#define DEVICE_NAME "gup_demo"
#define IOCTL_PIN_MEM _IOW('g', 1, struct gup_user_buf)
#define IOCTL_UNPIN_MEM _IO('g', 2)
struct gup_user_buf {
unsigned long addr;
unsigned long size;
};
static dev_t dev_num;
static struct class *cls;
static struct cdev cdev_gup;
static struct page **pages = NULL;
static int pinned_pages = 0;
static int nr_pages = 1;
/* IOCTL: 固定用户内存页 */
static long gup_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
long ret = 0;
int i;
struct gup_user_buf user;
pr_info("current comm = %s\n", current->comm);
switch (cmd) {
case IOCTL_PIN_MEM:
if (pinned_pages) {
pr_warn("Already pinned pages, unpin first\n");
return -EBUSY;
}
/* 从用户空间读取地址 */
if (copy_from_user(&user, (void __user *)arg, sizeof(user))){
pr_err("copy_from_user failed\n");
return -EFAULT;
}
pr_info("IOCTL_PIN_MEM: pinning user address 0x%lx\n", user.addr);
nr_pages = DIV_ROUND_UP(user.size + (user.addr & ~PAGE_MASK), PAGE_SIZE);
pr_info("pr_info = %d\n", nr_pages);
pages = kcalloc(nr_pages, sizeof(struct page *), GFP_KERNEL);
if (!pages)
return -ENOMEM;
mmap_read_lock(current->mm);
ret = get_user_pages(user.addr, nr_pages, FOLL_WRITE, pages);
mmap_read_unlock(current->mm);
if (ret < 0) {
pr_err("get_user_pages failed: %ld\n", ret);
kfree(pages);
pages = NULL;
return ret;
}
pinned_pages = ret;
pr_info("pinned_pages = %d\n", pinned_pages);
//get_user_pages(user.addr, 1, FOLL_WRITE, pages) 拿到的是整页;
//而字符串起始位置是页内偏移 offset = user.addr & ~PAGE_MASK。
unsigned long offset = user.addr & ~PAGE_MASK;
for (i = 0; i < pinned_pages; i++){
pr_info("page[%d]: PFN = %lu\n", i, page_to_pfn(pages[i]));
//kmap_local_page() 得到的页映射上加上偏移量来正确访问字符串
void *kaddr = kmap_local_page(pages[i]);
if (kaddr && i == 0) {
char *data = (char *)kaddr + offset;
/* 打印页前16字节十六进制 */
print_hex_dump(KERN_INFO, "gup_test data: ",
DUMP_PREFIX_OFFSET, 16, 1,
data, min_t(size_t, 64, user.size), true);
pr_info("gup_test data: %s\n",data);
kunmap_local(kaddr);
}
}
break;
case IOCTL_UNPIN_MEM:
if (!pinned_pages)
return -EINVAL;
pr_info("IOCTL_UNPIN_MEM: releasing %d pages\n", pinned_pages);
for (i = 0; i < pinned_pages; i++)
put_page(pages[i]);
kfree(pages);
pages = NULL;
pinned_pages = 0;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static const struct file_operations gup_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = gup_ioctl,
};
static int __init gup_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0)
return ret;
cdev_init(&cdev_gup, &gup_fops);
cdev_add(&cdev_gup, dev_num, 1);
cls = class_create(DEVICE_NAME);
device_create(cls, NULL, dev_num, NULL, DEVICE_NAME);
pr_info("/dev/%s created, use ioctl to pin/unpin memory\n", DEVICE_NAME);
return 0;
}
static void __exit gup_exit(void)
{
if (pinned_pages && pages) {
int i;
pr_info("Cleanup: releasing %d pinned pages\n", pinned_pages);
for (i = 0; i < pinned_pages; i++)
put_page(pages[i]);
kfree(pages);
}
device_destroy(cls, dev_num);
class_destroy(cls);
cdev_del(&cdev_gup);
unregister_chrdev_region(dev_num, 1);
pr_info("gup_demo unloaded\n");
}
module_init(gup_init);
module_exit(gup_exit);
MODULE_LICENSE("GPL");
加载内核模块:
c
$ make
$ sudo insmod get_user_pages.ko
用户态程序:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define IOCTL_PIN_MEM _IOW('g', 1, struct gup_user_buf)
#define IOCTL_UNPIN_MEM _IO('g', 2)
struct gup_user_buf {
unsigned long addr;
unsigned long size;
};
int main(void)
{
int fd;
void *buf;
struct gup_user_buf ubuf;
buf = malloc(4096);
if (!buf) {
perror("malloc");
return 1;
}
printf("Allocated user buffer: %p\n", buf);
strcpy(buf, "Hello from user space! This buffer will be pinned.\n");
ubuf.addr = (unsigned long)buf;
ubuf.size = 4096;
fd = open("/dev/gup_demo", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
printf("Pinning user memory via ioctl...\n");
if (ioctl(fd, IOCTL_PIN_MEM, &ubuf) < 0)
perror("ioctl PIN");
printf("Press ENTER to unpin and exit...\n");
getchar();
ioctl(fd, IOCTL_UNPIN_MEM);
close(fd);
free(buf);
return 0;
}
c
$ sudo ./a.out
Allocated user buffer: 0x5b79617d22a0
Pinning user memory via ioctl...
Press ENTER to unpin and exit...
查看结果:
c
$ sudo dmesg -c
[12186.225596] gup_demo unloaded
[12191.031194] /dev/gup_demo created, use ioctl to pin/unpin memory
[12192.876449] current comm = a.out
[12192.876454] IOCTL_PIN_MEM: pinning user address 0x5b79617d22a0
[12192.876456] pr_info = 2
[12192.876458] pinned_pages = 2
[12192.876459] page[0]: PFN = 1805595
[12192.876460] gup_test data: 00000000: 48 65 6c 6c 6f 20 66 72 6f 6d 20 75 73 65 72 20 Hello from user
[12192.876462] gup_test data: 00000010: 73 70 61 63 65 21 20 54 68 69 73 20 62 75 66 66 space! This buff
[12192.876463] gup_test data: 00000020: 65 72 20 77 69 6c 6c 20 62 65 20 70 69 6e 6e 65 er will be pinne
[12192.876464] gup_test data: 00000030: 64 2e 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 d...............
[12192.876464] gup_test data: Hello from user space! This buffer will be pinned.
[12192.876465] page[1]: PFN = 1924760
[12193.894884] current comm = a.out
[12193.894891] IOCTL_UNPIN_MEM: releasing 2 pages
备注:
(1)kmap_local_page
作用:将一个物理内存页(struct page *)临时映射到内核虚拟地址空间,返回其可访问的虚拟地址。
典型用途:当内核需要直接读写某个页面内容时(比如来自 get_user_pages() 的用户页或高端内存页),但该页面没有永久内核映射(direct mapping)。
返回值:映射后的内核虚拟地址(如 void *addr = kmap_local_page(page);),之后可通过 addr 读写页面数据。
kmap_local_page(page) 是一个高效、安全、可在任何上下文调用的接口,用于临时映射任意物理页面到内核地址空间,适用于现代内核编程中的页面直接访问场景。
c
// 本地映射,性能好
void *addr = kmap_local_page(page);
// 特点:
// - 映射在本地地址空间(每CPU)
// - 只能在当前上下文中使用
// - 无锁操作,性能优异
// - 基于栈管理,支持嵌套
而kmap:
c
// 全局映射,开销大
void *addr = kmap(page);
// 特点:
// - 映射在全局地址空间
// - 可以跨上下文使用
// - 需要全局锁,性能较差
// - 数量有限(通常最多64个同时映射)
性能关键代码应优先使用 kmap_local_page()。
确保 kunmap_local() 调用配对且顺序正确。
不要长期持有映射,尽快使用后释放。
(2)
c
nr_pages = DIV_ROUND_UP(user.size + (user.addr & ~PAGE_MASK), PAGE_SIZE);
在 Linux 内核中,页(page)是内存管理的最小单位,一般大小为 4KB(即 PAGE_SIZE = 4096)。
一个用户缓冲区可能 从任意地址开始,并且长度也不是页对齐的。
但 get_user_pages() 固定的单位是 整页。
因此,必须计算这个缓冲区到底覆盖了多少个完整页。
举例说明:
假设:
c
user.addr = 0x1003; // 用户缓冲区起始地址 (非对齐)
user.size = 5000; // 用户缓冲区长度
PAGE_SIZE = 4096; // 页大小
分步计算:
计算页内偏移量
c
user.addr & ~PAGE_MASK
等价于:
c
user.addr % PAGE_SIZE
在例子中:
c
0x1003 & 0xFFF = 0x003 = 3
说明这个缓冲区从页的第3个字节开始。
计算总共覆盖的内存范围:
c
user.size + (user.addr & ~PAGE_MASK)
在例子中:
c
5000 + 3 = 5003
也就是这个缓冲区从 页内偏移3 开始,总共跨越5003字节。
向上取整到页数:
c
DIV_ROUND_UP(5003, 4096) = 2
说明这段内存跨越 两页:
第一页从 0x1000 到 0x1FFF
第二页从 0x2000 到 0x2FFF
get_user_pages() 必须传入整页的数量,即便用户缓冲区只占其中一部分。
上述例子
user.addr | size | 实际覆盖范围 | 页数 |
---|---|---|---|
0x1000 | 4096 | 正好一页 | 1 |
0x1003 | 4096 | 末尾多出部分跨页 | 2 |
0x1FFF | 1 | 跨越页边界 | 2 |
0x2000 | 8192 | 对齐跨两页 | 2 |
(3)用户态的地址0x649f062702a0不是一个页对齐的,因此在内核态根据该用户态地址得到的是一整个页,调用kmap_local_page()获取的是该页映射的起始地址,是一个页大小对齐的,所以用户态的地址相对与kmap_local_page()获取的是该页映射的起始地址是有一个偏移量的,处理好偏移量才能读取到正确的数据。
代码如下:
c
unsigned long offset = user.addr & ~PAGE_MASK;
void *kaddr = kmap_local_page(pages[0]);
if (kaddr) {
char *data = (char *)kaddr + offset;
pr_info("user string: %s\n", data);
kunmap_local(kaddr);
}
三、get_user_pages_unlocked
c
/**
* get_user_pages_unlocked() - 无需手动加锁的获取用户页面函数
* @start: 起始用户空间地址
* @nr_pages: 要获取的页面数量
* @pages: 接收页面指针的数组
* @gup_flags: FOLL_* 标志位,控制获取行为
*
* 这个函数旨在替换以下代码模式:
*
* mmap_read_lock(mm);
* get_user_pages(mm, ..., pages, NULL);
* mmap_read_unlock(mm);
*
* 替换为:
*
* get_user_pages_unlocked(mm, ..., pages);
*
* 在功能上它等同于 get_user_pages_fast(),所以如果不需要特定的 gup_flags
* (例如 FOLL_FORCE),应该使用 get_user_pages_fast()。
*
* 返回值: 成功返回获取的页面数量,失败返回错误码
*/
long get_user_pages_unlocked(unsigned long start, unsigned long nr_pages,
struct page **pages, unsigned int gup_flags)
{
int locked = 0;
/* 验证参数有效性,自动添加 FOLL_TOUCH 和 FOLL_UNLOCKABLE 标志 */
if (!is_valid_gup_args(pages, NULL, &gup_flags,
FOLL_TOUCH | FOLL_UNLOCKABLE))
return -EINVAL;
/* 调用内部函数,locked=0 表示由内部管理锁 */
return __get_user_pages_locked(current->mm, start, nr_pages, pages,
&locked, gup_flags);
}
EXPORT_SYMBOL(get_user_pages_unlocked);
从注释可以看到自动锁管理:
c
// 传统方式需要手动管理锁:
mmap_read_lock(mm);
get_user_pages(mm, start, nr_pages, gup_flags, pages, NULL);
mmap_read_unlock(mm);
// 使用 unlocked 版本简化:
get_user_pages_unlocked(start, nr_pages, pages, gup_flags);
四、get_user_pages_fast
c
/**
* get_user_pages_fast() - 快速固定用户页面在内存中
* @start: 起始用户地址
* @nr_pages: 从起始地址开始要固定的页面数量
* @gup_flags: 修改固定行为的标志位
* @pages: 接收指向固定页面的指针数组
* 应该至少是 nr_pages 长度
*
* 尝试在不获取 mm->mmap_lock 的情况下固定用户页面在内存中。
* 如果不成功,它将回退到获取锁并调用 get_user_pages()。
*
* 返回值: 成功固定的页面数量。这可能少于请求的数量。
* 如果 nr_pages 为 0 或负数,返回 0。如果没有页面被固定,返回 -errno。
*/
int get_user_pages_fast(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages)
{
/*
* 调用者可能显式设置了 FOLL_GET,也可能没有设置;两种方式都可以。
* 然而,在内部(在 mm/gup.c 中),gup 快速变体必须设置 FOLL_GET,
* 因为 gup fast 始终是一个"固定页面并增加页面引用计数"的请求。
*/
if (!is_valid_gup_args(pages, NULL, &gup_flags, FOLL_GET))
return -EINVAL;
return gup_fast_fallback(start, nr_pages, gup_flags, pages);
}
EXPORT_SYMBOL_GPL(get_user_pages_fast);
get_user_pages_fast() 函数是 GUP(Get User Pages)机制中的高性能路径,用于快速锁定(pin)用户空间页面,避免在常见情况下持有重量级锁 mmap_lock。
核心目标:无锁快速获取用户页面。
正常的 get_user_pages() 必须先持有 mmap_read_lock(mm),这会阻塞其他线程修改内存映射,影响性能。
get_user_pages_fast() 尝试 不加锁 地遍历页表(PTE),直接查找物理页。
如果失败(比如遇到复杂 VMA、缺页、权限不足等),则退化为传统方式:加锁 + 调用 get_user_pages()。
备注:
get_user_pages_fast必须设置 FOLL_GET。
即:
get_user_pages_fast() 总是意味着"我要 pin 这些页面并增加引用计数"。
FOLL_GET 表示:为每个 page 增加 page->_refcount。
这是为了确保页面不会被意外释放。
五、pin_user_pages
c
/**
* pin_user_pages() - 为用户页面在内存中固定,供其他设备使用
*
* @start: 起始用户地址
* @nr_pages: 从起始地址开始要固定的页面数量
* @gup_flags: 修改查找行为的标志位
* @pages: 接收指向固定页面的指针数组
* 应该至少是 nr_pages 长度
*
* 几乎与 get_user_pages() 相同,除了不设置 FOLL_TOUCH,而是设置 FOLL_PIN。
*
* FOLL_PIN 表示这些页面必须通过 unpin_user_page() 释放。
* 详情请参阅 Documentation/core-api/pin_user_pages.rst。
*
* 注意:如果返回的页面中包含零页面(zero_page),它不会有固定计数,
* 并且 unpin_user_page*() 不会从中移除固定。
*/
long pin_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages)
{
int locked = 1;
if (!is_valid_gup_args(pages, NULL, &gup_flags, FOLL_PIN))
return 0;
return __gup_longterm_locked(current->mm, start, nr_pages,
pages, &locked, gup_flags);
}
EXPORT_SYMBOL(pin_user_pages);
pin_user_pages() 的语义:
"从用户态地址空间中,获取并长期固定一段物理页,使得外设或内核能够在一段时间内安全地访问它们,直到调用者显式释放(unpin)。"
pin_user_pages() 函数是 Linux 内核中用于 安全、长期固定用户页面(尤其是为设备如 DMA 使用)的现代接口。它是传统 get_user_pages() 的演进版本,专为解决"长期 pinning 问题"而设计。
与 get_user_pages() 的区别:
c
// get_user_pages() 使用 FOLL_TOUCH | FOLL_GET
// pin_user_pages() 使用 FOLL_PIN
// 主要区别:
// get_user_pages(): pin_user_pages():
// - FOLL_TOUCH (标记访问) - 无 FOLL_TOUCH
// - FOLL_GET (增加引用计数) - FOLL_PIN (长期固定)
// - put_page() 释放 - unpin_user_page() 释放
// - 短期使用 - 长期设备使用
特性 | get_user_pages() | pin_user_pages() |
---|---|---|
引用类型 | 普通引用 (get_page() ) |
固定引用 (pin_user_page() ) |
标志 | 通常包含 FOLL_TOUCH |
不包含 FOLL_TOUCH ,但强制包含 FOLL_PIN |
释放方式 | put_page() |
unpin_user_page() |
用途 | 短期访问(如 copy_to_user) | 长期 DMA、RDMA、GPU buffer 等场景 |
与零页的关系 | 可增加引用计数 | 零页不会被真正 pin |
核心目标:专为设备使用而设计
主要用途:让设备(如 GPU、网卡、FPGA)通过 DMA 访问用户空间内存。
强调"其他设备",意味着这些页面将被内核之外的实体访问。
这是它与普通 get_user_pages() 的根本区别:语义上声明"这是为了外部设备长期使用"。
FOLL_PIN 是一个语义标记,表示:
c
这是一个长期 pin 操作(long-term pinning)
页面将被设备使用(而非仅内核短暂访问)
必须使用专用释放函数 unpin_user_page() 来解 pin
pin_user_pages也有上述一系列函数:
c
pin_user_pages()
pin_user_pages_fast()
pin_user_pages_unlocked()
pin_user_pages_remote()