C语言定时器编程完全指南

系统编程定时器

定时器概述

定时器是操作系统提供的核心机制之一,用于在指定时间后或按固定间隔触发事件。在C语言中,定时器广泛应用于协议超时检测、周期性任务调度、性能统计、心跳机制等场景。

Linux定时器方案分类

  • sleep族函数sleepusleepnanosleep,简单但阻塞线程
  • alarm/setitimer:基于信号,仅支持秒/微秒级,一个进程只能有一个
  • POSIX定时器timer_create/timer_settime,纳秒级精度,可自定义通知方式
  • timerfd:Linux特有,将定时器与文件描述符结合,可配合epoll使用

时钟类型

  • CLOCK_REALTIME:系统实时时钟,受NTP调整影响
  • CLOCK_MONOTONIC:单调递增,不受系统时间修改影响,适合测量间隔
  • CLOCK_BOOTTIME:同MONOTONIC但包含休眠时间
  • CLOCK_PROCESS_CPUTIME_ID:进程CPU时间

sleep族函数

sleep族函数是最简单的定时方式,让当前线程暂停执行指定时间。

#include <unistd.h>
#include <time.h>

unsigned int sleep(unsigned int seconds);    /* 秒级 */
int usleep(useconds_t usec);                /* 微秒级(已废弃) */
int nanosleep(const struct timespec *req, struct timespec *rem); /* 纳秒级 */

nanosleep正确用法

#include <stdio.h>
#include <time.h>
#include <errno.h>

int main(void) {
    struct timespec req = { .tv_sec = 0, .tv_nsec = 500000000 };
    struct timespec rem;

    /* 被信号中断时需继续休眠剩余时间 */
    while (nanosleep(&req, &rem) == -1) {
        if (errno == EINTR) {
            req = rem;
        } else {
            perror("nanosleep");
            return 1;
        }
    }
    printf("Sleep completed\n");
    return 0;
}

sleep族函数的局限:阻塞线程、精度受内核调度限制(1-4ms)、无法实现多个并发定时任务、被信号中断需手动处理。

POSIX定时器接口

POSIX定时器支持纳秒级精度,每个进程可创建多个独立定时器,支持信号或线程通知。

#include <time.h>
#include <signal.h>

/* 创建定时器 */
int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);
/* 设置定时器 */
int timer_settime(timer_t timerid, int flags,
                   const struct itimerspec *new_value, struct itimerspec *old_value);
/* 获取剩余时间 */
int timer_gettime(timer_t timerid, struct itimerspec *curr_value);
/* 删除定时器 */
int timer_delete(timer_t timerid);
/* 获取超期溢出次数 */
int timer_getoverrun(timer_t timerid);

关键数据结构

struct timespec {
    time_t  tv_sec;   /* 秒 */
    long    tv_nsec;  /* 纳秒 (0-999999999) */
};

struct itimerspec {
    struct timespec it_value;    /* 首次超时时间 */
    struct timespec it_interval;  /* 周期间隔(0=单次) */
};

通知方式

  • SIGEV_NONE:不通知,仅通过 timer_gettime 查询
  • SIGEV_SIGNAL:超时时发送信号,在信号处理函数中处理
  • SIGEV_THREAD:超时时在新线程中调用指定函数
  • SIGEV_THREAD_ID(Linux扩展):发送信号到指定线程

信号驱动定时器

信号驱动是最常用的POSIX定时器模式,定时器超时时发送信号,进程在信号处理函数中执行定时逻辑。

#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

#define SIG_TIMER SIGRTMIN

static volatile int tick_count = 0;

static void timer_handler(int sig, siginfo_t *si, void *uc) {
    if (sig == SIG_TIMER) {
        tick_count++;
        const char msg[] = "tick\n";
        write(STDOUT_FILENO, msg, sizeof(msg) - 1);
    }
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_flags     = SA_SIGINFO;
    sa.sa_sigaction = timer_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIG_TIMER, &sa, NULL);

    timer_t timerid;
    struct sigevent sev = {0};
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo  = SIG_TIMER;
    sev.sigev_value.sival_ptr = &timerid;
    timer_create(CLOCK_MONOTONIC, &sev, &timerid);

    /* 1秒后首次超时,之后每500ms一次 */
    struct itimerspec its = {
        .it_value    = { .tv_sec = 1,  .tv_nsec = 0 },
        .it_interval = { .tv_sec = 0,  .tv_nsec = 500000000 }
    };
    timer_settime(timerid, 0, &its, NULL);

    while (tick_count < 20)
        pause();

    timer_delete(timerid);
    printf("Stopped after %d ticks\n", tick_count);
    return 0;
}

使用实时信号避免丢失

标准信号不可排队,同一信号多次发送只记录一次。实时信号(SIGRTMIN~SIGRTMAX)会排队不会丢失。通过 timer_getoverrun 可获取因排队限制丢失的超时次数。

线程通知模式

SIGEV_THREAD 模式在定时器超时时创建新线程执行回调,避免了信号处理的限制:

#define _GNU_SOURCE
#include <stdio.h>
#include <time.h>
#include <unistd.h>

static void timer_callback(union sigval sv) {
    int *counter = (int *)sv.sival_ptr;
    (*counter)++;
    printf("Timer tick #%d\n", *counter);
}

int main(void) {
    static int counter = 0;
    timer_t timerid;

    struct sigevent sev = {0};
    sev.sigev_notify          = SIGEV_THREAD;
    sev.sigev_notify_function = timer_callback;
    sev.sigev_value.sival_ptr = &counter;
    timer_create(CLOCK_MONOTONIC, &sev, &timerid);

    struct itimerspec its = {
        .it_value    = { .tv_sec = 1, .tv_nsec = 0 },
        .it_interval = { .tv_sec = 1, .tv_nsec = 0 }
    };
    timer_settime(timerid, 0, &its, NULL);

    while (counter < 5) sleep(1);
    timer_delete(timerid);
    return 0;
}

SIGEV_THREAD 的优点是回调在独立线程执行,不受异步信号安全限制;缺点是每次超时创建线程,开销较大,不适合高频定时器。

定时器精度与漂移

影响精度的因素

  • 内核HZ:时钟中断频率(100/250/1000Hz),决定最小调度粒度
  • 调度延迟:定时器到期后需等待内核调度
  • 信号延迟:不可中断系统调用期间信号会延迟投递
  • NTP调整:CLOCK_REALTIME 受NTP同步影响可能导致跳变

避免累积漂移

/* 错误:每次从当前时间计算下次超时,产生累积漂移 */
while (running) {
    nanosleep(&interval, NULL);
    do_periodic_task();  /* 任务执行时间被加入间隔 */
}

/* 正确:使用 TIMER_ABSTIME 绝对时间避免漂移 */
struct itimerspec its;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);

its.it_value.tv_sec  = now.tv_sec + 1;  /* 绝对时间 */
its.it_value.tv_nsec = now.tv_nsec;
its.it_interval.tv_sec  = 0;
its.it_interval.tv_nsec = 500000000;

/* TIMER_ABSTIME:it_value 为绝对时间戳 */
timer_settime(timerid, TIMER_ABSTIME, &its, NULL);

使用 TIMER_ABSTIME 时,it_value 被解释为绝对时间戳而非相对时间,内核自动在正确时间点触发定时器,消除累积漂移。

实战:多定时器管理框架

以下实现基于POSIX定时器和实时信号的多定时器管理框架,支持动态添加、删除定时器。

/* timer_framework.h */
#ifndef TIMER_FRAMEWORK_H
#define TIMER_FRAMEWORK_H
#include <time.h>
#include <signal.h>

#define MAX_TIMERS 32
typedef void (*timer_cb_t)(void *data);

typedef struct {
    timer_t     timerid;
    int         active;
    int         id;
    timer_cb_t  callback;
    void       *user_data;
} tmr_entry_t;

typedef struct {
    tmr_entry_t  timers[MAX_TIMERS];
    int          count;
    int          next_id;
} tmr_manager_t;

int  tmr_init(tmr_manager_t *mgr);
int  tmr_add(tmr_manager_t *mgr, double delay, double interval,
             timer_cb_t cb, void *data);
int  tmr_remove(tmr_manager_t *mgr, int id);
void tmr_destroy(tmr_manager_t *mgr);
tmr_manager_t *tmr_get_manager(void);
#endif

框架实现

/* timer_framework.c */
#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include "timer_framework.h"

#define TIMER_SIGNAL SIGRTMIN
static tmr_manager_t g_mgr;

static void timer_signal_handler(int sig, siginfo_t *si, void *uc) {
    if (sig != TIMER_SIGNAL) return;
    tmr_entry_t *e = (tmr_entry_t *)si->si_value.sival_ptr;
    if (e && e->active && e->callback)
        e->callback(e->user_data);
}

int tmr_init(tmr_manager_t *mgr) {
    memset(mgr, 0, sizeof(*mgr));
    mgr->next_id = 1;
    struct sigaction sa = {0};
    sa.sa_flags     = SA_SIGINFO | SA_RESTART;
    sa.sa_sigaction = timer_signal_handler;
    sigemptyset(&sa.sa_mask);
    return sigaction(TIMER_SIGNAL, &sa, NULL);
}

int tmr_add(tmr_manager_t *mgr, double delay, double interval,
             timer_cb_t cb, void *data) {
    if (mgr->count >= MAX_TIMERS) return -1;
    int slot = -1;
    for (int i = 0; i < MAX_TIMERS; i++)
        if (!mgr->timers[i].active) { slot = i; break; }
    if (slot < 0) return -1;

    tmr_entry_t *e = &mgr->timers[slot];
    e->id = mgr->next_id++;
    e->callback = cb;
    e->user_data = data;
    e->active = 1;

    struct sigevent sev = {0};
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo  = TIMER_SIGNAL;
    sev.sigev_value.sival_ptr = e;
    if (timer_create(CLOCK_MONOTONIC, &sev, &e->timerid) == -1)
        return -1;

    struct itimerspec its;
    its.it_value.tv_sec  = (time_t)delay;
    its.it_value.tv_nsec = (long)((delay - (time_t)delay) * 1e9);
    its.it_interval.tv_sec  = (time_t)interval;
    its.it_interval.tv_nsec = (long)((interval - (time_t)interval) * 1e9);
    if (interval <= 0) { its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; }

    if (timer_settime(e->timerid, 0, &its, NULL) == -1) {
        timer_delete(e->timerid);
        return -1;
    }
    mgr->count++;
    return e->id;
}

int tmr_remove(tmr_manager_t *mgr, int id) {
    for (int i = 0; i < MAX_TIMERS; i++) {
        if (mgr->timers[i].active && mgr->timers[i].id == id) {
            timer_delete(mgr->timers[i].timerid);
            mgr->timers[i].active = 0;
            mgr->count--;
            return 0;
        }
    }
    return -1;
}

void tmr_destroy(tmr_manager_t *mgr) {
    for (int i = 0; i < MAX_TIMERS; i++) {
        if (mgr->timers[i].active) {
            timer_delete(mgr->timers[i].timerid);
            mgr->timers[i].active = 0;
        }
    }
    mgr->count = 0;
}

tmr_manager_t *tmr_get_manager(void) { return &g_mgr; }

使用示例

#include <stdio.h>
#include <unistd.h>
#include "timer_framework.h"

static void heartbeat_cb(void *data) {
    int *c = (int *)data;
    (*c)++;
    printf("[heartbeat] count=%d\n", *c);
}

static void timeout_cb(void *data) {
    printf("[timeout] One-shot timer fired!\n");
}

int main(void) {
    tmr_manager_t *mgr = tmr_get_manager();
    tmr_init(mgr);
    static int hb_count = 0;

    /* 1秒间隔的周期性心跳定时器 */
    int hb_id = tmr_add(mgr, 1.0, 1.0, heartbeat_cb, &hb_count);
    /* 5秒单次超时定时器 */
    int to_id = tmr_add(mgr, 5.0, 0.0, timeout_cb, NULL);

    sleep(10);
    tmr_remove(mgr, hb_id);
    tmr_destroy(mgr);
    printf("Total heartbeats: %d\n", hb_count);
    return 0;
}
# 编译(需链接 rt 和 pthread 库)
gcc -o timer_demo timer_framework.c main.c -lrt -lpthread
./timer_demo

总结

  • sleep族函数 - 最简单的定时方式,但阻塞线程且精度有限
  • POSIX定时器 - 高精度、可扩展,支持信号和线程两种通知方式,推荐方案
  • 信号驱动 - 使用实时信号避免丢失,信号处理函数中只能调用异步安全函数
  • 线程通知 - SIGEV_THREAD更灵活但开销大,适合低频定时任务
  • 精度控制 - CLOCK_MONOTONIC避免时间跳变,TIMER_ABSTIME避免累积漂移
  • 多定时器框架 - 基于POSIX定时器和实时信号封装,通过sigev_value传递上下文

定时器编程是系统编程的重要技能。对于生产环境,推荐使用 CLOCK_MONOTONIC + TIMER_ABSTIME 组合确保精度和稳定性,注意信号安全和溢出处理。在需要与事件循环集成的场景中,可考虑 Linux 特有的 timerfd 机制,将定时器与 epoll 无缝结合。