3

我正在审查一些代码并对所使用的技术感到怀疑。

在 linux 环境中,有两个进程附加多个共享内存段。第一个进程定期加载一组要共享的新文件,并将共享内存 id (shmid) 写入“主”共享内存段中的某个位置。第二个进程不断读取这个“主”位置并使用 shmid 附加其他共享段。

在多 CPU 主机上,在我看来,如果一个进程在另一个进程正在写入内存时尝试读取内存会发生什么,这可能取决于实现。但也许硬件级总线锁定可以防止电线上的位错位?读取过程是否获得了一个很快就会改变的值并不重要,只有读取被破坏为既不是旧值也不是新值的东西才重要。这是一个边缘情况:只有 32 位被写入和读取。

谷歌搜索 shmat 的东西并没有让我在这个领域找到任何确定的东西。

我强烈怀疑它不安全或不理智,我真正想要的是一些详细描述问题的文章的指针。

4

13 回答 13

12

这是合法的——因为在操作系统中不会阻止你这样做。

但它聪明吗?不,你应该有某种类型的同步。

不会有“电线上的损坏位”。它们会以 1 或 0 的形式出现。但是没有什么可说的,在另一个进程尝试读取它们之前,你的所有位都会被写出。并且无法保证它们的写入速度与读取速度。

您应该始终假设 2 个进程(或线程)的操作之间绝对没有关系。

除非您做对了,否则不会发生硬件级总线锁定。使您的编译器/库/操作系统/cpu正确执行可能比预期的要难。编写同步原语以确保它正确发生。

锁定将使其安全,并且并不难做到。所以就去做吧。


@unknown - 自从我的答案发布以来,这个问题发生了一些变化。但是,您描述的行为完全取决于平台(硬件、操作系统、库和编译器)。

如果不给编译器特定的指令,实际上并不能保证一次性写出 32 位。想象一下 32 位字未在字边界上对齐的情况。这种不对齐的访问在 x86 上是可以接受的,而在 x68 的情况下,访问被 cpu 变成了一系列对齐的访问。

这些操作之间可能会发生中断。如果在中间发生上下文切换,则某些位会被写入,而另一些则不会。砰,你死定了。

另外,让我们考虑一下 16 位 CPU 或 64 位 CPU。两者仍然很受欢迎,并且不一定按您的想法工作。

因此,实际上您可能会遇到“其他一些 cpu 核心拾取 1/2 写入的字大小值”的情况。如果您不使用同步,您编写代码就好像预期会发生这种类型的事情。

现在,有一些方法可以预写你的文章,以确保你能写出一个完整的单词。这些方法属于同步的范畴,创建同步原语是最好留给库、编译器、操作系统和硬件设计人员的事情类型。特别是如果您对可移植性感兴趣(即使您从未移植过代码,也应该如此)

于 2009-04-17T21:46:56.060 回答
10

这个问题实际上比一些人讨论的还要严重。Zifre 是正确的,在当前的 x86 CPU 上,内存写入是原子的,但这种情况很快就不再是这样了——内存写入仅对单个核心是原子的——其他核心可能不会以相同的顺序看到写入。

换句话说,如果你这样做

a = 1;
b = 2;

在 CPU 2 上,您可能会看到b在位置“a”之前修改了位置。此外,如果您写入的值大于本机字长(x32 处理器上的 32 位),则写入不是原子的 - 因此 64 位写入的高 32 位将在与低位不同的时间到达总线32位写入。这会使事情变得非常复杂。

使用内存屏障,你会没事的。

于 2009-04-22T04:34:55.873 回答
7

你需要在某个地方锁定。如果不在代码级别,那么在硬件内存缓存和总线。

在后 PentiumPro 英特尔 CPU 上您可能没问题。从我刚刚读到的内容来看,英特尔让他们后来的 CPU 基本上忽略了机器代码上的 LOCK 前缀。相反,缓存一致性协议确保数据在所有 CPU 之间保持一致。因此,如果代码写入的数据不跨越缓存行边界,它将起作用。无法保证跨缓存线的内存写入顺序,因此多字写入是有风险的。

如果您使用的是 x86 或 x86_64 以外的任何东西,那么您就不行。许多非英特尔 CPU(可能还有英特尔安腾)通过使用显式缓存一致性机器命令来获得性能,如果您不使用它们(通过自定义 ASM 代码、编译器内部函数或库),则无法保证通过缓存写入内存永远对另一个 CPU 可见或以任何特定顺序发生。

因此,仅仅因为某些东西在您的 Core2 系统上运行并不意味着您的代码是正确的。如果您想检查可移植性,请在其他 SMP 架构上尝试您的代码,例如 PPC(较旧的 MacPro 或 Cell 刀片)或 Itanium 或 IBM Power 或 ARM。Alpha 是一款出色的 CPU,可用于揭示不良 SMP 代码,但我怀疑您能否找到它。

于 2009-04-17T22:24:17.750 回答
3

在通过内存共享数据时,两个进程、两个线程、两个 cpu、两个内核都需要特别注意。

这篇 IBM 文章很好地概述了您的选择。

Linux 同步方法剖析 内核原子、自旋锁和互斥锁,作者:M. Tim Jones (mtj@mtjones.com),Emulex 顾问工程师

http://www.ibm.com/developerworks/linux/library/l-linux-synchronization.html

于 2009-04-25T13:25:55.653 回答
2

我实际上认为这应该是完全安全的(但取决于确切的实现)。假设“主”段基本上是一个数组,只要 shmid 可以原子地写入(如果它是 32 位那么可能没问题),并且第二个进程只是读取,你应该没问题。仅当两个进程都在写入时才需要锁定,或者正在写入的值不能以原子方式写入。你永远不会得到一个损坏的(半写的值)。当然,可能有一些奇怪的架构无法处理这个问题,但在 x86/x64 上应该没问题(可能还有 ARM、PowerPC 和其他常见架构)。

于 2009-04-17T22:33:03.017 回答
2

阅读现代微处理器中的内存排序,第一部分第二部分

他们给出了为什么这在理论上是不安全的背景。

这是一场潜在的比赛:

  • 进程 A(在 CPU 内核 A 上)写入新的共享内存区域
  • 进程 A 将该共享内存 ID 放入共享的 32 位变量中(即 32 位对齐 - 如果您允许,任何编译器都会尝试像这样对齐)。
  • 进程 B(在 CPU 内核 B 上)读取变量。假设 32 位大小和 32 位对齐,它不应该在实践中得到垃圾。
  • 进程 B 尝试从共享内存区域读取。现在,不能保证它会看到 A 写入的数据,因为您错过了内存屏障。(实际上,在映射共享内存段的库代码中,CPU B 上可能碰巧存在内存屏障;问题是进程 A 没有使用内存屏障)。

此外,还不清楚如何使用这种设计安全地释放共享内存区域。

使用最新的内核和 libc,您可以将 pthreads 互斥锁放入共享内存区域。(这确实需要带有 NPTL 的最新版本——我使用的是 Debian 5.0 “lenny”,它工作正常)。围绕共享变量的简单锁定意味着您不必担心神秘的内存屏障问题。

于 2009-04-27T18:19:59.567 回答
1

我不敢相信你会问这个。,它不一定安全。至少,这将取决于编译器是否生成在您设置 shmid 时自动设置共享内存位置的代码。

现在,我不了解 Linux,但我怀疑 shmid 是 16 到 64 位的。这意味着至少有可能所有平台都有一些指令可以原子地写入这个值。但是你不能在不被询问的情况下依赖编译器来做这件事。

内存实现的细节是最特定于平台的东西之一!

顺便说一句,在您的情况下可能无关紧要,但总的来说,即使在单个 CPU 系统上,您也必须担心锁定问题。通常,某些设备可以写入共享内存。

于 2009-04-17T21:40:08.230 回答
1

合法的?我想。取决于您的“管辖权”。安全和理智?几乎可以肯定不是。

编辑:我会用更多信息更新它。

你可能想看看这个维基百科页面;特别是关于“协调对资源的访问”的部分。特别是,维基百科的讨论基本上描述了信心失败;即使对于原子资源,对共享资源的非锁定访问也会导致误报/误传对已完成操作的信心。本质上,在检查它是否可以修改资源之间的时间段内,资源被外部修改,因此,条件检查中固有的信心被破坏了。

于 2009-04-17T21:46:34.500 回答
1

我同意它可能会起作用 - 所以它可能是安全的,但并不理智。主要问题是是否真的需要这种低级共享 - 我不是 Linux 专家,但我会考虑为主共享内存段使用例如 FIFO 队列,以便操作系统为您完成锁定工作. 无论如何,消费者/生产者通常需要队列进行同步。

于 2009-04-22T04:42:56.853 回答
1

我不相信这里有人讨论过锁争用对总线的影响有多大,尤其是在总线带宽受限的系统上。

是一篇关于这个问题的深入文章,他们讨论了一些替代调度算法,这些算法减少了对通过总线的独占访问的总体需求。在某些情况下,这比简单的调度程序增加了 60% 以上的总吞吐量(当考虑显式锁定前缀指令或隐式 xchg cmpx 的成本时)。这篇论文不是最新的工作,也没有多少真正的代码(当学术的),但它值得阅读和考虑这个问题。

较新的 CPU ABI 提供了比简单的锁定任何其他操作。

来自 FreeBSD(许多内部内核组件的作者)的 Jeffr 讨论了 monitor 和mwait,为 SSE3 添加了 2 条指令,在一个简单的测试用例中确定了 20% 的改进。他后来假设;

所以现在这是自适应算法的第一阶段,我们旋转一段时间,然后在高功率状态下睡眠,然后根据负载在低功率状态下睡眠。

...

在大多数情况下,我们仍然在 hlt 中闲置,因此对功率应该没有负面影响。事实上,进入和退出空闲状态会浪费大量时间和精力,因此它可以通过减少所需的总 CPU 时间来提高负载下的功率。

我想知道使用暂停而不是hlt会产生什么影响。

来自英特尔的 TBB;

        ALIGN 8
        PUBLIC __TBB_machine_pause
__TBB_machine_pause:
L1:
        dw 090f3H; pause
        add ecx,-1
        jne L1
        ret
end

Art of Assembly还使用同步,不使用锁定前缀或 xchg。我有一段时间没有读过那本书,也不会直接谈论它在用户域保护模式 SMP 上下文中的适用性,但值得一看。

祝你好运!

于 2009-05-29T10:24:27.503 回答
0

如果 shmid 有其他类型,volatile sig_atomic_t那么您可以很确定即使在同一个 CPU 上,单独的线程也会遇到麻烦。如果类型是,volatile sig_atomic_t那么您就不能完全确定,但是您仍然可能会很幸运,因为多线程可以比信号可以做更多的交错。

如果 shmid 跨越高速缓存行(部分在一个高速缓存行中,部分在另一个高速缓存行中),那么当写入 cpu 正在写入时,您肯定会发现一个正在读取的 cpu 读取新值的一部分和旧值的一部分。

这正是发明“比较和交换”等指令的原因。

于 2009-04-22T06:24:30.370 回答
0

听起来你需要一个读写器锁:http ://en.wikipedia.org/wiki/Readers-writer_lock 。

于 2009-04-22T13:41:57.633 回答
-1

答案是——同时进行读写是绝对安全的。

很明显,shm 机制为用户提供了基本的工具。所有访问控制都必须由程序员负责。内核友好地提供了锁定和同步,这意味着用户不必担心竞争条件。请注意,此模型仅提供了一种在进程之间共享数据的对称方式。如果一个进程希望通知另一个进程新数据已插入共享内存,它必须使用信号、消息队列、管道、套接字或其他类型的 IPC。

来自Linux文章中的共享内存。

最新的 Linux shm 实现只是使用copy_to_usercopy_from_user调用,它们在内部与内存总线同步。

于 2009-04-27T17:47:58.770 回答