【问题标题】:shm_open and ftruncate race condition possible?shm_open 和 ftruncate 竞争条件可能吗?
【发布时间】:2013-05-06 08:22:00
【问题描述】:

来自shm_open 手册页:

一个新的共享内存对象最初的长度为零。的大小 可以使用 ftruncate(2) 设置对象。 [...] shm_open() 函数 本身不会创建指定大小的共享对象,因为 这样做会复制一个现有的函数,该函数设置一个 文件描述符引用的对象。

这不会将应用程序暴露给竞争条件吗?考虑以下伪代码:

int fd = shm_open("/foo", CREATE);
if ( fd is valid ) {
  // created shm object, so set its size
  ftruncate(fd, 128);
} else {
  fd = shm_open("/foo", GET_EXISTING);
}
void* mem = mmap(fd, 128);

由于shm_openftruncate 调用(一起)不是原子的,您可能会遇到一个竞争条件,即一个进程调用shm_openCREATE 情况),但在调用ftruncate 之前,另一个进程调用shm_open (GET_EXISTING case) 并尝试 mmap 大小为 0 的对象,甚至可能写入它。

我可以想出两种方法来避免这种竞争条件:

  1. 使用 IPC 互斥量/信号量使整个事物同步,或者...

  2. 如果安全(根据 POSIX),请在 CREATEGET_EXISTING 两种情况下调用 ftruncate

避免这种竞争条件的首选方法是什么?

【问题讨论】:

    标签: c++ c linux shared-memory mmap


    【解决方案1】:

    您的方法(从两者调用ftruncate)应该可以工作,但无论如何您需要一种方法来同步共享内存段的内容的使用。由于内存最初是空的(零填充),因此不包含有效的同步对象,除非您打算使用原子来滚动自己的同步对象,否则无论如何您都需要辅助形式的同步来控制对共享内存的访问。

    我通常认为,与其让多个进程竞相创建或打开一个具有固定名称的共享内存段,不如让一个所有者进程负责创建一个 随机 名称,使用O_EXCL 以避免随机或恶意冲突,然后在成功打开它、调整大小并在其中创建同步对象后将名称传递给需要访问它的其他进程。

    【讨论】:

    • 这是一个通用/库函数,因此共享内存本身的同步由应用程序决定(因为原子/无锁操作可能就足够了)。此外,名称由调用者指定。 (O_CREATE|O_EXCL) 始终按照伪代码中的指示进行检查。基本上,您建议的逻辑将取决于调用者。我想我要做的是从两者中调用ftruncate,并使用flock 来确保ftruncate 调用(可以将数据归零)不会导致竞争条件。
    • flock 甚至不是 POSIX,但即使您使用 fcntl 锁或 lockf,也不能保证它适用于共享内存文件描述符。无需担心两个ftruncate 调用之间的竞争;每个都是原子的,只要调用大小相同,操作就是幂等的。
    • 是否有文档显示ftruncate 如果从多个线程或进程同时调用,则保证是原子的和幂等的。这样只有一个调用将对内存进行零初始化,并且在初始化完成之前不会返回任何调用。如果ftruncate 有这种同步,我会感到惊讶,在这种情况下需要这种同步来避免竞争条件(即使它是十亿分之一)。这听起来很偏执,但是将使用它的应用程序是高度并行的,我无法控制它们如何或何时调用此函数。
    • 我会寻找一般保证,但这里来自truncate:“如果文件之前大于长度,则丢弃多余的数据。如果文件之前小于长度,它的大小增加,扩展区域看起来好像是零填充的。”请注意,没有单独的零填充步骤。从实现的角度来看,原子性是微不足道的,因为无论如何您都必须对打开的文件描述进行内部锁定。
    • 顺便说一句,从安全的角度来看,零填充基本上需要一个锁。如果有办法观察“扩展但未归零”的文件,则该进程将能够观察它不一定有权访问的数据(例如,释放内核内存或删除属于其他用户的文件)。
    【解决方案2】:

    作为@R。提到,这里的另一个问题是,在创建文件后,在内容(例如互斥锁)被初始化并准备好使用之前仍然有一个窗口。

    与上述略有不同的解决方案是:

    尝试打开()。 如果 open() 成功,只需 map() 并使用必要的保证(见下文)内容已经初始化并且可以使用。 如果 open() 失败,创建并初始化一个临时文件,然后尝试将临时文件硬链接() 为所需文件并 unlink() 临时名称。

    如果 link() 成功,我们现在已经为我们自己和其他进程提供了初始化文件。如果 link() 因 EEXIST 失败,则另一个进程首先到达那里(与 rename() 不同,如果目标名称存在,则 link() 失败)。无论哪种方式,我们重复的 open() 现在应该会成功并初始化一个可以使用的文件。

    在这种策略下,临时文件的初始化显然存在竞争条件,但只要初始化过程是幂等的,资源不会过于昂贵,并且每个进程都选择一个唯一的临时文件,这没有任何后果。如果多重初始化可能是一个问题,一个解决方案是将初始化分成一个两阶段的过程,第一阶段只是文件中的一个互斥锁,用于防止在第二阶段对文件的其余部分进行多次初始化。

    【讨论】:

      猜你喜欢
      • 2019-09-09
      • 1970-01-01
      • 2013-04-13
      • 2016-07-03
      • 2021-12-06
      • 1970-01-01
      • 1970-01-01
      • 2017-07-17
      • 2010-11-05
      相关资源
      最近更新 更多