mmap 是原子的吗?

Posted

技术标签:

【中文标题】mmap 是原子的吗?【英文标题】:Is mmap atomic? 【发布时间】:2020-05-07 19:09:55 【问题描述】:

mmap 调用的效果是原子的吗?

也就是说,mmap 所做的映射更改对访问受影响区域的其他线程来说是原子的吗?

作为一个试金石,考虑一下你在一个全为零的文件中执行mmap 的情况(来自线程T1,这是此时唯一的线程),然后启动第二个线程T2 从区域读取。然后,再次在 T1(原始线程)上对同一区域进行第二次 mmap 调用,将映射替换为针对所有文件的新映射。

阅读器线程是否有可能从某个页面读取一个 1(即,参见第二个 mmap),然后随后从某个页面读取一个 0(即,参见第一个映射有效)?

您可以假设读取器线程上的读取被正确隔离,即上述效果不会仅由于 CPU/一致性级别的内存访问重新排序而发生。

【问题讨论】:

是否有可能任何阅读器线程从某个页面读取一个(即,参见第二个有效的 mmap),然后从某个页面读取一个零(即,参见第一个映射有效)? 如果没有对此进行足够的思考以实际制定答案,我认为您不能排除以任何顺序替换页面的可能性。如果多个页面被替换,我怀疑没有原子性或任何排序保证。 @AndrewHenle - 确实,除非内核在更新映射时暂停所有进程线程,或者如果要离线创建一个带有更改的全新映射然后交换页表指针(例如, CR3 on x86) 到新的映射,很难看出它是原子的,但我准备好感到惊讶了...... Re,来自两个不同线程的两个冲突、不同步的 mmap 调用,都试图映射同一个 VM 区域。我当然希望两个 mmap 调用之一会失败。但就个人而言,我不会太担心如何它会失败的确切细节,因为我永远不会故意编写一个依赖于以任何特定方式解决种族问题的程序。 @SolomonSlow - 情况并非如此:两个mmap 调用来自同一个线程,这里只有一个线程调用mmap。显然,我希望 mmap 调用对于进行调用的线程来说是原子的(即,mmap 在返回后从代码的 POV 中完全生效),但问题是关于第二个线程从(或写入)受mmap 调用影响的区域。我会尽量澄清这个问题。 我认为一个线程访问一块虚拟地址空间是不合法的,而该地址空间的映射可能正在改变。据我所知,没有任何保证,操作可能会出错甚至损坏东西。它不仅不是原子的,还允许首先取消映射所有页面,然后以任何顺序开始映射新页面,或者以任何它想要的方式进行操作,只要它不会破坏未被操作更改的页面。跨度> 【参考方案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 的。欲了解更多详情,请搜索tlb shootdown,因为这是俗称的。

这两个阶段完成后,没有thread 可以访问此地址范围。任何这样做的尝试都会导致错误,这将导致faulting thread 锁定其映射结构,该结构已经被mapping thread 锁定,因此它会一直等到映射完成。映射完成后,所有旧映射都已被删除并替换为新映射,因此在此之后无法检索先前的映射。

如果另一个thread 在更新期间引用该地址范围怎么办?它将继续使用 stale 数据或故障。在这方面,stale 数据并不是不一致的,就好像它在 mapping thread 进入 mmap(2) 之前被引用过一样。故障情况与上述faulting thread 相同。

总之,对映射的更新是使用一系列确保地址空间视图一致的事务来实现的。这些事务的成本是特定于架构的。实现这一点的代码可能非常复杂,因为它需要防范隐式操作,例如推测性获取以及显式操作。

【讨论】:

我不确定munmap 案例如何解决mmap。如果我们将一个有效的页表条目替换为另一个,则线程不会出错,因此无法获取锁。在页表更新和TLB击落之间,为什么我们不能出现线程访问一页,错过TLB,遍历页表并获取新数据的新映射的情况;然后访问仍在其 TLB 中的另一个页面并获取旧数据的旧映射? 陈旧数据本身并不是不一致,但新数据后跟陈旧数据才是。 它没有;它遵循严格的无效交易;添加。任何其他行为都无可争议地被打破。不会发生新的,然后是陈旧的;只有陈旧的,然后是新的。 我明白了,并且在无效后被击落。这就说得通了。并且页面失效时的访问将触发故障,并且线程将阻塞在故障处理程序中(等待锁定),直到新的映射准备好。【参考方案2】:

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

【讨论】:

呵呵,当需要更改多个页面的映射时,实际上是如何实现的?整个进程被内核暂停了吗? @BeeOnRope 可能,因为它必须修改进程的页表。 @BeeOnRope mmap() 通常不会在性能关键的内部循环中找到。 @Barmer,是的,它不像任何人会基于 malloc 或其他东西。 我认为这个答案的技术性不足以被视为一个答案。如果您能够更准确地扩展“流程级别”的含义以及它如何体现为问题所需的保证,那就太好了。现在它看起来非常像挥手,就像人们在东西上拍volatile并声明它是线程安全的。

以上是关于mmap 是原子的吗?的主要内容,如果未能解决你的问题,请参考以下文章

ClearAsync 方法是原子的吗?

memcached是原子的吗

原子增加和比较是线程安全的吗

批量插入是原子的吗?

切换运算符是原子的吗?

C/C++ 中的结构赋值是原子的吗?