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.4 BMCA算法实现:民主选举的代码艺术

从协议到代码

第二章我们详细讲解了BMCA(最佳主时钟算法)的原理。

现在,让我们看看这个算法在LinuxPTP中是如何实现的。


BMCA的核心函数

LinuxPTP的BMCA实现集中在bmc.c文件中,只有175行代码

核心函数列表

bmc.c提供的函数:

1. dscmp(struct dataset *a, struct dataset *b)
   - 比较两个数据集,决定哪个更适合当主时钟

2. dscmp2(struct dataset *a, struct dataset *b)
   - 第二阶段比较(当clockIdentity相同时)

3. portid_cmp(struct PortIdentity *a, struct PortIdentity *b)
   - 比较两个端口标识

4. bmc_state_decision(struct clock *c, struct port *r, ...)
   - 根据BMCA结果决定端口状态

数据集比较函数详解

portid_cmp:端口标识比较

/* bmc.c, 第24-33行 */

static int portid_cmp(struct PortIdentity *a, struct PortIdentity *b)
{
    int diff = memcmp(&a->clockIdentity, &b->clockIdentity, 
                      sizeof(a->clockIdentity));

    if (diff == 0) {
        diff = a->portNumber - b->portNumber;
    }

    return diff;
}

返回值含义

diff < 0:a < b(a排在前面)
diff = 0:a = b(完全相同)
diff > 0:a > b(b排在前面)

比较顺序

先比较clockIdentity(8字节):
- 如果不同,直接返回比较结果

如果clockIdentity相同,再比较portNumber(2字节):
- 返回端口号的差异

这意味着:
- 同一设备的两个端口,用端口号区分
- 不同设备的端口,用clockIdentity区分

dscmp:主比较函数

/* bmc.c, 第83-127行 */

int dscmp(struct dataset *a, struct dataset *b)
{
    int diff;

    /* 特殊情况:同一数据集 */
    if (a == b)
        return 0;
    
    /* 特殊情况:其中一个为空 */
    if (a && !b)
        return A_BETTER;
    if (b && !a)
        return B_BETTER;

    /* 比较clockIdentity */
    diff = memcmp(&a->identity, &b->identity, sizeof(a->identity));
    if (!diff)
        return dscmp2(a, b);  /* 相同,进入第二阶段 */

    /* 比较priority1 */
    if (a->priority1 < b->priority1)
        return A_BETTER;
    if (a->priority1 > b->priority1)
        return B_BETTER;

    /* 比较clockClass */
    if (a->quality.clockClass < b->quality.clockClass)
        return A_BETTER;
    if (a->quality.clockClass > b->quality.clockClass)
        return B_BETTER;

    /* 比较clockAccuracy */
    if (a->quality.clockAccuracy < b->quality.clockAccuracy)
        return A_BETTER;
    if (a->quality.clockAccuracy > b->quality.clockAccuracy)
        return B_BETTER;

    /* 比较offsetScaledLogVariance */
    if (a->quality.offsetScaledLogVariance <
        b->quality.offsetScaledLogVariance)
        return A_BETTER;
    if (a->quality.offsetScaledLogVariance >
        b->quality.offsetScaledLogVariance)
        return B_BETTER;

    /* 比较priority2 */
    if (a->priority2 < b->priority2)
        return A_BETTER;
    if (a->priority2 > b->priority2)
        return B_BETTER;

    /* 比较clockIdentity */
    return diff < 0 ? A_BETTER : B_BETTER;
}

返回值定义

/* bmc.h */

#define A_BETTER        1   /* A更好,选择A */
#define B_BETTER       -1   /* B更好,选择B */
#define A_BETTER_TOPO   2   /* A更好(拓扑原因) */
#define B_BETTER_TOPO  -2   /* B更好(拓扑原因) */

比较流程图

dscmp比较流程(IEEE 1588-2019标准):

┌────────────────────────────────────────┐
│ 0. 检查特殊情况                        │
│    - 同一数据集 → 0                    │
│    - 一个为空 → 选择非空的              │
├────────────────────────────────────────┤
│ 1. 比较clockIdentity                   │
│    - 相同 → 调用dscmp2                 │
│    - 不同 → 记录差异,继续              │
├────────────────────────────────────────┤
│ 2. 比较priority1                       │
│    - A小 → A_BETTER                    │
│    - B小 → B_BETTER                    │
│    - 相同 → 继续                        │
├────────────────────────────────────────┤
│ 3. 比较clockClass                      │
│    - A小 → A_BETTER                    │
│    - B小 → B_BETTER                    │
│    - 相同 → 继续                        │
├────────────────────────────────────────┤
│ 4. 比较clockAccuracy                   │
│    - A小 → A_BETTER                    │
│    - B小 → B_BETTER                    │
│    - 相同 → 继续                        │
├────────────────────────────────────────┤
│ 5. 比较offsetScaledLogVariance         │
│    - A小 → A_BETTER                    │
│    - B小 → B_BETTER                    │
│    - 相同 → 继续                        │
├────────────────────────────────────────┤
│ 6. 比较priority2                       │
│    - A小 → A_BETTER                    │
│    - B小 → B_BETTER                    │
│    - 相同 → 继续                        │
├────────────────────────────────────────┤
│ 7. 比较clockIdentity(使用之前记录的差异)│
│    - A小 → A_BETTER                    │
│    - B大 → B_BETTER                    │
└────────────────────────────────────────┘

关键点

为什么先检查clockIdentity?

    diff = memcmp(&a->identity, &b->identity, sizeof(a->identity));
    if (!diff)
        return dscmp2(a, b);

如果两个数据集的clockIdentity相同:
- 它们来自同一个设备
- 需要用dscmp2进行拓扑比较
- 判断哪个路径更短

如果clockIdentity不同:
- 它们来自不同设备
- 继续比较priority1等属性
- 最后用clockIdentity打破平局

dscmp2:拓扑比较函数

/* bmc.c, 第35-81行 */

int dscmp2(struct dataset *a, struct dataset *b)
{
    int diff;
    unsigned int A = a->stepsRemoved, B = b->stepsRemoved;

    /* 情况1:stepsRemoved差距大于1 */
    if (A + 1 < B)
        return A_BETTER;  /* A的跳数明显少 */
    if (B + 1 < A)
        return B_BETTER;  /* B的跳数明显少 */

    /* 情况2:A的跳数少1 */
    if (A < B) {
        diff = portid_cmp(&b->receiver, &b->sender);
        if (diff < 0)
            return A_BETTER;
        if (diff > 0)
            return A_BETTER_TOPO;
        return 0;  /* error-1 */
    }

    /* 情况3:B的跳数少1 */
    if (A > B) {
        diff = portid_cmp(&a->receiver, &a->sender);
        if (diff < 0)
            return B_BETTER;
        if (diff > 0)
            return B_BETTER_TOPO;
        return 0;  /* error-1 */
    }

    /* 情况4:跳数相同,比较sender */
    diff = portid_cmp(&a->sender, &b->sender);
    if (diff < 0)
        return A_BETTER_TOPO;
    if (diff > 0)
        return B_BETTER_TOPO;

    /* 情况5:比较receiver端口号 */
    if (a->receiver.portNumber < b->receiver.portNumber)
        return A_BETTER_TOPO;
    if (a->receiver.portNumber > b->receiver.portNumber)
        return B_BETTER_TOPO;

    /* error-2 */
    return 0;
}

拓扑比较的逻辑

场景:同一设备的多个Announce报文(clockIdentity相同)

为什么要进行拓扑比较?

因为:
- 同一设备可能通过不同路径发送Announce
- 有些路径更短(stepsRemoved更小)
- 需要选择最短路径

步骤1:比较stepsRemoved
- 如果差距大于1,选择跳数少的
- 如果差距小于等于1,需要进一步分析

步骤2:分析拓扑关系
- 检查receiver和sender的关系
- 判断是否存在环路

步骤3:比较sender和receiver
- 选择端口号小的
- 确保决策一致性

图解dscmp2

场景一:stepsRemoved差距大

网络拓扑:
主时钟 → BC1 → BC2 → 端口A(stepsRemoved = 3)
主时钟 → BC3 → 端口B(stepsRemoved = 2)

比较:
A的stepsRemoved = 3
B的stepsRemoved = 2

判断:
B + 1 = 3 = A
不满足"A + 1 < B"
但满足"B < A"

结果:B_BETTER(B更接近主时钟)


场景二:stepsRemoved差距小

网络拓扑:
主时钟 → BC1 → 端口A(stepsRemoved = 2)
主时钟 → BC2 → 端口A(stepsRemoved = 1)

两个Announce来自同一个端口A,但stepsRemoved不同。

如果stepsRemoved差1:
- 检查拓扑关系
- 判断哪个路径更合理

状态决策函数

bmc_state_decision函数

/* bmc.c, 第129-175行 */

enum port_state bmc_state_decision(struct clock *c, struct port *r,
                                   int (*compare)(struct dataset *a, 
                                                  struct dataset *b))
{
    struct dataset *clock_ds, *clock_best, *port_best;
    enum port_state ps;

    clock_ds = clock_default_ds(c);        /* 本时钟的数据集 */
    clock_best = clock_best_foreign(c);    /* 全局最佳外部时钟 */
    port_best = port_best_foreign(r);      /* 本端口最佳外部时钟 */
    ps = port_state(r);                    /* 当前端口状态 */

    /* 特殊情况:BMCA_NOOP模式 */
    if (!port_best && port_bmca(r) == BMCA_NOOP) {
        return ps;
    }

    /* 特殊情况:LISTENING状态没有外部时钟 */
    if (!port_best && PS_LISTENING == ps)
        return ps;

    /* 规则M1/P1:clockClass <= 127 */
    if (clock_class(c) <= 127) {
        if (compare(clock_ds, port_best) > 0) {
            return PS_GRAND_MASTER; /* M1 */
        } else {
            return PS_PASSIVE;      /* P1 */
        }
    }

    /* 规则M2:本时钟比全局最佳更好 */
    if (compare(clock_ds, clock_best) > 0) {
        return PS_GRAND_MASTER; /* M2 */
    }

    /* 规则S1:本端口是最佳端口 */
    if (clock_best_port(c) == r) {
        return PS_SLAVE; /* S1 */
    }

    /* 规则P2/M3:比较全局最佳和端口最佳 */
    if (compare(clock_best, port_best) == A_BETTER_TOPO) {
        return PS_PASSIVE; /* P2 */
    } else {
        return PS_MASTER;  /* M3 */
    }
}

IEEE 1588标准的状态决策规则

规则M1:
- 本时钟的clockClass <= 127
- 本时钟比端口最佳外部时钟更好
- 结果:成为Grand Master

规则P1:
- 本时钟的clockClass <= 127
- 本时钟不如端口最佳外部时钟
- 结果:Passive(避免环路)

规则M2:
- 本时钟比全局最佳外部时钟更好
- 结果:成为Grand Master

规则S1:
- 本端口是全局最佳外部时钟的来源
- 结果:成为Slave

规则P2:
- 全局最佳比端口最佳拓扑更好
- 结果:Passive(环路避免)

规则M3:
- 其他情况
- 结果:成为Master

状态决策流程图

bmc_state_decision流程:

┌──────────────────────────────────────────────────────────┐
│ 开始                                                      │
└──────────────────────────────────────────────────────────┘
                         │
                         ▼
┌──────────────────────────────────────────────────────────┐
│ port_best为空 且 BMCA_NOOP?                              │
│ → 是:保持当前状态                                         │
└──────────────────────────────────────────────────────────┘
                         │ 否
                         ▼
┌──────────────────────────────────────────────────────────┐
│ port_best为空 且 LISTENING?                              │
│ → 是:保持LISTENING                                        │
└──────────────────────────────────────────────────────────┘
                         │ 否
                         ▼
┌──────────────────────────────────────────────────────────┐
│ clockClass <= 127?                                       │
│ → 是:                                                    │
│    compare(clock_ds, port_best) > 0?                     │
│    → 是:PS_GRAND_MASTER (M1)                             │
│    → 否:PS_PASSIVE (P1)                                  │
└──────────────────────────────────────────────────────────┘
                         │ 否
                         ▼
┌──────────────────────────────────────────────────────────┐
│ compare(clock_ds, clock_best) > 0?                       │
│ → 是:PS_GRAND_MASTER (M2)                                │
└──────────────────────────────────────────────────────────┘
                         │ 否
                         ▼
┌──────────────────────────────────────────────────────────┐
│ clock_best_port(c) == r?                                 │
│ → 是:PS_SLAVE (S1)                                       │
└──────────────────────────────────────────────────────────┘
                         │ 否
                         ▼
┌──────────────────────────────────────────────────────────┐
│ compare(clock_best, port_best) == A_BETTER_TOPO?         │
│ → 是:PS_PASSIVE (P2)                                     │
│ → 否:PS_MASTER (M3)                                      │
└──────────────────────────────────────────────────────────┘

外部时钟管理

foreign_clock结构

/* foreign.h */

struct foreign_clock {
    struct port *port;              /* 所属端口 */
    struct PortIdentity identity;   /* 外部时钟标识 */
    struct dataset dataset;         /* 外部时钟数据集 */
    
    /* Announce报文队列 */
    LIST_ENTRY(foreign_clock) list;
};

端口的外部时钟列表

/* port_private.h */

struct port {
    /* ... */
    
    struct foreign_clock *best;    /* 最佳外部时钟 */
    LIST_HEAD(foreign_clocks, foreign_clock) foreign;  /* 外部时钟列表 */
    
    /* ... */
};

外部时钟管理函数

/* foreign.c中的核心函数(简化) */

/* 添加外部时钟Announce */
struct foreign_clock *foreign_add(struct port *p, struct ptp_message *msg)
{
    struct foreign_clock *fc;
    
    /* 分配内存 */
    fc = calloc(1, sizeof(*fc));
    
    /* 初始化 */
    fc->port = p;
    fc->identity = msg->announce.hdr.sourcePortIdentity;
    
    /* 添加到列表 */
    LIST_INSERT_HEAD(&p->foreign, fc, list);
    
    return fc;
}

/* 查找外部时钟 */
struct foreign_clock *foreign_lookup(struct port *p, 
                                     struct PortIdentity *identity)
{
    struct foreign_clock *fc;
    
    LIST_FOREACH(fc, &p->foreign, list) {
        if (pid_eq(&fc->identity, identity))
            return fc;
    }
    
    return NULL;
}

/* 更新外部时钟 */
void foreign_update(struct foreign_clock *fc, struct ptp_message *msg)
{
    /* 更新数据集 */
    fc->dataset.priority1 = msg->announce.grandmasterPriority1;
    fc->dataset.quality = msg->announce.grandmasterClockQuality;
    /* ... */
}

计算最佳外部时钟

/* port.c中的port_compute_best(简化) */

struct foreign_clock *port_compute_best(struct port *p)
{
    struct foreign_clock *fc, *best = NULL;
    int (*compare)(struct dataset *a, struct dataset *b);
    
    compare = clock_dscmp(p->clock);
    
    /* 遍历所有外部时钟 */
    LIST_FOREACH(fc, &p->foreign, list) {
        if (!best || compare(&fc->dataset, &best->dataset) > 0) {
            best = fc;
        }
    }
    
    p->best = best;
    return best;
}

BMCA的完整流程

1. Announce报文接收

/* port.c中的Announce处理(简化) */

static int port_rx_announce(struct port *p, struct ptp_message *msg)
{
    struct foreign_clock *fc;
    
    /* 步骤1:查找或创建外部时钟 */
    fc = foreign_lookup(p, &msg->hdr.sourcePortIdentity);
    if (!fc) {
        fc = foreign_add(p, msg);
    }
    
    /* 步骤2:更新外部时钟信息 */
    foreign_update(fc, msg);
    
    /* 步骤3:重启Announce超时定时器 */
    timer_restart(&p->timers[ANNOUNCE_TIMER]);
    
    /* 步骤4:触发状态决策事件 */
    port_dispatch(p, EV_STATE_DECISION_EVENT, 0);
    
    return 0;
}

2. 状态决策事件处理

/* port.c中的状态决策事件处理(简化) */

static void port_state_decision(struct port *p)
{
    struct foreign_clock *best;
    enum port_state next;
    int mdiff = 0;
    
    /* 步骤1:计算最佳外部时钟 */
    best = port_compute_best(p);
    
    /* 步骤2:检查主时钟是否变化 */
    if (best && p->best && 
        !pid_eq(&best->identity, &p->best->identity)) {
        mdiff = 1;  /* 主时钟变了 */
    }
    
    /* 步骤3:执行BMCA状态决策 */
    next = bmc_state_decision(p->clock, p, clock_dscmp(p->clock));
    
    /* 步骤4:转换为事件 */
    enum fsm_event event = state_to_event(next);
    
    /* 步骤5:触发状态机 */
    port_dispatch(p, event, mdiff);
}

3. 状态到事件转换

/* port.c中的状态到事件转换(简化) */

static enum fsm_event state_to_event(enum port_state next)
{
    switch (next) {
    case PS_GRAND_MASTER:
        return EV_RS_GRAND_MASTER;
    case PS_MASTER:
        return EV_RS_MASTER;
    case PS_SLAVE:
        return EV_RS_SLAVE;
    case PS_PASSIVE:
        return EV_RS_PASSIVE;
    default:
        return EV_NONE;
    }
}

BMCA时序图

时间轴上的BMCA过程:

t=0: 端口启动
     │
     ▼
t=1: 进入INITIALIZING状态
     │
     ▼
t=2: 初始化完成,进入LISTENING状态
     │
     │ 启动Announce超时定时器
     │
     ▼
t=3: 收到Announce报文(来自外部时钟A)
     │
     │ 创建foreign_clock结构
     │ 更新外部时钟信息
     │ 重启Announce超时定时器
     │
     ▼
t=4: 计算最佳外部时钟
     │
     │ 比较本时钟与外部时钟A
     │ 决定状态(假设本时钟更好)
     │
     ▼
t=5: 进入PRE_MASTER状态
     │
     │ 启动资格超时定时器
     │
     ▼
t=6: 资格超时
     │
     ▼
t=7: 进入MASTER/GRAND_MASTER状态
     │
     │ 开始发送Announce
     │ 开始发送Sync
     │

假设t=10收到更好的外部时钟:

t=10: 收到Announce报文(来自外部时钟B)
      │
      │ B的priority1 < 本时钟的priority1
      │
      ▼
t=11: 状态决策:B更好
      │
      ▼
t=12: 进入UNCALIBRATED状态
      │
      │ 停止发送Announce
      │ 开始同步到B
      │
      ▼
t=13: 同步完成
      │
      ▼
t=14: 进入SLAVE状态

小结:BMCA的代码智慧

分层设计

  • 数据集比较(dscmp/dscmp2)
  • 状态决策(bmc_state_decision)
  • 外部时钟管理(foreign_clock)

函数式编程

  • 比较函数作为参数传递
  • 无副作用,易测试

标准映射

  • 代码逻辑与IEEE 1588规则一一对应
  • 注释标注规则编号(M1、P1、S1等)

效率优化

  • 快速路径处理
  • 避免不必要的比较

下集预告

BMCA决定谁当主时钟,伺服控制器决定如何调整时钟。

下一节,我们将分析伺服控制器实现——看看LinuxPTP如何实现PI控制器。

【悬念留给3.5】

时钟同步需要伺服控制器。

LinuxPTP实现了多种伺服:

  • PI控制器(最常用)
  • 线性回归滤波器
  • 空滤波器

其中PI控制器只有231行代码,却实现了:

  • 频率估计
  • 相位调整
  • 阶跃检测

它是如何工作的?

下一节,我们详细解读伺服控制器。