内核态不能直接解引用用户态指针,必须通过 copy_from_user/copy_to_user 等安全接口访问;单字节操作可用 __get_user/__put_user(需先 access_ok);高频共享内存应映射而非拷贝,并严格管理生命周期与 cache 一致性。

内核态不能直接解引用用户态指针
内核代码里拿到一个来自用户空间的指针(比如 copy_from_user 的第二个参数),绝不能直接用 *user_ptr 或 user_ptr->field 访问。x86_64 上用户态地址在内核态页表中通常无效,会触发 page fault 导致 Oops;ARM64 更严格,可能直接 abort。所有用户地址必须经内核提供的安全拷贝接口中转。
用 copy_from_user 和 copy_to_user 替代裸指针操作
这是最常用也最安全的方式。这两个函数内部做了地址合法性检查、页表映射临时切换(如 x86 的 fixmap)、原子性保障和返回实际拷贝字节数(便于错误判断)。
常见错误现象:
- 传入未对齐或越界的用户地址,
copy_from_user返回 0,但程序误判为“拷贝成功” - 忘记检查返回值,导致内核使用了未初始化的结构体字段
- 在中断上下文(atomic context)中调用,而这两个函数可能 sleep(仅当配置了
CONFIG_MMU且发生缺页时罕见,但不可依赖)
实操建议:
- 始终检查返回值:
if (copy_from_user(&kdata, udata, sizeof(kdata)) != 0) return -EFAULT; - 避免拷贝大块数据(>4KB),考虑用
get_user_pages+kmap_atomic分页映射 -
copy_to_user同理,尤其注意目标缓冲区长度是否足够,防止用户侧 buffer overflow
需要频繁访问时:用 access_ok + __put_user/__get_user
当只读/写单个基本类型(int、long、pointer),且确定地址在当前进程地址空间内(比如 ioctl 参数里的小结构体字段),可用更轻量的宏。它们不处理缺页,要求调用前先用 access_ok 检查。
使用场景:
- ioctl 中解析少量整型参数
- perf event handler 里快速读取用户计数器地址
注意点:
-
__get_user(val, ptr)的ptr必须是用户态地址,且access_ok(VERIFY_READ, ptr, sizeof(*ptr))为真 - 这些宏展开为汇编指令(如 x86 的
mov+sete),失败时设val为 0 并返回非零错误码 - 不支持浮点、结构体、指针解引用(除非你确认该指针指向的内存也在用户空间且已
access_ok过)
共享内存场景:用 remap_pfn_range 或 dma_buf 映射物理页
如果用户态和内核态需长期、高频共享一块内存(如 GPU 帧缓冲、DMA 缓冲区),不应反复拷贝,而应建立页表映射。这时指针本身不是“传递数据”,而是“共享访问入口”。
关键差异:
-
remap_pfn_range适合已知物理页帧号(PFN)的场景(如alloc_pages分配的连续内存),需手动管理 cache 属性(pgprot_writecombine等) -
dma_buf框架更适合跨驱动共享,用户态通过 fd 传递,内核用dma_buf_attach+dma_buf_map_attachment获取映射,自动处理 cache coherency 和 IOMMU - 用户态拿到的是 mmap 出来的虚拟地址,内核态用
vm_insert_page或remap_vmalloc_range把同一物理页映射进内核地址空间
容易踩的坑:
- 忘记
flush_cache_range或dma_sync_single_for_device,导致 cache 不一致 - 用户态 munmap 后,内核仍持有映射,引发 use-after-free
- 在非模块初始化/释放路径中动态 remap,可能破坏 vmalloc 区域布局
真正麻烦的从来不是“怎么传”,而是“谁负责生命周期管理”和“cache 与 TLB 怎么同步”。哪怕用了 copy_to_user,如果用户 buffer 是 mmap 的匿名页且刚被 swap out,内核仍可能 block 在缺页处理上——这决定了你到底该用同步拷贝,还是异步通知 + 用户态预分配锁页内存。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











