Winsock のタイムスタンプ

はじめに

パケット タイムスタンプは、精度タイム プロトコルなど、多くのクロック同期アプリケーションにとって重要な機能です。 タイムスタンプの生成がネットワーク アダプター ハードウェアによってパケットの受信/送信に近いほど、同期アプリケーションの精度が高くなります。

そのため、このトピックで説明するタイムスタンプ API は、アプリケーションレイヤーの下に生成されたタイムスタンプを報告するメカニズムをアプリケーションに提供します。 具体的には、ミニポートと NDIS の間のインターフェイスにあるソフトウェア タイムスタンプと、NIC ハードウェアのハードウェア タイムスタンプ。 タイムスタンプ API を使用すると、クロック同期の精度を大幅に向上させることができます。 現在、サポートのスコープはユーザー データグラム プロトコル (UDP) ソケットです。

タイムスタンプを受信する

SIO_TIMESTAMPING IOCTL を使用して、受信タイムスタンプ受信を構成します。 その IOCTL を使用して、受信タイムスタンプの受信を有効にします。 LPFN_WSARECVMSG (WSARecvMsg) 関数を使用してデータグラムを受信すると、そのタイムスタンプ (使用可能な場合) がSO_TIMESTAMP制御メッセージに含まれます。

SO_TIMESTAMP (0x300A) は で mstcpip.h定義されます。 制御メッセージ・データは UINT64 として返されます。

送信タイムスタンプ

送信タイムスタンプ受信は、 SIO_TIMESTAMPING IOCTL を介して構成することもできます。 その IOCTL を使用して、送信タイムスタンプ受信を有効にし、システムがバッファーする送信タイムスタンプの数を指定します。 送信タイムスタンプが生成されると、バッファーに追加されます。 バッファーがいっぱいの場合、新しい送信タイムスタンプは破棄されます。

データグラムを送信するときは、データグラムを SO_TIMESTAMP_ID 制御メッセージに関連付けます。 これには一意の識別子が含まれている必要があります。 WSASendMsg を使用して、データグラムとそのSO_TIMESTAMP_ID制御メッセージを送信します。 送信タイムスタンプは、 WSASendMsg が返された直後に使用できない場合があります。 送信タイムスタンプが使用可能になると、それらはソケットごとのバッファーに配置されます。 SIO_GET_TX_TIMESTAMP IOCTL を使用して、ID でタイムスタンプをポーリングします。 タイムスタンプが使用可能な場合は、バッファーから削除されて返されます。 タイムスタンプが使用できない場合、 WSAGetLastError はWSAEWOULDBLOCK を返します。 バッファーがいっぱいの間に送信タイムスタンプが生成された場合、新しいタイムスタンプは破棄されます。

SO_TIMESTAMP_ID (0x300B) は で mstcpip.h定義されています。 制御メッセージ・データは UINT32 として指定する必要があります。

タイムスタンプは 64 ビット カウンター値として表されます。 カウンターの頻度は、タイムスタンプのソースによって異なります。 ソフトウェア タイムスタンプの場合、カウンターは QueryPerformanceCounter (QPC) 値であり、 QueryPerformanceFrequency を使用してその頻度を決定できます。 NIC ハードウェアタイムスタンプの場合、カウンターの頻度は NIC ハードウェアに依存し、 CaptureInterfaceHardwareCrossTimestamp によって提供される追加情報を使用して判断できます。 タイムスタンプのソースを確認するには、 GetInterfaceActiveTimestampCapabilities 関数と GetInterfaceSupportedTimestampCapabilities 関数を使用します。

ソケットのタイムスタンプ受信を有効にするには、 SIO_TIMESTAMPING ソケット オプションを使用したソケット レベルの構成に加えて、システム レベルの構成も必要です。

ソケット送信パスの待機時間の見積もり

このセクションでは、送信タイムスタンプを使用して、ソケット送信パスの待機時間を推定します。 アプリケーション レベルの IO タイムスタンプを使用する既存のアプリケーションがある場合 (タイムスタンプを実際の転送ポイントにできるだけ近くする必要がある場合)、このサンプルでは、Winsock タイムスタンプ API によってアプリケーションの精度がどれだけ向上できるかについての定量的な説明が提供されます。

この例では、システムにネットワーク インターフェイス カード (NIC) が 1 つだけあり、interfaceLuid がそのアダプターの LUID であることを前提としています。

void QueryHardwareClockFrequency(LARGE_INTEGER* clockFrequency)
{
    // Returns the hardware clock frequency. This can be calculated by
    // collecting crosstimestamps via CaptureInterfaceHardwareCrossTimestamp
    // and forming a linear regression model.
}

void estimate_send_latency(SOCKET sock,
    PSOCKADDR_STORAGE addr,
    NET_LUID* interfaceLuid,
    BOOLEAN hardwareTimestampSource)
{
    DWORD numBytes;
    INT error;
    CHAR data[512];
    CHAR control[WSA_CMSG_SPACE(sizeof(UINT32))] = { 0 };
    WSABUF dataBuf;
    WSABUF controlBuf;
    WSAMSG wsaMsg;
    ULONG64 appLevelTimestamp;

    dataBuf.buf = data;
    dataBuf.len = sizeof(data);
    controlBuf.buf = control;
    controlBuf.len = sizeof(control);
    wsaMsg.name = (PSOCKADDR)addr;
    wsaMsg.namelen = (INT)INET_SOCKADDR_LENGTH(addr->ss_family);
    wsaMsg.lpBuffers = &dataBuf;
    wsaMsg.dwBufferCount = 1;
    wsaMsg.Control = controlBuf;
    wsaMsg.dwFlags = 0;

    // Configure tx timestamp reception.
    TIMESTAMPING_CONFIG config = { 0 };
    config.flags |= TIMESTAMPING_FLAG_TX;
    config.txTimestampsBuffered = 1;
    error =
        WSAIoctl(
            sock,
            SIO_TIMESTAMPING,
            &config,
            sizeof(config),
            NULL,
            0,
            &numBytes,
            NULL,
            NULL);
    if (error == SOCKET_ERROR) {
        printf("WSAIoctl failed %d\n", WSAGetLastError());
        return;
    }

    // Assign a tx timestamp ID to this datagram.
    UINT32 txTimestampId = 123;
    PCMSGHDR cmsg = WSA_CMSG_FIRSTHDR(&wsaMsg);
    cmsg->cmsg_len = WSA_CMSG_LEN(sizeof(UINT32));
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SO_TIMESTAMP_ID;
    *(PUINT32)WSA_CMSG_DATA(cmsg) = txTimestampId;

    // Capture app-layer timestamp prior to send call.
    if (hardwareTimestampSource) {
        INTERFACE_HARDWARE_CROSSTIMESTAMP crossTimestamp = { 0 };
        crossTimestamp.Version = INTERFACE_HARDWARE_CROSSTIMESTAMP_VERSION_1;
        error = CaptureInterfaceHardwareCrossTimestamp(interfaceLuid, &crossTimestamp);
        if (error != NO_ERROR) {
            printf("CaptureInterfaceHardwareCrossTimestamp failed %d\n", error);
            return;
        }
        appLevelTimestamp = crossTimestamp.HardwareClockTimestamp;
    }
    else { // software source
        LARGE_INTEGER t1;
        QueryPerformanceCounter(&t1);
        appLevelTimestamp = t1.QuadPart;
    }

    error =
        sendmsg(
            sock,
            &wsaMsg,
            0,
            &numBytes,
            NULL,
            NULL);
    if (error == SOCKET_ERROR) {
        printf("sendmsg failed %d\n", WSAGetLastError());
        return;
    }

    printf("sent packet\n");

    // Poll for the socket tx timestamp value. The timestamp may not be available
    // immediately.
    UINT64 socketTimestamp;
    ULONG maxTimestampPollAttempts = 6;
    ULONG txTstampRetrieveIntervalMs = 1;
    BOOLEAN retrievedTimestamp = FALSE;
    for (ULONG i = 0; i < maxTimestampPollAttempts; i++) {
        error =
            WSAIoctl(
                sock,
                SIO_GET_TX_TIMESTAMP,
                &txTimestampId,
                sizeof(txTimestampId),
                &socketTimestamp,
                sizeof(socketTimestamp),
                &numBytes,
                NULL,
                NULL);
        if (error != SOCKET_ERROR) {
            ASSERT(numBytes == sizeof(timestamp));
            ASSERT(timestamp != 0);
            retrievedTimestamp = TRUE;
            break;
        }

        error = WSAGetLastError();
        if (error != WSAEWOULDBLOCK) {
            printf(“WSAIoctl failed % d\n”, error);
            break;
        }

        Sleep(txTstampRetrieveIntervalMs);
        txTstampRetrieveIntervalMs *= 2;
    }

    if (retrievedTimestamp) {
        LARGE_INTEGER clockFrequency;
        ULONG64 elapsedMicroseconds;

        if (hardwareTimestampSource) {
            QueryHardwareClockFrequency(&clockFrequency);
        }
        else { // software source
            QueryPerformanceFrequency(&clockFrequency);
        }

        // Compute socket send path latency.
        elapsedMicroseconds = socketTimestamp - appLevelTimestamp;
        elapsedMicroseconds *= 1000000;
        elapsedMicroseconds /= clockFrequency.QuadPart;
        printf("socket send path latency estimation: %lld microseconds\n",
            elapsedMicroseconds);
    }
    else {
        printf("failed to retrieve TX timestamp\n");
    }
}

ソケット受信パスの待機時間の見積もり

受信パスの同様のサンプルを次に示します。 この例では、システムにネットワーク インターフェイス カード (NIC) が 1 つだけあり、interfaceLuid がそのアダプターの LUID であることを前提としています。

void QueryHardwareClockFrequency(LARGE_INTEGER* clockFrequency)
{
    // Returns the hardware clock frequency. This can be calculated by
    // collecting crosstimestamps via CaptureInterfaceHardwareCrossTimestamp
    // and forming a linear regression model.
}

void estimate_receive_latency(SOCKET sock,
    NET_LUID* interfaceLuid,
    BOOLEAN hardwareTimestampSource)
{
    DWORD numBytes;
    INT error;
    CHAR data[512];
    CHAR control[WSA_CMSG_SPACE(sizeof(UINT64))] = { 0 };
    WSABUF dataBuf;
    WSABUF controlBuf;
    WSAMSG wsaMsg;
    UINT64 socketTimestamp = 0;
    ULONG64 appLevelTimestamp;

    dataBuf.buf = data;
    dataBuf.len = sizeof(data);
    controlBuf.buf = control;
    controlBuf.len = sizeof(control);
    wsaMsg.name = NULL;
    wsaMsg.namelen = 0;
    wsaMsg.lpBuffers = &dataBuf;
    wsaMsg.dwBufferCount = 1;
    wsaMsg.Control = controlBuf;
    wsaMsg.dwFlags = 0;

    // Configure rx timestamp reception.
    TIMESTAMPING_CONFIG config = { 0 };
    config.flags |= TIMESTAMPING_FLAG_RX;
    error =
        WSAIoctl(
            sock,
            SIO_TIMESTAMPING,
            &config,
            sizeof(config),
            NULL,
            0,
            &numBytes,
            NULL,
            NULL);
    if (error == SOCKET_ERROR) {
        printf("WSAIoctl failed %d\n", WSAGetLastError());
        return;
    }

    error =
        recvmsg(
            sock,
            &wsaMsg,
            &numBytes,
            NULL,
            NULL);
    if (error == SOCKET_ERROR) {
        printf("recvmsg failed %d\n", WSAGetLastError());
        return;
    }

    // Capture app-layer timestamp upon message reception.
    if (hardwareTimestampSource) {
        INTERFACE_HARDWARE_CROSSTIMESTAMP crossTimestamp = { 0 };
        crossTimestamp.Version = INTERFACE_HARDWARE_CROSSTIMESTAMP_VERSION_1;
        error = CaptureInterfaceHardwareCrossTimestamp(interfaceLuid, &crossTimestamp);
        if (error != NO_ERROR) {
            printf("CaptureInterfaceHardwareCrossTimestamp failed %d\n", error);
            return;
        }
        appLevelTimestamp = crossTimestamp.HardwareClockTimestamp;
    }
    else { // software source
        LARGE_INTEGER t1;
        QueryPerformanceCounter(&t1);
        appLevelTimestamp = t1.QuadPart;
    }

    printf("received packet\n");

    // Look for socket rx timestamp returned via control message.
    BOOLEAN retrievedTimestamp = FALSE;
    PCMSGHDR cmsg = WSA_CMSG_FIRSTHDR(&wsaMsg);
    while (cmsg != NULL) {
        if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMP) {
            socketTimestamp = *(PUINT64)WSA_CMSG_DATA(cmsg);
            retrievedTimestamp = TRUE;
            break;
        }
        cmsg = WSA_CMSG_NXTHDR(&wsaMsg, cmsg);
    }

    if (retrievedTimestamp) {
        // Compute socket receive path latency.
        LARGE_INTEGER clockFrequency;
        ULONG64 elapsedMicroseconds;

        if (hardwareTimestampSource) {
            QueryHardwareClockFrequency(&clockFrequency);
        }
        else { // software source
            QueryPerformanceFrequency(&clockFrequency);
        }

        // Compute socket send path latency.
        elapsedMicroseconds = appLevelTimestamp - socketTimestamp;
        elapsedMicroseconds *= 1000000;
        elapsedMicroseconds /= clockFrequency.QuadPart;
        printf("RX latency estimation: %lld microseconds\n",
            elapsedMicroseconds);
    }
    else {
        printf("failed to retrieve RX timestamp\n");
    }
}

制限事項

Winsock タイムスタンプ API の 1 つの制限事項は、 SIO_GET_TX_TIMESTAMP を呼び出すことは常に非ブロッキング操作であるということです。 IOCTL を OVERLAPPED 形式で呼び出しても、現在使用可能な送信タイムスタンプがない場合は 、WSAEWOULDBLOCK がすぐに返されます。 WSASendMsg が返された後、送信タイムスタンプをすぐに使用できない可能性があるため、アプリケーションは、タイムスタンプが使用可能になるまで IOCTL をポーリングする必要があります。 送信タイムスタンプ バッファーがいっぱいでないことを考えると、 WSASendMsg 呼び出しが成功した後に、送信タイムスタンプが使用可能であることが保証されます。