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行 */
enum port_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行 */
enum fsm_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行 */
enum port_state ptp_fsm(enum port_state state, enum fsm_event event, int mdiff)
{
enum port_state next = state;
/* 特殊处理:初始化事件 */
if (EV_INITIALIZE == event || EV_POWERUP == event)
return PS_INITIALIZING;
/* 状态转换表 */
switch (state) {
case PS_INITIALIZING:
/* ... */
break;
case PS_FAULTY:
/* ... */
break;
/* ... 其他状态 */
}
return next;
}
设计亮点:
亮点一:函数式设计
状态机是一个纯函数:
- 输入:当前状态 + 事件 + mdiff
- 输出:下一状态
- 无副作用
好处:
- 可测试性强
- 易于理解
- 便于调试
亮点二:默认保持当前状态
enum port_state next = state;
如果事件不被处理,状态保持不变。
这避免了复杂的错误处理。
亮点三:快速路径处理
if (EV_INITIALIZE == event || EV_POWERUP == event)
return PS_INITIALIZING;
无论当前什么状态,初始化事件都回到INITIALIZING。
避免在每个case中重复处理。
状态转换详解
INITIALIZING状态
/* fsm.c, 第29-40行 */
case PS_INITIALIZING:
switch (event) {
case EV_FAULT_DETECTED:
next = PS_FAULTY;
break;
case EV_INIT_COMPLETE:
next = PS_LISTENING;
break;
default:
break;
}
break;
状态转换图:
INITIALIZING
│
├─ EV_FAULT_DETECTED ──→ PS_FAULTY
│
└─ EV_INIT_COMPLETE ──→ PS_LISTENING
说明:
- 初始化过程中检测到故障 → 进入故障状态
- 初始化完成 → 进入监听状态,开始接收Announce
LISTENING状态
/* fsm.c, 第60-86行 */
case PS_LISTENING:
switch (event) {
case EV_DESIGNATED_DISABLED:
next = PS_DISABLED;
break;
case EV_FAULT_DETECTED:
next = PS_FAULTY;
break;
case EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:
next = PS_MASTER;
break;
case EV_RS_MASTER:
next = PS_PRE_MASTER;
break;
case EV_RS_GRAND_MASTER:
next = PS_GRAND_MASTER;
break;
case EV_RS_SLAVE:
next = PS_UNCALIBRATED;
break;
case EV_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行 */
case PS_PRE_MASTER:
switch (event) {
case EV_DESIGNATED_DISABLED:
next = PS_DISABLED;
break;
case EV_FAULT_DETECTED:
next = PS_FAULTY;
break;
case EV_QUALIFICATION_TIMEOUT_EXPIRES:
next = PS_MASTER;
break;
case EV_RS_SLAVE:
next = PS_UNCALIBRATED;
break;
case EV_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行 */
case PS_MASTER:
case PS_GRAND_MASTER: /* 两个状态处理相同 */
switch (event) {
case EV_DESIGNATED_DISABLED:
next = PS_DISABLED;
break;
case EV_FAULT_DETECTED:
next = PS_FAULTY;
break;
case EV_RS_SLAVE:
next = PS_UNCALIBRATED;
break;
case EV_RS_PASSIVE:
next = PS_PASSIVE;
break;
default:
break;
}
break;
代码合并技巧:
case PS_MASTER:
case PS_GRAND_MASTER:
/* 两个状态共享同一套处理逻辑 */
这是C语言的switch特性:
- 多个case可以共享同一个代码块
- 减少代码重复
- 提高可维护性
SLAVE状态
/* fsm.c, 第186-216行 */
case PS_SLAVE:
switch (event) {
case EV_DESIGNATED_DISABLED:
next = PS_DISABLED;
break;
case EV_FAULT_DETECTED:
next = PS_FAULTY;
break;
case EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:
next = PS_MASTER;
break;
case EV_SYNCHRONIZATION_FAULT:
next = PS_UNCALIBRATED;
break;
case EV_RS_MASTER:
next = PS_PRE_MASTER;
break;
case EV_RS_GRAND_MASTER:
next = PS_GRAND_MASTER;
break;
case EV_RS_SLAVE:
if (mdiff) /* 主时钟变化了 */
next = PS_UNCALIBRATED;
break;
case EV_RS_PASSIVE:
next = PS_PASSIVE;
break;
default:
break;
}
break;
mdiff参数的意义:
case EV_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行 */
enum port_state ptp_slave_fsm(enum port_state state, enum fsm_event event,
int mdiff)
{
/* ... 与ptp_fsm类似,但限制了部分状态转换 */
}
与主状态机的区别:
ptp_fsm(完整状态机):
- 可以成为主时钟
- 可以成为从时钟
- 可以进入所有状态
ptp_slave_fsm(仅从状态机):
- 永远不能成为主时钟
- 忽略EV_RS_MASTER和EV_RS_GRAND_MASTER事件
- 只能在SLAVE、UNCALIBRATED、LISTENING之间切换
适用场景:
- slaveOnly = TRUE的设备
- 不想参与BMCA的终端设备
关键区别示例
/* ptp_slave_fsm中的LISTENING状态 */
case PS_LISTENING:
switch (event) {
case EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:
case EV_RS_MASTER:
case EV_RS_GRAND_MASTER:
case EV_RS_PASSIVE:
next = PS_LISTENING; /* 保持LISTENING,不当主时钟 */
break;
case EV_RS_SLAVE:
next = PS_UNCALIBRATED;
break;
}
break;
对比主状态机:
/* ptp_fsm中的LISTENING状态 */
case PS_LISTENING:
switch (event) {
case EV_ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES:
next = PS_MASTER; /* 可以成为主时钟 */
break;
case EV_RS_MASTER:
next = PS_PRE_MASTER; /* 可以成为主时钟 */
break;
case EV_RS_GRAND_MASTER:
next = PS_GRAND_MASTER; /* 可以成为主时钟 */
break;
}
break;
状态机的使用
在端口中调用状态机
/* port.c中的状态决策(简化) */
void port_dispatch(struct port *p, enum fsm_event event, int mdiff)
{
enum port_state next;
/* 调用状态机 */
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中的状态进入动作(简化) */
static void port_state_enter(struct port *p)
{
switch (p->state) {
case PS_INITIALIZING:
port_init(p);
break;
case PS_LISTENING:
port_start_listening(p);
break;
case PS_MASTER:
case PS_GRAND_MASTER:
port_start_master(p);
break;
case PS_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?
下一节,我们详细解读。