WSLでusbipdの上限を増やす

概要

WSLでは通常USB2系8ポート、USB3系が8ポートの上限があります。8ポート上限にあたることが多かったのでカーネルリビルドで上限突破してみました。

カーネルビルドの準備

まずはカーネルをビルドできる状態を作ります。

(Windows)WSLの更新確認

>wsl --update
更新プログラムを確認しています。
Linux 用 Windows サブシステムの最新バージョンは既にインストールされています。

まずはWindows上でWSLが最新かをたしかめます。

(WSL)とりあえず最新化

sudo apt update
sudo apt upgrade

念のため最新化しておきます。

(WSL)カーネルバージョン確認

$ uname -r
6.18.33.2-microsoft-standard-WSL2

WSLなのでMicrosoftのカーネルで動いています。

(WSL)アーキテクチャ確認

$ uname -m
x86_64

ARMの特殊Windows以外であれば通常はx86_64のはず。

(WSL)オプション確認

$ zgrep 'CONFIG_USBIP_VHCI' /proc/config.gz
CONFIG_USBIP_VHCI_HCD=m
CONFIG_USBIP_VHCI_HC_PORTS=8
CONFIG_USBIP_VHCI_NR_HCS=1

上記で8ポートのUSBが1組あるので、8ポート上限であることがわかります。

(WSL)カーネルビルド環境構築

sudo apt install git build-essential flex bison dwarves libssl-dev libelf-dev libncurses-dev cpio qemu-utils rsync

必要なパッケージをインストールします。

カーネルビルド

(WSL)ディレクトリ移動

cd /tmp/

とりあえず適当なところに移動してビルドします。

(WSL)ソース取得

KVER="$(uname -r | sed -E 's/-microsoft-standard-WSL2.*//')"
echo "$KVER"

git clone --depth 1 --branch "linux-msft-wsl-${KVER}" https://github.com/microsoft/WSL2-Linux-Kernel.git

自分のカーネルと同じバージョンを指定する必要があります。

(WSL)ソースディレクトリに移動

cd WSL2-Linux-Kernel

(WSL)ビルドオプション変更

./scripts/config --file Microsoft/config-wsl \
  --set-val CONFIG_USBIP_VHCI_NR_HCS 4

(WSL)コンフィグ確認

make KCONFIG_CONFIG=Microsoft/config-wsl olddefconfig

エラーがでなければ大丈夫のはずです。

(WSL)ビルド

make -j"$(nproc)" KCONFIG_CONFIG=Microsoft/config-wsl

これでカーネルのビルドが始まります。

(WSL)モジュールのビルド

make KCONFIG_CONFIG=Microsoft/config-wsl \
  INSTALL_MOD_PATH="$PWD/modules" modules_install

make KCONFIG_CONFIG=Microsoft/config-wsl \
  headers_install INSTALL_HDR_PATH="$PWD/headers"

make -C tools/perf \
  NO_JEVENTS=1 \
  NO_JVMTI=1 \
  NO_LIBTRACEEVENT=1 \
  install DESTDIR="$PWD/perf" prefix=/

上記でVHDXモジュールに必要なものをビルドします。

(WSL)VHDXモジュール作成

./Microsoft/scripts/gen_artifacts_vhdx.sh \
  "$PWD/modules" \
  "$PWD/headers" \
  "$PWD/perf" \
  "$(make -s kernelrelease)" \
  modules.vhdx

(WSL)Windows側へコピー

mkdir -p /mnt/c/Users/<Windowsユーザー名>/wsl-kernel

cp arch/x86/boot/bzImage \
   /mnt/c/Users/<Windowsユーザー名>/wsl-kernel/

cp modules.vhdx \
   /mnt/c/Users/<Windowsユーザー名>/wsl-kernel/

適当な場所にコピーします。

(Windows)%USERPROFILE%.wslconfig を設定

notepad $env:USERPROFILE\.wslconfig

上記で編集をします。

[wsl2]
kernel=C:\\Users\\<Windowsユーザー名>\\wsl-kernel\\bzImage
kernelModules=C:\\Users\\<Windowsユーザー名>\\wsl-kernel\\modules.vhdx

さっき作成したファイルを追加します。これで次回からこのカーネルとモジュールで起動するはずです。ただし最新バージョンではこの手順だとたぶん失敗します。したにあるスクリプトの中身のほうがたぶん安全です。

(Windows)WSLの停止

wsl --shutdown

いま動いているWSLを停止します。これで次回から新しいカーネルで起動するはずです。

(WSL)動作確認

$ ls /sys/devices/platform/ | grep vhci
vhci_hcd.0

上記の行が増えていれば成功です。増えていなかったりまったく起動しなかった場合にはいろいろ失敗していますので、AIに確認してもらってください。

自動化

ここまでの手順を自動化したスクリプトを作りました。カーネルを自分でビルドすると古くなりますので適度に最新に入れ替える必要があります。ちなみにちょうど最新のカーネルと使っていたカーネルで作業内容が変わっています。。。

#!/usr/bin/env bash
set -Eeuo pipefail

# ============================================================
# ここだけ設定
# ============================================================
NR_HCS=4
# ============================================================

BUILD_DIR="$HOME/wsl-usbip-kernel-build"
REPO_URL="https://github.com/microsoft/WSL2-Linux-Kernel.git"
API_URL="https://api.github.com/repos/microsoft/WSL2-Linux-Kernel/releases/latest"

echo
echo "========================================"
echo " WSL USB/IP kernel build"
echo " NR_HCS=$NR_HCS"
echo "========================================"
echo


# ------------------------------------------------------------
# WSL確認
# ------------------------------------------------------------
if ! grep -qi microsoft /proc/sys/kernel/osrelease; then
    echo "ERROR: WSL2内で実行してください。" >&2
    exit 1
fi


# ------------------------------------------------------------
# Architecture
# ------------------------------------------------------------
case "$(uname -m)" in
    x86_64)
        CONFIG_FILE="Microsoft/config-wsl"
        KERNEL_IMAGE="arch/x86/boot/bzImage"
        ;;
    aarch64|arm64)
        CONFIG_FILE="Microsoft/config-wsl-arm64"
        KERNEL_IMAGE="arch/arm64/boot/Image"
        ;;
    *)
        echo "ERROR: unsupported architecture: $(uname -m)" >&2
        exit 1
        ;;
esac


# ------------------------------------------------------------
# 必要パッケージ
# ------------------------------------------------------------
echo "==> Installing build dependencies"

sudo apt-get update

sudo apt-get install -y \
    git \
    curl \
    jq \
    build-essential \
    flex \
    bison \
    dwarves \
    libssl-dev \
    libelf-dev \
    libncurses-dev \
    cpio \
    rsync


# ------------------------------------------------------------
# Windows USERPROFILE
# ------------------------------------------------------------
WIN_PROFILE_WIN="$(
    cmd.exe /C 'echo %USERPROFILE%' 2>/dev/null |
    tr -d '\r' |
    tail -n 1
)"

if [[ ! "$WIN_PROFILE_WIN" =~ ^[A-Za-z]:\\ ]]; then
    echo "ERROR: Windows USERPROFILEを取得できません。" >&2
    exit 1
fi

WIN_PROFILE="$(
    wslpath -u "$WIN_PROFILE_WIN"
)"

DEST_DIR="$WIN_PROFILE/wsl-kernel"
WSLCONFIG="$WIN_PROFILE/.wslconfig"


# ------------------------------------------------------------
# kernelModules が残っていないか確認
#
# modules.vhdx は今回一切使わない
# ------------------------------------------------------------
if [[ -f "$WSLCONFIG" ]] &&
   grep -qi '^[[:space:]]*kernelModules[[:space:]]*=' "$WSLCONFIG"
then
    echo
    echo "ERROR:"
    echo "  .wslconfig に kernelModules= が残っています。"
    echo
    echo "  $WSLCONFIG"
    echo
    echo "kernelModules= の行を削除して、"
    echo
    echo "  wsl --shutdown"
    echo
    echo "した後、Ubuntuを起動し直してこのスクリプトを再実行してください。"
    echo
    exit 1
fi


# ------------------------------------------------------------
# 最新release取得
# ------------------------------------------------------------
echo
echo "==> Finding latest WSL kernel release"

TAG="$(
    curl -fsSL "$API_URL" |
    jq -r '.tag_name'
)"

if [[ -z "$TAG" || "$TAG" == "null" ]]; then
    echo "ERROR: latest releaseを取得できません。" >&2
    exit 1
fi

echo "    $TAG"


# ------------------------------------------------------------
# Source取得
# ------------------------------------------------------------
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"

cd "$BUILD_DIR"

echo
echo "==> Cloning $TAG"

git clone \
    --depth 1 \
    --branch "$TAG" \
    "$REPO_URL" \
    src

cd src


# ------------------------------------------------------------
# Config
#
# NR_HCSだけ変更
# USBIP_CORE / VHCI_HCD は標準の =m を維持
# ------------------------------------------------------------
CONFIG_FILE="$(
    readlink -f "$CONFIG_FILE"
)"

echo
echo "==> Setting CONFIG_USBIP_VHCI_NR_HCS=$NR_HCS"

./scripts/config \
    --file "$CONFIG_FILE" \
    --set-val USBIP_VHCI_NR_HCS "$NR_HCS"

make \
    KCONFIG_CONFIG="$CONFIG_FILE" \
    olddefconfig


# ------------------------------------------------------------
# 設定確認
# ------------------------------------------------------------
echo
echo "==> Checking config"

grep -E \
    '^CONFIG_USBIP_(CORE|VHCI_HCD|VHCI_HC_PORTS|VHCI_NR_HCS)=' \
    "$CONFIG_FILE"


CORE="$(
    sed -n 's/^CONFIG_USBIP_CORE=//p' "$CONFIG_FILE"
)"

VHCI="$(
    sed -n 's/^CONFIG_USBIP_VHCI_HCD=//p' "$CONFIG_FILE"
)"

HCS="$(
    sed -n 's/^CONFIG_USBIP_VHCI_NR_HCS=//p' "$CONFIG_FILE"
)"


if [[ "$CORE" != "m" ]]; then
    echo "ERROR: CONFIG_USBIP_CORE が m ではありません。" >&2
    exit 1
fi

if [[ "$VHCI" != "m" ]]; then
    echo "ERROR: CONFIG_USBIP_VHCI_HCD が m ではありません。" >&2
    exit 1
fi

if [[ "$HCS" != "$NR_HCS" ]]; then
    echo "ERROR: CONFIG_USBIP_VHCI_NR_HCS の設定に失敗しました。" >&2
    exit 1
fi


# ------------------------------------------------------------
# Kernel build
# ------------------------------------------------------------
echo
echo "==> Building kernel"

make \
    -j"$(nproc)" \
    KCONFIG_CONFIG="$CONFIG_FILE"


# ------------------------------------------------------------
# Kernel release
# ------------------------------------------------------------
KREL="$(
    make \
        -s \
        KCONFIG_CONFIG="$CONFIG_FILE" \
        kernelrelease
)"

echo
echo "==> Kernel release"
echo "    $KREL"


if [[ ! -f "$KERNEL_IMAGE" ]]; then
    echo "ERROR: kernel image がありません。" >&2
    exit 1
fi


# ------------------------------------------------------------
# modules_install
#
# 一度 staging directory に作る
# ------------------------------------------------------------
echo
echo "==> Building kernel modules"

rm -rf "$PWD/modules"

make \
    KCONFIG_CONFIG="$CONFIG_FILE" \
    INSTALL_MOD_PATH="$PWD/modules" \
    modules_install


MODULE_SOURCE="$PWD/modules/lib/modules/$KREL"

if [[ ! -d "$MODULE_SOURCE" ]]; then
    echo "ERROR: modules_install に失敗しました。" >&2
    exit 1
fi


# ------------------------------------------------------------
# vhci-hcd.ko確認
# ------------------------------------------------------------
VHCI_KO="$(
    find "$MODULE_SOURCE" \
        -type f \
        -name 'vhci-hcd.ko*' \
        -print \
        -quit
)"

USBIP_KO="$(
    find "$MODULE_SOURCE" \
        -type f \
        -name 'usbip-core.ko*' \
        -print \
        -quit
)"

if [[ -z "$VHCI_KO" ]]; then
    echo "ERROR: vhci-hcd.ko が生成されていません。" >&2
    exit 1
fi

if [[ -z "$USBIP_KO" ]]; then
    echo "ERROR: usbip-core.ko が生成されていません。" >&2
    exit 1
fi

echo
echo "    $VHCI_KO"
echo "    $USBIP_KO"


# ------------------------------------------------------------
# /lib/modules へ直接install
#
# modules.vhdx は使わない
# ------------------------------------------------------------
MODULE_DEST="/lib/modules/$KREL"

echo
echo "==> Installing modules"
echo "    $MODULE_DEST"


# kernelModules VHDがmountされていたら止める
if mountpoint -q "$MODULE_DEST" 2>/dev/null; then
    echo
    echo "ERROR:"
    echo "  $MODULE_DEST"
    echo "  がmount pointになっています。"
    echo
    echo "古い kernelModules VHD がまだ有効です。"
    echo ".wslconfig の kernelModules= を削除して"
    echo "wsl --shutdown 後に再実行してください。"
    echo
    exit 1
fi


sudo rm -rf "$MODULE_DEST"

sudo cp -a \
    "$MODULE_SOURCE" \
    /lib/modules/

sudo rm -f \
    "$MODULE_DEST/build" \
    "$MODULE_DEST/source"

sudo depmod -a "$KREL"


# ------------------------------------------------------------
# 正しい位置に入ったことを確認
# ------------------------------------------------------------
echo
echo "==> Checking installed modules"

find "$MODULE_DEST" \
    -type f \
    \( \
        -name 'vhci-hcd.ko*' \
        -o \
        -name 'usbip-core.ko*' \
    \) \
    -print


if [[ ! -f "$MODULE_DEST/kernel/drivers/usb/usbip/vhci-hcd.ko" ]]; then
    echo
    echo "ERROR:"
    echo "vhci-hcd.ko が正しい場所にありません。"
    echo
    echo "expected:"
    echo "  $MODULE_DEST/kernel/drivers/usb/usbip/vhci-hcd.ko"
    exit 1
fi


# ------------------------------------------------------------
# Windows側へkernelコピー
# ------------------------------------------------------------
echo
echo "==> Installing kernel into Windows"

mkdir -p "$DEST_DIR"

if [[ -f "$DEST_DIR/kernel" ]]; then
    cp -a \
        "$DEST_DIR/kernel" \
        "$DEST_DIR/kernel.bak.$(date +%Y%m%d-%H%M%S)"
fi

cp -f \
    "$KERNEL_IMAGE" \
    "$DEST_DIR/kernel"


# ------------------------------------------------------------
# .wslconfig
#
# kernel= のみ使用
# kernelModules= は使用しない
# ------------------------------------------------------------
KERNEL_WIN="${WIN_PROFILE_WIN}\\wsl-kernel\\kernel"
KERNEL_CFG="${KERNEL_WIN//\\/\\\\}"


if [[ -f "$WSLCONFIG" ]]; then
    cp -a \
        "$WSLCONFIG" \
        "$WSLCONFIG.bak.$(date +%Y%m%d-%H%M%S)"
fi


python3 - \
    "$WSLCONFIG" \
    "$KERNEL_CFG" <<'PY'

import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
kernel = sys.argv[2]

if path.exists():
    lines = path.read_text(
        encoding="utf-8-sig"
    ).splitlines()
else:
    lines = []

section_re = re.compile(
    r"^\s*\[([^]]+)\]\s*$"
)

start = None

for i, line in enumerate(lines):
    m = section_re.match(line)

    if (
        m
        and
        m.group(1).strip().lower() == "wsl2"
    ):
        start = i
        break


if start is None:

    if lines and lines[-1].strip():
        lines.append("")

    lines.extend(
        [
            "[wsl2]",
            f"kernel={kernel}",
        ]
    )

else:

    end = len(lines)

    for i in range(start + 1, len(lines)):
        if section_re.match(lines[i]):
            end = i
            break

    block = lines[start + 1:end]

    new_block = []
    kernel_done = False

    for line in block:

        if re.match(
            r"^\s*kernelModules\s*=",
            line,
            re.I
        ):
            continue

        if re.match(
            r"^\s*kernel\s*=",
            line,
            re.I
        ):
            if not kernel_done:
                new_block.append(
                    f"kernel={kernel}"
                )
                kernel_done = True
            continue

        new_block.append(line)

    if not kernel_done:
        new_block.append(
            f"kernel={kernel}"
        )

    lines = (
        lines[:start + 1]
        + new_block
        + lines[end:]
    )


path.write_text(
    "\n".join(lines) + "\n",
    encoding="utf-8"
)

PY


# ------------------------------------------------------------
# 最終表示
# ------------------------------------------------------------
echo
echo "========================================"
echo " Build/install completed"
echo "========================================"
echo
echo "Kernel:"
echo "  $KREL"
echo
echo "Config:"
echo "  CONFIG_USBIP_CORE=m"
echo "  CONFIG_USBIP_VHCI_HCD=m"
echo "  CONFIG_USBIP_VHCI_NR_HCS=$NR_HCS"
echo
echo "Kernel image:"
echo "  $KERNEL_WIN"
echo
echo "Modules:"
echo "  $MODULE_DEST"
echo
echo ".wslconfig:"
cat "$WSLCONFIG"
echo
echo "========================================"
echo " インストール完了"
echo "========================================"
echo
echo "このシェルを終了したあと、"
echo "Windows の PowerShell またはコマンドプロンプトから:"
echo
echo "  wsl --shutdown"
echo
echo "を実行してください。"
echo
echo "その後、新しいターミナルでUbuntuを起動してください。"
echo

上記を実行することで最新カーネルのソースを取得して更新するところまでを自動化しています。ただ最近のバージョンでいろいろ動きが変わっているところがあるみたいなので、変更してもおかしくなったら.wslconfigのカーネル指定を削除してみてください。

まとめ

普通に使っているとWSLでUSBを同時8台以上を使うことはないと思います。しかしpytestの実機検証用サーバーには検証機材が大量にぶら下がっていて同時利用出来ない状態になってしまいました。

現状のところ無印ESP32が2台、ESP32-S3が3台、ESP32-P4が4台、WCH-LinkEが6台接続されていて、用途の応じてattachし直しているので不便になっています。そしてどれがどれだかわからなくなるので困ったものです。。。

$ ll /dev/serial/by-path/|grep v2
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:1:1.0 -> ../../ttyACM0
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:2:1.0 -> ../../ttyACM1
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:3:1.0 -> ../../ttyACM2
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:4:1.0 -> ../../ttyACM3
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:5:1.0-port0 -> ../../ttyUSB0
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:6:1.0-port0 -> ../../ttyUSB1
lrwxrwxrwx 1 root root  13 Sep 14 21:08 platform-vhci_hcd.0-usbv2-0:7:1.0-port0 -> ../../ttyUSB2
lrwxrwxrwx 1 root root  13 Sep 14 21:09 platform-vhci_hcd.0-usbv2-0:8:1.1 -> ../../ttyACM4
lrwxrwxrwx 1 root root  13 Sep 14 21:09 platform-vhci_hcd.1-usbv2-0:1:1.1 -> ../../ttyACM5
lrwxrwxrwx 1 root root  13 Sep 14 21:09 platform-vhci_hcd.1-usbv2-0:2:1.1 -> ../../ttyACM6
lrwxrwxrwx 1 root root  13 Sep 14 21:09 platform-vhci_hcd.1-usbv2-0:3:1.1 -> ../../ttyACM7
lrwxrwxrwx 1 root root  13 Sep 14 21:21 platform-vhci_hcd.1-usbv2-0:4:1.1 -> ../../ttyACM8
lrwxrwxrwx 1 root root  13 Sep 14 21:21 platform-vhci_hcd.1-usbv2-0:5:1.1 -> ../../ttyACM9

こんな感じでUSBの認識が8台を超えてできるようになりました。

コメント