【免费下载链接】linux-insides-zh
Linux 内核揭秘
内核内部的时钟管理(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:02glibc 侧的符号解析与 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 快速路径的核心技巧:
- 首先通过
gtod_read_begin尝试访问gtod(global time of day,即vsyscall_gtod_data),并且用gtod_read_retry做顺序锁(seqlock)校验,直到读取成功——这是典型的无锁读者模式,保证在读者读取过程中写入者(时钟中断处理)修改数据时能够检测到并重试; - 用
gtod->wall_time_sec填充ts->tv_sec,它保存的是 Linux 内核时间管理子系统初始化时从**实时时钟(RTC)**读到的、以秒计的当前时间; gtod->wall_time_snsec保存的是纳秒部分,再加上vgetsns(&mode)根据当前时钟源(如 TSC)实时读取的纳秒增量,再右移gtod->shift做刻度换算;- 最后用
__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:56elapsed_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_COARSE | CLOCK_REALTIME的更快版本 |
CLOCK_MONOTONIC | 自某个未指定起始点以来的单调时间 |
CLOCK_MONOTONIC_COARSE | CLOCK_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):
- 先把当前任务的状态设置为
TASK_INTERRUPTIBLE(可中断睡眠态),这意味着任务可以被信号唤醒; - 调用
hrtimer_start_expires在当前处理器上启动给定的高分辨率定时器; - 定时器到期之前,任务通过
freezable_schedule让出 CPU 进入睡眠; - 循环条件是
t->task仍然存在且当前任务没有收到待处理信号(!signal_pending(current))——如果被信号打断则提前退出循环; - 循环结束后把任务状态恢复为
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 内核揭秘
相关推荐
Linux 内核揭秘:时钟相关系统调用,gettimeofday 与 clock_gettime
Linux 内核揭秘:时钟相关系统调用,gettimeofday 与 clock_gettime 你是否曾在编程时遇到时间获取不准的问题?日志时间戳混乱、定时任
文档教程操作系统深入解析Linux内核时间获取:getnstimeofday与clock_gettime系统调用
深入解析Linux内核时间获取:getnstimeofday与clock_gettime系统调用 在Linux系统中,时间管理是内核最核心的功能之一。了解如何正
文档教程操作系统Linux内核时间管理机制解析:系统调用实现篇
Linux内核时间管理机制解析:系统调用实现篇 前言 在Linux内核的时间管理机制中,系统调用扮演着用户空间程序与内核时间服务之间的桥梁角色。本文将深入剖析L
文档教程操作系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考