【问题标题】:Behavior of mprotect with multiple threads多线程的 mprotect 行为
【发布时间】:2017-09-29 01:35:33
【问题描述】:

为了并发/并行GC, 我对 mprotect 系统调用提供的内存顺序保证感兴趣(即 mprotect 具有多个线程的行为或 mprotect 的内存模型)。我的问题是(假设没有编译器重新排序或有足够的编译器障碍)

  1. 如果线程 1 由于 mprotect on 而触发地址上的段错误 线程 2,我可以确定一切都发生在线程 2 之前 系统调用可以在线程 1 的信号处理程序中观察到 段错误?如果在信号中放置了一个完整的内存屏障怎么办 在线程 1 上执行加载之前的处理程序?

  2. 如果线程 1 对设置为 线程 2 的 PROT_NONE 并且没有触发段错误,这足够了吗 a 发生在两者之间的关系之前。或者换句话说,如果 两个线程都可以(*ga0 开头,p 是一个以只读方式启动的页面对齐地址)

    // thread 1
    *ga = 1;
    *(volatile int*)p; // no segfault happens
    
    // thread 2
    mprotect(p, 4096, PROT_NONE); // Or replace 4096 by the real userspace-visible page size
    a = *ga;
    

    是否可以保证线程 2 上的 a 将是 1? (假设没有 在线程 1 上观察到 segfault 并且没有其他代码修改 *ga)

我最感兴趣的是 Linux 行为,尤其是 x86(_64)、arm/aarch64 和 ppc,尽管欢迎访问有关其他 archs/OS 的信息(对于 Windows,将 mprotect 替换为 VirtualProtect 或其他任何名称... .)。到目前为止,我在 x64 和 aarch64 Linux 上的测试表明没有违反这些规定,但我不确定我的测试是否是结论性的,或者是否可以长期依赖这种行为。

一些搜索表明mprotect 可能会在删除权限时对具有映射地址的所有线程发出 TLB 击落,这可能提供此处所述的保证(或者换句话说,提供此保证似乎是此类操作的目标) 虽然我不清楚内核代码的未来优化是否会破坏这一保证。

Ref LKML post 一周前我在哪里问过这个问题,但还没有回复...

编辑:澄清问题。我知道 tlb 击落应该提供我正在寻找的保证,但我想知道是否可以依赖这种行为。换句话说,内核发出此类请求的原因是什么,因为如果不是为了提供某种排序保证,就不需要它。

【问题讨论】:

  • 你所说的“在系统调用可以被观察到之前”是什么意思?怎么观察?你是说系统调用的效果吗?如果我们从内核角度来看,系统调用是一个更改虚拟内存属性的请求,其影响将通过翻译缓冲区传播,并且如果多个用户空间线程在不同的内核(或不同的 CPU 上运行,具体取决于在架构上),他们可能会在不同的物理时刻看到效果,除非完成刷新。在 Linux 中,TLB 刷新已完成,除非旧属性不允许访问。 [...]
  • [...] 这意味着行为会有所不同,具体取决于系统调用是允许(PROT_NONEPROT_READ)还是不允许(PROT_READPROT_NONE)访问。在 Linux 中,前一种情况下省略了 TLB 刷新,因此并发运行的线程可能会在不同时间看到变化。在后一种情况下,TLB 刷新已完成,因此线程应同时观察更改(在 TLB 刷新时)——尽管我不确定是否有硬件在单独的 CPU(包!)上进行 TLB 刷新非同时的。
  • 我怀疑没有回复 LKLM 的原因有两个:一,这听起来很抽象,内核开发人员很务实;大多数人都有足够多有趣的现实世界问题来处理,而不太关心没有现实世界影响的抽象问题。第二,很难理解这里的确切情况是什么。一个真实世界的用例——“mm 逻辑是否保证这个场景有效?” -- 使用 ASCII 艺术图显示在同时线程中会发生什么会有很大帮助。
  • @NominalAnimal 观察系统调用意味着由于 mprotect 调用,权限被翻转的页面上的内存操作出错(或不出错)。重新 TLB 刷新,是的,我知道它是在 linux 上完成的,尽管我的问题更多是关于我可以依赖什么行为,或者换句话说,它试图通过发出这样的刷新来维持什么保证。回复:一个真实世界的用例,我正在跳伪代码以防情况2足以开始,似乎不是=(

标签: c multithreading garbage-collection virtual-memory mprotect


【解决方案1】:

所以我在这里发帖一天后在机械同情小组上问了这个问题,并得到了 Gil Tene 的回答。征得他的同意,这是我对他的回答的总结。完整的线程可用here 以防我没有包括任何不清楚的内容。

对于可以从操作系统获得的整体行为。

(如“操作系统不满足会令人惊讶):

  1. 对 mprotect() 的调用按照调用前后发生的加载和存储完全排序。这往往在 CPU 和操作系统级别上很容易实现,因为 mprotect 是一个系统调用,它涉及一个陷阱,而该陷阱又涉及全排序。 [在奇怪的无环转换实现(例如内核执行等)中,保护调用可能负责模拟这种排序假设]。

  2. 在保护请求从语义上占据进程中的所有位置之前,不会返回对 mprotect 的调用。如果 mprotect() 调用设置了会导致错误的保护,则在此 mprotect() 调用之后发生的任何线程上的任何操作都需要出现错误。同样,如果 mprotect() 调用设置了防止故障的保护,则在此 mprotect() 调用之后发生的任何线程上的任何操作都必须不出错。

这实质上意味着其他线程上受影响页面上的内存操作与调用mprotect的线程同步。更具体地说,可以预期原始问题中提到的两种情况都得到保证。即

  1. 如果观察到由于 mprotect 调用导致受影响页面中的一个线程上的负载发生错误,则此错误发生在 mprotect() 调用之后,因此之后并且能够观察到在 mprotect 之前发生的所有内存操作.

  2. 如果观察到受影响页面中的一个线程上的加载不会导致 mprotect 调用发生故障,则加载发生在 mprotect 调用之前,mprotect 调用以及之后的任何代码都在加载之后并且将能够观察加载之前发生的任何内存操作。

还有人指出,传递性可能不起作用,即一个线程上的错误加载可能不在另一个线程上的非错误加载之后。这可能(有效地)是由于 tlb 刷新的非原子性导致不同的线程/cpu 在不同时间观察访问权限的变化。

【讨论】:

  • 我实际上不太确定您报价中的数字 2。这是一个重言式。 “在你观察到 X 之后发生的任何事情都会在你观察到 X 之后发生”。我的意思是“在这个 mprotect 调用之后”实际上应该说“在这个线程观察到 mprotect 调用的效果之后”。某些内存操作的排序问题在于,“之前”和“之后”仅在您有内存屏障或显式排序指令时才有意义。我认为拯救我们的是 TLB 击落必须是内存屏障(我无法想象在没有它的架构上工作)。
  • 但是您的实际问题很可能没有答案:我从未见过系统调用的正式内存模型。我认为假设是“它们当然是同步的,没有它就不可能完成任何事情”,但正式地说这很可能是未定义的。即使在我使用过的最疯狂的内存模型上,系统调用也不会返回,直到一切都同步(我已经编写了代码,我们没有等待 tlb 完全同步,直到返回用户空间之前的最后一条指令,但是太危险了,没买多少)。
  • "before" and "after" are only meaningful when ... -- 确切地说,这就是为什么当您说线程 1 上的 A 发生在线程 2 上 B 之前发生时,这意味着同步。这正是我询问的 mprotect 调用是否暗示的。 Even on the craziest memory models I've worked with system calls didn't return until everything was in sync -- 这正是我要问的,因为我对非 x86/多套接字系统的经验非常有限。而且问题还包括 mprotect 的“同步”的精确定义。
  • 所以另一种(不太正式的)提问方式是:“在另一个线程上的 mprotect 之前/之后是否发生某事”有意义或“操作系统在mprotect 系统调用”。
  • 这个周末我一直在想这个问题。完全忽略系统调用,我认为系统调用在这里是一个红鲱鱼。另一种看待这个问题的方式是:陷阱是否同步?如果您的 CPU 有一个存储缓冲区,可以将写入保存到不同的缓存行/页面和一个写入陷阱?它们会按照发出的顺序被冲走吗?如果存储缓冲区是写组合怎么办?存储缓冲区的全部意义在于重新排序写入。如果其中一个存储陷阱,存储缓冲区会耗尽吗?如果排水导致另一个陷阱怎么办?应该先送哪个陷阱?
猜你喜欢
  • 2013-08-26
  • 2015-10-26
  • 1970-01-01
  • 2013-09-20
  • 2015-11-05
  • 2012-02-29
  • 2010-10-25
  • 2016-02-09
  • 1970-01-01
相关资源
最近更新 更多