TimerDelegate(), TimerDelegate_r()

QNX SDP8.0C Library ReferenceAPIDeveloper

Delegate timers between CPUs

Note:
The TimerDelegate*() functions are experimental along with the offlining and onlining scheduling feature.

Synopsis:

#include <sys/neutrino.h>

int TimerDelegate( uint32_t action,
                   int32_t owner_cpu,
                   int32_t delegate_cpu );

int TimerDelegate_r( uint32_t action,
                     int32_t owner_cpu,
                     int32_t delegate_cpu );

Arguments:

action
The action to perform. One of the following:
  • _NTO_TIMER_DELEGATE — Delegate a CPU's timers to a delegate CPU.
  • _NTO_TIMER_RECLAIM — Reclaim a CPU's timers from a delegate CPU.
  • _NTO_TIMER_QUERY_DELEGATE — Query the kernel to determine a CPU's current delegate CPU.
owner_cpu
The CPU the call originated from, or -1 to use the current CPU. The owner_cpu can't be a CPU that's acting on the behalf of another CPU.
delegate_cpu
The delegate CPU. The acceptable values depend on the action you specified:
  • DELEGATE — You can't specify a CPU that has timers delegated to another CPU.
  • RECLAIM — Either specify the CPU that's taking care of the timers from owner_cpu, or set this field to -1 to let the kernel decide.
  • QUERY — Ignored.

Library:

libc

Use the -l c option to qcc to link against this library. This library is usually included automatically.

Description:

The TimerDelegate() and TimerDelegate_r() functions move the handling of timers between processors. This allows one processor to temporarily delegate (i.e., give ownership of) its timers to another processor, and then reclaim (i.e., take back the ownership of) these timers. Software timer queues are associated with a CPU in the QNX OS; the purpose of these functions is to delegate a CPU's timers to another CPU allowing you to take a CPU offline and keep its assigned timers firing.

The TimerDelegate() and TimerDelegate_r() functions are identical, except in the way they indicate errors. Refer to the "Returns" section for details. Privileged applications responsible for offlining and onlining CPUs, such as power managers, should call these functions.

Only one thread can call TimerDelegate() at a time. The function returns EBUSY if multiple threads attempt to call the function concurrently. The thread calling this function can loop on EBUSY until the function returns another return code.

CAUTION:
If you set _NTO_TIMER_DELEGATE as the action, TimerDelegate() blocks if it has to wait for the clock IST to finish running. Once the function successfully returns, the thread that called it must call TimerDelegate() again with the _NTO_TIMER_RECLAIM action before performing any blocking operation or causing a context switch to a thread of lower priority.

Delegating a CPU's timer to another CPU

You should delegate a CPU's timers before taking the CPU offline. To do so, call TimerDelegate() with the _NTO_TIMER_DELEGATE action to delegate a CPU's timers. The application then proposes a delegate CPU candidate. The kernel can refuse the proposed CPU if the CPU isn't handling its own timers at that moment (i.e., the proposed CPU already delegated its timers to another CPU).

Reclaiming a CPU's timer from another CPU

Once a CPU is back in operation, you should ensure that it reclaims its timers before the CPU notifies the kernel that it's ready to take part in scheduling. To do so, call TimerDelegate() with the _NTO_TIMER_RECLAIM action to reclaim a CPU's timers. If the application is responsible for tracking the delegate CPU, the application passes on this information to the kernel. Otherwise, the kernel must find the delegate CPU itself (set delegate_cpu to -1), but this is costly.

The CPU only reclaims its own timers, which excludes any timers that it had previously been delegated with as a delegate CPU (refer to the section below on "Transitive Delegation" for more information).

Transitive Delegation

When a CPU delegates its timers with a delegate CPU, all timers in its care go to the delegate CPU. When the CPU comes back online, it only reclaims its own timers. The delegate CPU takes care of the timers the CPU was previously delegated with before going offline.

For example, imagine a system with four CPUs. CPU3 needs to be taken offline and has previously been delegated with CPU2's timers. CPU3 delegates its timers to CPU1. CPU1 is then responsible for the timers of both CPU2 and CPU3.

If CPU3 comes back online before CPU2, then CPU3 is only responsible for its own timers from that point on. Subsequently, CPU1 is responsible for its own timers and CPU2's timers, until CPU2 reclaims its timers.

Requirements

The TimerDelegate*() functions only succeed if the following requirements are met:

  • The privileged application can run a thread at a higher priority that the clock IST (254 by default) and a lower priority than the IPI IST (always 255). Therefore, you must configure the system so that the clock IST runs at a maximum priority of 253, by passing the -C option to procnto as follows:
    procnto-smp-instr -C0,253
  • The process must have the PROCMGR_AID_RUNSTATE ability enabled. For more information, go to procmgr_ability().
  • The thread that calls the function must:
    • be pinned to the CPU delegating its timers.
    • have its scheduling policy set to SCHED_OFFLINING. This policy is a FIFO scheduling policy that allows a thread to be of higher priority than the clock IST.
    • have a higher priority than the clock IST.

Returns:

On success, DELEGATE and RECLAIM return EOK, and QUERY returns a CPU number. On failure;
  • TimerDelegate() returns -1 and sets errno.
  • TimerDelegate_r() returns the negative of a value from the Errors section and doesn't set errno.

Errors:

EAGAIN
The kernel refused the proposed delegate_cpu during a DELEGATE operation. For more information, refer to the Delegating a CPU's timer to another CPU section.
EBUSY
Multiple threads tried to call TimerDelegate() concurrently.
EINVAL
An invalid CPU was provided.
ENOEXEC
The calling thread doesn't have its scheduling policy set to SCHED_OFFLINING.
ENOSYS
An invalid action was provided, or the CPU provided isn't the current CPU.
ENOTSUP
The call was performed on a uniprocessor system.
EPERM
The application process didn't have the PROCMGR_AID_RUNSTATE ability enabled.
ERANGE
The calling thread's priority is less than or equal to that of the clock IST.
ESRCH
The kernel couldn't find the current delegate CPU (i.e., the current CPU hasn't delegated its timers).

Examples:

#include <sys/neutrino.h>
#include <sys/syspage.h>
#include <sys/resource.h>
#include <sys/cpuinline.h>
#include <stdio.h>
#include <stdbool.h>
#include <inttypes.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <pthread.h>

#define cps SYSPAGE_ENTRY(qtime)->cycles_per_sec
#define cc ClockCycles()

static unsigned ncpus;

static void burn(uint64_t const sec)
{
    uint64_t now_cc = ClockCycles(), end_cc = now_cc + sec * cps;
    while(ClockCycles() < end_cc) {
        // Burn CPU cycles.
        __cpu_bus_backoff();
    }
}

static void pin(unsigned const cpu)
{
    unsigned const runmask = (1U << cpu);
    void *threadctl_arg = (void *)(uintptr_t)runmask;
    assert(ThreadCtl(_NTO_TCTL_RUNMASK, threadctl_arg) != -1);
}

static void unpin(void)
{
    unsigned const runmask = ~0U;
    void *threadctl_arg = (void *)(uintptr_t)runmask;
    assert(ThreadCtl(_NTO_TCTL_RUNMASK, threadctl_arg) != -1);
}

static void powerdown(unsigned const delegate_cpu)
{
    for (;;) {
        int const rc = TimerDelegate_r(_NTO_TIMER_DELEGATE,
                                        -1, delegate_cpu);
        if (rc == 0) { break; }
        if (rc == -EBUSY) { __cpu_bus_backoff(); continue; }
        // A real power manager would do some handling because it did something
        // wrong if it gets here.
        exit(3);
    }
}

static void powerup(unsigned const delegate_cpu)
{
    for (;;) {
        int const rc = TimerDelegate_r(_NTO_TIMER_RECLAIM,
                                        -1, delegate_cpu);
        if (rc == 0) { break; }
        if (rc == -EBUSY) { __cpu_bus_backoff(); continue; }
        // A real power manager would do some handling because it did something
        // wrong if it gets here.
        exit(3);
    }
}

static unsigned find_delegate(void)
{
    unsigned delegate_cpu = -1;
    for (;;) {
        int const rc = TimerDelegate_r(_NTO_TIMER_QUERY_DELEGATE,
                                         -1, delegate_cpu);
        if (rc >= 0) { delegate_cpu = (unsigned)rc; break; }
        if (rc == -EBUSY) { __cpu_bus_backoff(); continue; }
        // A real power manager would do some handling because it did something
        // wrong if it gets here.
        exit(3);
    }
    return delegate_cpu;
}

static void *powerman(void * const arg)
{
    assert(arg == (void *)0UL);

    // Thread will be pinned on CPU0 and will delegate CPU0's timers to CPU1
    int const owner_cpu = (int)(intptr_t)arg;
    int const delegate_cpu = owner_cpu + 1;

    pin(owner_cpu);

    struct sched_param const param = { .sched_priority = 254, };
    pthread_t const self = pthread_self();
    int const rc = pthread_setschedparam(self, SCHED_OFFLINING, &param);
    assert(rc == 0);

    // Powering down CPU0, the timer associated with CPU0 will keep ticking on
    // CPU1.
    powerdown(delegate_cpu);
    // Burn CPU cycles for two seconds, we should see 10 more ticks from the
    // timer at 200ms interval.
    burn(2);
    // Reclaim timers.
    powerup(find_delegate());

    return NULL;
}

static void SIGALRM_handler(int const signo)
{
    printf("%lu ms got signo %d\n", clock_gettime_mon_ns()/1000000UL, signo);
}

#define MS(x) ((uint64_t)(x) * 1000000UL)
int main(int argc, char *argv[])
{
    ncpus = _syspage_ptr->num_cpu;
    assert(ncpus > 1U);

    int rc;

    // Pin current thread on CPU0, that will force the timer to be associated
    // with CPU0, starts after 500ms, ticks every 200ms
    pin(0);
    signal(SIGALRM, SIGALRM_handler);
    int t = TimerCreate(CLOCK_MONOTONIC, NULL); // SIGALRM
    assert(t != -1);
    struct _itimer itime = { MS(500), MS(200), };
    rc = TimerSettime(t, 0, &itime, NULL);
    assert(rc != -1);
    unpin();

    // let timer fire for a few times
    burn(1);

    // until end point, CPU0 is down, timer keeps firing on CPU1
    printf("start @ %lu ms\n", clock_gettime_mon_ns()/1000000UL);

    // spawn thread that simulates a powerdown of CPU0
    pthread_t powerman_tid;
    rc = pthread_create(&powerman_tid, NULL, powerman, (void *)(uintptr_t)0);
    assert(rc == 0);

    rc = pthread_join(powerman_tid, NULL);

    // until end point, CPU0 has reclaimed its timers.
    printf("end @ %lu ms\n", clock_gettime_mon_ns()/1000000UL);

    burn(1); // run timer for an extra second

    return 0;
}

Classification:

QNX OS

Safety:
Cancellation point No
Signal handler No
Thread Yes
Page updated: