Linux内核DMA固定用户空间缓冲区触发内核Oops问题求助
搞定DMA直接映射的"bad page state"内核Oops
老兄,你这是踩了直接DMA映射用户缓冲区的经典陷阱——内核页交换和硬件DMA访问的冲突!咱们一步步拆解问题,给你解决思路:
问题根因
你直接用应用层分配的缓冲区做DMA,本质上是把用户态虚拟地址对应的物理页交给硬件去读写。但问题在于:这些用户页默认是可被内核交换到磁盘的!当updatedb这类工具触发系统页扫描,或者内核的kswapd守护进程要回收内存时,它会尝试把这些正在被DMA访问的页换出去——这时候内核发现页的状态完全矛盾(明明应该是空闲/可交换,却正在被硬件占用),直接就抛出"bad page state"的Oops了。
核心解决方案:锁定用户页,禁止内核交换
要解决这个问题,关键是在DMA操作期间,把用户对应的物理页锁定在内存中,不让内核碰它。具体要用到内核的pin_user_pages()(或老版本的get_user_pages())函数,操作流程如下:
- 锁定页:在设置DMA之前,调用
pin_user_pages()把用户缓冲区对应的物理页全部锁定,标记为不可交换。记得加上FOLL_WRITE(如果DMA要写数据)和FOLL_LONGTERM(如果DMA操作持续时间较长)标志。 - 设置DMA:用锁定后得到的
struct page指针,获取物理地址来配置硬件DMA。 - 解锁页:DMA操作完成后,立刻调用
put_user_pages()解锁,让内核可以正常管理这些页。
给你一段示例代码参考:
struct page **dma_pages; size_t buf_len = 1024 * 1024 * 1024; // 1GB缓冲区 int num_pages = (buf_len + PAGE_SIZE - 1) / PAGE_SIZE; // 分配存储page指针的数组 dma_pages = kmalloc_array(num_pages, sizeof(struct page *), GFP_KERNEL); if (!dma_pages) { pr_err("Failed to allocate page array\n"); return -ENOMEM; } // 锁定用户页,禁止交换 int ret = pin_user_pages((unsigned long)user_buffer, num_pages, FOLL_WRITE | FOLL_LONGTERM, dma_pages, NULL); if (ret < 0) { pr_err("Failed to pin user pages: %d\n", ret); kfree(dma_pages); return ret; } // 这里用dma_pages中的物理页信息,设置DMA地址(比如用page_to_phys()) // ... 配置硬件DMA寄存器 ... // 等待DMA操作完成(比如用中断或轮询) // ... // DMA完成后,解锁页 put_user_pages(dma_pages, num_pages); kfree(dma_pages);
额外注意事项
- 异常路径处理:如果DMA操作中途失败(比如硬件出错),一定要记得解锁页,不然这些页会一直被锁定,导致内存泄漏。
- 避免长期锁定:不要把页锁定太长时间,否则会耗尽系统可用内存,反而引发其他内存回收问题。
- 应用层配合(可选):也可以让应用层用
mlock()或mmap(MAP_LOCKED)来锁定自己的缓冲区,但这种方式依赖应用层正确实现,不如内核层锁定可靠。
再啰嗦两句为什么Oops会出现
内核的页状态管理是严格的:一个页要么是可交换的用户页,要么是被锁定的正在使用的页。当DMA在访问某个页,但内核没收到"这个页不能动"的通知,就会认为这是状态错误——毕竟正常情况下,被硬件占用的页应该被标记为锁定状态。所以内核直接抛出BUG,终止相关进程来避免更严重的内存 corruption。
内容的提问来源于stack exchange,提问作者T.O.M.
相关产品推荐
相关产品推荐

