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 を呼び出しても、現在使用可能な送信タイムスタンプがない場合は、WSAEWOULDBLOCK 直ちに戻ります。 WSASendMsgが返直後に送信タイムスタンプを使用できない可能性があるため、アプリケーションは、タイムスタンプが使用可能になるまで IOCTL をポーリングする必要があります。 送信タイムスタンプ バッファーがいっぱいでないことを考えると、WSASendMsg 呼び出し 成功した後は、送信タイムスタンプが使用可能であることが保証されます。