mmap是原子操作吗?

10
mmap调用的效果是否是原子的?
也就是说,mmap所做的映射更改是否会在其他访问受影响区域的线程中呈现出原子性?
作为一个试金石,考虑这样一个情况:你在一个全为零的文件中进行mmap(来自此时唯一的线程T1),然后启动第二个线程T2读取该区域。接着,在T1(原始线程)上再次进行第二个mmap调用,将映射替换为针对全为1的文件的新映射。
读取器线程是否可能从某个页面读取1(即看到第二个mmap生效),然后随后从某个页面读取0(即看到第一个映射生效)?
你可以假设读取器线程上的读取被正确地分隔开,即上述影响不仅仅是由于CPU/一致性级别内存访问重新排序引起的。

1
任何读取线程是否可能从某个页面读取一个(即看到第二个mmap生效),然后随后从某个页面读取零(即看到第一个映射生效)?没有充分考虑这个问题以实际制定答案,我认为您无法排除以任何顺序替换页面。如果多个页面被替换,我怀疑没有原子性或任何排序保证。 - Andrew Henle
1
@AndrewHenle - 事实上,除非内核在更新映射时暂停所有进程线程,或者如果它离线创建一个完全新的映射并交换页面表指针(例如,在x86上的CR3)到新的映射,否则很难看出它如何是原子性的,但我准备受到惊喜... - BeeOnRope
两个不同的线程发出了相互冲突、未同步的 mmap 调用,均试图映射相同的VM区域。我当然“希望”这两个 mmap 调用中的一个会失败。但就个人而言,我不会太担心它将以何种方式失败的具体细节,因为我从未有意写过一个依赖于该竞争以任何特定方式解决的程序。 - Solomon Slow
1
@SolomonSlow - 这不是这种情况:两个mmap调用来自同一个线程,只有一个线程在此处调用mmap。显然,我希望mmap调用对于进行调用的线程(即从返回后的代码的角度来看,mmap已完全生效)是原子的,但问题是第二个线程从受mmap调用影响的区域读取(或写入)。我将尝试澄清问题。 - BeeOnRope
2
我认为当一个地址空间的映射可能正在改变时,一个线程访问一块虚拟地址空间是不合法的。据我所知,没有任何保证,操作可能会故障或破坏事物。这不仅不是原子性操作,而且允许先取消映射所有页面,然后以任何顺序开始映射新页面或以任何方式进行操作,只要不破坏未被操作改变的页面即可。 - David Schwartz
2个回答

1

Mmap(2) 在所有线程之间的映射是原子性的;至少部分原因是 unmap(2) 也是如此。简单来说,所描述的情况大致如下:

MapRegion(from, to, obj) {
     Lock(&CurProc->map)
     while MapIntersect(&CurProc->map, from, to, &range) {
            MapUnMap(&CurProc->map, range.from, range.to)
            MapObjectRemove(&CurProc->map, range.from, range.to)
     }
     MapInsert(&CurProcc->map, from, to, obj)
     UnLock(&CurProc->map)
}

接下来,map_unmap必须确保在删除映射时,没有线程可以访问它们。请注意Lock(&thisproc->map)
MapUnMap(map, from, to) {
    foreach page in map.mmu[from .. to] {
         update page structure to invalidate mapping
    }
    foreach cpu in map.HasUsed {
         cause cpu to invoke tlb cache invalidation for (map, from, to)
    }
}

第一阶段是重写处理器特定的页面表以使区域失效。
第二阶段是强制每个曾经加载过此映射到其转换缓存中的cpu来使该缓存失效。这一位高度依赖于架构。在旧的x86上,重新编写cr3通常足够了,因此HasUsed实际上是CurrentlyUsing;而较新的amd64可能能够缓存多个地址空间标识符,因此将是HasUsed。在ARM上,本地tlb失效被广播到本地集群;因此HasUsed将指向集群id而不是cpu id。要了解更多详细信息,请搜索“tlb shootdown”,因为它通常被称为。
完成这两个阶段后,没有线程可以访问此地址范围。任何尝试这样做的尝试都会导致故障,从而导致故障线程锁定其映射结构,该结构已由映射线程锁定,因此它将等待映射完成。当映射完成时,所有旧映射都已被删除并替换为新映射,因此在此点之后无法检索先前的映射。
如果另一个线程在更新期间引用了地址范围,会怎样呢?它将继续使用旧数据或者出现故障。在这方面,旧数据并不是一种不一致性,就好像在“映射线程”进入“mmap(2)”之前刚刚引用了它一样。出现故障的情况与上述“故障线程”相同。
总之,通过一系列事务来实现对映射的更新,以确保地址空间的一致视图。这些事务的成本因架构而异。实现此功能的代码可能非常复杂,因为它需要防范隐式操作(例如,推测获取)以及显式操作。

我不确定munmap如何与mmap有关。如果我们用一个有效的页表项替换另一个,线程不会出现错误情况,因此无法进行锁定操作。在页面表更新和TLB清空之间,为什么不能出现这样一种情况:线程访问一个页面,未命中TLB,在页面表上查找并获取具有新数据的新映射;然后访问另一页,该页仍在其TLB中,并获得旧数据的旧映射? - Nate Eldredge
1
虽然陈旧的数据本身并不是不一致,但是新数据后面跟着陈旧的数据就会产生不一致。 - Nate Eldredge
它不这样做;它遵循一个严格的失效;添加的事务。任何其他行为都是无可辩驳的错误。新的先于旧的不会发生;只有旧的先于新的。 - mevets
1
我明白了,这样在失效后进行拦截很有意义。而在页面失效时访问会触发错误,并且线程将在错误处理程序中阻塞(等待锁),直到新映射准备好为止。 - Nate Eldredge

-3

内存映射发生在进程级别,因此同一进程中的所有线程都可以立即看到。


哎呀,当需要更改多个页面的映射时,实际上是如何实现的?内核会暂停整个过程吗? - BeeOnRope
@BeeOnRope 可能是因为它需要修改进程的页表。 - Barmar
这似乎是在高度线程化的进程中一个巨大的可扩展性瓶颈! - BeeOnRope
2
mmap()通常不会出现在性能关键的内部循环中。 - Barmar
3
@Barmer,是的,这不像有人会以此为基础实现 malloc 函数之类的东西。 - mevets
1
我认为这个答案不够技术性,不能被视为一个答案。如果您能更精确地扩展“进程级别”的含义以及它如何体现出问题所需的保证,那就太好了。现在看起来很像挥手之间的波动,就像人们在东拼西凑地使用“volatile”并宣称它是线程安全的一样。 - GManNickG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接