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

时间:2022-04-10 15:12:57

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


by  loda

hlchou@mail2000.com.tw

Loda's Blog

App BizOrz

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加以探索.


簡要來說,位於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整體運作的概念,可參考下圖


Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(4) Linux 多核心啟動流程-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()) { ...}"迴圈,確認目前是否有被執行終止動作,若無,就往後繼續執行.並透過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_fnsinline實作宣告在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),外部執行檔路徑與相關參數.

因此,一旦khelperKernelThreadWorkQueue中執行到新工作時,就會呼叫函式__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_workkblockdWorkQueue進行工作的指派.


kseriod

參考檔案drivers/input/serio/serio.c,

參考KernelSource Code, SerioLinuxKernel是對"SerialI/O "周邊的支援模組,只要有使用到SerialI/O的輸入裝置(InputDevice),例如:ATkeyboard, PS/2 mouse, joysticks搖桿..etc都屬此類.

也因此,屬於Serio的輸入裝置,底層就可以包括RS232(ComPort),使用i8042Controller晶片的ATPS/2鍵盤/滑鼠,使用ct82c710Controller晶片的QuickPort滑鼠...etc.Serio底層可支援的Controller很豐富,在此只列舉部分底層控制晶片,有興趣的開發者請自行參考LinuxKernel Source.


啟動時,會在KernelInit執行到initCall時呼叫serio_init函式,並執行"serio_task= kthread_run(serio_thread, NULL, "kseriod");"產生kseriodKernel Thread,以函式serio_threadKernelThread的執行函式


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新的EventLinkedList 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可以擁有一個以上的處理器,而在這就會選擇NodeHIGH_MEMORYState有被設定的Node.也就是說,如果該CPUNodeHIGH_MEMORYState有被設定,就會執行kswapd_run,並把該NodeId帶入成為nid的值.(也就是最後kswapd後面的數字).


筆者對LinuxKernel HighMemory的理解是,當所配置的實體記憶體大於LinuxKernel虛擬記憶體範圍(通常在ARM上是0xc0000000-0xffffffff1GB的範圍),而因為受限於Memory MappedI/O或實際上的Kernel需要,有部分所需的實體記憶體無法被Mapped到這1GB虛擬記憶體空間中,此時就會需要KernelEnable High Memory的能力.目前在ARMLinux Kernel中也有支援HighMemory,並可針對HighMemory需求配置2nd-levelPageTables.在沒開啟HighMemoryLinuxKernel, Node StatesN_HIGH_MEMORY的值會等於N_NORMAL_MEMORY,也就是會讓NormalMemoryHighMemory的設定一致.一般而言,在考慮系統效能時,並不建議開啟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配置記憶體時(SourceCodemm/page_alloc.c),透過核心的OOM模組(SourceCodemm/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");"產生執行WorkQueuekernelThread,並在函式stripe_ctr中透過"INIT_WORK(&sc->kstriped_ws,trigger_event);"設定函式trigger_eventWorkQueueWork函式,並設定相關Strip所需參數.在函式trigger_event,會再呼叫dm_table_event(indrivers/md/dm-table.c)然後執行event_fn.


MD (MultipleDevice)主要用以支援把多個裝置虛擬為單一裝置,一般會應用在SoftwareRAIDLVM(Logical Volume Management)的功能上.同時,參考LinuxKernel文件Documentation/device-mapper/striped.txt,dm-stripe (inDevice-Mapper)主要用以支援像是RAID0Striped儲存裝置,可以把要寫入的資料,分割為不同的Chunks循序且循環的寫入到多個底層的儲存裝置中,由於是把單筆資料分割為不同的Chunks個別對多個裝置寫入,因此可以大幅改善儲存媒體寫入的效率.(相對於只對單一裝置寫入,把一筆資料分割後同時對多個裝置寫入可以縮短寫入等待的時間).


目前LinuxKernelSoftwareRAID可支援軟體把多個磁碟Partition整合為一個邏輯磁碟,並支援像是RAID1,45,藉此避免硬碟損壞時造成的資料損失.不過在重視效能的環境中,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檔案系統JournalingKernelThread.或參考檔案fs/jbd2/journal.c,會透過執行"t= kthread_run(kjournald2, journal,"jbd2/%s",journal->j_devname);",產生支援Ext4OCFS2檔案系統,並可延伸支援更大空間64bits block numbers機制的JournalingKernel Thread.


以支援Ext3kjournald來說,主要負責兩類工作,

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的時間點為現在的Tickjiffies加上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->flagsTIF_FREEZEbit是否為1(TIF_FREEZE=19). (例如可以透過函式freeze_taskTask發出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 ThreadTimer運作的示意圖.

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


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到記憶體的內容為DirtyPages,負責回寫到檔案系統中.可以透過設定/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的行為,也因此可以視這個KernelThreadStopperKernel 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,CPUAllowedBitmask被修改,且該Task正位於不被允許的處理器上

例如,當使用者透過set_cpus_allowed_ptr(inkernel/sched.c)修改TaskCPUAllowed 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_domainSD_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_CANCELEDCPU_POST_DEAD事件,會在透過函式kthread_stopmigrationKernel Thread結束.



而在函式cpu_stopper_thread,就會進入一個goto的迴圈中,確認stopper->worksQueue中是否有被指派的工作,若有就會執行該工作對應的Callback函式.


其他常見的KernelThread還包括suspend,cqueue,aio,mtdblockd,hid_compat,rpciod,mmcqd....etc,就不在這多做介紹,有興趣的開發者請自行參閱LinuxKernel Source Code.





結語


本文基本上以kthreaddTask為起點,探討了一部分以此為基礎的核心模組,然而由於涉及的領域很廣泛,筆者主要以自己認為值得加以說明的模組進行介紹,對核心解析有熱情的開發者,建議可以自行進一步的Hacking.


有關LinuxKernel啟動流程的介紹,就以本文告結.