kthread_worker和kthread_work機制

quinoa發表於2021-03-30

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上

相關文章