gettimeofday飞快的根因:vdso
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
内核版本 5.4.119
引言
众所周知工作时间等于工作效率乘以工作总量,我一直认为效率的权重远大于时间,这也是我从大一开始就逃课睡觉打豆豆的原因。大抵总会有一个更加显然的例子,比如一个下山的功夫,一个叫做adl的坏家伙便把我从一个现实主义者变成了虚无主义者;再比如半个小时的功夫,我就强迫y7n05h,adl以及我搞定了这个看似有点难以解释的小问题。
上文得出两个结论:
- 效率的权重大于时间,远大于!
- 人得逼一下
下文着重解释两个问题:
- gettimeofday这么快的原因
- gettimeofday可能出现效率下降的原因
调用栈
其实很多人没有搞清楚到底调用gettimeofday的时候实际执行的路径是什么,遂出现了大量无用博客开始瞎分析内核和glibc中一些带gettimeofday的函数,我们先来gdb一下,看看gettimeofday在动态链接以后实际调用的函数到底是什么。
基础代码是这样的:
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
printf(" tv_usec = %ld tv_sec = %ld\n", tv.tv_usec, tv.tv_sec);
for(int i = 0; i < 4; i++){
gettimeofday(&tv, NULL);
printf("%d) tv_usec = %ld tv_sec = %ld\n", i, tv.tv_usec, tv.tv_sec);
sleep(1);
}
return 0;
}
这里必须先引出vdso,不清楚的同学可以先查一下[1]。我想说的是目前vdso支持的函数并不多:
那么到底是不是只有这些呢,我们来看看:
找到程序里面vdso段的地址以后我们执行dump memory vdso.so 0x7ffff7ffb000 0x7ffff7ffc000
,然后另起一个终端,执行如下:
发现确实就只有man7中描述的四个符号。
然后我们开始调试:
经过漫长的动态链接,最后找到需要的符号以后直接跳转:
我们知道0x7ffff7ffb850
其实就是__vdso_gettimeofday
的地址。
也不一定非要一直si
的走汇编,因为动态链接中有一个循环需要非常长时间的调试,不如直接把断点挂在__vdso_gettimeofday
,可以更早的看到结果:b * 0x7ffff7ffb850
现在我们知道实际最终调用了__vdso_gettimeofday
,我们也确实可以在内核中找到这段代码:
int __vdso_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz)
{
return __cvdso_gettimeofday(tv, tz);
}
static __maybe_unused int
__cvdso_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz)
{ // 从全局变量拿到vdso_data数据,实际调用mov
const struct vdso_data *vd = __arch_get_vdso_data();
if (likely(tv != NULL)) {
struct __kernel_timespec ts;
if (do_hres(&vd[CS_HRES_COARSE], CLOCK_REALTIME, &ts))
return gettimeofday_fallback(tv, tz);
// 当返回非零的时候就会执行gettimeofday_fallback,这隐含着一个系统调用
// asm("syscall" : "=a" (ret) :
// "0" (__NR_gettimeofday), "D" (_tv), "S" (_tz) : "memory");
tv->tv_sec = ts.tv_sec;
tv->tv_usec = (u32)ts.tv_nsec / NSEC_PER_USEC;
}
if (unlikely(tz != NULL)) {
tz->tz_minuteswest = vd[CS_HRES_COARSE].tz_minuteswest;
tz->tz_dsttime = vd[CS_HRES_COARSE].tz_dsttime;
}
return 0;
}
static int do_hres(const struct vdso_data *vd, clockid_t clk,
struct __kernel_timespec *ts)
{ // 拿到vdso_ts相关的数据项
const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
u64 cycles, last, sec, ns;
u32 seq;
do {
seq = vdso_read_begin(vd);
// 获取硬件计数器的数据,和vdso一起计算一个返回给用户的时间值
// __arch_get_hw_counter 会根据 clock_mode 求出 cycles 值,这是一个 u64 类型,
// 如果转成 s64 为负数,那就返回 -1, 此时会触发 fallback 系统调用逻辑。
cycles = __arch_get_hw_counter(vd->clock_mode);
ns = vdso_ts->nsec;
last = vd->cycle_last;
if (unlikely((s64)cycles < 0))
return -1;
ns += vdso_calc_delta(cycles, last, vd->mask, vd->mult);
ns >>= vd->shift;
sec = vdso_ts->sec;
} while (unlikely(vdso_read_retry(vd, seq)));
/*
* Do this outside the loop: a race inside the loop could result
* in __iter_div_u64_rem() being extremely slow.
*/
ts->tv_sec = sec + __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns);
ts->tv_nsec = ns;
return 0;
}
gettimeofday的精度是microseconds,可以在代码中看到精度其实依赖于__arch_get_hw_counter
,其中如果clock_mode
设置为VCLOCK_TSC
,则调用rdtsc
指令, 该指令返回CPU自启动以来的时钟周期数,所以精度其实还是很高的。
在timekeeping_update->update_vsyscall
中执行vdso段的更新:
void update_vsyscall(struct timekeeper *tk)
{
struct vdso_data *vdata = __arch_get_k_vdso_data();
struct vdso_timestamp *vdso_ts;
u64 nsec;
/* copy vsyscall data */
vdso_write_begin(vdata);
vdata[CS_HRES_COARSE].clock_mode = __arch_get_clock_mode(tk);
vdata[CS_RAW].clock_mode = __arch_get_clock_mode(tk);
/* CLOCK_REALTIME also required for time() */
// vdso实际应该是使用这部分
vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME];
vdso_ts->sec = tk->xtime_sec;
vdso_ts->nsec = tk->tkr_mono.xtime_nsec;
/* CLOCK_REALTIME_COARSE */
vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME_COARSE];
vdso_ts->sec = tk->xtime_sec;
vdso_ts->nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift;
/* CLOCK_MONOTONIC_COARSE */
vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC_COARSE];
vdso_ts->sec = tk->xtime_sec + tk->wall_to_monotonic.tv_sec;
nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift;
nsec = nsec + tk->wall_to_monotonic.tv_nsec;
vdso_ts->sec += __iter_div_u64_rem(nsec, NSEC_PER_SEC, &vdso_ts->nsec);
/*
* Read without the seqlock held by clock_getres().
* Note: No need to have a second copy.
*/
WRITE_ONCE(vdata[CS_HRES_COARSE].hrtimer_res, hrtimer_resolution);
/*
* Architectures can opt out of updating the high resolution part
* of the VDSO.
*/
if (__arch_update_vdso_data())
update_vdso_data(vdata, tk);
__arch_update_vsyscall(vdata, tk);
vdso_write_end(vdata);
__arch_sync_vdso_data(vdata);
}
综上所述,其实gettimeofday的时间是由xtime
和jiffies
结合加以一些计算得到的。正常情况不需要系统调用(可以strace一下,发现确实正常情况下确实没有时间相关的系统调用)只是一个动态链接到用户态程序,和一个普通函数调用的开销相同,但是有特殊情况。
gettimeofday性能下降原因
有两个方面可能使得gettimeofday性能下降:
- 时钟源改变(也是硬件故障引起的)
- 硬件出现故障,
__arch_get_hw_counter
失败,导致执行系统调用
可以执行如下指令查看目前机器的时间源:
➜ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
kvm-clock tsc acpi_pm
➜ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
kvm-clock
我们来看看时钟源初始化相关逻辑:
static int __init init_tsc_clocksource(void)
{
if (!boot_cpu_has(X86_FEATURE_TSC) || !tsc_khz)
return 0;
if (tsc_unstable)
goto unreg;
if (tsc_clocksource_reliable || no_tsc_watchdog)
clocksource_tsc.flags &= ~CLOCK_SOURCE_MUST_VERIFY;
if (boot_cpu_has(X86_FEATURE_NONSTOP_TSC_S3))
clocksource_tsc.flags |= CLOCK_SOURCE_SUSPEND_NONSTOP;
/*
* When TSC frequency is known (retrieved via MSR or CPUID), we skip
* the refined calibration and directly register it as a clocksource.
*/
if (boot_cpu_has(X86_FEATURE_TSC_KNOWN_FREQ)) {
if (boot_cpu_has(X86_FEATURE_ART))
art_related_clocksource = &clocksource_tsc;
clocksource_register_khz(&clocksource_tsc, tsc_khz);
unreg:
clocksource_unregister(&clocksource_tsc_early);
return 0;
}
schedule_delayed_work(&tsc_irqwork, 0);
return 0;
}
static struct clocksource clocksource_tsc = {
.name = "tsc",
.rating = 300,
.read = read_tsc,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS |
CLOCK_SOURCE_VALID_FOR_HRES |
CLOCK_SOURCE_MUST_VERIFY,
.archdata = { .vclock_mode = VCLOCK_TSC },
.resume = tsc_resume,
.mark_unstable = tsc_cs_mark_unstable,
.tick_stable = tsc_cs_tick_stable,
.list = LIST_HEAD_INIT(clocksource_tsc.list),
};
默认情况下时钟源为clocksource_tsc
,即vclock_mode
为VCLOCK_TSC
。
static void __init xen_time_init(void)
{
......
clocksource_register_hz(&xen_clocksource, NSEC_PER_SEC);
......
}
static void xen_setup_vsyscall_time_info(void)
{
......
xen_clocksource.archdata.vclock_mode = VCLOCK_PVCLOCK;
}
xen时钟源的vclock_mode
为VCLOCK_PVCLOCK
,kvm时钟源的vclock_mode
为VCLOCK_HVCLOCK
。
下面引用来自于muahao:
内核在启动过程中会根据既定的优先级选择时钟源。优先级的排序根据时钟的精度与访问速度。其中CPU中的TSC寄存器是精度最高(与CPU最高主频等同),访问速度最快(只需一条指令,一个时钟周期)的时钟源,因此内核优选TSC作为计时的时钟源。其它的时钟源,如HPET, ACPI-PM,PIT等则作为备选。但是,TSC不同与HPET等时钟,它的频率不是预知的。因此,内核必须在初始化过程中,利用HPET,PIT等始终来校准TSC的频率。如果两次校准结果偏差较大,则认为TSC是不稳定的,则使用其它时钟源。并打印内核日志:Clocksource tsc unstable.
正常来说,TSC的频率很稳定且不受CPU调频的影响(如果CPU支持constant-tsc)。内核不应该侦测到它是unstable的。但是,计算机系统中存在一种名为SMI(System Management Interrupt)的中断,该中断不可被操作系统感知和屏蔽。如果内核校准TSC频率的计算过程quick_ pit_ calibrate ()被SMI中断干扰,就会导致计算结果偏差较大(超过1%),结果是tsc基准频率不准确。最后导致机器上的时间戳信息都不准确,可能偏慢或者偏快。
当内核认为TSC unstable时,切换到HPET等时钟,不会给你的系统带来过大的影响。当然,时钟精度或访问时钟的速度会受到影响。通过实验测试,访问HPET的时间开销为访问TSC时间开销的7倍左右。如果您的系统无法忍受这些,可以尝试以下解决方法:在内核启动时,加入启动参数:tsc=reliable
下图是一个vdso ARM的架构,X86的流程也大差不差。
所以在硬件没什么问题的情况下,时间相关的函数都还是比较快的,如果发现系统qps下降,perf发现时间相关调用比较慢时,也可以用strace很快的发现相关的系统调用,以此定位问题。
总结
一个马里奥奥德赛句式的文章标题暗示着我不再是一个纯粹的技术博主,我逐渐更趋向于自我满足式写作而不是满足他人式写作,从某种角度来说这代表了我最近近乎畸形的心理状态。
三分之一杯拿铁下肚,又开始惆怅起来,明天就是毕设代码检查的deadline,但是现在还是悠哉悠哉,思考过后发现原罪即是对美好生活的向往,但我已经分不清现在的下场是惩罚还是奖励了。
参考: