You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

使用libfaketime时MacOS与Linux下setTimeout行为差异原因探究

libfaketime下Node.js定时器跨平台行为差异解析

咱们先从实际现象说起:当用libfaketime修改进程的时间流速时,Linux和macOS上Node.js的setTimeout表现完全不一样——Linux上的超时会跟着修改后的时间走,macOS上却依然遵循系统真实时间。

复现示例

macOS环境

执行以下命令后,你得等整整1小时才会看到输出,和原系统时间的流逝速度一致:

DYLD_INSERT_LIBRARIES=src/libfaketime.1.dylib DYLD_FORCE_FLAT_NAMESPACE=y FAKETIME="@2020-12-24 00:00:00 x3600" node -e "setTimeout(() => {console.log('hello');}, 3600 * 1000);"

Linux环境

同样的逻辑,这里只需要1秒就会输出,因为时间流速被加速了3600倍:

LD_PRELOAD=src/libfaketime.1.so FAKETIME="@2020-12-24 00:00:00 x3600" node -e "setTimeout(() => {console.log('hello');}, 3600 * 1000);"

排查后的关键发现

我通过在libfaketime的函数里加printf日志验证过:

  • Linux上的Node.js(底层靠libuv驱动)会反复调用libc的clock_gettime函数来计算定时器到期时间
  • macOS上的Node.js根本不会碰这个函数,所以libfaketime的拦截完全没生效

为什么会有这种差异?

1. libuv的跨平台定时器实现不同

Node.js的定时器全靠libuv,而libuv在两个平台上用了完全不同的时间源:

  • Linux侧:libuv用clock_gettime(CLOCK_MONOTONIC)作为定时器的时间基准。这个函数属于libc,而libfaketime正是通过LD_PRELOAD拦截libc的时间相关函数来篡改时间的,自然能影响到定时器的计算。
  • macOS侧:libuv用的是mach内核提供的mach_absolute_time()。这是内核直接暴露的接口,不属于libc范畴,libfaketime的DYLD_INSERT_LIBRARIES只能拦截用户态的库函数,碰不到内核级的mach接口,所以改不了这个时间源,定时器就只能按真实时间走。

2. 为什么选不同的时间源?

这和两个平台的系统设计历史有关:

  • Linux的CLOCK_MONOTONIC是稳定的单调时间源,不会被系统时间调整影响,适合做定时器基准,而且libc有标准调用接口,用起来方便。
  • macOS的mach_absolute_time()是基于硬件时钟的高分辨率单调时间,精度比libc的clock_gettime更高,是苹果官方推荐的高精度时间接口,libuv为了在macOS上获得更好的定时器性能和稳定性,就选了这个接口,而非libc的实现。

额外发现:Linux冻结时间下setImmediate和setTimeout(cb, 0)的差异

当用libfaketime冻结时间时,你会发现setImmediate的回调能正常执行,但setTimeout(cb, 0)的回调却不会跑,原因很简单:

  • setImmediate是libuv的check handle,属于事件循环的检查阶段,只要事件循环还在转,每次迭代都会执行它,完全不依赖时间计算。
  • setTimeout(cb, 0)本质还是个定时器,哪怕超时设为0,libuv也会把它转成最小超时(通常是1ms)的定时器,它的到期判断依赖clock_gettime获取的时间。时间被冻结后,到期条件永远满足不了,回调自然不会触发。

内容的提问来源于stack exchange,提问作者talisman

火山引擎 最新活动