☰
Linux 内核时间相关的系统调用:gettimeofday、clock_gettime 与 nanosleep 的 x86_64 实现解析
2026/9/28 2:35:20 网站建设 项目流程

【免费下载链接】linux-insides-zh

Linux 内核揭秘

项目地址:https://gitcode.com/hust-open-atom-club/linux-insides-zh
点击查看免费下载

内核内部的时钟管理(jiffies、clocksource、clockevents 等)只解决了内核自身"感知时间"的问题,而用户态程序同样需要向内核索取时间——这就引出了时间相关的系统调用。本篇是 linux-insides-zh 仓库中"定时器和时钟管理"章节(Timers/README.md)的收官之篇,围绕gettimeofday、clock_gettime和nanosleep三个系统调用,完整走一遍"标准库函数 → vDSO / syscall 指令 → 内核实现"的调用链,帮助读者掌握 x86_64 架构下时间相关系统调用的实际落地路径与底层原理。

背景:用户态进程如何感知内核时间

Linux 内核内部有一整套时间管理基础设施:jiffies全局计数器在每次时钟中断时递增,配合编译期常量HZ就能换算出自启动以来经过的秒数(即系统 uptime),详见 Timers/linux-timers-2.md;clocksource框架则为各种硬件时钟源(如 x86_64 上的tsc、hpet、acpi_pm,见 Timers/linux-timers-6.md)提供了统一的抽象接口。

但时间的概念并不仅属于内核,用户态程序同样需要知道"现在是几点""进程运行了多久""再睡五秒"。因此内核必须向用户态暴露时间相关的系统调用接口。本篇要分析的三个系统调用是:

  • gettimeofday—— 获取当前墙上时钟时间;
  • clock_gettime—— 按指定时钟 ID 获取时间;
  • nanosleep—— 让调用线程睡眠指定的时间。

每个架构都有自己的系统调用实现,本篇严格限定在 x86_64 架构(这也是本仓库所依托的架构语境)。另外,本篇不展开介绍"什么是系统调用"这一基础概念,相关背景可参考本仓库的系统调用章节。

在深入实现之前,先明确一条最重要的调用路径:用户态应用并不会直接进入内核。在执行真正的系统调用入口之前,首先调用的是标准库(glibc)的函数;而gettimeofday与clock_gettime这两个系统调用比较特殊——它们被放进了vDSO(virtual dynamic shared object)区域,可以在用户态直接完成大部分工作,从而省去系统调用所需的上下文切换开销。关于vsyscall与vDSO的完整原理,可参考 SysCall/linux-syscall-3.md。

实现gettimeofday系统调用

从名字就能看出,gettimeofday返回当前时间。先看一个最简单的用户态 C 程序:

#include <time.h> #include <sys/time.h> #include <stdio.h> int main(int argc, char **argv) { char buffer[40]; struct timeval time; gettimeofday(&time, NULL); strftime(buffer, 40, "Current date/time: %m-%d-%Y/%T", localtime(&time.tv_sec)); printf("%s\n",buffer); return 0; }

这里调用了gettimeofday函数,它接受两个参数。第一个参数是指向timeval结构的指针,该结构表示流逝的时间:

struct timeval { time_t tv_sec; /* 秒 */ suseconds_t tv_usec; /* 微秒 */ };

第二个参数是指向timezone结构的指针,表示时区信息。在上面的例子中,我们把struct timeval time的地址传给gettimeofday,内核会填充这个timeval结构并返回给我们。随后用strftime把时间格式化成人可读的字符串。编译运行:

~$ gcc date.c -o date ~$ ./date Current date/time: 03-26-2016/16:42:02

glibc 侧的符号解析与 vDSO 回退

用户态应用不会直接从内核空间调用系统调用。在真正触发系统调用入口之前,先调用的是标准库函数。以 glibc 为例,gettimeofday的实现位于 glibc 的sysdeps/unix/sysv/linux/x86/gettimeofday.c。如前面所说,gettimeofday不是普通的系统调用,它位于名为vDSO的特殊区域。

glibc 的实现会尝试通过内部函数_dl_vdso_vsym解析给定符号——在这里就是__vdso_gettimeofday。如果符号无法解析(返回NULL),就回退到普通的系统调用:

return (_dl_vdso_vsym ("__vdso_gettimeofday", &linux26) ?: (void*) (&__gettimeofday_syscall));

vDSO 入口:__vdso_gettimeofday

gettimeofday的 vDSO 入口位于内核源码arch/x86/entry/vdso/vclock_gettime.c。可以看到,gettimeofday是__vdso_gettimeofday的弱别名:

int gettimeofday(struct timeval *, struct timezone *) __attribute__((weak, alias("__vdso_gettimeofday")));

__vdso_gettimeofday定义在同一个源文件中:如果给定的timeval非空,就调用do_realtime函数:

notrace int __vdso_gettimeofday(struct timeval *tv, struct timezone *tz) { if (likely(tv != NULL)) { if (unlikely(do_realtime((struct timespec *)tv) == VCLOCK_NONE)) return vdso_fallback_gtod(tv, tz); tv->tv_usec /= 1000; } if (unlikely(tz != NULL)) { tz->tz_minuteswest = gtod->tz_minuteswest; tz->tz_dsttime = gtod->tz_dsttime; } return 0; }

如果do_realtime失败(返回VCLOCK_NONE,表示当前时钟源不支持 vDSO 快速路径),就通过syscall指令回退到真正的系统调用,并传入__NR_gettimeofday系统调用号以及给定的timeval和timezone:

notrace static long vdso_fallback_gtod(struct timeval *tv, struct timezone *tz) { long ret; asm("syscall" : "=a" (ret) : "0" (__NR_gettimeofday), "D" (tv), "S" (tz) : "memory"); return ret; }

do_realtime:从vsyscall_gtod_data读取时间数据

do_realtime函数从vsyscall_gtod_data结构(定义于arch/x86/include/asm/vgtod.h)获取时间数据。该结构包含timespec结构的映射,以及几个与当前系统时钟源相关的字段。这个结构中的时间数据由时钟中断周期性地更新。do_realtime用给定的timeval结构填充这些值:

do { seq = gtod_read_begin(gtod); mode = gtod->vclock_mode; ts->tv_sec = gtod->wall_time_sec; ns = gtod->wall_time_snsec; ns += vgetsns(&mode); ns >>= gtod->shift; } while (unlikely(gtod_read_retry(gtod, seq))); ts->tv_sec += __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns); ts->tv_nsec = ns;

这段代码揭示了 vDSO 快速路径的核心技巧:

  1. 首先通过gtod_read_begin尝试访问gtod(global time of day,即vsyscall_gtod_data),并且用gtod_read_retry做顺序锁(seqlock)校验,直到读取成功——这是典型的无锁读者模式,保证在读者读取过程中写入者(时钟中断处理)修改数据时能够检测到并重试;
  2. 用gtod->wall_time_sec填充ts->tv_sec,它保存的是 Linux 内核时间管理子系统初始化时从**实时时钟(RTC)**读到的、以秒计的当前时间;
  3. gtod->wall_time_snsec保存的是纳秒部分,再加上vgetsns(&mode)根据当前时钟源(如 TSC)实时读取的纳秒增量,再右移gtod->shift做刻度换算;
  4. 最后用__iter_div_u64_rem把累计纳秒拆分成"秒 + 剩余纳秒",填进timespec。

整个过程中没有任何特权指令,完全在用户态完成,因此 vDSO 版本的gettimeofday相比传统系统调用省去了上下文切换的开销。这正是 SysCall/linux-syscall-3.md 中介绍的 vDSO 机制的价值所在:linux-vdso.so.1作为共享库被映射进每个进程,与静态固定地址、仅实现 3 个系统调用的旧式vsyscall相比,vDSO动态加载、地址随机化,并实现了__vdso_clock_gettime、__vdso_getcpu、__vdso_gettimeofday、__vdso_time四个入口。

实现clock_gettime系统调用

clock_gettime获取由第二个参数指定的时钟所对应的时间。一般地,clock_gettime接受两个参数:

  • clk_id—— 时钟标识符;
  • timespec—— 指向timespec结构的指针,表示流逝的时间。

看一个简单的例子——用CLOCK_BOOTTIME获取自开机以来的秒数:

#include <time.h> #include <sys/time.h> #include <stdio.h> int main(int argc, char **argv) { struct timespec elapsed_from_boot; clock_gettime(CLOCK_BOOTTIME, &elapsed_from_boot); printf("%d - seconds elapsed from boot\n", elapsed_from_boot.tv_sec); return 0; }

编译运行,它会打印 uptime 信息:

~$ gcc uptime.c -o uptime ~$ ./uptime 14180 - seconds elapsed from boot

可以用系统自带的uptime工具验证结果:

~$ uptime up 3:56

elapsed_from_boot.tv_sec表示以秒计的流逝时间,所以:

>>> 14180 / 60 236 >>> 14180 / 60 / 60 3 >>> 14180 / 60 % 60 56

正好对应up 3:56(3 小时 56 分钟)。

clk_id的取值

clk_id可以是下面这些取值之一:

时钟 ID含义
CLOCK_REALTIME系统级时钟,度量真实时间 / 墙上时钟时间
CLOCK_REALTIME_COARSECLOCK_REALTIME的更快版本
CLOCK_MONOTONIC自某个未指定起始点以来的单调时间
CLOCK_MONOTONIC_COARSECLOCK_MONOTONIC的更快版本
CLOCK_MONOTONIC_RAW与CLOCK_MONOTONIC相同,但提供未经 NTP 调整的时间
CLOCK_BOOTTIME与CLOCK_MONOTONIC相同,但加上系统挂起(suspend)的时间
CLOCK_PROCESS_CPUTIME_ID进程内所有线程消耗的 per-process 时间
CLOCK_THREAD_CPUTIME_ID线程专属时钟

vDSO 入口:按时钟 ID 分发

与gettimeofday一样,clock_gettime也不是普通系统调用,同样位于vDSO区域,其入口和gettimeofday在同一个源文件arch/x86/entry/vdso/vclock_gettime.c中。

clock_gettime的具体实现取决于传入的时钟 ID。如果传入CLOCK_REALTIME,就调用do_realtime:

notrace int __vdso_clock_gettime(clockid_t clock, struct timespec *ts) { switch (clock) { case CLOCK_REALTIME: if (do_realtime(ts) == VCLOCK_NONE) goto fallback; break; ... ... ... fallback: return vdso_fallback_gettime(clock, ts); }

其他情况下,则调用对应的do_{name_of_clock_id}函数,其中不少实现的思路是相似的。例如传入CLOCK_MONOTONIC时:

... ... ... case CLOCK_MONOTONIC: if (do_monotonic(ts) == VCLOCK_NONE) goto fallback; break; ... ... ...

do_monotonic:与do_realtime同构的单调时钟读取

do_monotonic函数与do_realtime的实现非常相似:

notrace static int __always_inline do_monotonic(struct timespec *ts) { do { seq = gtod_read_begin(gtod); mode = gtod->vclock_mode; ts->tv_sec = gtod->monotonic_time_sec; ns = gtod->monotonic_time_snsec; ns += vgetsns(&mode); ns >>= gtod->shift; } while (unlikely(gtod_read_retry(gtod, seq))); ts->tv_sec += __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns); ts->tv_nsec = ns; return mode; }

在上一节关于gettimeofday的内容中我们已经见过这段逻辑。这里唯一的区别是:timespec的sec和nsec基于gtod->monotonic_time_sec而不是gtod->wall_time_sec。monotonic_time_sec映射的是tk->tkr_mono.xtime_nsec,即自启动以来流逝的纳秒数——这正是"单调时间"(不受墙上时钟被手工调整或 NTP 校时影响)的来源。

实现nanosleep系统调用

本篇列表中的最后一个系统调用是nanosleep。从名字可知,这个函数提供"睡眠"能力。看下面的简单示例:

#include <time.h> #include <stdlib.h> #include <stdio.h> int main (void) { struct timespec ts = {5,0}; printf("sleep five seconds\n"); nanosleep(&ts, NULL); printf("end of sleep\n"); return 0; }

编译并运行,会先看到第一行输出,五秒之后才看到第二行:

~$ gcc sleep_test.c -o sleep ~$ ./sleep sleep five seconds end of sleep

与 vDSO 不同:nanosleep走真实系统调用

与gettimeofday、clock_gettime不同,nanosleep不在 vDSO 区域。所以需要看标准库如何调用位于内核空间的真实系统调用。nanosleep系统调用借助syscall指令触发。在执行syscall指令之前,必须按照 [System V Application Binary Interface] 规定的顺序把系统调用参数放入处理器寄存器,即:

  • rdi—— 第一个参数;
  • rsi—— 第二个参数;
  • rdx—— 第三个参数;
  • r10—— 第四个参数;
  • r8—— 第五个参数;
  • r9—— 第六个参数。

nanosleep系统调用有两个参数——两个指向timespec结构的指针。该调用会挂起调用线程,直到给定的超时时间过去;如果执行被信号打断,它也会提前结束。第一个参数是表示睡眠超时时间的timespec;第二个参数同样是timespec指针,当nanosleep被中断时,其中保存剩余的时间。

nanosleep有两个参数:

int nanosleep(const struct timespec *req, struct timespec *rem);

调用系统调用时,需要把req放入rdi寄存器,把rem放入rsi寄存器。glibc 通过INTERNAL_SYSCALL宏完成这一工作,该宏位于sysdeps/unix/sysv/linux/x86_64/sysdep.h:

# define INTERNAL_SYSCALL(name, err, nr, args...) \ INTERNAL_SYSCALL_NCS (__NR_##name, err, nr, ##args)

该宏接收系统调用名、存放执行期间可能错误的空间、系统调用号(x86_64 全部系统调用可在内核源码arch/x86/entry/syscalls/syscall_64.tbl表中找到)以及该系统调用的参数。INTERNAL_SYSCALL只是展开为对INTERNAL_SYSCALL_NCS宏的调用,后者负责准备系统调用参数(按正确顺序放入处理器寄存器)、执行syscall指令并返回结果:

# define INTERNAL_SYSCALL_NCS(name, err, nr, args...) \ ({ \ unsigned long int resultvar; \ LOAD_ARGS_##nr (args) \ LOAD_REGS_##nr \ asm volatile ( \ "syscall\n\t" \ : "=a" (resultvar) \ : "0" (name) ASM_ARGS_##nr : "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \ (long int) resultvar; })

LOAD_ARGS_##nr宏会调用LOAD_ARGS_N宏,其中N是系统调用的参数个数。nanosleep有两个参数,因此展开为LOAD_ARGS_2。这些宏最终会被展开成类似下面的形式——用register ... asm("rdi")/asm("rsi")语法把参数绑定到对应寄存器:

# define LOAD_REGS_TYPES_1(t1, a1) \ register t1 _a1 asm ("rdi") = __arg1; \ LOAD_REGS_0 # define LOAD_REGS_TYPES_2(t1, a1, t2, a2) \ register t2 _a2 asm ("rsi") = __arg2; \ LOAD_REGS_TYPES_1(t1, a1) ... ... ...

syscall指令执行后会发生上下文切换,内核把执行权移交给系统调用处理程序。

内核侧实现:SYSCALL_DEFINE2(nanosleep, ...)

nanosleep系统调用的处理程序位于内核源码kernel/time/hrtimer.c,使用SYSCALL_DEFINE2宏辅助定义(SYSCALL_DEFINE2宏的细节可参考本仓库的系统调用章节):

SYSCALL_DEFINE2(nanosleep, struct timespec __user *, rqtp, struct timespec __user *, rmtp) { struct timespec tu; if (copy_from_user(&tu, rqtp, sizeof(tu))) return -EFAULT; if (!timespec_valid(&tu)) return -EINVAL; return hrtimer_nanosleep(&tu, rmtp, HRTIMER_MODE_REL, CLOCK_MONOTONIC); }

可以看到,nanosleep的实现从调用copy_from_user开始——该函数把用户态传入的数据复制到内核空间。在这里我们把睡眠超时值复制到内核空间的timespec结构tu中,然后用timespec_valid校验给定的timespec是否合法:

static inline bool timespec_valid(const struct timespec *ts) { if (ts->tv_sec < 0) return false; if ((unsigned long)ts->tv_nsec >= NSEC_PER_SEC) return false; return true; }

该函数只检查两件事:给定的timespec不能表示 1970 年之前的时间(tv_sec < 0),且纳秒部分不能超过 1 秒(tv_nsec >= NSEC_PER_SEC)。任一条件不满足即返回false,系统调用返回-EINVAL。

hrtimer_nanosleep与do_nanosleep:用高精度定时器实现睡眠

校验通过后,nanosleep最终调用同一个源文件里的hrtimer_nanosleep函数。hrtimer_nanosleep会创建一个高分辨率定时器(hrtimer)(定时器机制的详细讲解见 Timers/linux-timers-4.md),然后调用do_nanosleep完成主要工作。do_nanosleep提供了一个循环:

do { set_current_state(TASK_INTERRUPTIBLE); hrtimer_start_expires(&t->timer, mode); if (likely(t->task)) freezable_schedule(); } while (t->task && !signal_pending(current)); __set_current_state(TASK_RUNNING); return t->task == NULL;

这段代码在睡眠期间冻结当前任务(task):

  1. 先把当前任务的状态设置为TASK_INTERRUPTIBLE(可中断睡眠态),这意味着任务可以被信号唤醒;
  2. 调用hrtimer_start_expires在当前处理器上启动给定的高分辨率定时器;
  3. 定时器到期之前,任务通过freezable_schedule让出 CPU 进入睡眠;
  4. 循环条件是t->task仍然存在且当前任务没有收到待处理信号(!signal_pending(current))——如果被信号打断则提前退出循环;
  5. 循环结束后把任务状态恢复为TASK_RUNNING。

当高分辨率定时器到期时,任务会再次被调度运行。这正是nanosleep"睡指定秒数、可被信号提前打断"语义的内核实现来源。

小结:本章时间管理的全景

这是"定时器和时钟管理"章节的最后一篇。前面的部分依次介绍了jiffies与clocksource框架(linux-timers-1.md、linux-timers-2.md)、tick broadcast 与 dyntick/NO_HZ 模式(linux-timers-3.md)、软件定时器机制(linux-timers-4.md)、clockevents 框架(linux-timers-5.md)以及 x86_64 相关的时钟源(linux-timers-6.md)。

本篇则为这一章画上句号:gettimeofday和clock_gettime借助 vDSO 在用户态直接读取vsyscall_gtod_data中的时间数据(配合 seqlock 无锁读取与时钟源实时纳秒增量),绝大多数调用无需进入内核;nanosleep则通过标准库的宏展开、syscall指令进入内核,由kernel/time/hrtimer.c中的高精度定时器与任务调度状态机协作完成睡眠。三者的实现共同印证了一个事实:用户态每一次看似简单的时间操作,背后都是内核时间管理子系统(clocksource、timekeeping、hrtimer)与系统调用 / vDSO 机制的精密配合。当然,时间管理还远不止这些,它与调度(scheduling)紧密相关的部分,将在其他章节中继续展开。

【免费下载链接】linux-insides-zh

Linux 内核揭秘

项目地址:https://gitcode.com/hust-open-atom-club/linux-insides-zh
点击查看免费下载

相关推荐

上一篇:SpeechBrain中的混合CTC/注意力:端到端ASR系统构建
下一篇:electerm Web版部署教程:搭建浏览器终端服务

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询