How does the time source used in the Linux kernel SCHED_DEADLINE relate to those availabe in c++ via std::chrono?

47 Views Asked by At

I recently tried implementing a program that executes a piece of code every 200ms [were execution time itself varied between 1ms and 50ms]. This - to me - sounded exactly like a problem, the SCHED_DEADLINE was designed for.

Reference timing measurements using std::chrono::steady_clock [and std::chrono::system_clock] however showed a drift of the SCHED_DEADLINE periods relative to those time sources. And this drift depended on the machine it was run on:

  • On one machine [Ubuntu Mate 22.04, kernel 6.02, Ryzen 7 16C/32T] the scheduler scheduled the task earlier than I would have expected by looking at std::chrono::steady_clock.
  • On another machine [Debian 12, kernel 6.1.0, Intel i7-10510U] the scheduler scheduled the task later than I would have expected by looking at std::chrono::steady_clock.

I had a short look into the Linux kernel source code [see here] - I must however admit that I did not quite follow all the way through, were the jiffies actually originate from and how clock.c then incorporates it all together with

  • GTOD (clock monotonic)
  • sched_clock()
  • explicit idle events

for a

monotonic

fast (execution time) high resolution clock with bounded drift between CPUs.

  1. Could someone enlighten me on how these two time sources relate to each other?

  2. What would be the better choice for my application, which requires a 200ms period [in real time...]?

PS: As I am not allowed to upload code as a user-friendly tar.gz project and I refuse to upload it to a different website and only put a link here, here goes the code...

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(TestingRts LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(TestingRts
    main.cpp
    realTimeScheduler.cpp
    realTimeScheduler.hpp
)

include(GNUInstallDirs)

install(TARGETS TestingRts
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

realTimeScheduler.hpp

#ifndef LINUX_REAL_TIME_SCHEDULER_HPP
#define LINUX_REAL_TIME_SCHEDULER_HPP

#ifdef WIN32

#warning "Real-time-scheduler only exists under Linux - this file should not be included for Windows."

#else // WIN32

#include <cstdint>

namespace Linux
{

int setRealTimeSchedulerForThisThread(uint64_t const periodInNanoSeconds, uint64_t const runtimeInNanoSeconds);

} // namespace Linux

#endif // WIN32

#endif /* LINUX_REAL_TIME_SCHEDULER_HPP */

realTimeScheduler.cpp

#include "realTimeScheduler.hpp"

#ifdef WIN32

#warning "Real-time-scheduler only exists under Linux - this file should not be included for Windows."

#else // WIN32

#include <linux/sched.h> /* SCHED_DEADLINE */
#include <linux/sched/types.h> /* struct sched_attr */
#include <sys/syscall.h>
#include <unistd.h>


namespace Linux
{

int setRealTimeSchedulerForThisThread(uint64_t const periodInNanoSeconds, uint64_t const runtimeInNanoSeconds)
{
    pid_t const threadId = syscall(SYS_gettid);

    struct sched_attr schedulerAttributes = {
        .size = sizeof(struct sched_attr),
        .sched_policy = SCHED_DEADLINE,
        .sched_flags = 0,
        .sched_nice = 0,
        .sched_priority = 0,
        .sched_runtime = runtimeInNanoSeconds, // nanoseconds
        .sched_deadline = periodInNanoSeconds, // nanoseconds
        .sched_period = periodInNanoSeconds // nanoseconds
    };

    return syscall(__NR_sched_setattr, threadId, &schedulerAttributes, 0);
}    

} // namespace Linux

#endif // WIN32

main.cpp

#include "realTimeScheduler.hpp"

#include <chrono>
#include <iostream>
#include <math.h>
#include <memory>
#include <thread>
#include <vector>

struct Measurements
{
    Measurements(std::chrono::steady_clock::time_point const startBusyWait,
                 std::chrono::steady_clock::time_point const start,
                 std::chrono::steady_clock::time_point const finish,
                 std::chrono::system_clock::time_point startLocal,
                 std::chrono::system_clock::time_point finishLocal)
        : startBusyWait(startBusyWait)
        , start(start)
        , finish(finish)
        , startLocal(startLocal)
        , finishLocal(finishLocal)
    {
    }

    std::chrono::steady_clock::time_point startBusyWait;
    std::chrono::steady_clock::time_point start;
    std::chrono::steady_clock::time_point finish;
    std::chrono::system_clock::time_point startLocal;
    std::chrono::system_clock::time_point finishLocal;
};


static unsigned index = 0;
static bool isRunning_ = false;
static std::shared_ptr<std::thread> thread_;
static std::vector<Measurements> times_;
static std::vector<std::chrono::steady_clock::time_point> processedTimes_;


static constexpr size_t numberOfPeriods = 1000;
static constexpr std::chrono::duration periodDuration = std::chrono::milliseconds(200);
static constexpr std::chrono::duration totalDuration = periodDuration * numberOfPeriods;

static std::chrono::steady_clock::time_point nextStartTime_;
static constexpr std::chrono::steady_clock::duration nominalBusyWaitDuration_ = std::chrono::milliseconds(10);

static inline std::chrono::steady_clock::time_point now()
{
    return std::chrono::steady_clock::now();
}


double delayDummy(size_t const iterations)
{
    double blub = 0;
    for (size_t index = 0; index < iterations; ++index)
    {
        blub = tan(blub + 3.);
    }
    return blub;
}


void executeCycle()
{
    try
    {
       int const result = Linux::setRealTimeSchedulerForThisThread(
           std::chrono::duration_cast<std::chrono::nanoseconds>(periodDuration).count(),
           .8 * std::chrono::duration_cast<std::chrono::nanoseconds>(periodDuration).count());
//        int const result = Linux::setOtherNiceSchedulerThisThread(-2);
        // int const result = Linux::setFifoSchedulerThisThread(1);

        if (result < 0)
        {
            error_t const errorNumber = errno; // prevent the macro from overwriting the actual error code
            std::cout << "error: " << errorNumber << std::endl;
            throw std::runtime_error("Failed to modify thread scheduling!");
        }


        nextStartTime_ = now() + nominalBusyWaitDuration_;

        while (isRunning_)
        {
            // busy wait until time
            std::chrono::steady_clock::time_point const startBusyWait = now();
            std::chrono::steady_clock::time_point start = startBusyWait;
            while (nextStartTime_ > start)
            {
                start = now();
            }
            std::chrono::system_clock::time_point const startLocal = std::chrono::system_clock::now();
            nextStartTime_ += periodDuration;

            std::cout << index << ": " << delayDummy((0 == (index % 20)) ? (4 * 1000 * 1000) : (1000 * 1000)) << std::endl;
            ++index;

            std::chrono::steady_clock::time_point const finish = now();
            std::chrono::system_clock::time_point const finishLocal = std::chrono::system_clock::now();
            times_.emplace_back(startBusyWait, start, finish, startLocal, finishLocal);


            // // todo: check it being in the future and otherwise call yield?
            // std::chrono::steady_clock::time_point const wakeupTime = nextStartTime_ - nominalBusyWaitDuration_;
            // std::this_thread::sleep_until(wakeupTime);
            std::this_thread::yield();
        }
    }
    catch (std::exception const & e)
    {
        std::cout << "exception: " << e.what() << std::endl;
    }

    isRunning_ = false;
}

void stopExecuteCycle()
{
    isRunning_ = false;

    if (static_cast<bool>(thread_))
    {
        thread_->join();
        thread_.reset();
    }
}

void startExecuteCycle()
{
    isRunning_ = true;

    thread_ = std::make_shared<std::thread>(&executeCycle);
}





int main()
{
    times_.reserve(periodDuration / totalDuration);

    startExecuteCycle();

    std::chrono::steady_clock::time_point const endTime = now() + totalDuration;
    while ((endTime > now()) && (isRunning_))
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    stopExecuteCycle();

    std::cout << "durations [us]:" << std::endl;
    if (1 < times_.size())
    {
        for (size_t index = 0; index < (times_.size() - 1); ++index)
        {
            std::cout << "index: " << index << "\t";
            std::cout << "wait: " << std::chrono::duration_cast<std::chrono::microseconds>(times_[index].start - times_[index].startBusyWait).count() << "\t";
            std::cout << "period: " << std::chrono::duration_cast<std::chrono::microseconds>(times_[index + 1].start - times_[index].start).count() << "\t";
            std::cout << "processing: " << std::chrono::duration_cast<std::chrono::microseconds>(times_[index].finish - times_[index].start).count() << "\t";
            std::cout << "[period local: " << std::chrono::duration_cast<std::chrono::microseconds>(times_[index + 1].startLocal - times_[index].startLocal).count() << "]" << std::endl;
        }

        std::cout << "average: " << (std::chrono::duration_cast<std::chrono::microseconds>(times_.back().start - times_.front().start).count() / (times_.size() - 1)) << " us"
                  << " [local: " << (std::chrono::duration_cast<std::chrono::microseconds>(times_.back().startLocal - times_.front().startLocal).count() / (times_.size() - 1)) << " us]"<< std::endl;
    }

    return 0;
}

PPS: Please also note, that I was forced to put the scheduler things in a separate translation unit than main.cpp, as there exists a duplicate definition in the kernel/GNU/compiler headers [error: redefinition of ‘struct sched_param’ in /usr/include/x86_64-linux-gnu/bits/types/struct_sched_param.h and /usr/include/linux/sched/types.h].

PPPS: As a final note: please note that you will have to run the executable with sudo or at least set the respective capability, for it to be able to configure the scheduler.

0

There are 0 best solutions below