Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(4) Linux 多核心啟動流程-kthreadd 與相關的
by loda
Loda's Blogkthread第一次出現在LinuxKernel中是在Kernel版本2.6.4時,一開始的實作尚未有本文提到的kthreaddTask的具體架構,隨著版本的演進,除了這部份的設計完整外,需要產生KernelThread的實作也都已經改用kthread機制.
本文會先針對kthreaddTask行為加以說明,並會以在啟動後,屬於KernelMode產生的KernelThread的個別行為與功能做一個介紹,由於這部份涉及的範圍不少,筆者會以自己的角度選擇認為值得加以說明的專案,相信應該足以涵蓋大多數人對於
簡要來說,位於KernelMode的Tasks產生,除了直接透過kernel_thread函式外,還可以有兩個來源,一個是透過kthread_create(實作上,也是透過函式Kernel_Thread)產生的KernelThread,另一個則是透過WorkQueue機制產生的KernelThread,前者可以依據設計者自己對系統架構的掌握,去設計多工機制(例如,使用稍後提到的LinkedList Queue).而後者,則是由LinuxKernel
在實際的應用上,WorkQueue還可以用以實現中斷的BottomHalf機制,讓中斷觸發時對Timing要求高的部分(TopHalf)可以在InterruptHandler中盡快執行完畢,透過WorkQueue把需要較長時間執行的部分(BottomHalf)交由WorkQueue延遲執行實現.(等同於RTOS下的LISR/HISR).
LinuxKernel的基礎Tasks.
在LinuxKernel中有三個最基礎的Tasks,
其中,IdleTask主要用來在系統沒有其他工作執行時,可以執行省電機制(PMIdle)或透過IdleMigration把多核心其它處理器上的工作進行重分配(LoadBalance),讓處於Idle的處理器可以分擔Task工作,充分利用系統運算資源.
而initTask是所有UserMode Tasks的父行程,包含啟動時的ShellScript執行,或是載入必要的應用程式,都會基於initTask的執行來實現.
再來就是本文主要談的kthreaddTask,這是在LinuxKernel 2.6所引入的機制,在這之前要產生KernelThread需直接使用函式kernel_thread,而所產生的KernelThread父行程會是當下產生KernelThread的行程(或該父行程結束後,改為initTask(PID=1)).在kthreadd的機制下,UserMode與KernelMode 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,進入kthreadd的for(;;)無窮迴圈,
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(®s,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, ®s, 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_thread的Task行程所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都會是以kthreadd為Parent的Task,跟原本透過kernel_thread所產生的Task是源自於各自的Tasks是有所不同的.
如下所示為透過kernel_thread所自行產生的KernelThread,在insmod指令結束後,這個KernelThread的ParentTask為PID=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,並可在支援NUMA(Non-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”後方的數字意義分別是A為CPU ID而B為透過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 function的KernelThread. 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_thread為WorkQueue機制的核心,包括新的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_PREPARE或CPU_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()) { ...}"迴圈,確認目前是否有被執行終 |