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 交流讨论。
:phc2sys工具分析——PHC与系统时钟的“桥梁“&spm=1001.2101.3001.5002&articleId=160635140&d=1&t=3&u=9bbd45aa599447d79cd26cedc82a872b)
377

被折叠的 条评论
为什么被折叠?



