OSX上的单调时钟

22

CLOCK_MONOTONIC不可用,因此clock_gettime也不能用。

我在一些地方读到说mach_absolute_time()可能是正确的选择,但在阅读了它是“cpu相关值”之后,我立刻想知道它是否正在使用rtdsc。 因此,即使是单调的,该值随时间漂移。 此外,线程亲和力的问题可能导致从调用函数获得有意义的不同结果(使其在所有核心上都不是单调的)。

当然,这只是猜测。 有人知道mach_absolute_time实际上是如何工作的吗? 我实际上正在寻找替代clock_gettime(CLOCK_MONOTONIC…或类似物)的方法,适用于OSX。 无论时钟源是什么,我都希望至少具有毫秒精度和毫秒准确性。

我想了解有哪些时钟可用,哪些时钟是单调的,某些时钟是否会漂移,是否存在线程亲和性问题,在所有Mac硬件上是否支持某些时钟或执行需要“超高”的CPU周期数。

以下是我能够找到的关于此主题的链接(其中一些已经失效并且在archive.org上不可找到):

https://developer.apple.com/library/mac/#qa/qa1398/_index.html http://www.wand.net.nz/~smr26/wordpress/2009/01/19/monotonic-time-in-mac-os-x/ http://www.meandmark.com/timing.pdf

谢谢! Brett


据我所知,CLOCK_MONOTONIC并不能保证值不会漂移,也不能保证线程亲和性没有问题。 - zneak
3
有点超出我的理解范围,但是可以从源代码中看到mach_absolute_time确实使用了rtdsc,具体请参考源代码. - cobbal
@zneak:缺乏set_time类型函数是单调时钟的定义属性。 单调时钟即使在连续查询时经过夏令时,也永远不会倒退。来自clock_gettime手册页的介绍:"CLOCK_MONOTONIC是一个不能被设置的时钟,表示从某个未指定的起点开始的单调时间。" - Brett
@Brett,不用担心,因为clock_set_time 系统性地返回KERN_FAILURE。至于哪个是合适的,SYSTEM_CLOCK "单调且均匀地前进"。这听起来很像CLOCK_MONOTONIC - zneak
你也可以阅读Mach源代码,以确保它的正确性 :) http://opensource.apple.com/source/xnu/xnu-2422.1.72/libsyscall/wrappers/mach_absolute_time.s - nielsbot
显示剩余7条评论
3个回答

28
Mach 内核提供访问系统时钟的功能,其中至少有一个(SYSTEM_CLOCK)被文档宣传为单调递增。
#include <mach/clock.h>
#include <mach/mach.h>

clock_serv_t cclock;
mach_timespec_t mts;

host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock);
clock_get_time(cclock, &mts);
mach_port_deallocate(mach_task_self(), cclock);

mach_timespec_t 具有纳秒级精度。但我对其准确性并不确定。

Mac OS X 支持三种时钟:

  • SYSTEM_CLOCK 返回自开机以来的时间;
  • CALENDAR_CLOCK 返回自1970-01-01以来的UTC时间;
  • REALTIME_CLOCK 已经被弃用,目前实现与 SYSTEM_CLOCK 相同。

clock_get_time的文档称时钟是单调递增的,除非有人调用clock_set_time。 调用clock_set_time不鼓励的, 因为它可能会破坏时钟的单调性质,实际上,当前的实现在不执行任何操作的情况下返回KERN_FAILURE


似乎 mach_task_self() 不再存在,现在有一个 mach_task_self_。谢谢回答! - Zhao
那是哪个操作系统?El Capitan? - zneak
抱歉,我的系统是Yosemite 10.10.5。我正在进行iOS开发。我已经安装了最新的iOS 9 SDK。我的部署目标是8.0。 - Zhao
@zhaow mach_task_self()是Darwin和iOS,tvOS以及macOS的基本功能之一,这些系统都基于Dawin。 没有这个函数,整个系统将停止工作,因为没有它就无法在用户空间和内核空间之间进行交互。 因此,只要苹果在Darwin中使用mach微内核,我相信这个函数仍然存在并且将永远存在。 - Mecki
从测试(在macOS Sierra 10.12.2上),看起来 SYSTEM_CLOCK 支持纳秒级分辨率,但 CALENDAR_CLOCK 只支持微秒级分辨率;mts.tv_nsec 的值始终是 CALENDAR_CLOCK 的1000的倍数。(并且 mach_task_self() 存在足够编译问题中所示的代码没有问题。) - Jonathan Leffler
@Zhao 如果你没有 mach_task_self(),那么你没有包含正确的头文件。(它是一个宏定义为 mach_task_self_ - tbodt

9

在查阅了几个不同的答案之后,我最终定义了一个头文件,模拟了mach上的clock_gettime函数:

#include <sys/types.h>
#include <sys/_types/_timespec.h>
#include <mach/mach.h>
#include <mach/clock.h>

#ifndef mach_time_h
#define mach_time_h

/* The opengroup spec isn't clear on the mapping from REALTIME to CALENDAR
 being appropriate or not.
 http://pubs.opengroup.org/onlinepubs/009695299/basedefs/time.h.html */

// XXX only supports a single timer
#define TIMER_ABSTIME -1
#define CLOCK_REALTIME CALENDAR_CLOCK
#define CLOCK_MONOTONIC SYSTEM_CLOCK

typedef int clockid_t;

/* the mach kernel uses struct mach_timespec, so struct timespec
    is loaded from <sys/_types/_timespec.h> for compatability */
// struct timespec { time_t tv_sec; long tv_nsec; };

int clock_gettime(clockid_t clk_id, struct timespec *tp);

#endif

并且在mach_gettime.c文件中出现

#include "mach_gettime.h"
#include <mach/mach_time.h>

#define MT_NANO (+1.0E-9)
#define MT_GIGA UINT64_C(1000000000)

// TODO create a list of timers,
static double mt_timebase = 0.0;
static uint64_t mt_timestart = 0;

// TODO be more careful in a multithreaded environement
int clock_gettime(clockid_t clk_id, struct timespec *tp)
{
    kern_return_t retval = KERN_SUCCESS;
    if( clk_id == TIMER_ABSTIME)
    {
        if (!mt_timestart) { // only one timer, initilized on the first call to the TIMER
            mach_timebase_info_data_t tb = { 0 };
            mach_timebase_info(&tb);
            mt_timebase = tb.numer;
            mt_timebase /= tb.denom;
            mt_timestart = mach_absolute_time();
        }

        double diff = (mach_absolute_time() - mt_timestart) * mt_timebase;
        tp->tv_sec = diff * MT_NANO;
        tp->tv_nsec = diff - (tp->tv_sec * MT_GIGA);
    }
    else // other clk_ids are mapped to the coresponding mach clock_service
    {
        clock_serv_t cclock;
        mach_timespec_t mts;

        host_get_clock_service(mach_host_self(), clk_id, &cclock);
        retval = clock_get_time(cclock, &mts);
        mach_port_deallocate(mach_task_self(), cclock);

        tp->tv_sec = mts.tv_sec;
        tp->tv_nsec = mts.tv_nsec;
    }

    return retval;
}

将此内容复制到Gist中以备将来参考:https://gist.github.com/alfwatt/3588c5aa1f7a1ef7a3bb - alfwatt
1
预先除以mt_timebase可能会对精度产生不利影响。在x86上不会,因为numer和denom都是1,但在ARM上会有影响。 - Aktau

4

只需使用Mach Time即可。
它是公共API,适用于macOS、iOS和tvOS,并且可以在沙盒内使用。

Mach Time返回一个抽象的时间单位,我通常称之为“时钟滴答声”。时钟滴答声的长度是系统特定的,取决于CPU。在当前的Intel系统上,时钟滴答声实际上正好是一纳秒,但您不能依赖于这一点(对于ARM可能不同,对于PowerPC CPU肯定不同)。系统还可以告诉您将时钟滴答声转换为纳秒和将纳秒转换为时钟滴答声所需的转换因子(此因子是静态的,在运行时不会更改)。当您的系统启动时,时钟从0开始,然后随着每个时钟滴答声单调递增,因此您也可以使用Mach Time获取系统的正常运行时间(当然,正常运行时间是单调递增的!)。

下面是一些代码:

#include <stdio.h>
#include <inttypes.h>
#include <mach/mach_time.h>

int main ( ) {
    uint64_t clockTicksSinceSystemBoot = mach_absolute_time();
    printf("Clock ticks since system boot: %"PRIu64"\n",
        clockTicksSinceSystemBoot
    );

    static mach_timebase_info_data_t timebase;
    mach_timebase_info(&timebase);
    // Cast to double is required to make this a floating point devision,
    // otherwise it would be an interger division and only the result would
    // be converted to floating point!
    double clockTicksToNanosecons = (double)timebase.numer / timebase.denom;

    uint64_t systemUptimeNanoseconds = (uint64_t)(
        clockTicksToNanosecons * clockTicksSinceSystemBoot
    );
    uint64_t systemUptimeSeconds = systemUptimeNanoseconds / (1000 * 1000 * 1000);
    printf("System uptime: %"PRIu64" seconds\n", systemUptimeSeconds);
}

你还可以将线程休眠直到达到特定的 Mach 时间。以下是相关代码:

// Sleep for 750 ns
uint64_t machTimeNow = mach_absolute_time();
uint64_t clockTicksToSleep = (uint64_t)(750 / clockTicksToNanosecons);
uint64_t machTimeIn750ns = machTimeNow + clockTicksToSleep;
mach_wait_until(machTimeIn750ns);

作为 Mach Time 与任何实际时间无关,因此您可以随意调整系统的日期和时间设置,这不会对 Mach Time 产生任何影响。
但是有一个特殊的考虑因素可能使 Mach Time 不适用于某些用例:当您的系统处于睡眠状态时,CPU 时钟不运行!因此,如果您让一个线程等待 5 分钟,而在 1 分钟后系统进入睡眠状态并保持 30 分钟,那么该线程仍将在系统唤醒后等待另外 4 分钟,因为这 30 分钟的睡眠时间不计算在内!CPU 时钟在此期间也在休息。然而,在其他情况下,这正是您想要发生的事情。
Mach Time 还是一种非常精确的测量时间消耗的方法。以下是一些展示该任务的代码:
// Measure time
uint64_t machTimeBegin = mach_absolute_time();
sleep(1);
uint64_t machTimeEnd = mach_absolute_time();
uint64_t machTimePassed = machTimeEnd - machTimeBegin;
uint64_t timePassedNS = (uint64_t)(
    machTimePassed * clockTicksToNanosecons
);
printf("Thread slept for: %"PRIu64" ns\n", timePassedNS);

你会发现线程并不会准确地休眠一秒钟,这是因为将线程置于休眠状态需要一些时间,唤醒线程再次运行也需要时间,即使线程被唤醒后,如果所有核心都已经在运行一个线程,那么它也不会立即获得CPU时间。
更新(2018-09-26)
自macOS 10.12(Sierra)以来,也存在mach_continuous_time。 mach_continuous_time和mach_absolute_time之间唯一的区别在于连续时间在系统休眠时也会增加。因此,如果这是迄今为止的问题和不使用Mach Time的原因,则10.12及以上版本提供了解决此问题的解决方案。使用方法与上述描述完全相同。

从 macOS 10.9(Mavericks)开始,有一个 mach_approximate_time,在 10.12 中还有一个 mach_continuous_approximate_time。这两个与 mach_absolute_timemach_continuous_time 相同,唯一的区别是它们更快但不太准确。标准函数需要调用内核,因为内核负责 Mach 时间。这样的调用有点昂贵,特别是在已经修复了 Meltdown 的系统上。近似版本不必总是调用内核。它们使用用户空间中的时钟,只有在必要时才与内核时钟同步,以防止其运行过于失调,然而小偏差总是可能的,因此它只是“近似”Mach时间。


mach_approximate_time() 需要 Mac 10.10 或以上的版本。 - Vortico
@Vortico 请查看 https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/mach_time.h.auto.html - __OSX_AVAILABLE_STARTING(__MAC_10_9, __IPHONE_8_0) - Mecki
有趣的是,这份文档说需要10.10版本,而我在10.9版本中无法加载链接到该函数的二进制文件。 - Vortico
@Vortico 我认为这只是自10.10以来的官方API,但如果您在代码中使用它并将其编译为10.9,则应该实际工作。您只是不能在10.9系统上这样做,因为它不在头文件中,但如果在更新的系统上构建,则应该可以部署回到10.9。 - Mecki
这与我的经验不同。在尝试运行在较新的Mac版本上编译的二进制文件时,找不到该符号。 - Vortico

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接