2015年12月22日 星期二

linux workqueue

參考自: http://nano-chicken.blogspot.tw/2010/12/linux-modules73-work-queue.html

Work queue提供一個interface,讓使用者輕易的建立kernel thread並且將work綁在這個kernel thread上面,如下圖[1]所示。

由於work queue是建立一個kernel thread來執行,所以是在process context,不同於tasklet的interrupt context,因此,work queue可以sleep(設定semaphore或者執行block I/O等等)。

Creating Work
透過 DECLARE_WORK(name, void (work_func_t)(struct work_struct *work)); // statically
或者
INIT_WORK(struct work_struct*, void (work_func_t)(struct work_struct *work)); //dynamically
建立work,就是要執行的工作。 
有了work還需要將它和work thread結合,您可以透過create_singlethread_workqueue("name")建立一個名為name的single thread(執行work的thread就稱為work thread),或者create_workqueue("name")建立per cpu的thread。接著就是要將work和work thread做關聯了,透過queue_work(work_thread, work)就可以將work送給work thread執行了。

queue_delayed_work(work_thread, delayed_work, delay)為queue_work()的delay版本。 
flush_workqueue(work_thread)會wait直到這個work_thread的work都做完。flush_workqueue()並不會取消任何被delay執行的work,如果要取消delayed的work則需要呼叫cancel_delayed_work(delayed_work)將delayed_work自某個work thread中移除。

最後,要將work_thread摧毀要呼叫destroy_workqueue(work_thread)。


event/n
除了自己建立work thread以外,kernel還建立一個公用的work thread稱為event

kernel/workqueue.c
void __init init_workqueues(void)
{
    …
    keventd_wq = create_workqueue("events");
    …
}

您可以透過schedule_work(&work)將,work送給"events"執行,flush_scheduled_work(void)等待"events"中所有的work執行完畢。


#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/workqueue.h>
#include <linux/sched.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL");

static void brook_1_routine(struct work_struct *);
static void brook_2_routine(struct work_struct *);
static void brook_3_routine(struct work_struct *);

static struct work_struct *brook_1_work; // for event
static DECLARE_WORK(brook_2_work, brook_2_routine);
static DECLARE_DELAYED_WORK(brook_3_work, brook_3_routine);
static struct workqueue_struct *brook_workqueue;
static int stop_wq;
module_param(stop_wq, int, S_IRUGO | S_IWUGO);

static int __init init_modules(void)
{
    // for event
    brook_1_work = kzalloc(sizeof(typeof(*brook_1_work)), GFP_KERNEL);
    INIT_WORK(brook_1_work, brook_1_routine);
    schedule_work(brook_1_work);

    // for brook_wq
    brook_workqueue = create_workqueue("brook_wq");
    queue_work(brook_workqueue, &brook_2_work);
    queue_delayed_work(brook_workqueue, &brook_3_work, 0);
    stop_wq = 0;
    return 0;
}

static void __exit exit_modules(void)
{
    cancel_delayed_work(&brook_3_work);
    flush_workqueue(brook_workqueue);
    stop_wq = 1;
    destroy_workqueue(brook_workqueue);
}

static void brook_1_routine(struct work_struct *ws)
{
    printk("%s(): on cpu:%d, pname:%s\n",
            __func__, smp_processor_id(), current->comm);
}

static void brook_2_routine(struct work_struct *ws)
{
    printk("%s(): on cpu:%d, pname:%s\n",
            __func__, smp_processor_id(), current->comm);
    // do something to block/sleep
    // the work in the same workqueue is also deferred.
    msleep(5000);
    if (!stop_wq) {
        queue_work(brook_workqueue, &brook_2_work);
    }
}

static void brook_3_routine(struct work_struct *ws)
{
    printk("%s(): on cpu:%d, pname:%s\n",
            __func__, smp_processor_id(), current->comm);
    queue_delayed_work(brook_workqueue, &brook_3_work, 50);
}

module_init(init_modules);
module_exit(exit_modules);


Kernel Version:2.6.35
參考資料:
  1. http://www.embexperts.com/viewthread.php?tid=12&highlight=work%2Bqueue
  2. Linux Kernel Development 2nd, Novell Press

Linux delay function


延遲執行
  設備驅動常常需要延後一段時間執行一個特定片段的代碼, 常常允許硬件完成某個任務.
  長延遲
  有時,驅動需要延後執行相對長時間,長於一個時鐘嘀噠。
  忙等待(盡量別用)
  若想延遲執行若干個時鐘嘀噠,精度要求不高。最容易的( 儘管不推薦) 實現是一個監視jiffy 計數器的循環。這種忙等待實現的代碼如下:
  while (time_before(jiffies, j1))
  cpu_relax();
  對cpu_relex 的調用將以體系相關的方式執行,在許多系統中它根本不做任何事,這個方法應當明確地避免。對於ARM體係來說:
  #define cpu_relax() barrier()
  也就是說在ARM上運行忙等待相當於:
  while (time_before(jiffies, j1)) ;
  這種忙等待嚴重地降低了系統性能。如果未配置內核為搶占式, 這個循環在延時期間完全鎖住了處理器,計算機直到時間j1 到時會完全死掉。如果運行一個可搶占的內核時會改善一點,但是忙等待在可搶占系統中仍然是浪費資源的。更糟的是, 當進入循環時如果中斷碰巧被禁止, jiffies 將不會被更新, 並且while 條件永遠保持真,運行一個搶占的內核也不會有幫助, 唯一的解決方法是重啟。
  讓出處理器
  忙等待加重了系統負載,必須找出一個更好的技術:不需要CPU時釋放CPU 。這可通過調用schedule函數實現(在中聲明):
  while (time_before(jiffies, j1)) {
  schedule();
  }
  在計算機空閒時運行空閒任務(進程號0, 由於歷史原因也稱為swapper)可減輕處理器工作負載、降低溫度、增加壽命。
  超時
  實現延遲的最好方法應該是讓內核為我們完成相應的工作。
  (1)若驅動使用一個等待隊列來等待某些其他事件,並想確保它在一個特定時間段內運行,可使用:
  #include
  long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
  long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);
  /*這些函數在給定隊列上睡眠, 但是它們在超時(以jiffies 表示)到後返回。如果超時,函數返回0; 如果這個進程被其他事件喚醒,則返回以jiffies 表示的剩餘的延遲實現;返回值從不會是負值*/
  (2)為了實現進程在超時到期時被喚醒而又不等待特定事件(避免聲明和使用一個多餘的等待隊列頭),內核提供了schedule_timeout 函數:
  #include
  signed long schedule_timeout(signed long timeout);
  /*timeout 是要延時的jiffies 數。除非這個函數在給定的timeout 流失前返回,否則返回值是0 。schedule_timeout 要求調用者首先設置當前的進程狀態。為獲得一個不可中斷的延遲, 可使用TASK_UNINTERRUPTIBLE 代替。如果你忘記改變當前進程的狀態, 調用schedule_time 如同調用shcedule,建立一個不用的定時器。一個典型調用如下:*/
  set_current_state(TASK_INTERRUPTIBLE);
  schedule_timeout (delay);
  短延遲
  當一個設備驅動需要處理硬件的延遲(latency潛伏期), 涉及到的延時通常最多幾個毫秒,在這個情況下, 不應依靠時鐘嘀噠,而是內核函數ndelay, udelay和mdelay ,他們分別延後執行指定的納秒數, 微秒數或者毫秒數,定義在,原型如下:
  #include
  void ndelay(unsigned long nsecs);
  void udelay(unsigned long usecs);
  void mdelay(unsigned long msecs);
  重要的是記住這3 個延時函數是忙等待; 其他任務在時間流失時不能運行。每個體係都實現udelay, 但是其他的函數可能未定義; 如果它們沒有定義, 提供一個缺省的基於udelay 的版本。在所有的情況中, 獲得的延時至少是要求的值, 但可能更多。udelay 的實現使用一個軟件循環, 它基於在啟動時計算的處理器速度和使用整數變量loos_per_jiffy確定循環次數。
  為避免在循環計算中整數溢出, 傳遞給udelay 和ndelay的值有一個上限,如果你的模塊無法加載和顯示一個未解決的符號:__bad_udelay, 這意味著你調用udleay時使用太大的參數。
  作為一個通用的規則:若試圖延時幾千納秒, 應使用udelay 而不是ndelay; 類似地, 毫秒規模的延時應當使用mdelay 完成而不是一個更細粒度的函數。
  有另一個方法獲得毫秒(和更長)延時而不用涉及到忙等待的方法是使用以下函數(在中聲明):
  void msleep(unsigned int millisecs);
  unsigned long msleep_interruptible(unsigned int millisecs);
  void ssleep(unsigned int seconds)
  若能夠容忍比請求的更長的延時,應使用schedule_timeout, msleep 或ssleep。
  內核定時器
  當需要調度一個以後發生的動作, 而在到達該時間點時不阻塞當前進程, 則可使用內核定時器。內核定時器用來調度一個函數在將來一個特定的時間(基於時鐘嘀噠)執行,從而可完成各類任務。
  內核定時器是一個數據結構, 它告訴內核在一個用戶定義的時間點使用用戶定義的參數執行一個用戶定義的函數,函數位於和kernel/timer.c 。被調度運行的函數幾乎確定不會在註冊它們的進程在運行時運行,而是異步運行。實際上, 內核定時器通常被作為一個”軟件中斷”的結果而實現。當在進程上下文之外(即在中斷上下文)中運行程序時, 必須遵守下列規則:
  (1)不允許訪問用戶空間;
  (2)current 指針在原子態沒有意義;
  (3)不能進行睡眠或者調度. 例如:調用kmalloc(…, GFP_KERNEL) 是非法的,信號量也不能使用因為它們可能睡眠。
  通過調用函數in_interrupt()能夠告知是否它在中斷上下文中運行,無需參數並如果處理器當前在中斷上下文運行就返回非零。
  通過調用函數in_atomic()能夠告知調度是否被禁止,若調度被禁止返回非零; 調度被禁止包含硬件和軟件中斷上下文以及任何持有自旋鎖的時候。
  在後一種情況, current 可能是有效的,但是訪問用戶空間是被禁止的,因為它能導致調度發生. 當使用in_interrupt()時,都應考慮是否真正該使用的是in_atomic 。他們都在中聲明。
  內核定時器的另一個重要特性是任務可以註冊它本身在後面時間重新運行,因為每個timer_list 結構都會在運行前從激活的定時器鍊錶中去連接,因此能夠立即鏈入其他的鍊錶。一個重新註冊它自己的定時器一直運行在同一個CPU.
  即便在一個單處理器系統,定時器是一個潛在的態源,這是異步運行直接結果。因此任何被定時器函數訪問的數據結構應當通過原子類型或自旋鎖被保護,避免並發訪問。
  定時器API
  內核提供給驅動許多函數來聲明、註冊以及刪除內核定時器:
  #include
  struct timer_list {
  struct list_head entry;
  unsigned long expires;/*期望定時器運行的絕對jiffies 值,不是一個jiffies_64 值,因為定時器不被期望在將來很久到時*/
  void (*function)(unsigned long); /*期望調用的函數*/
  unsigned long data;/*傳遞給函數的參數,若需要在參數中傳遞多個數據項,可以將它們捆綁成單個數據結構並且將它的指針強制轉換為unsiged long 的指針傳入。這種做法在所有支持的體系上都是安全的並且在內存管理中相當普遍*/
  struct tvec_t_base_s *base;
  #ifdef CONFIG_TIMER_STATS
  void *start_site;
  char start_comm[16];
  int start_pid;
  #endif
  };
  /*這個結構必須在使用前初始化,以保證所有的成員被正確建立(包括那些對調用者不透明的初始化):*/
  void init_timer(struct timer_list *timer);
  struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
  /*在初始化後和調用add_timer 前,可以改變3 個公共成員:expires、function和data*/
  void add_timer(struct timer_list * timer);
  int del_timer(struct timer_list * timer);/*在到時前禁止一個已註冊的定時器*/
  int del_timer_sync(struct timer_list *timer); /* 如同del_timer ,但還保證當它返回時, 定時器函數不在任何CPU 上運行,以避免在SMP 系統上競態, 並且在單處理器內核中和del_timer 相同。這個函數應當在大部分情況下優先考慮。如果它被從非原子上下文調用, 這個函數可能睡眠,但是在其他情況下會忙等待。當持有鎖時要小心調用del_timer_sync ,如果這個定時器函數試圖獲得同一個鎖, 系統會死鎖。如果定時器函數重新註冊自己, 調用者必須首先確保這個重新註冊不會發生; 這通常通過設置一個” 關閉“標誌來實現, 這個標誌被定時器函數檢查*/
  int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一個定時器的超時時間, 常用於超時定時器。也可在正常使用add_timer時在不活動的定時器上調用mod_timer*/
  int timer_pending(const struct timer_list * timer); /*通過調用timer_list結構中一個不可見的成員,返回定時器是否在被調度運行*/
  內核定時器的實現《LDD3》介紹的比較籠統,以後看《ULK3》的時候再細細研究。
  一個內核定時器還遠未完善,因為它受到jitter 、硬件中斷,還有其他定時器和其他異步任務的影響。雖然一個簡單數字I/O關聯的定時器對簡單任務是足夠的,但不合適在工業環境中的生產系統,對於這樣的任務,你將最可能需要實時內核擴展(RT-Linux).

2015年12月3日 星期四