Linux核心線程之父pid=2的kthreadd線程

來源:互聯網
上載者:User

轉自http://embexperts.com/viewthread.php?tid=30


因為所涉及的話題在代碼的實現上是體系架構相關的,所以本貼基於ARM架構。
這裡所謂的核心線程,實際上是由kernel_thread函數建立的一個進程,有自己獨立的task_struct結構並可被調度器調度,這種進程的特殊之處在於它只在核心態運行。
在Linux source code中, init/main.c中的rest_init()中就開始調用kernel_thread來構造核心線程了,比如:
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

我們在原始碼中通過跟蹤kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)的調用來揭示Linux中的這種特殊的核心態進程的背後秘密。
在ARM中,kernel_thread定義如下:
/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
        struct pt_regs regs;

        memset(&regs, 0, sizeof(regs));

        regs.ARM_r1 = (unsigned long)arg;
        regs.ARM_r2 = (unsigned long)fn;
        regs.ARM_r3 = (unsigned long)do_exit;
        regs.ARM_pc = (unsigned long)kernel_thread_helper;
        regs.ARM_cpsr = SVC_MODE;

        return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
}
[註:這裡有個調試方面的小技巧。當初海豚用BDI3000調試BALI板的時候,為了調試產生的核心線程代碼,需要將上述do_fork中的CLONE_UNTRACED flag移除,重新編譯核心,調試器才可停在核心線程函數代碼所設的斷點上]

kernel_thread函數的最後一行是調用do_fork來產生一個進程架構(主體結構是task_struct),在do_fork中會將新產生的進程執行進入點設定為ret_from_fork()。這樣,當新進程被調度器調度時,將從ret_from_fork()函數開始執行。在ret_from_fork中,會調用kernel_thread函數中設定的ARM_pc,也就是說調用kernel_thread_helper.

kernel_thread_helper
/*
* Shuffle the argument into the correct register before calling the
* thread function.  r1 is the thread argument, r2 is the pointer to
* the thread function, and r3 points to the exit function.
*/
extern void kernel_thread_helper(void);
asm(        ".section .text\n"
"        .align\n"
"        .type        kernel_thread_helper, #function\n"
"kernel_thread_helper:\n"
"        mov        r0, r1\n"
"        mov        lr, r3\n"
"        mov        pc, r2\n"
"        .size        kernel_thread_helper, . - kernel_thread_helper\n"
"        .previous");

這段彙編代碼將r1賦給r0,r0在函數調用時作為傳遞參數寄存器。在1樓的kernel_thread函數中,regs.ARM_r1 = (unsigned long)arg;
r3給了lr,實際上就是儲存核心線程函數返回時的調用地址,在本例中,也就是kthreadd返回後所調用的函數,該函數為do_exit,這意味著當核心線程函數退出後,其所在的進程將會被銷毀。所以,核心線程函數一般都不會輕易退出。

mov pc, r2代碼的執行將會調用kthreadd核心線程函數。

kthreadd

int kthreadd(void *unused)
{
        struct task_struct *tsk = current;

        /* Setup a clean context for our children to inherit. */
        set_task_comm(tsk, "kthreadd");
        ignore_signals(tsk);
        set_user_nice(tsk, KTHREAD_NICE_LEVEL);
        set_cpus_allowed(tsk, CPU_MASK_ALL);

        current->flags |= PF_NOFREEZE;

        for (;;) {
                set_current_state(TASK_INTERRUPTIBLE);
                if (list_empty(&kthread_create_list))
                        schedule();
                __set_current_state(TASK_RUNNING);

                spin_lock(&kthread_create_lock);
                while (!list_empty(&kthread_create_list)) {
                        struct kthread_create_info *create;

                        create = list_entry(kthread_create_list.next,
                                            struct kthread_create_info, list);
                        list_del_init(&create->list);
                        spin_unlock(&kthread_create_lock);

                        create_kthread(create);

                        spin_lock(&kthread_create_lock);
                }
                spin_unlock(&kthread_create_lock);
        }

        return 0;
}

kthreadd的核心是一for和while迴圈體。在for迴圈中,如果發現kthread_create_list是一空鏈表,則調用schedule調度函數,因為此前已經將該進程的狀態設定為TASK_INTERRUPTIBLE,所以schedule的調用將會使當前進程進入睡眠。如果kthread_create_list不為空白,則進入while迴圈,在該迴圈體中會遍曆該kthread_create_list列表,對於該列表上的每一個entry,都會得到對應的類型為struct kthread_create_info的節點的指標create.

然後函數在kthread_create_list中刪除create對應的列表entry,接下來以create指標為參數調用create_kthread(create).
在create_kthread()函數中,會調用kernel_thread來產生一個新的進程,該進程的核心功能為kthread,調用參數為create:
kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);

kthread--

static int kthread(void *_create)
{
        struct kthread_create_info *create = _create;
        int (*threadfn)(void *data);
        void *data;
        int ret = -EINTR;

        /* Copy data: it's on kthread's stack */
        threadfn = create->threadfn;
        data = create->data;

        /* OK, tell user we're spawned, wait for stop or wakeup */
        __set_current_state(TASK_UNINTERRUPTIBLE);
        complete(&create->started);
        schedule();

        if (!kthread_should_stop())
                ret = threadfn(data);

        /* It might have exited on its own, w/o kthread_stop.  Check. */
        if (kthread_should_stop()) {
                kthread_stop_info.err = ret;
                complete(&kthread_stop_info.done);
        }
        return 0;
}
kthread會將其所在進程的狀態設為TASK_UNINTERRUPTIBLE,然後調用schedule函數。所以,kthread將會使其所在的進程進入休眠狀態,直到被別的進程喚醒。如果被喚醒,將會調用create->threadfn(create->data);

其中的kthread_should_stop()如果返回真,表明對於當前進程p,有別的進程調用了kthread_stop(p),否則kthread_should_stop返回假。

Summary--

kthreadd will work on a global list named
kthread_create_list, if the list is empty then the
kthreadd will sleep until someone else wake it up.

Now let's see which one will update the kthread_create_list
meaning insert a node into the list. kthread_create() will insert a node named
create into the list. After it insert the
create into the kthread_create_list, it will call
wake_up_process(kthreadd_task) to wake up the process which kernel thread function is
kthreadd. In this case, kthreadd will create a new process which the initial state is TASK_UNINTERRUPTIBLE, so the new process will enter into sleep until someone wake it up.

The work queue make use of the kthread_create.

Then comes to the last question, who will wake up the process created by the kthreaddd?

The question is, for the work queue, when the driver call __create_workqueue_key, the latter will call start_workqueue_thread to wake up the process created by the kthreadd. worker_thread will sleep on a wait queue until the driver call queue_work to insert
a working node into this wait queue, and the worker_thread will be waked up by the queue_work meanwhile...

worker_thread has been woken up, that doesn't mean the worker_thread will be called immediatelly after queque_work being called, queue_work just change the state of worker_thread process to TASK_RUNNING, this worker_thread function will be called until next
schedule point, because of the higher schedule priority, so the worker_thread will be called quickly upon the coming schedule point.

So-- from the work queue point of view, __create_workqueue_key() can be divided into 2 major parts: The first one will create a new process in the system with the help of kthreadd, which has a kernel thread
function named worker_thread. The new process will enter into sleep state. The second one will call start_workqueue_thread() to wake up the process created in the first part, once woken up, the worker_thread will be executed, but it will enter sleep again
because the wait queue is empty.
When driver call queue_work, it will insert a working node into the wait queue and wake up the worker_thread (put the worker_thread process into TASK_RUNNING state). In the coming schedule point, worker_thread will be called to handle all the nodes in the wait
queue for which it's waiting.

關於worker_thread核心線程的創立過程:
最開始由void __init init_workqueues(void)發起(Workqueue.c),依次的關鍵調用節點分別是(為了便於敘述,在核心代碼基礎上略有改動,但不影響核心調用過程)

create_workqueue("events");
__create_workqueue((("events"), 0, 0);
__create_workqueue_key("events", 0, 0, NULL, NULL);

在__create_workqueue_key("events", 0, 0, NULL, NULL)中:
1).首先建立一個struct workqueue_struct指標型變數*wq, wq->name = "events".這個建立的隊列指標將被記錄在全域變數keventd_wq中.
    workqueue_struct定義如下:
   struct workqueue_struct {
        struct cpu_workqueue_struct *cpu_wq;
        struct list_head list;
        const char *name;
        int singlethread;
        int freezeable;                /* Freeze threads during suspend */
#ifdef CONFIG_LOCKDEP
        struct lockdep_map lockdep_map;
#endif
};

2).把wq所在的隊列節點加入到一名為workqueues的全域變數中(list_add(&wq->list, &workqueues)).
3).調用create_workqueue_thread(),最終調用kthread_create(worker_thread, cwq, fmt, wq->name, cpu)來產生核心線程worker_thread.
Linux核心中最終是通過do_fork來產生核心線程(正如前面所說,這其實是個能被調度的進程,擁有自己的task_struct結構),這個過程在核心中是個比較複雜的過程,比較重要的節點總結如下:在當前進程下調用do_fork來產生一個新進程時,會大量copy當前進程的task_struct結構到新進程的task_struct變數中,新進程如果被調度運行,進入點(pc值)是kernel_thread_helper函數,在該函數中會再次將pc值設定為kernel_thread()中的function指標,也就是在調用kernel_thread函數時第一參數所表示的函數。

所以,在Linux系統初始化期間,會產生一個新進程,該進程的執行線程/函數為worker_thread,該進程被建立出來之後的狀態是STOP,這意味著該線程無法進入調度隊列直到針對該進程調用wake_up_process(),該進程才會真正進入調度隊列,如果被調度,則開始運行worker_thread函數。新進程被賦予的調度優先順序為KTHREAD_NICE_LEVEL(-5),這個標誌的實際含義將在Linux進程調度的文章裡去寫。然後對於worker_thread線程函數本身,會進一步調整調度優先順序(set_user_nice(current,
-5)),這樣worker_thread所在進程的優先值將為-10,在可搶佔式的Linux核心中,如此高的調度優先順序極易導致一個調度時點,即使當前進程也許正運行在核心態,也可能被切換出CPU,代之以worker_thread.

在kernel_thread_helper函數中,在調用核心線程函數之前設定返回地址為do_exit()函數,所以當核心線程函數退出的話將導致該進程的消失。
比如ARM中的kernel_thread_helper函數代碼:
extern void kernel_thread_helper(void);
asm(        ".section .text\n"
"        .align\n"
"        .type        kernel_thread_helper, #function\n"
"kernel_thread_helper:\n"
"        mov        r0, r1\n"
"        mov        lr, r3\n"
"        mov        pc, r2\n"
"        .size        kernel_thread_helper, . - kernel_thread_helper\n"
"        .previous");

mov lr, r3設定核心線程函數返回後的返回地址,Linux核心代碼設定為do_exit().

可以想象,象worker_thread這種核心線程函數,一般不會輕易退出,除非對核心線程函數所在的進程上調用kthread_stop函數。
上述的kernel_thread_helper函數中的mov pc, r2將會導致worker_thread()函數被調用,該函數定義如下:
static int worker_thread(void *__cwq)
{
        struct cpu_workqueue_struct *cwq = __cwq;
        DEFINE_WAIT(wait);

        if (cwq->wq->freezeable)
                set_freezable();

        set_user_nice(current, -5);

        for (;;) {
                prepare_to_wait(&cwq->more_work, &wait, TASK_INTERRUPTIBLE);
                if (!freezing(current) &&
                    !kthread_should_stop() &&
                    list_empty(&cwq->worklist))
                        schedule();
                finish_wait(&cwq->more_work, &wait);

                try_to_freeze();

                if (kthread_should_stop())
                        break;

                run_workqueue(cwq);
        }
}

正如前面猜測的那樣,該函數不會輕易退出,其核心是一for迴圈,如果任務隊列中有新的節點,則執行該節點上的函數(在run_workqueue()內部),否則worker_thread所在的進程將會繼續休眠。

對work queue的深入理解需要瞭解Linux的調度機制,最基本的是Linux核心的調度時機,因為這關係到驅動程式開發人員能對自己註冊到任務隊列函數的執行時機有大體的瞭解。在2.4核心中,除了進程主動調用schedule()這種主動的調度方式外,調度發生在由核心態向使用者態轉變(從中斷和系統調用返回)的時刻,因為核心不可搶佔性,所以核心態到核心態的轉變時調度不會發生。而在2.6核心中,因為核心可搶佔已經被支援,這意味著調度的時機除了發生在核心態向使用者態轉變時,運行在核心態的進程也完全有可能被調度出處理器,比如當前進程重新允許搶佔(調用preempt_enable())。在核心態的進程允許被搶佔,意味著對高優先順序進程的調度粒度更細:如果當前進程允許被搶佔,那麼一旦當前調度隊列中有比當前進程優先順序更高的進程,當前進程將被切換出處理器(最常見的情況是在系統調用的代碼中接收到中斷,當中斷返回時,2.4代碼會繼續運行被中斷的系統調用,而2.6代碼的可搶佔性會導致一個調度時點,原先被中斷的系統調用所在的進程可能會被調度隊列中更高優先順序的進程所取代)。關於進程的調度,會在另外的文章中詳細介紹。
create_singlethread_workqueue(name)與create_workqueue(name)
Driver調用這兩個宏來建立自己的工作隊列以及相應的核心進程(其核心線程函數為worker_thread,下來為了方便敘述,就簡稱該進程為worker_thread進程)
1. create_singlethread_workqueue(name)
該函數的實現機制如所示,函數返回一個類型為struct workqueue_struct的指標變數,該指標變數所指向的記憶體位址在函數內部調用kzalloc動態產生。所以driver在不再使用該work queue的情況下調用void destroy_workqueue(struct workqueue_struct *wq)來釋放此處的記憶體位址。

圖中的cwq是一per-CPU類型的地址空間。對於create_singlethread_workqueue而言,即使是對於多CPU系統,核心也只負責建立一個worker_thread核心進程。該核心進程被建立之後,會先定義一個圖中的wait節點,然後在一迴圈體中檢查cwq中的worklist,如果該隊列為空白,那麼就會把wait節點加入到cwq中的more_work中,然後休眠在該等待隊列中。

Driver調用queue_work(struct workqueue_struct *wq, struct work_struct *work)向wq中加入工作節點。work會依次加在cwq->worklist所指向的鏈表中。queue_work向cwq->worklist中加入一個work節點,同時會調用wake_up來喚醒休眠在cwq->more_work上的worker_thread進程。wake_up會先調用wait節點上的autoremove_wake_function函數,然後將wait節點從cwq->more_work中移走。

worker_thread再次被調度,開始處理cwq->worklist中的所有work節點...當所有work節點處理完畢,worker_thread重新將wait節點加入到cwq->more_work,然後再次休眠在該等待隊列中直到Driver調用queue_work...

create_workqueue

本帖最後由 Dolphin 於 2010-7-16 10:18 編輯

相對於create_singlethread_workqueue, create_workqueue同樣會分配一個wq的工作隊列,但是不同之處在於,對於多CPU系統而言,對每一個CPU,都會為之建立一個per-CPU的cwq結構,對應每一個cwq,都會產生一個新的worker_thread進程。但是當用queue_work向cwq上提交work節點時,是哪個CPU調用該函數,那麼便向該CPU對應的cwq上的worklist上增加work節點。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.