欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > Linux时钟与时间API

Linux时钟与时间API

2025/5/4 5:49:09 来源:https://blog.csdn.net/m0_54069809/article/details/147687504  浏览:    关键词:Linux时钟与时间API

深入理解 Linux 时钟与时间 API

时间是计算领域的基础概念之一。在 Linux 系统中,精确可靠的时间管理对于系统日志记录、任务调度、网络通信、性能分析、文件系统操作乃至应用程序的正确运行都至关重要。本文将深入探讨 Linux 中的时钟类型、相关的 C API、使用示例、流程、常见应用场景以及注意事项。

1. Linux 中的时钟概念

Linux 系统中主要涉及两种类型的时钟:

  1. 硬件时钟 (Hardware Clock - RTC):

    • 也称为实时时钟 (Real-Time Clock) 或 CMOS 时钟。
    • 这是一个由电池供电的物理硬件芯片,通常位于主板上。
    • 作用: 即使在系统关机或断电的情况下,也能持续记录时间。它主要用于在系统启动时初始化系统时钟。
    • 交互: 用户通常通过 hwclock 命令来读取或设置硬件时钟。内核在启动时读取 RTC 来设置系统时间,在关机时(可选)将系统时间写回 RTC。
  2. 系统时钟 (System Clock / Software Clock):

    • 也称为内核时钟。这是操作系统内核维护的时钟。
    • 来源: 系统时钟通常由硬件定时器(如 PIT - Programmable Interval Timer, HPET - High Precision Event Timer, 或 CPU 的 TSC - Time Stamp Counter)产生的中断驱动。系统启动时,它会根据硬件时钟进行初始化。
    • 运行期间: 系统运行时,应用程序和内核主要依赖系统时钟。为了保持准确性,系统时钟通常会通过网络时间协议 (NTP - Network Time Protocol) 与外部时间服务器进行同步,这可能导致系统时钟发生跳变(向前或向后调整)。
    • 特性: 系统时钟是易变的,并且精度和分辨率取决于底层的硬件定时器和内核实现。

系统时钟内部又可以根据不同的参照系和特性细分为多种类型(clockid_t),其中最重要的是:

  • CLOCK_REALTIME: 系统范围的实时时钟(挂钟时间 Wall-clock time)。它表示自 Unix Epoch (1970-01-01 00:00:00 +0000 UTC) 以来的秒数和纳秒数。这个时钟是可调的,意味着它可以因为系统管理员手动修改、NTP 同步、闰秒调整等原因而发生不连续的跳变(向前或向后)。因此,它适合表示日历时间,但不适合测量精确的时间间隔。
  • CLOCK_MONOTONIC: 系统范围的单调时钟。它表示自系统启动(或其他未指定起点)以来的时间。这个时钟保证是单调递增的,不受 NTP 调整或管理员手动修改时间的影响(除非系统重启)。它非常适合用来测量时间间隔、设置超时等场景。它的起点是未定义的,所以其绝对值没有意义,只有差值有意义。
  • CLOCK_MONOTONIC_RAW: 类似于 CLOCK_MONOTONIC,但它试图提供更接近硬件的原始单调时间,不受 NTP 频率调整(skew)的影响。NTP 为了让 CLOCK_REALTIME 更平滑地接近目标时间,可能会微调 CLOCK_MONOTONIC 的前进速率,而 CLOCK_MONOTONIC_RAW 则不受此影响。
  • CLOCK_PROCESS_CPUTIME_ID: 测量当前进程所消耗的 CPU 时间(用户态 + 内核态)。
  • CLOCK_THREAD_CPUTIME_ID: 测量当前线程所消耗的 CPU 时间(用户态 + 内核态)。

2. 关键 C/C++ 时间 API

Linux 提供了丰富的 C API 来与这些时钟交互。需要包含 <time.h> 头文件。

2.1 time() - 获取低精度日历时间

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

功能: 获取 CLOCK_REALTIME 的当前时间,但精度只有秒级别。它是最古老、最简单的获取日历时间的方式。

参数:

  • time_t *tloc: 如果非 NULL,获取到的时间值也会存储在 tloc 指向的位置。

返回值:

  • 成功: 返回自 Unix Epoch 以来的秒数 (time_t 类型,通常是 long int)。
  • 失败: 返回 (time_t)-1

2.2 gettimeofday() - 获取较高精度日历时间 (已不推荐)

#include <sys/time.h> // 注意头文件int gettimeofday(struct timeval *tv, struct timezone *tz);struct timeval {time_t tv_sec;      /* seconds */suseconds_t tv_usec;   /* microseconds */
};

功能: 获取 CLOCK_REALTIME 的当前时间,精度可以达到微秒。在 POSIX.1-2008 标准中,此函数已被标记为过时 (obsolete),推荐使用 clock_gettime() 替代。

参数:

  • struct timeval *tv: 指向 timeval 结构体的指针,用于存储获取到的秒数和微秒数。
  • struct timezone *tz: 此参数已废弃,应始终传递 NULL。它以前用于获取时区信息,但现在不应再使用。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno

2.3 clock_gettime() - 获取指定时钟的高精度时间 (推荐)

#include <time.h>int clock_gettime(clockid_t clk_id, struct timespec *tp);struct timespec {time_t tv_sec;      /* seconds */long tv_nsec;   /* nanoseconds */
};

功能: 获取由 clk_id 指定的时钟的当前时间,精度可达纳秒。这是目前推荐使用的获取高精度时间的主要接口。

参数:

  • clockid_t clk_id: 指定要获取哪个时钟的时间。常用的值包括:
    • CLOCK_REALTIME: 挂钟时间。
    • CLOCK_MONOTONIC: 单调递增时间。
    • CLOCK_PROCESS_CPUTIME_ID: 进程 CPU 时间。
    • CLOCK_THREAD_CPUTIME_ID: 线程 CPU 时间。
    • CLOCK_MONOTONIC_RAW: 原始单调时间。
    • 还有其他特定用途的时钟 ID。
  • struct timespec *tp: 指向 timespec 结构体的指针,用于存储获取到的秒数和纳秒数 (0-999,999,999)。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno (例如 EINVAL 表示 clk_id 无效)。

2.4 clock_getres() - 获取时钟分辨率

#include <time.h>int clock_getres(clockid_t clk_id, struct timespec *res);

功能: 获取由 clk_id 指定的时钟的分辨率(即能够区分的最小时间间隔)。

参数:

  • clockid_t clk_id: 指定要查询哪个时钟。
  • struct timespec *res: 指向 timespec 结构体的指针,用于存储获取到的时钟分辨率。例如,如果分辨率是 1 纳秒,则 res->tv_sec 为 0,res->tv_nsec 为 1。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno

2.5 nanosleep() / clock_nanosleep() - 高精度睡眠

#include <time.h>int nanosleep(const struct timespec *req, struct timespec *rem);
int clock_nanosleep(clockid_t clockid, int flags, const struct timespec *request, struct timespec *remain);

nanosleep()

功能: 使当前线程暂停执行指定的相对时间段 (req)。它基于 CLOCK_REALTIME,可能会受系统时间调整影响,且易被信号中断。

nanosleep() 参数:

  • const struct timespec *req: 指向 timespec 结构体,指定需要睡眠的时长。
  • struct timespec *rem: 如果睡眠被信号中断,剩余未睡够的时间会存储在这里(如果非 NULL)。

nanosleep() 返回值:

  • 成功 (睡足时间): 返回 0。
  • 失败 (通常是被信号中断): 返回 -1,errno 设置为 EINTR

clock_nanosleep()

功能: 更强大的睡眠函数,允许指定基于哪个时钟 (clockid) 进行睡眠,并且可以指定是相对时间还是绝对时间 (flags)。推荐使用它替代 nanosleep,特别是需要基于单调时钟的精确延时。

clock_nanosleep() 参数:

  • clockid_t clockid: 指定用于计时的时钟,通常使用 CLOCK_MONOTONIC 来避免 CLOCK_REALTIME 的跳变问题。
  • int flags: 控制睡眠类型:
    • 0: 表示 request 是一个相对时间间隔。
    • TIMER_ABSTIME: 表示 request 是一个绝对时间点(基于 clockid 指定的时钟)。线程将睡眠直到指定时钟到达 request 的时间。
  • const struct timespec *request: 指向 timespec 结构体,指定睡眠时长(相对时间)或目标唤醒时间点(绝对时间)。
  • struct timespec *remain: 类似于 nanosleep,如果睡眠被信号中断(仅对相对睡眠有效),剩余时间会存储在这里(如果非 NULL)。

clock_nanosleep() 返回值:

  • 成功 (睡足时间或到达绝对时间点): 返回 0。
  • 失败: 返回错误码 (正数,与 errno 不同)。常见的错误码包括 EINTR (被信号中断), EINVAL (参数无效), ENOTSUP (不支持指定的时钟或标志)。

3. C 代码测试用例

测试用例 1: 获取不同时钟的当前时间

#include <stdio.h>
#include <time.h>
#include <sys/time.h>  // for gettimeofday
#include <unistd.h>  // for sleepint main() {// 1. Using time() (seconds precision)time_t current_time_t = time(NULL);if (current_time_t == (time_t)-1) {perror("time failed");} else {// ctime converts time_t to a string (includes newline)printf("time() : %s", ctime(&current_time_t));}// 2. Using gettimeofday() (microseconds precision - obsolete)struct timeval tv;if (gettimeofday(&tv, NULL) == -1) {perror("gettimeofday failed");} else {printf("gettimeofday() : %ld seconds, %ld microseconds\n", (long)tv.tv_sec, (long)tv.tv_usec);}// 3. Using clock_gettime() (nanoseconds precision - recommended)struct timespec ts_realtime, ts_monotonic;// Get CLOCK_REALTIMEif (clock_gettime(CLOCK_REALTIME, &ts_realtime) == -1) {perror("clock_gettime CLOCK_REALTIME failed");} else {printf("CLOCK_REALTIME : %ld seconds, %ld nanoseconds\n", (long)ts_realtime.tv_sec, ts_realtime.tv_nsec);}// Get CLOCK_MONOTONICif (clock_gettime(CLOCK_MONOTONIC, &ts_monotonic) == -1) {perror("clock_gettime CLOCK_MONOTONIC failed");} else {// Note: Monotonic time's absolute value isn't meaningful, only its differenceprintf("CLOCK_MONOTONIC : %ld seconds, %ld nanoseconds (since boot/epoch)\n", (long)ts_monotonic.tv_sec, ts_monotonic.tv_nsec);}// 4. Get Clock Resolutionstruct timespec res;if (clock_getres(CLOCK_MONOTONIC, &res) == -1) {perror("clock_getres failed");} else {printf("CLOCK_MONOTONIC resolution: %ld seconds, %ld nanoseconds\n", (long)res.tv_sec, res.tv_nsec);}return 0;
}

编译: gcc time_example1.c -o time_example1 -lrt (需要链接 librt 库,因为 clock_* 函数在其中)

测试用例 2: 使用 CLOCK_MONOTONIC 测量时间间隔

#include <stdio.h>
#include <time.h>
#include <unistd.h>  // for sleep// Helper function to calculate difference between two timespecs
void timespec_diff(struct timespec *start, struct timespec *stop, struct timespec *result) {if ((stop->tv_nsec - start->tv_nsec) < 0) {result->tv_sec = stop->tv_sec - start->tv_sec - 1;result->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000;} else {result->tv_sec = stop->tv_sec - start->tv_sec;result->tv_nsec = stop->tv_nsec - start->tv_nsec;}
}int main() {struct timespec start_time, end_time, elapsed_time;// Record start time using monotonic clockif (clock_gettime(CLOCK_MONOTONIC, &start_time) == -1) {perror("clock_gettime start failed");return 1;}printf("Starting operation...\n");// Simulate some work (e.g., sleep for 1.5 seconds)sleep(1);  // Standard sleep uses secondsusleep(500000);  // usleep uses microseconds// Record end time using monotonic clockif (clock_gettime(CLOCK_MONOTONIC, &end_time) == -1) {perror("clock_gettime end failed");return 1;}printf("Operation finished.\n");// Calculate elapsed timetimespec_diff(&start_time, &end_time, &elapsed_time);printf("Elapsed time: %ld seconds, %ld nanoseconds\n", (long)elapsed_time.tv_sec, elapsed_time.tv_nsec);// Convert to milliseconds for easier readingdouble elapsed_ms = (double)elapsed_time.tv_sec * 1000.0 + (double)elapsed_time.tv_nsec / 1000000.0;printf("Elapsed time: %.3f milliseconds\n", elapsed_ms);return 0;
}

编译: gcc time_example2.c -o time_example2 -lrt

测试用例 3: 使用 clock_nanosleep 进行精确延时

#include <stdio.h>
#include <time.h>
#include <errno.h>  // For errnoint main() {struct timespec start_time, end_time, sleep_duration, elapsed_time;int ret;// Define sleep duration: 0 seconds, 500 million nanoseconds (0.5 seconds)sleep_duration.tv_sec = 0;sleep_duration.tv_nsec = 500000000;  // 0.5 secondsprintf("Attempting to sleep for %ld ns using clock_nanosleep with CLOCK_MONOTONIC...\n", sleep_duration.tv_nsec);// Record start timeif (clock_gettime(CLOCK_MONOTONIC, &start_time) == -1) {perror("clock_gettime start failed");return 1;}// Use clock_nanosleep for relative sleep based on CLOCK_MONOTONICdo {ret = clock_nanosleep(CLOCK_MONOTONIC, 0, &sleep_duration, &sleep_duration);// Keep sleeping for the remaining duration if interrupted by a signal} while (ret == EINTR);if (ret != 0) {fprintf(stderr, "clock_nanosleep failed with error: %d\n", ret);return 1;}// Record end timeif (clock_gettime(CLOCK_MONOTONIC, &end_time) == -1) {perror("clock_gettime end failed");return 1;}printf("Sleep finished.\n");// Calculate actual elapsed timetimespec_diff(&start_time, &end_time, &elapsed_time);printf("Actual elapsed time: %ld seconds, %ld nanoseconds\n", (long)elapsed_time.tv_sec, elapsed_time.tv_nsec);return 0;
}// Include the timespec_diff function from Example 2 here
void timespec_diff(struct timespec *start, struct timespec *stop, struct timespec *result) {if ((stop->tv_nsec - start->tv_nsec) < 0) {result->tv_sec = stop->tv_sec - start->tv_sec - 1;result->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000;} else {result->tv_sec = stop->tv_sec - start->tv_sec;result->tv_nsec = stop->tv_nsec - start->tv_nsec;}
}

编译: gcc time_example3.c -o time_example3 -lrt

4. 流程图:测量代码块执行时间

下面是一个使用 CLOCK_MONOTONIC 测量代码块执行时间的简化流程图(使用 Mermaid 语法):

graph TDA[Start] --> B{Call clock_gettime(CLOCK_MONOTONIC, &start_time)};B --> C[Execute Code Block to Measure];C --> D{Call clock_gettime(CLOCK_MONOTONIC, &end_time)};D --> E[Calculate Difference: elapsed_time = end_time - start_time];E --> F[Use/Display elapsed_time];F --> G[End];B -- Error Handling --> H{Handle Error};D -- Error Handling --> H;

流程图解释:

  1. 程序开始。
  2. 调用 clock_gettime 获取基于单调时钟的起始时间点,并检查错误。
  3. 执行需要测量时间的代码块。
  4. 再次调用 clock_gettime 获取结束时间点,并检查错误。
  5. 计算结束时间与起始时间的差值,得到精确的时间间隔。
  6. 使用或显示计算出的时间间隔。
  7. 程序结束。

5. 开源项目中的使用方式

Linux 时间 API 在几乎所有类型的开源项目中都有广泛应用:

  • 日志系统 (e.g., systemd-journald, rsyslog): 使用 CLOCK_REALTIME (通常通过 gettimeofdayclock_gettime) 为每条日志消息打上精确的时间戳,用于事件排序和问题诊断。有时也会记录 CLOCK_MONOTONIC 时间戳用于启动相关的事件排序。
  • Web 服务器 (e.g., Nginx, Apache): 记录请求到达和响应发出的时间戳 (通常是 CLOCK_REALTIME) 用于访问日志;使用 CLOCK_MONOTONIC 实现连接超时、请求超时、keep-alive 超时等。
  • 数据库 (e.g., PostgreSQL, MySQL): 使用 CLOCK_REALTIME 记录事务提交时间、行版本时间戳;使用 CLOCK_MONOTONIC 实现内部操作的超时(如锁等待超时、查询执行超时)。
  • 调度器与作业队列 (e.g., cron, systemd timers, Celery): 使用 CLOCK_REALTIME 来决定何时触发预定的任务。内部可能使用 CLOCK_MONOTONIC 实现等待间隔。
  • 性能监控工具 (e.g., perf, eBPF tools, Prometheus node_exporter): 大量使用 CLOCK_MONOTONICCLOCK_MONOTONIC_RAW 来精确测量函数执行时间、系统调用耗时、网络延迟等性能指标。CLOCK_(THREAD/PROCESS)_CPUTIME_ID 用于测量 CPU 使用率。
  • 网络协议栈: 使用 CLOCK_MONOTONIC 来管理 TCP 重传超时 (RTO)、keep-alive 定时器等。
  • 实时系统 (RTOS features in Linux with PREEMPT_RT patch): 对时钟精度和定时器延迟有极高要求,大量依赖高精度定时器和 clock_nanosleep(TIMER_ABSTIME) 实现精确的周期性任务和截止时间调度。

6. 常用场景总结

  • 日志记录: 需要知道事件发生的确切日历时间 -> clock_gettime(CLOCK_REALTIME)
  • 测量时间间隔/代码耗时: 需要精确测量一个操作持续了多长时间,不受时钟调整影响 -> clock_gettime(CLOCK_MONOTONIC)
  • 设置超时: 需要等待一段时间或等到某个未来时间点,且不受时钟调整影响 -> clock_nanosleep(CLOCK_MONOTONIC, ...)
  • 任务调度: 需要在特定的日历时间执行任务 -> 基于 CLOCK_REALTIME 检查。
  • 性能分析: 需要测量 CPU 消耗 -> clock_gettime(CLOCK_PROCESS_CPUTIME_ID / CLOCK_THREAD_CPUTIME_ID);需要测量代码段执行时间 -> clock_gettime(CLOCK_MONOTONIC)
  • 网络通信: 实现协议超时 -> CLOCK_MONOTONIC
  • 需要与外部世界同步的时间: 显示给用户、与其他系统交换基于日历时间的数据 -> CLOCK_REALTIME

7. 注意事项

  • CLOCK_REALTIME 的不连续性: 切记 CLOCK_REALTIME 可能随时向前或向后跳变。绝不能用它来测量时间间隔。
  • CLOCK_MONOTONIC 的起点: 它的起点未定义(通常是系统启动时间),不同系统或同系统重启后起点会变。只能用于测量时间差。
  • 时钟分辨率 vs. 精度: clock_getres 返回的是理论上的最小可分辨单位,但实际精度可能受硬件、内核调度延迟等因素影响。
  • API 调用开销: clock_gettime 通常比旧的 gettimeofdaytime 更快(因为它可能通过 VDSO 在用户空间执行,避免陷入内核),但频繁调用仍有开销。对于极高性能敏感的代码,有时会直接读取 TSC(但需处理 CPU 频率变化和多核同步问题,通常更复杂)。
  • 时间类型溢出: 对于非常长的运行时间,time_t (秒) 最终也会溢出(例如 32 位系统的 Y2038 问题)。struct timespectv_sec 同样受 time_t 限制。
  • 链接库: 使用 clock_* 系列函数通常需要链接 librt (-lrt)。较新的 glibc 版本可能已将其集成到主 libc 中,不再需要显式链接。

8. 总结

Linux 提供了强大而灵活的时间管理机制。理解硬件时钟和系统时钟(特别是 CLOCK_REALTIMECLOCK_MONOTONIC)的区别是正确使用时间 API 的基础。clock_gettime()clock_nanosleep() 是现代 Linux 编程中处理高精度时间和延时的首选工具。根据具体需求(需要日历时间还是时间间隔?是否需要抵抗时钟调整?)选择合适的时钟 ID 和 API,并注意错误处理和潜在的时钟跳变问题,是编写健壮、可靠的 Linux 应用程序的关键一环。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com