內核 API,第 3 部分: 2.6 內核中的計時器和列表

Linux® 內核包含種類繁多的 API,旨在幫助開發人員構建更簡單、更高效的驅動程序和內核應用程序。可以用於任務延遲的兩個常見 API 是列表管理 API 和計時器 API。探索這些 API,瞭解如何使用計時器和列表開發內核應用程序。


(Linux)時間的起源

在 Linux 內核中,時間由一個名爲 jiffies 的全局變量衡量,該變量標識系統啓動以來經過的滴答數。在最低的級別上,計算滴答數的方式取決於正在運行的特定硬件平臺;但是,滴答計數通常在一次中斷期間仍然繼續進行。滴答速率(jiffies 的最不重要的位)可以配置,但在最近針對 x86 的 2.6 內核中,一次滴答等於 4ms(250Hz)。jiffies 全局變量在內核中廣泛使用,目的有幾個,其中之一是提供用於計算一個計時器的超時值的當前絕對時間(稍後將展示一個例子)。

內核計時器

最近的 2.6 內核中有幾個不同的計時器模式,其中最簡單、最不精確(但適用於大多數實例)的模式就是計時器 API。這個 API 允許構造在 jiffies 域(最低 4ms 超時)中運行的計時器。還有一個高精確度計時器 API,它允許構造在以納秒定義的時間中運行的計時器。根據您的處理器和處理器運行的速度,您的里程(mileage)可能會不同,但這個 API 的確提供了一種方法來在 jiffies 滴答間隔下調度超時。

標準計時器

標準計時器 API 作爲 Linux 內核的一部分已經有很長一段時間了(自從 Linux 內核的早期版本開始)。儘管它提供的精確性比高精確度計時器要低,但它對於在處理物理設備時提供錯誤覆蓋的傳統驅動程序超時來說比較理想。在很多情況下,這些超時實際上從不觸發,而是被啓動,然後被刪除。

簡單內核計時器使用計時器輪(timer wheel) 實現。這個主意是由 Finn Arne Gangstad 在 1997 年首次引入的。它不理睬管理大量計時器的問題,而是很好地管理數量合理的計時器 — 典型情況。(原始計時器實現只是按照過期順序將計時器實現雙重鏈接。儘管在概念上比較簡單,但這種方法是不可伸縮的。)時間輪是一個 buckets 集合,其中每個 bucker 表示將來計時器過期的一個時間塊。這些 buckets 使用基於 5 個 bucket 的對數時間定義。使用 jiffies 作爲時間粒度,定義了幾個組,它們表示將來的過期時段(其中每個組通過一列計時器表示)。計時器插入使用具有 O(1) 複雜度的列表操作發生,過期發生在 O(N) 時間內。計時器過期以串聯的形式出現,其中計時器被從高粒度 buckets 刪除,然後隨着它們的過期時間的下降被插入到低粒度 buckets 中。現在我們查看一下針對這個計時器實現的 API。

計時器 API

Linux 提供了一個簡單的 API 來構造和管理計時器。它包含一些函數(和助手函數),用於創建、取消和管理計時器。

計時器通過 timer_list 結構定義,該結構包括實現一個計時器所需的所有數據(其中包括列表指針和在編譯時配置的可選計時器統計數據)。從用戶角度看,timer_list 包含一個過期時間,一個回調函數(當/如果計時器過期),以及一個用戶提供的上下文。用戶必須初始化計時器,可以採取幾種方法,最簡單的方法是調用 setup_timer,該函數初始化計時器並設置用戶提供的回調函數和上下文。或者,用戶可以設置計時器中的這些值(函數和數據)並簡單地調用 init_timer。注意,init_timersetup_timer 內部調用。

void init_timer( struct timer_list *timer );
void setup_timer( struct timer_list *timer, 
                     void (*function)(unsigned long), unsigned long data );

擁有一個經過初始化的計時器之後,用戶現在需要設置過期時間,這通過調用 mod_timer 來完成。由於用戶通常提供一個未來的過期時間,他們通常在這裏添加 jiffies 來從當前時間偏移。用戶也可以通過調用 del_timer 來刪除一個計時器(如果它還沒有過期):

int mod_timer( struct timer_list *timer, unsigned long expires );
void del_timer( struct timer_list *timer );

最後,用戶可以通過調用 timer_pending(如果正在等待,將返回 1)來發現計時器是否正在等待(還沒有發出):

int timer_pending( const struct timer_list *timer );

計時器示例

我們來檢查一下這些 API 函數的實際運行情況。清單 1 提供了一個簡單的內核模塊,用於展示簡單計時器 API 的核心特點。在 init_module 中,您使用 setup_timer 初始化了一個計時器,然後調用 mod_timer 來啓動它。當計時器過期時,將調用回調函數 my_timer_callback。最後,當您刪除模塊時,計時器刪除(通過 del_timer)發生。(注意來自 del_timer 的返回檢查,它確定計時器是否還在使用。)

清單 1. 探索簡單計時器 API
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>

MODULE_LICENSE("GPL");

static struct timer_list my_timer;

void my_timer_callback( unsigned long data )
{
  printk( "my_timer_callback called (%ld).\n", jiffies );
}

int init_module( void )
{
  int ret;

  printk("Timer module installing\n");

  // my_timer.function, my_timer.data
  setup_timer( &my_timer, my_timer_callback, 0 );

  printk( "Starting timer to fire in 200ms (%ld)\n", jiffies );
  ret = mod_timer( &my_timer, jiffies + msecs_to_jiffies(200) );
  if (ret) printk("Error in mod_timer\n");

  return 0;
}

void cleanup_module( void )
{
  int ret;

  ret = del_timer( &my_timer );
  if (ret) printk("The timer is still in use...\n");

  printk("Timer module uninstalling\n");

  return;
}

您可以在 ./include/linux/timer.h 中進一步瞭解計時器 API。儘管簡單計時器 API 簡單有效,但它並不能提供實時應用程序所需的準確性。爲此,我們來看一下 Linux 最近新增的功能,該功能用於支持精確度更高的計時器。

高精確度計時器

高精確度計時器(簡稱 hrtimers)提供一個高精確度的計時器管理框架,這個框架獨立於此前討論過的計時器框架,原因是合併這兩個框架太複雜。儘管計時器在 jiffies 粒度上運行,hrtimers 在納秒粒度上運行。

hrtimer 框架的實現方式與傳統計時器 API 不同。hrtimer 不使用 buckets 和串聯操作,而是維護一個按時間排序的計時器數據結構(按時間順序插入計時器,以最小化激活時的處理)。這個數據結構是一個 “紅-黑” 樹,對於注重性能的應用程序很理想(且恰好作爲內核中的一個庫普遍可用)。

hrtimer 框架作爲內核中的一個 API 可用,用戶空間應用程序也可以通過 nanosleepitimers 和 Portable Operating System Interface (POSIX)-timers interface 使用它。hrtimer 框架被主線化(mainlined)到 2.6.21 內核中。

高精確度計時器 API

hrtimer API 與傳統 API 有些相似,但它們之間的一些根本差別是它能夠進行額外的時間控制。應該注意的第一點是:時間不是用 jiffies 表示的,而是以一種名爲 ktime 的特殊數據類型表示。這種表示方法隱藏了在這個粒度上有效管理時間的一些細節。hrtimer API 正式確認(formalize)了絕對時間和相對時間之間的區別,要求調用者指定類型。

與傳統的計時器 API 類似,高精確度計時器通過一個結構表示 — 這裏是 hrtimer。這個結構從用戶角度定義定時器(回調函數、過期時間等)幷包含了管理信息(其中計時器存在於 “紅-黑” 樹、可選統計數據等中)。

定義過程首先通過 hrtimer_init 初始化一個計時器。這個調用包含計時器、時鐘定義和計時器模式(one-shot 或 restart)。使用的時鐘在 ./include/linux/time.h 中定義,表示系統支持的各種時鐘(比如實時時鐘或者單一時鐘,後者只表示從一個起點[比如系統啓動]開始的時間)。計時器被初始化之後,就可以通過 hrtimer_start 啓動。這個調用包含過期時間(在 ktime_t 中)和時間值的模式(絕對或相對值)。

void hrtimer_init( struct hrtimer *time, clockid_t which_clock, 
			enum hrtimer_mode mode );
int hrtimer_start(struct hrtimer *timer, ktime_t time, const 
			enum hrtimer_mode mode);

hrtimer 啓動後,可以通過調用 hrtimer_cancelhrtimer_try_to_cancel 來取消。每個函數都包含將被停止的計時器的 hrtimer 引用。這兩個函數的區別在於:hrtimer_cancel 函數試圖取消計時器,但如果計時器已經發出,那麼它將等待回調函數結束;hrtimer_try_to_cancel 函數也試圖取消計時器,但如果計時器已經發出,它將返回失敗。

int hrtimer_cancel(struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer);

可以通過調用 hrtimer_callback_running 來檢查 hrtimer 是否已經激活它的回調函數。注意,這個函數由 hrtimer_try_to_cancel 內部調用,以便在計時器的回調函數被調用時返回一個錯誤。

int hrtimer_callback_running(struct hrtimer *timer);

ktime API

本文沒有討論 ktime API,它提供一組豐富的函數來以較高的精確度管理時間。可以在 ./linux/include/ktime.h 中查看 ktime API。

一個 hrtimer 示例

hrtimer API 的使用方法非常簡單,如 清單 2 所示。在 init_module 中,首先定義針對超時的相對時間(本例中爲 200ms)。然後,通過調用 hrtimer_init 來初始化您的 hrtimer(使用單一時鐘),並設置回調函數。最後,使用此前創建的 ktime 值啓動計時器。當計時器發出時,將調用 my_hrtimer_callback 函數,該函數返回 HRTIMER_NORESTART,以避免計時器自動重新啓動。在 cleanup_module 函數中,通過調用 hrtimer_cancel 來取消計時器。

清單 2. 探索 hrtimer API
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>

MODULE_LICENSE("GPL");

#define MS_TO_NS(x)	(x * 1E6L)

static struct hrtimer hr_timer;

enum hrtimer_restart my_hrtimer_callback( struct hrtimer *timer )
{
  printk( "my_hrtimer_callback called (%ld).\n", jiffies );

  return HRTIMER_NORESTART;
}

int init_module( void )
{
  ktime_t ktime;
  unsigned long delay_in_ms = 200L;

  printk("HR Timer module installing\n");

  ktime = ktime_set( 0, MS_TO_NS(delay_in_ms) );

  hrtimer_init( &hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL );
  
  hr_timer.function = &my_hrtimer_callback;

  printk( "Starting timer to fire in %ldms (%ld)\n", delay_in_ms, jiffies );

  hrtimer_start( &hr_timer, ktime, HRTIMER_MODE_REL );

  return 0;
}

void cleanup_module( void )
{
  int ret;

  ret = hrtimer_cancel( &hr_timer );
  if (ret) printk("The timer was still in use...\n");

  printk("HR Timer module uninstalling\n");

  return;
}

關於 hrtimer API,還有許多內容這裏沒有涉及到。一個有趣的方面是它能夠定義回調函數的執行上下文(比如在 softirq 或 hardiirq 上下文中)。您可以在 ./include/linux/hrtimer.h 文件中進一步瞭解 hrtimer API。

內核列表

如本文此前所述,列表是有用的結構,內核提供了一個有效的通用使用實現。另外,您將在我們此前討論過的 APIs 下面發現列表。理解這個雙重鏈接的列表 API 有助於使用這個有效的數據結構進行開發,您會發現,代碼在這個利用列表的內核中是多餘的。現在我們來快速瞭解一下這個內核列表 API。

這個 API 提供一個 list_head 結構,用於表示列表頭(錨點)和結構內(in-structure)列表指針。我們來檢查一個包含列表功能的樣例結構(參見 清單 3)。注意,清單 3 添加了 list_head 結構,該結構用於對象鏈接(object linkage)。注意,可以在您的結構中的任意位置添加這個 list_head 結構 — 通過一些 GCC(list_entrycontainer_of,在 ./include/kernel/kernel.h 中定義)— 可以取消從列表指針到超對象的引用。

清單 3. 帶有列表引用的樣例結構
struct my_data_structure {
	int value;
	struct list_head list;
};

與其他列表實現一樣,需要一個列表頭來充當列表的錨點。這通常通過 LIST_HEAD 宏來完成,這個宏提供列表的聲明和初始化。這個宏創建一個結構 list_head 對象,可以在該對象上添加其他一些對象。

LIST_HEAD( new_list )

也可以通過使用 LIST_HEAD_INIT 宏手動創建一個列表頭(例如,您的列表頭位於另一個結構中)。

主初始化完成後,可以使用 list_addlist_del 等函數來操縱列表。下面,我們將跳到示例代碼,以便更好地解釋這個 API 的使用方法。

列表 API 示例

清單 4 提供一個簡單的內核模塊來探索幾個列表 AIO 函數(./include/linux/list.h 中包含更多函數)。這個示例創建了兩個列表,使用 init_module 函數來填充它們,然後使用 cleanup_module 函數來操縱這兩個列表。

一開始,您創建了您的數據結構(my_data_struct),該結構包含一些數據和兩個列表頭。這個示例展示可以將一個對象同時插入到多個列表中。然後,您創建了兩個列表頭(my_full_listmy_odd_list)。

init_module 函數中,您創建了 10 個數據對象,並使用 list_add 函數將它們加載到列表中(所有對象加載到 my_full_list 中,所有奇值對象加載到 my_odd_list) 中)。這裏要注意一點:list_add 接受兩個參數,一個是將用到的對象中的列表引用,另一個是列表錨點。這個示例展示了將一個數據對象插入多個列表的能力,方法是使用內核的內部機制來識別包含列表引用的超級對象。

cleanup_module 函數提供了這個 API 的其他幾個功能,其中之一是 list_for_each 宏,這個宏簡化了列表迭代。對於這個宏,您提供了一個對當前對象(pos)的引用以及將被迭代的列表引用。對於每次迭代,您接收一個 list_head 引用,list_entry 接收這個引用以識別容器對象(您的數據結構)。指定您的結構和結構之內的列表變量,後者用於在內部取消引用,返回容器。

爲發出奇值列表(odd list),要使用另一個名爲 list_for_each_entry 的迭代宏。這個宏更簡單,因爲它自動提供數據結構,無需再執行一個 list_entry 函數。

最後,使用 list_for_each_safe 來迭代列表,以便釋放已分配的元素。這個宏將迭代列表,但阻止刪除列表條目(刪除列表條目是迭代操作的一部分)。您使用 list_entry 來獲取您的數據對象(以便將它釋放回內核池),然後使用 list_del 來釋放列表中的條目。

清單 4. 探索列表 API
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/list.h>

MODULE_LICENSE("GPL");

struct my_data_struct {
  int value;
  struct list_head full_list;
  struct list_head odd_list;
};

LIST_HEAD( my_full_list );
LIST_HEAD( my_odd_list );


int init_module( void )
{
  int count;
  struct my_data_struct *obj;

  for (count = 1 ; count < 11 ; count++) {

    obj = (struct my_data_struct *)
            kmalloc( sizeof(struct my_data_struct), GFP_KERNEL );

    obj->value = count;

    list_add( &obj->full_list, &my_full_list );

    if (obj->value & 0x1) {
      list_add( &obj->odd_list, &my_odd_list );
    }

  }

  return 0;
}


void cleanup_module( void )
{
  struct list_head *pos, *q;
  struct my_data_struct *my_obj;

  printk("Emit full list\n");
  list_for_each( pos, &my_full_list ) {
    my_obj = list_entry( pos, struct my_data_struct, full_list );
    printk( "%d\n", my_obj->value );
  }

  printk("Emit odd list\n");
  list_for_each_entry( my_obj, &my_odd_list, odd_list ) {
    printk( "%d\n", my_obj->value );
  }

  printk("Cleaning up\n");
  list_for_each_safe( pos, q, &my_full_list ) {
    struct my_data_struct *tmp;
    tmp = list_entry( pos, struct my_data_struct, full_list );
    list_del( pos );
    kfree( tmp );
  }

  return;
}

還有很多其他函數,它們的用途包括在列表末尾而不是頭部添加數據(list_add_tail)、連接列表(list_splice)、測試列表的內容(list_empty)等。請參見 參考資料 詳細瞭解內核列表函數。

結束語

本文探索了幾個 API,展示了在必要時隔離功能的能力(計時器 API 和高精確度 hrtimer API)以及編寫通用代碼以實現代碼重用的能力(列表 API)。傳統計時器爲典型的驅動程序超時提供了一種有效的機制,而 hrtimer 爲更精確的計時器功能提供了更高水平的服務質量。列表 API 提供了一個非常通用,但功能豐富的高效接口。當您編寫內核代碼時,您將遇到一個或多個這樣的 API,因此它們肯定值得深入研究。



http://www.ibm.com/developerworks/cn/linux/l-timers-list/

發佈了25 篇原創文章 · 獲贊 9 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章