1. 程式人生 > >Linux C/C++定時器的實現原理和使用方法

Linux C/C++定時器的實現原理和使用方法

定時器的實現原理

定時器的實現依賴的是CPU時鐘中斷,時鐘中斷的精度就決定定時器精度的極限。一個時鐘中斷源如何實現多個定時器呢?對於核心,簡單來說就是用特定的資料結構管理眾多的定時器,在時鐘中斷處理中判斷哪些定時器超時,然後執行超時處理動作。而使用者空間程式不直接感知CPU時鐘中斷,通過感知核心的訊號、IO事件、排程,間接依賴時鐘中斷。用軟體來實現動態定時器常用資料結構有:時間輪、最小堆和紅黑樹。下面就是一些知名的實現:

Linux核心定時器相關(Linux v4.9.7, x86體系架構)的一些相關程式碼:

核心啟動註冊時鐘中斷

// @file: arch/x86/kernel/time.c - Linux 4.9.7
// 核心init階段註冊時鐘中斷處理函式 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) { // 呼叫體系架構無關的時鐘處理流程 global_clock_event->event_handler(global_clock_event); return IRQ_HANDLED; }

核心時鐘中斷處理流程

// @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); // 標記一個軟中斷去處理所有到期的定時器 }

核心定時器時間輪演算法

單層時間輪演算法的原理比較簡單:用一個數組表示時間輪,每個時鐘週期,時間輪 current 往後走一個格,並處理掛在這個格子的定時器連結串列,如果超時則進行超時動作處理,然後刪除定時器,沒有則剩餘輪數減一。原理如圖:
這裡寫圖片描述
Linux 核心則採用的是 Hierarchy 時間輪演算法,Hierarchy 時間輪將單一的 bucket 陣列分成了幾個不同的陣列,每個陣列表示不同的時間精度,Linux 核心中用 jiffies 記錄時間,jiffies記錄了系統啟動以來經過了多少tick。下面是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;

Hierarchy 時間輪的原理大致如下,下面是一個時分秒的Hierarchy時間輪,不同於Linux核心的實現,但原理類似。對於時分秒三級時間輪,每個時間輪都維護一個cursor,新建一個timer時,要掛在合適的格子,剩餘輪數以及時間都要記錄,到期判斷超時並調整位置。原理圖大致如下:
這裡寫圖片描述

定時器的使用方法

在Linux 使用者空間程式開發中,常用的定期器可以分為兩類:

  1. 執行一次的單次定時器 single-short;
  2. 迴圈執行的週期定時器 Repeating Timer;

其中,Repeating Timer 可以通過在Single-Shot Timer 終止之後,重新再註冊到定時器系統裡來實現。當一個程序需要使用大量定時器時,同樣利用時間輪、最小堆或紅黑樹等結構來管理定時器。而時鐘週期來源則需要藉助系統呼叫,最終還是從時鐘中斷。Linux使用者空間程式的定時器可用下面方法來實現:

  • 通過alarm()setitimer()系統呼叫,非阻塞非同步,配合SIGALRM訊號處理;
  • 通過select()nanosleep()系統呼叫,阻塞呼叫,往往需要新建一個執行緒;
  • 通過timefd()呼叫,基於檔案描述符,可以被用於 select/poll 的應用場景;
  • 通過RTC機制, 利用系統硬體提供的Real Time Clock機制, 計時非常精確;

上面方法沒提sleep(),因為Linux中並沒有系統呼叫sleep(),sleep()是在庫函式中實現,是通過呼叫alarm()來設定報警時間,呼叫sigsuspend()將程序掛起在訊號SIGALARM上,而且sleep()也只能精確到秒級上,精度不行。當使用阻塞呼叫作為定時週期來源時,可以單獨啟一個執行緒用來管理所有定時器,當定時器超時的時候,向業務執行緒傳送定時器訊息即可。

一個基於時間輪的定時器簡單實現

#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)
{
    // 使用二級指標刪進行單鏈表的刪除
    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);
            *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));

    // 插入到對應格子的連結串列頭部即可, O(1)複雜度
    node->next = timer.slot[pos];
    timer.slot[pos] = node;
    node->rotation = len / TIME_WHEEL_SIZE;
    node->data = 0;
    node->proc = action;
}

 // test case1: 1s迴圈定時器
int g_sec = 0;
void do_time1(int data)
{
    printf("timer %s, %d\n", __FUNCTION__, g_sec++);
    add_timer(1, do_time1);
}

// test case2: 2s單次定時器
void do_time2(int data)
{
    printf("timer %s\n", __FUNCTION__);
}

// test case3: 9s迴圈定時器
void do_time9(int data)
{
    printf("timer %s\n", __FUNCTION__);
    add_timer(9, do_time9);
}

int main()
{
    signal(SIGALRM, tick);
    alarm(1); // 1s的週期心跳

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

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

在實際專案中,一個常用的做法是新起一個執行緒,專門管理定時器,定時來源使用rtc、select等比較精確的來源,定時器超時後向主要的work執行緒發訊息即可,或者使用timefd介面。

參考:

相關推薦

C++迭代實現原理(附帶了Java)

前言 只要用過C++的容器,相信大家對迭代器都不會陌生。它提供一種統一的介面形式來遍歷相應的容器(例如陣列,連結串列,map等)。 例子1:迭代器的遍歷 利用迭代器遍歷陣列vector vector<int> vi{ 1, 3, 5, 7,

LINUX使用一個定時實現設置任意數量定時

ftw rup () int stdlib.h val span 時鐘 sof 本例子參考 Don Libes的Title: Implementing Software Timers例子改寫 為什麽需要這個功能,因為大多數計算機軟件時鐘系統通常只能有一個時鐘觸發一次

C#.NET定時類及使用方法

C#.NET 定時器類及使用方法        在.net常用的定時器類有下面三種,使用定時器時需要設定引數,如間斷時間、定時器計溢位後的回撥函式、延時、開始等,定時器的的主要方法有開始、終止等,不同的定時器實現上述的方法會有一些差異,本文會針對具體的定時

SpringMVC:攔截實現原理登入實現

SpringMVC 攔截器的原理圖 springMVC攔截器的實現一般有兩種方式      第一種方式是要定義的Interceptor類要實現了Spring的HandlerInterceptor 介面    &n

Go中定時實現原理及原始碼解析

> 轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格:https://www.luozhiyun.com > > 本文使用的go的原始碼15.7,需要注意的是由於timer是1.14版本進行改版,但是1.14和1.15版本的timer並無很大區別 我在春節期間寫了一篇文章有關時間輪的:https

Python搜尋引擎實現原理方法

如何在龐大的資料中高效的檢索自己需要的東西?本篇內容介紹了Python做出一個大資料搜尋引擎的原理和方法,以及中間進行資料分析的原理也給大家做了詳細介紹。 布隆過濾器 (Bloom Filter) 第一步我們先要實現一個布隆過濾器。 布隆過濾器是大資料領域的一個常見演算法,它的目的是過濾

Linux C/C++定時實現原理使用方法

定時器的實現原理 定時器的實現依賴的是CPU時鐘中斷,時鐘中斷的精度就決定定時器精度的極限。一個時鐘中斷源如何實現多個定時器呢?對於核心,簡單來說就是用特定的資料結構管理眾多的定時器,在時鐘中斷處理中判斷哪些定時器超時,然後執行超時處理動作。而使用者空間程式不

C# System.Timers.Timer定時的使用定時自動清理內存應用

for process work proc program 指定時間 handle 清理 interval 項目比較大有時候會比較卡,雖然有GC自動清理機制,但是還是有不盡人意的地方。所以嘗試在項目啟動文件中,手動寫了一個定時器,定時清理內存,加快項目運行速度。 pub

Win10-VS2017平臺下C語言定時延時使用

#include "pch.h" #define _CRT_SECURE_NO_WARNINGS  #include <iostream> #include <stdio.h> #include <windows.h> #include <s

基於Libevent最小根堆定時C++定時實現

在libevent中定時器的實現是通過基於最小堆的優先順序佇列來實現的,對於這兩個資料結構比較陌生的可以去翻演算法導論的6.5節中,主要的原始碼都在min_heap.c中,下面是C++的實現。 資料結構 typedef struct min_heap {   struc

高自由度:c++八大排序演算法實現程式碼原理

網上有很多八大排序的程式碼,不過那都比較簡約,只是想表明演算法原理。當然也有個人的部落格寫的也是很好的。我寫的八大排序演算法有以下幾個特點:1、只要改變一個數值,就能實現從小到大或從大到小的排序。2、改變一個N的值可以隨便改變排序陣列的元素的多少。3、排序適合int、lon

C++異常機制的實現方式開銷分析 (大圖,編譯器會為每個函數增加EHDL結構,組成一個單向鏈表,非常著名的“內存訪問違例”出錯對話框就是該機制的一種體現)

執行 對話框 這也 很多 包括 一個棧 簡單 tid 一點 白楊 http://baiy.cn   在我幾年前開始寫《C++編碼規範與指導》一文時,就已經規劃著要加入這樣一篇討論 C++ 異常機制的文章了。沒想到時隔幾年以後才有機會把這個尾巴補完 :-)。 還

C++多態的實現原理

記得 找到 內部 轉載 文件 調用函數 參數 角度 個數 轉載自http://blog.csdn.net/tujiaw/article/details/6753498 1. 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。2. 存在虛函數的類都有一個一

C++ 多態的實現原理

編譯 實現原理 父類 調用 blog 區分 所有 存儲 print 當類中聲明虛函數時,編譯器會在類中生成一個虛函數表 虛函數表是一個存儲類成員函數指針的數據結構 虛函數表是由編譯器自動生成與維護的 virtual成員函數會被編譯器放入虛函數表中 存在虛函數時,每個對象

Objective-C Associated Objects 的實現原理

單獨 維護 borde 強引用 否則 高手 nag 研究 pro 我們知道,在 Objective-C 中可以通過 Category 給一個現有的類添加屬性,但是卻不能添加實例變量,這似乎成為了 Objective-C 的一個明顯短板。然而值得慶幸的是,我們可以通過 As

C++函式模板及實現原理

    C++為我們提供了函式模板機制。所謂函式模板,實際上是建立一個通用函式,其函式型別和形參型別不具體指定,用一個虛擬的型別來代表。這個通用函式就稱為函式模板。     凡是函式體相同的函式都可以用這個模板來代替,不必定義多個函式,只需在模板中定義

C++多型呼叫實現原理(虛擬函式表詳解)

1.帶有虛擬函式的基類物件模型 我們先看段程式碼: #include<iostream> using namespace std; class B1 { public: void func1() {} int _b; }; class B2 { pub

Linux 高精度定時hrtimers簡單介紹應用場景

hrtimer:high-resolution kernel timers:   hrtimers的誕生是由於核心開發者在使用過程中發現,原始的定時器kernel/timers.c,已經可以滿足所有場景的,但是在實際的大量測試中發現還是無法滿足所有場景,所以hrtime

Objective-C Autorelease Pool 的實現原理

記憶體管理一直是學習 Objective-C 的重點和難點之一,儘管現在已經是 ARC 時代了,但是瞭解 Objective-C 的記憶體管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我們才算是真正瞭解了 Obje

c# 迭代實現

開發中如果想要自己實現一個集合資料介面並且可以用foreach來遍歷該集合,一般需要實現2個類 IEnumerable public interface IEnumerable { IEnumerator GetEnumerator(); } 複製程式碼 IEnumerator public i