The realization principle and use method of Linux C / C + + timer

Time:2021-6-7

The realization principle of timer

The implementation of timer depends on CPU clock interrupt, and the precision of clock interrupt determines the limit of timer precision. How can a clock interrupt source implement multiple timers? For the kernel, simply speaking, it is to use a specific data structure to manage many timers, determine which timers timeout in the clock interrupt processing, and then execute the timeout processing action. The user space program does not directly sense the CPU clock interrupt, but indirectly relies on the clock interrupt by sensing the signal, IO events and scheduling of the kernel. The commonly used data structures of dynamic timer are: time wheel, minimum heap and red black tree.

In depth learning video address:Linux high concurrency programming | timer based on red black tree | timer based on time wheel

Linux kernel timer related code:

Kernel start register clock interrupt

// @file: arch/x86/kernel/time.c - Linux 4.9.7
//Register clock interrupt handler in kernel init phase
static struct irqaction irq0  = {
    .handler = timer_interrupt,
    .flags = IRQF_NOBALANCING | IRQF_IRQPOLL | IRQF_TIMER,
    .name = "timer"
};

void __init setup_default_timer_irq(void)
{
    if (!nr_legacy_irqs())
        return;
    setup_irq(0, &irq0);
}

// Default timer interrupt handler for PIT/HPET
static irqreturn_t timer_interrupt(int irq, void *dev_id)
{
    //Call architecture independent clock processing flow
    global_clock_event->event_handler(global_clock_event);
    return IRQ_HANDLED;
}

Processing flow of kernel clock interrupt

// @file: kernel/time/timer.c - Linux 4.9.7
/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
    struct task_struct *p = current;

    /* Note: this timer irq context must be accounted for as well. */
    account_process_tick(p, user_tick);
    run_local_timers();
    rcu_check_callbacks(user_tick);
#ifdef CONFIG_IRQ_WORK
    if (in_irq())
        irq_work_tick();
#endif
    scheduler_tick();
    run_posix_cpu_timers(p);
}

/*
 * Called by the local, per-CPU timer interrupt on SMP.
 */
void run_local_timers(void)
{
    struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);

    hrtimer_run_queues();
    /* Raise the softirq only if required. */
    if (time_before(jiffies, base->clk)) {
        if (!IS_ENABLED(CONFIG_NO_HZ_COMMON) || !base->nohz_active)
            return;
        /* CPU is awake, so check the deferrable base. */
        base++;
        if (time_before(jiffies, base->clk))
            return;
    }
    raise_ softirq(TIMER_ SOFTIRQ); //  Mark a soft interrupt to handle all expired timers
}

Need C / C + + Linux server architect learning materials plus Qun access (materials include C / C + +, Linux, golang technology, nginx, zeromq, mysql, redis, fastdfs, mongodb, ZK, streaming media, CDN, P2P, k8s, docker, TCP / IP, Xie Cheng, dpdk, ffmpeg, etc.), free sharing

The realization principle and use method of Linux C / C + + timerThe realization principle and use method of Linux C / C + + timer

Kernel timer time wheel algorithm

The principle of the single-layer time wheel algorithm is relatively simple: use an array to represent the time wheel. For each clock cycle, the time wheel current moves backward one lattice, and processes the timer chain list hanging in this lattice. If it times out, it will process the timeout action, and then delete the timer. If it does not, the remaining rounds will be reduced by one. The principle is as follows:

The realization principle and use method of Linux C / C + + timer

The Linux kernel uses the hierarchy time wheel algorithm. The hierarchy time wheel divides a single bucket array into several different arrays, and each array represents different time precision. In the Linux kernel, jiffies is used to record the time, and jiffies records how many ticks have passed since the system started. Here are some code for Linux 4.9:

// @file: kernel/time/timer.c - Linux 4.9.7
/*
 * The timer wheel has LVL_DEPTH array levels. Each level provides an array of
 * LVL_SIZE buckets. Each level is driven by its own clock and therefor each
 * level has a different granularity.
 */

/* Size of each clock level */
#define LVL_BITS    6
#define LVL_SIZE    (1UL << LVL_BITS)

/* Level depth */
#if HZ > 100
# define LVL_DEPTH  9
# else
# define LVL_DEPTH  8
#endif

#define WHEEL_SIZE  (LVL_SIZE * LVL_DEPTH)

struct timer_base {
    spinlock_t      lock;
    struct timer_list   *running_timer;
    unsigned long       clk;
    unsigned long       next_expiry;
    unsigned int        cpu;
    bool            migration_enabled;
    bool            nohz_active;
    bool            is_idle;
    DECLARE_BITMAP(pending_map, WHEEL_SIZE);
    struct hlist_head   vectors[WHEEL_SIZE];
} ____cacheline_aligned;

The principle of hierarchy time wheel is roughly as follows. The following is a hierarchy time wheel of hours, minutes and seconds, which is different from the implementation of Linux kernel, but the principle is similar. For the time, minute and second level time wheel, each time wheel maintains a cursor. When creating a new timer, it should be hung in the appropriate grid. The number of remaining rounds and time should be recorded. When the timer is due, it should judge the timeout and adjust the position. The schematic diagram is as follows:

The realization principle and use method of Linux C / C + + timer

How to use timer

In the development of Linux user space program, there are two kinds of timers

  1. A single short timer is executed once;
  2. The cycle timer of cycle execution is repeating timer;

Among them, repeating timer can be realized by re registering in the timer system after the single shot timer is terminated. When a process needs to use a large number of timers, it also uses time wheel, minimum heap or red black tree to manage timers. The source of the clock cycle needs the help of system call, and finally interrupt from the clock. The timer of Linux user space program can be realized by the following methods:

  • adoptalarm()orsetitimer()System call, non blocking, asynchronous, coordinationSIGALRMSignal processing;
  • adoptselect()ornanosleep()System calls, blocking calls, often need to create a new thread;
  • adopttimefd()Call, based on file descriptor, can be used in select / poll application scenarios;
  • Through RTC mechanism and real time clock mechanism provided by system hardware, the timing is very accurate;

Sleep () is not mentioned in the above method because there is no system call to sleep () in Linux. Sleep () is implemented in library functions. It sets the alarm time by calling alarm (), and suspends the process on sigalarm by calling sigsuspend (). Moreover, sleep () can only be accurate to the second level, which is not accurate. When the blocking call is used as the source of timing cycle, a thread can be started to manage all timers. When the timer times out, the timer message can be sent to the business thread.

A simple implementation of timer based on time wheel

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

#define TIME_WHEEL_SIZE 8

typedef void (*func)(int data);

struct timer_node {
    struct timer_node *next;
    int rotation;
    func proc;
    int data;
};

struct timer_wheel {
    struct timer_node *slot[TIME_WHEEL_SIZE];
    int current;
};

struct timer_wheel timer = {{0}, 0};

void tick(int signo)
{
    //Using secondary pointer deletion to delete single linked list
    struct timer_node **cur = &timer.slot[timer.current];
    while (*cur) {
        struct timer_node *curr = *cur;
        if (curr->rotation > 0) {
            curr->rotation--;
            cur = &curr->next;
        } else {
            curr->proc(curr->data);  //  Bug fix: swap locations as follows
            *cur = curr->next;
            free(curr);
        }
    }
    timer.current = (timer.current + 1) % TIME_WHEEL_SIZE;
    alarm(1);
}

void add_timer(int len, func action)
{
    int pos = (len + timer.current) % TIME_WHEEL_SIZE;
    struct timer_node *node = malloc(sizeof(struct timer_node));

    //It can be inserted into the head of the linked list of the corresponding lattice, with O (1) complexity
    node->next = timer.slot[pos];
    timer.slot[pos] = node;
    node->rotation = len / TIME_WHEEL_SIZE;
    node->data = 0;
    node->proc = action;
}

 //Test case 1: 1s cycle timer
int g_sec = 0;
void do_time1(int data)
{
    printf("timer %s, %d\n", __FUNCTION__, g_sec++);
    add_timer(1, do_time1);
}

//Test case 2: 2S single timer
void do_time2(int data)
{
    printf("timer %s\n", __FUNCTION__);
}

//Test case 3: 9s cycle timer
void do_time9(int data)
{
    printf("timer %s\n", __FUNCTION__);
    add_timer(9, do_time9);
}

int main()
{
    signal(SIGALRM, tick);
    alarm(1); //  One second cycle heartbeat

    // test
    add_timer(1, do_time1);
    add_timer(2, do_time2);
    add_timer(9, do_time9);

    while(1) pause();
    return 0;
}

In the actual project, a common practice is to create a new thread to manage the timer. The timing source uses RTC, select and other more accurate sources. After the timer times out, send a message to the main work thread, or use the timefd interface.