【问题标题】:clock_gettime(CLOCK_MONOTONIC) monotonicity across cores/threadsclock_gettime(CLOCK_MONOTONIC) 跨内核/线程的单调性
【发布时间】:2017-10-23 15:27:40
【问题描述】:

我有多个相互通信的进程在双处理器 X86-64 Linux 机器的不同内核上运行。通信的内容包括时间戳。我想编写与时间相关的程序逻辑,并假设所有时间戳都来自同一个全局时钟。我可以指望clock_gettime(CLOCK_MONOTONIC) 给我单调的时间戳,即使是在不同内核上运行的不同线程上?

特别是,假设进程 A 获取时间戳 X 并通过共享内存将其发送到进程 B。进程 B 读取它,然后获取时间戳 Y。X 不能大于 Y。

使用clock_gettime(CLOCK_MONOTONIC) 获取的时间戳是否具有上述属性?如果不是,还有哪些其他类型的单调时间戳具有此属性?

【问题讨论】:

  • 您应该阅读有关时钟同步的内容。恕我直言,您不需要这个,因为您的进程之间已经存在因果关系(如果 A 向 B 发送了一条消息,显然,A 在 B 收到它之前就已经这样做了,因此您可以将 A +1 的时间戳作为新的时间戳)。

标签: c linux multithreading clock


【解决方案1】:

我能否指望clock_gettime(CLOCK_MONOTONIC) 为我提供单调时间戳,即使在不同内核上运行的不同线程中也是如此?

时间戳仅在同一核心上保证单调。也就是说,如果你有

Thread on CPU A core C                 Thread on CPU B core D

pthread_mutex_lock(&lock);
T1 = clock_gettime(CLOCK_MONOTONIC);
pthread_mutex_unlock(&lock);           pthread_mutex_lock(&lock);
                                       T2 = clock_gettime(CLOCK_MONOTONIC);
                                       pthread_mutex_unlock(&lock);

没有绝对保证T2 > T1。


Linux 内核尽最大努力确保T2 > T1,但问题在于硬件:一些硬件没有足够好的同步时间源。在这样的硬件上,创建一个在所有 CPU 和内核之间保持同步的可靠单调时钟将需要进程间中断或其他方式来将单个时钟值保持在某个地方,这太慢而无法高效。

在某些配置中,已知时钟源在所有 CPU 内核之间同步。例如,如果/proc/cpuinfo 中的所有physical ID: 字段都相同,并且所有flags: 字段都具有tsc_reliable,则已知时间戳计数器寄存器在所有内核之间同步并用作时间源。但是,在实践中,您不会进行此类检查,因为结果是推断的,内核无法保证,因此可能是错误的。

在实践中,我们计算事物时就好像我们假设 CLOCK_MONOTONIC 在内核之间是单调的,但是是务实的和检查的。

对于可能不同内核上的线程之间的时间消息传递或信号,我们测量往返时间。使用大量的往返,并选择时间的中位数:这会给你一个可靠的结果,你可以说“至少有一半的往返在时间 T 内完成" 充满信心,毫不含糊。 (通常你可能会选择一个更高的点,比如 68.3% 或 95%。)


如果您需要可靠的 CLOCK_MONOTONIC 派生时间戳跨可以访问同一共享内存段的进程,您可以通过将“当前”时间戳存储在该共享内存中来实现它。

当一个进程想要一个时间戳时,它相当于

Do:
    T0 = clock_gettime(CLOCK_MONOTONIC)
    Ts = shared timestamp
    T = max(T0, Ts)
While CompareExchange(shared timestamp, Ts, T) fails.
Use T as timestamp.

也就是说,它比较本地单调时钟和共享时间戳,将共享时间戳更新为两者中较高的一个,并将其用作时间戳。

您可以使用 GCC 的 __atomic_compare_exchange_n() 内置更新共享时间戳,而无需持有任何锁。 (不必从共享内存中原子地读取时间戳,因为原子性比较和交换会处理原子性。)

唯一的缺点是,如果很多线程经常这样做,由于缓存线乒乓,你确实会得到一些开销。

请注意,如果您使用uint64_t(以纳秒为单位)作为时间戳,您将需要在max 函数中考虑回绕:

static inline uint64_t  max_wraparound(const uint64_t  a, const uint64_t  b)
{
    return ((uint64_t)(a - b) < UINT64_C(9223372036854775808)) ? a : b;
}

这样,任何两个此类时间戳(before 和 after)之间的差异始终为 (uint64_t)(after - before),即使时间戳值介于两者之间。

【讨论】:

    【解决方案2】:

    POSIX 根据系统范围的时钟定义 CLOCK_MONOTONIC。系统范围意味着符合单个系统映像的所有内核、套接字、集群 [即。一个内核]。

    我饶有兴趣地阅读了 Nominal Animals 的回答,对我来说似乎有点震惊,有人可以观察到 CLOCK_MONOTONIC 倒退并且似乎违反了 POSIX 合同。

    【讨论】:

    • 归咎于硬件.. 现实并不总是像我们希望的那样美好和可预测。不要忘记:Linux 可以在各种各样的硬件上运行,而不仅仅是 x86 和 x86-64。例如,发现了 Allwinner A64 (ARM Cortex-A53) 上的硬件计时器问题,并且显然已修复 this year, 2018(链接是 LKML 讨论)。尽管该特定内核解决方法似乎有效(并向后修复 CLOCK_MONOTONIC),但基于软件的修复并不总是可行的。
    • 冒着跨入宗教的风险,内核的主要职责是使硬件可靠且一致。选择不这样做只是失败。
    • 理论上一切都很好。正是这种做法像冷鳕鱼一样打在我们脸上。无论走到哪里都不戴遮阳板,以防万一有鳕鱼飞来飞去。这是软件工程和计算机科学之间的确切区别:一个处理代码和工作级别的实际问题,另一个处理数学上精确的完美机器。如果我们实用,我们将两者结合起来。然后,我们不会失败,而是可以稳健并从小故障中恢复,并将主要故障通知人类用户。
    猜你喜欢
    • 1970-01-01
    • 2011-10-13
    • 2017-03-23
    • 1970-01-01
    • 2021-03-01
    • 2022-01-18
    • 2016-05-08
    • 1970-01-01
    相关资源
    最近更新 更多