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 使用者空間程式開發中,常用的定期器可以分為兩類:
- 執行一次的單次定時器 single-short;
- 迴圈執行的週期定時器 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