Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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?

下一节,我们详细解读。