PTP协议精讲(3.7):传输层实现——PTP报文的“高速公路“

3.7 传输层实现:PTP报文的"高速公路"

PTP报文如何穿越网络

上一节我们学会了如何操作PHC硬件时钟,但有个问题:

PTP报文是怎么在网络中传输的?

复制代码
PTP报文诞生流程:
1. 应用层:构造PTP消息(Sync, Follow_Up, Delay_Req...)
2. 传输层:封装成UDP或以太网帧
3. 网络层:发送到组播地址
4. 数据链路层:网卡发送
5. 物理层:电信号传输

反向流程:
5. 物理层:接收电信号
4. 数据链路层:网卡接收
3. 网络层:识别PTP报文
2. 传输层:提取PTP消息
1. 应用层:处理消息内容

LinuxPTP支持多种传输方式,如何统一管理?


传输抽象接口

transport_type枚举

c 复制代码
/* transport.h, 第33-42行 */

enum transport_type {
    /* 0 is Reserved in spec. Use it for UDS */
    TRANS_UDS = 0,          /* Unix域套接字(管理接口) */
    TRANS_UDP_IPV4 = 1,     /* UDP/IPv4传输 */
    TRANS_UDP_IPV6,         /* UDP/IPv6传输 */
    TRANS_IEEE_802_3,       /* 原始以太网传输 */
    TRANS_DEVICENET,        /* DeviceNet(工业协议) */
    TRANS_CONTROLNET,       /* ControlNet(工业协议) */
    TRANS_PROFINET,         /* PROFINET(工业协议) */
};

IEEE 1588定义的传输类型

复制代码
networkProtocol enumeration(7.4.1 Table 3):

值    含义                  描述
0     Reserved              保留(LinuxPTP用于UDS)
1     UDP/IP (IPv4)         UDP over IPv4
2     UDP/IP (IPv6)         UDP over IPv6
3     IEEE 802.3            原始以太网
4     DeviceNet             工业以太网协议
5     ControlNet            工业以太网协议
6     PROFINET              工业以太网协议

LinuxPTP实现了前4种(0-3),工业协议未实现。

transport_event枚举

c 复制代码
/* transport.h, 第48-54行 */

enum transport_event {
    TRANS_GENERAL,      /* 普通消息(不需要时间戳) */
    TRANS_EVENT,        /* 事件消息(需要时间戳) */
    TRANS_ONESTEP,      /* 单步时钟(同步消息) */
    TRANS_P2P1STEP,     /* 单步时钟(P2P延迟测量) */
    TRANS_DEFER_EVENT,  /* 延迟获取时间戳 */
};

普通消息 vs 事件消息

复制代码
PTP消息分类:

事件消息(需要精确时间戳):
- Sync:同步消息
- Delay_Req:延迟请求
- Pdelay_Req:P2P延迟请求
- Pdelay_Resp:P2P延迟响应

特点:
- 发送和接收时都需要打时间戳
- 时间戳精度直接影响同步精度
- 使用EVENT端口(319)

普通消息(不需要精确时间戳):
- Follow_Up:跟随消息
- Delay_Resp:延迟响应
- Announce:通告消息
- Management:管理消息
- Signaling:信号消息

特点:
- 不需要精确时间戳
- 携带事件消息的时间戳信息
- 使用GENERAL端口(320)

transport结构体

c 复制代码
/* transport_private.h, 第29-50行 */

struct transport {
    enum transport_type type;    /* 传输类型 */
    struct config *cfg;          /* 配置对象 */

    /* 操作函数指针 */
    int (*close)(struct transport *t, struct fdarray *fda);
    int (*open)(struct transport *t, struct interface *iface,
                struct fdarray *fda, enum timestamp_type tt);
    int (*recv)(struct transport *t, int fd, void *buf, int buflen,
                struct address *addr, struct hw_timestamp *hwts);
    int (*send)(struct transport *t, struct fdarray *fda,
                enum transport_event event, int peer, void *buf, int buflen,
                struct address *addr, struct hw_timestamp *hwts);
    void (*release)(struct transport *t);
    int (*physical_addr)(struct transport *t, uint8_t *addr);
    int (*protocol_addr)(struct transport *t, uint8_t *addr);
};

这是典型的面向对象设计

复制代码
C语言的"虚函数表"模式:

struct transport定义了接口(抽象类)
具体的传输实现(UDP、Raw)继承这个接口
通过函数指针实现多态

好处:
- 统一的接口调用
- 方便添加新的传输类型
- 上层代码不关心具体传输

公共接口函数

transport_create:创建传输实例

c 复制代码
/* transport.c, 第101-128行 */

struct transport *transport_create(struct config *cfg, enum transport_type type)
{
    struct transport *t = NULL;
    
    switch (type) {
    case TRANS_UDS:
        t = uds_transport_create();
        break;
    case TRANS_UDP_IPV4:
        t = udp_transport_create();
        break;
    case TRANS_UDP_IPV6:
        t = udp6_transport_create();
        break;
    case TRANS_IEEE_802_3:
        t = raw_transport_create();
        break;
    case TRANS_DEVICENET:
    case TRANS_CONTROLNET:
    case TRANS_PROFINET:
        break;      /* 不支持 */
    }
    
    if (t) {
        t->type = type;
        t->cfg = cfg;
    }
    return t;
}

工厂模式

复制代码
transport_create是"工厂函数":
- 根据类型参数创建相应实例
- 每种传输有自己的create函数
- 返回统一接口

调用示例:
t = transport_create(cfg, TRANS_UDP_IPV4);
    → 调用udp_transport_create()
    → 返回UDP传输实例

t = transport_create(cfg, TRANS_IEEE_802_3);
    → 调用raw_transport_create()
    → 返回Raw传输实例

transport_open:打开传输

c 复制代码
/* transport.c, 第34-38行 */

int transport_open(struct transport *t, struct interface *iface,
                   struct fdarray *fda, enum timestamp_type tt)
{
    return t->open(t, iface, fda, tt);
}

多态调用

复制代码
transport_open是"虚函数"调用:
- 通过函数指针调用具体实现
- UDP:调用udp_open
- Raw:调用raw_open

参数说明:
- iface:网络接口配置
- fda:文件描述符数组(存放打开的socket)
- tt:时间戳类型(软件/硬件)

返回值:
- 0:成功
- -1:失败

fdarray结构

c 复制代码
/* fd.h, 第49-51行 */

struct fdarray {
    int fd[N_POLLFD];
};

/* fd数组索引定义 */
enum {
    FD_EVENT,          /* 事件消息socket */
    FD_GENERAL,        /* 普通消息socket */
    FD_DELAY_TIMER,    /* 延迟定时器 */
    FD_ANNOUNCE_TIMER, /* 通告定时器 */
    FD_SYNC_RX_TIMER,  /* 同步接收定时器 */
    ...
    N_POLLFD,          /* 总数量 */
};

两个socket的设计

复制代码
PTP需要两个socket:

FD_EVENT(事件socket):
- 端口319(UDP)或EtherType 0x88F7
- 发送/接收事件消息
- 配置硬件时间戳

FD_GENERAL(普通socket):
- 端口320(UDP)
- 发送/接收普通消息
- 可选软件时间戳

为什么分开?
- 事件消息需要精确时间戳
- 普通消息不需要精确时间戳
- 分开可以独立配置时间戳

transport_send:发送消息

c 复制代码
/* transport.c, 第45-51行 */

int transport_send(struct transport *t, struct fdarray *fda,
                   enum transport_event event, struct ptp_message *msg)
{
    int len = ntohs(msg->header.messageLength);

    return t->send(t, fda, event, 0, msg, len, NULL, &msg->hwts);
}

参数解析

复制代码
transport_send参数:
- t:传输实例
- fda:socket数组
- event:消息类型(TRANS_GENERAL/TRANS_EVENT)
- msg:PTP消息结构

内部调用:
t->send(t, fda, event, 0, msg, len, NULL, &msg->hwts)
- event=0表示非peer消息(使用主组播地址)
- NULL表示使用默认地址
- &msg->hwts用于存放时间戳

消息长度:
ntohs(msg->header.messageLength)
- 从消息头获取长度
- ntohs转换字节序

transport_peer:发送peer消息

c 复制代码
/* transport.c, 第53-59行 */

int transport_peer(struct transport *t, struct fdarray *fda,
                   enum transport_event event, struct ptp_message *msg)
{
    int len = ntohs(msg->header.messageLength);

    return t->send(t, fda, event, 1, msg, len, NULL, &msg->hwts);
}

peer消息的含义

复制代码
peer参数的作用:
- event=0:使用主组播地址(ptp_dst)
- event=1:使用peer组播地址(p2p_dst)

主组播地址:
- 用于E2E延迟测量
- 发送Sync, Announce等
- IPv4:224.0.1.129
- MAC:01:1B:19:00:00:00

Peer组播地址:
- 用于P2P延迟测量
- 发送Pdelay_Req, Pdelay_Resp
- IPv4:224.0.0.107
- MAC:01:80:C2:00:00:0E

区别:
transport_send → 主组播地址
transport_peer → peer组播地址

transport_recv:接收消息

c 复制代码
/* transport.c, 第40-43行 */

int transport_recv(struct transport *t, int fd, struct ptp_message *msg)
{
    return t->recv(t, fd, msg, sizeof(msg->data), &msg->address, &msg->hwts);
}

接收流程

复制代码
transport_recv调用具体传输的recv:
- 从socket读取数据
- 同时获取时间戳
- 记录源地址

参数:
- fd:socket文件描述符
- msg->data:数据缓冲区
- msg->address:源地址
- msg->hwts:硬件时间戳

返回值:
- 正数:接收的字节数
- 负数:错误

UDP/IPv4传输实现

UDP传输结构

c 复制代码
/* udp.c, 第43-47行 */

struct udp {
    struct transport t;      /* 传输接口 */
    struct address ip;       /* IP地址 */
    struct address mac;      /* MAC地址 */
};

继承关系

复制代码
struct udp包含struct transport:
- 第一个成员是基类
- 可以用container_of宏获取派生类

container_of(ptr, struct udp, t):
- 从t指针获取udp结构体指针
- C语言实现继承的关键技巧

UDP端口定义

c 复制代码
/* udp.c, 第40-41行 */

#define EVENT_PORT        319    /* PTP事件端口 */
#define GENERAL_PORT      320    /* PTP普通端口 */

IEEE 1588规定的端口

复制代码
PTP使用两个UDP端口:

端口319(事件端口):
- IANA注册:ptp-event
- 发送需要时间戳的消息
- 配置硬件时间戳

端口320(普通端口):
- IANA注册:ptp-general
- 发送不需要时间戳的消息
- Follow_Up, Announce等

查看端口注册:
$ grep ptp /etc/services
ptp-event    319/udp    # PTP Event
ptp-general  320/udp    # PTP General

组播地址配置

c 复制代码
/* config.c中的默认配置 */

PORT_ITEM_STR("ptp_dst_ipv4", "224.0.1.129"),
PORT_ITEM_STR("p2p_dst_ipv4", "224.0.0.107"),

组播地址选择

复制代码
IPv4组播地址范围:
224.0.0.0 - 224.0.0.255:本地网络控制块
224.0.1.0 - 224.0.1.255:本地网络范围

PTP主组播地址(224.0.1.129):
- 属于本地网络范围
- 需要路由器转发(跨子网)
- 用于E2E延迟测量

PTP peer组播地址(224.0.0.107):
- 属于本地网络控制块
- 不转发,仅本子网
- 用于P2P延迟测量

为什么不同?
- P2P测量相邻节点,不需要跨子网
- E2E测量可能跨多个节点

mcast_join:加入组播组

c 复制代码
/* udp.c, 第63-82行 */

static int mcast_join(int fd, int index, const struct sockaddr_in *sa)
{
    int err, off = 0;
    struct ip_mreqn req;

    memset(&req, 0, sizeof(req));
    memcpy(&req.imr_multiaddr, &sa->sin_addr, sizeof(struct in_addr));
    req.imr_ifindex = index;
    
    /* 加入组播组 */
    err = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req));
    if (err) {
        pr_err("setsockopt IP_ADD_MEMBERSHIP failed: %m");
        return -1;
    }
    
    /* 禁止本地回环 */
    err = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &off, sizeof(off));
    if (err) {
        pr_err("setsockopt IP_MULTICAST_LOOP failed: %m");
        return -1;
    }
    return 0;
}

组播加入详解

复制代码
IP_ADD_MEMBERSHIP:
- 加入指定的组播组
- 网卡开始接收该组播地址的报文
- 参数:struct ip_mreqn

struct ip_mreqn {
    struct in_addr imr_multiaddr;  /* 组播地址 */
    struct in_addr imr_address;    /* 本地地址(可选) */
    int imr_ifindex;               /* 网卡索引 */
};

IP_MULTICAST_LOOP:
- 控制是否接收自己发送的组播
- off=0:禁止回环
- 防止自己发送的消息被自己接收

为什么禁止回环

复制代码
组播回环问题:

如果不禁止:
1. ptp4l发送Sync消息到组播地址
2. 同一网卡接收自己发送的消息
3. 接收时间戳和发送时间戳几乎相同
4. 导致误判(以为收到其他节点的消息)

禁止回环后:
- 只接收其他节点发送的消息
- 不会误判

open_socket:创建UDP socket

c 复制代码
/* udp.c, 第91-145行 */

static int open_socket(const char *name, struct in_addr mc_addr[2], short port, int ttl)
{
    struct sockaddr_in addr;
    int fd, index, on = 1;

    /* 步骤1:创建socket */
    fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (fd < 0) {
        pr_err("socket failed: %m");
        goto no_socket;
    }

    /* 步骤2:获取网卡索引 */
    index = sk_interface_index(fd, name);
    if (index < 0)
        goto no_option;

    /* 步骤3:设置地址重用 */
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) {
        pr_err("setsockopt SO_REUSEADDR failed: %m");
        goto no_option;
    }

    /* 步骤4:绑定端口 */
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  /* 绑定所有地址 */
    addr.sin_port = htons(port);
    if (bind(fd, (struct sockaddr *) &addr, sizeof(addr))) {
        pr_err("bind failed: %m");
        goto no_option;
    }

    /* 步骤5:绑定到指定网卡 */
    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, name, strlen(name))) {
        pr_err("setsockopt SO_BINDTODEVICE failed: %m");
        goto no_option;
    }

    /* 步骤6:设置组播TTL */
    if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl))) {
        pr_err("setsockopt IP_MULTICAST_TTL failed: %m");
        goto no_option;
    }

    /* 步骤7:加入两个组播组 */
    addr.sin_addr = mc_addr[0];
    if (mcast_join(fd, index, &addr)) {
        goto no_option;
    }
    addr.sin_addr = mc_addr[1];
    if (mcast_join(fd, index, &addr)) {
        goto no_option;
    }

    /* 步骤8:设置组播输出接口 */
    if (mcast_bind(fd, index)) {
        goto no_option;
    }

    return fd;
no_option:
    close(fd);
no_socket:
    return -1;
}

socket创建的8个步骤

复制代码
1. socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)
   - 创建UDP socket
   - PF_INET表示IPv4协议族

2. sk_interface_index(fd, name)
   - 获取网卡索引
   - 用于后续组播配置

3. SO_REUSEADDR
   - 允许地址重用
   - 多个进程可以绑定同一端口

4. bind(INADDR_ANY, port)
   - 绑定端口
   - INADDR_ANY表示接收所有地址的报文

5. SO_BINDTODEVICE
   - 绑定到指定网卡
   - 只从该网卡接收报文

6. IP_MULTICAST_TTL
   - 设置组播报文的TTL
   - 控制转发范围

7. mcast_join(两次)
   - 加入主组播组(ptp_dst)
   - 加入peer组播组(p2p_dst)

8. mcast_bind
   - 设置组播报文的输出接口
   - 发送组播时使用指定网卡

udp_open:打开UDP传输

c 复制代码
/* udp.c, 第151-214行 */

static int udp_open(struct transport *t, struct interface *iface,
                    struct fdarray *fda, enum timestamp_type ts_type)
{
    struct udp *udp = container_of(t, struct udp, t);
    const char *name = interface_name(iface);
    uint8_t event_dscp, general_dscp;
    int efd, gfd, ttl;
    char *str;

    /* 步骤1:获取配置 */
    ttl = config_get_int(t->cfg, name, "udp_ttl");
    
    /* 步骤2:获取MAC地址 */
    sk_interface_macaddr(name, &udp->mac);

    /* 步骤3:获取IP地址 */
    sk_interface_addr(name, AF_INET, &udp->ip);

    /* 步骤4:解析主组播地址 */
    str = config_get_string(t->cfg, name, "ptp_dst_ipv4");
    if (!inet_aton(str, &mcast_addr[MC_PRIMARY])) {
        pr_err("invalid ptp_dst_ipv4 %s", str);
        return -1;
    }

    /* 步骤5:解析peer组播地址 */
    str = config_get_string(t->cfg, name, "p2p_dst_ipv4");
    if (!inet_aton(str, &mcast_addr[MC_PDELAY])) {
        pr_err("invalid p2p_dst_ipv4 %s", str);
        return -1;
    }

    /* 步骤6:打开事件socket */
    efd = open_socket(name, mcast_addr, EVENT_PORT, ttl);
    if (efd < 0)
        goto no_event;

    /* 步骤7:打开普通socket */
    gfd = open_socket(name, mcast_addr, GENERAL_PORT, ttl);
    if (gfd < 0)
        goto no_general;

    /* 步骤8:配置时间戳 */
    if (sk_timestamping_init(efd, interface_label(iface), ts_type, 
                             TRANS_UDP_IPV4, interface_get_vclock(iface)))
        goto no_timestamping;

    if (sk_general_init(gfd))
        goto no_timestamping;

    /* 步骤9:配置DSCP优先级 */
    event_dscp = config_get_int(t->cfg, NULL, "dscp_event");
    general_dscp = config_get_int(t->cfg, NULL, "dscp_general");

    if (event_dscp && sk_set_priority(efd, AF_INET, event_dscp)) {
        pr_warning("Failed to set event DSCP priority.");
    }
    if (general_dscp && sk_set_priority(gfd, AF_INET, general_dscp)) {
        pr_warning("Failed to set general DSCP priority.");
    }

    /* 步骤10:保存文件描述符 */
    fda->fd[FD_EVENT] = efd;
    fda->fd[FD_GENERAL] = gfd;
    return 0;

no_timestamping:
    close(gfd);
no_general:
    close(efd);
no_event:
    return -1;
}

DSCP优先级

复制代码
DSCP(Differentiated Services Code Point):
- IP头中的服务质量字段
- 用于区分报文优先级
- 高优先级报文获得更好的网络服务

PTP报文优先级:
- event_dscp:事件消息优先级
- general_dscp:普通消息优先级

作用:
- 事件消息需要高优先级
- 减少网络延迟抖动
- 提高同步精度

配置示例:
dscp_event 46    # 高优先级( Expedited Forwarding)
dscp_general 0   # 普通优先级

udp_send:发送UDP消息

c 复制代码
/* udp.c, 第222-271行 */

static int udp_send(struct transport *t, struct fdarray *fda,
                    enum transport_event event, int peer, void *buf, int len,
                    struct address *addr, struct hw_timestamp *hwts)
{
    struct address addr_buf;
    unsigned char junk[1600];
    ssize_t cnt;
    int fd = -1;

    /* 步骤1:选择socket */
    switch (event) {
    case TRANS_GENERAL:
        fd = fda->fd[FD_GENERAL];
        break;
    case TRANS_EVENT:
    case TRANS_ONESTEP:
    case TRANS_P2P1STEP:
    case TRANS_DEFER_EVENT:
        fd = fda->fd[FD_EVENT];
        break;
    }

    /* 步骤2:设置目标地址 */
    if (!addr) {
        memset(&addr_buf, 0, sizeof(addr_buf));
        addr_buf.sin.sin_family = AF_INET;
        addr_buf.sin.sin_addr = peer ? mcast_addr[MC_PDELAY] : 
                                       mcast_addr[MC_PRIMARY];
        addr_buf.len = sizeof(addr_buf.sin);
        addr = &addr_buf;
    }

    /* 步骤3:设置端口 */
    addr->sin.sin_port = htons(event ? EVENT_PORT : GENERAL_PORT);

    /* 步骤4:单步时钟特殊处理 */
    if (event == TRANS_ONESTEP)
        len += 2;    /* 为UDP校验修正扩展 */

    /* 步骤5:发送消息 */
    cnt = sendto(fd, buf, len, 0, &addr->sa, sizeof(addr->sin));
    if (cnt < 1) {
        pr_err("sendto failed: %m");
        return -errno;
    }

    /* 步骤6:获取发送时间戳 */
    return event == TRANS_EVENT ? 
           sk_receive(fd, junk, len, NULL, hwts, MSG_ERRQUEUE) : cnt;
}

发送时间戳获取

复制代码
MSG_ERRQUEUE的魔法:

普通socket发送:
sendto(fd, buf, len, 0, &addr, sizeof(addr))
- 发送数据
- 返回发送字节数

获取发送时间戳:
sk_receive(fd, junk, len, NULL, hwts, MSG_ERRQUEUE)
- 从错误队列读取
- 不是真正的"接收"
- 而是获取发送时的时间戳

原理:
- Linux内核在发送报文时打时间戳
- 时间戳保存在socket的错误队列
- 用recvmsg(MSG_ERRQUEUE)读取
- junk缓冲区存放发送的报文副本

单步时钟的UDP扩展

c 复制代码
if (event == TRANS_ONESTEP)
    len += 2;
复制代码
单步时钟(One-Step):
- 在发送Sync时直接打时间戳
- 修改报文中的时间戳字段
- 不需要Follow_Up消息

问题:
- 网卡可能在发送后才修改时间戳
- UDP校验和需要重新计算
- PHY芯片(如DP83640)需要额外2字节

len += 2:
- 为UDP校验修正预留空间
- PHY会用这些字节计算校验

UDP/IPv6传输实现

IPv6组播地址

c 复制代码
/* config.c中的IPv6配置 */

PORT_ITEM_STR("ptp_dst_ipv6", "FF0E:0:0:0:0:0:0:181"),
PORT_ITEM_STR("p2p_dst_ipv6", "FF02:0:0:0:0:0:0:6B"),

IPv6组播地址结构

复制代码
IPv6组播地址格式:
FF0s:0:0:0:0:0:0:xxxx

FF:组播前缀
0:标志(临时=1,永久=0)
s:范围:
   1:接口本地
   2:链路本地
   5:站点本地
   8:组织本地
   E:全球范围

PTP主组播(FF0E::181):
- FF0E:全球范围永久组播
- 181:PTP协议编号

PTP peer组播(FF02::6B):
- FF02:链路本地永久组播
- 6B:PTP P2P编号(107)

区别:
- E2E需要跨范围 → 全球范围
- P2P仅链路 → 链路本地

IPv6 scope配置

c 复制代码
/* udp6.c, 第181-182行 */

udp6->mc6_addr[MC_PRIMARY].s6_addr[1] = config_get_int(t->cfg, name, "udp6_scope");

动态修改scope

复制代码
udp6_scope配置:
- 默认值:0x0E(全球范围)
- 可配置为其他范围

s6_addr[1]是地址的第2字节:
FF0E::181 → s6_addr[1] = 0x0E
修改为FF05::181 → s6_addr[1] = 0x05

作用:
- 控制组播报文的传播范围
- 根据网络拓扑调整

链路本地地址处理

c 复制代码
/* udp6.c, 第53-56行 */

static int is_link_local(struct in6_addr *addr)
{
    return addr->s6_addr[1] == 0x02 ? 1 : 0;
}

/* 发送时使用 */
if (is_link_local(&addr_buf.sin6.sin6_addr))
    addr_buf.sin6.sin6_scope_id = udp6->index;

链路本地地址的特殊性

复制代码
IPv6链路本地地址(fe80::/10):
- 仅在本链路有效
- 需要指定网卡索引
- scope_id字段用于标识网卡

PTP peer组播(FF02::6B):
- 虽然不是fe80::,但也是链路本地范围
- 需要设置scope_id

sin6_scope_id:
- 指定使用的网卡
- 发送到链路本地地址时必须设置

原始以太网传输

为什么使用原始以太网

复制代码
UDP vs 原始以太网:

UDP传输:
- 需要IP协议栈处理
- 有UDP和IP头部开销
- 受IP路由影响
- 穿越路由器时时间戳可能变化

原始以太网:
- 绕过IP协议栈
- 直接以太网帧传输
- 无IP路由影响
- 时间戳更稳定
- 精度更高

适用场景:
- 工业控制网络
- 不需要路由的环境
- 追求最高精度

Ethernet EtherType

c 复制代码
/* ether.h, 第25-39行 */

#define EUI48 6      /* MAC地址长度 */
#define EUI64 8      /* GUID长度(InfiniBand) */
#define MAC_LEN EUI48

typedef uint8_t eth_addr[MAC_LEN];

struct eth_hdr {
    eth_addr dst;    /* 目标MAC */
    eth_addr src;    /* 源MAC */
    uint16_t type;   /* EtherType */
} __attribute__((packed));

PTP EtherType

复制代码
IEEE 1588定义的EtherType:
0x88F7:PTP over IEEE 802.3

以太网帧结构:
┌─────────┬─────────┬─────────┬─────────────┬─────┐
│Dst MAC  │Src MAC  │EtherType│PTP Message  │FCS  │
│6 bytes  │6 bytes  │2 bytes  │N bytes      │4    │
└─────────┴─────────┴─────────┴─────────────┴─────┘

EtherType:
0x0800:IPv4
0x86DD:IPv6
0x8100:VLAN
0x88F7:PTP

PTP组播MAC地址

c 复制代码
/* config.c中的MAC配置 */

PORT_ITEM_STR("ptp_dst_mac", "01:1B:19:00:00:00"),
PORT_ITEM_STR("p2p_dst_mac", "01:80:C2:00:00:0E"),

组播MAC地址生成规则

复制代码
IPv4组播 → MAC组播映射:

IPv4组播地址:224.0.1.129
MAC组播地址:01:00:5E:00:01:81

映射规则:
- 前24位:01:00:5E(IPv4组播MAC前缀)
- 后23位:IPv4组播地址的后23位
- 第25位:0(固定)

224.0.1.129 → 0xE0.0.1.81
取后23位 → 0.01.81(去掉最高位)
映射MAC → 01:00:5E:00:01:81

但IEEE 1588定义了特殊的MAC地址:
ptp_dst_mac:01:1B:19:00:00:00
- 不是标准映射
- IEEE 1588专用组播MAC

p2p_dst_mac:01:80:C2:00:00:0E
- IEEE 802.1桥接协议组播地址范围
- 不会被交换机转发到其他端口
- 仅在本地链路有效

BPF过滤器

c 复制代码
/* raw.c, 第131-149行 */

/*
 * tcpdump -d \
 * '((ether[12:2] == 0x8100 and ether[12 + 4 :2] == 0x88F7 and ether[14+4 :1] & 0x8 == 0x8) or '\
 * ' (ether[12:2] == 0x88F7 and                                ether[14   :1] & 0x8 == 0x8)) and '\
 * 'not ether src de:ad:de:ad:be:ef'
 */

static struct sock_filter raw_filter_vlan_norm_event[] = {
    { 0x28, 0, 0, 0x0000000c },   /* ldh      [12]       */
    { 0x15, 0, 5, 0x00008100 },   /* jeq      #0x8100    */
    { 0x28, 0, 0, 0x00000010 },   /* ldh      [16]       */
    { 0x15, 0, 12, 0x000088f7 },  /* jeq      #0x88f7    */
    { 0x30, 0, 0, 0x00000012 },   /* ldb      [18]       */
    { 0x54, 0, 0, 0x00000008 },   /* and      #0x8       */
    { 0x15, 4, 9, 0x00000008 },   /* jeq      #0x8       */
    { 0x15, 0, 8, 0x000088f7 },   /* jeq      #0x88f7    */
    { 0x30, 0, 0, 0x0000000e },   /* ldb      [14]       */
    { 0x54, 0, 0, 0x00000008 },   /* and      #0x8       */
    { 0x15, 0, 5, 0x00000008 },   /* jeq      #0x8       */
    { 0x20, 0, 0, 0x00000008 },   /* ld       [8]        */
    { 0x15, 0, 2, 0xdeadbeef },   /* jeq      #0xdeadbeef */
    { 0x28, 0, 0, 0x00000006 },   /* ldh      [6]        */
    { 0x15, 1, 0, 0x0000dead },   /* jeq      #0xdead    */
    { 0x6, 0, 0, 0x00040000 },    /* ret      #262144    */
    { 0x6, 0, 0, 0x00000000 },    /* ret      #0         */
};

BPF(Berkeley Packet Filter)

复制代码
BPF是Linux内核的包过滤器:
- 在内核态运行
- 高效过滤网络包
- 减少用户态处理开销

PTP BPF过滤器的作用:
1. 只接收PTP报文(EtherType 0x88F7)
2. 区分事件消息和普通消息
3. 过滤自己发送的消息
4. 支持VLAN封装

过滤器逻辑:
- 检查EtherType是否为PTP(0x88F7)
- 检查消息类型(事件/普通)
- 检查源MAC是否是自己(避免回环)
- 返回:接受(262144)或拒绝(0)

BPF指令解读

复制代码
ldh [12]:加载以太网帧[12:14](EtherType位置)
jeq #0x8100:判断是否为VLAN标签(TPID)
ldh [16]:如果是VLAN,加载[16:18](VLAN后的EtherType)
jeq #0x88f7:判断是否为PTP
ldb [18]:加载字节[18](PTP消息头)
and #0x8:检查bit 3(事件消息标志)
...

逻辑流程:
1. 先检查是否VLAN封装
2. 找到实际的EtherType
3. 判断是否PTP
4. 判断是否事件消息
5. 检查源MAC避免回环

raw_configure:配置过滤器

c 复制代码
/* raw.c, 第154-228行 */

static int raw_configure(int fd, int event, int index,
                         unsigned char *local_addr, unsigned char *addr1,
                         unsigned char *addr2, int enable)
{
    int err1, err2, option;
    struct packet_mreq mreq;
    struct sock_fprog prg;

    /* 步骤1:安装BPF过滤器 */
    if (event) {
        prg.len = ARRAY_SIZE(raw_filter_vlan_norm_event);
        prg.filter = raw_filter_vlan_norm_event;
    } else {
        prg.len = ARRAY_SIZE(raw_filter_vlan_norm_general);
        prg.filter = raw_filter_vlan_norm_general;
    }

    /* 修改过滤器中的源MAC */
    memcpy(&prg.filter[FILTER_EVENT_POS_SRC0].k, local_addr, 2);
    memcpy(&prg.filter[FILTER_EVENT_POS_SRC2].k, local_addr + 2, 4);
    prg.filter[FILTER_EVENT_POS_SRC0].k = ntohs(prg.filter[FILTER_EVENT_POS_SRC0].k);
    prg.filter[FILTER_EVENT_POS_SRC2].k = ntohl(prg.filter[FILTER_EVENT_POS_SRC2].k);

    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prg, sizeof(prg))) {
        pr_err("setsockopt SO_ATTACH_FILTER failed: %m");
        return -1;
    }

    /* 步骤2:加入组播 */
    option = enable ? PACKET_ADD_MEMBERSHIP : PACKET_DROP_MEMBERSHIP;

    memset(&mreq, 0, sizeof(mreq));
    mreq.mr_ifindex = index;
    mreq.mr_type = PACKET_MR_MULTICAST;
    mreq.mr_alen = MAC_LEN;
    memcpy(mreq.mr_address, addr1, MAC_LEN);

    err1 = setsockopt(fd, SOL_PACKET, option, &mreq, sizeof(mreq));
    
    memcpy(mreq.mr_address, addr2, MAC_LEN);
    err2 = setsockopt(fd, SOL_PACKET, option, &mreq, sizeof(mreq));

    if (!err1 && !err2)
        return 0;

    /* 步骤3:如果组播失败,尝试全组播模式 */
    mreq.mr_type = PACKET_MR_ALLMULTI;
    if (!setsockopt(fd, SOL_PACKET, option, &mreq, sizeof(mreq))) {
        return 0;
    }

    /* 步骤4:如果还失败,尝试混杂模式 */
    mreq.mr_type = PACKET_MR_PROMISC;
    if (!setsockopt(fd, SOL_PACKET, option, &mreq, sizeof(mreq))) {
        return 0;
    }

    pr_err("all socket options failed");
    return -1;
}

三层fallback

复制代码
组播配置的fallback策略:

第1层: PACKET_MR_MULTICAST
- 加入指定的组播MAC地址
- 只接收PTP组播报文
- 最高效

第2层: PACKET_MR_ALLMULTI
- 接收所有组播报文
- 包括PTP和其他组播
- 稍低效,但兼容性好

第3层: PACKET_MR_PROMISC
- 混杂模式,接收所有报文
- 包括组播和单播
- 最低效,但一定能工作

为什么需要fallback?
- 有些网卡不支持特定组播
- 有些驱动有bug
- 混杂模式保证可用性

raw_recv:接收以太网帧

c 复制代码
/* raw.c, 第407-446行 */

static int raw_recv(struct transport *t, int fd, void *buf, int buflen,
                    struct address *addr, struct hw_timestamp *hwts)
{
    struct raw *raw = container_of(t, struct raw, t);
    unsigned char *ptr = buf;
    struct eth_hdr *hdr;
    int cnt, hlen;

    /* 计算头部长度 */
    if (raw->vlan) {
        hlen = sizeof(struct vlan_hdr);   /* 16字节 */
    } else {
        hlen = sizeof(struct eth_hdr);    /* 14字节 */
    }

    /* 调整缓冲区位置 */
    ptr -= hlen;           /* 预留头部空间 */
    buflen += hlen;        /* 增加缓冲区大小 */
    hdr = (struct eth_hdr *) ptr;

    /* 接收以太网帧 */
    cnt = sk_receive(fd, ptr, buflen, addr, hwts, MSG_DONTWAIT);

    if (cnt >= 0)
        cnt -= hlen;       /* 返回PTP消息长度 */
    if (cnt < 0)
        return cnt;

    /* 处理PRP尾部 */
    if (has_prp_trailer(buf, cnt))
        cnt -= PRP_TRAILER_LEN;

    /* 检测VLAN并更新状态 */
    if (raw->vlan) {
        if (ETH_P_1588 == ntohs(hdr->type)) {
            pr_notice("raw: disabling VLAN mode");
            raw->vlan = 0;
        }
    } else {
        if (ETH_P_8021Q == ntohs(hdr->type)) {
            pr_notice("raw: switching to VLAN mode");
            raw->vlan = 1;
        }
    }

    return cnt;
}

VLAN动态检测

复制代码
VLAN封装:

普通以太网帧:
┌─────────┬─────────┬─────────┬─────────────┐
│Dst MAC  │Src MAC  │0x88F7   │PTP Message  │
│6 bytes  │6 bytes  │2 bytes  │N bytes      │
└─────────┴─────────┴─────────┴─────────────┘

VLAN封装帧:
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────────┐
│Dst MAC  │Src MAC  │0x8100   │TCI      │0x88F7   │PTP Message  │
│6 bytes  │6 bytes  │2 bytes  │2 bytes  │2 bytes  │N bytes      │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────────┘

raw->vlan状态:
- 0:普通模式,hlen=14
- 1:VLAN模式,hlen=16

动态检测:
- 接收报文时检查EtherType
- 根据实际情况切换模式
- 自动适应网络环境

PRP(Parallel Redundancy Protocol)

c 复制代码
/* raw.c, 第297-346行 */

static bool has_prp_trailer(unsigned char *ptr, int cnt)
{
    unsigned short suffix_id, lane_size_field, lsdu_size;
    int ptp_msg_len, trailer_start;
    struct ptp_header *hdr;

    /* 检查是否有效PTP消息 */
    if (cnt < sizeof(struct ptp_header))
        return false;

    hdr = (struct ptp_header *)ptr;
    if ((hdr->ver & MAJOR_VERSION_MASK) != PTP_MAJOR_VERSION)
        return false;

    ptp_msg_len = ntohs(hdr->messageLength);
    if (cnt < (ptp_msg_len + PRP_TRAILER_LEN))
        return false;

    trailer_start = cnt - PRP_TRAILER_LEN;

    /* 检查RCT尾部 */
    lane_size_field = ntohs(*(unsigned short*)(ptr + trailer_start + 2));
    lsdu_size = lane_size_field & 0x0FFF;
    if (lsdu_size != cnt)
        return false;

    suffix_id = ntohs(*(unsigned short*)(ptr + trailer_start + 4));
    if (suffix_id == ETH_P_PRP) {
        return true;
    }

    return false;
}

PRP冗余协议

复制代码
PRP(IEC 62439-3):
- 双网并行冗余
- 两个独立网络同时传输
- 接收端丢弃重复报文

PRP尾部(RCT):
┌───────────┬───────────┬───────────┐
│SeqNr      │LanId+Size │Suffix     │
│16 bits    │16 bits    │16 bits    │
└───────────┴───────────┴───────────┘

Suffix:0x88FB(标识PRP)

PTP over PRP:
- PTP报文携带PRP尾部
- 需要在接收时去除
- cnt -= PRP_TRAILER_LEN

Socket辅助函数

sk_receive:收发统一接口

c 复制代码
/* sk.c, 第418-508行 */

int sk_receive(int fd, void *buf, int buflen,
               struct address *addr, struct hw_timestamp *hwts, int flags)
{
    char control[256];
    int cnt = 0, res = 0, level, type;
    struct cmsghdr *cm;
    struct iovec iov = { buf, buflen };
    struct msghdr msg;
    struct timespec *sw, *ts = NULL;

    /* 设置消息结构 */
    memset(control, 0, sizeof(control));
    memset(&msg, 0, sizeof(msg));
    if (addr) {
        msg.msg_name = &addr->ss;
        msg.msg_namelen = sizeof(addr->ss);
    }
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = control;
    msg.msg_controllen = sizeof(control);

    /* 如果是获取发送时间戳,需要poll等待 */
    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, flags);
    if (cnt < 0) {
        pr_err("recvmsg%sfailed: %m",
               flags == MSG_ERRQUEUE ? " tx timestamp " : " ");
    }

    /* 解析控制消息(时间戳) */
    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);
        }
    }

    if (addr)
        addr->len = msg.msg_namelen;

    if (!ts) {
        memset(&hwts->ts, 0, sizeof(hwts->ts));
        return cnt < 0 ? -errno : cnt;
    }

    /* 根据时间戳类型选择 */
    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;
    }

    return cnt < 0 ? -errno : cnt;
}

recvmsg详解

复制代码
recvmsg是高级接收函数:

struct msghdr {
    void *msg_name;          /* 源地址 */
    socklen_t msg_namelen;   /* 地址长度 */
    struct iovec *msg_iov;   /* 数据缓冲区数组 */
    size_t msg_iovlen;       /* iov数量 */
    void *msg_control;       /* 控制信息 */
    size_t msg_controllen;   /* 控制信息长度 */
    int msg_flags;           /* 接收标志 */
};

控制信息(msg_control):
- 包含时间戳等辅助数据
- 通过CMSG_FIRSTHDR/NXTHDR遍历
- 不同类型的时间戳在不同位置

时间戳数组

复制代码
SO_TIMESTAMPING返回3个时间戳:

ts[0]:软件时间戳
- 内核协议栈处理时的时间
- 精度较低(微秒级)

ts[1]:硬件时间戳(legacy)
- 旧的硬件时间戳格式
- 某些旧驱动使用

ts[2]:硬件时间戳(raw)
- PHY/网卡硬件时间戳
- 精度最高(纳秒级)

选择:
TS_SOFTWARE → ts[0]
TS_LEGACY_HW → ts[1]
TS_HARDWARE → ts[2]

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 err, filter1, filter2 = 0, flags, tx_type = HWTSTAMP_TX_ON;
    struct so_timestamping timestamping;

    /* 步骤1:确定时间戳标志 */
    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;
    default:
        return -1;
    }

    /* 步骤2:硬件时间戳需要配置驱动 */
    if (type != TS_SOFTWARE) {
        filter1 = HWTSTAMP_FILTER_PTP_V2_EVENT;
        switch (type) {
        case TS_HARDWARE:
        case TS_LEGACY_HW:
            tx_type = HWTSTAMP_TX_ON;
            break;
        case TS_ONESTEP:
            tx_type = HWTSTAMP_TX_ONESTEP_SYNC;
            break;
        case TS_P2P1STEP:
            tx_type = HWTSTAMP_TX_ONESTEP_P2P;
            break;
        }

        switch (transport) {
        case TRANS_UDP_IPV4:
        case TRANS_UDP_IPV6:
            filter2 = HWTSTAMP_FILTER_PTP_V2_L4_EVENT;
            break;
        case TRANS_IEEE_802_3:
            filter2 = HWTSTAMP_FILTER_PTP_V2_L2_EVENT;
            break;
        }

        err = hwts_init(fd, device, filter1, filter2, tx_type);
        if (err)
            return err;
    }

    /* 步骤3:绑定虚拟时钟 */
    if (vclock >= 0)
        flags |= SOF_TIMESTAMPING_BIND_PHC;

    timestamping.flags = flags;
    timestamping.bind_phc = vclock;

    /* 步骤4:启用时间戳 */
    if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING,
                   &timestamping, sizeof(timestamping)) < 0) {
        pr_err("ioctl SO_TIMESTAMPING failed: %m");
        return -1;
    }

    /* 步骤5:配置错误队列 */
    flags = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_SELECT_ERR_QUEUE,
                   &flags, sizeof(flags)) < 0) {
        pr_warning("%s: SO_SELECT_ERR_QUEUE: %m", device);
        sk_events = 0;
        sk_revents = POLLERR;
    }

    return 0;
}

硬件时间戳配置

复制代码
SIOCSHWTSTAMP ioctl:

struct hwtstamp_config {
    int flags;       /* 配置标志 */
    int tx_type;     /* 发送时间戳类型 */
    int rx_filter;   /* 接收时间戳过滤 */
};

tx_type:
HWTSTAMP_TX_OFF:不打发送时间戳
HWTSTAMP_TX_ON:普通硬件时间戳
HWTSTAMP_TX_ONESTEP_SYNC:单步时钟(Sync消息)
HWTSTAMP_TX_ONESTEP_P2P:单步时钟(P2P消息)

rx_filter:
HWTSTAMP_FILTER_NONE:不接收时间戳
HWTSTAMP_FILTER_PTP_V2_L2_EVENT:以太网PTP
HWTSTAMP_FILTER_PTP_V2_L4_EVENT:UDP PTP
HWTSTAMP_FILTER_PTP_V2_EVENT:所有PTP
HWTSTAMP_FILTER_ALL:所有报文

Unix域套接字(UDS)

UDS的作用

复制代码
UDS(Unix Domain Socket):
- 本地进程间通信
- 不经过网络
- 用于管理接口

用途:
- pmc工具与ptp4l通信
- 发送管理消息
- 获取时钟信息
- 配置参数

UDS不是PTP网络传输!
- 只是管理接口
- 不参与PTP同步
- transport_type=0(保留值)

UDS路径配置

c 复制代码
/* uds.c, 第53-103行 */

static int uds_open(struct transport *t, struct interface *iface, 
                    struct fdarray *fda, enum timestamp_type tt)
{
    char *uds_ro_path = config_get_string(t->cfg, NULL, "uds_ro_address");
    const char *uds_path = interface_remote(iface);
    struct uds *uds = container_of(t, struct uds, t);
    const char *name = interface_name(iface);
    struct sockaddr_un sa;
    mode_t file_mode;
    int fd, err;

    /* 创建socket */
    fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
    
    /* 绑定路径 */
    memset(&sa, 0, sizeof(sa));
    sa.sun_family = AF_LOCAL;
    strncpy(sa.sun_path, name, sizeof(sa.sun_path) - 1);

    /* 删除已存在的文件 */
    if (!unlink(name))
        pr_err("uds: removed existing %s", name);

    /* 绑定 */
    err = bind(fd, (struct sockaddr *) &sa, sizeof(sa));

    /* 设置文件权限 */
    file_mode = (mode_t)config_get_int(t->cfg, name, "uds_file_mode");
    chmod(name, file_mode);

    fda->fd[FD_EVENT] = -1;
    fda->fd[FD_GENERAL] = fd;
    return 0;
}

默认UDS路径

复制代码
配置默认值:

uds_address:/var/run/ptp4l
- ptp4l创建,用于管理
- pmc连接到此路径

uds_ro_address:/var/run/ptp4lro
- 只读接口
- 允许非root用户读取

文件权限:
uds_file_mode:0660
- 用户和组可读写

使用示例:
pmc -u /var/run/ptp4l "GET CURRENT_DATA_SET"

传输层初始化流程

port创建传输

c 复制代码
/* port.c中的传输创建(简化) */

struct port *port_create(struct interface *iface, ...)
{
    struct port *p;
    enum transport_type transport;

    /* 获取传输类型配置 */
    transport = config_get_int(cfg, interface_name(iface), "network_transport");
    
    switch (transport) {
    case TRANS_UDP_IPV4:
        p->transport = transport_create(cfg, TRANS_UDP_IPV4);
        break;
    case TRANS_IEEE_802_3:
        p->transport = transport_create(cfg, TRANS_IEEE_802_3);
        break;
    ...
    }

    /* 打开传输 */
    transport_open(p->transport, iface, &p->fda, ts_type);

    return p;
}

配置参数

bash 复制代码
# /etc/linuxptp/ptp4l.conf

[global]
# 传输类型
network_transport UDPv4    # 或 L2(以太网)

# 组播地址
ptp_dst_ipv4 224.0.1.129
p2p_dst_ipv4 224.0.0.107

# 组播TTL
udp_ttl 1

# DSCP优先级
dscp_event 46
dscp_general 0

[eth0]
# 网卡特定配置

实战示例

查看传输配置

bash 复制代码
# 查看网卡组播地址
$ ip maddr show eth0
2:  eth0
    link  01:1b:19:00:00:00
    link  01:80:c2:00:00:0e
    inet  224.0.1.129
    inet  224.0.0.107

# 查看网卡时间戳能力
$ ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
    hardware-transmit
    hardware-receive
    hardware-raw-clock
PTP Hardware Clock: 0

抓包分析

bash 复制代码
# 抓取PTP UDP报文
$ tcpdump -i eth0 'udp port 319 or port 320'

# 抓取PTP以太网报文
$ tcpdump -i eth0 'ether proto 0x88f7'

# 解析PTP内容
$ tcpdump -i eth0 'udp port 319' -vv
...
PTPv2, length 44, version 2, subtype 0 (SYNC)
    ...

小结:传输层的设计智慧

抽象接口

  • 统一的transport接口
  • 多种传输实现多态调用
  • 上层代码不关心具体传输

UDP传输

  • 端口319/320
  • 组播地址配置
  • DSCP优先级

原始以太网

  • EtherType 0x88F7
  • BPF过滤器
  • VLAN支持

时间戳配置

  • SO_TIMESTAMPING
  • SIOCSHWTSTAMP
  • 软件和硬件时间戳

UDS管理

  • 本地通信
  • pmc工具接口

下集预告

传输层解决了"如何发送报文",但硬件时间戳是怎么工作的?

下一节,我们将深入分析硬件时间戳机制------看看网卡如何精确记录报文时间。

【悬念留给3.8】

硬件时间戳是实现高精度PTP的关键。

网卡PHY如何在纳秒级精度打时间戳?

Linux内核如何传递时间戳给用户态?

One-Step时钟如何修改报文时间戳?

下一节,揭示时间戳的黑科技。

📚 本文内容摘自本人的开源书《PTP技术书 - 从思想实验到协议实现》

全书从时间本质的思想实验出发,深度解析 IEEE 1588 协议、逐章分析 LinuxPTP 源码,并带你动手实现一个轻量级 PTP 程序(ptp-lite)。

🔗 在线阅读/下载:ptp-book

bash 复制代码
git clone https://github.com/Lularible/ptp-book.git

⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。

相关推荐
S1998_1997111609•X2 小时前
RSS/RSA\-SSh,G\-bps^&&·iOS\Cd/,~…:cade?_code in/@$&¥_buy=ID card|want_M_GEN.M*L
网络协议·百度·ssh·gpu算力·oneapi
郝学胜-神的一滴2 小时前
深入epoll反应堆模型:从libevent源码看高性能IO设计精髓
linux·服务器·开发语言·c++·网络协议·unix·信息与通信
qq_283720052 小时前
本地大模型部署全教程:Python 低成本调用开源 AI 模型
人工智能·python·开源
SilentSamsara2 小时前
Kubernetes 网络模型:CNI 插件与 Pod 间通信的底层实现
网络·云原生·容器·架构·kubernetes·k8s
我也不曾来过12 小时前
传输层协议UDP和TCP
linux·网络·udp
Hello__77773 小时前
开源鸿蒙 Flutter 实战|应用启动页(Splash Screen)全流程实现
flutter·开源·harmonyos
奇妙之二进制3 小时前
zmq源码分析之消息可读通知机制
服务器·网络
techdashen3 小时前
不开端口,不配 DNS,用树莓派在家搭一个公网可访问的 Web 服务
前端·网络·智能路由器
笨熊呆呆瓜3 小时前
【可靠性配置】华为M-LAG防环机制
网络