一、介绍
1.1 ntp 介绍
NTP(Network Time Protocol,网络时间协议)是一种用于在计算机网络中同步时钟的协议,旨在确保网络中所有计算机和设备具有相同的准确时间。
NTP 可以达到以下精度:
在局域网 (LAN) 环境中,NTP 可以达到毫秒级别(通常能够实现1毫秒内)甚至亚毫秒级别的精度。
在广域网 (WAN) 环境中,NTP 的精度通常在几毫秒到几十毫秒之间,取决于网络条件。
官方历史版本和协议规范:
-
NTPv0:
- 最初的 NTP 版本,虽然详细文档不如后续版本那么易于获取,但该版本标志着 NTP 的开端。
-
NTPv1:
- 文档: RFC 958(1985年)
- 描述了早期的 NTP 协议,包括其基本结构和操作。
-
NTPv2:
- 文档: RFC 1059(1988年)
- 引入了更详细的报头格式和错误检测机制,增强了协议的健壮性。
-
NTPv3:
- 文档: RFC 1305(1992年)
- 增加了广播模式和多播模式,支持了对称和非对称的通信方式,增强了安全性。引入了许多现代化特性,如对原子钟和 GPS 的支持。
-
NTPv4:
- 文档: RFC 5905(2010年)
- 目前广泛使用的版本,支持 64 位时间戳,提供了增强的安全性、灵活性和更好的时间同步精度。NTPv4 规范包括了加密和认证功能,并对时间同步的稳定性和性能进行了优化。
1.2 sntp和 ntp
SNTP(Simple Network Time Protocol,简单网络时间协议)是 NTP(Network Time Protocol,网络时间协议)的一个简化版本。它设计用于那些不需要 NTP 所有复杂功能和精度的环境。
NTP(Network Time Protocol)和 SNTP(Simple Network Time Protocol)的精度差异主要取决于它们的设计和实现。以下是一些关于两者精度的关键点:
NTP 精度
-
高精度:NTP 设计用于提供高精度的时间同步,通常可以达到毫秒级别甚至更高的精度。它通过精细的算法来处理网络延迟和时间漂移,能够在不同的网络条件下保持准确性。
-
网络延迟补偿:NTP 采用复杂的算法来补偿网络延迟和抖动,包括使用时间戳来测量延迟,并通过时间戳来计算精确的时钟漂移。
-
服务器选择:NTP 客户端可以从多个时间源选择最优的服务器,以提高同步的准确性和可靠性。
-
实测精度:在理想环境下,NTP 可以达到亚秒级别的精度。在网络条件良好的情况下,甚至可以达到毫秒级别的精度。
SNTP 精度
-
较低精度:SNTP 比 NTP 简化了很多复杂的功能,因此通常提供较低的精度。SNTP 主要用于那些对精度要求不高的场景,通常在毫秒级别以下。
-
网络延迟补偿:SNTP 不进行复杂的网络延迟补偿,而是直接从一个时间服务器获取时间,因此其精度受到网络延迟变化的影响较大。
-
单一时间源:SNTP 通常使用单一的时间服务器,没有 NTP 的时间源选择机制,这可能会影响同步精度,特别是在网络条件不稳定的情况下。
-
实测精度:SNTP 的精度通常比 NTP 差,可能在几十毫秒到几秒之间,具体取决于网络环境和服务器的稳定性。
总结:
-
NTP 提供了高精度的时间同步,能够在各种网络条件下保持较高的准确性。它适用于对时间精度有较高要求的应用。
-
SNTP 精度较低,适用于对时间精度要求不高的场景。其同步精度受限于网络延迟和简单的时间同步机制。
如果需要非常精确的时间同步,NTP 是更好的选择。如果时间同步精度要求较低,且希望简化实现,SNTP 可以满足需求。
1.3 ntp 校时原理
- 六千字详细图解网络时间协议(NTP),带你领略NTP的魅力!
- 时间同步协议NTP - 原理&实践
- NTP的工作模式
- 使用NTP协议获取网络时间戳(C/C++实现)
- 影响因素:路由层级、网络波动
- 端口:UDP协议的端口123
NTP 协议的基本工作原理:
-
时间服务器体系结构:
NTP 使用一种分层的时间服务器体系结构来实现时间同步。这些服务器被分为多个层级(stratum),最高层级(stratum 1)通常使用原子钟或GPS设备作为时间源,较低层级(stratum 2、stratum 3等)的服务器通过网络与更高层级的服务器同步。 -
时钟同步过程:
- 时钟选择:客户端选择最优的时间服务器进行同步,考虑服务器的可用性、延迟和精度。
- 时钟调整:客户端使用时钟漂移和网络延迟等信息,调整本地时钟。
- 时钟稳定:通过周期性的同步和调整,保持本地时钟与远程服务器的同步性和精确性。
-
安全性增强:
最新版本的 NTP 提供了加密通信和身份验证功能,确保时间源的真实性和数据完整性,防止恶意攻击和篡改。
二、Linux C/C++ ntp 选型
2.1 实现方案
-
方案一:使用库,C/C++ 主进程通过调用库API完成校时
-
方案二:使用管理进程,C/C++ 主进程通过管理ntp校时后台进程 (配置文件 + 控制启停)
- ntpd:https://www.ntp.org/
- chronyd 和 chronyc : https://chrony-project.org/index.html
- ntpclient
- openntpd-6.2p3 (portable)
2.2 选型依据
- NTP实现比较:ntp、chrony、openntpd
- 校时精度:越高越好
- 易用性:是不是简单易用
- 流行度:
- 常见Linux 发行版默认使用的校时工具:Centos、Debian、Ubuntu
- CentOS:之前是ntpd,CentOS 7及以后版本使用Chrony。
- Debian 默认使用 systemd-timesyncd 来进行时钟同步。systemd-timesyncd 是 systemd 的一部分,提供简单的网络时间协议(NTP)客户端功能,用于自动校时。
- Ubuntu 也使用 systemd-timesyncd 作为默认的校时工具,与Debian类似。
目前Linux中最常用的校时工具:
- systemd-timesyncd:由于其与systemd的集成和轻量级特性,systemd-timesyncd已成为许多现代Linux发行版的默认选择,特别是在桌面和通用用途的发行版中。
- Chrony:以其高精度和良好的网络条件适应性而受到青睐,常用于服务器和企业级环境。
- ntpd:作为传统的NTP守护进程,仍然在一些系统中使用,尤其是在一些老旧或特定的环境。
2.3 选型结论(chrony)
选择chrony 理由
- 相对 ntpd 更轻量,同时配置简单
- 同时支持 ntpclient 和 ntpserver
- 支持更高精度 ntp 校时(chrony和ntpd 相对 sntp 校时精度更高)
- 支持 GPS 校时(需要搭配gpsd使用)
- 支持 PPS 信号处理(GPS + PPS 实现高精度校时)
2.4 选型范围
- 范围:Other NTP and SNTP implementations
- ntpd:https://www.ntp.org/
- chrony : https://chrony-project.org/index.html
- openntpd-6.2p3 (portable)
- sntp 库:V3使用,svn opensource 归档的库 (sntp 精度不够高)
- ntpclient:V2使用(很小很简单的一个)
2.4.1 ntpd 更新记录
三、chrony 使用
3.1 编译/交叉编译
- 源码下载:https://chrony-project.org/download.html
- 配置PPS:默认缺少 <timepps.h> 文件,可下载 pps-tools 源码,将该文件拷贝进来
Checking for <sys/timepps.h> : No Checking for <timepps.h> : No
-
配置编译选项
# 配置本机编译 make distclean ./configure --prefix=$PWD/install_x64 make -j && make install
配置交叉编译:
make distclean export CC="arm-fslc-linux-gnueabi-gcc -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard --sysroot=/opt/fslc-x11/2.4.4/sysroots/armv7at2hf-neon-fslc-linux-gnueabi" export CXX="arm-fslc-linux-gnueabi-g++ -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard --sysroot=/opt/fslc-x11/2.4.4/sysroots/armv7at2hf-neon-fslc-linux-gnueabi" export CPPFLAGS="-I. " ./configure --prefix=$PWD/install_imx6q make -j
-
编译
- 会生成 config.h 文件:使能开关、默认配置路径
#define LINUX 1 #define DEBUG 0 #define FEAT_CMDMON 1 #define FEAT_NTP 1 #define FEAT_REFCLOCK 1 #define HAVE_IN_PKTINFO 1 #define FEAT_IPV6 1 #define _GNU_SOURCE 1 #define HAVE_IN6_PKTINFO 1 #define HAVE_CLOCK_GETTIME 1 #define FEAT_ASYNCDNS 1 #define USE_PTHREAD_ASYNCDNS 1 #define HAVE_GETRANDOM 1 #define HAVE_RECVMMSG 1 #define HAVE_LINUX_TIMESTAMPING 1 #define FEAT_RTC 1 #define FEAT_PHC 1 #define HAVE_PTHREAD_SETSCHEDPARAM 1 #define HAVE_MLOCKALL 1 #define HAVE_SETRLIMIT_MEMLOCK 1 #define FORCE_DNSRETRY 1 #define DEFAULT_CONF_FILE "/etc/chrony.conf" #define DEFAULT_HWCLOCK_FILE "" #define DEFAULT_PID_FILE "/var/run/chrony/chronyd.pid" #define DEFAULT_RTC_DEVICE "/dev/rtc" #define DEFAULT_USER "root" #define DEFAULT_COMMAND_SOCKET "/var/run/chrony/chronyd.sock" #define MAIL_PROGRAM "/usr/lib/sendmail" #define CHRONYC_FEATURES "-READLINE -SECHASH +IPV6 -DEBUG" #define CHRONYD_FEATURES "+CMDMON +NTP +REFCLOCK +RTC -PRIVDROP -SCFILTER -SIGND +ASYNCDNS -NTS -SECHASH +IPV6 -DEBUG" #define CHRONY_VERSION "4.5"
-
安装
- 必要文件是:chronyd 和 chronyc 两个可执行程序 (其他非必要)
- 默认编译配置项会用到 /var/run 和 /var/lib 路径,若没有root权限 make install 会报错,此时手动拷贝上面两个文件即可。
3.2 C++ demo
- 自定义封装 ChronydHelper.hpp 纯头文件接口:配置chrony.conf 参数、控制chronyd 启动和停止
- ChronydHelperDemo.cpp :调用示例
- 注意事项:
- 运行需要root权限
- chronyd 要放在 ChronydHelper.hpp 指定路径
- 目录:/var/lib 和 /var/run 要存在
3.2.1 ChronydHelper.hpp
/**
* @file ChronydHelper.hpp
* @brief chronyd 进程控制
* @date 日期
* @log 修改内容
*/
#ifndef CHRONY_CONTROL_HPP
#define CHRONY_CONTROL_HPP
#include <iostream>
#include <fstream>
#include <string>
#include <memory> // for std::shared_ptr
#include <mutex>
#include <cmath>
#include <sys/stat.h>
#include <unistd.h>
#define CHRONY_CHRONYD_BIN "/tmp/chrony/chronyd" // 自定义BIN程序路径(按实际修改)
#define CHRONY_CONFIG_FILE "/tmp/chrony/chrony.conf" // 自定义配置文件路径(按实际修改)
#define CHRONY_DRIFTFILE "/var/lib/chrony/drift" // 自定义drift存储路径, 方便重启快速校时
// notice:默认编译,运行环境要存在 /var/lib 和 /var/run 路径
class ChronyControl {
public:
ChronyControl() = default;
~ChronyControl(){
(void)StopChronyd();
};
/**
* @brief 启动chrond后台进程
* @param host [in] ntp 服务器域名/IP
* @param port [in] ntp 服务器端口
* @param interval [in] ntp 校时间隔,单位:分钟
* @return true
* @return false
*/
inline bool StartChronyd(const std::string& host, const uint32_t& port, const uint32_t& interval)
{
(void)StopChronyd();
std::lock_guard<std::mutex> lock(mtx_);
if(!UpdateChronyConfig(host, port, interval))
{
return false;
}
return StartChronyd();
}
/**
* @brief 启动chrond后台进程
* @return true
* @return false
*/
inline bool StopChronyd()
{
std::lock_guard<std::mutex> lock(mtx_);
std::string command = "pkill -9 chronyd";
return ExecuteCommand(command);
}
/**
* @brief 自定义chrony.conf文件内容,并重启chrond后台进程
* @param newConfig [in]
* @return true
* @return false
* @note StartChronyd 满足不了的情况下,使用该接口
*/
inline bool UpdateChronyConfig(const std::string& newConfig)
{
(void)StopChronyd();
(void)CreateFilePath(CHRONY_CONFIG_FILE);
std::lock_guard<std::mutex> lock(mtx_);
std::ofstream configFile(CHRONY_CONFIG_FILE);
if (!configFile.is_open()) {
std::cerr << "Failed to open configuration file" << std::endl;
return false;
}
configFile << newConfig;
configFile.close();
return StartChronyd();
}
private:
inline bool StartChronyd()
{
if (access(CHRONY_CHRONYD_BIN, F_OK) == -1)
{
std::cerr << "Failed to access file: " << CHRONY_CHRONYD_BIN << std::endl;
return false;
}
(void)CreateFilePath(CHRONY_DRIFTFILE);
std::string command = std::string(CHRONY_CHRONYD_BIN) + " -f " + std::string(CHRONY_CONFIG_FILE) + " &";
return ExecuteCommand(command);
}
inline bool UpdateChronyConfig(const std::string& host, const uint32_t& port, const uint32_t& interval)
{
(void)CreateFilePath(CHRONY_CONFIG_FILE);
int minpoll = static_cast<int>(std::log2(interval*60));
int maxpoll = minpoll + 1;
std::string server = "server " + host + " port " + std::to_string(port) + " iburst minpoll " + std::to_string(minpoll) + " maxpoll " + std::to_string(maxpoll);
std::string driftfile = "driftfile " + std::string(CHRONY_DRIFTFILE);
std::string newConfig = server + "\n" +
driftfile + "\n" +
"makestep 0.1 -1\n"
"rtcsync\n";
std::ofstream configFile(CHRONY_CONFIG_FILE);
if (!configFile.is_open()) {
std::cerr << "Failed to open configuration file" << std::endl;
return false;
}
configFile << newConfig;
configFile.close();
return true;
}
inline bool ExecuteCommand(const std::string& command) {
std::array<char, 1024> buffer;
std::string result;
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(command.c_str(), "r"), pclose);
if (!pipe) {
return false;
}
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
result += buffer.data();
}
return true;
}
inline bool CreateFilePath(const std::string& path)
{
if (access(path.c_str(), F_OK) != -1) {
return true;
}
std::string dir = path.substr(0, path.find_last_of('/'));
if (mkdir(dir.c_str(), 0755) == -1 && errno != EEXIST) {
std::cerr << "Failed to create directory: " << dir << std::endl;
return false;
}
// 创建文件
std::ofstream file(path, std::ios::out | std::ios::app);
if (!file) {
std::cerr << "Failed to create file: " << path << std::endl;
return false;
}
file.close();
return true;
}
std::mutex mtx_; // Renamed from mtx_ to mtx_ to avoid naming conflict
};
#endif // CHRONY_CONTROL_HPP
3.2.1 ChronydHelperDemo.cpp
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ChronydControl.hpp"
int main() {
ChronyControl chronyControl_;
// 方式一:只配置必要参数
bool ret = chronyControl_.StartChronyd("ntp.aliyun.com", 123, 1);
std::cout << "StartChronyd ret:" << ret << std::endl;
sleep(5);
// 方式二:修改 chrony.conf 配置
std::string newConfig = "pool 0.ntp.aliyun.com iburst minpoll 3 maxpoll 6\n"
"pool 1.ntp1.aliyun.com iburst\n"
"pool 2.ntp2.aliyun.com iburst\n"
"pool 3.ntp3.aliyun.com iburst\n"
"driftfile /var/lib/chrony/drift\n"
"logdir /var/log/chrony\n"
"makestep 0.1 3\n"
"rtcsync\n";
// 作为校时服务器可添加
// "allow all\n"
// "local stratum 10\n"
ret = chronyControl_.UpdateChronyConfig(newConfig);
std::cout << "UpdateChronyConfig ret:" << ret << std::endl;
while(1)
{
sleep(5);
}
return 0;
}
五、chrony + gpsd + pps 校时
5.1 基本介绍
- PPS 必须要硬件、内核、驱动支持
- 有PPS可实现高精度校时
- 没有PPS可以完成根据GPS输出的NMEA完成GPS低精度校时
- chrony 和 gpsd 编译必须配置支持 pps
- 参考资料:
- 基本命令:
su - //获取root权限 killall -9 gpsd chronyd //清除已经启动的gpsd和chrony进程 chronyd -f /etc/chrony.conf //启动chrony指定从/etc/chrony.conf读取配置文件 sleep 2 gpsd -n /dev/ttymxc0 /dev/pps0 //指定从/dev/ttymxc0串口获取GNSS数据 sleep 2 chronyc tracking //检查时间是否同步 chronyc sources //检测时间源状态
chrony.conf 和 FAQ 配置参考:
5.2 chrony + gpsd + 域套接字通讯
-
使用版本
- gpsd 3.25.1
- chronyd (chrony) version 4.5
-
chrony.conf 配置
# First option refclock SOCK /run/chrony.clk.ttyUSB5.sock refid GPS1 poll 2 filter 4 # Second option refclock PPS /dev/pps1 lock NMEA refid GPS refclock SOCK /run/chrony.clk.ttyUSB5.sock offset 0.5 delay 0.1 refid NMEA makestep 0.1 -1 driftfile /var/lib/chrony/drift rtcsync # 作为校时服务器 allow all local stratum 10
- 上面配置使用时:二选一
- poll 被定义为2的幂,可以为负以指定亚秒间隔,默认值为4(16秒)
- refid 最多四个字符
-
chronyd 先启动(使用域套接字要先启动监听)
./sbin/chronyd -f ./chrony.conf -d
-
gpsd 后启动
./gpsd -n -G -s 115200 -F /run/chrony.ttyUSB5.sock /dev/ttyUSB5 /dev/pps1 -N -D 5
- 注意配置规则:比如串口节点:/dev/ttyUSB5
- chrony.conf 配置SOCK:/run/chrony.clk.ttyUSB5.sock
- gpsd 配置:/run/chrony.ttyUSB5.sock (相对上面少.clk)
-
问题
- 启动顺序:使用域套接字时,能否设置 gpsd 连不上一直重连,而不必关系启动顺序
- PPS:是否支持PPS、gpsd 是否已使用上PPS、chrony 是否已使用上PPS
- 人为校时:手动校时 date -s “01:01” 要等一到两个周期被修正
域套接字监听:
5.3 chrony + gpsd + 共享内存通讯(校时很快)
- chrony.conf 配置
refclock PPS /dev/pps1 lock 0183 refid GPS3 refclock SHM 0 poll -2 refid 0183 precision 1e-1 offset 0.9999 delay 0.2 makestep 0.1 -1 driftfile /var/lib/chrony/drift rtcsync # 作为校时服务器 allow all local stratum 10
- 启动顺序:都可以 (chronyd、gpsd)
./sbin/chronyd -f ./chrony.conf -d
./gpsd -n -G -s 115200 -F /run/chrony.ttyUSB5.sock /dev/ttyUSB5 /dev/pps1 -N -D 5
- 人为校时: 手动校时 date -s “01:01”,会立刻被修正过来(上面域套接字的不行、看看是哪里配参问题)。
- PPS 会被校准:上面貌似没有 (要等一会 几十秒)
- 问题:PPS 是chrony、gpsd 谁使用了,谁校准了?
- 实测:chrony.conf 必须配置PPS,而 gpsd 可配可不配、最好两个都配上(理论上更好)。
- 未校准前:
六、优质转载
6.1 嵌入式Linux 时间同步 gpsd+chrony
五、调试总结
5.1 gpsd+chrony基本原理
- gpsd从串口或者网络读取gnss数据并进行解析,将解析结果以UNIX socket或者共享内存等方式输出
- chrony从UNIX socket或者共享内存取得解析后的gnss和pps信息,两者结合,校正系统时间
- 注意:chrony校正时并不会使系统时间跳变,通过拨快或者拨慢的方式(控制时间增减率)达到时间校正的目的
5.2 gpsd+chrony配合注意事项
-
以下内核或者根文件系统选项必须配置
CONFIG_PPS=y CONFIG_PPS_CLIENT_LDISC=y CONFIG_PPS_CLIENT_GPIO=y CONFIG_GPIO_SYSFS=y BR2_PACKAGE_PPS_TOOLS=y
-
以 root 身份运行gpsd
-
gpsd必须在 -n 模式下运行,这样即使没有客户端处于活动状态,时钟也会更新
-
与 chronyd 对话不需要 gpsd 配置。chronyd 是使用文件 /etc/chrony.conf 或 /etc/chrony/chrony.conf 配置的。检查您的配置文档以获取正确的位置
-
最后请注意,chronyd 需要在 gpsd 之前启动,以便在 gpsd 启动之前准备好套接字。
-
使用套接字方法让 chronyd 连接到 gpsd,请在您的 chrony.conf 文件中添加以下几行。套接字被命名为 /run/chrony.XXXX.sock。其中 XXXX 被 gpsd 使用的设备名称的基本名称替换。如果您的接收器在 /dev/ttyS0 上输出串行数据,则相应的套接字是/run/chrony.ttyS0.sock。如果您的 PPS 在 /dev/pps0 上,那么对应的套接字是 /run/chrony.pps0.sock。
将 XXXX 替换为设备串行端口的基本名称,通常是 ttyS0、ttyACM0 或 ttyAMA0。
将 YYYY 替换为您的 PPS 设备的基本名称,通常是 pps0。refclock SOCK /run/chrony.XXXX.sock refid GPS precision 1e-1 offset 0.9999 refclock SOCK /run/chrony.YYYY.sock refid PPS precision 1e-7
-
使用基本的 ntpd 兼容 SHM 方法让 gpsd 连接到 chronyd。要使用它而不是套接字,请将这些行添加到基本的 chrony.conf 文件中:
server 0.us.pool.ntp.org server 1.us.pool.ntp.org server 2.us.pool.ntp.org server 3.us.pool.ntp.org driftfile /var/lib/chrony/drift allow # set larger delay to allow the NMEA source to overlap with # the other sources and avoid the falseticker status refclock SHM 0 refid GPS precision 1e-1 offset 0.9999 delay 0.2 refclock SHM 1 refid PPS precision 1e-7
- 您需要在 SHM 1 行上添加“precision 1e-7”,因为 chronyd 无法从 SHM 结构中读取精度。在不知道 SHM 1 上 PPS 的高精度的情况下,它可能不会对其数据给予足够的重视。