PTP协议精讲(3.11):phc2sys工具分析——PHC与系统时钟的“桥梁“

3.11 phc2sys工具分析:PHC与系统时钟的"桥梁"

为什么需要phc2sys

PTP同步存在一个关键问题:

PTP同步的是PHC(PTP Hardware Clock):
- ptp4l操作PHC
- 时间戳来自PHC
- 调整的是PHC

但应用使用的是系统时钟(CLOCK_REALTIME):
- gettimeofday()读取系统时钟
- 系统时钟≠ PHC
- 应用看到的时间可能错误

问题:
如何让应用获得准确的时间?

答案:
phc2sys!

ptp4l vs phc2sys

ptp4l的作用:
- 同步PHC到网络主时钟
- 处理PTP协议
- 调整PHC频率和相位

phc2sys的作用:
- 同步系统时钟到PHC
- 或同步PHC到系统时钟
- 或同步PHC到PHC

典型部署:
网络主时钟 → ptp4l → PHC → phc2sys → 系统时钟 → 应用

phc2sys工作原理

时钟层级

时钟层次结构:

1. 网络主时钟(Grandmaster)
   - GPS时间源
   - 原子钟
   - 最高精度

2. PHC(PTP Hardware Clock)
   - 网卡硬件时钟
   - 纳秒级精度
   - ptp4l同步到网络主时钟

3. 系统时钟(CLOCK_REALTIME)
   - Linux内核时钟
   - 应用访问的时钟
   - phc2sys同步到PHC

同步链:
Grandmaster → PHC → System Clock

同步方法

/* phc2sys.c, 第576-647行 */

static void update_clock(struct domain *domain, struct clock *clock,
                         int64_t offset, uint64_t ts, int64_t delay)
{
    enum servo_state state = SERVO_UNLOCKED;
    double ppb = 0.0;

    /* 创建servo */
    if (!clock->servo) {
        clock->servo = servo_add(domain, clock);
        if (!clock->servo)
            return;
    }

    /* 处理闰秒 */
    if (clock_handle_leap(domain, clock, offset, ts))
        return;

    /* 应用偏移调整 */
    offset += get_sync_offset(domain, clock);

    /* 如果不是free running模式,调整时钟 */
    if (!domain->free_running) {
        /* sanity check */
        if (clock->sanity_check && clockcheck_sample(clock->sanity_check, ts))
            servo_reset(clock->servo);

        /* servo采样 */
        ppb = servo_sample(clock->servo, offset, ts, 1.0, &state);
        clock->servo_state = state;

        /* 根据servo状态调整时钟 */
        switch (state) {
        case SERVO_UNLOCKED:
            break;
        case SERVO_JUMP:
            clockadj_step(clock->clkid, -offset);
            break;
        case SERVO_LOCKED:
        case SERVO_LOCKED_STABLE:
            clockadj_set_freq(clock->clkid, -ppb);
            break;
        }
    }
}

同步流程

phc2sys主循环:

1. 读取源时钟时间(PHC或系统时钟)
2. 读取目标时钟时间
3. 计算偏差:offset = 目标时间 - 源时间
4. 调用servo计算调整量
5. 根据servo状态调整目标时钟:
   - SERVO_JUMP:时钟跳变(大偏差)
   - SERVO_LOCKED:频率调整(小偏差)
   - SERVO_LOCKED_STABLE:稳定同步

调整方法:
- 时钟跳变:立即修改时间(可能影响应用)
- 频率调整:渐进调整(平滑,无跳变)

phc2sys命令行

基本用法

# 同步系统时钟到PHC
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -w

# 参数说明:
# -s /dev/ptp0:源时钟(PHC)
# -c CLOCK_REALTIME:目标时钟(系统时钟)
# -w:等待ptp4l同步

常用参数

# 指定源和目标
-s [device]    源时钟设备
-c [device]    目标时钟设备

# 同步模式
-w             等待ptp4l同步
-W [timeout]   等待超时时间

# 伺服参数
-P [kp]        PI控制器的比例增益(默认0.7)
-I [ki]        PI控制器的积分增益(默认0.3)
-S             使用非线性回归servo

# 其他
-O [offset]    时间偏移(UTC/TAI)
-l [interval]  日志间隔
-m             打印到stdout

典型场景

# 场景1:PTP从时钟
# ptp4l同步PHC到网络主时钟
# phc2sys同步系统时钟到PHC
ptp4l -i eth0 -s -m &
phc2sys -s eth0 -c CLOCK_REALTIME -w -m

# 场景2:PTP主时钟
# PHC由本地GPS同步
# phc2sys同步系统时钟到PHC
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -m

# 场景3:多网卡
# 同步多个PHC
phc2sys -s eth0 -c eth1 -m

phc2sys实现详解

clock结构体

/* phc2sys.c, 第72-93行 */

struct clock {
    LIST_ENTRY(clock) list;           /* 链表节点 */
    clockid_t clkid;                  /* 时钟ID */
    int phc_index;                    /* PHC索引 */
    int sysoff_method;                /* 系统偏移方法 */
    int is_utc;                       /* 是否UTC时间 */
    int dest_only;                    /* 仅作为目标 */
    int state;                        /* 状态 */
    int new_state;                    /* 新状态 */
    int sync_offset;                  /* 同步偏移 */
    int leap_set;                     /* 闰秒设置 */
    int utc_offset_set;               /* UTC偏移设置 */
    struct servo *servo;              /* 伺服控制器 */
    enum servo_state servo_state;     /* 伺服状态 */
    char *device;                     /* 设备名 */
    const char *source_label;         /* 源标签 */
    struct stats *offset_stats;       /* 偏移统计 */
    struct stats *freq_stats;         /* 频率统计 */
    struct stats *delay_stats;        /* 延迟统计 */
    struct clockcheck *sanity_check;  /* 健康检查 */
};

domain结构体

/* phc2sys.c, 第102-121行 */

struct domain {
    unsigned int stats_max_count;     /* 统计最大数量 */
    int sanity_freq_limit;            /* 频率限制 */
    enum servo_type servo_type;       /* 伺服类型 */
    int phc_readings;                 /* PHC读取次数 */
    double phc_interval;              /* PHC间隔 */
    int forced_sync_offset;           /* 强制同步偏移 */
    int kernel_leap;                  /* 内核闰秒 */
    int state_changed;                /* 状态变化 */
    int free_running;                 /* 自由运行 */
    int has_rt_clock;                 /* 有实时时钟 */
    struct pmc_agent *agent;          /* PMC代理 */
    int agent_subscribed;             /* 代理订阅 */
    LIST_HEAD(port_head, port) ports; /* 端口列表 */
    LIST_HEAD(clock_head, clock) clocks; /* 时钟列表 */
    LIST_HEAD(dst_clock_head, clock) dst_clocks; /* 目标时钟列表 */
    struct clock *src_clock;          /* 源时钟 */
    struct domain *src_domain;        /* 源域 */
    int src_priority;                 /* 源优先级 */
};

自动配置

/* phc2sys.c, 第382-466行 */

static int reconfigure_domain(struct domain *domain)
{
    struct clock *c, *src = NULL;
    int src_cnt = 0, dst_cnt = 0;

    /* 遍历所有时钟 */
    LIST_FOREACH(c, &domain->clocks, list) {
        if (c->clkid == CLOCK_REALTIME) {
            /* 系统时钟总是可以作为目标 */
            LIST_INSERT_HEAD(&domain->dst_clocks, c, dst_list);
            domain->src_clock = c->dest_only ? NULL : c;
            return 0;
        }

        /* 根据端口状态选择源时钟 */
        switch (c->state) {
        case PS_SLAVE:
            src = c;
            src_cnt++;
            break;
        case PS_MASTER:
        case PS_PASSIVE:
            /* 作为目标时钟 */
            LIST_INSERT_HEAD(&domain->dst_clocks, c, dst_list);
            dst_cnt++;
            break;
        }
    }

    /* 选择源时钟 */
    if (src_cnt == 1) {
        domain->src_clock = src;
        pr_info("selecting %s as domain source clock", src->device);
    }

    return 0;
}

自动选择源时钟

phc2sys可以自动选择源时钟:

1. 监听ptp4l的端口状态通知
2. 当端口变为SLAVE状态时
3. 自动将该PHC选为源时钟
4. 同步其他时钟到该PHC

配置:
phc2sys -a(自动模式)

工作流程:
- 端口A:SLAVE状态 → PHC A作为源
- 端口B:MASTER状态 → PHC B作为目标
- phc2sys自动同步:PHC A → PHC B

时间偏移处理

UTC vs TAI

PTP使用TAI时间:
- International Atomic Time
- 不受闰秒影响
- 连续递增

系统时钟通常使用UTC:
- Coordinated Universal Time
- 受闰秒影响
- 有跳变

偏移:
UTC = TAI - leap_seconds

当前(2024年):TAI - UTC = 37秒

phc2sys需要处理这个偏移:
- 读取PHC(TAI)
- 读取系统时钟(UTC)
- offset = PHC_time - (sys_time + 37)

同步偏移配置

/* phc2sys.c, 第528-535行 */

static int64_t get_sync_offset(struct domain *domain, struct clock *dst)
{
    int direction = domain->forced_sync_offset;

    if (!direction)
        direction = dst->is_utc - domain->src_clock->is_utc;
    
    return (int64_t)dst->sync_offset * NS_PER_SEC * direction;
}

配置示例

# PHC(TAI)→ 系统时钟(UTC)
# 自动处理37秒偏移
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -w

# 手动指定偏移
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -O 37

统计和监控

输出示例

$ phc2sys -s eth0 -c CLOCK_REALTIME -w -m

phc2sys[1234.567]: selected /dev/ptp0 as PTP clock
phc2sys[1234.567]: selecting CLOCK_REALTIME for synchronization
phc2sys[1235.567]: CLOCK_REALTIME rms  100 max  150 freq -100 +/-  10
phc2sys[1236.567]: CLOCK_REALTIME rms   50 max   80 freq  -50 +/-   5
phc2sys[1237.567]: CLOCK_REALTIME rms   20 max   30 freq  -20 +/-   2

字段说明

CLOCK_REALTIME:目标时钟
rms  100:偏差的均方根值(纳秒)
max  150:最大偏差(纳秒)
freq -100:频率调整(ppb)
+/-   10:频率标准差

统计实现

/* phc2sys.c, 第537-574行 */

static void update_clock_stats(struct clock *clock, unsigned int max_count,
                               int64_t offset, double freq, int64_t delay)
{
    struct stats_result offset_stats, freq_stats, delay_stats;

    /* 添加统计值 */
    stats_add_value(clock->offset_stats, offset);
    stats_add_value(clock->freq_stats, freq);
    if (delay >= 0)
        stats_add_value(clock->delay_stats, delay);

    /* 达到最大数量时输出 */
    if (stats_get_num_values(clock->offset_stats) < max_count)
        return;

    /* 获取统计结果 */
    stats_get_result(clock->offset_stats, &offset_stats);
    stats_get_result(clock->freq_stats, &freq_stats);

    /* 输出 */
    pr_info("%s "
            "rms %4.0f max %4.0f "
            "freq %+6.0f +/- %3.0f",
            clock->device,
            offset_stats.rms, offset_stats.max_abs,
            freq_stats.mean, freq_stats.stddev);

    /* 重置统计 */
    stats_reset(clock->offset_stats);
    stats_reset(clock->freq_stats);
    stats_reset(clock->delay_stats);
}

实战示例

场景1:PTP从时钟完整部署

# 终端1:运行ptp4l
sudo ptp4l -i eth0 -s -m -S

# 终端2:运行phc2sys
sudo phc2sys -s eth0 -c CLOCK_REALTIME -w -m

# 终端3:监控同步状态
watch -n 1 'pmc -u "GET TIME_STATUS_NP"'

# 终端4:检查时间
date          # 系统时间(UTC)
hwclock       # 硬件时钟
phc_ctl /dev/ptp0 -- get  # PHC时间(TAI)

场景2:多域同步

# 域0:主域
ptp4l -i eth0 -m --domainNumber 0 &
phc2sys -a -r --domainNumber 0 &

# 域1:备用域
ptp4l -i eth1 -m --domainNumber 1 &
phc2sys -a -r --domainNumber 1 &

# 自动选择最佳源时钟

场景3:使用PPS信号

# 使用PPS信号辅助同步
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -w -m \
    --pps /dev/pps0

# PPS提供精确的秒脉冲
# 提高同步精度

小结:phc2sys的关键要点

核心作用

  • 连接PHC和系统时钟
  • 让应用获得准确时间

工作原理

  • 读取两个时钟
  • 计算偏差
  • servo控制调整

典型部署

  • ptp4l:网络→ PHC
  • phc2sys:PHC → 系统时钟

自动配置

  • 监听端口状态
  • 自动选择源时钟
  • 多域支持

UTC/TAI处理

  • 自动处理闰秒偏移
  • 可手动配置

下集预告

phc2sys解决了PHC与系统时钟的同步,但单播协商如何工作?

下一节,我们将分析单播协商实现——看看如何建立单播PTP会话。

【悬念留给3.12】

默认PTP使用组播。

但有些场景需要单播:

  • 跨路由器
  • 精确控制
  • 减少网络负载

单播协商如何建立?

如何维护单播会话?

下一节,深入单播世界。

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

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

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

git clone https://github.com/Lularible/ptp-book.git

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值