clock_gettime(CLOCK_MONOTONIC) 跨内核/线程的单调性

Syn*_*ted 5 c linux multithreading clock

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

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

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

Nom*_*mal 9

我可以依靠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);
Run Code Online (Sandbox Code Playgroud)

没有绝对保证T2 > T1


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

在某些配置中,已知时钟源在所有 CPU 内核之间同步。例如,如果physical ID:中的所有字段都相同/proc/cpuinfo,并且所有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.
Run Code Online (Sandbox Code Playgroud)

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

您可以使用 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;
}
Run Code Online (Sandbox Code Playgroud)

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