文章目录
-
- [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_count
和tick_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授时精度有显著影响,为了进一步提高授时精度,需要进一步优化时钟管理接口或硬件支持。