std::chrono now() 在 WSL2 下与 Ubuntu 24.04 产生非常显着的偏差

问题描述 投票:0回答:1

我有一个多年来使用的算法,用于在非实时环境中模拟实时时钟。它一直工作得非常好,直到在 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(&currentTime);
    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 纳秒(即,如前两个示例)。
  • 每次跑步都有一点不同,第一次大跳跃似乎可能发生在前 10 秒左右的任何时间,但通常发生在前 3 秒。
c++ windows-subsystem-for-linux c++-chrono drift ubuntu-24.04
1个回答
0
投票

系统时钟使用的类型错误。来自 cppref (强调我的):

类 std::chrono::system_clock 代表系统范围的实时挂钟。

它可能不是单调的:在大多数系统上,系统时间可以随时调整

您想使用

steady_clock

类 std::chrono::steady_clock 表示单调时钟。该时钟的时间点不会随着物理时间的向前移动而减少,并且该时钟的滴答之间的时间是恒定的。这个时钟与挂钟时间无关(例如,它可以是自上次重新启动以来的时间),并且最适合测量间隔

© www.soinside.com 2019 - 2024. All rights reserved.