Android 筆記-LinuxKernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(4)Linux 多核心啟動流程-kthreadd與相關的核心模組
by loda
hlchou@mail2000.com.tw
Loda's Blog
Android/Linux Source Code Tags
App BizOrz
BizOrz.COM
BizOrz Blog
kthread第一次出現在LinuxKernel中是在Kernel版本2.6.4時,一開始的實作尚未有本文提到的kthreaddTask的具體架構,隨著版本的演進,除了這部份的設計完整外,需要產生KernelThread的實作也都已經改用kthread機制.
本文會先針對kthreaddTask行為加以說明,並會以在啟動後,屬於KernelMode產生的KernelThread的個別行為與功能做一個介紹,由於這部份涉及的範圍不少,筆者會以自己的角度選擇認為值得加以說明的項目,相信應該足以涵蓋大多數人對於LinuxKernel Mode Tasks所需的範圍.如果有所不足之處,也請透過LinuxKernel Hacking加以探索.
簡要來說,位於KernelMode的Tasks產生,除了直接透過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=0的IdleTask,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的機制下,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()) { ...}"迴圈,確認目前是否有被執行終止動作,若無,就往後繼續執行.並透過local_softirq_pending確認是否有SoftIRQ被觸發,為了系統效能考量,目前LinuxKernel會把TopHalves放在對應的中斷Routine中執行,而把屬於BottomHalves的部份透過kirqsoftd執行.並且在ARM多核心的架構下,每個處理器都會有自己的LocalIRQ,因此,如果發現有LocalSoftIRQ待處理,就會由各自的處理器對應的ksoftirqdKernel Thread負責執行BottomHalves中的工作. 如果發現,待處理的CPU已經OffLine也會立刻結束ksoftirqd的執行.而在ksoftirqd中,會透過函式__do_softirq執行SoftIRQBottom Halves 的工作. 若有進一步興趣的開發者,也可以參考這篇文章http://book.chinaunix.net/special/ebook/oreilly/Understanding_Linux_Network_Internals/0596002556/understandlni-CHP-9-SECT-3.html. |
khelper |
參考檔案kernel/kmod.c,在KernelInit的過程中會呼叫函式usermodehelper_init,在這函式中就會執行"khelper_wq =create_singlethread_workqueue("khelper");"產生khelperKernel Thread. 在LinuxKernel執行的過程中,就可以透過函式call_usermodehelper_exec執行 "queue_work(khelper_wq,&sub_info->work);"把工作指派給khelperWorkQueue中. 例如,要載入一個LinuxKernelModule時,就會透過函式__request_module,之後呼叫call_usermodehelper_fns並帶入modprobe路徑與相關參數作為函式參數(函式call_usermodehelper_fns的inline實作宣告在include/linux/kmod.h中),最後會呼叫進入call_usermodehelper_exec,把工作放到khelperWorkQueue中,來透過khelperKernel Thread帶起UserMode的應用程式執行. 如下為khelper在筆者環境中產生的行程資訊 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 2 11 0 0 ? -1 S< 0 0:00 \_[khelper] 而每一個要放到khelperWorkQueue的工作,都會透過函式call_usermodehelper_setup執行"INIT_WORK(&sub_info->work,__call_usermodehelper); " 設定WorkQueue要執行的Work函式(khelperWork函數固定設為__call_usermodehelper),外部執行檔路徑與相關參數. 因此,一旦khelperKernelThread從WorkQueue中執行到新工作時,就會呼叫函式__call_usermodehelper,最後透過函式____call_usermodehelper,執行核心函式kernel_execve,來達成執行User-Mode應用程式的目的.(這也就是如何由核心去執行外部的LinuxDevice Driver Module工具的執行路徑,清楚khelper的行為與機制,對了解LinuxDrivers載入/運作原理會很有幫助.) |
kblockd |
參考檔案block/blk-core.c,如同khelper,這同樣是透過WorkQueue產生的KernelThread,在initCall中呼叫genhd_device_init函式時,會執行blk_dev_init並執行"kblockd_workqueue= alloc_workqueue("kblockd",WQ_MEM_RECLAIM | WQ_HIGHPRI,0);" 產生kblockdKernel Thread. 如下為kblockd在筆者環境中產生的行程資訊 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 2 15 0 0 ? -1 S< 0 0:00 \_[kblockd] 在系統運作過程中,可透過blk_delay_queue/blk_run_queue_async/kblockd_schedule_work/kblockd_schedule_delayed_work對kblockdWorkQueue進行工作的指派. |
kseriod |
參考檔案drivers/input/serio/serio.c, 參考KernelSource Code, Serio在LinuxKernel是對"SerialI/O "周邊的支援模組,只要有使用到SerialI/O的輸入裝置(InputDevice),例如:ATkeyboard, PS/2 mouse, joysticks搖桿..etc都屬此類. 也因此,屬於Serio的輸入裝置,底層就可以包括RS232(ComPort),使用i8042Controller晶片的AT與PS/2鍵盤/滑鼠,使用ct82c710Controller晶片的QuickPort滑鼠...etc.Serio底層可支援的Controller很豐富,在此只列舉部分底層控制晶片,有興趣的開發者請自行參考LinuxKernel Source. 啟動時,會在KernelInit執行到initCall時呼叫serio_init函式,並執行"serio_task= kthread_run(serio_thread, NULL, "kseriod");"產生kseriodKernel Thread,以函式serio_thread為KernelThread的執行函式 staticint serio_thread(void *nothing) { do{ serio_handle_event(); wait_event_interruptible(serio_wait, kthread_should_stop()|| !list_empty(&serio_event_list)); }while (!kthread_should_stop()); return0; } 只要serio_queue_event被呼叫到(像是EventSERIO_RESCAN_PORT/SERIO_RECONNECT_CHAIN/SERIO_REGISTER_PORT/SERIO_ATTACH_DRIVER/SERIO_RECONNECT_PORT)就會把Add新的Event到LinkedList Queue中,並喚醒kseriod KernelThread處理對應SerioEvent. 如下為kseriod在筆者環境中產生的行程資訊 USER PID PPID VSIZE RSS WCHAN PC NAME root 9 2 0 0 c018179c 00000000 S kseriod |
kmmcd |
參考檔案drivers/mmc/core/core.c,如同khelper,這同樣是透過WorkQueue產生的KernelThread,是透過執行"workqueue= create_singlethread_workqueue("kmmcd");"產生的kmmcdKernel Thread.用以支援MMC/SD卡的行為. 如下為kseriod在筆者環境中產生的行程資訊 USER PID PPID VSIZE RSS WCHAN PC NAME root 10 2 0 0 c004b2c4 00000000 S kmmcd |
kswapd |
參考檔案mm/vmscan.c, 啟動時,會在KernelInit執行到initCall時呼叫kswapd_init函式,並進入kswapd_run後,執行"pgdat->kswapd= kthread_run(kswapd, pgdat, "kswapd%d", nid);"產生kswapdKernel Thread,並以函式kswapd(宣告:staticint kswapd(void *p) )為KernelThread的執行函式.如下為函式kswapd_init的內容,在swap_setup(in mm/swap.c)之後,會透過for_each_node_state(in include/linux/nodemask.h), staticint __init kswapd_init(void) { intnid; swap_setup(); for_each_node_state(nid,N_HIGH_MEMORY) kswapd_run(nid); hotcpu_notifier(cpu_callback,0); return0; } 其中 for_each_node_state宣告如下, #definefor_each_node_state(__node, __state) \ for_each_node_mask((__node),node_states[__state]) 對Linux而言,在多核心的架構下,可以支援以Node方式去GroupCPU,也就是說每個Node可以擁有一個以上的處理器,而在這就會選擇Node的HIGH_MEMORYState有被設定的Node.也就是說,如果該CPUNode的HIGH_MEMORYState有被設定,就會執行kswapd_run,並把該NodeId帶入成為nid的值.(也就是最後kswapd後面的數字). 筆者對LinuxKernel HighMemory的理解是,當所配置的實體記憶體大於LinuxKernel虛擬記憶體範圍(通常在ARM上是0xc0000000-0xffffffff這1GB的範圍),而因為受限於Memory MappedI/O或實際上的Kernel需要,有部分所需的實體記憶體無法被Mapped到這1GB虛擬記憶體空間中,此時就會需要KernelEnable High Memory的能力.目前在ARMLinux Kernel中也有支援HighMemory,並可針對HighMemory需求配置2nd-levelPageTables.在沒開啟HighMemory的LinuxKernel, Node States中N_HIGH_MEMORY的值會等於N_NORMAL_MEMORY,也就是會讓NormalMemory跟HighMemory的設定一致.一般而言,在考慮系統效能時,並不建議開啟HighMemory能力.前提也是硬體設計時,也要避免遇到這樣的問題,如果選擇把KernelSpace加大(例如從1G/3G變成2G/2G),則會限制到需要大量記憶體應用程式的執行. 這有一篇關於LinuxKernel HighMemory的文章,有興趣的開發者也可以參考看看http://kerneltrap.org/node/2450. 如下為kswapd在筆者環境中產生的行程資訊 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 2 22 0 0 ? -1 S 0 0:00 \_ [kswapd0] 而在kswapdKernel Thread的函式kswapd中,主要進行縮減Slab Cache(例如檔案系統快取),並在FreePages數量過低時,把記憶體SwapOut到磁碟中,會透過函式balance_pgdat對每個處理器Node(這定義是針對多核心的內化設計.)的記憶體區域(Zone)進行記憶體釋放,直到所有記憶體區域的FreePages都達到HighWaterMark(透過呼叫high_wmark_pages確認),也就是"free_pages> high_wmark_pages(zone)". 參考include/linux/mmzone.h,有關MemoryZone Water Marker相關巨集宣告如下 #definemin_wmark_pages(z) (z->watermark[WMARK_MIN]) #definelow_wmark_pages(z) (z->watermark[WMARK_LOW]) #definehigh_wmark_pages(z) (z->watermark[WMARK_HIGH]) 最後,附帶補充一下,LinuxKernel本身支援在每次透過PageAllocate配置記憶體時(SourceCode在mm/page_alloc.c),透過核心的OOM模組(SourceCode在mm/oom_kill.c),評估目前記憶體資源是不是快耗盡,如果是就會透過函式mem_cgroup_out_of_memory呼叫select_bad_process選擇一個透過badness計算後,要被終止的行程,然後呼叫函式oom_kill_process,最後在函式__oom_kill_task中發送SIGKILLSignal強制終止行程執行. 而Android本身也有在LinuxKernel中加入LowMemory Killer模組(並關閉LinuxKernel原本的OOM),啟動時會透過函式register_shrinker(inmm/vmscan.c)註冊自己的lowmem_shrinker服務. |
kstriped |
參考檔案drivers/md/dm-stripe.c, 會在執行dm_stripe_init時,透過"kstriped= create_singlethread_workqueue("kstriped");"產生執行WorkQueue的kernelThread,並在函式stripe_ctr中透過"INIT_WORK(&sc->kstriped_ws,trigger_event);"設定函式trigger_event為WorkQueue的Work函式,並設定相關Strip所需參數.在函式trigger_event中,會再呼叫dm_table_event(indrivers/md/dm-table.c)然後執行event_fn. MD (MultipleDevice)主要用以支援把多個裝置虛擬為單一裝置,一般會應用在SoftwareRAID或LVM(Logical Volume Management)的功能上.同時,參考LinuxKernel文件Documentation/device-mapper/striped.txt,dm-stripe (inDevice-Mapper)主要用以支援像是RAID0的Striped儲存裝置,可以把要寫入的資料,分割為不同的Chunks循序且循環的寫入到多個底層的儲存裝置中,由於是把單筆資料分割為不同的Chunks個別對多個裝置寫入,因此可以大幅改善儲存媒體寫入的效率.(相對於只對單一裝置寫入,把一筆資料分割後同時對多個裝置寫入可以縮短寫入等待的時間). 目前LinuxKernel的SoftwareRAID可支援軟體把多個磁碟Partition整合為一個邏輯磁碟,並支援像是RAID1,4或5,藉此避免硬碟損壞時造成的資料損失.不過在重視效能的環境中,HardwareRAID還是比較有效率的. 如下為kstriped在筆者環境中產生的行程資訊 USER PID PPID VSIZE RSS WCHAN PC NAME root 23 2 0 0 c004b2c4 00000000 S kstriped Android本身也有支援對Device-Mapper的操作,細節就不在本文範圍中,各位可以參考2.3Source Code "gingerbread/system/vold/Devmapper.cpp". |
kjournald |
參考檔案fs/jbd/journal.c,會透過執行"t= kthread_run(kjournald, journal, "kjournald");",產生支援Ext3檔案系統Journaling的KernelThread.或參考檔案fs/jbd2/journal.c,會透過執行"t= kthread_run(kjournald2, journal,"jbd2/%s",journal->j_devname);",產生支援Ext4與OCFS2檔案系統,並可延伸支援更大空間64bits block numbers機制的JournalingKernel Thread. 以支援Ext3的kjournald來說,主要負責兩類工作, 1,Commit: 用以把JoirnalingFileSystem所產生的MetaData寫回對應的檔案系統位置.用以正式的把資料完成寫入動作,並可釋出MetaData的空間. 2,CheckPoint:會由這個Thread執行查核點的動作,把JournalingLog內容回寫,以便這些空間可以重複利用. 啟動流程為,在Ext3初始化時,執行函式ext3_create_journal(in fs/ext3/super.c),之後進入函式journal_create(in fs/jbd/journal.c)->journal_reset (in fs/jbd/journal.c)->journal_start_thread (in fs/jbd/journal.c),透過函式kthread_run產生kjournaldKernel Thread,之後進入"wait_event(journal->j_wait_done_commit,journal->j_task != NULL);"等待kjournaldKernel Thread產生完畢,呼叫"wake_up(&journal->j_wait_done_commit);"讓函式journal_start_thread結束執行. 有關kjournald會被TimerWakeUp起來執行Commit的流程,其中Interval的設定如下"journal->j_commit_interval= (HZ * JBD_DEFAULT_MAX_COMMI T_AGE);" (infs/jbd/journal.c),而HZ為每秒觸發的Tick中斷次數(在筆者環境為100).同時參考檔案"include/linux/jbd.h"中有關JBD_DEFAULT_MAX_COMMI T_AGE的設定為5.("#define JBD_DEFAULT_MAX_COMMIT_AGE 5"),也就是說kjournaldKernel Thread會被Timer喚醒的Interval值為5秒. 函式kjournald(原型為"staticint kjournald(void *arg)"),為KernelThread的主函式,會執行如下的動作, 1,設定Timer(呼叫"setup_timer(&journal->j_commit_timer,commit_timeout,(unsignedlong)current);"),固定週期透過Callback函式commit_timeout喚醒kjournaldKernel Thread,進行Journaling檔案系統Log回寫的動作.參考檔案"fs/jbd/transaction.c",在函式get_transaction中,會設定Expire的時間點為現在的Tick值jiffies加上journal->j_commit_int erval 的值("transaction->t_expires= jiffies + journal->j_commit_interval;"),(在筆者環境Interval設定為5秒.). 2,進入Loop中, 2-1,確認journal->j_flags,若狀態JFS_UNMOUNT成立,就結束Loop 2-2,呼叫函式journal_commit_transaction(in fs/jbd/commit.c),執行Log回寫 2-2-1,......至於回寫機制....過於細節,就不在這討論了. 2-3,喚醒等待journal->j_wait_done_commit的行程(等同告知Log回寫結束.) 2-4,確認thread_info->flags的TIF_FREEZEbit是否為1(TIF_FREEZE=19). (例如可以透過函式freeze_task對Task發出freezerequest ). 2-4-1,若上述成立,就會進入函式refrigerator(inkernel/freezer.c),並進入for(;;)迴圈中,直到frozen(current)不成立,才會結束迴圈,把函式執行完畢. 2-5,反之,若2-4條件不成立,就會等待journal->j_wait_commitWait Event,或判斷是否可以進入等待,例如還有待回寫的Log沒有執行完畢("if(journal->j_commit_sequence != journal->j_commit_request)"),或有執行中的Transaction且現在的系統Tick值已經大於等於原本kjournald所設定的Timer要觸發TimeOut的時間值(也就是說已經發生過TimeOut了),若上述條件成立,就會讓系統繼續執行下去,而不進入等待. 2-6,kjournald處於執行狀態(有可能是因為TimeOut或是journal->j_wait_commitWait Event喚醒),透過比對現在的Tick值是否大於等於TimerTimeOut值(transaction->t_expires),確認是否為透過Timer喚醒. 2-7,重複回到2-1,繼續kjournaldKernel Thread的執行. 參考下圖,為整個kjournaldKernel Thread與Timer運作的示意圖. |
events |
參考檔案kernel/workqueue.c, events KernelThread主要提供核心延後工作執行的方式,如前述的實作介紹,有的應用會透過自己建立的WorkQueue來實現這樣的設計,或是也可以透過GlobalWorkQueue來達成目的,在啟動過程中會透過函式init_workqueues來初始化keventd_wq (也就是keventdWorkQueue).參考如下程式碼, void__init init_workqueues(void) { ….. keventd_wq= create_workqueue("events"); …. } 當有需要使用預設的WorkQueue時,可以透過函式schedule_work,把要處理的工作放到GlobalWork Queue,這函式參考實作如下 intschedule_work(struct work_struct *work) { returnqueue_work(keventd_wq, work); } 如果希望放到GlobalWorkQueue並加入Delayed值,可以透過函式schedule_delayed_work,參考實作如下 intschedule_delayed_work(struct delayed_work *dwork, unsignedlong delay) { returnqueue_delayed_work(keventd_wq, dwork, delay); } 或透過函式schedule_work_on把工作指派到指定的處理器上,參考實作如下 intschedule_work_on(int cpu, struct work_struct *work) { returnqueue_work_on(cpu, keventd_wq, work); } 也可以透過函式schedule_delayed_work_on指定在所要的處理器上,並附帶Delayed值,參考實作如下 intschedule_delayed_work_on(int cpu, structdelayed_work *dwork, unsigned long delay) { returnqueue_delayed_work_on(cpu, keventd_wq, dwork, delay); } 有興趣的開發者,也可以參考這篇LinuxKernel Korner文章"KernelKorner - The New Work Queue Interface in the 2.6 Kernel" (inhttp://www.linuxjournal.com/article/6916) |
pdflush |
參考檔案mm/pdflush.c, pdflush主要的工作為把由檔案系統Mapping到記憶體的內容為Dirty的Pages,負責回寫到檔案系統中.可以透過設定/proc/sys/vm/dirty_background_ratio決定當DirtyPages超過多少比率時,便執行回寫到檔案系統的動作.(在筆者的環境中為10%). 在運作時,會透過函式start_one_pdflush_thread產生pdflush KrnelThread,參考如下程式碼 staticvoid start_one_pdflush_thread(void) { structtask_struct *k; k= kthread_run(pdflush, NULL, "pdflush"); if(unlikely(IS_ERR(k))) { spin_lock_irq(&pdflush_lock); nr_pdflush_threads--; spin_unlock_irq(&pdflush_lock); } } 並可視目前執行回寫動作忙碌的情況,再透過函式start_one_pdflush_thread產生新的pdflushKernel Thread. (總量不超過MAX_PDFLUSH_THREADS). |
Migration |
參考檔案kernel/stop_machine.c, 在多核心的架構下,Migration在意義上主要是讓運作在處理器#A的行程可以轉移到處理器#B或其它處理器上.但在目前筆者使用的LinuxKernel中,Migration主要為當處理器進行CPUDown流程時,負責執行Callback函式,讓屬於要停止的處理器上的Tasks可以轉移到還能持續運作的處理器上. Migration KernelThread主要的任務為在多核心的架構下,支援StopCPU的行為,也因此可以視這個KernelThread為StopperKernel Thread, 當處理器進行CPUDown時,就會透過這個KernelThread呼叫所指定CallbackFunctions. 如之前介紹到的其它模組,在多核心的處理器上,會產生對應的Migration KernelThread,例如:[migration/0]與[migration/1]. 1,當新執行的應用程式,被分配到的處理器跟目前所在處理器不同時(dest_cpu不等於smp_processor_id()時). 例如,當使用者執行一個新的程式,就會透過函式do_execve(in fs/exec.c)開啟該執行檔,並呼叫函式sched_exec(in kernel/sched.c)在新產生的Task行程中執行"p->sched_class->select_task_rq",用以確認這個新產生的Task要被排程的目標處理器,是否跟目前所在的處理器一致,那是那就返回繼續執行,若非,就會執行"stop_one_cpu(cpu_of(rq),migration_cpu_stop, &arg);"透過函式stop_one_cpu,讓migrationKernel Thread執行函式migration_cpu_stop來進行Task轉移的工作. 2,當CPU的AllowedBitmask被修改,且該Task正位於不被允許的處理器上 例如,當使用者透過set_cpus_allowed_ptr(inkernel/sched.c)修改Task的CPUAllowed Bitmask時,會執行"if(cpumask_test_cpu(task_cpu(p), new_mask))"確認目前所在的處理器是否符合新的Bitmask設定,若所在的處理器並非允許執行的處理器,就會執行"stop_one_cpu(cpu_of(rq),migration_cpu_stop, &arg);"透過函式stop_one_cpu,讓migrationKernel Thread執行函式migration_cpu_stop來進行Task轉移的工作. 3,當處理器沒有Task運作時(也就是處於Idle的狀態),由忙碌的處理器轉移Task來執行. 例如:在排程函式schedule中,會透過"if(unlikely(!rq->nr_running))"確認目前該處理器是否處於沒有Task執行的狀態,若是就會嘗試把其它處理器上的Tasks移到目前Idle的處理器上執行.首先,會呼叫函式idle_balance(in kernel/sched_fair.c),並確認sched_domain的SD_BALANCE_NEWIDLEflag是否成立,若成立就會執行"pulled_task= load_balance(this_cpu, this_rq,sd, CPU_NEWLY_IDLE,&balance);",在函式load_balance(in kernel/sched_fair.c)中,會執行"stop_one_cpu_nowait(cpu_of(busiest),active_load_balance_cpu_stop, busiest,&busiest->active_balance_work);"透過函式stop_one_cpu_nowait,讓migrationKernelThread執行函式active_load_balance_cpu_stop,把Task從目前最忙碌的處理器移到Idle的處理器上.有關處理器TaskBalance的動作,值得討論的事項很多,後續有機會再加以詳細說明. 4,當處理器被關閉時(在有支援HotPlug的環境下,進行CPUDown) 例如:進行CPUDown時會透過函式cpu_down(in kernel/cpu.c)進入 _cpu_down(in kernel/cpu.c)執行"err= __stop_machine(take_cpu_down, &tcd_param,cpumask_of(cpu));",透過函式__stop_machine (inkernel/stop_machine.c)呼叫stop_cpus(in kernel/stop_machine.c),進入__stop_cpus(inkernel/stop_machine.c)中,執行函式cpu_stop_queue_work,把工作透過list_add_tail(&work->list,&stopper->works);放到stopper->worksQueue中,讓migrationKernel Thread執行函式take_cpu_down(in kernel/cpu.c)完成CPUDown的流程. 上述四個條件,是在目前LinuxKernel下,會透過migration KernelThread執行的情況. 在啟動過程中,會呼叫函式cpu_stop_init,並以啟動的處理器作為BootCPU來呼叫函式cpu_stop_cpu_callback,執行CPU_UP_PREPARE的動作,會以函式cpu_stopper_thread做為migrationKernel Thread的執行起點,並以函式kthread_bind設定該Thread只能在目前產生該KernelThread的處理器上執行.可以參考如下程式碼. staticint __cpuinit cpu_stop_cpu_callback(struct notifier_block *nfb, unsignedlong action, void *hcpu) { unsignedint cpu = (unsigned long)hcpu; structcpu_stopper *stopper = &per_cpu(cpu_stopper, cpu); structtask_struct *p; switch(action & ~CPU_TASKS_FROZEN) { caseCPU_UP_PREPARE: BUG_ON(stopper->thread|| stopper->enabled || !list_empty(&stopper->works)); p= kthread_create_on_node(cpu_stopper_thread, stopper, cpu_to_node(cpu), "migration/%d",cpu); if(IS_ERR(p)) returnnotifier_from_errno(PTR_ERR(p)); get_task_struct(p); kthread_bind(p,cpu); sched_set_stop_task(cpu,p); stopper->thread= p; break; ….….. migration KernelThread會在CPU_UP_PREPARE事件中產生,並在CPU_ONLINE事件被WakeUp與設定"stopper->enabled= true;". 在有支援CPUHotPlug的環境中,會有額外的CPU_UP_CANCELED與CPU_POST_DEAD事件,會在透過函式kthread_stop把migrationKernel Thread結束. 而在函式cpu_stopper_thread中,就會進入一個goto的迴圈中,確認stopper->worksQueue中是否有被指派的工作,若有就會執行該工作對應的Callback函式. |
其他常見的KernelThread還包括suspend,cqueue,aio,mtdblockd,hid_compat,rpciod,mmcqd....etc,就不在這多做介紹,有興趣的開發者請自行參閱LinuxKernel Source Code. |
結語
本文基本上以kthreaddTask為起點,探討了一部分以此為基礎的核心模組,然而由於涉及的領域很廣泛,筆者主要以自己認為值得加以說明的模組進行介紹,對核心解析有熱情的開發者,建議可以自行進一步的Hacking.
有關LinuxKernel啟動流程的介紹,就以本文告結.