你的 gettimeofday 其实没进内核
你写
内核在启动时编译了一段特殊的共享库,叫 vDSO(virtual Dynamic Shared Object),然后把它映射进每个进程的地址空间。你的程序调用
这个只读数据页是内核写好的,里面存着时钟源值、时区偏移、vvar 版本号等。内核在每个 tick 或时钟中断时更新它,vDSO 的代码只管读。读写分离,天生无锁,甚至连原子操作都不需要——因为内核写的是对齐的 64 位值,读侧要么拿到旧的要么拿到新的,不会撕列。这就是一种很经典的设计思路:。
你可以
一个有意思的问题是: 答案藏在数据依赖里——vDSO 能安全读取的数据,必须是内核无条件暴露到用户空间的只读快照。时间值满足这个条件,因为内核写、用户读、不冲突。但如果你要读文件、发网络包、创建进程,数据根本没办法提前准备好放在共享页上,只能老老实实进内核,该 trap 就 trap。
vDSO 的源码在内核树的
#CS
你写
gettimeofday() 的时候,脑子里大概浮现的是一次系统调用:用户态 → 内核态切换,保存寄存器,走中断向量,查内核时间变量,再原路返回。一次调用,两次上下文切换。但在现代 Linux 上,这件事压根没发生。内核在启动时编译了一段特殊的共享库,叫 vDSO(virtual Dynamic Shared Object),然后把它映射进每个进程的地址空间。你的程序调用
gettimeofday() 时,动态链接器把它解析到了 vDSO 里的函数,而不是 libc 的常规 syscall 封装。于是整件事在用户态就完成了:vDSO 的代码直接读内核映射的只读数据页上的时间值,算出结果返回给你,全程没有 syscall 指令。这个只读数据页是内核写好的,里面存着时钟源值、时区偏移、vvar 版本号等。内核在每个 tick 或时钟中断时更新它,vDSO 的代码只管读。读写分离,天生无锁,甚至连原子操作都不需要——因为内核写的是对齐的 64 位值,读侧要么拿到旧的要么拿到新的,不会撕列。这就是一种很经典的设计思路:。
你可以
objdump -d /lib64/libc.so.6 | grep -A 20 clock_gettime 看到它先尝试走 vDSO,失败才走 syscall 的 fallback 路径。也可以 cat /proc/self/maps | grep vdso 看到这段内存就躺在你进程里,地址在栈和 libc 之间。甚至 _start 入口点之前就被映射好了,你没有任何办法拒绝它。一个有意思的问题是: 答案藏在数据依赖里——vDSO 能安全读取的数据,必须是内核无条件暴露到用户空间的只读快照。时间值满足这个条件,因为内核写、用户读、不冲突。但如果你要读文件、发网络包、创建进程,数据根本没办法提前准备好放在共享页上,只能老老实实进内核,该 trap 就 trap。
vDSO 的源码在内核树的
arch/x86/entry/vdso/ 和 lib/vdso/ 下,加起来几百行 C,却把时间类系统调用的开销从上千个时钟周期降到了几十个。下次你 strace -c 一个高频调 gettimeofday 的程序却看不到多少 syscall 次数的时候,别疑惑——它根本没进内核。#CS