3.2 数据集与消息结构:PTP数据的编码艺术
从协议规范到代码实现
第二章我们详细讲解了PTP的数据集(defaultDS、currentDS、parentDS等)。
现在,让我们看看这些数据集在LinuxPTP中是如何定义和使用的。
数据集定义
defaultDS:默认数据集
/* ds.h, 第33-43行 */
struct defaultDS {
UInteger8 flags; /* 标志位 */
UInteger8 reserved1; /* 保留 */
UInteger16 numberPorts; /* 端口数量 */
UInteger8 priority1; /* 优先级1 */
struct ClockQuality clockQuality; /* 时钟质量 */
UInteger8 priority2; /* 优先级2 */
struct ClockIdentity clockIdentity; /* 时钟标识 */
UInteger8 domainNumber; /* 域编号 */
UInteger8 reserved2; /* 保留 */
} PACKED;
与IEEE 1588的对比:
IEEE 1588-2019定义的defaultDS完整字段:
1. twoStepFlag → 压缩到flags字段的bit 0
2. slaveOnly → 压缩到flags字段的bit 1
3. numberPorts → 直接对应
4. priority1 → 直接对应
5. clockQuality → 直接对应
- clockClass
- clockAccuracy
- offsetScaledLogVariance
6. priority2 → 直接对应
7. domainNumber → 直接对应
8. sdoId → 未在struct中体现(通过其他方式处理)
9. clockIdentity → 直接对应
LinuxPTP的设计选择:
- 将twoStepFlag和slaveOnly压缩为flags
- 节省内存空间
- 符合嵌入式系统的优化思路
flags字段的位定义:
/* ds.h, 第30-31行 */
#define DDS_TWO_STEP_FLAG (1<<0) /* bit 0: twoStepFlag */
#define DDS_SLAVE_ONLY (1<<1) /* bit 1: slaveOnly */
使用示例:
/* 设置twoStepFlag */
dds->flags |= DDS_TWO_STEP_FLAG;
/* 检查slaveOnly */
if (dds->flags & DDS_SLAVE_ONLY) {
/* 这是一个仅从时钟 */
}
currentDS:当前数据集
/* ds.h, 第64-68行 */
struct currentDS {
UInteger16 stepsRemoved; /* 到主时钟的跳数 */
TimeInterval offsetFromMaster; /* 与主时钟的时间偏差 */
TimeInterval meanPathDelay; /* 平均路径延迟 */
} PACKED;
字段详解:
stepsRemoved:
- 表示从主时钟到本时钟经过了多少个边界时钟
- 初始值为0
- 每经过一个边界时钟加1
- 用于BMCA决策
offsetFromMaster:
- 当前与主时钟的时间偏差(纳秒)
- 由伺服计算得出
- 正值表示本地时钟比主时钟快
meanPathDelay:
- 到主时钟的平均路径延迟(纳秒)
- 由E2E或P2P延迟测量得出
- 用于计算总延迟
parentDS:父时钟数据集
/* ds.h, 第70-80行 */
struct parentDS {
struct PortIdentity parentPortIdentity; /* 父端口标识 */
UInteger8 parentStats; /* 父时钟统计标志 */
UInteger8 reserved; /* 保留 */
UInteger16 observedParentOffsetScaledLogVariance; /* 观察到的方差 */
Integer32 observedParentClockPhaseChangeRate; /* 相位变化率 */
UInteger8 grandmasterPriority1; /* 主时钟优先级1 */
struct ClockQuality grandmasterClockQuality; /* 主时钟质量 */
UInteger8 grandmasterPriority2; /* 主时钟优先级2 */
struct ClockIdentity grandmasterIdentity; /* 主时钟标识 */
} PACKED;
设计解读:
parentDS的作用:
记录"我的上游时钟是谁"以及"网络主时钟是谁"。
例如,网络拓扑:
主时钟(A) → BC(B) → BC(C) → 从时钟(D)
从时钟(D)的parentDS:
- parentPortIdentity = BC(C)的端口标识
- grandmasterIdentity = 主时钟(A)的标识
- grandmasterPriority1/2 = 主时钟(A)的优先级
这样,从时钟就知道:
- 直接上游是BC(C)
- 最终的主时钟是A
portDS:端口数据集
/* ds.h, 第98-109行 */
struct portDS {
struct PortIdentity portIdentity; /* 端口标识 */
Enumeration8 portState; /* 端口状态 */
Integer8 logMinDelayReqInterval;/* Delay_Req最小间隔 */
TimeInterval peerMeanPathDelay; /* P2P链路延迟 */
Integer8 logAnnounceInterval; /* Announce间隔 */
UInteger8 announceReceiptTimeout;/* Announce超时 */
Integer8 logSyncInterval; /* Sync间隔 */
Enumeration8 delayMechanism; /* 延迟机制 */
Integer8 logMinPdelayReqInterval;/* Pdelay_Req间隔 */
UInteger8 versionNumber; /* PTP版本 */
} PACKED;
字段详解:
portIdentity:
- 端口唯一标识
- 包含clockIdentity(8字节)+ portNumber(2字节)
portState:
- 当前端口状态
- 值为enum port_state之一
logMinDelayReqInterval:
- Delay_Req报文的最小发送间隔(log2秒)
- 例如:logMinDelayReqInterval=0 表示间隔1秒
- logMinDelayReqInterval=1 表示间隔2秒
peerMeanPathDelay:
- P2P模式下的链路延迟
- 由Pdelay测量得出
delayMechanism:
- 延迟测量机制
- E2E或P2P
数据集的比较函数
dscmp:数据集比较函数
/* bmc.c, 第83-127行 */
int dscmp(struct dataset *a, struct dataset *b)
{
int diff;
/* 特殊情况:同一数据集 */
if (a == b)
return 0;
/* 特殊情况:其中一个为空 */
if (a && !b)
return A_BETTER;
if (b && !a)
return B_BETTER;
/* 步骤1:比较clockIdentity */
diff = memcmp(&a->identity, &b->identity, sizeof(a->identity));
if (!diff)
return dscmp2(a, b); /* 相同,进入第二阶段比较 */
/* 步骤2:比较priority1 */
if (a->priority1 < b->priority1)
return A_BETTER;
if (a->priority1 > b->priority1)
return B_BETTER;
/* 步骤3:比较clockClass */
if (a->quality.clockClass < b->quality.clockClass)
return A_BETTER;
if (a->quality.clockClass > b->quality.clockClass)
return B_BETTER;
/* 步骤4:比较clockAccuracy */
if (a->quality.clockAccuracy < b->quality.clockAccuracy)
return A_BETTER;
if (a->quality.clockAccuracy > b->quality.clockAccuracy)
return B_BETTER;
/* 步骤5:比较offsetScaledLogVariance */
if (a->quality.offsetScaledLogVariance < b->quality.offsetScaledLogVariance)
return A_BETTER;
if (a->quality.offsetScaledLogVariance > b->quality.offsetScaledLogVariance)
return B_BETTER;
/* 步骤6:比较priority2 */
if (a->priority2 < b->priority2)
return A_BETTER;
if (a->priority2 > b->priority2)
return B_BETTER;
/* 步骤7:比较clockIdentity */
return diff < 0 ? A_BETTER : B_BETTER;
}
返回值定义:
/* bmc.h */
#define A_BETTER 1 /* A更好 */
#define B_BETTER -1 /* B更好 */
#define A_BETTER_TOPO 2 /* A更好(拓扑原因) */
#define B_BETTER_TOPO -2 /* B更好(拓扑原因) */
比较逻辑图解:
dscmp比较流程:
┌─────────────────────────────────────────────────┐
│ 1. clockIdentity 相同? │
│ 是 → 调用dscmp2(第二阶段比较) │
│ 否 → 继续 │
├─────────────────────────────────────────────────┤
│ 2. priority1 │
│ A小 → A_BETTER │
│ B小 → B_BETTER │
│ 相同 → 继续 │
├─────────────────────────────────────────────────┤
│ 3. clockClass │
│ A小 → A_BETTER │
│ B小 → B_BETTER │
│ 相同 → 继续 │
├─────────────────────────────────────────────────┤
│ 4. clockAccuracy │
│ A小 → A_BETTER │
│ B小 → B_BETTER │
│ 相同 → 继续 │
├─────────────────────────────────────────────────┤
│ 5. offsetScaledLogVariance │
│ A小 → A_BETTER │
│ B小 → B_BETTER │
│ 相同 → 继续 │
├─────────────────────────────────────────────────┤
│ 6. priority2 │
│ A小 → A_BETTER │
│ B小 → B_BETTER │
│ 相同 → 继续 │
├─────────────────────────────────────────────────┤
│ 7. clockIdentity │
│ A小 → A_BETTER │
│ B大 → B_BETTER │
└─────────────────────────────────────────────────┘
dscmp2:第二阶段比较
/* bmc.c, 第35-81行 */
int dscmp2(struct dataset *a, struct dataset *b)
{
int diff;
unsigned int A = a->stepsRemoved, B = b->stepsRemoved;
/* 情况1:stepsRemoved差距大于1 */
if (A + 1 < B)
return A_BETTER; /* A的跳数明显少 */
if (B + 1 < A)
return B_BETTER; /* B的跳数明显少 */
/* 情况2:A的跳数少1 */
if (A < B) {
diff = portid_cmp(&b->receiver, &b->sender);
if (diff < 0)
return A_BETTER;
if (diff > 0)
return A_BETTER_TOPO;
return 0; /* error-1 */
}
/* 情况3:B的跳数少1 */
if (A > B) {
diff = portid_cmp(&a->receiver, &a->sender);
if (diff < 0)
return B_BETTER;
if (diff > 0)
return B_BETTER_TOPO;
return 0; /* error-1 */
}
/* 情况4:跳数相同,比较sender */
diff = portid_cmp(&a->sender, &b->sender);
if (diff < 0)
return A_BETTER_TOPO;
if (diff > 0)
return B_BETTER_TOPO;
/* 情况5:比较receiver端口号 */
if (a->receiver.portNumber < b->receiver.portNumber)
return A_BETTER_TOPO;
if (a->receiver.portNumber > b->receiver.portNumber)
return B_BETTER_TOPO;
/* error-2 */
return 0;
}
拓扑比较的意义:
当两个时钟的属性完全相同,但stepsRemoved不同时:
例如:
A的stepsRemoved = 3
B的stepsRemoved = 4
显然A更接近主时钟,应该选择A。
但如果:
A的stepsRemoved = 4
B的stepsRemoved = 5
差距只有1,这时需要检查拓扑关系:
- 是否存在环路?
- 谁的路径更优?
A_BETTER_TOPO和B_BETTER_TOPO表示"由于拓扑原因"选择某个时钟。
消息结构定义
PTP头部结构
/* msg.h, 第90-103行 */
struct ptp_header {
uint8_t tsmt; /* transportSpecific | messageType */
uint8_t ver; /* minorVersionPTP | versionPTP */
UInteger16 messageLength;
UInteger8 domainNumber;
Octet reserved1;
Octet flagField[2];
Integer64 correction;
UInteger32 reserved2;
struct PortIdentity sourcePortIdentity;
UInteger16 sequenceId;
UInteger8 control;
Integer8 logMessageInterval;
} PACKED;
字段编码技巧:
tsmt字段(1字节):
- 高4位:transportSpecific(传输特定)
- 低4位:messageType(消息类型)
例如:
tsmt = 0x10
- 高4位 = 0x1 → transportSpecific = 1(如802.1AS)
- 低4位 = 0x0 → messageType = SYNC
ver字段(1字节):
- 高4位:minorVersionPTP(次版本号)
- 低4位:versionPTP(主版本号)
例如:
ver = 0x21
- 高4位 = 0x2 → minorVersionPTP = 2
- 低4位 = 0x1 → versionPTP = 1
- 完整版本:PTP 2.1(IEEE 1588-2019)
消息类型定义:
/* msg.h, 第45-54行 */
#define SYNC 0x0 /* 同步报文 */
#define DELAY_REQ 0x1 /* 延迟请求 */
#define PDELAY_REQ 0x2 /* 对等延迟请求 */
#define PDELAY_RESP 0x3 /* 对等延迟响应 */
#define FOLLOW_UP 0x8 /* 跟随报文 */
#define DELAY_RESP 0x9 /* 延迟响应 */
#define PDELAY_RESP_FOLLOW_UP 0xA /* 对等延迟跟随 */
#define ANNOUNCE 0xB /* 公告报文 */
#define SIGNALING 0xC /* 信令报文 */
#define MANAGEMENT 0xD /* 管理报文 */
Announce消息结构
/* msg.h, 第105-117行 */
struct announce_msg {
struct ptp_header hdr; /* 公共头部 */
struct Timestamp originTimestamp; /* 发送时间戳 */
Integer16 currentUtcOffset; /* UTC偏移 */
Octet reserved; /* 保留 */
UInteger8 grandmasterPriority1; /* 主时钟优先级1 */
struct ClockQuality grandmasterClockQuality; /* 主时钟质量 */
UInteger8 grandmasterPriority2; /* 主时钟优先级2 */
struct ClockIdentity grandmasterIdentity; /* 主时钟标识 */
UInteger16 stepsRemoved; /* 跳数 */
Enumeration8 timeSource; /* 时间源 */
uint8_t suffix[0]; /* TLV后缀 */
} PACKED;
设计亮点:
suffix[0]的设计:
这是一个"柔性数组"(flexible array member):
- 数组长度为0,不占用结构体大小
- 用于访问结构体末尾的数据
- Announce消息的TLV附加在结构体末尾
使用方式:
struct announce_msg *msg = ...;
struct tlv_extra *extra = (struct tlv_extra *)msg->suffix;
这是C语言的常用技巧,用于处理变长数据。
Sync消息结构
/* msg.h, 第119-123行 */
struct sync_msg {
struct ptp_header hdr; /* 公共头部 */
struct Timestamp originTimestamp; /* 发送时间戳 */
uint8_t suffix[0]; /* TLV后缀 */
} PACKED;
originTimestamp的含义:
originTimestamp是Sync报文携带的"发送时间"。
Two-step模式:
- Sync报文的originTimestamp通常为0
- 真正的时间戳由Follow_Up报文携带
- 原因:硬件时间戳在发送后才能获得
One-step模式:
- Sync报文携带精确的时间戳
- 硬件需要预测并插入时间戳
- 实现复杂,但效率高
Delay_Req/Delay_Resp消息结构
/* msg.h, 第125-142行 */
struct delay_req_msg {
struct ptp_header hdr; /* 公共头部 */
struct Timestamp originTimestamp; /* 发送时间戳 */
uint8_t suffix[0]; /* TLV后缀 */
} PACKED;
struct delay_resp_msg {
struct ptp_header hdr; /* 公共头部 */
struct Timestamp receiveTimestamp; /* 接收时间戳 */
struct PortIdentity requestingPortIdentity; /* 请求者标识 */
uint8_t suffix[0]; /* TLV后缀 */
} PACKED;
四个时间戳的关系:
E2E延迟测量涉及四个时间戳:
t1(主时钟发送Sync):由Sync或Follow_Up携带
t2(从时钟接收Sync):由从时钟本地记录
t3(从时钟发送Delay_Req):由delay_req_msg.originTimestamp携带
t4(主时钟接收Delay_Req):由delay_resp_msg.receiveTimestamp携带
偏移计算:
offset = [(t2 - t1) - (t4 - t3)] / 2
路径延迟:
delay = [(t2 - t1) + (t4 - t3)] / 2
消息处理函数
消息解析流程
/* port.c中的消息处理(简化) */
static int port_rx_sync(struct port *p, struct ptp_message *msg)
{
struct sync_msg *sync = &msg->sync;
struct ptp_header *hdr = &sync->hdr;
/* 步骤1:检查消息是否来自主时钟 */
if (!pid_eq(&hdr->sourcePortIdentity, &p->parentDS.parentPortIdentity)) {
return 0; /* 忽略 */
}
/* 步骤2:获取时间戳 */
t2 = msg->hwts.ts; /* 接收时间戳 */
/* 步骤3:检查是否是two-step */
if (hdr->flagField[0] & TWO_STEP) {
/* 保存t2,等待Follow_Up */
p->t2 = t2;
p->seqId = hdr->sequenceId;
return 0;
}
/* 步骤4:One-step模式,直接处理 */
t1 = timestamp_to_tmv(&sync->originTimestamp);
t1 = tmv_add(t1, hdr->correction);
/* 步骤5:同步时钟 */
clock_synchronize(p->clock, t2, t1);
return 0;
}
消息发送流程
/* port.c中的消息发送(简化) */
int port_tx_sync(struct port *p)
{
struct ptp_message *msg;
struct sync_msg *sync;
int err;
/* 步骤1:分配消息 */
msg = msg_allocate();
if (!msg)
return -ENOMEM;
/* 步骤2:填充头部 */
msg->hdr.tsmt = SYNC | p->transportSpecific;
msg->hdr.ver = PTP_VERSION;
msg->hdr.messageLength = sizeof(struct sync_msg);
msg->hdr.domainNumber = clock_domain_number(p->clock);
msg->hdr.flagField[0] = TWO_STEP; /* 使用two-step */
msg->hdr.sourcePortIdentity = p->portIdentity;
msg->hdr.sequenceId = p->seqId++;
msg->hdr.logMessageInterval = p->logSyncInterval;
/* 步骤3:发送 */
err = port_prepare_and_send(p, msg, TRANS_EVENT);
/* 步骤4:保存序列号,等待硬件时间戳 */
p->sync_seqid = msg->hdr.sequenceId;
/* 步骤5:释放消息 */
msg_put(msg);
return err;
}
时间戳处理
时间戳类型
/* msg.h, 第76-82行 */
enum timestamp_type {
TS_SOFTWARE, /* 软件时间戳 */
TS_HARDWARE, /* 硬件时间戳 */
TS_LEGACY_HW, /* 传统硬件时间戳 */
TS_ONESTEP, /* 单步时间戳 */
TS_P2P1STEP, /* P2P单步时间戳 */
};
时间戳结构
/* msg.h, 第84-88行 */
struct hw_timestamp {
enum timestamp_type type; /* 时间戳类型 */
tmv_t ts; /* 时间戳值 */
tmv_t sw; /* 软件时间戳(用于诊断) */
};
tmv_t类型
/* tmv.h */
typedef int64_t tmv_t; /* 时间值,单位纳秒 */
时间运算函数:
/* 时间加法 */
tmv_t tmv_add(tmv_t a, tmv_t b);
/* 时间减法 */
tmv_t tmv_sub(tmv_t a, tmv_t b);
/* 时间比较 */
int tmv_cmp(tmv_t a, tmv_t b);
/* 时间转换 */
tmv_t timestamp_to_tmv(struct Timestamp *ts);
struct Timestamp tmv_to_timestamp(tmv_t t);
小结:数据集与消息的设计智慧
内存优化:
- 使用flags压缩布尔字段
- 柔性数组处理变长数据
- PACKED宏确保结构体紧凑
协议映射:
- 结构体定义与IEEE 1588报文格式对应
- 字段顺序与协议规范一致
- 便于直接网络传输
编码技巧:
- 位域压缩(tsmt、ver字段)
- 柔性数组(suffix[0])
- 类型别名(tmv_t)
功能完整:
- 支持所有PTP消息类型
- 支持硬件/软件时间戳
- 支持One-step和Two-step模式
下集预告
数据集和消息结构是PTP协议的基础。
下一节,我们将分析端口状态机——看看LinuxPTP如何实现PTP复杂的9种状态转换。
【悬念留给3.3】
PTP端口有9种状态,状态转换规则复杂。
LinuxPTP用200多行的fsm.c实现了这个状态机。
它是如何处理这么多状态和事件的?
使用了什么技巧让代码简洁清晰?
下一节,我们详细解读状态机的实现。