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.5 伺服控制器:让时钟追上主人的艺术

同步的核心问题

当PTP端口进入SLAVE状态后,面临的核心问题是:

如何调整本地时钟,使其与主时钟保持同步?

这就是伺服控制器(Servo)的任务。


伺服控制器概述

伺服的作用

伺服控制器的输入:
- offset:本地时钟与主时钟的偏差(纳秒)
- local_ts:本地时间戳
- weight:样本权重

伺服控制器的输出:
- ppb:频率调整值(parts per billion,十亿分之一)

伺服控制器的目标:
- 最小化offset
- 使本地时钟频率与主时钟一致

LinuxPTP支持的伺服类型

/* servo.h, 第33-39行 */

enum servo_type {
    CLOCK_SERVO_PI,          /* PI控制器(最常用) */
    CLOCK_SERVO_LINREG,      /* 线性回归滤波器 */
    CLOCK_SERVO_NTPSHM,      /* NTP共享内存 */
    CLOCK_SERVO_NULLF,       /* 空滤波器 */
    CLOCK_SERVO_REFCLOCK_SOCK, /* 参考时钟套接字 */
};

各伺服类型的特点

PI控制器:
- 比例-积分控制器
- 经典的控制理论方法
- 参数可调,适应性强
- 适合大多数场景

线性回归滤波器:
- 基于统计学方法
- 自适应窗口大小
- 对异常值鲁棒
- 适合高精度场景

NTP共享内存:
- 与NTP共享时间信息
- 用于混合部署
- 不直接调整时钟

空滤波器:
- 不进行任何滤波
- 直接传递offset
- 用于调试或特殊用途

参考时钟套接字:
- 从外部参考时钟获取时间
- 用于GPS等外部源

伺服接口设计

基类设计

/* servo_private.h,第27-49行 */

struct servo {
    /* 公共属性 */
    double max_frequency;          /* 最大频率调整 */
    double step_threshold;         /* 阶跃阈值 */
    double first_step_threshold;   /* 首次阶跃阈值 */
    int first_update;              /* 是否首次更新 */
    int64_t offset_threshold;      /* 偏差阈值 */
    int num_offset_values;         /* 检查次数 */
    int curr_offset_values;        /* 当前计数 */

    /* 虚函数 */
    void (*destroy)(struct servo *servo);
    double (*sample)(struct servo *servo,
                     int64_t offset, uint64_t local_ts, double weight,
                     enum servo_state *state);
    void (*sync_interval)(struct servo *servo, double interval);
    void (*reset)(struct servo *servo);
    double (*rate_ratio)(struct servo *servo);
    void (*leap)(struct servo *servo, int leap);
};

面向对象设计

LinuxPTP用C语言实现了面向对象设计:

基类:struct servo
- 定义虚函数指针
- 定义公共属性

派生类:
- struct pi_servo(PI控制器)
- struct linreg_servo(线性回归)
- struct ntpshm_servo(NTP共享内存)
- ...

派生类包含基类作为第一个成员:
struct pi_servo {
    struct servo servo;  /* 基类 */
    /* 派生类特有成员 */
    double kp;
    double ki;
    ...
};

这样可以用基类指针操作派生类对象。

伺服创建函数

/* servo.c, 第33-93行 */

struct servo *servo_create(struct config *cfg, enum servo_type type,
                           double fadj, int max_ppb, int sw_ts)
{
    struct servo *servo;

    /* 步骤1:根据类型创建具体伺服 */
    switch (type) {
    case CLOCK_SERVO_PI:
        servo = pi_servo_create(cfg, fadj, sw_ts);
        break;
    case CLOCK_SERVO_LINREG:
        servo = linreg_servo_create(fadj);
        break;
    case CLOCK_SERVO_NTPSHM:
        servo = ntpshm_servo_create(cfg);
        break;
    case CLOCK_SERVO_NULLF:
        servo = nullf_servo_create();
        break;
    case CLOCK_SERVO_REFCLOCK_SOCK:
        servo = refclock_sock_servo_create(cfg);
        break;
    default:
        return NULL;
    }

    if (!servo)
        return NULL;

    /* 步骤2:设置公共参数 */
    servo_step_threshold = config_get_double(cfg, NULL, "step_threshold");
    if (servo_step_threshold > 0.0) {
        servo->step_threshold = servo_step_threshold * NSEC_PER_SEC;
    } else {
        servo->step_threshold = 0.0;
    }

    servo_first_step_threshold =
        config_get_double(cfg, NULL, "first_step_threshold");
    if (servo_first_step_threshold > 0.0) {
        servo->first_step_threshold =
            servo_first_step_threshold * NSEC_PER_SEC;
    } else {
        servo->first_step_threshold = 0.0;
    }

    servo->max_frequency = max_ppb;
    servo->first_update = 1;
    
    return servo;
}

采样函数

/* servo.c, 第116-150行 */

double servo_sample(struct servo *servo,
                    int64_t offset,
                    uint64_t local_ts,
                    double weight,
                    enum servo_state *state)
{
    double r;

    /* 调用具体伺服的采样函数 */
    r = servo->sample(servo, offset, local_ts, weight, state);

    /* 根据状态更新公共属性 */
    switch (*state) {
    case SERVO_UNLOCKED:
        servo->curr_offset_values = servo->num_offset_values;
        break;
    case SERVO_JUMP:
        servo->curr_offset_values = servo->num_offset_values;
        servo->first_update = 0;
        break;
    case SERVO_LOCKED:
        if (check_offset_threshold(servo, offset)) {
            *state = SERVO_LOCKED_STABLE;
        }
        servo->first_update = 0;
        break;
    case SERVO_LOCKED_STABLE:
        break;
    }

    return r;
}

PI控制器详解

PI控制器原理

PI控制器 = 比例(P)+ 积分(I)

控制方程:
output = Kp × offset + Ki × ∫offset dt + drift

其中:
- Kp:比例增益
- Ki:积分增益
- offset:当前偏差
- drift:累积的频率漂移

比例项(P):
- 响应当前偏差
- 偏差越大,调整越大
- 快速响应

积分项(I):
- 响应历史累积偏差
- 消除稳态误差
- 长期稳定

PI控制器结构

/* pi.c, 第38-56行 */

struct pi_servo {
    struct servo servo;     /* 基类 */
    
    /* 历史数据 */
    int64_t offset[2];      /* 最近两次offset */
    uint64_t local[2];      /* 最近两次本地时间戳 */
    
    /* 状态变量 */
    double drift;           /* 累积频率漂移 */
    double kp;              /* 当前比例增益 */
    double ki;              /* 当前积分增益 */
    double last_freq;       /* 上次频率调整 */
    int count;              /* 采样计数 */
    
    /* 配置参数 */
    double configured_pi_kp;
    double configured_pi_ki;
    double configured_pi_kp_scale;
    double configured_pi_kp_exponent;
    double configured_pi_kp_norm_max;
    double configured_pi_ki_scale;
    double configured_pi_ki_exponent;
    double configured_pi_ki_norm_max;
};

PI控制器采样函数

/* pi.c, 第64-155行 */

static double pi_sample(struct servo *servo,
                        int64_t offset,
                        uint64_t local_ts,
                        double weight,
                        enum servo_state *state)
{
    struct pi_servo *s = container_of(servo, struct pi_servo, servo);
    double ki_term, ppb = s->last_freq;
    double freq_est_interval, localdiff;

    switch (s->count) {
    case 0:
        /* 第一次采样:只记录数据 */
        s->offset[0] = offset;
        s->local[0] = local_ts;
        *state = SERVO_UNLOCKED;
        s->count = 1;
        break;

    case 1:
        /* 第二次采样:估计频率偏差 */
        s->offset[1] = offset;
        s->local[1] = local_ts;

        /* 确保时间顺序正确 */
        if (s->local[0] >= s->local[1]) {
            *state = SERVO_UNLOCKED;
            s->count = 0;
            break;
        }

        /* 检查时间间隔是否足够 */
        localdiff = (s->local[1] - s->local[0]) / 1e9;
        localdiff += localdiff * FREQ_EST_MARGIN;
        freq_est_interval = 0.016 / s->ki;
        if (freq_est_interval > 1000.0) {
            freq_est_interval = 1000.0;
        }
        if (localdiff < freq_est_interval) {
            *state = SERVO_UNLOCKED;
            break;
        }

        /* 估计频率偏差 */
        s->drift += (1e9 - s->drift) * (s->offset[1] - s->offset[0]) /
                                    (s->local[1] - s->local[0]);

        /* 限制频率范围 */
        if (s->drift < -servo->max_frequency)
            s->drift = -servo->max_frequency;
        else if (s->drift > servo->max_frequency)
            s->drift = servo->max_frequency;

        /* 判断是否需要阶跃 */
        if ((servo->first_update &&
             servo->first_step_threshold &&
             servo->first_step_threshold < llabs(offset)) ||
            (servo->step_threshold &&
             servo->step_threshold < llabs(offset)))
            *state = SERVO_JUMP;
        else
            *state = SERVO_LOCKED;

        ppb = s->drift;
        s->count = 2;
        break;

    case 2:
        /* 正常工作:PI控制 */
        
        /* 检查是否需要重置 */
        if (servo->step_threshold &&
            servo->step_threshold < llabs(offset)) {
            *state = SERVO_UNLOCKED;
            s->count = 0;
            break;
        }

        /* PI控制方程 */
        ki_term = s->ki * offset * weight;
        ppb = s->kp * offset * weight + s->drift + ki_term;
        
        /* 限制输出范围 */
        if (ppb < -servo->max_frequency) {
            ppb = -servo->max_frequency;
        } else if (ppb > servo->max_frequency) {
            ppb = servo->max_frequency;
        } else {
            s->drift += ki_term;  /* 更新积分项 */
        }
        
        *state = SERVO_LOCKED;
        break;
    }

    s->last_freq = ppb;
    return ppb;
}

三阶段采样详解

阶段0(count=0):初始化
- 记录第一个样本
- 状态:UNLOCKED
- 输出:上次频率(无新调整)

阶段1(count=1):频率估计
- 记录第二个样本
- 计算两个样本之间的频率偏差
- 公式:drift = (offset[1] - offset[0]) / (time[1] - time[0])
- 这是对本地振荡器频率偏差的估计
- 状态:LOCKED或JUMP
- 输出:估计的drift

阶段2(count=2):正常PI控制
- 每次采样都执行PI控制
- 公式:ppb = kp × offset + drift + ki × offset
- 状态:LOCKED
- 输出:PI控制器输出

PI参数调整

/* pi.c, 第157-171行 */

static void pi_sync_interval(struct servo *servo, double interval)
{
    struct pi_servo *s = container_of(servo, struct pi_servo, servo);

    /* 根据Sync间隔自动调整Kp */
    s->kp = s->configured_pi_kp_scale * 
            pow(interval, s->configured_pi_kp_exponent);
    if (s->kp > s->configured_pi_kp_norm_max / interval)
        s->kp = s->configured_pi_kp_norm_max / interval;

    /* 根据Sync间隔自动调整Ki */
    s->ki = s->configured_pi_ki_scale * 
            pow(interval, s->configured_pi_ki_exponent);
    if (s->ki > s->configured_pi_ki_norm_max / interval)
        s->ki = s->configured_pi_ki_norm_max / interval;

    pr_debug("PI servo: sync interval %.3f kp %.3f ki %.6f",
             interval, s->kp, s->ki);
}

自适应参数的意义

为什么要根据Sync间隔调整参数?

Sync间隔长:
- 两个样本之间间隔长
- 频率估计更准确
- 但响应慢
- 需要更大的Kp和Ki

Sync间隔短:
- 两个样本之间间隔短
- 频率估计可能不准
- 但响应快
- 需要更小的Kp和Ki

公式:
kp = kp_scale × interval^kp_exponent
ki = ki_scale × interval^ki_exponent

默认值:
- kp_scale = 0.7(硬件时间戳)或 0.1(软件时间戳)
- kp_exponent = 0.0(如果用户未指定)
- ki_scale = 0.3(硬件时间戳)或 0.001(软件时间戳)
- ki_exponent = 0.0(如果用户未指定)

这意味着:
- 默认情况下,Kp和Ki不随间隔变化
- 但用户可以通过配置文件设置指数

PI控制器创建

/* pi.c, 第180-231行 */

struct servo *pi_servo_create(struct config *cfg, double fadj, int sw_ts)
{
    struct pi_servo *s;

    s = calloc(1, sizeof(*s));
    if (!s)
        return NULL;

    /* 设置虚函数 */
    s->servo.destroy = pi_destroy;
    s->servo.sample  = pi_sample;
    s->servo.sync_interval = pi_sync_interval;
    s->servo.reset   = pi_reset;
    
    /* 初始化状态 */
    s->drift         = fadj;       /* 初始频率偏差 */
    s->last_freq     = fadj;
    s->kp            = 0.0;
    s->ki            = 0.0;
    
    /* 读取配置 */
    s->configured_pi_kp = config_get_double(cfg, NULL, "pi_proportional_const");
    s->configured_pi_ki = config_get_double(cfg, NULL, "pi_integral_const");
    s->configured_pi_kp_scale = config_get_double(cfg, NULL, "pi_proportional_scale");
    s->configured_pi_kp_exponent = config_get_double(cfg, NULL, "pi_proportional_exponent");
    s->configured_pi_kp_norm_max = config_get_double(cfg, NULL, "pi_proportional_norm_max");
    s->configured_pi_ki_scale = config_get_double(cfg, NULL, "pi_integral_scale");
    s->configured_pi_ki_exponent = config_get_double(cfg, NULL, "pi_integral_exponent");
    s->configured_pi_ki_norm_max = config_get_double(cfg, NULL, "pi_integral_norm_max");

    /* 如果用户指定了Kp和Ki,使用固定值 */
    if (s->configured_pi_kp && s->configured_pi_ki) {
        s->configured_pi_kp_scale = s->configured_pi_kp;
        s->configured_pi_ki_scale = s->configured_pi_ki;
        s->configured_pi_kp_exponent = 0.0;
        s->configured_pi_ki_exponent = 0.0;
        s->configured_pi_kp_norm_max = MAX_KP_NORM_MAX;
        s->configured_pi_ki_norm_max = MAX_KI_NORM_MAX;
    } 
    /* 否则使用自适应值 */
    else if (!s->configured_pi_kp_scale || !s->configured_pi_ki_scale) {
        if (sw_ts) {
            s->configured_pi_kp_scale = SWTS_KP_SCALE;  /* 0.1 */
            s->configured_pi_ki_scale = SWTS_KI_SCALE;  /* 0.001 */
        } else {
            s->configured_pi_kp_scale = HWTS_KP_SCALE;  /* 0.7 */
            s->configured_pi_ki_scale = HWTS_KI_SCALE;  /* 0.3 */
        }
    }

    return &s->servo;
}

线性回归滤波器详解

原理

线性回归滤波器的思想:
- 收集多个(offset, time)样本点
- 用线性回归拟合一条直线
- 直线的斜率 = 频率偏差
- 直线的截距 = 初始偏差

优点:
- 利用多个样本,减少噪声影响
- 自适应窗口大小
- 对异常值鲁棒

数学公式:
给定n个点(x_i, y_i),回归直线:
y = slope × x + intercept

其中:
slope = (n∑xy - ∑x∑y) / (n∑x² - (∑x)²)
intercept = (∑y - slope×∑x) / n

线性回归结构

/* linreg.c, 第58-84行 */

struct linreg_servo {
    struct servo servo;
    
    /* 样本缓冲区 */
    struct point points[MAX_POINTS];   /* 最多64个点 */
    struct point reference;            /* 参考点 */
    
    /* 缓冲区管理 */
    unsigned int num_points;           /* 当前点数 */
    unsigned int last_point;           /* 最新点索引 */
    
    /* 回归结果 */
    struct result results[MAX_SIZE - MIN_SIZE + 1]; /* 多个窗口大小 */
    unsigned int size;                 /* 当前窗口大小 */
    
    /* 状态变量 */
    double x_remainder;
    uint64_t last_update;
    double clock_freq;                 /* 当前频率 */
    double update_interval;
    double frequency_ratio;            /* 频率比 */
    int leap;                          /* 闰秒 */
};

线性回归采样

/* linreg.c中的采样函数(简化) */

static double linreg_sample(struct servo *servo,
                            int64_t offset,
                            uint64_t local_ts,
                            double weight,
                            enum servo_state *state)
{
    struct linreg_servo *s = container_of(servo, struct linreg_servo, servo);
    
    /* 步骤1:添加新样本点 */
    add_point(s, local_ts, offset, weight);
    
    /* 步骤2:执行多个窗口大小的回归 */
    for (i = 0; i < ARRAY_SIZE(s->results); i++) {
        regression(s, i, &slope, &intercept);
        s->results[i].slope = slope;
        s->results[i].intercept = intercept;
    }
    
    /* 步骤3:选择最佳窗口大小 */
    s->size = select_best_size(s);
    
    /* 步骤4:计算频率调整 */
    freq = s->results[s->size].slope;
    
    /* 步骤5:更新状态 */
    *state = s->num_points >= MIN_POINTS ? SERVO_LOCKED : SERVO_UNLOCKED;
    
    return freq;
}

自适应窗口选择

/* 选择最佳窗口大小 */

static unsigned int select_best_size(struct linreg_servo *s)
{
    double err, min_err = HUGE_VAL;
    unsigned int i, best = 0;
    
    /* 比较不同窗口大小的预测误差 */
    for (i = 0; i < ARRAY_SIZE(s->results); i++) {
        err = s->results[i].err;  /* 预测误差 */
        if (err < min_err) {
            min_err = err;
            best = i;
        }
    }
    
    return best;
}

自适应原理

线性回归滤波器维护多个窗口大小:
- 窗口大小:4, 8, 16, 32, 64(可配置)
- 每个窗口独立计算回归
- 跟踪每个窗口的预测误差
- 选择预测误差最小的窗口

好处:
- 样本少时,用小窗口(快速响应)
- 样本多时,用大窗口(更稳定)
- 自动适应不同场景

伺服状态详解

四种状态

/* servo.h, 第44-67行 */

enum servo_state {
    SERVO_UNLOCKED,       /* 未锁定 */
    SERVO_JUMP,           /* 需要跳变 */
    SERVO_LOCKED,         /* 已锁定 */
    SERVO_LOCKED_STABLE,  /* 稳定锁定 */
};

状态含义

SERVO_UNLOCKED:
- 伺服还没有足够的数据
- 或者偏差过大需要重置
- 不能可靠地调整时钟

SERVO_JUMP:
- 偏差超过阈值
- 需要时钟跳变
- 用于快速修正大偏差

SERVO_LOCKED:
- 伺服正在跟踪
- 偏差在正常范围
- 进行正常的频率调整

SERVO_LOCKED_STABLE:
- 连续多次偏差小于阈值
- 伺服已稳定
- 可以考虑降低Sync频率

状态转换

/* servo.c中的状态处理 */

double servo_sample(struct servo *servo, int64_t offset, ...)
{
    r = servo->sample(servo, offset, local_ts, weight, state);

    switch (*state) {
    case SERVO_UNLOCKED:
        /* 重置稳定计数 */
        servo->curr_offset_values = servo->num_offset_values;
        break;
        
    case SERVO_JUMP:
        /* 重置稳定计数 */
        servo->curr_offset_values = servo->num_offset_values;
        servo->first_update = 0;
        break;
        
    case SERVO_LOCKED:
        /* 检查是否达到稳定 */
        if (check_offset_threshold(servo, offset)) {
            *state = SERVO_LOCKED_STABLE;
        }
        servo->first_update = 0;
        break;
        
    case SERVO_LOCKED_STABLE:
        /* 这个状态只从这里设置 */
        break;
    }

    return r;
}

static int check_offset_threshold(struct servo *s, int64_t offset)
{
    long long int abs_offset = llabs(offset);

    if (s->offset_threshold) {
        if (abs_offset < s->offset_threshold) {
            if (s->curr_offset_values)
                s->curr_offset_values--;
        } else {
            s->curr_offset_values = s->num_offset_values;
        }
        return s->curr_offset_values ? 0 : 1;
    }
    return 0;
}

稳定检测逻辑

配置参数:
- servo_offset_threshold:偏差阈值
- servo_num_offset_values:检查次数

工作原理:
1. 每次采样,检查|offset|是否小于阈值
2. 如果小于,计数器减1
3. 如果大于,计数器重置
4. 当计数器减到0,进入LOCKED_STABLE

示例:
配置:offset_threshold = 100ns, num_offset_values = 5

样本1:|offset| = 80ns < 100ns → count = 4
样本2:|offset| = 90ns < 100ns → count = 3
样本3:|offset| = 150ns > 100ns → count = 5(重置)
样本4:|offset| = 70ns < 100ns → count = 4
样本5:|offset| = 60ns < 100ns → count = 3
样本6:|offset| = 80ns < 100ns → count = 2
样本7:|offset| = 90ns < 100ns → count = 1
样本8:|offset| = 85ns < 100ns → count = 0 → LOCKED_STABLE

这确保:
- 连续N次偏差都小于阈值才认为稳定
- 任何一次超差都会重置计数

阶跃阈值配置

阶跃的作用

当偏差很大时:
- 逐渐调整太慢
- 需要时钟跳变

阶跃阈值决定何时跳变:
- step_threshold:正常运行时的阈值
- first_step_threshold:首次同步的阈值

为什么区分首次和正常?

首次同步:
- 设备刚启动,偏差可能很大(毫秒甚至秒级)
- 允许更大的首次跳变
- 快速进入同步状态

正常运行:
- 偏差应该在纳秒级
- 只允许小的跳变
- 避免服务中断

配置示例

# /etc/linuxptp/ptp4l.conf

[global]
# 阈值为0表示禁用阶跃
step_threshold 0.0

# 首次阶跃阈值:100微秒
first_step_threshold 0.0001

# 正常阶跃阈值:10微秒
step_threshold 0.00001

# 最大频率调整:500000 ppb(500 ppm)
max_frequency 500000

实际效果

场景:设备刚启动,偏差 = 1秒

配置:
first_step_threshold = 0.1秒
step_threshold = 0.00001秒

处理:
1. |offset| = 1秒 > 0.1秒
2. 状态:SERVO_JUMP
3. 执行时钟跳变:clock_step(1秒)
4. 偏差变为0
5. 进入正常同步

后续运行中:
1. |offset| = 20微秒 > 10微秒
2. 状态:SERVO_JUMP
3. 执行时钟跳变:clock_step(20微秒)
4. 继续同步

配置参数详解

PI控制器参数

[global]

# 比例常数(固定模式)
pi_proportional_const 0.0

# 积分常数(固定模式)
pi_integral_const 0.0

# 比例缩放因子(自适应模式)
pi_proportional_scale 0.7

# 比例指数
pi_proportional_exponent 0.0

# 比例归一化最大值
pi_proportional_norm_max 1.0

# 积分缩放因子
pi_integral_scale 0.3

# 积分指数
pi_integral_exponent 0.0

# 积分归一化最大值
pi_integral_norm_max 2.0

两种配置模式

模式一:固定参数
pi_proportional_const = 0.7
pi_integral_const = 0.3

效果:
- Kp固定为0.7
- Ki固定为0.3
- 不随Sync间隔变化

模式二:自适应参数
pi_proportional_scale = 0.7
pi_proportional_exponent = 0.5

效果:
- Kp = 0.7 × interval^0.5
- Ki = 0.3 × interval^0.5
- 随Sync间隔自动调整

实际调优建议

硬件时间戳场景

[global]
# 适合硬件时间戳的默认参数
pi_proportional_scale 0.7
pi_integral_scale 0.3

# 阶跃阈值
step_threshold 0.00001  # 10微秒

# 稳定检测
servo_offset_threshold 100  # 100纳秒
servo_num_offset_values 5

软件时间戳场景

[global]
# 软件时间戳需要更保守的参数
pi_proportional_scale 0.1
pi_integral_scale 0.001

# 更大的阶跃阈值
step_threshold 0.001  # 1毫秒

高精度场景

[global]
# 更激进的参数
pi_proportional_scale 1.0
pi_integral_scale 0.5

# 更小的阶跃阈值
step_threshold 0.000001  # 1微秒
first_step_threshold 0.0001  # 100微秒

# 更严格的稳定要求
servo_offset_threshold 50  # 50纳秒
servo_num_offset_values 10

小结:伺服控制器的设计智慧

面向对象设计

  • 基类定义接口
  • 派生类实现具体算法
  • 统一的调用方式

三阶段采样

  • 初始化 → 频率估计 → 正常控制
  • 逐步进入稳定状态

自适应参数

  • 根据Sync间隔自动调整
  • 适应不同网络条件

多种算法选择

  • PI控制器:经典可靠
  • 线性回归:鲁棒性强
  • 空滤波器:调试用

下集预告

伺服控制器决定如何调整时钟,但调整需要通过PHC(PTP硬件时钟)接口。

下一节,我们将分析PHC操作和时钟调整——看看LinuxPTP如何与硬件时钟交互。

【悬念留给3.6】

PHC是Linux内核提供的PTP硬件时钟子系统。

LinuxPTP通过以下接口操作PHC:

  • clock_gettime:读取时间
  • clock_adjtime:调整频率
  • clock_settime:设置时间

这些操作封装在phc.c和clockadj.c中。

如何在用户态操作内核时钟?

下一节,我们详细解读。