WindowsとWSL2の時刻のずれを修正する

# はじめに

**仮想マシン上の Windows 10** で WSL2 を使うと大きく時刻がずれるが、Qiita を始め、あちこちで紹介されている `hwclock` を使う方法ではうまくいかない。`ntpdate` などで Windows と同じ NTP サーバに同期させる手もあるが、WSL2 から Windows ファイルシステム上にファイルを `touch` し、そのファイルのタイムスタンプで WSL2 の時刻を設定すれば、このずれが簡単に修正できる。[シェルスクリプトでも良い](#ファイルのタイムスタンプで修正する)が、[Rust で専用のコマンドを作ってみる](#ファイルのタイムスタンプで修正するrust版)。

# どんな感じか...

Windows 側の PowerShell で、時刻を表示するコマンドを叩いてみる。PowerShell の `Get-Date` と Linux の `date` で同じフォーマット（ここでは RFC 3339 もどきの形式）で表示させてみる。

```
PS> wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-20.04    Running         2
PS> $rfc3339 =  "yyyy-MM-dd HH:mm:ss.fffffff00zzz"
PS> Get-Date -Format $rfc3339; wsl date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-02 16:21:51.792292300+09:00
2021-07-02 13:38:28.750043500+09:00
2021-07-02 16:21:52.238112100+09:00
```

だいぶズレている...

このズレは、ホストマシンをスリープ状態から復帰させたり、仮想 Windows マシンをポーズやサスペンドから復帰させたり、時刻を変更した場合に発生するし、WSL 2 をしばらく Running 状態にしておいても発生してしまう。こういった時刻のズレは WSL 1 では発生しない。

WSL 1 では Windows 側と WSL 側で時計を共用しているため、両者の間で時刻のズレは発生しないが、WSL 2 では Windows 側と Linux 側でそれぞれ独立した時計を持ち、個別に時計を管理しているということなのだろう。

なお、環境は次の通り。

+ Ubuntu 20.04.2 LTS (Linux Kernel 5.10.16.3-microsoft-standard-WSL2)
+ Windows 10 Enterprise 20H2
+ VMware Fusion 11.5.7 (macOS Catalina 10.15.7 (19H1217))

今回の記事は **Windows が仮想マシン上で動いている場合であり、物理マシン上で動いている場合ではない**ので、念のため。

# 原因を探ってみる...

WSL 2 の時刻のズレについては、WSL の issues に色々と投稿がある。

それらによると、現在の WSL 2 では、[Linux カーネルの Hyper-V ドライバに問題があり](https://github.com/microsoft/WSL/issues/5324#issuecomment-785264317)、Hyper-V 経由で Linux へ時刻修正のメッセージが飛んでいるものの、Linuxカーネルの Hyper-V ドライバで、[Hyper-V からの時刻修正のメッセージが Hyper-V ドライバで処理されなかったときにエラーを返していなかったり](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/hv/hv_util.c?id=90b125f4cd2697f949f5877df723a0b710693dd0)、[古いメッセージが処理されずに溜まったままとなることで](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/hv/hv_util.c?id=b46b4a8a57c377b72a98c7930a9f6969d2d4784e)、Windows と Linux の時刻が発生するらしい。

ちなみに、今回は、Windows が仮想環境上にあるが、ホストと仮想 Windows の間では時刻のズレは発生しない。このあたりの事情を Windows のイベントログで探ってみる。

Event Viewer を立ち上げて、Windows Logs の System ログをクリアし、仮想 Windows をサスペンド、レジュームし、System ログを覗いてみる。

```
PS> Get-WinEvent -LogName System
...
   ProviderName: Microsoft-Windows-Kernel-General

TimeCreated                      Id LevelDisplayName Message
-----------                      -- ---------------- -------
7/2/2021 4:35:41 PM               1 Information      The system time has changed to 2021-07-02T07:35:41.682000000Z from 2021-07-02T07:35:18.305136000Z....
7/2/2021 4:35:41 PM              24 Information      The time zone information was refreshed with exit reason 0. Current time zone bias is -540.
...
```

色々なログも出ていたが、関係なさそうなログは省略した。`Microsoft-Windows-Kernel-General` の `Event Id 1` で、レジューム時に時刻が修正されていることがわかる。ログの詳細を見てみよう[^1]。

```
PS> Get-WinEvent -ProviderName Microsoft-Windows-Kernel-General | Where-Object { $_.Id -eq 1 } | Format-List -Property TimeCreated,Message

TimeCreated : 7/2/2021 4:35:41 PM
Message     : The system time has changed to 2021-07-02T07:35:41.682000000Z from 2021-07-02T07:35:18.305136000Z.

              Change Reason: An application or system component changed the time.
              Process: '\Device\HarddiskVolume2\Program Files\VMware\VMware Tools\vmtoolsd.exe' (PID 3200).
...
```

VMware Tools が仮想 Windows の時刻を修正している。Parallels Desktop でも試してみたが、Parallels Tools が仮想 Windows の時刻を修正していた。

以上をまとめると、次のようになる。

+ “Windows on VMware Fusion on ホストOS” で時刻のズレが生じないのは、Windows にインストールされた VMware Tools が VMware Fusion と連携して、macOS と Windows の間で時刻を同期させているため。
+ WSL 2 = “Linux on Hyper-V on Windows” で時刻のズレが生じるのは、Linux カーネルに組み込まれた Hyper-V ドライバ 〜 Hyper-V 〜 Windows の連携がうまくいっていないため。

# 解決策を考えてみる...

さて、解決策はいくつかありそうだ。

1. [Insider に加入して、WSL を最新にする](https://github.com/microsoft/WSL/issues/5324#issuecomment-821421563)。
2. 修正された Linux カーネルイメージを使って WSL2 を起動する。
3. レジュームをトリガに、および、定期的に時刻同期のコマンドを起動する。
4. （気がついた時に）手動で修正する。
5. WSL 2 を再起動する。
6. WSL 1 に戻す。

まずは、6. と 5. は論外として、まずは「4. 手動で修正する」方法を考えてみる。

## WSL 2 上のハードウェアクロックで修正する

Linux では `hwclock` コマンドを使ってハードウェアクロックを読み出し、その結果に基づいてカーネル時計を修正することができる。この方法は、[いろいろなサイトで紹介されている](https://www.google.co.jp/search?hl=ja&q=wsl2+時刻+ずれる+hwclock)。実際に時刻を修正する前に、Linux のハードウェアクロックを読み出してみよう。

```
PS> wsl -u root hwclock ";" date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-03 03:39:07.932030+09:00
2021-07-03 03:39:09.782474300+09:00
2021-07-03 15:37:52.806205600+09:00
```

結果の１行目がハードウェアクロック、２行目が Linux、３行目が Windows。Linux から見えるハードウェアクロックも Linux の時計と同じようにズレており、残念ながらハードウェアクロックを参照する `hwclock` では WSL 2 上の Linux の時刻のズレは修正はできない。

Windows 側から Linux 側の時計を修正するコマンドとして [wsl-clock.exe](https://github.com/stuartleeks/wsl-clock/releases/download/v0.1.632448672/wsl-clock_windows_amd64.zip) があるが、このコマンドも内部的に `wsl.exe -u root hwclock -s` を呼び出しているだけなので、残念ながら仮想環境上の Windows では WSL 2 上の Linux の時刻のズレは修正できない。

## NTP で修正する

Windows 側をインターネット時刻へ同期に設定してあるのなら、Linux 側も同じサーバに NTP で同期をかけるのが良さそう。Linux 側に `ntpdate` をインストールしておく。Ubuntu などの Debian 系であれば、`sudo apt install ntpdate` でインストールできる。

```
PS> Get-WinEvent -ProviderName Microsoft-Windows-Time-Service | Where-Object {$_.Id -eq 35 } | Format-List -Property TimeCreated,Message

TimeCreated : 7/3/2021 3:40:06 PM
Message     : The time service is now synchronizing the system time with the time source ntp.nict.jp,0x9
              (ntp.m|0x9|0.0.0.0:123->133.243.238.163:123) with reference id 2750346117. Current local stratum number is 2.

PS> wsl -u root ntpdate ntp.nict.jp
 3 Jul 15:41:02 ntpdate[1599]: step time server 133.243.238.243 offset 43123.034989 sec
PS> Get-Date -Format $rfc3339; wsl date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-03 15:41:40.210553100+09:00
2021-07-03 15:41:40.466928900+09:00
2021-07-03 15:41:40.579134300+09:00
```

個人的な趣味で、同期先は ntp.nict.jp としている。先に、コントロールパネルで Windows 側を同期させてから、Linux 側を同期させてみた。タイムスタンプの順序はあっているので、時刻は概ねあっていそう。

ここでは、Windows 側に設定されている NTP サーバに同期することで、Windows 側と Linux 側の時刻同期をしてみたが、Windows 側で NTP サーバ機能を有効化し、そちらに NTP で同期することもできるだろう。

## ファイルのタイムスタンプで修正する

やりたいことはインターネット時刻に合わせたいというよりも、Windows 側の時刻に合わせたいことなので、もう少し直接的にできないか考えてみる。

Linux でリモートファイルシステムを利用する場合、ファイルの作成や更新などのタイムスタンプはマウントする側ではなく、マウントされている側（リモートのファイルサーバ側）で付与される。WSL 2 では Windows 側のファイルシステムを 9p というリモートファイルシステムをアクセスするためのプロトコルを使って Linux 側にマウントしているので、Windows 側のファイルシステム上にファイルを作成し、そのタイムスタンプを Linux 側で読み出すことで、Windows 側の時刻が読み出せそうだ。

簡単なシェルスクリプトを書いてこの方法で良いかどうか確かめてみる。環境変数`USERPROFILE`には Windows の `%USERPROFILE%` の Linux 側からみたパスが設定されているものとする。まずはエラーは無視。

```sh:ftimesync.sh
#/bin/bash

timestamp="$USERPROFILE/.wsltimestamp"

date --rfc-3339=ns
touch "$timestamp"
windate=$(date --rfc-3339=ns -r "$timestamp")
date --rfc-3339=ns -s "$windate"
```

これを Linux 側のパスの通ったところ（例えば、`/usr/local/bin`）におく。

では、やってみよう。

```
PS> wsl -u root ftimesync.sh
2021-07-03 15:03:05.899271800+09:00
2021-07-03 16:18:51.772752000+09:00
PS> Get-Date -Format $rfc3339; wsl date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-03 16:18:57.893327000+09:00
2021-07-03 16:18:58.056519600+09:00
2021-07-03 16:18:58.206326900+09:00
```

Linux側がちょっと遅れ気味だが、この方法で良さそう。

## ファイルのタイムスタンプで修正する（Rust版）

折角なので、もう少ししっかりとプログラムを書いてみる。やることは次の通り。

1. Windows のコントロールパネルなどで Windows 側のユーザー環境変数 `WSLENV` に `USERPROFILE/p` を設定し、WSL を再起動する。  
　・これによって、Linux 側からみた `USERPROFILE` のパスが取得できる。
2. `USERPROFILE` の下にファイルを作成する。  
　・ファイル名は `.wsltimestamp` としておく。
3. ファイルが作成された時の Linux 側の時刻と Windows 側の時刻を取得する。  
　・Linux 側の時刻での推定値はファイル作成の関数呼び出しの直前直後の時刻の真ん中としてみる。  
　・Windows 側の時刻はファイル修正時刻とする。
4. 時刻の差分を Linux 側の時刻に反映する。

ざっくり書いてみた[^2]。[プログラムの全体は GitHub を見ていただければ](https://github.com/moriai/wsltimesync.git)。途中でエラーが発生したら、エラーメッセージを出力して即座に終了。

<details><summary>WSL2時刻同期コマンド Rust版</summary><div>

```rust:src/main.rs
use std::{error,env};
use std::fs::{self,File};
use std::time::SystemTime;
use nix::time::{self,ClockId};
use nix::sys::time::TimeSpec;

trait SetTime {
    fn set(&self) -> Result<(), Box<dyn error::Error>>;
}

impl SetTime for SystemTime {
    fn set(&self) -> Result<(), Box<dyn error::Error>> {
        let unix_time = self.duration_since(SystemTime::UNIX_EPOCH)?;
        time::clock_settime(ClockId::CLOCK_REALTIME, TimeSpec::from(unix_time))?;
        Ok(())
    }
}

fn main() -> Result<(), Box<dyn error::Error>> {    
    let timestamp = env::var("USERPROFILE")? + "/.wsltimestamp";

    let start_time = SystemTime::now();
    { let _ = File::create(&timestamp)?; }
    let end_time = SystemTime::now();
    let estimated = start_time + (end_time.duration_since(start_time)?/2);

    let metadata = fs::metadata(&timestamp)?;
    if let Ok(mtime) = metadata.modified() {
        if let Ok(diff) = mtime.duration_since(estimated) {
            println!("difference: {:?} behind", diff);
            let new_time = SystemTime::now() + diff;
            new_time.set()?;
        } else {
            let diff = estimated.duration_since(mtime)?;
            println!("difference: {:?} ahead", diff);
        }
    } else {
        println!("Not supported on this platform");
    }

    Ok(())
}
```
</div></details>

Linux 側で `cargo build --release` して、できたバイナリ `target/release/ftimesync` を `/usr/local/bin` などの下にコピーする。

では、動かしてみよう。Linux 側で時刻を設定するためには root 権限が必要なので、`wsl.exe` コマンドを `-u root` を付けて起動する。

```
PS> wsl -u root ftimesync
difference: 4350.2458098s behind
PS> Get-Date -Format $rfc3339; wsl date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-03 17:44:47.701689000+09:00
2021-07-03 17:44:47.860125900+09:00
2021-07-03 17:44:47.998349700+09:00
```

良い感じに修正されているようだ。

## 修正された Linux カーネルイメージを使う

次に、2. をやってみよう。

コンパイル済みのカーネルは [Microsoft のこちらのサイト](https://www.catalog.update.microsoft.com/Search.aspx?q=Windows%20Subsystem%20for%20Linux)から入手できる。[リリースノート](https://docs.microsoft.com/en-us/windows/wsl/kernel-release-notes#510163)によれば、5.10.16 で時刻のズレが修正されたとあるので、"Windows Subsystem for Linux Update - 5.10.16" をダウンロードしてインストールする。

```
PS> wsl uname -r
5.10.16.3-microsoft-standard-WSL2
PS> Get-Date -Format $rfc3339; wsl date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-04 15:17:16.173660900+09:00
2021-07-04 15:17:16.311780200+09:00
2021-07-04 15:17:16.481472400+09:00
```

さて、サスペンド、レジューム、2分くらい待つ。

```
PS> Get-Date -Format $rfc3339; wsl date --rfc-3339=ns; Get-Date -Format $rfc3339
2021-07-04 15:22:47.979658300+09:00
2021-07-04 15:19:53.178180800+09:00
2021-07-04 15:22:48.362688600+09:00
```

残念、時刻はズレている。

次に、カーネルのソースコードからカーネルイメージをビルドしてみる。ソースコードは[linux-msft-wsl-5.10.16.3](https://github.com/microsoft/WSL2-Linux-Kernel/releases/tag/linux-msft-wsl-5.10.16.3) からダウンロードする。カーネルイメージのビルドは[こちら](https://github.com/microsoft/WSL2-Linux-Kernel/blob/master/README-Microsoft.WSL2)に記載されている通りだが、個人的にはコンパイル結果は別ディレクトリに出力するようにするのがオススメ。

    1. 新しめの Ubuntu ディストロ（例えば 20.04LTS）をインストールする
    2. sudo apt install build-essential flex bison libssl-dev libelf-dev dwarves
    3. カーネルのソースコードを展開（もしくは、git clone）し、ソースコードのトップへ cd する
    4. mkdir ../build
    5. make O=../build/ KCONFIG_CONFIG=Microsoft/config-wsl

できたカーネルイメージ `vmlinux` を適当なところにコピーし、`.wslconfig` にその場所を指定する。

```%USERPROFILE%\.wslconfig
[wsl2]
kernel=C:\\Users\\xxx\\AppData\\Local\\WSL\\linux-msft-wsl-5.10.16.3.vmlinux
```

これで再起動して実験してみたが、サスペンド／レジュームでの時刻のズレは解消されない。

たぶん、仮想環境特有の問題があり、それには対処していないのだろう。

# おわりに

とりあえずは、レジューム時と定期的にここで紹介したシェルスクリプトやコマンドを実行すれば良さそう。なお、「3. レジュームをトリガに、および、定期的に時刻同期のコマンドを起動する」は、あちこちにたくさん記事があるようなので省略。

現時点では、最新の WSL2 のカーネルも含め、仮想環境特有の時刻のズレの問題には対処してないようだ。

[^1]: たくさんのログからマッチするエントリを抽出する場合、`Where-Object` でパイプラインするよりも、`Get-WinEvent` で `-FilterHashtable` を使った方が効率が良い。

[^2]: GitHub で公開しているコードを解説用にシンプルにした。