【问题标题】:How does the time() function in C work?C 中的 time() 函数是如何工作的?
【发布时间】:2018-08-13 10:04:44
【问题描述】:

有人知道time() 函数的工作原理吗?

纯粹出于好奇,我在网上寻找实现,但只能找到difftime() 的 NetBSD 实现

还有什么可以描述计算时间的过程(非系统特定或系统特定)?

注意:我不是在寻找关于如何使用 time() 的答案,而是在我调用它时它在幕后的实际工作方式。

【问题讨论】:

  • 然后浏览 time() 调用的确切实现。搜索标准库,即:glibc time() 调用 gettimeofday,它调用函数 __vdso_gettimeofday,这是一个 VDSO 调用,但通常它只是 syscall(__NR_gettimeofday, ..)。然后进入linux内核,可能到do_gettimeofday()

标签: c time


【解决方案1】:

在您计算机深处的某个地方,通常在硬件中,有一个时钟振荡器以某个频率 f 运行。出于本示例的目的,假设它以 1 kHz 或每秒 1,000 个周期运行。设置好振荡器的每个周期都会触发 CPU 中断。

还有一个低级计数器c。每次触发时钟中断时,操作系统都会递增计数器。目前我们假设它会增加 1,尽管在实践中通常不会出现这种情况。

操作系统还会在计数器增加时检查它的值。当c 等于 1,000 时,这意味着正好一秒钟过去了。此时操作系统做了两件事:

  1. 它增加另一个计数器变量,该变量以秒为单位跟踪一天中的实际时间。我们将调用这个其他计数器t。 (这将是一个很大的数字,所以它至少是一个 32 位的变量,或者现在,如果可能的话,是 64 位。)
  2. 它将c 重置为0。

最后,当您调用time() 时,内核会简单地返回t 的当前值。真的很简单!

嗯,实际上,它比这要复杂一些。我忽略了最初设置计数器 t 的值的细节,以及操作系统如何确保振荡器以正确的频率运行,以及其他一些事情。

当操作系统启动时,如果它位于 PC、工作站、大型机或其他“大型”计算机上,它通常有一个电池供电的实时时钟,可用于设置 t 的初始值。 (如果我们谈论的 CPU 是嵌入式微控制器,另一方面,它可能没有任何类型的时钟,所有这些都没有实际意义,time() 根本没有实现。)

此外,当您(以 root 身份)调用 settimeofday 时,您基本上只是提供了一个值来塞入内核的 t 计数器。

当然,在联网系统上,诸如 NTP 之类的东西正忙于使系统的时间保持最新。

NTP 可以通过两种方式做到这一点:

  1. 如果它注意到 t 已经偏离,它可以将其设置为一个新值,或多或少就像 settimeofday() 所做的那样。
  2. 如果它注意到 t 只是稍微偏离了一点,或者如果它注意到底层振荡器没有以完全正确的频率计数,它可以尝试调整该频率。

调整频率听起来很简单,但细节可能会变得相当复杂。您可以想象,底层振荡器的频率 f 略有调整。或者,您可以想象 f 保持不变,但是当时间中断触发时,添加到 c 的数字增量会略有调整。

特别是,内核通常不会在每次定时器中断时给c 加1,而当c 达到1,000 时,就表示一秒钟过去了。内核更有可能在每次定时器中断时将像 1,000,000 这样的数字添加到 c,这意味着它会等到 c 达到 1,000,000,000 后再决定一秒钟过去了。这样,内核可以对时钟频率进行更细粒度的调整:如果运行速度有点慢,它可以改变主意,在每个定时器中断时将 1,000,001 加到c,这样就可以运行了只是 tiny 快一点。 (大约是百万分之一,您可以很容易地看到。)

我忽略的另一件事是time() 不是询问系统时间的唯一方法。您还可以拨打 gettimeofday() 之类的电话,它会为您提供以秒 + 微秒 (struct timeval) 表示的亚秒级时间戳,或 clock_gettime(),它会为您提供以秒 + 纳秒 ( struct timespec)。这些是如何实施的?好吧,内核不仅可以读取t 的值,还可以查看c 以查看下一秒的距离。特别是,如果c 的计数达到 1,000,000,000,那么内核可以通过将c 除以 1,000 为您提供微秒,并且可以通过直接返回 c 为您提供纳秒。


两个脚注:

(1) 如果我们调整了频率,并且在每个低级计时器滴答声中将 1,000,001 加到 c 上,c 通常不会准确地达到 1,000,000,000,因此在决定是否增加时进行测试t 必须涉及大于或等于条件,我们必须从 c 中减去 1,000,000,000,而不仅仅是清除它。换句话说,代码看起来像

if(c >= 1000000000) {
    t++;
    c -= 1000000000;
}

(2) 由于time()gettimeofday() 是最简单的两个系统调用,并且由于调用它们的程序可能(根据定义)对系统调用开销引起的任何延迟特别敏感,因此这些调用最有可能基于vDSO 机制实现,如果它正在使用的话。

【讨论】:

  • 谢谢,这正是我想要的!在你理解了底层的概念之后,内核中的实现似乎一点也不复杂。您知道与此相关的主题的任何类型的资源吗?
  • @Nils 我不知道任何资源。我希望有一些。我通过检查 Linux 内核源代码解决了这个问题。我希望有一天能写出一些综合性的文档。
  • 查看有关这方面的文档会使事情变得容易得多。但是阅读内核源代码是我当时所处的位置。 (我查看了您的网站,除了查看您的主页之外,您似乎没有可以从您那里获取更新的地方,对吗?) - 再次感谢您的详尽回答!
  • 是否需要上下文切换来获取时间,或者操作系统通常有更简单的返回方式?
  • @TodorMarkov 确实有一种更简单的方法,可以避免上下文切换,至少在某些 Linux 内核下是这样:en.wikipedia.org/wiki/VDSO。我知道只有少数使用 vDSO 实现过的系统调用,但获取时间绝对是最重要的。
【解决方案2】:

C 规范没有说明库函数如何工作。它仅说明可观察到的行为。内部工作方式取决于编译器和平台。

概要

    #include <time.h>
    time_t time(time_t *timer);

说明

时间函数确定当前日历时间。该值的编码未指定。

退货

时间函数返回当前日历时间的最佳近似值。如果日历时间不可用,则返回值 (time_t)(-1)。如果 timer 不是空指针,则返回值也分配给它指向的对象。

https://port70.net/~nsz/c/c11/n1570.html

这是一种实现方式:

time_t
time (timer)
     time_t *timer;
{
  __set_errno (ENOSYS);

  if (timer != NULL)
    *timer = (time_t) -1;
  return (time_t) -1;
}

https://github.com/lattera/glibc/blob/master/time/time.c

【讨论】:

  • 示例实现代码有点无意义,因为它似乎描述了计时器不可用的系统。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-13
  • 1970-01-01
相关资源
最近更新 更多