Linux 内存 --- get_user_pages/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()
相关推荐
江公望5 小时前
Qt enum ApplicationAttribute枚举值浅解
linux·qt
Yuki’5 小时前
Linux系统的ARM库移植
linux·arm开发
GilgameshJSS5 小时前
STM32H743-ARM例程26-TCP_CLIENT
c语言·arm开发·stm32·单片机·tcp/ip
报错小能手5 小时前
linux学习笔记(51)Redis发布订阅 主从复制 缓存 雪崩
linux·笔记·学习
杨福瑞5 小时前
数据结构:顺序表讲解(1)
c语言·开发语言·数据结构
程序猿编码5 小时前
轻量级却实用:sigtrace 如何靠 ptrace 实现 Linux 信号的捕获与阻断(C/C++代码实现)
linux·c语言·c++·信号·捕获·ptrace
qq_393060476 小时前
阿里云创建交换分区、设置内存监控预警和自动处理内存占用过大进程的脚本
linux·服务器·阿里云
LaoZhangGong1236 小时前
IR红外遥控器和接收器
c语言·遥控器·红外·ir
迎風吹頭髮7 小时前
Linux内核架构浅谈60-Linux块设备驱动:请求队列与BIO结构的交互流程
linux·运维·交互