我有一个算法,多年来一直用于在非实时环境中模拟实时时钟。在 Windows 11 中的 WSL2 中的 Ubuntu 24.04 上使用之前,它一直运行得非常好。该逻辑在系统时钟时间中预测每个下一个时间步骤应该发生的时间,并在循环中反复调用 chrono 的 now(),直到达到该时间点,然后继续执行下一帧。在这种环境中设置时,now() 的返回值偶尔会在连续调用之间向前跳跃相当长的时间(例如,~25 秒)。只需观察应用程序运行就会发现,now() 不需要花费 ~25 秒才能返回一个值,因为整个应用程序将在 ~5 秒内完成,而它应该花费 ~30 秒。这意味着调用 now() 实际上改变了时钟的时间。为了验证这一点,我将示例代码包装在一个 bash 脚本中,我可以在其中监视和重新同步 WSL 的时钟……now() 确实在改变时钟的时间。
示例代码已在此环境中使用 g++ 和 clang++、C++17 和 C++20 构建。在这四种情况下都观察到了相同的效果。
经观察,示例代码在运行 Ubuntu-24.04(旧硬件)和 Windows11/WSL2/Ubuntu-22.04(相同硬件)的机器上正常工作,时钟时间没有改变
我发现无数帖子都在讨论在 WSL 下运行时 Linux 时钟漂移问题。大多数帖子声称通过重新同步硬件时钟“解决”了该问题。但这并不能“解决”漂移问题,只是将其重置。显然系统中某个地方存在错误,这可能与此有关吗?某些 WSL 配置中的 now() 是否会影响时间,而我在极端情况下反复使用 now() 是否只是在强调问题。我该如何解决这个问题?
可重现的示例代码:
#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
void print(std::chrono::duration<long double> elapsed_seconds,
std::chrono::_V2::system_clock::time_point,
std::chrono::duration<long double> error);
int main()
{
std::cout << "commanded_elapsed_time, corresponding_measured_time_points, error" << std::endl;
using namespace std::chrono_literals;
auto start_tp = std::chrono::system_clock::now();
auto dt = 1.0s;
auto elapsed = 0.0s;
auto next_tp = start_tp + elapsed;
std::chrono::_V2::system_clock::time_point now;
print(elapsed, start_tp, start_tp - next_tp);
for (int t = 1; t < 30; t++)
{
elapsed = dt * t;
next_tp = start_tp + elapsed;
while((now = std::chrono::system_clock::now()) < next_tp) {}
print(elapsed, now, now - next_tp);
}
return 0;
}
void print(
std::chrono::duration<long double> elapsed_seconds,
std::chrono::_V2::system_clock::time_point tp,
std::chrono::duration<long double> error)
{
std::time_t currentTime = std::chrono::system_clock::to_time_t(tp);
std::tm* localTime = std::localtime(¤tTime);
std::stringstream ss;
ss << std::fixed << std::setprecision(3) << std::setfill('0') << std::setw(6) << elapsed_seconds.count() << ", ";
ss << std::put_time(localTime, "%H:%M:%S");
auto nowInMicroseconds = std::chrono::time_point_cast<std::chrono::microseconds>(tp);
auto epoch = nowInMicroseconds.time_since_epoch();
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(epoch).count() % 1000000;
ss << '.' << std::setfill('0') << std::setw(9) << microseconds * 1000 << ", ";
ss << std::fixed << std::setprecision(9) << error.count();
std::cout << ss.str() << std::endl;
}
测试脚本:
#! /bin/bash
TRUE=0
FALSE=1
using_wsl=$FALSE
echo "kernel name:........" $(uname -s)
echo "kernel release:....." $(uname -r)
echo "kernel version:....." $(uname -v)
echo "machine:............" $(uname -m)
echo "processor:.........." $(uname -p)
echo "hardware platform..." $(uname -i)
echo "operating system...." $(uname -o)
if [ "$(systemd-detect-virt)" = "wsl" ]; then
echo "running in wsl...... yes"
using_wsl=$TRUE
if ! command -v hwclock >/dev/null 2>&1; then
echo ""
echo "When running this script within WSL, the 'hwclock'"
echo "command is used to sync the WSL clock with the"
echo "host system's clock. 'hwclock' is not found on"
echo "this system. Install and rerun test."
exit 1
fi
else
echo "running in wsl...... no"
using_wsl=$FALSE
fi
echo ""
if [ $using_wsl -eq $TRUE ]; then
echo "WSL time before syncing with host...:" $(date +"%T.%6N")
echo "syncing WSL clock with host ..."
sudo hwclock -s
fi
start_time=$(date +"%s.%N")
echo "test system start time..............: $(date -d "@$start_time" +"%T.%6N")"
echo "test starting ..."
echo ""
./zero_drift_dt
end_test_time=$(date +"%s.%N")
echo ""
echo "... test complete"
echo "test system finish time............................: $(date -d "@$end_test_time" +"%T.%6N")"
if [ $using_wsl -eq $TRUE ]; then
sudo hwclock -s
echo "WSL time after syncing with host (again)...........:" $(date +"%T.%6N")
fi
final_time=$(date +"%s.%N")
expected_duration=$(awk "BEGIN {print $end_test_time - $start_time}")
actual_duration=$(awk "BEGIN {print $final_time - $start_time}")
error_duration=$(awk "BEGIN {print $end_test_time - $final_time}")
echo "Expected approximate test duration (s).............:" $expected_duration
echo "Measured test duration (s).........................:" $actual_duration
echo "During this test, now() moved clock forward (s)....:" $error_duration
问题系统的结果:
~/Projects/proving_grounds/now_error$ sudo ./test.sh
[sudo] password for blumert:
kernel name:........ Linux
kernel release:..... 5.15.153.1-microsoft-standard-WSL2
kernel version:..... #1 SMP Fri Mar 29 23:14:13 UTC 2024
machine:............ x86_64
processor:.......... x86_64
hardware platform... x86_64
operating system.... GNU/Linux
running in wsl...... yes
WSL time before syncing with host...: 13:39:30.320309
syncing WSL clock with host ...
test system start time..............: 13:39:31.001844
test starting ...
commanded_elapsed_time, corresponding_measured_time_points, error
00.000, 13:39:31.003072000, 0.000000000
01.000, 13:39:32.003072000, 0.000000070
02.000, 13:39:52.464335000, 19.461263572
03.000, 13:39:52.464537000, 18.461465685
04.000, 13:39:52.464569000, 17.461497372
05.000, 13:39:52.464573000, 16.461501714
06.000, 13:39:52.464575000, 15.461503561
07.000, 13:39:52.464577000, 14.461505319
08.000, 13:39:52.464579000, 13.461506932
09.000, 13:39:52.464580000, 12.461508617
10.000, 13:39:52.464582000, 11.461510324
11.000, 13:39:52.464584000, 10.461512480
12.000, 13:39:52.464586000, 9.461514319
13.000, 13:39:52.464588000, 8.461515990
14.000, 13:39:52.464589000, 7.461517739
15.000, 13:39:52.464591000, 6.461519320
16.000, 13:39:52.464593000, 5.461520922
17.000, 13:39:52.464594000, 4.461522511
18.000, 13:39:52.464596000, 3.461524055
19.000, 13:39:52.464597000, 2.461525592
20.000, 13:39:52.464599000, 1.461527167
21.000, 13:39:52.464600000, 0.461528720
22.000, 13:39:57.473118000, 4.470046550
23.000, 13:39:57.473178000, 3.470106269
24.000, 13:39:57.473182000, 2.470110610
25.000, 13:39:57.473184000, 1.470112218
26.000, 13:39:57.473185000, 0.470113725
27.000, 13:40:02.476388000, 4.473316514
28.000, 13:40:02.476452000, 3.473380680
29.000, 13:40:02.476457000, 2.473385528
... test complete
test system finish time............................: 13:40:02.477504
WSL time after syncing with host (again)...........: 13:39:43.002630
Expected approximate test duration (s).............: 31.4757
Measured test duration (s).........................: 12.0018
During this test, now() moved clock forward (s)....: 19.4738
- 请注意,测试结束时时钟会向后重置。如果系统被搁置一段时间,第一次重新同步也会表现出这种行为。
- 根据其定义用途,该应用程序的执行时间应略超过 30 秒。根据重新同步时间的测量,它仅用了 12 秒。
- 在完美运行中(如上面描述的所有其他环境一样),相应的测量时间点以精确的 1 秒增量进展,并且误差很少超过几十纳秒(即,像前两个样本一样)。
- 每次跑步都有点不同,第一次大跳跃似乎可以在前 10 秒左右的任何时候发生,但通常发生在前 3 秒。
系统时钟不是合适的类型,因为它会(并且确实)变化。来自 cppreference(重点是我的):
你想使用
steady_clock