3.3 端口状态机:200行代码驾驭9种状态
状态机的艺术
PTP端口有9种状态,状态转换规则复杂。
但LinuxPTP只用337行代码就实现了完整的状态机。
这是如何做到的?
状态机概述
IEEE 1588定义的状态
第二章我们详细讲解了PTP端口的9种状态:
1. INITIALIZING - 初始化 2. FAULTY - 故障 3. DISABLED - 禁用 4. LISTENING - 监听 5. PRE_MASTER - 预备主 6. MASTER - 主时钟 7. PASSIVE - 被动 8. UNCALIBRATED - 未校准 9. SLAVE - 从时钟LinuxPTP的状态定义
/* fsm.h, 第24-35行 */enumport_state{PS_INITIALIZING=1,PS_FAULTY,PS_DISABLED,PS_LISTENING,PS_PRE_MASTER,PS_MASTER,PS_PASSIVE,PS_UNCALIBRATED,PS_SLAVE,PS_GRAND_MASTER,/* 非标准扩展 */};PS_GRAND_MASTER的由来:
IEEE 1588只定义了PS_MASTER状态。 但LinuxPTP添加了PS_GRAND_MASTER状态: 区别: - PS_MASTER:端口处于主时钟状态 - PS_GRAND_MASTER:端口是整个网络的主时钟 为什么需要这个扩展? 便于判断: if (port_state(port) == PS_GRAND_MASTER) { /* 我就是网络主时钟,可以安全地宣告 */ }状态机事件
/* fsm.h, 第38-56行 */enumfsm_event{EV_NONE,/* 无事件 */EV_POWERUP,/* 上电 */EV_INITIALIZE,/* 初始化 */EV_DESIGNATED_ENABLED,/* 被启用 */EV_DESIGNATED_DISABLED,/* 被禁用 */EV_FAULT_CLEARED,/* 故障清除 */EV_FAULT_DETECTED,/* 故障检测 */EV_STATE_DECISION_EVENT,/* 状态决策事件 */EV_QUALIFICATION_TIMEOUT_EXPIRES,/* 资格超时 */EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES,/* Announce超时 */EV_SYNCHRONIZATION_FAULT,/* 同步故障 */EV_MASTER_CLOCK_SELECTED,/* 主时钟选中 */EV_INIT_COMPLETE,/* 初始化完成 */EV_RS_MASTER,/* 推荐状态:主时钟 */EV_RS_GRAND_MASTER,/* 推荐状态:网络主时钟 */EV_RS_SLAVE,/* 推荐状态:从时钟 */EV_RS_PASSIVE,/* 推荐状态:被动 */};事件分类:
第一类:基础事件 - EV_POWERUP:设备上电 - EV_INITIALIZE:重新初始化 - EV_INIT_COMPLETE:初始化完成 第二类:控制事件 - EV_DESIGNATED_ENABLED:管理员启用端口 - EV_DESIGNATED_DISABLED:管理员禁用端口 第三类:故障事件 - EV_FAULT_DETECTED:检测到故障 - EV_FAULT_CLEARED:故障已清除 - EV_SYNCHRONIZATION_FAULT:同步故障 第四类:定时器事件 - EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:Announce超时 - EV_QUALIFICATION_TIMEOUT_EXPIRES:资格超时 第五类:BMCA事件 - EV_STATE_DECISION_EVENT:状态决策 - EV_MASTER_CLOCK_SELECTED:主时钟选中 - EV_RS_*:BMCA推荐状态主状态机实现
ptp_fsm函数
/* fsm.c, 第21-220行 */enumport_stateptp_fsm(enumport_statestate,enumfsm_eventevent,intmdiff){enumport_statenext=state;/* 特殊处理:初始化事件 */if(EV_INITIALIZE==event||EV_POWERUP==event)returnPS_INITIALIZING;/* 状态转换表 */switch(state){casePS_INITIALIZING:/* ... */break;casePS_FAULTY:/* ... */break;/* ... 其他状态 */}returnnext;}设计亮点:
亮点一:函数式设计 状态机是一个纯函数: - 输入:当前状态 + 事件 + mdiff - 输出:下一状态 - 无副作用 好处: - 可测试性强 - 易于理解 - 便于调试 亮点二:默认保持当前状态 enum port_state next = state; 如果事件不被处理,状态保持不变。 这避免了复杂的错误处理。 亮点三:快速路径处理 if (EV_INITIALIZE == event || EV_POWERUP == event) return PS_INITIALIZING; 无论当前什么状态,初始化事件都回到INITIALIZING。 避免在每个case中重复处理。状态转换详解
INITIALIZING状态
/* fsm.c, 第29-40行 */casePS_INITIALIZING:switch(event){caseEV_FAULT_DETECTED:next=PS_FAULTY;break;caseEV_INIT_COMPLETE:next=PS_LISTENING;break;default:break;}break;状态转换图:
INITIALIZING │ ├─ EV_FAULT_DETECTED ──→ PS_FAULTY │ └─ EV_INIT_COMPLETE ──→ PS_LISTENING 说明: - 初始化过程中检测到故障 → 进入故障状态 - 初始化完成 → 进入监听状态,开始接收AnnounceLISTENING状态
/* fsm.c, 第60-86行 */casePS_LISTENING:switch(event){caseEV_DESIGNATED_DISABLED:next=PS_DISABLED;break;caseEV_FAULT_DETECTED:next=PS_FAULTY;break;caseEV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:next=PS_MASTER;break;caseEV_RS_MASTER:next=PS_PRE_MASTER;break;caseEV_RS_GRAND_MASTER:next=PS_GRAND_MASTER;break;caseEV_RS_SLAVE:next=PS_UNCALIBRATED;break;caseEV_RS_PASSIVE:next=PS_PASSIVE;break;default:break;}break;状态转换图:
LISTENING │ ├─ EV_DESIGNATED_DISABLED ──────→ PS_DISABLED │ ├─ EV_FAULT_DETECTED ───────────→ PS_FAULTY │ ├─ EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES ──→ PS_MASTER │ (长时间没收到Announce,自己当主时钟) │ ├─ EV_RS_MASTER ────────────────→ PS_PRE_MASTER │ (BMCA建议当主时钟) │ ├─ EV_RS_GRAND_MASTER ──────────→ PS_GRAND_MASTER │ (BMCA建议当网络主时钟) │ ├─ EV_RS_SLAVE ─────────────────→ PS_UNCALIBRATED │ (BMCA建议当从时钟) │ └─ EV_RS_PASSIVE ───────────────→ PS_PASSIVE (BMCA建议保持被动)PRE_MASTER状态
/* fsm.c, 第88-108行 */casePS_PRE_MASTER:switch(event){caseEV_DESIGNATED_DISABLED:next=PS_DISABLED;break;caseEV_FAULT_DETECTED:next=PS_FAULTY;break;caseEV_QUALIFICATION_TIMEOUT_EXPIRES:next=PS_MASTER;break;caseEV_RS_SLAVE:next=PS_UNCALIBRATED;break;caseEV_RS_PASSIVE:next=PS_PASSIVE;break;default:break;}break;PRE_MASTER的作用:
为什么需要PRE_MASTER状态? 防止网络震荡: - 端口决定当主时钟 - 先进入PRE_MASTER等待一段时间 - 确认没有更好的主时钟 - 超时后才进入MASTER状态 等待时间: - 由qualificationTimeout决定 - 通常是announceInterval的几倍 - 确保Announce信息充分传播MASTER和GRAND_MASTER状态
/* fsm.c, 第110-128行 */casePS_MASTER:casePS_GRAND_MASTER:/* 两个状态处理相同 */switch(event){caseEV_DESIGNATED_DISABLED:next=PS_DISABLED;break;caseEV_FAULT_DETECTED:next=PS_FAULTY;break;caseEV_RS_SLAVE:next=PS_UNCALIBRATED;break;caseEV_RS_PASSIVE:next=PS_PASSIVE;break;default:break;}break;代码合并技巧:
casePS_MASTER:casePS_GRAND_MASTER:/* 两个状态共享同一套处理逻辑 */这是C语言的switch特性:-多个case可以共享同一个代码块-减少代码重复-提高可维护性SLAVE状态
/* fsm.c, 第186-216行 */casePS_SLAVE:switch(event){caseEV_DESIGNATED_DISABLED:next=PS_DISABLED;break;caseEV_FAULT_DETECTED:next=PS_FAULTY;break;caseEV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:next=PS_MASTER;break;caseEV_SYNCHRONIZATION_FAULT:next=PS_UNCALIBRATED;break;caseEV_RS_MASTER:next=PS_PRE_MASTER;break;caseEV_RS_GRAND_MASTER:next=PS_GRAND_MASTER;break;caseEV_RS_SLAVE:if(mdiff)/* 主时钟变化了 */next=PS_UNCALIBRATED;break;caseEV_RS_PASSIVE:next=PS_PASSIVE;break;default:break;}break;mdiff参数的意义:
caseEV_RS_SLAVE:if(mdiff)next=PS_UNCALIBRATED;break;mdiff="master difference"(主时钟差异) 含义:-mdiff=0:主时钟没变-mdiff=1:主时钟变了 行为:-如果收到EV_RS_SLAVE且主时钟没变(mdiff=0) → 保持SLAVE状态,不需要重新校准-如果收到EV_RS_SLAVE且主时钟变了(mdiff=1) → 进入UNCALIBRATED,重新同步 这是优化:-主时钟切换是常见情况-避免不必要的状态切换仅从时钟状态机
ptp_slave_fsm函数
/* fsm.c, 第222-337行 */enumport_stateptp_slave_fsm(enumport_statestate,enumfsm_eventevent,intmdiff){/* ... 与ptp_fsm类似,但限制了部分状态转换 */}与主状态机的区别:
ptp_fsm(完整状态机): - 可以成为主时钟 - 可以成为从时钟 - 可以进入所有状态 ptp_slave_fsm(仅从状态机): - 永远不能成为主时钟 - 忽略EV_RS_MASTER和EV_RS_GRAND_MASTER事件 - 只能在SLAVE、UNCALIBRATED、LISTENING之间切换 适用场景: - slaveOnly = TRUE的设备 - 不想参与BMCA的终端设备关键区别示例
/* ptp_slave_fsm中的LISTENING状态 */casePS_LISTENING:switch(event){caseEV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:caseEV_RS_MASTER:caseEV_RS_GRAND_MASTER:caseEV_RS_PASSIVE:next=PS_LISTENING;/* 保持LISTENING,不当主时钟 */break;caseEV_RS_SLAVE:next=PS_UNCALIBRATED;break;}break;对比主状态机:
/* ptp_fsm中的LISTENING状态 */casePS_LISTENING:switch(event){caseEV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:next=PS_MASTER;/* 可以成为主时钟 */break;caseEV_RS_MASTER:next=PS_PRE_MASTER;/* 可以成为主时钟 */break;caseEV_RS_GRAND_MASTER:next=PS_GRAND_MASTER;/* 可以成为主时钟 */break;}break;状态机的使用
在端口中调用状态机
/* port.c中的状态决策(简化) */voidport_dispatch(structport*p,enumfsm_eventevent,intmdiff){enumport_statenext;/* 调用状态机 */if(port_slave_only(p)){next=ptp_slave_fsm(p->state,event,mdiff);}else{next=ptp_fsm(p->state,event,mdiff);}/* 状态变化 */if(next!=p->state){/* 退出旧状态 */port_state_exit(p);/* 更新状态 */p->state=next;/* 进入新状态 */port_state_enter(p);}}状态进入/退出动作
/* port.c中的状态进入动作(简化) */staticvoidport_state_enter(structport*p){switch(p->state){casePS_INITIALIZING:port_init(p);break;casePS_LISTENING:port_start_listening(p);break;casePS_MASTER:casePS_GRAND_MASTER:port_start_master(p);break;casePS_SLAVE:port_start_slave(p);break;/* ... */}}状态进入动作详解:
PS_INITIALIZING: - 初始化端口数据结构 - 检查网络链路状态 - 设置初始参数 PS_LISTENING: - 启动Announce接收定时器 - 清空外部时钟列表 - 开始监听网络 PS_MASTER/PS_GRAND_MASTER: - 启动Announce发送定时器 - 启动Sync发送定时器 - 准备响应Delay_Req PS_SLAVE: - 清空同步状态 - 准备接收Sync和Announce - 启动Delay_Req发送状态机可视化
完整状态转换图
┌───────────────────┐ │ PS_INITIALIZING │ └───────────────────┘ │ │ ▲ EV_FAULT_DETECTED EV_FAULT_CLEARED │ │ │ ▼ │ ┌─────────────┐ ┌─────────┐ │ │ PS_FAULTY │ │ │ └────┤ │ │ ┌──────┴──────┴──────┐ │ │ │ │ │ │ │ EV_DESIGNATED_DISABLED │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────────┐ │ │ │ PS_DISABLED │ │ │ └─────────────────────────┘ │ │ │ │ EV_DESIGNATED_ENABLED │ │ │ ▼ │ ┌──────────────┐ │ │ PS_LISTENING │◄───────────────────┤ └──────────────┘ │ │ │ │ EV_ANNOUNCE_TIMEOUT│ │EV_RS_SLAVE │ │ │ │ │ ▼ │ ┌───────────────┴───────────────┐ │ │ │ │ │ ┌─────────────┐ ┌──────────────┐ │ │ │ PS_PRE_MASTER│ │PS_UNCALIBRATED│ │ │ └─────────────┘ └──────────────┘ │ │ │ │ │ │ EV_QUAL_TIMEOUT │ EV_MASTER_SELECTED │ │ │ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ PS_MASTER │ │ PS_SLAVE │──────┘ │ └──────────────┘ └──────────────┘ │ ▲ │ │ └───────────────────────────────┘ EV_SYNCHRONIZATION_FAULT ┌──────────────┐ │ PS_PASSIVE │ (环路检测后进入) └──────────────┘设计模式分析
状态模式
LinuxPTP的状态机实现了经典的状态模式:
状态模式的要素: 1. 状态枚举(enum port_state) 2. 事件枚举(enum fsm_event) 3. 状态转换表(switch-case嵌套) 4. 状态进入/退出动作 优点: - 状态转换逻辑集中在一处 - 易于添加新状态 - 易于添加新事件 - 状态转换清晰可见表驱动 vs 嵌套switch
LinuxPTP选择嵌套switch而不是状态转换表:
状态转换表方式(伪代码): struct { enum port_state from; enum fsm_event event; enum port_state to; } state_table[] = { {PS_INITIALIZING, EV_INIT_COMPLETE, PS_LISTENING}, {PS_LISTENING, EV_RS_SLAVE, PS_UNCALIBRATED}, // ... }; 嵌套switch方式: switch (state) { case PS_INITIALIZING: switch (event) { case EV_INIT_COMPLETE: next = PS_LISTENING; } } 为什么选择嵌套switch? 优点: - 代码紧凑 - 编译器优化好 - 无需额外数据结构 - 易于调试(可以在case中加日志) 缺点: - 添加状态需要修改多处代码 - 不如表驱动直观 LinuxPTP的考虑: - 状态数量固定(9个) - 事件数量固定(17个) - 状态转换规则稳定 - 选择嵌套switch更高效小结:状态机的设计智慧
函数式设计:
- 纯函数,无副作用
- 易于测试和调试
默认保持状态:
- 未处理的事件不改变状态
- 避免意外状态转换
快速路径处理:
- 特殊事件提前返回
- 减少嵌套深度
状态合并:
- 相似状态共享代码
- 减少代码重复
两种状态机:
- 完整状态机(ptp_fsm)
- 仅从状态机(ptp_slave_fsm)
- 满足不同需求
下集预告
状态机决定端口"要做什么",BMCA决定端口"要当什么"。
下一节,我们将分析BMCA算法实现——看看LinuxPTP如何选择主时钟。
【悬念留给3.4】
BMCA是PTP协议的核心算法。
它需要比较两个数据集,决定谁更适合当主时钟。
LinuxPTP的BMCA实现只有175行,包括:
- 数据集比较函数
- 状态决策函数
它是如何在这么少的代码中实现完整的BMCA?
下一节,我们详细解读。
📚本文内容摘自本人的开源书《PTP技术书 - 从思想实验到协议实现》
全书从时间本质的思想实验出发,深度解析 IEEE 1588 协议、逐章分析 LinuxPTP 源码,并带你动手实现一个轻量级 PTP 程序(ptp-lite)。
🔗 在线阅读/下载:ptp-book
gitclone https://github.com/Lularible/ptp-book.git⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。