在内核中映射的fork和用户空间内存的交互
考虑一个Linux驱动程序,它使用get_user_pages
(或get_page
)来映射调用进程中的页面。然后将页面的物理地址传递给硬件设备。进程和设备都可以读取和写入页面,直到各方决定结束通信。特别是,在调用get_user_pages
返回的系统调用之后,通信可能会继续使用页面。系统调用实际上在进程和硬件设备之间设置了一个共享内存区域。在内核中映射的fork和用户空间内存的交互
我很担心,如果进程调用fork
发生了什么(它可能是从另一个线程,并且情况可能会同时调用get_user_pages
系统调用过程中或更高版本)。特别是,如果在fork之后父进程写入共享内存区域,我对底层物理地址有何了解(可能是因写入时拷贝而改变)?我想了解:
- 什么内核需要做,以抵御潜在的行为不端的处理(!我不想创建一个安全孔);
-
该进程需要遵守什么限制,以便驱动程序的功能能够正常工作(即物理内存仍然映射在父进程中的相同地址)。
- 理想情况下,我想那里的孩子的过程完全不使用我们的驱动程序(它可能几乎立即调用
exec
)工作的常见情况。 - 理想情况下,父进程在分配内存时不应该采取任何特殊的步骤,因为我们有现有的代码将栈分配的缓冲区传递给驱动程序。
- 我知道
madvise
与MADV_DONTFORK
,它可以确保内存从子进程的空间消失,但它不适用于堆栈分配缓冲区。 - “不要使用叉子,而您的连接与我们的驱动程序处于激活状态”会很烦人,但如果满足第一点,则可以作为最后的手段。
- 理想情况下,我想那里的孩子的过程完全不使用我们的驱动程序(它可能几乎立即调用
我愿意指出,以文件或源代码。我特别看过Linux Device Drivers,但没有发现这个问题。 RTFS仅适用于内核源代码的相关部分,这有点令人难以置信。
内核版本不是完全固定的,而是最近的版本(比如≥2.6.26)。如果它很重要的话,我们只瞄准Arm平台(目前为止的单处理器,但多核处理器即将到来)。
A fork()
不会干扰get_user_pages()
:get_user_pages()
会给你一个struct page
。
你需要kmap()
才能访问它,这个映射是在内核空间而不是用户空间中完成的。
编辑:get_user_pages()
触摸页表,但你不应该担心这个(它只是确保该网页在用户空间映射),并返回-EFAULT如果有任何问题,这样做。
如果你调用fork(),直到写入时复制执行,孩子就能够看到该页面。 一旦完成写入操作(因为孩子/驱动程序/父母通过用户空间映射写入页面 - 而不是驱动程序所拥有的内核kmap()),该页面将不再共享。如果您仍然在页面上(在驱动程序代码中)持有kmap(),您将无法知道您是否持有父页面或孩子的页面。
1)这不是一个安全漏洞,因为一旦你的execve(),所有这一切都消失了。 2)当你fork()你想要两个进程是相同的(这是一个叉!!)。我认为你的设计应该允许父母和孩子访问驱动程序。 Execve()将刷新所有内容。
怎么样在用户空间中增加了一些功能,例如:
f = open("/dev/your_thing")
mapping = mmap(f, ...)
当mmap()的调用您的设备上,安装一个内存映射,有特殊标志: http://os1a.cs.columbia.edu/lxr/source/include/linux/mm.h#071
你有一些有趣之类的东西:
#define VM_SHARED 0x00000008
#define VM_LOCKED 0x00002000
#define VM_DONTCOPY 0x00020000 /* Do not copy this vma on fork */
VM_SHARED将在写入禁止复制 VM_LOCKED将d isable交换该页面 上VM_DONTCOPY会告诉内核不要到VMA区域复制叉,但我不认为这是一个好主意
简短的回答是你给你的驱动程序的任何用户空间的缓冲区使用madvise(addr, len, MADV_DONTFORK)
。这告诉内核映射不应该从父到子复制,所以没有CoW。
的缺点是,孩子继承在该地址的映射关系,所以如果你想要孩子,然后开始使用的驱动程序将需要重新映射内存。但是这在用户空间中很容易实现。
更新:在堆栈上的缓冲是有问题的,我不知道,你可以把它一般是安全的。
你不能标记它DONTFORK
,因为你的孩子可能在分岔时在堆栈页上运行,或者(更糟糕的是)它可能会稍后做一个函数返回并打到未映射的堆栈页。 (我甚至测试过这个,你可以愉快地标记你的堆栈DONTFORK,当你分叉时会发生不好的事情)。
的另一种方式,以避免牛是创建一个共享的映射,但你不能地图你的筹码原因很明显共享。
这意味着如果你分叉的话,你会冒一个CoW的风险。即使孩子“只是”执行,它仍可能触摸堆栈页面并导致CoW,导致父母获得不同的页面,这是不好的。
对你有利的一个微小的点是使用上的栈缓冲区只需要担心代码调用它分叉,即代码。函数返回后不能使用堆栈缓冲区。所以你只需要对你的被调用者进行审计,如果他们从不分叉,你是安全的,但是这仍然是不可行的,并且如果代码改变的话是脆弱的。
我觉得你真的想有一个是给你的驱动来自于用户空间自定义分配器的所有记忆。它不应该是侵入性的。分配器可以直接使用mmap
您的设备,或者使用匿名的mmap
,madvise(DONTFORK)
和可能的mlock()
来避免换出。
其实我已经意识到了'MADV_DONTFORK',但是与我们现在所做的(我们有使用堆栈上的缓冲区的代码)相比,这将是一种限制。我应该在我的问题中提到这一点。如果你有很长的回答,我会有兴趣阅读它。 – Gilles 2010-10-28 22:52:30
感谢这个有趣的答案。我正在维护现有的代码,并从Linux内核编程开始,并没有将'kmap'视为相关的。我并不在乎孩子是否无法使用我们的司机;如果过程分叉,这将是一个无关的目的,如“popen”。我不控制如何在用户空间分配内存(我们甚至有一些代码将栈上的缓冲区传递给我们的驱动程序)。有没有办法让*驱动程序*说“我希望这个物理页面保持映射到父级”(无论父级如何获得页面)? – Gilles 2010-10-28 20:35:19
您可以使用syscall'mlock()',它将基本上在目标vma上添加一个VM_LOCKED标志。 – 2010-10-28 21:31:52
@Gilles:我同意这两个优秀的答案。你基本上已经发现为什么其他设备不能这样工作,而是倾向于使用'mmap()'-of-device。如果可能的话,你应该更新驱动程序并修复用户空间。 – caf 2010-11-08 07:33:04