1. 程式人生 > >Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(4) Linux 多核心啟動流程-kthreadd 與相關的

Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(4) Linux 多核心啟動流程-kthreadd 與相關的

by  loda

[email protected]

Loda's Blog

kthread第一次出現在LinuxKernel中是在Kernel版本2.6.4,一開始的實作尚未有本文提到的kthreaddTask的具體架構,隨著版本的演進,除了這部份的設計完整外,需要產生KernelThread的實作也都已經改用kthread機制.

本文會先針對kthreaddTask行為加以說明,並會以在啟動後,屬於KernelMode產生的KernelThread的個別行為與功能做一個介紹,由於這部份涉及的範圍不少,筆者會以自己的角度選擇認為值得加以說明的專案,相信應該足以涵蓋大多數人對於

LinuxKernel Mode Tasks所需的範圍.如果有所不足之處,也請透過LinuxKernel Hacking加以探索.

簡要來說,位於KernelModeTasks產生,除了直接透過kernel_thread函式外,還可以有兩個來源,一個是透過kthread_create(實作上,也是透過函式Kernel_Thread)產生的KernelThread,另一個則是透過WorkQueue機制產生的KernelThread,前者可以依據設計者自己對系統架構的掌握,去設計多工機制(例如,使用稍後提到的LinkedList Queue).而後者,則是由LinuxKernel

提供延遲處理工作的機制,讓每個核心的Tasks可以透過WorkQueue機制把要延遲處理/指定處理器/指定延遲時間的工作,交派給WorkQueue.

在實際的應用上,WorkQueue還可以用以實現中斷的BottomHalf機制,讓中斷觸發時對Timing要求高的部分(TopHalf)可以在InterruptHandler中盡快執行完畢,透過WorkQueue把需要較長時間執行的部分(BottomHalf)交由WorkQueue延遲執行實現.(等同於RTOS下的LISR/HISR).

LinuxKernel的基礎Tasks.

LinuxKernel中有三個最基礎的Tasks,

分別為PID=0IdleTask,PID=1負責初始化所有使用者環境與行程的init Task,PID=2負責產生KernelMode行程的kthreaddTask.

其中,IdleTask主要用來在系統沒有其他工作執行時,可以執行省電機制(PMIdle)或透過IdleMigration把多核心其它處理器上的工作進行重分配(LoadBalance),讓處於Idle的處理器可以分擔Task工作,充分利用系統運算資源.

initTask是所有UserMode Tasks的父行程,包含啟動時的ShellScript執行,或是載入必要的應用程式,都會基於initTask的執行來實現.

再來就是本文主要談的kthreaddTask,這是在LinuxKernel 2.6所引入的機制,在這之前要產生KernelThread需直接使用函式kernel_thread,而所產生的KernelThread父行程會是當下產生KernelThread的行程(或該父行程結束後,改為initTask(PID=1)).kthreadd的機制下,UserModeKernelMode Task的產生方式做了調整,並讓kthreaddTask成為使用kthread_create產生的KernelThread統一的父行程.也因此,在這機制實現下的LinuxKernel,屬於UserMode行程最上層的父行程為initTask (PID=1),而屬於KernelMode行程最上層的父行程為kthreaddTask (PID=2),而這兩個行程共同的父行程就是idle Task (PID=0).

kthreadd

kthreaddKernel Thread主要實作在檔案kernel/kthread.c,入口函式為kthreadd(宣告為intkthreadd(void *unused) ),主要負責的工作是檢視目前LinkedList Queue "kthread_create_list"是否有要產生KernelThread的需求,若有,就呼叫函式create_kthread進行後續的產生工作.而要透過kthreadd產生KernelThread需求,就可以透過呼叫kthread_create(與其他衍生的kthread函式,ex:kthread_create_on_node....etc.)把需求加入到LinkedList Queue "kthread_create_list",WakeUpkthreadd Task,就可以使用目前kthreadd新設計的機制.

有關函式kthreadd內部的運作流程,概述如下

1,執行set_task_comm(tsk,"kthreadd"),設定Task的執行檔名稱,會把"kthreadd"複製給task_struct中的comm (structtask_struct宣告在include/linux/sched.h).

2,執行ignore_signals(tsk),其中tsk= current,會設定讓Taskkthreadd忽略所有的Signals

3,設定kthreadd可以在所有處理器上執行.

4,進入kthreaddfor(;;)無窮迴圈,

4-1,透過函式list_empty確認kthread_create_list是否為空,若為空,就觸發Task排程

4-2,透過list_entry,取出要產生的KernelThread的”structkthread_create_info” Pointer

4-3,呼叫create_kthread產生KernelThread.

4-3-1,create_kthread中會以"pid= kernel_thread(kthread, create, CLONE_FS | CLONE_FILES |SIGCHLD);"呼叫函式kernel_thread(inarch/arm/kernel/process.c),其中,入口函式為kthread,新產生的Task的第一個函式引數為create.

4-3-1-1,在函式kernel_thread,會執行如下的程式碼,把最後新產生的KernelThread入口函式指給新Task的暫存器r5,要傳遞給該入口函式的變數只給暫存器r4,而該入口函式結束時要返回的函式kernel_thread_exit位址指給暫存器r7,並透過透過do_fork產生新的行程時,暫存器PC(Program Counter)指向函式kernel_thread_helper.也就是說每一個KernelThread的第一個函式統一都是kernel_thread_helper,而結束函式統一都為kernel_thread_exit.函式kernel_thread的參考程式碼如下所示

/*

*Create a kernel thread.

*/

pid_tkernel_thread(int (*fn)(void *), void *arg, unsigned long flags)

{

structpt_regs regs;

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

regs.ARM_r4= (unsigned long)arg;

regs.ARM_r5= (unsigned long)fn;

regs.ARM_r6= (unsigned long)kernel_thread_exit;

regs.ARM_r7= SVC_MODE | PSR_ENDSTATE | PSR_ISETSTATE;

regs.ARM_pc= (unsigned long)kernel_thread_helper;

regs.ARM_cpsr= regs.ARM_r7 | PSR_I_BIT;

returndo_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);

}

產生的Task會執行函式kernel_thread_helper,並把結束函式由暫存器r6指給暫存器LR(LinkerRegister),把入口函式的第一個引數指給暫存器r0,Task的入口函式由暫存器r5指給暫存器PC,開始新Task的執行.有關函式kernel_thread_helper的參考程式碼如下所示

externvoid kernel_thread_helper(void);

asm( ".pushsection .text\n"

" .align\n"

" .type kernel_thread_helper, #function\n"

"kernel_thread_helper:\n"

#ifdefCONFIG_TRACE_IRQFLAGS

" bl trace_hardirqs_on\n"

#endif

" msr cpsr_c, r7\n"

" mov r0, r4\n"

" mov lr, r6\n"

" mov pc, r5\n"

" .size kernel_thread_helper, . - kernel_thread_helper\n"

" .popsection");

由於新Task的入口函式統一為"kthread",第一個函式引數統一為”structkthread_create_info*create”,檢視函式kthread的實作,可以看到在新行程由kernel_thread_helper呼叫進入kthread,就會執行函式引數create中的create->threadfn函式指標,執行其他應用透過kthread_create產生KernelThread時的最終函式入口,參考程式碼如下所示

staticint kthread(void *_create)

{

/*Copy data: it's on kthread's stack */

structkthread_create_info *create = _create;

int(*threadfn)(void *data) = create->threadfn;

void*data = create->data;

.......................

ret= -EINTR;

if(!self.should_stop)

ret=threadfn(data);

/*we can't just return, we must preserve "self" on stack */

do_exit(ret);

}

有關kthreadd整體運作的概念,可參考下圖


kthread_createvs kernel_thread

kernel_thread函式,kthreadd機制產生前,要使用KernelThread主要的方式,而根據前述的介紹,可以看到其實kthread_create也是透過函式kernel_thread實現.

如果我們選擇直接透過kernel_thread產生KernelThread,跟透過kthreadd機制相比,兩者的差別在於,一個是由當下呼叫的kernel_threadTask行程所fork出來的,採用kthread_create機制則是由kthreaddTask行程所fork出來的.

執行指令ps -axjf可看到如下結果

PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND

0 2 0 0 ? -1 S 0 0:00 [kthreadd]

2 3 0 0 ? -1 S 0 0:00 \_ [ksoftirqd/0]

2 4 0 0 ? -1 S 0 0:01 \_ [kworker/0:0]

2 5 0 0 ? -1 S 0 0:00 \_ [kworker/u:0]

2 6 0 0 ? -1 S 0 0:00 \_ [migration/0]

2 7 0 0 ? -1 S 0 0:00 \_ [migration/1]

2 8 0 0 ? -1 S 0 0:00 \_ [kworker/1:0]

2 9 0 0 ? -1 S 0 0:00 \_ [ksoftirqd/1]

2 10 0 0 ? -1 S 0 0:01 \_ [kworker/0:1]

2 11 0 0 ? -1 S< 0 0:00 \_ [khelper]

2 12 0 0 ? -1 S< 0 0:00 \_ [netns]

2 13 0 0 ? -1 S 0 0:00 \_ [sync_supers]

2 14 0 0 ? -1 S 0 0:00 \_ [bdi-default]

2 15 0 0 ? -1 S< 0 0:00 \_ [kblockd]

2 16 0 0 ? -1 S< 0 0:00 \_ [kacpid]

2 17 0 0 ? -1 S< 0 0:00 \_[kacpi_notify]

2 18 0 0 ? -1 S< 0 0:00 \_[kacpi_hotplug]

2 19 0 0 ? -1 S 0 0:00 \_ [khubd]

2 20 0 0 ? -1 S< 0 0:00 \_ [md]

2 21 0 0 ? -1 S 0 0:00 \_ [khungtaskd]

2 22 0 0 ? -1 S 0 0:00 \_ [kswapd0]

2 23 0 0 ? -1 S 0 0:00 \_[fsnotify_mark]

我們可以知道,透過kthreadd所產生的Thread都會是以kthreaddParentTask,跟原本透過kernel_thread所產生的Task是源自於各自的Tasks是有所不同的.

如下所示為透過kernel_thread所自行產生的KernelThread,insmod指令結束後,這個KernelThreadParentTaskPID=1(也就是initTask.).

PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND

..

1 2122 2118 1987 pts/0 2126 R 0 0:21 insmod hello.ko

透過kthreadd的相關函式

函式名稱

說明

kthread_create_on_node

宣告為

structtask_struct *kthread_create_on_node(int (*threadfn)(void *data),

void*data,

intnode,

constchar namefmt[],

)

用以產生與命名KernelThread,並可在支援NUMANon-UniformMemory Access Architecture)的核心下,透過node可以設定要在哪個處理器上執行所產生的KernelThread.(會透過設定task->pref_node_fork),產生後的行程會等待被函式wake_up_process喚醒或是被kthread_stop所終止.

kthread_create

參考檔案include/linux/kthread.h,

函式kthread_create的宣告如下

#definekthread_create(threadfn, data, namefmt, arg...) \

kthread_create_on_node(threadfn,data, -1, namefmt, ##arg)

可以看到,kthread_create也是透過kthread_create_on_node實現,差異在於node值為-1,也就是可以在所有處理器上運作,產生後的行程會等待被函式wake_up_process喚醒或是被kthread_stop所終止.

kthread_run

參考檔案include/linux/kthread.h,

函式kthread_run的宣告如下

#definekthread_run(threadfn, data, namefmt, ...) \

({ \

structtask_struct *__k \

=kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \

if(!IS_ERR(__k)) \

wake_up_process(__k); \

__k; \

})

主要用以產生並喚醒KernelThread.(開發者可以省去要呼叫wake_up_process的動作.),且這呼叫是基於kthread_create,所以產生的KernelThread也不限於在特定的處理器上執行.

kthread_bind

繫結KernelThread到指定的處理器,主要是透過設定CPUAllowed Bitmask,所以在多核心的架構下,就可以指定給一個以上的處理器執行.

kthread_stop

用來暫停透過kthread_create產生的KernelThread,會等待KernelThread結束,並傳回函式threadfn(=產生KernelThread的函式)的返回值.

透過"wait_for_completion(&kthread->exited);"等待KernelThread結束,並透過"int ret;....ret =k->exit_code;.....return ret; "取得該結束的KernelThread返回值,傳回給函式kthread_stop的呼叫者.

kthread_should_stop

用來在KernelThread中呼叫,如果該值反為True,就表示該KernelThread X有被透過呼叫函式kthread_stop指定結束,因此,KernelThread X就必須要準備進行KernelThread的結束流程.

基於kthreadd產生的KernelTasks

接下來,我們把在LinuxKernel,基於kthread產生的KernelTasks做一個概要的介紹,藉此可以知道LinuxKernel有哪些模組基於這機制實現了哪些核心的能力.包括檔案系統Journaling機制,InetrruptBottomHalf,Kernel USB-Hub,Kernel Helper..,都是基於這機制下,所衍生出來的核心實作.

在這段落會頻繁提及的WorkQueue可以有兩種實現方式,分別為

1,WorkQueue (實作在kernel/workqueue.c).

2,產生Kernel Thread,並搭配Linked List Queue (ininclude/linux/list.h)機制.

如下,逐一介紹筆者認為值得說明的KernelThread與內部機制.

Kernel Tasks

名稱

kworker

參考檔案kernel/workqueue.c,kworker主要提供系統非同步執行的共用WorkerPool方案(WorkQueue),一般而言會區分為每個處理器專屬的WorkerPool或是屬於整個系統使用的WorkerPool

kworker/A:B”後方的數字意義分別是ACPU IDB為透過ida_get_new配置的ID(範圍從0-0x7fffffff).

以筆者雙核心的環境為例,CPU#0來說,會透過kthread_create_on_node產生固定在CPU#0上執行的[kworker/0:0],[kworker/0:1],[kworker/1:0][kworker/1:1].

並透過kthread_create產生不固定在特定處理器上執行的[kworker/u:0][kworker/u:1].

總共呼叫create_worker執行六個執行gcwq(GlobalCPU Workqueue) worker thread functionKernelThread.

PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND

2 4 0 0 ? -1 S 0 0:01 \_[kworker/0:0]

2 5 0 0 ? -1 S 0 0:00 \_[kworker/u:0]

2 8 0 0 ? -1 S 0 0:00 \_[kworker/1:0]

2 10 0 0 ? -1 S 0 0:00 \_[kworker/0:1]

2 30 0 0 ? -1 S 0 0:00 \_[kworker/1:1]

2 46 0 0 ? -1 S 0 0:00 \_[kworker/u:1]

worker_threadWorkQueue機制的核心,包括新的WorkQueue產生(alloc_workqueue),指派工作到WorkQueue(queue_work),把工作指派到特定的處理器(queue_work_on),指派工作並設定延遲執行(queue_delayed_work),在指定的處理器上指派工作並延遲執行(queue_delayed_work_on)...

限於本次預定的篇幅,有關WorkQueue的進一步討論,會在後續文章中介紹.

需要進一步資訊的開發者,可以自行參閱LinuxKernel檔案Documentation/workqueue.txt.

ksoftirqd

參考檔案kernel/softirq.c,會在每個處理器進入CPU_UP_PREPARECPU_UP_PREPARE_FROZEN狀態時,在個別處理器產生ksoftirqdKernel Thread,如下所示,在雙核心環境中會有兩個KernelThread各自在兩個處理器上執行.

PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND

2 3 0 0 ? -1 S 0 0:00 \_[ksoftirqd/0]

2 9 0 0 ? -1 S 0 0:00 \_[ksoftirqd/1]

ksoftirqdKernel Thread函式run_ksoftirqd,會透過

"while(!kthread_should_stop()) { ...}"迴圈,確認目前是否有被執行終