하드웨어 타임스탬핑 (Hardware Timestamping)
하드웨어 타임스탬핑은 NIC(Network Interface Card)가 패킷을 물리적으로 전송하거나 수신하는 시점을 PHC(PTP Hardware Clock) 카운터로 기록하는 기능입니다. 소프트웨어 타임스탬핑에 비해 OS 스케줄링 지터가 제거되어 PTP 동기화 정밀도를 나노초 수준으로 높입니다. 커널 인터페이스(SIOCSHWTSTAMP, hwtstamp_config), SOF_TIMESTAMPING_* 플래그, skb_hwtstamps 구조체, SO_TIMESTAMPING 소켓 옵션, NIC 드라이버 구현 패턴, TX 완료 타임스탬프 전달 경로를 체계적으로 정리합니다.
핵심 요약
- SIOCSHWTSTAMP — 인터페이스별 타임스탬핑 설정을 활성화하는 ioctl입니다.
hwtstamp_config로 TX/RX 타임스탬핑 모드를 지정합니다. - hwtstamp_config —
tx_type(OFF/ON/ONESTEP_SYNC)과rx_filter(PTP만, 전체 등)를 담는 설정 구조체입니다. - SOF_TIMESTAMPING_* 플래그 — 소켓에서 어떤 종류(HW/SW, TX/RX)의 타임스탬프를 요청할지 지정합니다.
- skb_hwtstamps — sk_buff에 첨부되는 하드웨어 타임스탬프 구조체입니다.
hwtstamp필드에 PHC 기준 나노초 값을 담습니다. - TX 타임스탬프 전달 경로 — TX 완료 인터럽트에서
skb_tstamp_tx()를 호출하면 소켓 에러 큐로 타임스탬프가 전달됩니다. - SO_TIMESTAMPING — 소켓에 설정하여
recvmsg()의cmsg로 타임스탬프를 받거나, TX의 경우 에러 큐(MSG_ERRQUEUE)에서 읽습니다. - SCM_TIMESTAMPING —
recvmsg()보조 데이터로 전달되는 3개짜리timespec배열(소프트웨어, 예약, 하드웨어)입니다. - ethtool -T — 인터페이스가 지원하는 타임스탬핑 기능과 PHC 인덱스를 조회합니다.
단계별 이해
- NIC 지원 여부 확인 —
ethtool -T <iface>로 TX/RX 하드웨어 타임스탬핑 지원 여부와 PHC 인덱스를 확인합니다. - SIOCSHWTSTAMP로 활성화 —
hwtstamp_config를 설정하여 원하는 TX/RX 필터를 활성화합니다. - SO_TIMESTAMPING 소켓 옵션 설정 — 소켓에 원하는 타임스탬프 플래그를 설정합니다.
- recvmsg로 타임스탬프 수신 — RX는
recvmsg()의cmsg로, TX는MSG_ERRQUEUE로 타임스탬프를 읽습니다. - 드라이버 구현 이해 — NIC 드라이버에서
skb_hwtstamps()로 타임스탬프를 채우고skb_tstamp_tx()로 TX 완료를 보고하는 경로를 파악합니다.
개요 — 타임스탬핑 계층
리눅스 네트워크 스택은 패킷 타임스탬프를 세 레이어에서 찍을 수 있습니다.
| 레이어 | 시점 | 정밀도 | 지터 원인 |
|---|---|---|---|
| 소프트웨어 (SW) | 드라이버 RX 핸들러 진입 시 | 수십 μs | IRQ 처리 지연, CPU 스케줄링 |
| NIC 드라이버 (SW-nic) | DMA 완료 직후 | 수 μs | CPU 캐시 미스 |
| 하드웨어 (HW) | MAC/PHY 레이어 패킷 경계 | 수 ns~수십 ns | PHC 클럭 해상도 |
PTP에서 나노초 수준의 정밀도를 달성하려면 반드시 하드웨어 타임스탬핑이 필요합니다.
ethtool로 지원 기능 조회
ethtool -T <인터페이스>는 NIC 드라이버의 get_ts_info()를 호출하여 하드웨어가 지원하는 타임스탬핑 기능을 나열합니다. PTP Hardware Clock 항목이 있으면 해당 인터페이스는 PHC를 가지며 /dev/ptpN으로 접근할 수 있습니다. Hardware Transmit Timestamp Modes에 on이 있어야 TX 타임스탬핑이 가능하고, Hardware Receive Filter Modes의 ptpv2-event 또는 all이 있어야 RX 타임스탬핑이 됩니다. 이 정보를 먼저 확인한 후에 SIOCSHWTSTAMP ioctl로 실제 설정을 적용합니다.
ethtool -T eth0
# 출력 예시:
# Time stamping parameters for eth0:
# Capabilities:
# hardware-transmit
# software-transmit
# hardware-receive
# software-receive
# software-system-clock
# hardware-raw-clock
# PTP Hardware Clock: 0 ← /dev/ptp0
# Hardware Transmit Timestamp Modes:
# off
# on
# onestep-sync
# Hardware Receive Filter Modes:
# none
# ptpv1-l4-event
# ptpv2-l4-event
# ptpv2-l2-event
# ptpv2-event
# all
hwtstamp_config 설정 (SIOCSHWTSTAMP)
하드웨어 타임스탬핑을 활성화하려면 SIOCSHWTSTAMP ioctl로 hwtstamp_config를 전달합니다.
#include <linux/net_tstamp.h>
#include <sys/ioctl.h>
#include <net/if.h>
int enable_hw_timestamp(const char *ifname)
{
struct ifreq ifr = {};
struct hwtstamp_config config = {};
int sock, ret;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
return -errno;
strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
/* TX: 모든 패킷에 타임스탬프 */
config.tx_type = HWTSTAMP_TX_ON;
/* RX: PTPv2 이벤트 패킷만 타임스탬프 */
config.rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
/* 또는 RX 전체 패킷 */
/* config.rx_filter = HWTSTAMP_FILTER_ALL; */
ifr.ifr_data = (void *)&config;
ret = ioctl(sock, SIOCSHWTSTAMP, &ifr);
/* config.rx_filter에 실제 적용된 필터가 반환됨 */
printf("applied rx_filter: %d\n", config.rx_filter);
close(sock);
return ret;
}
TX 타임스탬프 모드
| 값 | 이름 | 설명 |
|---|---|---|
| 0 | HWTSTAMP_TX_OFF | TX 타임스탬핑 비활성 |
| 1 | HWTSTAMP_TX_ON | 모든 패킷에 TX 타임스탬핑 |
| 2 | HWTSTAMP_TX_ONESTEP_SYNC | PTP Sync 메시지를 원패스로 처리 (Follow_Up 불필요) |
| 3 | HWTSTAMP_TX_ONESTEP_P2P | Sync + Pdelay_Resp를 원패스 처리 |
RX 필터 모드 주요 값
| 값 | 이름 | 설명 |
|---|---|---|
| 0 | HWTSTAMP_FILTER_NONE | RX 타임스탬핑 비활성 |
| 1 | HWTSTAMP_FILTER_ALL | 모든 수신 패킷 |
| 9 | HWTSTAMP_FILTER_PTP_V2_EVENT | PTPv2 이벤트 메시지 (Sync, Delay_Req 등) |
| 10 | HWTSTAMP_FILTER_PTP_V2_SYNC | PTPv2 Sync 메시지만 |
| 12 | HWTSTAMP_FILTER_PTP_V2_DELAY_REQ | PTPv2 Delay_Req만 |
SO_TIMESTAMPING 소켓 옵션
SO_TIMESTAMPING을 소켓에 설정하면 recvmsg()의 보조 데이터(cmsg)로 타임스탬프를 받을 수 있습니다.
#include <linux/net_tstamp.h>
int flags = 0;
/* 하드웨어 RX 타임스탬프 요청 */
flags |= SOF_TIMESTAMPING_RX_HARDWARE;
/* RAW 하드웨어 클럭(PHC) 기준 타임스탬프 */
flags |= SOF_TIMESTAMPING_RAW_HARDWARE;
/* TX 완료 타임스탬프 (에러 큐로 전달) */
flags |= SOF_TIMESTAMPING_TX_HARDWARE;
setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
SOF_TIMESTAMPING_* 플래그
| 플래그 | 의미 |
|---|---|
SOF_TIMESTAMPING_TX_HARDWARE | TX 하드웨어 타임스탬프 요청 |
SOF_TIMESTAMPING_TX_SOFTWARE | TX 소프트웨어 타임스탬프 요청 |
SOF_TIMESTAMPING_RX_HARDWARE | RX 하드웨어 타임스탬프 요청 |
SOF_TIMESTAMPING_RX_SOFTWARE | RX 소프트웨어 타임스탬프 요청 |
SOF_TIMESTAMPING_SOFTWARE | 시스템 시간(CLOCK_REALTIME) 기준 |
SOF_TIMESTAMPING_RAW_HARDWARE | PHC 기준 원시 하드웨어 클럭 타임스탬프 |
SOF_TIMESTAMPING_OPT_ID | TX 타임스탬프에 패킷 식별자 포함 |
SOF_TIMESTAMPING_OPT_TX_SWHW | TX SW+HW 타임스탬프 둘 다 요청 |
RX 타임스탬프 수신
수신 패킷의 타임스탬프는 recvmsg()의 ancillary data(보조 데이터)로 전달됩니다. 소켓에 SO_TIMESTAMPING 옵션을 설정하고 MSG_WAITFORONE 또는 일반 recvmsg()를 호출하면, 커널이 SCM_TIMESTAMPING cmsg 헤더와 함께 scm_timestamping 구조체를 반환합니다. 이 구조체의 세 필드(ts[0]: 소프트웨어, ts[2]: 하드웨어 RAW)에서 원하는 타임스탬프 종류를 읽습니다. 하드웨어 타임스탬프(ts[2])는 PHC 도메인의 시각이므로, CLOCK_REALTIME과의 오프셋을 보정해야 합니다.
char buf[1500];
char control[512];
struct msghdr msg = {};
struct iovec iov = { buf, sizeof(buf) };
struct cmsghdr *cmsg;
struct scm_timestamping *ts;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
ssize_t n = recvmsg(sock, &msg, 0);
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET &&
cmsg->cmsg_type == SCM_TIMESTAMPING) {
ts = (struct scm_timestamping *)CMSG_DATA(cmsg);
/* ts->ts[0]: 소프트웨어 타임스탬프 (CLOCK_REALTIME) */
/* ts->ts[1]: 예약 (항상 0) */
/* ts->ts[2]: 하드웨어 원시 타임스탬프 (PHC) */
printf("HW RX timestamp: %ld.%09ld\n",
ts->ts[2].tv_sec, ts->ts[2].tv_nsec);
}
}
TX 타임스탬프 수신 (에러 큐)
TX 타임스탬프는 패킷 전송 완료 후 소켓 에러 큐로 전달됩니다.
/* TX 타임스탬프를 위해 MSG_ERRQUEUE에서 읽기 */
char control[512];
struct msghdr msg = {};
struct iovec iov = { buf, sizeof(buf) };
struct cmsghdr *cmsg;
struct scm_timestamping *ts;
struct sock_extended_err *serr;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
/* sendmsg() 후 잠시 대기 (비동기) */
ssize_t n = recvmsg(sock, &msg, MSG_ERRQUEUE);
if (n < 0 && errno == EAGAIN) {
/* 아직 TX 완료 안 됨 */
return;
}
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET &&
cmsg->cmsg_type == SCM_TIMESTAMPING) {
ts = (struct scm_timestamping *)CMSG_DATA(cmsg);
printf("HW TX timestamp: %ld.%09ld\n",
ts->ts[2].tv_sec, ts->ts[2].tv_nsec);
}
/* SOF_TIMESTAMPING_OPT_ID 사용 시 패킷 ID */
if (cmsg->cmsg_level == SOL_IP &&
cmsg->cmsg_type == IP_RECVERR) {
serr = (struct sock_extended_err *)CMSG_DATA(cmsg);
printf("TX pkt id: %u\n", serr->ee_data);
}
}
커널 내 타임스탬프 경로
커널에서 타임스탬프는 struct sk_buff에 부착됩니다. RX 경로에서는 NIC 드라이버가 DMA 완료 후 skb_hwtstamps(skb)->hwtstamp에 PHC 값을 기록하고 스택으로 전달합니다. TX 경로에서는 소켓 레이어가 SKBTX_HW_TSTAMP 플래그를 설정하고 ndo_start_xmit이 DMA에 제출한 뒤, TX 완료 인터럽트에서 skb_tstamp_tx()를 호출하여 타임스탬프를 소켓 에러 큐에 복사합니다. 이 메커니즘을 통해 유저스페이스는 recvmsg(MSG_ERRQUEUE)로 TX 완료 시각을 나노초 단위로 확인합니다.
RX 경로 — NIC → 소켓
/* NIC 드라이버 RX 핸들러 (drivers/net/ethernet/*/xxxx.c) */
static void my_nic_rx_complete(struct my_nic_priv *priv, struct sk_buff *skb)
{
struct skb_shared_hwtstamps *hwts = skb_hwtstamps(skb);
u64 ns;
/* 하드웨어 타임스탬프 레지스터에서 읽기 */
ns = my_nic_read_rx_timestamp(priv);
/* ktime_t로 변환하여 skb에 저장 */
hwts->hwtstamp = ns_to_ktime(ns);
/* 스택으로 전달 */
napi_gro_receive(&priv->napi, skb);
}
TX 경로 — 소켓 → NIC → 완료 통보
/* net_device_ops.ndo_start_xmit */
static netdev_tx_t my_nic_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct my_nic_priv *priv = netdev_priv(dev);
/* TX 타임스탬프가 요청된 패킷인지 확인 */
if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP))
skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
/* DMA에 패킷 제출 */
my_nic_submit_tx(priv, skb);
return NETDEV_TX_OK;
}
/* TX 완료 인터럽트 핸들러 */
static void my_nic_tx_complete(struct my_nic_priv *priv, int idx)
{
struct sk_buff *skb = priv->tx_ring[idx].skb;
struct skb_shared_hwtstamps hwts = {};
if (skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS) {
u64 ns = my_nic_read_tx_timestamp(priv, idx);
hwts.hwtstamp = ns_to_ktime(ns);
/* 소켓 에러 큐로 타임스탬프 전달 */
skb_tstamp_tx(skb, &hwts);
}
dev_kfree_skb_any(skb);
}
ndo_get_tstamp — 지연 타임스탬프 읽기
일부 NIC는 TX 완료 인터럽트가 아닌 별도 큐에서 타임스탬프를 제공합니다. 이 경우 ndo_get_tstamp()를 구현합니다.
/* netdev_ops */
static int my_nic_get_tstamp(struct net_device *dev,
const struct skb_shared_info *shinfo,
struct kernel_hwtstamps *hwtstamps)
{
struct my_nic_priv *priv = netdev_priv(dev);
u64 ns;
if (!my_nic_ts_ready(priv, shinfo->tskey))
return -EAGAIN; /* 아직 준비 안 됨 */
ns = my_nic_read_deferred_ts(priv, shinfo->tskey);
hwtstamps->hwtstamp = ns_to_ktime(ns);
return 0;
}
ethtool_ops 타임스탬핑 구현
NIC 드라이버가 하드웨어 타임스탬핑을 지원하려면 ethtool_ops에 두 콜백을 구현해야 합니다. get_ts_info()는 드라이버가 지원하는 타임스탬프 플래그 세트와 연결된 PHC 인덱스를 ethtool -T와 소켓 API에 알립니다. get_set_hwtstamp() (또는 ndo_hwtstamp_set)는 SIOCSHWTSTAMP ioctl을 받아 하드웨어에 TX/RX 필터 설정을 적용합니다. 이 두 함수가 정확하게 구현되어야 ptp4l과 애플리케이션이 올바른 타임스탬프를 얻습니다.
static int my_nic_get_ts_info(struct net_device *dev,
struct kernel_ethtool_ts_info *info)
{
struct my_nic_priv *priv = netdev_priv(dev);
info->so_timestamping =
SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_RX_SOFTWARE |
SOF_TIMESTAMPING_SOFTWARE |
SOF_TIMESTAMPING_RAW_HARDWARE;
if (priv->ptp_clock)
info->phc_index = ptp_clock_index(priv->ptp_clock);
else
info->phc_index = -1;
info->tx_types =
BIT(HWTSTAMP_TX_OFF) |
BIT(HWTSTAMP_TX_ON) |
BIT(HWTSTAMP_TX_ONESTEP_SYNC);
info->rx_filters =
BIT(HWTSTAMP_FILTER_NONE) |
BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
BIT(HWTSTAMP_FILTER_ALL);
return 0;
}
static int my_nic_hwtstamp_set(struct net_device *dev,
struct kernel_hwtstamp_config *config,
struct netlink_ext_ack *extack)
{
struct my_nic_priv *priv = netdev_priv(dev);
priv->hwts_tx_type = config->tx_type;
priv->hwts_rx_filter = config->rx_filter;
/* 하드웨어에 설정 적용 */
my_nic_configure_hwtstamp(priv);
/* 실제 적용된 필터 반환 */
config->rx_filter = priv->hwts_rx_filter;
return 0;
}
One-Step 타임스탬핑
2-step 모드에서는 Sync 패킷 전송 후 Follow_Up 패킷으로 TX 타임스탬프를 별도 전달합니다. One-step 모드는 하드웨어가 Sync 패킷을 전송하는 동시에 패킷 본문에 타임스탬프를 인라인으로 기록하여 Follow_Up 패킷을 없앱니다.
/* one-step TX: 드라이버에서 패킷 body의 correctionField를 직접 업데이트 */
if (hwtstamp_config.tx_type == HWTSTAMP_TX_ONESTEP_SYNC) {
/* PTP 헤더의 correctionField 오프셋 = 8 */
ptp_header = skb->data + skb_network_offset(skb) + /* IP/UDP 오프셋 */;
correction = my_nic_get_egress_latency(priv);
put_unaligned_be64(correction, ptp_header + 8);
}
NIC 드라이버 완전 구현 패턴
NIC 드라이버가 하드웨어 타임스탬핑을 완전히 지원하려면 ndo_set_tstamp, ethtool_ops.get_ts_info, TX 완료 인터럽트에서의 타임스탬프 읽기, PTP 클럭 등록을 모두 구현해야 합니다.
ndo_set_tstamp 구현
static int my_nic_set_tstamp(struct net_device *ndev,
struct ifreq *ifr)
{
struct my_nic_priv *priv = netdev_priv(ndev);
struct hwtstamp_config cfg;
if (copy_from_user(&cfg, ifr->ifr_data, sizeof(cfg)))
return -EFAULT;
switch (cfg.tx_type) {
case HWTSTAMP_TX_OFF:
priv->hwts_tx_en = false;
break;
case HWTSTAMP_TX_ON:
priv->hwts_tx_en = true;
break;
case HWTSTAMP_TX_ONESTEP_SYNC:
priv->hwts_tx_en = true;
priv->onestep_en = true;
break;
default:
return -ERANGE;
}
switch (cfg.rx_filter) {
case HWTSTAMP_FILTER_NONE:
priv->hwts_rx_en = false;
cfg.rx_filter = HWTSTAMP_FILTER_NONE;
break;
case HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
case HWTSTAMP_FILTER_PTP_V2_EVENT:
case HWTSTAMP_FILTER_ALL:
priv->hwts_rx_en = true;
cfg.rx_filter = HWTSTAMP_FILTER_ALL;
break;
default:
return -ERANGE;
}
/* 하드웨어에 설정 적용 */
my_nic_hw_set_tstamp(priv, priv->hwts_tx_en, priv->hwts_rx_en);
return copy_to_user(ifr->ifr_data, &cfg, sizeof(cfg)) ? -EFAULT : 0;
}
TX 완료 인터럽트에서 타임스탬프 읽기
static void my_nic_tx_complete(struct my_nic_priv *priv,
struct sk_buff *skb)
{
struct skb_shared_hwtstamps hwts = {};
if (priv->hwts_tx_en && (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)) {
/* 하드웨어 TX 타임스탬프 레지스터 읽기 */
u64 ns = my_nic_read_tx_timestamp(priv);
hwts.hwtstamp = ns_to_ktime(ns);
/* skb를 에러 큐로 반환 (소켓에 MSG_ERRQUEUE로 전달됨) */
skb_tstamp_tx(skb, &hwts);
}
}
RX 경로 타임스탬프 기록
static void my_nic_rx_timestamp(struct my_nic_priv *priv,
struct sk_buff *skb,
struct my_nic_rx_desc *desc)
{
if (priv->hwts_rx_en && (desc->flags & MY_RX_HW_TSTAMP)) {
struct skb_shared_hwtstamps *hwts = skb_hwtstamps(skb);
/* DMA 디스크립터에서 타임스탬프 읽기 */
hwts->hwtstamp = ns_to_ktime(le64_to_cpu(desc->timestamp));
}
}
ethtool get_ts_info
static int my_nic_get_ts_info(struct net_device *ndev,
struct ethtool_ts_info *info)
{
struct my_nic_priv *priv = netdev_priv(ndev);
info->so_timestamping =
SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_RX_SOFTWARE |
SOF_TIMESTAMPING_RAW_HARDWARE;
if (priv->ptp_clock)
info->phc_index = ptp_clock_index(priv->ptp_clock);
else
info->phc_index = -1;
info->tx_types =
BIT(HWTSTAMP_TX_OFF) |
BIT(HWTSTAMP_TX_ON) |
BIT(HWTSTAMP_TX_ONESTEP_SYNC);
info->rx_filters =
BIT(HWTSTAMP_FILTER_NONE) |
BIT(HWTSTAMP_FILTER_PTP_V2_L4_EVENT) |
BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
BIT(HWTSTAMP_FILTER_ALL);
return 0;
}
문제 해결
| 증상 | 원인 | 진단/해결 |
|---|---|---|
| HW 타임스탬프가 0으로 나옴 | SIOCSHWTSTAMP 미설정 | HWTSTAMP_TX_ON/HWTSTAMP_FILTER_ALL 설정 |
| TX 타임스탬프가 오지 않음 | MSG_ERRQUEUE로 읽지 않음 | recvmsg(, MSG_ERRQUEUE) 확인 |
| ptp4l "tx timestamp timeout" | TX 완료 타임스탬프 지연 | tx_timestamp_timeout 증가, 드라이버 TX TS 구현 확인 |
| ethtool -T에 phc_index -1 | PHC 미등록 | 드라이버의 ptp_clock_register() 확인 |
| 타임스탬프 정밀도 μs 수준 | 소프트웨어 타임스탬핑 사용 중 | SOF_TIMESTAMPING_RAW_HARDWARE + HW filter 확인 |