在c中有效暂停执行的最佳方法

时间:2014-07-08 10:35:13

标签: c linux state-machine

我在Linux上运行的库中有一个状态机实现。程序的主循环是等待,直到有足够的时间来要求下一次执行状态机。

在他们的时刻,我有一个类似于以下伪代码的循环:

while( 1 )
{
    while( StateTicks() > 0 )
        StateMachine();

    Pause( 10ms );
}

StateTicks可能每隔50ms左右返回一次。我使Pause()越短,我在程序中使用的CPU时间就越多。

是否有更好的方法来测试一段时间的传递,可能是基于信号?我宁愿暂停执行,直到StateTicks()为> 0而不是完全调用Pause()。

在状态机实现的底层,StateTicks使用clock_gettime(PFT_CLOCK ...),效果很好。我非常希望保持这种计时,因为如果一个StateMachine()调用花费的时间超过了状态机,那么这个实现就会赶上。

暂停使用nanosleep来获得合理准确的暂停时间。

也许这已经是最好的方式,但它看起来并不特别优雅。

2 个答案:

答案 0 :(得分:5)

使用timer_create()创建一个定期计时器,并让它在“计时器刻度信号量”上调用sem_post()

为避免丢失蜱,我建议使用real-time signal,可能是SIGRTMIN+0SIGRTMAX-0sem_post()async-signal-safe,因此您可以安全地在信号处理程序中使用它。

你的状态机只是等待信号量;不需要其他计时。如果您花费太长时间来处理勾号,则以下sem_wait()将不会阻止,但会立即返回。从本质上讲,信号量计算“丢失”的滴答声。

示例代码(未经测试!):

#define  _POSIX_C_SOURCE 200809L
#include <semaphore.h>
#include <signal.h>
#include <errno.h>
#include <time.h>

#define   TICK_SIGNAL (SIGRTMIN+0)

static timer_t tick_timer;
static sem_t   tick_semaphore;

static void tick_handler(int signum, siginfo_t *info, void *context)
{
    if (info && info->si_code == SI_TIMER) {
        const int saved_errno = errno;
        sem_post((sem_t *)info->si_value.sival_ptr);
        errno = saved_errno;
    }
}

static int tick_setup(const struct timespec interval)
{
    struct sigaction  act;
    struct sigevent   evt;
    struct itimerspec spec;

    if (sem_init(&tick_semaphore, 0, 0))
        return errno;

    sigemptyset(&act.sa_mask);
    act.sa_handler = tick_handler;
    act.sa_flags = 0;
    if (sigaction(TICK_SIGNAL, &act, NULL))
        return errno;

    evt.sigev_notify = SIGEV_SIGNAL;
    evt.sigev_signo = TICK_SIGNAL;
    evt.sigev_value.sival_ptr = &tick_semaphore;
    if (timer_create(CLOCK_MONOTONIC, &evt, &tick_timer))
        return errno;

    spec.it_interval = interval;
    spec.it_value = interval;
    if (timer_settime(tick_timer, 0, &spec, NULL))
        return errno;

    return 0;
}

,滴答循环只是

    if (tick_setup(some_interval))
        /* failed, see errno; abort */

    while (!sem_wait(&tick_semaphore)) {

        /* process tick */

    }

如果支持多个并发状态,则一个信号处理程序就足够了。您的州通常会包括

    timer_t          timer;
    sem_t            semaphore;
    struct timespec  interval;

唯一棘手的事情是确保在破坏信号可以访问的状态时没有待处理的定时器信号。

由于信号传递会中断用于信号传递的线程中的任何阻塞I / O,您可能希望在库中设置一个特殊线程来处理定时器滴答实时信号,其中所有其他信号都阻止实时信号线程。您可以标记库初始化函数__attribute__((constructor)),以便它在main()之前自动执行。

最理想的是,您应该使用与信号传递的滴答处理相同的线程。否则,如果使用与运行刻度处理的CPU核心不同的CPU核心传送信号,则在滴答处理中会出现一些小的抖动或延迟。


Basile Starynkevitch的回答让我记忆了等待和信号传递所涉及的延迟:如果你使用nanosleep()clock_gettime(CLOCK_MONOTONIC,),你可以调整睡眠时间以考虑典型情况的等待时间。

以下是使用clock_gettime(CLOCK_MONOTONIC,)nanosleep()的快速测试计划:

#define _POSIX_C_SOURCE 200809L
#include <sys/select.h>
#include <time.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

static const long   tick_latency = 75000L; /* 0.75 ms */
static const long   tick_adjust  = 75000L; /* 0.75 ms */

typedef struct {
    struct timespec  next;
    struct timespec  tick;
} state;

void state_init(state *const s, const double ticks_per_sec)
{
    if (ticks_per_sec > 0.0) {
        const double interval = 1.0 / ticks_per_sec;
        s->tick.tv_sec = (time_t)interval;
        s->tick.tv_nsec = (long)(1000000000.0 * (interval - (double)s->tick.tv_sec));
        if (s->tick.tv_nsec < 0L)
            s->tick.tv_nsec = 0L;
        else
        if (s->tick.tv_nsec > 999999999L)
            s->tick.tv_nsec = 999999999L;
    } else {
        s->tick.tv_sec = 0;
        s->tick.tv_nsec = 0L;
    }
    clock_gettime(CLOCK_MONOTONIC, &s->next);
}

static unsigned long count;

double state_tick(state *const s)
{
    struct timespec now, left;

    /* Next tick. */
    s->next.tv_sec += s->tick.tv_sec;
    s->next.tv_nsec += s->tick.tv_nsec;
    if (s->next.tv_nsec >= 1000000000L) {
        s->next.tv_nsec -= 1000000000L;
        s->next.tv_sec++;
    }

    count = 0UL;

    while (1) {

        /* Get current time. */
        clock_gettime(CLOCK_MONOTONIC, &now);

        /* Past tick time? */
        if (now.tv_sec > s->next.tv_sec ||
            (now.tv_sec == s->next.tv_sec &&
             now.tv_nsec >= s->next.tv_nsec - tick_latency))
            return (double)(now.tv_sec - s->next.tv_sec)
                 + (double)(now.tv_nsec - s->next.tv_nsec) / 1000000000.0;

        /* Calculate duration to wait */
        left.tv_sec = s->next.tv_sec - now.tv_sec;
        left.tv_nsec = s->next.tv_nsec - now.tv_nsec - tick_adjust;
        if (left.tv_nsec >= 1000000000L) {
            left.tv_nsec -= 1000000000L;
            left.tv_sec++;
        } else
        if (left.tv_nsec < -1000000000L) {
            left.tv_nsec += 2000000000L;
            left.tv_sec += 2;
        } else
        if (left.tv_nsec < 0L) {
            left.tv_nsec += 1000000000L;
            left.tv_sec--;
        }

        count++;

        nanosleep(&left, NULL);
    }
}


int main(int argc, char *argv[])
{
    double  rate, jitter;
    long    ticks, i;
    state   s;
    char    dummy;

    if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s TICKS_PER_SEC TICKS\n", argv[0]);
        fprintf(stderr, "\n");
        return 1;
    }

    if (sscanf(argv[1], " %lf %c", &rate, &dummy) != 1 || rate <= 0.0) {
        fprintf(stderr, "%s: Invalid tick rate.\n", argv[1]);
        return 1;
    }
    if (sscanf(argv[2], " %ld %c", &ticks, &dummy) != 1 || ticks < 1L) {
        fprintf(stderr, "%s: Invalid tick count.\n", argv[2]);
        return 1;
    }

    state_init(&s, rate);
    for (i = 0L; i < ticks; i++) {
        jitter = state_tick(&s);
        if (jitter > 0.0)
            printf("Tick %9ld: Delayed   %9.6f ms, %lu sleeps\n", i+1L, +1000.0 * jitter, count);
        else
        if (jitter < 0.0)
            printf("Tick %9ld: Premature %9.6f ms, %lu sleeps\n", i+1L, -1000.0 * jitter, count);
        else
            printf("Tick %9ld: Exactly on time, %lu sleeps\n", i+1L, count);
        fflush(stdout);
    }

    return 0;
}

上面,tick_latency是您愿意提前接受“滴答”的纳秒数,tick_adjust是您从每个睡眠持续时间中减去的纳秒数。

这些的最佳值是高度配置特定的,我没有一个强大的方法来估计它们。对它们进行硬编码(如上所述0.75ms)对我来说也听起来不太好;可能使用命令行选项或环境值让用户控制它,默认为零会更好。

无论如何,将上面的内容编译为

gcc -O2 test.c -lrt -o test

并以50Hz滴答速度运行500-tick测试,

./test 50 500 | sort -k 4

显示在我的机器上,在所需时刻的0.051毫秒(51微秒)内接受滴答。即使降低优先级似乎也不会对其产生太大影响。使用5000蜱以5kHz的速率进行测试(每蜱0.2ms),

nice -n 19 ./test 5000 5000 | sort -k 4

产生类似的结果 - 虽然我没有费心去检查在运行期间机器负载如何变化会发生什么。

换句话说,对单台机器的初步测试表明它可能是一个可行的选项,因此您可能希望在不同的机器上和不同的负载下测试该方案。它比我在我自己的机器上预期的要精确得多(Ubuntu 3.11.0-24-通用x86_64,在AMD Athlon II X4 640 CPU上运行)。

这种方法具有一个有趣的特性,即使它们使用不同的滴答速率,您也可以轻松地使用单个线程来维护多个状态。您只需要检查哪个状态具有下一个刻度(最早->next时间),nanosleep()如果将来发生,并处理刻度,将该状态推进到下一个刻度。

有问题吗?

答案 1 :(得分:0)

除了Nominal Animal's answer

如果 Pause 时间是几毫秒,您可以使用poll(2)nanosleep(2)(您可以计算剩余的睡眠时间,例如使用clock_gettime(2)CLOCK_REALTIME ...)

如果您关心StateMachine可能需要几毫秒(或大部分毫秒)并且您想要一个10毫秒的时间,请考虑使用基于poll的{​​{3}它使用 Linux特定的 event loop

另请参阅timerfd_create(2)time(7)this个答案(关于poll等问题...)