PTPD 在 QNX 系统上的授时精度验证与误差排查

文章目录

    • [0. 引言](#0. 引言)
    • 1.关键函数实现
    • [2. 验证策略与结果](#2. 验证策略与结果)
    • [3. 授时误差的排查与解决](#3. 授时误差的排查与解决)
    • [3. 授时误差的排查与解决](#3. 授时误差的排查与解决)
    • [4. 结论](#4. 结论)

0. 引言

PTPD是一种时间同步的开源实现,在不同操作系统上的表现可能存在显著差异。

本文通过在QNX系统上运行PTPD,针对其授时精度进行详细验证,并对出现的误差进行深入排查和分析,旨在提升QNX系统中的时间同步精度。

1.关键函数实现

在QNX系统上运行PTPD进行时间同步时,我们经过一系列调试和优化,采用PTP4L作为主时钟(软时钟)和PTPD2作为从时钟(软时钟)。

在收发PTP event报文时,我们发现原始PTPD代码使用的SO_TIMESTAMP从CMSH_DATA中获取时间戳数据更新周期为8ms,导致0-8ms的误差。为了减少这种误差,我们改为在应用层直接获取当前系统时间,将其作为报文的时间戳。

接收和发送报文时获取系统时间的关键实现如下:

cpp 复制代码
void getTime(TimeInternal *time) {
    struct timespec tp_now;
    if (clock_gettime(CLOCK_REALTIME, &tp_now) < 0) {
        PERROR("clock_gettime() failed, exiting.");
        exit(0);
    }
    time->seconds = tp_now.tv_sec;
    time->nanoseconds = tp_now.tv_nsec;
    return;
}

ssize_t netRecvEvent(Octet *buf, TimeInternal *time, NetPath *netPath, int flags) {
    ssize_t ret = 0;
    struct msghdr msg;
    struct iovec vec[1];
    struct sockaddr_in fromaddr;

#if defined(_QNXNTO) && defined(PTPD_EXPERIMENTAL)
    TimeInternal tmpTime;
    getTime(&tmpTime);
    *time = tmpTime;
#endif

    return ret;
}

ssize_t netSendEvent(Octet *buf, UInteger16 length, NetPath *netPath, const RunTimeOpts *rtOpts, Integer32 destinationAddress, TimeInternal *time) {
    ssize_t ret;
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PTP_EVENT_PORT);

#if defined(_QNXNTO) && defined(PTPD_EXPERIMENTAL)
    TimeInternal tmpTime;
    getTime(&tmpTime);
    *time = tmpTime;
#endif

    return ret;
}

2. 验证策略与结果

为了验证这种时间戳处理策略的效果,我们使用clockdiff脚本测量了主从时钟之间的误差,并通过一系列实验确定了授时精度。实验结果显示,主从时钟之间的offset基本在几十到几百微秒,未超过1ms。这些结果表明在QNX系统上采取的改进方法有效地降低了时间同步的误差。

为进一步验证时间戳更新周期的影响,我们开发了一个简单的C程序so_timestamp.c,不断循环获取SO_TIMESTAMP的值并打印出来。以下是该程序的简化代码示例:

c 复制代码
// so_timestamp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <inttypes.h>
#include <time.h>
#include <sys/select.h>
#include <sys/ioctl.h>
#include <netinet/tcp.h>
#include <netinet/if_ether.h>
#include <netinet/ip.h>
#ifdef _QNX_
#include <sys/neutrino.h>
#endif
#define log(fmt, ...) printf(fmt "\n", ##__VA_ARGS__);
int create_server_socket() {
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        log("cannot create socket");
        return -1;
    }
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = 8080;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0) {
        log("cannot bind socket");
        return -1;
    }
    int optval = 1;
    ret = setsockopt(sock, SOL_SOCKET, SO_TIMESTAMP, &optval, sizeof(optval));
    if (ret < 0) {
        log("cannot setsockopt SO_TIMESTAMP");
        return -1;
    }
    return sock;
}
int destroy_socket(int sock) {
    if (sock < 0) {
        log("invalid socket");
        return -1;
    }
    int ret = close(sock);
    if (ret < 0) {
        log("cannot close socket");
        return -1;
    }
    return 0;
}
int64_t get_so_timestampns(int sock) {
    struct msghdr msg;
    struct iovec iov;
    char cmsgbuf[4096];
    char buf[1024];
    struct cmsghdr *cmsg;
    struct timeval *tv;
    int ret;
    memset(buf, 0, sizeof(buf));
    memset(&msg, 0, sizeof(msg));
    memset(&iov, 0, sizeof(iov));
    memset(cmsgbuf, 0, sizeof(cmsgbuf));
    iov.iov_base = buf;
    iov.iov_len = sizeof(buf);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cmsgbuf;
    msg.msg_controllen = sizeof(cmsgbuf);
    ret = recvmsg(sock, &msg, 0);
    if (ret < 0) {
        log("cannot recvmsg %s", strerror(errno));
        return -1;
    }
    write(sock, buf, strlen(buf));
    for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
#ifdef _QNX_
        if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMP) {
#else
        if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMP) {
#endif
            tv = (struct timeval *)CMSG_DATA(cmsg);
            return tv->tv_sec * 1000000000 + tv->tv_usec * 1000;
        }
    }
    log("cannot find SCM_TIMESTAMP");
    return -1;
}
int send_to_sock(int sock, const char* buf, int len) {
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = 8080;
    // localhost
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    int ret = sendto(sock, buf, len, 0, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0) {
        log("cannot sendto");
        return -1;
    }
    return 0;
}
int main() {
#ifdef _QNX_
    struct _clockperiod period;
    period.nsec = 10000;
    period.fract = 0;
    ClockPeriod(CLOCK_REALTIME, &period, NULL, 0);
#endif
    int sock = create_server_socket();
    if (sock < 0) {
        return -1;
    }
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(sock, &rfds);
	struct timeval timeOut = {0,0};
    const char* buf = "hello world";
    int64_t sec, msec, usec, nsec;
    int64_t ns = -1;
    int ret = -1;
    for (;;) {
        ret = send_to_sock(sock, buf, strlen(buf));
        if (ret < 0) {
            return -1;
        }
        ret = select(sock + 1, &rfds, NULL, NULL, &timeOut);
        if (ret < 0) {
            log("cannot select");
            return -1;
        }
        ns = get_so_timestampns(sock);
        if (ns < 0) {
            return -1;
        }
        sec = ns / 1000000000;
        msec = (ns % 1000000000) / 1000000;
        usec = (ns % 1000000) / 1000;
        nsec = ns % 1000;
        log("ts: %ld.%03ld.%03ld.%03ld", sec, msec, usec, nsec);
    }
    destroy_socket(sock);
}

以上程序会持续输出时间戳,显示其在一段时间内保持不变,然后突变,突变增量大约为8ms。通过这种方式,我们能直观地观察到时间戳的更新频率和模式,为我们的误差分析提供了实验数据。

如上图所示,是 clockdiff 测出的授时误差统计直方图。横轴表示授时误差,纵轴表示统计计数。可以看到,从 -8ms 到 0ms 的误差都有,而且分布比较均匀。

3. 授时误差的排查与解决

为了提高这段文字的清晰度和逻辑性,我们可以调整其结构和表述,使其更为精准和易于理解。下面是优化后的版本:


3. 授时误差的排查与解决

在对QNX系统中的PTPD进行授时精度测试时,我们发现存在显著的授时误差,最小误差也达到10毫秒左右。更为严重的是,在计算同步(sync)报文的发送和接收时间差时,我们观察到的时间偏差竟高达几百毫秒,这远远超出了正常范围。初步调查表明,这一问题可能与QNX系统的时钟频率调节接口有关。

进一步的诊断显示,主时钟上的时间误差本身接近几百毫秒。我们使用的SO_TIMESTAMPING机制的更新周期长达8毫秒,这成为误差的主要原因。我们发现,QNX系统在获取时间戳时无法有效触发中断,导致时间戳保持不变,并且系统每次都会进行时钟步进(clock step),从而产生较大的误差。

QNX系统中的clockAdjust接口允许通过设置tick_counttick_nsec_inc来调整系统时钟,具体调整方法如下:

  • 设定每个时钟周期(tick)为10000纳秒。当tick_count设为100,tick_nsec_inc设为10时,在接下来的100个周期中,每个周期时长会增加至10010纳秒,从而加速时钟。
  • 如果需要减缓时钟速度,则将tick_nsec_inc设置为负值。

以下是调整时钟的示例代码:

cpp 复制代码
printf("QNX: adj: %.9f, dt: %.9f, ticks per dt: %d, inc per tick %d\n", adj, ptpClock->servo.dT, clockadj.tick_count, clockadj.tick_nsec_inc);

if (ClockAdjust(CLOCK_REALTIME, &clockadj, NULL) < 0) {
    printf("QNX: failed to call ClockAdjust: %s\n", strerror(errno));
}

clockAdjust操作基于时钟周期(tick),其最小分辨率为10000纳秒。这个接口的灵活性使得我们能够通过调整时钟的运行速度来尝试修正授时误差。

通过分析,我们确认QNX系统在获取时间戳时的局限性是主要误差来源。这要求我们进一步优化时钟管理接口或寻求硬件支持以改进授时精度。

4. 结论

通过对QNX系统和Linux系统上运行PTPD的对比分析,我们确认了QNX系统对系统时钟频率调节的局限性是影响授时精度的主要因素。采用相同代码编译的ARM Linux版本PTPD,在Linux系统上授时精度达到几十微秒,进一步证明了问题所在。

通过分析和测试,发现QNX系统时钟频率调节的局限性对PTPD授时精度有显著影响,为了进一步提高授时精度,需要进一步优化时钟管理接口或硬件支持。

相关推荐
橘色的喵6 天前
GNSS和PTP时间同步的基础原理介绍
gnss·phy·ptp·时间同步·gptp
橘色的喵25 天前
使用Python实现系统时间跳变检测与日志记录
python·时间同步·timestamp·时间跳变·时间回退·jump·time jump
DeanLeung1 个月前
CentOS 7.9 局域网配置指定同步时间服务器
centos·时间同步·ntp
极客先躯1 个月前
高级java每日一道面试题-2024年9月26日-运维篇[分布式篇]-如何保证每个服务器的时间都是同步的?
linux·运维·分布式·面试·时间同步·网络时间协议(ntp)·精密时间协议(ptp)
林多2 个月前
【Qnx】使用ClockCycles完成计时功能
超时·定时器·时钟周期·qnx·clockcycles
安徽京准2 个月前
京准电钟:NTP授时服务器(卫星授时)助力云计算
运维·服务器·云计算·时间同步·卫星授时服务·网络授时服务·ntp时钟同步
橘色的喵4 个月前
自定义线程管理类:解决std::thread()线程创建失败的优化方案
线程·thread·qnx·eagain·task_size
安徽京准5 个月前
京准电钟 NTP时间同步服务器助力水库水坝水利自动化建设
卫星时钟服务器·ntp授时服务器·时间同步·时钟同步·ntp时间服务器·时钟服务器·时间服务器
安徽京准5 个月前
京准电钟|基于纳秒级的GPS北斗卫星授时服务器
卫星授时系统·网络时钟同步·时钟同步·时间同步系统·时钟服务器·ntp服务器