Linux內核開發基礎-低精度timer_list和高精度hrtimer定時器

Linux內核定時器開發-低精度和高精度定時器

上篇文章講解了如何正確的使用內核延時函數,在進行驅動開發時,可能會經常用到精確地延時操作。除此之外,如果要實現一個定時任務,那就需要用到定時器。作爲一項基礎功能需求,Linux內核提供了定時器相關的實現。下面就具體看一下,Linux內核所提供的定時器實現。

定時器種類

爲了適應不同的應用場景,Linux內核提供了兩種定時器:低精度和高精度定時器。低精度定時器基於硬件的週期性中斷實現,其定時週期的粒度爲1/HZms,例如,內核HZ爲1000,那麼低精度的定時器最小定時時間爲1ms;高精度定時器可以實現ns級的定時,不過,實際的定時週期粒度與CPU的主頻有關,比如,桌面級的CPU一般都是GHZ級別,那麼,其定時粒度可以達到ns級別,而對於嵌入式CPU,其主頻一般在百兆級別,那麼定時粒度就只能達到us級別了。在進行開發時,需要根據實際場景,選擇合適的定時器來實現功能。那麼,實際開發時,如何使用這兩類定時器呢?下面兩節,就來具體看一下。

低精度定時器

上文說了,低精度定時器依賴於內核時鐘中斷實現,內核中所有的此類定時器會組成一個鏈表,處理時鐘中斷時,內核會檢查定時器列表是否有到時的定時器,如果有,就會調用定時器處理函數,進行相關的處理。

這裏需要強調的是,執行此類定時器的處理函數時處於“軟中斷”上下文,軟中斷是中斷下半部處理的一種機制,“軟中斷”上下文是原子性的,不可以執行可能會引起系統調度或可能睡眠的操作,比如,kmalloc、copy_from_user、msleep、mutex_lock等等。如果在定時器處理函數中調用了上述函數,比如,msleep函數,會引其內核模塊崩潰,嚴重的會導致系統崩潰,所以,使用時切記小心!

基本數據結構

struct timer_list {
	/*
	 * All fields that change during normal runtime grouped to the
	 * same cacheline
	 */
	struct hlist_node	entry;
	unsigned long		expires;
	void			(*function)(unsigned long);
	unsigned long		data;
	u32			flags;
};

上面是低精度定時器timer_list的數據結構,從名字就可以判斷出其基於鏈表實現,其通過entry掛載到內核的定時器哈希列表中。expires表示定時時間,注意:expires的單位爲時鐘滴答間隔,比如,你想設置一個定時週期爲10ms的定時器,那麼expires應該表示爲jiffies+msec_to_jiffies(10);function爲定時器處理函數,data爲function的入參,flags爲定時器的一些選項,一般不需要配置。

可以看到,定時器的數據結構十分的簡單,基本上沒有什麼難理解的地方。下面主要看一下,如何使用該定時器。

主要API

要想在內核中使用timer_list定時器,一般需要以下幾個步驟:

  1. 聲明一個定時器,例如,struct timer_list timer;

  2. 初始化定義定時器,初始化定時器有幾種方式:

    • #define TIMER_INITIALIZER(_function, _expires, _data) ,例如,struct timer_list timer = TIMER_INITIALIZER(func, expires, data);
    • #define DEFINE_TIMER(_name, _function, _expires, _data),例如,DEFIME_TIMER(timer,func,expires,data);
    • #define setup_timer(timer, fn, data), 例如,setup_timer(&timer, func, data)
  3. 增加定時器, add_timer用於將定時器添加到內核定時器鏈表中,至此,定時器開始工作。timer_list,爲單次觸發的定時器,如果想連續執行定時任務,那麼需要再定時處理函數的末尾再次執行add_timer,以便再次激活定時器。add_timer函數原型如下:

    extern void add_timer(struct timer_list *timer);

  4. 刪除定時器,del_timer用於將定時器從內核定時器鏈表中立刻去除,不管是否正在處理該定時器。del_timer_sync(),是del_timer的同步版本,其會等待該定時器被處理完畢,注意,該函數可能會導致發生系統調度,所以其不能用在原子上下文中,比如,中斷上下文。del_timer函數原型如下:

    extern int del_timer(struct timer_list * timer);

  5. 修改定時器,mod_timer可以修改定時器的超時時間,其函數原型如下:

    mod_timer(struct timer_list *timer, unsigned long expires)

    • timer,表示當前被修改的定時器
    • expires,基於jiffiesd新的超時時間

對於已經激活的定時器(未激活時,mod_timer會將其激活),修改超時時間,使用mod_timer十分高效的,其相當於下面的操作:

del_timer(timer); timer->expires = expires; add_timer(timer); 

這裏需要注意的是,如果系統中有多個用戶同步的使用同一個已激活的定時器(未加鎖進行串行化),那麼使用mod_timer是唯一可以安全的修改定時器的方法,因爲mod_timer對於定時器的超時時間的修改是原子性的

定時器使用模板

好了,實際開發時,知道上面的API使用方式,完全可以應付90%的低速定時器使用場景。下面說一下此類定時器的使用時的具體模式,請看代碼。

lrtimer.c文件:

    #include <linux/kernel.h>
	#include <linux/module.h>
	#include <linux/time.h>
	 
	static struct timer_list timer;
	static unsigned long data = 10; 	 
	 
	/*
	 * @breif:定時器處理函數
	 *
	 * @func:
	 *
	 * @param: data爲setup_timer時傳入的data參數
	 *
	 * @return:
	 *
	 */
	static void timer_cb(unsigned long data)
	{
	    printk("data = %lu.\n", data);
	 
	    mod_timer(&timer, jiffies + msecs_to_jiffies(10)); /*step3:重新激活定時器*/
	}
	 
	 
	static int __init module_lrtimer_init( void )
	{
	    printk("low resolution timer init.\n");
	    
	    setup_timer(&timer, timer_cb, data);              /*step1:定義並初始化定時器*/
	 
	    mod_timer(&timer, jiffies + msecs_to_jiffies(10/*ms*/));/*step2:修改定時器超時時間,並激活定時器*/
	 
	    return 0;
	}
	 
	 
	static void __exit module_lrtimer_exit( void )
	{
	    printk("low resolution timer exit.\n");
	    del_timer(&timer);
	}
	 
	module_init(module_lrtimer_init);
	module_exit(module_lrtimer_exit);

Makefile文件:

	KVERS = $(shell uname -r)
	        
	# Kernel modules
	obj-m += lrtimer.o
	        
	# Specify flags for the module compilation.
	EXTRA_CFLAGS=-g -O0 
	        
	build: kernel_modules
	        
	kernel_modules:
	    make -C $(KVERS)/build M=$(CURDIR) modules
	        
	clean:
	    make -C $(KVERS)/build M=$(CURDIR) clean         

高精度定時器

上文看到低精度定時器的分辨率嚴重依賴內核的時鐘中斷,如果HZ爲1000,那麼其分辨率也僅僅爲1ms,如果想要使用更高分辨率的定時器,那隻能求助於hrtimer了。Linux內核在2005年開始在內核中增加hrtimer的支持,其基本特性如下:

  • 高精度,定時的分辨率爲1ns(ps:實際的分辨率依賴於CPU時鐘的頻率,嵌入式系統的分辨率在us級別),其時鐘源來自於CPU的硬件clock。
  • 與timer_list不同,與jiffies沒有任何關係,所有內核邏輯都工作在64-bit ns級分辨率之上。
  • 支持多種平臺,i386, x86_64, ARM, PPC, PPC64, IA64。

hrtimer的應用也十分的廣泛,依賴於posix-timers的用戶空間程序,內核中需要高分辨率的驅動程序(多媒體驅動程序)等。

檢查是否支持hrtimer

在使用hrtimer之前,需要確認當前內核是否支持,檢測是否支持的方式有兩種:

  1. 對於自行編譯的內核,可以檢測內核的配置文件是否打開了CONFIG_HIGH_RES_TIMERS選項。

     #
     # Timers subsystem
     #
     CONFIG_TICK_ONESHOT=y
     CONFIG_NO_HZ_COMMON=y
     # CONFIG_HZ_PERIODIC is not set
     CONFIG_NO_HZ_IDLE=y
     # CONFIG_NO_HZ_FULL is not set
     CONFIG_NO_HZ=y
     CONFIG_HIGH_RES_TIMERS=y
    
  2. 對於現存的內核,可以通過查看/proc/timer_list的信息,來確定是否支持hrtimer。

     root@zpd /proc$ cat timer_list 
     Timer List Version: v0.7
     HRTIMER_MAX_CLOCK_BASES: 4
     now at 1559369165518 nsecs
     
     cpu: 0
      clock 0:
       .base:       8bdc0308
       .index:      0
       .resolution: 1 nsecs
    

注意到,HRTIMER_MAX_CLOCK_BASES:4,.resolution: 1 nsecs表示當前內核是支持hrtimer的。

基本數據結構

  1. ktime:

     union ktime {   
         s64 tv64;
     	#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
     	struct {
     	# ifdef __BIG_ENDIAN
     	s32 sec, nsec;
     	# else
     	s32 nsec, sec;
     	# endif
     	} tv; 
     	#endif	
     };
    

ktime用於保存hrtimer的定時時間,從定義中可以看出其完美高效的支持32bit/64bit系統。

  1. hrtimer

     struct hrtimer {
     	struct timerqueue_node		node;
     	ktime_t				_softexpires;
     	enum hrtimer_restart		(*function)(struct hrtimer *);
     	struct hrtimer_clock_base	*base;
     	u8				state;
     	u8				is_rel;
     };
    

struct hrtimer爲hrtimer的基本數據結構,其主要包括如下幾部分:

  • node,hrtimer最終通過node掛接到timerqueue中。
  • _softexpires,表示hrtimer的定時時間,_soft表示此定時時間只是軟件意義上的。
  • function,表示hrtimer的超時處理函數。

主要API

  1. ktime相關的API

    • ktime_t ktime_set(const long secs, const unsigned long nsecs),通過secs和nsecs生成ktime_t
    • static inline ktime_t ns_to_ktime(u64 ns),通過ns生成ktime_t
    • static inline ktime_t ms_to_ktime(u64 ms),通過ms生成ktime_t
  2. hrtimer_init 用於初始化一個hrtimer

     extern void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);
    
     - timer:表示hrtimer定時器
     - which_clock:表示選擇系統的哪種時鐘,主要包括兩種:CLOCK_REALTIME、CLOCK_MONOTONIC,兩種時鐘的區別是,CLOCK_REALTIME表示絕對時間,而CLOCK_MONOTONIC表示相對時間。
     - mode:表示hrtimer的類型,主要包括兩種:HRTIMER_MODE_ABS、HRTIMER_MODE_REL,前者是絕對模式,對應於CLOCK_REALTIME時鐘,後者是相對模式,對應於CLOCK_MONOTONIC。
    
  3. hrtimer_start用於啓動一個定時器

     int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode)
     
    - timer:表示當前的定時器
    - tim:定時時間
    - mode:與hrtimer_init中的mode一樣。
    
  4. hrtimer_cancle應於取消一個定時器, 並等待其執行完畢

     int hrtimer_cancel(struct hrtimer *timer)
    
  5. hrtimer_forward_now,重新設置hrtimer的超時時間,用於實現連續定時。

      u64 hrtimer_forward_now(struct hrtimer *timer, ktime_t interval)                                                                                                                                                  
    

示例

下面是基於hrtimer的簡單示例,定時器的超時時間爲10ms,並且在定時器超時處理函數中,調用hrtimer_forward_now再次啓動定時器。

hrtimer.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
 
MODULE_LICENSE("GPL");
                                                                                                                                                                                                                    
static struct hrtimer hr_timer;
static unsigned long interval= 10; /* unit: ms */
struct timespec uptimeLast;
 
unsigned long long diff_tv(struct timespec start, struct timespec end) 
{
    return (end.tv_sec - start.tv_sec)*1000000000 + (end.tv_nsec-start.tv_nsec);
}
 
enum hrtimer_restart my_hrtimer_callback( struct hrtimer *timer )
{
    struct timespec uptime;
 
    do_posix_clock_monotonic_gettime(&uptime);  
 
    printk(KERN_INFO"hrtimer:%9lu sec, %9lu ns, interval_delay=%lu us\n", 
            (unsigned long) uptime.tv_sec, 
            uptime.tv_nsec,
            (unsigned long)(diff_tv(uptimeLast, uptime))/1000); 
 
    uptimeLast=uptime;
 
    hrtimer_forward_now(timer, ns_to_ktime(interval));
 
    return HRTIMER_RESTART;
}
 
static int __init module_hrtimer_init( void )
{
    struct timespec uptime; 
    static ktime_t ktime;
 
    printk(KERN_INFO"HR Timer module installing\n");
 
    hrtimer_init( &hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL );
 
    ktime = ms_to_ktime(interval);
 
    hr_timer.function = my_hrtimer_callback;
    hrtimer_start(&hr_timer, ktime, HRTIMER_MODE_REL );
 
    do_posix_clock_monotonic_gettime(&uptime);
    uptimeLast = uptime;
 
    printk(KERN_INFO "hrtimer:%9lu sec, %9lu ns\n", 
            (unsigned long) uptime.tv_sec, uptime.tv_nsec ); 
 
    return 0;
}

static void __exit module_hrtimer_exit( void )
{
	int ret;
    ret = hrtimer_cancel( &hr_timer );
    if (ret) 
      printk("The timer was still in use...\n");
    printk("HR Timer module uninstalling.\n");
}

module_init(module_hrtimer_init);
module_exit(module_hrtimer_exit);   

Makefile文件:

#KVERS = $(shell uname -r)                                                                                                                                                                                          

# Kernel modules
obj-m += hrtimer.o

# Specify flags for the module compilation.	
EXTRA_CFLAGS=-g -O0 

build: kernel_modules

kernel_modules:
    make -C $(KVERS)/build M=$(CURDIR) modules

clean:
    make -C $(KVERS)/build M=$(CURDIR) clean
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章