3.8 硬件时间戳详解:纳秒级精度的魔法
时间戳精度的重要性
在PTP同步中,精度取决于时间戳精度:
时间戳精度与同步精度:
软件时间戳:
- 在内核协议栈打时间戳
- 经过网络协议栈处理
- 延迟不确定(调度、中断等)
- 精度:微秒级(100-1000 ns误差)
- 适用:普通场景
硬件时间戳:
- 在网卡PHY打时间戳
- 最接近物理传输
- 延迟固定且极小
- 精度:纳秒级(<10 ns误差)
- 适用:工业、电信等高精度场景
差距有多大?
- 软件:±1微秒 → 同步精度±10微秒
- 硬件:±10纳秒 → 同步精度±100纳秒
- 差100倍!
时间戳类型
timestamp_type枚举
c
/* util.h中定义 */
enum timestamp_type {
TS_SOFTWARE, /* 软件时间戳 */
TS_HARDWARE, /* 硬件时间戳 */
TS_LEGACY_HW, /* 旧版硬件时间戳 */
TS_ONESTEP, /* 单步时钟(Sync) */
TS_P2P1STEP, /* 单步时钟(P2P) */
};
五种时间戳类型:
TS_SOFTWARE:
- 内核在协议栈处理时打时间戳
- 使用系统时钟(CLOCK_REALTIME)
- 配置:SOF_TIMESTAMPING_TX_SOFTWARE
- 精度:微秒级
TS_HARDWARE:
- 网卡PHY硬件打时间戳
- 使用PHC时钟
- 配置:SOF_TIMESTAMPING_TX_HARDWARE
- 精度:纳秒级
- 需要硬件支持
TS_LEGACY_HW:
- 旧版硬件时间戳实现
- 某些老网卡使用
- 配置:SOF_TIMESTAMPING_SYS_HARDWARE
- 精度:亚微秒级
TS_ONESTEP:
- 单步时钟(One-Step Clock)
- 发送Sync时自动修改报文时间戳
- 不需要Follow_Up消息
- 需要硬件支持(HWTSTAMP_TX_ONESTEP_SYNC)
TS_P2P1STEP:
- 单步时钟用于P2P延迟测量
- 发送Pdelay_Resp时自动修改时间戳
- 需要硬件支持(HWTSTAMP_TX_ONESTEP_P2P)
ts_str函数
c
/* util.c, 第74行 */
const char *ts_str(enum timestamp_type ts)
{
switch (ts) {
case TS_SOFTWARE:
return "software";
case TS_HARDWARE:
return "hardware";
case TS_LEGACY_HW:
return "legacy_hwtstamp";
case TS_ONESTEP:
return "onestep";
case TS_P2P1STEP:
return "p2p1step";
default:
return "unknown";
}
}
Linux时间戳子系统
SO_TIMESTAMPING标志
c
/* <linux/net_tstamp.h> */
/* 软件时间戳 */
#define SOF_TIMESTAMPING_TX_SOFTWARE (1<<0)
#define SOF_TIMESTAMPING_RX_SOFTWARE (1<<1)
#define SOF_TIMESTAMPING_SOFTWARE (1<<2)
/* 硬件时间戳 */
#define SOF_TIMESTAMPING_TX_HARDWARE (1<<3)
#define SOF_TIMESTAMPING_RX_HARDWARE (1<<4)
#define SOF_TIMESTAMPING_RAW_HARDWARE (1<<6) /* 真正的硬件时间戳 */
#define SOF_TIMESTAMPING_SYS_HARDWARE (1<<5) /* legacy硬件时间戳 */
/* 其他标志 */
#define SOF_TIMESTAMPING_OPT_TSONESTEP (1<<11) /* 单步时钟 */
#define SOF_TIMESTAMPING_BIND_PHC (1<<15) /* 绑定虚拟PHC */
标志组合:
c
/* 软件时间戳配置 */
flags = SOF_TIMESTAMPING_TX_SOFTWARE | /* 发送软件时间戳 */
SOF_TIMESTAMPING_RX_SOFTWARE | /* 接收软件时间戳 */
SOF_TIMESTAMPING_SOFTWARE; /* 报告软件时间戳 */
/* 硬件时间戳配置 */
flags = SOF_TIMESTAMPING_TX_HARDWARE | /* 发送硬件时间戳 */
SOF_TIMESTAMPING_RX_HARDWARE | /* 接收硬件时间戳 */
SOF_TIMESTAMPING_RAW_HARDWARE; /* 报告原始硬件时间戳 */
sk_timestamping_init详解
c
/* sk.c, 第560-650行 - 简化版 */
int sk_timestamping_init(int fd, const char *device, enum timestamp_type type,
enum transport_type transport, int vclock)
{
int flags;
/* 根据时间戳类型设置标志 */
switch (type) {
case TS_SOFTWARE:
flags = SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_RX_SOFTWARE |
SOF_TIMESTAMPING_SOFTWARE;
break;
case TS_HARDWARE:
case TS_ONESTEP:
case TS_P2P1STEP:
flags = SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_RAW_HARDWARE;
break;
case TS_LEGACY_HW:
flags = SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_SYS_HARDWARE;
break;
}
/* 硬件时间戳需要额外配置驱动 */
if (type != TS_SOFTWARE) {
/* 配置网卡驱动 */
hwts_init(fd, device, filter, tx_type);
}
/* 绑定虚拟时钟 */
if (vclock >= 0)
flags |= SOF_TIMESTAMPING_BIND_PHC;
/* 设置socket选项 */
struct so_timestamping timestamping;
timestamping.flags = flags;
timestamping.bind_phc = vclock;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING,
×tamping, sizeof(timestamping));
/* 配置错误队列选择 */
setsockopt(fd, SOL_SOCKET, SO_SELECT_ERR_QUEUE, &flags, sizeof(flags));
return 0;
}
硬件时间戳驱动配置
hwtstamp_config结构
c
/* <linux/net_tstamp.h> */
struct hwtstamp_config {
int flags; /* 配置标志 */
int tx_type; /* 发送时间戳类型 */
int rx_filter; /* 接收时间戳过滤 */
};
发送时间戳类型
c
/* 发送时间戳类型 */
enum hwtstamp_tx_types {
HWTSTAMP_TX_OFF, /* 不打发送时间戳 */
HWTSTAMP_TX_ON, /* 普通硬件时间戳 */
HWTSTAMP_TX_ONESTEP_SYNC, /* 单步时钟(Sync消息) */
HWTSTAMP_TX_ONESTEP_P2P, /* 单步时钟(P2P消息) */
};
发送类型详解:
HWTSTAMP_TX_OFF:
- 网卡不打发送时间戳
- 用于只接收时间戳的场景
- 或者软件时间戳
HWTSTAMP_TX_ON:
- 网卡打发送时间戳
- 时间戳存入错误队列
- 用户态用recvmsg(MSG_ERRQUEUE)读取
- 需要Follow_Up消息携带时间戳
HWTSTAMP_TX_ONESTEP_SYNC:
- 单步时钟
- 网卡在发送Sync时直接修改报文
- 将时间戳写入PTP消息的originTimestamp字段
- 不需要Follow_Up消息
- 硬件要求极高
HWTSTAMP_TX_ONESTEP_P2P:
- P2P单步时钟
- 网卡修改Pdelay_Resp的时间戳
- 将receiveTimestamp写入报文
- 不需要Pdelay_Resp_Follow_Up
接收时间戳过滤
c
/* 接收时间戳过滤 */
enum hwtstamp_rx_filters {
HWTSTAMP_FILTER_NONE, /* 不接收时间戳 */
/* 普通时间戳过滤 */
HWTSTAMP_FILTER_ALL, /* 所有报文 */
HWTSTAMP_FILTER_SOME, /* 部分报文 */
/* PTP专用过滤 */
HWTSTAMP_FILTER_PTP_V1_L4_EVENT, /* PTPv1 UDP */
HWTSTAMP_FILTER_PTP_V2_L4_EVENT, /* PTPv2 UDP */
HWTSTAMP_FILTER_PTP_V2_L2_EVENT, /* PTPv2 以太网 */
HWTSTAMP_FILTER_PTP_V2_EVENT, /* PTPv2 所有 */
/* 其他协议 */
HWTSTAMP_FILTER_NTP_ALL, /* NTP报文 */
};
过滤类型详解:
为什么需要过滤?
网卡时间戳资源有限:
- PHY硬件时间戳缓冲区有限
- 打所有报文时间戳会消耗资源
- 只给PTP报文打时间戳更高效
过滤级别:
HWTSTAMP_FILTER_NONE:
- 不给任何报文打时间戳
- 用于只发送时间戳的场景
HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
- 只给PTPv2 UDP事件消息打时间戳
- EtherType=IPv4/IPv6, UDP port=319
- 最常用的配置
HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
- 只给PTPv2以太网事件消息打时间戳
- EtherType=0x88F7
- 原始以太网传输
HWTSTAMP_FILTER_PTP_V2_EVENT:
- 给所有PTPv2事件消息打时间戳
- UDP和以太网都包含
- 最通用
HWTSTAMP_FILTER_ALL:
- 给所有报文打时间戳
- 最消耗资源
- 用于调试或特殊场景
hwts_init函数
c
/* sk.c, 第59-136行 */
static int hwts_init(int fd, const char *device, int rx_filter,
int rx_filter2, int tx_type)
{
struct ifreq ifreq;
struct hwtstamp_config cfg;
int err;
init_ifreq(&ifreq, &cfg, device);
/* 检查VLAN over bond支持 */
cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX;
err = ioctl(fd, SIOCGHWTSTAMP, &ifreq);
if (err < 0) {
if (errno == EINVAL || errno == EOPNOTSUPP) {
init_ifreq(&ifreq, &cfg, device);
} else {
pr_err("ioctl SIOCGHWTSTAMP failed: %m");
return err;
}
}
switch (sk_hwts_filter_mode) {
case HWTS_FILTER_CHECK:
/* 只检查,不修改 */
err = ioctl(fd, SIOCGHWTSTAMP, &ifreq);
if (err < 0) {
pr_err("ioctl SIOCGHWTSTAMP failed: %m");
return err;
}
break;
case HWTS_FILTER_FULL:
/* 全量过滤 */
cfg.tx_type = tx_type;
cfg.rx_filter = HWTSTAMP_FILTER_ALL;
err = ioctl(fd, SIOCSHWTSTAMP, &ifreq);
if (err < 0) {
pr_err("ioctl SIOCSHWTSTAMP failed: %m");
return err;
}
break;
case HWTS_FILTER_NORMAL:
/* 正常过滤,fallback机制 */
cfg.tx_type = tx_type;
cfg.rx_filter = rx_filter;
err = ioctl(fd, SIOCSHWTSTAMP, &ifreq);
if (err < 0) {
pr_info("driver rejected most general HWTSTAMP filter");
/* 尝试备用过滤 */
init_ifreq(&ifreq, &cfg, device);
cfg.tx_type = tx_type;
cfg.rx_filter = rx_filter2;
err = ioctl(fd, SIOCSHWTSTAMP, &ifreq);
if (err < 0) {
pr_err("ioctl SIOCSHWTSTAMP failed: %m");
return err;
}
}
break;
}
/* 验证配置 */
if (cfg.tx_type != tx_type ||
(cfg.rx_filter != rx_filter && cfg.rx_filter != rx_filter2)) {
pr_err("The current filter does not match the required");
return -1;
}
return 0;
}
SIOCSHWTSTAMP ioctl:
SIOCSHWTSTAMP:设置硬件时间戳配置
struct ifreq {
char ifr_name[IFNAMSIZ]; /* 网卡名称 */
void *ifr_data; /* 指向hwtstamp_config */
};
调用流程:
1. 构造hwtstamp_config
2. 设置tx_type和rx_filter
3. ioctl(fd, SIOCSHWTSTAMP, &ifreq)
4. 网卡驱动接收配置
5. 驱动配置PHY芯片
6. PHY开始给指定报文打时间戳
注意:
- 每个网卡只能有一个配置
- 多个进程不能同时配置
- 配置后需要重新配置才能修改
Fallback机制:
c
/* 过滤配置的fallback */
filter1 = HWTSTAMP_FILTER_PTP_V2_EVENT; /* 最通用 */
filter2 = HWTSTAMP_FILTER_PTP_V2_L4_EVENT; /* UDP专用 */
尝试filter1:
- 如果成功,使用最通用过滤
- 如果失败(驱动不支持),尝试filter2
为什么需要fallback?
- 不同网卡支持的过滤不同
- 有些网卡只支持L4过滤
- 有些网卡只支持L2过滤
- 需要根据实际情况选择
时间戳获取
时间戳数组结构
c
/* SO_TIMESTAMPING返回三个时间戳 */
struct timespec ts[3];
ts[0]:软件时间戳(SOF_TIMESTAMPING_SOFTWARE)
- 内核协议栈处理时的时间
- 使用系统时钟
ts[1]:系统硬件时间戳(SOF_TIMESTAMPING_SYS_HARDWARE)
- legacy硬件时间戳
- 经过内核转换的时间戳
ts[2]:原始硬件时间戳(SOF_TIMESTAMPING_RAW_HARDWARE)
- PHY直接提供的时间戳
- 使用PHC时钟
- 最高精度
sk_receive解析时间戳
c
/* sk.c中的时间戳解析 */
for (cm = CMSG_FIRSTHDR(&msg); cm != NULL; cm = CMSG_NXTHDR(&msg, cm)) {
level = cm->cmsg_level;
type = cm->cmsg_type;
if (SOL_SOCKET == level && SO_TIMESTAMPING == type) {
ts = (struct timespec *) CMSG_DATA(cm);
}
if (SOL_SOCKET == level && SO_TIMESTAMPNS == type) {
sw = (struct timespec *) CMSG_DATA(cm);
hwts->sw = timespec_to_tmv(*sw);
}
}
/* 根据类型选择时间戳 */
switch (hwts->type) {
case TS_SOFTWARE:
hwts->ts = timespec_to_tmv(ts[0]);
break;
case TS_HARDWARE:
case TS_ONESTEP:
case TS_P2P1STEP:
hwts->ts = timespec_to_tmv(ts[2]);
break;
case TS_LEGACY_HW:
hwts->ts = timespec_to_tmv(ts[1]);
break;
}
控制消息详解:
recvmsg的控制消息(ancillary data):
msg.msg_control包含控制消息
控制消息格式:
struct cmsghdr {
size_t cmsg_len; /* 数据长度 */
int cmsg_level; /* 协议层(SOL_SOCKET) */
int cmsg_type; /* 类型(SO_TIMESTAMPING) */
/* 后面是数据 */
};
遍历控制消息:
CMSG_FIRSTHDR(&msg):第一个控制消息
CMSG_NXTHDR(&msg, cm):下一个控制消息
CMSG_DATA(cm):数据指针
SO_TIMESTAMPING数据:
- 包含3个struct timespec
- 总长度:3 * sizeof(struct timespec)
发送时间戳获取
MSG_ERRQUEUE机制
c
/* 发送时间戳从错误队列获取 */
if (flags == MSG_ERRQUEUE) {
struct pollfd pfd = { fd, sk_events, 0 };
res = poll(&pfd, 1, sk_tx_timeout);
if (res < 0 && errno == EINTR)
res = poll(&pfd, 1, sk_tx_timeout);
if (res < 0) {
pr_err("poll for tx timestamp failed: %m");
return -errno;
} else if (!res) {
pr_err("timed out while polling for tx timestamp");
errno = ETIME;
return -1;
}
}
cnt = recvmsg(fd, &msg, MSG_ERRQUEUE);
错误队列原理:
发送时间戳的工作流程:
1. 应用层发送报文
sendto(fd, buf, len, 0, &addr, sizeof(addr))
2. 报文经过内核协议栈
→ 到达网卡驱动
3. 网卡发送报文
→ PHY硬件打时间戳
4. 时间戳存入socket错误队列
→ 不是真正的"错误"
→ 只是借用错误队列机制
5. 应用层从错误队列读取
recvmsg(fd, &msg, MSG_ERRQUEUE)
注意:
- MSG_ERRQUEUE不是接收新报文
- 是读取之前发送报文的时间戳
- 报文内容作为"错误信息"返回
poll等待时间戳
c
/* 发送后需要等待时间戳可用 */
res = poll(&pfd, 1, sk_tx_timeout);
为什么需要poll?
时间戳生成延迟:
- PHY打时间戳需要时间
- 时间戳需要传回内核
- 内核需要放入错误队列
典型延迟:
- 网卡发送:几微秒
- PHY打时间戳:<100纳秒
- 传回内核:几微秒
- 总延迟:10-100微秒
sk_tx_timeout:
- 默认值:1毫秒
- 可配置(tx_timestamp_timeout)
- 如果超时,可能是驱动bug
单步时钟(One-Step Clock)
单步时钟原理
普通时钟(Two-Step):
1. 主时钟发送Sync
2. 主时钟记录发送时间t1
3. 主时钟发送Follow_Up携带t1
4. 从时钟接收Sync(时间t2)
5. 从时钟接收Follow_Up获得t1
单步时钟(One-Step):
1. 主时钟构造Sync(originTimestamp=0)
2. 网卡发送时硬件修改originTimestamp
3. 直接填入实际发送时间t1
4. 不需要Follow_Up消息
5. 从时钟接收Sync直接获得t1和t2
优势:
- 减少一个消息
- 降低网络负载
- 减少延迟(t1更精确)
- 简化协议栈
挑战:
- 网卡必须在发送时修改报文
- 需要硬件支持
- 时间戳必须在PHY发送前计算
- 技术难度极高
One-Step实现
c
/* port.c,第1831-1866行(简化示例) */
/* 检查是否需要设置TWO_STEP标志 */
if (p->timestamping != TS_ONESTEP && p->timestamping != TS_P2P1STEP) {
msg->header.flagField[0] |= TWO_STEP;
}
/* 发送Sync */
err = port_prepare_and_send(p, msg, event);
if (err) {
pr_err("%s: send sync failed", p->log_name);
goto out;
}
/* 单步时钟:硬件自动完成,无需Follow_Up */
if (p->timestamping == TS_ONESTEP || p->timestamping == TS_P2P1STEP) {
goto out;
} else if (msg_sots_missing(msg)) {
pr_err("missing timestamp on transmitted sync");
err = -1;
goto out;
}
/* 两步时钟:发送Follow_Up */
fup->follow_up.preciseOriginTimestamp = tmv_to_Timestamp(msg->hwts.ts);
err = port_prepare_and_send(p, fup, TRANS_GENERAL);
硬件修改过程:
单步时钟的硬件操作:
1. 应用层构造Sync报文
originTimestamp = 0(或预估值)
2. 报文传给网卡驱动
3. 驱动配置PHY
HWTSTAMP_TX_ONESTEP_SYNC
4. PHY发送前:
- 读取PHC时间
- 修改报文的originTimestamp字段
- 计算UDP校验和(如果需要)
- 发送报文
5. 报文发出
originTimestamp = 实际发送时间
硬件修改位置:
- PTP消息头中
- originTimestamp字段(10字节)
- seconds字段(6字节)+ nanoseconds字段(4字节)
UDP校验和修正
c
/* udp.c中的处理 */
if (event == TRANS_ONESTEP)
len += 2; /* 为UDP校验修正扩展 */
单步时钟的UDP挑战:
UDP校验和覆盖:
- UDP头部
- UDP数据(PTP消息)
- IPv6 pseudo-header
修改PTP消息后:
- UDP校验和失效
- 需要重新计算
硬件处理方法:
1. 发送前预留2字节
2. PHY计算校验和修正值
3. 填入预留字节
4. 实际UDP校验和 = 原校验和 + 修正值
DP83640等PHY芯片:
- 支持自动UDP校验修正
- 需要预留空间
- len += 2
虚拟时钟(Virtual Clock)
vclock概念
c
/* missing.h, 第70-79行 */
enum {
SOF_TIMESTAMPING_BIND_PHC = (1 << 15),
};
struct so_timestamping {
int flags;
int bind_phc; /* 虚拟时钟索引 */
};
虚拟PHC:
Linux 5.18引入虚拟时钟:
背景:
- 一个物理网卡可以有多个虚拟PHC
- 每个虚拟PHC独立运行
- 用于多域PTP
虚拟时钟索引:
- bind_phc = 0:物理PHC(默认)
- bind_phc = 1:第一个虚拟PHC
- bind_phc = 2:第二个虚拟PHC
作用:
- 时间戳绑定到指定虚拟时钟
- 发送报文使用指定虚拟时钟的时间
- 接收报文使用指定虚拟时钟的时间
配置:
vclock = interface_get_vclock(iface);
if (vclock >= 0)
flags |= SOF_TIMESTAMPING_BIND_PHC;
timestamping.bind_phc = vclock;
多域PTP
虚拟时钟的应用场景:
场景1:电信网络
- 同一物理网络承载多个域
- 域1:频率同步(1588v2)
- 域2:相位同步(1588-2019)
- 使用不同虚拟时钟
场景2:数据中心
- 多个租户共享网络
- 每个租户独立时钟域
- 虚拟时钟隔离
配置示例:
# 创建虚拟时钟
$ echo 2 > /sys/class/net/eth0/device/vclocks
# 查看
$ ls /sys/class/net/eth0/device/ptp/
ptp0/ ptp1/ ptp2/
# ptp0:物理时钟
# ptp1, ptp2:虚拟时钟
# ptp4l使用虚拟时钟
ptp4l -i eth0 --phc_index 1
时间戳延迟补偿
ingressLatency和egressLatency
c
/* port.c中的延迟补偿 */
p->rx_timestamp_offset = config_get_int(cfg, p->name, "ingressLatency");
p->rx_timestamp_offset <<= 16; /* 转换为nanoseconds_scaled */
p->tx_timestamp_offset = config_get_int(cfg, p->name, "egressLatency");
p->tx_timestamp_offset <<= 16;
/* 接收时减去延迟 */
ts_add(&msg->hwts.ts, -p->rx_timestamp_offset);
/* 发送时加上延迟 */
ts_add(&msg->hwts.ts, p->tx_timestamp_offset);
延迟补偿原理:
时间戳延迟:
接收延迟(ingressLatency):
PHY打时间戳 → MAC → DMA → 内核 → 应用层
延迟包括:
- PHY到MAC的延迟
- MAC处理延迟
- DMA传输延迟
- 内核协议栈延迟
- 总延迟:几微秒到几十微秒
发送延迟(egressLatency):
应用层 → 内核 → DMA → MAC → PHY
延迟类似接收
补偿方法:
接收时间戳 = 硬件时间戳 - ingressLatency
发送时间戳 = 硅件时间戳 + egressLatency
配置:
[eth0]
ingressLatency 0 # 接收延迟(纳秒)
egressLatency 0 # 发送延迟(纳秒)
通常:
- PHY硬件时间戳不需要补偿
- ingressLatency/egressLatency = 0
- 只在特殊场景需要
实战:检查时间戳能力
ethtool -T命令
bash
$ ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
hardware-transmit (SOF_TIMESTAMPING_TX_HARDWARE)
hardware-receive (SOF_TIMESTAMPING_RX_HARDWARE)
hardware-raw-clock (SOF_TIMESTAMPING_RAW_HARDWARE)
PTP Hardware Clock: 0
Hardware Transmit Timestamp Modes:
off (HWTSTAMP_TX_OFF)
on (HWTSTAMP_TX_ON)
Hardware Receive Timestamp Modes:
none (HWTSTAMP_FILTER_NONE)
all (HWTSTAMP_FILTER_ALL)
ptpv2-l4-event (HWTSTAMP_FILTER_PTP_V2_L4_EVENT)
ptpv2-l4-sync (HWTSTAMP_FILTER_PTP_V2_L4_SYNC)
...
解读输出:
Capabilities:时间戳能力
- hardware-transmit:支持发送硬件时间戳
- hardware-receive:支持接收硬件时间戳
- hardware-raw-clock:支持原始硬件时间戳
PTP Hardware Clock:关联的PHC索引
- 0:/dev/ptp0
- 如果为-1:不支持硬件时间戳
Transmit Timestamp Modes:发送时间戳模式
- HWTSTAMP_TX_ON:普通硬件时间戳
- HWTSTAMP_TX_ONESTEP_SYNC:单步时钟(如果有)
Receive Timestamp Modes:接收时间戳过滤
- 各种PTP过滤模式
- 根据网卡能力列出
检查单步时钟支持
bash
# 查看是否支持单步时钟
$ ethtool -T eth0 | grep onestep
# 如果有onestep,说明支持单步时钟
# 查看网卡驱动信息
$ ethtool -i eth0
driver: igb
version: 5.15.0-1014-intel
firmware-version: 1.0.4
...
# Intel igb驱动支持单步时钟
常见网卡时间戳能力
Intel网卡
Intel I210/I350/igb:
- 支持硬件时间戳
- 支持单步时钟
- PHC精度:<100纳秒
- 广泛使用
Intel X710/i40e:
- 支持硬件时间戳
- 精度:<50纳秒
- 不支持单步时钟
Intel E810/ice:
- 最新一代
- 支持硬件时间戳
- 支持虚拟时钟
- 精度:<10纳秒
其他网卡
Broadcom bnxt_en:
- 支持硬件时间戳
- 精度:<100纳秒
Marvell mv88e6xxx:
- 交换机芯片
- 支持硬件时间戳
- 用于透明时钟
NXP sja1105:
- 交换机芯片
- 支持硬件时间戳
- 支持透明时钟
Realtek rtl8366:
- 低成本方案
- 部分型号支持硬件时间戳
时间戳调试
ts_proc调试接口
bash
# 查看时间戳状态
$ cat /proc/net/ts
# 某些驱动提供调试信息
$ cat /sys/kernel/debug/ptp/ptp0/
# 查看时间戳统计
$ dmesg | grep timestamp
测试时间戳
bash
# 使用testptp工具测试
$ testptp -d /dev/ptp0 --getcap
# 测试发送时间戳
$ testptp -d /dev/ptp0 --extts 0
# 测试接收时间戳
# 发送PTP报文并检查时间戳
小结:硬件时间戳的关键要点
时间戳类型:
- 软件、硬件、单步时钟
- 根据需求选择
配置流程:
- SIOCSHWTSTAMP配置驱动
- SO_TIMESTAMPING配置socket
- recvmsg获取时间戳
单步时钟:
- 网卡硬件修改报文
- 减少Follow_Up消息
- 需要特殊硬件支持
虚拟时钟:
- 多域PTP支持
- Linux新特性
延迟补偿:
- ingressLatency/egressLatency
- 补偿系统延迟
下集预告
硬件时间戳解决了"何时打时间戳",但PTP消息中的扩展信息如何处理?
下一节,我们将分析TLV处理实现------看看LinuxPTP如何编解码各种TLV扩展。
【悬念留给3.9】
TLV(Type-Length-Value)是PTP的扩展机制。
管理消息、信号消息都使用TLV。
LinuxPTP如何解析复杂的TLV结构?
如何构造TLV发送给其他节点?
下一节,深入TLV的世界。
📚 本文内容摘自本人的开源书《PTP技术书 - 从思想实验到协议实现》
全书从时间本质的思想实验出发,深度解析 IEEE 1588 协议、逐章分析 LinuxPTP 源码,并带你动手实现一个轻量级 PTP 程序(ptp-lite)。
🔗 在线阅读/下载:ptp-book
bash
git clone https://github.com/Lularible/ptp-book.git
⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。