Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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实现了这个状态机。

它是如何处理这么多状态和事件的?

使用了什么技巧让代码简洁清晰?

下一节,我们详细解读状态机的实现。