Linux 内存 get_user_pages_remote 函数
文章目录
- 一、函数简介
- 二、demo
一、函数简介
// linux/v6.14/source/mm/gup.clong get_user_pages_remote(struct mm_struct *mm,unsigned long start, unsigned long nr_pages,unsigned int gup_flags, struct page **pages,int *locked)
{int local_locked = 1;if (!is_valid_gup_args(pages, locked, &gup_flags,FOLL_TOUCH | FOLL_REMOTE))return -EINVAL;return __get_user_pages_locked(mm, start, nr_pages, pages,locked ? locked : &local_locked,gup_flags);
}
EXPORT_SYMBOL(get_user_pages_remote);
get_user_pages_remote() 的功能是:
在 指定 mm_struct(目标进程地址空间) 中,从给定用户虚拟地址开始,获取(pin)若干页的 struct page *。
这是一个“跨进程”的版本 —— 可用于访问别的进程的用户内存。
get_user_pages_remote 是 Linux 内核中用于获取另一个进程(或当前进程)用户空间地址对应的物理页的函数。它属于 GUP(Get User Pages)机制的一部分,用来在内核态直接访问用户态内存页(例如进行 DMA、零拷贝 I/O、RDMA、KVM 映射等)。
它遍历指定进程(mm)的页表,找到从 start 开始的一系列用户虚拟页,把它们对应的 struct page * 引用计数 +1(防止被回收),并返回这些页面的指针。
使用的一些注意事项:
必须在调用时持有 mmap_lock(读或写锁)。
备注:
内核不推荐使用这个函数,原因如下:
handle_mm_fault() 是内核处理缺页的核心函数。它有个 flag:FAULT_FLAG_ALLOW_RETRY。
如果设置了这个标志,当出现可重试缺页时,内核会:
释放 mmap_lock
处理缺页
重新获取锁
再次重试访问页表
这能避免死锁与卡顿。
但 get_user_pages_remote() 由于接口层次设计问题,无法把这个 flag 传下去,因此它在处理缺页时不能自动重试。结果:
驱动要自己 retry;
性能更低;
在复杂 VMA 状态下容易失败。
推荐使用:get_user_pages_fast()/get_user_pages_unlocked()。
但是get_user_pages_fast()/get_user_pages_unlocked()只能访问本进程的地址空间,不能跨进程访问。
二、demo
接下来给出一个代码示例,编写一个驱动模块访问用户空间的虚拟地址。
用户程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PFN_MASK ((1UL << 55) - 1)int main() {int fd;uint64_t va, pa, page, pfn;unsigned long virt_addr;uint64_t file_offset;pid_t pid = getpid();printf("当前进程的 PID: %d\n", pid);// 分配一个页面char *buffer = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (buffer == MAP_FAILED) {perror("mmap failed");return 1;}// 写入数据确保页面已分配buffer[0] = 'A';virt_addr = (unsigned long)buffer;printf("虚拟地址: 0x%lx\n", virt_addr);// 打开 pagemap 文件fd = open("/proc/self/pagemap", O_RDONLY);if (fd < 0) {perror("open pagemap failed");munmap(buffer, PAGE_SIZE);return 1;}// 计算在 pagemap 文件中的偏移量file_offset = (virt_addr / PAGE_SIZE) * sizeof(uint64_t);if (lseek(fd, file_offset, SEEK_SET) == (off_t)-1) {perror("lseek failed");close(fd);munmap(buffer, PAGE_SIZE);return 1;}// 读取页表项if (read(fd, &page, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("read pagemap failed");close(fd);munmap(buffer, PAGE_SIZE);return 1;}close(fd);// 检查页面是否在内存中if ((page & (1UL << 63)) == 0) {printf("页面不在内存中\n");munmap(buffer, PAGE_SIZE);return 1;}// 获取物理帧号 (PFN)pfn = page & PFN_MASK;pa = (pfn << PAGE_SHIFT) | (virt_addr & (PAGE_SIZE - 1));printf("物理地址: 0x%lx\n", pa);printf("PFN: 0x%lx\n", pfn);// 阻塞等待,保持页面在内存中printf("进程进入阻塞状态,按回车键退出...\n");getchar();munmap(buffer, PAGE_SIZE);return 0;
}
结果:
$ sudo ./a.out
当前进程的 PID: 5989
虚拟地址: 0x778653700000
物理地址: 0x18c335000
PFN: 0x18c335
进程进入阻塞状态,按回车键退出...
内核态程序:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/sched/signal.h>
#include <linux/mm.h>
#include <linux/pid.h>
#include <linux/highmem.h>static int target_pid = -1;
static unsigned long target_vaddr = 0;module_param(target_pid, int, 0444);
MODULE_PARM_DESC(target_pid, "Target process PID");module_param(target_vaddr, ulong, 0444);
MODULE_PARM_DESC(target_vaddr, "Target virtual address (hex)");static int __init vaddr_verify_init(void)
{struct pid *pid_struct;struct task_struct *task;struct mm_struct *mm;struct page *page = NULL;long rc;unsigned long pfn;unsigned long phys_addr;int locked = 1; pr_info("init pid=%d vaddr=0x%lx\n", target_pid, target_vaddr);if (target_pid <= 0 || target_vaddr == 0) {pr_err("invalid parameters\n");return -EINVAL;}pid_struct = find_get_pid(target_pid);if (!pid_struct) {pr_err("find_get_pid failed for %d\n", target_pid);return -ESRCH;}task = pid_task(pid_struct, PIDTYPE_PID);if (!task) {pr_err("pid_task failed\n");put_pid(pid_struct);return -ESRCH;}pr_info("comm = %s\n", task->comm);mm = get_task_mm(task);if (!mm) {pr_err("get_task_mm failed\n");put_pid(pid_struct);return -ESRCH;}mmap_read_lock(mm);//locked 是一个 输入/输出参数 ,输入:调用者是否已持有 mmap_lock//locked = 1 表示我们已持有 mmap_lock//locked 传 NULL 表示你不关心 locked 机制, 让 GUP 自己决定锁的生命周期rc = get_user_pages_remote(mm, target_vaddr & PAGE_MASK, 1, FOLL_GET, &page, &locked);/* 根据 locked 状态决定是否释放锁 */if (locked)mmap_read_unlock(mm);if (rc <= 0) {pr_err("get_user_pages_remote returned %ld\n", rc);mmput(mm);put_pid(pid_struct);return -EFAULT;}/* page pinned; compute PFN and physical address */pfn = page_to_pfn(page);phys_addr = (pfn << PAGE_SHIFT) | (target_vaddr & ~PAGE_MASK);pr_info("success pid=%d vaddr=0x%lx -> pfn=0x%lx phys=0x%lx\n",target_pid, target_vaddr, pfn, phys_addr);put_page(page);mmput(mm);put_pid(pid_struct);return 0;
}static void __exit vaddr_verify_exit(void)
{pr_info("exit\n");
}module_init(vaddr_verify_init);
module_exit(vaddr_verify_exit);MODULE_LICENSE("GPL");
运行结果:
$ make
$ sudo insmod user_virt_to_phy.ko target_pid=5989 target_vaddr=0x778653700000
$ sudo dmesg -c
[ 6545.773724] init pid=5989 vaddr=0x778653700000
[ 6545.773767] comm = a.out
[ 6545.773771] success pid=5989 vaddr=0x778653700000 -> pfn=0x18c335 phys=0x18c335000
get_user_pages_remote的locked也可以传入NULL:
mmap_read_lock(mm);rc = get_user_pages_remote(mm, target_vaddr & PAGE_MASK, 1, FOLL_GET, &page, NULL);mmap_read_unlock(mm);