C/C++ ntp 校时(chrony使用)

一、介绍

1.1 ntp 介绍

NTP(Network Time Protocol,网络时间协议)是一种用于在计算机网络中同步时钟的协议,旨在确保网络中所有计算机和设备具有相同的准确时间。

NTP 可以达到以下精度

在局域网 (LAN) 环境中,NTP 可以达到毫秒级别(通常能够实现1毫秒内)甚至亚毫秒级别的精度。
在广域网 (WAN) 环境中,NTP 的精度通常在几毫秒到几十毫秒之间,取决于网络条件。

官方历史版本和协议规范:

  1. NTPv0:

    • 最初的 NTP 版本,虽然详细文档不如后续版本那么易于获取,但该版本标志着 NTP 的开端。
  2. NTPv1:

    • 文档: RFC 958(1985年)
    • 描述了早期的 NTP 协议,包括其基本结构和操作。
  3. NTPv2:

    • 文档: RFC 1059(1988年)
    • 引入了更详细的报头格式和错误检测机制,增强了协议的健壮性。
  4. NTPv3:

    • 文档: RFC 1305(1992年)
    • 增加了广播模式和多播模式,支持了对称和非对称的通信方式,增强了安全性。引入了许多现代化特性,如对原子钟和 GPS 的支持。
  5. 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 精度

  1. 高精度:NTP 设计用于提供高精度的时间同步,通常可以达到毫秒级别甚至更高的精度。它通过精细的算法来处理网络延迟和时间漂移,能够在不同的网络条件下保持准确性。

  2. 网络延迟补偿:NTP 采用复杂的算法来补偿网络延迟和抖动,包括使用时间戳来测量延迟,并通过时间戳来计算精确的时钟漂移。

  3. 服务器选择:NTP 客户端可以从多个时间源选择最优的服务器,以提高同步的准确性和可靠性。

  4. 实测精度:在理想环境下,NTP 可以达到亚秒级别的精度。在网络条件良好的情况下,甚至可以达到毫秒级别的精度。

SNTP 精度

  1. 较低精度:SNTP 比 NTP 简化了很多复杂的功能,因此通常提供较低的精度。SNTP 主要用于那些对精度要求不高的场景,通常在毫秒级别以下。

  2. 网络延迟补偿:SNTP 不进行复杂的网络延迟补偿,而是直接从一个时间服务器获取时间,因此其精度受到网络延迟变化的影响较大。

  3. 单一时间源:SNTP 通常使用单一的时间服务器,没有 NTP 的时间源选择机制,这可能会影响同步精度,特别是在网络条件不稳定的情况下。

  4. 实测精度:SNTP 的精度通常比 NTP 差,可能在几十毫秒到几秒之间,具体取决于网络环境和服务器的稳定性。

总结:

  • NTP 提供了高精度的时间同步,能够在各种网络条件下保持较高的准确性。它适用于对时间精度有较高要求的应用。

  • SNTP 精度较低,适用于对时间精度要求不高的场景。其同步精度受限于网络延迟和简单的时间同步机制。

如果需要非常精确的时间同步,NTP 是更好的选择。如果时间同步精度要求较低,且希望简化实现,SNTP 可以满足需求。

1.3 ntp 校时原理

NTP 协议的基本工作原理

  • 时间服务器体系结构:
    NTP 使用一种分层的时间服务器体系结构来实现时间同步。这些服务器被分为多个层级(stratum),最高层级(stratum 1)通常使用原子钟或GPS设备作为时间源,较低层级(stratum 2、stratum 3等)的服务器通过网络与更高层级的服务器同步。

  • 时钟同步过程:

    • 时钟选择:客户端选择最优的时间服务器进行同步,考虑服务器的可用性、延迟和精度。
    • 时钟调整:客户端使用时钟漂移和网络延迟等信息,调整本地时钟。
    • 时钟稳定:通过周期性的同步和调整,保持本地时钟与远程服务器的同步性和精确性。
  • 安全性增强:
    最新版本的 NTP 提供了加密通信和身份验证功能,确保时间源的真实性和数据完整性,防止恶意攻击和篡改。

二、Linux C/C++ ntp 选型

2.1 实现方案

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 选型范围

2.4.1 ntpd 更新记录

三、chrony 使用

3.1 编译/交叉编译

  1. 配置编译选项

    # 配置本机编译
    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
    
  2. 编译

    • 会生成 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"
    
  3. 安装

    • 必要文件是: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 + 域套接字通讯

  1. 使用版本

    • gpsd 3.25.1
    • chronyd (chrony) version 4.5
  2. 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 最多四个字符
  3. chronyd 先启动(使用域套接字要先启动监听)

    ./sbin/chronyd -f ./chrony.conf -d
    
  4. 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)
  5. 问题

    • 启动顺序:使用域套接字时,能否设置 gpsd 连不上一直重连,而不必关系启动顺序
    • PPS:是否支持PPS、gpsd 是否已使用上PPS、chrony 是否已使用上PPS
    • 人为校时:手动校时 date -s “01:01” 要等一到两个周期被修正

域套接字监听:
在这里插入图片描述

5.3 chrony + gpsd + 共享内存通讯(校时很快)

  1. 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
    
  2. 启动顺序:都可以 (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
  3. 人为校时: 手动校时 date -s “01:01”,会立刻被修正过来(上面域套接字的不行、看看是哪里配参问题)。
  4. 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配合注意事项

  1. 以下内核或者根文件系统选项必须配置

    CONFIG_PPS=y
    CONFIG_PPS_CLIENT_LDISC=y
    CONFIG_PPS_CLIENT_GPIO=y
    CONFIG_GPIO_SYSFS=y
    
    BR2_PACKAGE_PPS_TOOLS=y
    
  2. 以 root 身份运行gpsd

  3. gpsd必须在 -n 模式下运行,这样即使没有客户端处于活动状态,时钟也会更新

  4. 与 chronyd 对话不需要 gpsd 配置。chronyd 是使用文件 /etc/chrony.conf 或 /etc/chrony/chrony.conf 配置的。检查您的配置文档以获取正确的位置

  5. 最后请注意,chronyd 需要在 gpsd 之前启动,以便在 gpsd 启动之前准备好套接字。

  6. 使用套接字方法让 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
    
  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 的高精度的情况下,它可能不会对其数据给予足够的重视。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值