kthread_worker和kthread_work機制

1、概述

在閱讀內核源碼時,可以看到kthread_worker、kthread_work兩個數據結構配合內核執行緒創建函數一起使用的場景。剛開始看到這塊時,比較困惑,緊接著仔細分析源碼後,終於弄清楚了其中的機制,也不由的感嘆內核的設計者內功之深厚以及生活處處皆學問。其實,這塊使用機制就是抽象了現實生活中的經常看到的現象——工人(worker)和工作(work)間的關係。我們知道,在上班期間每個工人會被不定時的分配到一些不同的工作,當有工作來了,工人會對自己接到的工作進行處理,當手頭上的工作做完後可以進行稍微的休息,等待後面任務的到來時再繼續的處理接到的工作。

內核中要實現這種機制其實也不難,內核執行緒創建函數創建一個內核執行緒,該執行緒模仿工人的這個特性,它去判斷屬於這個執行緒的kthread_worker中是否有要處理的kthread_work,如果有,就取出這個kthread_work,然後調用kthread_work上面指定的處理函數,如果沒有這個執行緒就進行休眠,當有新的kthread_work添加到kthread_worker上時,會再次喚醒kthread_worker的處理執行緒重複上述工作。

2、內核使用場景

前面總的概況了kthread_worker和kthread_work這一機制,讓大家有了個大致的了解,下面結合內核中對這一機制的實際使用場景的源碼再進行詳細的分析。

內核在SPI驅動的SPI主機控制器這塊使用了這一機制,先來看下源碼中對kthread_worker、kthread_work這一機制進行初始化的函數

static int spi_init_queue(struct spi_controller *ctlr)
{
    ...

    kthread_init_worker(&ctlr->kworker);
    ctlr->kworker_task = kthread_run(kthread_worker_fn, &ctlr->kworker,
                     "%s", dev_name(&ctlr->dev));
    ...
    kthread_init_work(&ctlr->pump_messages, spi_pump_messages);

    ...
}

spi_init_queue函數中調用了kthread_init_worker、kthread_run和kthread_init_work函數,下面按照順序依次對這三個函數進行分析

2.1 kthread_init_worker函數

#define kthread_init_worker(worker)                    \
    do {                                \
        static struct lock_class_key __key;            \
        __kthread_init_worker((worker), "("#worker")->lock", &__key); \
    } while (0)
void __kthread_init_worker(struct kthread_worker *worker,
                const char *name,
                struct lock_class_key *key)
{
    memset(worker, 0, sizeof(struct kthread_worker));
    raw_spin_lock_init(&worker->lock);
    lockdep_set_class_and_name(&worker->lock, key, name);
    INIT_LIST_HEAD(&worker->work_list);
    INIT_LIST_HEAD(&worker->delayed_work_list);
}

kthread_init_worker是一個宏,它內部接著調用了__kthread_init_worker,主要的工作是在__kthread_init_worker函數中完成的

__kthread_init_worker函數初始化傳入的kthread_worker結構體的成員變數,如初始化了該結構體內部的鏈表和自旋鎖

2.2 kthread_run函數

#define kthread_run(threadfn, data, namefmt, ...)               \
({                                       \
    struct task_struct *__k                           \
        = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                           \
        wake_up_process(__k);                       \
    __k;                                   \
})

kthread_run也是一個宏,它內部調用kthread_create創建一個內核執行緒。當內核執行緒創建成功後,立刻調用wake_up_process去喚醒創建的這個執行緒,讓創建的執行緒進入就緒態等待內核調度

thread_fn這個參數傳入的是這個內核執行緒要執行的函數,data是傳給內核執行緒函數的參數。在上面spi_init_queue函數中,thread_fn參數對應的是kthread_worker_fn,data對應的是spi_controller結構中的kthread_worker類型的對象

這裡的傳入的kthread_worker_fn是由內核實現的函數,我們來看下它的作用是否和我們前面想像的一樣,取出kthread_worker中掛接的kthread_work,並調用每個kthread_work中指定的處理函數

int kthread_worker_fn(void *worker_ptr)
{
    struct kthread_worker *worker = worker_ptr;
    struct kthread_work *work;
    ...
    
repeat:
    ...
    work = NULL;
    raw_spin_lock_irq(&worker->lock);
    if (!list_empty(&worker->work_list)) {------------------------------------>①
        work = list_first_entry(&worker->work_list,
                    struct kthread_work, node);
        list_del_init(&work->node);------------------------------------------->②
    }
    worker->current_work = work;
    raw_spin_unlock_irq(&worker->lock);

    if (work) {---------------------------------------->③
        __set_current_state(TASK_RUNNING);
        work->func(work);
    } else if (!freezing(current))
        schedule();
    ...
    goto repeat; ------------------------------------>④
}

① 判斷kthread_worker上是否有要處理的kthread_work,kthread_work掛接kthread_worker的work_list鏈表上

② 從kthread_worker的work_list鏈表中刪除①中取出kthread_work節點

③ 如果在kthread_worker的work_list鏈表中找到了kthread_work,就執行kthread_work上的處理函數,如果沒有,就讓本執行緒進入休眠狀態

④ 跳轉到repeat處,重複執行①~④之間操作

果然kthread_worker_fn函數和我們之前預想的一樣,從kthread_worker中取出掛接的kthread_work,並調用每個kthread_work中指定的處理函數

2.3 kthread_init_work函數

#define kthread_init_work(work, fn)                    \
    do {                                \
        memset((work), 0, sizeof(struct kthread_work));        \
        INIT_LIST_HEAD(&(work)->node);                \
        (work)->func = (fn);                    \
    } while (0)

kthread_init_work和kthread_init_worker一樣,也是一個宏,它有兩個參數,第一個參數work傳入的是要初始化的kthread_work結構,第二個參數fn傳入的是給kthread_work指定的處理函數。kthread_init_work初始化tkhread_work,設置了tkread_work的處理函數。

前面雖然初始化了kthread_worker、kthread_work結構,創建了處理kthread_worker的內核執行緒,但似乎還不夠完整,還沒有向kthread_worker中添加kthread_work呢?

創建的內核執行緒只有kthread_worker中掛接有kthread_work後,內核執行緒被喚醒工作,那麼我們如何將kthread_work掛接到kthread_worker中並且喚醒休眠的內核執行緒呢?同樣,內核中也提供了相應的函數,我們來看SPI的驅動中是怎麼用的。

__spi_queued_transfer:

static int __spi_queued_transfer(struct spi_device *spi,
                 struct spi_message *msg,
                 bool need_pump)
{
    struct spi_controller *ctlr = spi->controller;
    ...
    list_add_tail(&msg->queue, &ctlr->queue);
    if (!ctlr->busy && need_pump)
        kthread_queue_work(&ctlr->kworker, &ctlr->pump_messages);----------->①
    ...
}

當SPI設備驅動程式訪問SPI設備時最終會調用到__spi_queued_transfer函數,該函數將構造的消息掛入SPI主機控制器的鏈表上,然後調用kthread_queue_work將kthread_work掛入到kthread_woke鏈表中。

①中的ctlr->kworker和ctlr->pump_messages是描述SPI主機控制器的結構體里的成員,其實對應的就是kthread_worker和kthread_work類型數據結構。

可以看出kthread_queue_work就是我們想要找將wok(工作)交給worker(工人)的函數,下面我們來進行詳細分析

2.2 kthread_queue_work函數

bool kthread_queue_work(struct kthread_worker *worker,
            struct kthread_work *work)
{
    ...
    if (!queuing_blocked(worker, work)) {
        kthread_insert_work(worker, work, &worker->work_list);
        ret = true;
    }
    ...
}

kthread_queue_work函數中繼續調用了kthread_insert_work函數,具體的工作是在這個函數中完成了,我們直接來看kthread_insert_work這個函數

static void kthread_insert_work(struct kthread_worker *worker,
                struct kthread_work *work,
                struct list_head *pos)
{
    kthread_insert_work_sanity_check(worker, work);

    list_add_tail(&work->node, pos);-------------------->①
    work->worker = worker;
    if (!worker->current_work && likely(worker->task))
        wake_up_process(worker->task);---------------->②
}

① 將傳入的kthread_work掛接到對應的kthread_worker鏈表中

② 調用wake_up_process函數,喚醒休眠的處理kthread_worker的內核執行緒

可以看出,當有work要處理時,直接調用kthread_queue_work將work交給所屬的worker,worker的處理執行緒會被喚醒去處理它裡面的work

3、小結

好了,到此我們也將內核中kthread_worker和kthread_work機制講解清楚了。這裡引用了內核中SPI驅動對該機制的使用,如果想更深入的了解可以去閱讀driver/spi目錄下的內容。SPI驅動中巧妙的運用了這一機制,在後面未講解的kthread_work的處理函數spi_pump_messages中,它會不斷的去取出SPI設備驅動程式掛接到SPI主機控制器上的消息,將這些消息內容的通過硬體一個個的發送出去。

我們在內核編程中也可以通過內核提供的介面函數去使用這種機制,步驟如下:

  1. 使用kthread_init_worker初始化一個kthread_worker結構
  2. 調用kthread_run創建一個處理kthread_worker的內核執行緒,執行緒的執行函數一定是kthread_worker_fn
  3. 使用kthread_work_init初始化一個kthread_work,並指定它的處理函數
  4. 當有kthread_work要處理時,調用kthread_queue_work將kthread_work掛接到kthread_worker上