Linux内核驱动创建用户空间缓冲区失败:get_user_pages返回EFAULT
我正尝试将一款旧内核驱动适配到Debian Bookworm系统,但遇到了问题。该驱动封装了另一串口驱动(如/dev/ttyS4),通过filp_open()打开目标驱动后调用其文件操作(ioctl、read、write)来控制,缓冲区/结构体参数需在用户空间与内核空间之间传输。
为实现这一逻辑,调用方内核驱动需创建用户空间缓冲区(原代码使用mm_seg_t等机制)。我尝试通过vmalloc()分配内核内存块,kmalloc()分配页指针数组,再调用get_user_pages(),但该函数返回-14(EFAULT)。
以下是我的当前实现代码:
// Calculate page-aligned size of user data block size_t szUserdata_sizeof = PAGE_ALIGN(sizeof(struct userdata)); // Calculate the number of pages int iUserdata_numpages = szUserdata_sizeof / PAGE_SIZE; // Allocate block of memory in kernel space struct userdata * pstUserdata_kernel = vmalloc(szUserdata_sizeof); if (pstUserdata_kernel == NULL) { return -ENOMEM; } // Array to store pointers to the pinned pages static struct page **papstUserdata_pages; papstUserdata_pages = kmalloc(iUserdata_numpages * sizeof(struct page *), GFP_KERNEL); if (papstUserdata_pages == NULL) { return -ENOMEM; } // Determine the (fake) user-space address we will be using unsigned long ulUserdata_user = (unsigned long)pstUserdata_kernel; // Use get_user_pages() to pin the buffer's pages, simulating user-space memory long ret = get_user_pages(ulUserdata_user, iUserdata_numpages, FOLL_WRITE, papstUserdata_pages, NULL);
注:我了解使用tty_kopen_exclusive()等现代方法控制tty内核驱动更优,但这意味着要重写大部分驱动,我暂不想这么做。
解决方案
核心问题分析
你用vmalloc()分配的内核地址无法直接作为用户空间地址传给get_user_pages()——这个函数的作用是把用户空间虚拟地址映射到物理页,而内核虚拟地址(尤其是vmalloc分配的)不在当前进程的用户地址空间范围内,自然会返回EFAULT。
另外,旧代码用的mm_seg_t是早期内核用来切换地址空间的机制(比如set_fs(KERNEL_DS)),现在新内核已经废弃了这种用法,不能直接照搬。
可行的替代方案
方案1:使用copy_to_user()/copy_from_user()配合临时用户缓冲区
这是最合规的做法,步骤如下:
- 在用户空间通过驱动提供的syscall接口分配一块缓冲区,传给内核;
- 内核用
copy_to_user()把要发送的数据写到这块用户缓冲区,再调用目标驱动的ioctl/write; - 操作完成后用
copy_from_user()把结果读回内核。
方案2:用vm_insert_page()手动把内核页映射到用户地址空间
如果必须在纯内核态模拟用户缓冲区,可以按以下步骤实现:
- 用
alloc_pages()分配连续物理页(避免用vmalloc,它的页离散,映射更复杂); - 在当前进程的用户地址空间找一块空闲虚拟地址区间(调用
get_unmapped_area()); - 调用
vm_insert_page()把每个物理页映射到这个用户地址区间; - 使用该用户地址调用目标驱动的文件操作;
- 操作完成后解除映射并释放内存,避免泄漏。
示例代码片段:
// 分配物理页 struct page *page = alloc_pages(GFP_KERNEL, get_order(szUserdata_sizeof)); if (!page) return -ENOMEM; // 获取当前进程的地址空间结构体 struct mm_struct *mm = current->mm; unsigned long user_addr; // 锁定地址空间,防止并发修改 down_write(&mm->mmap_sem); // 查找用户空间空闲地址区间 user_addr = get_unmapped_area(NULL, 0, szUserdata_sizeof, 0, 0); if (IS_ERR_VALUE(user_addr)) { up_write(&mm->mmap_sem); __free_pages(page, get_order(szUserdata_sizeof)); return user_addr; } // 逐页映射到用户地址空间 for (int i = 0; i < iUserdata_numpages; i++) { struct page *p = nth_page(page, i); int ret = vm_insert_page(mm, user_addr + i*PAGE_SIZE, p); if (ret) { // 出错时回滚已完成的映射 up_write(&mm->mmap_sem); // 此处需补充解除已映射页的逻辑,省略示例代码 __free_pages(page, get_order(szUserdata_sizeof)); return ret; } } up_write(&mm->mmap_sem); // 现在可使用user_addr作为用户空间地址调用目标驱动函数 // ... // 用完后需手动解除映射或让用户态调用munmap释放
方案3:临时切换地址空间(不推荐)
新内核已弱化set_fs()机制,直接用内核地址调用用户态接口存在安全风险,容易触发内核panic。如果一定要尝试,可结合access_ok()、__put_user()/__get_user()操作,但兼容性极差,不建议采用。
关键注意事项
- 禁止直接将内核地址当作用户地址传给需要用户空间指针的内核函数,违反地址空间隔离机制;
- 操作进程地址空间时必须持有
mmap_sem锁,避免并发修改导致崩溃; - 所有分配的内存和映射必须及时释放,防止内存泄漏。
内容的提问来源于stack exchange,提问作者nickpelling

