实时线程(RT Thread)的定时器稳定性测试

实时线程(RT Thread)定时器稳定性测试

参考链接

  1. HOWTO build a simple RT application

  2. C/C++中如何稳定地每隔5ms执行某个函数? - 知乎

1 背景介绍

在工业控制领域,实时(Real Time) 是一个核心要求。实时系统是指计算的正确性不仅依赖于逻辑的正确性而且依赖于产生结果的时间,如果系统的时间限制不能得到满足,系统将会产生故障。在工业领域这种故障可能造成灾难性的结果。实时操作系统有能力提供一个指定范围内的服务响应时间。

参考链接1的示例程序(下称rt_app)是一个实时的POSIX线程示例,它创建一个实时线程来执行特定的任务。程序的原理是通过使用POSIX线程库中的函数来设置线程的属性和调度策略,以确保线程能够以实时方式运行。首先,程序使用mlockall函数来锁定内存,以防止线程因为内存被交换而出现延迟。然后,通过使用pthread_attr_t结构来初始化线程属性,并设置线程的堆栈大小,并设置为最小的堆栈大小。接下来,通过pthread_attr_setschedpolicy函数设置线程的调度策略为SCHED_FIFO,并设置优先级为80。同时将线程的调度参数设置为显示继承调度属性。随后,通过调用pthread_create函数来创建一个线程,并传递指定的线程属性。创建成功后,程序等待线程执行完毕并回收资源,然后返回线程执行结果。(By ChatGPT)

参考链接2给出了定时器的一个实现(下称timer)。

2 测试过程

2.1 Windows 10中的测试(timer)

使用Visual Studio 2022(编译器:MSVC)编译运行timer程序代码。

CPU型号为i5-12400F(6核12线程)。3次测试结果如下:

Test Total Ticks Time (s) Std Error (us) Max Error (us) 95% Error (us) 99% Error (us)
Test 1 12000 60.011 8962.305 16037.100 14726.500 15579.500
Test 2 11998 60.003 8948.704 16048.200 14737.600 15635.800
Test 3 12000 60.014 8904.265 21278.300 14720.300 15638.300

标准误差(Std Error)的平均值为8938us。可见,Windows系统中定时器很不稳定。

注:上面为非实时测试。实时测试程序中,sys/mman.h是Unix风格的头文件,Windows不支持此头文件,故实时测试将在Linux中进行。

2.2 Linux(Ubuntu 22.04)虚拟机中的测试(timer)

在Ubuntu 22.04 x64上用g++编译timer代码(后缀-lstdc++表示编译时链接到标准C++库):

sudo g++ -o timer timer.cpp -lstdc++

分配给Ubuntu虚拟机的CPU为i5-12400F(4核8线程)。3次测试结果如下:

Test Total Ticks Time (s) Std Error (us) Max Error (us) 95% Error (us) 99% Error (us)
Test 1 12000 60.001 721.868 2067.194 1139.271 1262.141
Test 2 12000 60.001 706.440 1713.631 1128.074 1256.496
Test 3 12000 60.001 697.005 2049.300 1139.311 1321.148

标准误差(Std Error)的平均值为708.4us。

2.3 Linux(Ubuntu 22.04)虚拟机中的测试(timer+rt_app)

结合参考链接1和2中的程序,在Ubuntu 22.04 x64上将计时器程序作为RT中的线程运行(代码见文末)。

CPU为i5-12400F(4核8线程),3次测试结果如下:

Test Total Ticks Time (s) Std Error (us) Max Error (us) 95% Error (us) 99% Error (us)
Test 1 12000 60.001 532.253 1596.973 919.239 1072.571
Test 2 12000 60.000 629.790 1692.128 1082.571 1201.347
Test 3 12000 60.000 677.307 1478.963 1095.540 1226.734

标准误差(Std Error)的平均值为613.1us,比非实时更小一些。

2.4 openEuler Embedded 22.03下的测试(timer)

由于openEuler Embedded 22.03内核缺省已打抢占包,所以无需rt_app框架,直接对timer程序进行测试。操作系统具体版本为openEuler-22.03-LTS-SP3-UKUI-raspi-aarch64-alpha1(包含UKUI桌面,账户密码为{pi, raspberry}, {root, openeuler})。

硬件为树莓派4B,3次测试结果如下:

Test Total Ticks Time (s) Std Error (us) Max Error (us) 95% Error (us) 99% Error (us)
Test 1 12000 60.000 70.633 5552.031 2.554 2.680
Test 2 12000 60.000 67.272 6310.313 2.570 2.680
Test 3 12000 60.000 87.944 4958.020 2.596 2.683

误差标准差(Std Error)的平均值为75.3us,同时95%和99%误差<3us,显著小于无抢占包的Ubuntu系统。

3 结论

从测试结果可以看出:

  1. 在同一系统(如Ubuntu)中,将计时器程序作为实时线程运行后,误差与非实时相比有所减小,精度提高;

  2. 内核打抢占包的系统(如openEuler)中,计时器程序运行误差显著减小。

4 rt_app+timer程序代码

将参考链接2的定时器程序作为线程,运行在了参考链接1的RT应用中,并无特别的改动。需在Linux系统中编译,且需添加后缀-lstdc++

#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <thread>
#include <vector>

#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

using namespace std::literals::chrono_literals;

static const constexpr auto kInterval = 5ms;
static const constexpr auto kSpin = 100us;

int rt_timer(int argc, char* argv[]) {
    std::chrono::nanoseconds duration = 1min;
    if (argc > 1) {
        duration = std::chrono::seconds(std::atoi(argv[1]));
    }
    int ticks = duration / kInterval;
    std::vector<double> errors;
    errors.reserve(ticks);

    int digits = 0;
    do {
        ticks /= 10;
        ++digits;
    } while (ticks > 0);

    auto now = std::chrono::high_resolution_clock::now();
    auto begin = now;
    auto prev = now;
    auto tick = now;
    auto final = now + duration + kInterval;
    begin += kInterval;  // 多执行一次作为预热
    int i = 0;
    while (now < final) {
        prev = now;
        tick += kInterval;
        std::this_thread::sleep_until(tick - kSpin);
        now = std::chrono::high_resolution_clock::now();
        while (now < tick) {
            std::this_thread::yield();
            now = std::chrono::high_resolution_clock::now();
        }
        auto error =
            std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(
                now - tick);
        errors.push_back(error.count());
        ++i;
    }
    errors.erase(errors.begin());  // 统计时丢弃第一个预热tick

    std::ios::sync_with_stdio(false);
    std::cout.tie(nullptr);
    std::cout.precision(3);
    std::cout.setf(std::ios::fixed);
    double std_err = 0.0;
    double max_err = 0.0;
    for (size_t i = 0; i < errors.size(); ++i) {
        std_err += std::pow(errors.at(i), 2);
        max_err = std::max(std::abs(max_err), std::abs(errors.at(i)));
        std::cout << "tick " << std::setw(digits) << (i + 1)
            << " error: " << std::setw(8) << errors.at(i) << "us"
            << std::endl;
    }
    std_err = std::sqrt(std_err / errors.size());
    for (double& error : errors) {
        error = std::abs(error);
    }
    std::sort(errors.begin(), errors.end());
    double most_95 = errors.at(errors.size() * 0.95);
    double most_99 = errors.at(errors.size() * 0.99);
    auto elapsed =
        std::chrono::duration_cast<std::chrono::duration<double>>(now - begin);
    std::cout << "total " << errors.size() << " ticks in " << elapsed.count()
        << "s" << std::endl;
    std::cout << "std error: " << std_err << "us" << std::endl;
    std::cout << "max error: " << max_err << "us" << std::endl;
    std::cout << "95% error: " << most_95 << "us" << std::endl;
    std::cout << "99% error: " << most_99 << "us" << std::endl;

    return 0;
}

void* thread_func(void* data)
{
    /* Do RT specific stuff here */
    rt_timer(0, NULL);
    return NULL;
}

int main(int argc, char* argv[])
{
    struct sched_param param;
    pthread_attr_t attr;
    pthread_t thread;
    int ret;

    /* Lock memory */
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        printf("mlockall failed: %m\n");
        exit(-2);
    }

    /* Initialize pthread attributes (default values) */
    ret = pthread_attr_init(&attr);
    if (ret) {
        printf("init pthread attributes failed\n");
        goto out;
    }

    /* Set a specific stack size  */
    ret = pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);
    if (ret) {
        printf("pthread setstacksize failed\n");
        goto out;
    }

    /* Set scheduler policy and priority of pthread */
    ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    if (ret) {
        printf("pthread setschedpolicy failed\n");
        goto out;
    }
    param.sched_priority = 80;
    ret = pthread_attr_setschedparam(&attr, &param);
    if (ret) {
        printf("pthread setschedparam failed\n");
        goto out;
    }
    /* Use scheduling parameters of attr */
    ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    if (ret) {
        printf("pthread setinheritsched failed\n");
        goto out;
    }

    /* Create a pthread with specified attributes */
    ret = pthread_create(&thread, &attr, thread_func, NULL);
    if (ret) {
        printf("create pthread failed\n");
        goto out;
    }

    /* Join the thread and wait until it is done */
    ret = pthread_join(thread, NULL);
    if (ret)
        printf("join pthread failed: %m\n");

out:
    return ret;
}
posted @ 2024-02-26 16:32  Digitzh  阅读(465)  评论(0)    收藏  举报