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

Posted by Lawpig on 6月 03, 2017 | No comments

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


Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(4) Linux 多核心啟動流程kthreadd 與相關的核心模組
by loda
hlchou@mail2000.com.tw
kthread第一次出現在Linux Kernel 中是在Kernel版本2.6.4,一開始的實作尚未有本文提到的kthreadd Task的具體架構,隨著版本的演進,除了這部份的設計完整外,需要產生Kernel Thread的實作也都已經改用kthread機制.
本文會先針對kthreadd Task行為加以說明,並會以在啟動後,屬於Kernel Mode產生的Kernel Thread的個別行為與功能做一個介紹,由於這部份涉及的範圍不少,筆者會以自己的角度選擇認為值得加以說明的項目,相信應該足以涵蓋大多數人對於Linux Kernel Mode Tasks所需的範圍.如果有所不足之處,也請透過Linux Kernel Hacking加以探索.
簡要來說,位於Kernel ModeTasks產生,除了直接透過kernel_thread函式外,還可以有兩個來源,一個是透過kthread_create (實作上,也是透過函式 Kernel_Thread)產生的Kernel Thread,另一個則是透過WorkQueue機制產生的Kernel Thread,前者可以依據設計者自己對系統架構的掌握,去設計多工機制(例如,使用稍後提到的Linked List Queue).而後者,則是由Linux Kernel提供延遲處理工作的機制,讓每個核心的Tasks可以透過WorkQueue機制把要延遲處理/指定處理器/指定延遲時間的工作,交派給WorkQueue.
在實際的應用上,WorkQueue還可以用以實現中斷的BottomHalf機制,讓中斷觸發時對Timing要求高的部分(TopHalf)可以在Interrupt Handler中盡快執行完畢,透過WorkQueue把需要較長時間執行的部分(BottomHalf)交由WorkQueue延遲執行實現. (等同於RTOS下的LISR/HISR).
Linux Kernel的基礎Tasks.
Linux Kernel中有三個最基礎的Tasks,分別為PID=0Idle Task,PID=1負責初始化所有使用者環境與行程的 init Task,PID=2負責產生Kernel Mode行程的 kthreadd Task.
其中,Idle Task主要用來在系統沒有其他工作執行時,可以執行省電機制(PM Idle)或透過 Idle Migration把多核心其它處理器上的工作進行重分配(Load Balance),讓處於Idle的處理器可以分擔Task工作,充分利用系統運算資源.
init Task是所有User Mode Tasks的父行程,包含啟動時的Shell Script執行,或是載入必要的應用程式,都會基於init Task的執行來實現.
再來就是本文主要談的kthreadd Task,這是在Linux Kernel 2.6所引入的機制,在這之前要產生Kernel Thread需直接使用函式kernel_thread,而所產生的Kernel Thread父行程會是當下產生Kernel Thread的行程(或該父行程結束後,改為init Task(PID=1)). kthreadd的機制下,User ModeKernel Mode Task的產生方式做了調整,並讓kthreadd Task成為使用kthread_create產生的Kernel Thread統一的父行程也因此,在這機制實現下的Linux Kernel,屬於User Mode行程最上層的父行程為init Task (PID=1),而屬於Kernel Mode行程最上層的父行程為 kthreadd Task (PID=2),而這兩個行程共同的父行程就是 idle Task (PID=0).
kthreadd
kthreadd Kernel Thread主要實作在檔案kernel/kthread.c,入口函式為kthreadd (宣告為 int kthreadd(void *unused) ),主要負責的工作是檢視目前Linked List Queue “kthread_create_list”是否有要產生Kernel Thread的需求,若有,就呼叫函式create_kthread進行後續的產生工作而要透過kthreadd 產生Kernel Thread需求,就可以透過呼叫kthread_create (與其他衍生的kthread函式,ex:kthread_create_on_node….etc.)把需求加入到Linked List Queue “kthread_create_list”,WakeUp kthreadd Task,就可以使用目前kthreadd新設計的機制.
有關函式 kthreadd內部的運作流程,概述如下
1,執行set_task_comm(tsk, “kthreadd”),設定Task的執行檔名稱,會把“kthreadd”複製給task_struct中的 comm (struct task_struct宣告在 include/linux/sched.h).
2,執行ignore_signals(tsk) ,其中 tsk = current,會設定讓Task kthreadd 忽略所有的Signals
3,設定kthreadd可以在所有處理器上執行.
4,進入kthreadd的 for(;;) 無窮迴圈,
4-1,透過函式list_empty確認kthread_create_list是否為空,若為空,就觸發Task排程
4-2,透過list_entry,取出要產生的Kernel Thread 的”struct kthread_create_info” Pointer
4-3,呼叫create_kthread產生Kernel Thread.
4-3-1,create_kthread中會以“pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);”呼叫函式kernel_thread (in arch/arm/kernel/process.c),其中,入口函式為kthread,新產生的Task的第一個函式參數為create.
4-3-1-1,在函式kernel_thread,會執行如下的程式碼,把最後新產生的Kernel Thread入口函式指給新Task的暫存器r5,要傳遞給該入口函式的變數只給暫存器r4,而該入口函式結束時要返回的函式kernel_thread_exit位址指給暫存器r7,並透過透過do_fork產生新的行程時,暫存器PC (Program Counter)指向函式kernel_thread_helper. 也就是說每一個Kernel Thread的第一個函式統一都是 kernel_thread_helper,而結束函式統一都為kernel_thread_exit. 函式 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_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;
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
}
新產生的Task會執行函式kernel_thread_helper,並把結束函式由暫存器r6指給暫存器LR(Linker Register),把入口函式的第一個參數指給暫存器r0,Task的入口函式由暫存器r5指給暫存器PC,開始新Task的執行.有關函式 kernel_thread_helper的參考程式碼如下所示
extern void kernel_thread_helper(void);
asm( “.pushsection .text\n”
” .align\n”
” .type kernel_thread_helper, #function\n”
“kernel_thread_helper:\n”
#ifdef CONFIG_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”,第一個函式參數統一為”struct kthread_create_info *create”,檢視函式kthread的實作,可以看到在新行程由kernel_thread_helper呼叫進入kthread,就會執行函式參數create中的create->threadfn函式指標,執行其他應用透過kthread_create 產生Kernel Thread時的最終函式入口,參考代碼如下所示
static int kthread(void *_create)
{
/* Copy data: it’s on kthread’s stack */
struct kthread_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_create vs kernel_thread
kernel_thread函式,kthreadd機制產生前,要使用Kernel Thread主要的方式,而根據前述的介紹,可以看到其實kthread_create也是透過函式kernel_thread實現.
如果我們選擇直接透過kernel_thread產生Kernel Thread,跟透過kthreadd機制相比,兩者的差別在於,一個是由當下呼叫的kernel_threadTask行程所fork出來的,採用kthread_create機制則是由kthreadd Task行程所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所自行產生的Kernel Thread,insmod指令結束後,這個Kernel ThreadParent Task為 PID=1 (也就是 init Task.).
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宣告為
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node,
const char namefmt[],
)
用以產生與命名Kernel Thread,並可在支援NUMANon-Uniform Memory Access Architecture)的核心下,透過node可以設定要在哪個處理器上執行所產生的Kernel Thread. (會透過設定task->pref_node_fork),產生後的行程會等待被函式wake_up_process喚醒或是被kthread_stop所終止.
kthread_create參考檔案include/linux/kthread.h,
函式kthread_create的宣告如下
#define kthread_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的宣告如下
#define kthread_run(threadfn, data, namefmt, …) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
主要用以產生並喚醒Kernel Thread. (開發者可以省去要呼叫wake_up_process的動作.),且這呼叫是基於kthread_create,所以產生的Kernel Thread也不限於在特定的處理器上執行.
kthread_bind綁定Kernel Thread到指定的處理器,主要是透過設定CPU Allowed Bitmask,所以在多核心的架構下,就可以指定給一個以上的處理器執行.
kthread_stop用來暫停透過kthread_create產生的Kernel Thread,會等待Kernel Thread結束,並傳回函式threadfn(=產生Kernel Thread的函式)的返回值.
透過 “wait_for_completion(&kthread->exited); ” 等待 Kernel Thread結束,並透過 “int ret;….ret = k->exit_code;…..return ret; “取得該結束的Kernel Thread返回值,傳回給函式kthread_stop的呼叫者.
kthread_should_stop用來在Kernel Thread中呼叫,如果該值反為True,就表示該Kernel Thread X有被透過呼叫函式kthread_stop指定結束,因此,Kernel Thread X就必須要準備進行Kernel Thread的結束流程.
基於kthreadd產生的Kernel Tasks
接下來,我們把在Linux Kernel,基於kthread產生的Kernel Tasks做一個概要的介紹,藉此可以知道Linux Kernel有哪些模組基於這機制實現了哪些核心的能力包括檔案系統 Journaling機制,Inetrrupt BottomHalf,Kernel USB-Hub,Kernel Helper..,都是基於這機制下,所衍生出來的核心實作.
在這段落會頻繁提及的Work Queue可以有兩種實現方式,分別為
1, WorkQueue (實作在kernel/workqueue.c).
2, 產生Kernel Thread,並搭配 Linked List Queue (in include/linux/list.h)機制.
如下逐一介紹筆者認為值得說明的Kernel Thread與內部機制.
Kernel Tasks名稱
kworker參考檔案kernel/workqueue.c,kworker主要提供系統非同步執行的共用Worker Pool方案 (WorkQueue),一般而言會區分為每個處理器專屬的Worker Pool或是屬於整個系統使用的Worker Pool
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(Global CPU Workqueue) worker thread functionKernel Thread.
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的進一步討論,會在後續文章中介紹.
需要進一步資訊的開發者,可以自行參閱Linux Kernel 文件Documentation/workqueue.txt .
ksoftirqd參考檔案kernel/softirq.c,會在每個處理器進入CPU_UP_PREPARECPU_UP_PREPARE_FROZEN狀態時,在個別處理器產生ksoftirqd Kernel Thread,如下所示,在雙核心環境中會有兩個Kernel Thread各自在兩個處理器上執行.
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]
ksoftirqd Kernel Thread函式run_ksoftirqd,會透過
“while (!kthread_should_stop()) { …}”迴圈,確認目前是否有被執行終止動作,若無,就往後繼續執行並透過local_softirq_pending確認是否有Soft IRQ 被觸發,為了系統效能考量,目前Linux Kernel會把Top Halves 放在對應的中斷Routine中執行,而把屬於Bottom Halves 的部份透過kirqsoftd執行並且在ARM多核心的架構下,每個處理器都會有自己的Local IRQ,因此,如果發現有Local SoftIRQ待處理,就會由各自的處理器對應的ksoftirqd Kernel Thread負責執行Bottom Halves中的工作.
如果發現,待處理的CPU已經OffLine也會立刻結束ksoftirqd的執行而在ksoftirqd,會透過函式__do_softirq執行SoftIRQ Bottom Halves 的工作.
khelper參考檔案kernel/kmod.c,Kernel Init的過程中會呼叫函式usermodehelper_init,在這函式中就會執行 “khelper_wq = create_singlethread_workqueue(“khelper”);”產生khelper Kernel Thread.
Linux Kernel執行的過程中,就可以透過函式call_usermodehelper_exec執行
“queue_work(khelper_wq, &sub_info->work);”把工作指派給khelper WorkQueue.
例如,要載入一個Linux Kernel Module,就會透過函式__request_module,之後呼叫call_usermodehelper_fns並帶入modprobe路徑與相關參數作為函式參數(函式call_usermodehelper_fnsinline實作宣告在include/linux/kmod.h),最後會呼叫進入call_usermodehelper_exec,把工作放到khelper WorkQueue,來透過khelper Kernel Thread帶起User Mode的應用程式執行.
如下為khelper在筆者環境中產生的行程資訊
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2 11 0 0 ? -1 S< 0 0:00 \_ [khelper]
而每一個要放到khelper WorkQueue的工作,都會透過函式call_usermodehelper_setup 執行“INIT_WORK(&sub_info->work, __call_usermodehelper); ” 設定WorkQueue要執行的Work函式 (khelper Work函數固定設為__call_usermodehelper),外部執行檔路徑與相關參數.
因此,一旦khelper Kernel ThreadWorkQueue中執行到新工作時,就會呼叫函式__call_usermodehelper,最後透過函式____call_usermodehelper,執行核心函式kernel_execve,來達成執行User-Mode 應用程式的目的(這也就是如何由核心去執行外部的 Linux Device Driver Module工具的執行路徑,清楚khelper的行為與機制,對了解Linux Drivers載入/運作原理會很有幫助.)
kblockd參考檔案block/blk-core.c,如同khelper,這同樣是透過WorkQueue產生的Kernel Thread,init Call中呼叫genhd_device_init函式時,會執行blk_dev_init並執行 “kblockd_workqueue = alloc_workqueue(“kblockd”,WQ_MEM_RECLAIM | WQ_HIGHPRI, 0);” 產生kblockd Kernel 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_workkblockd WorkQueue進行工作的指派.
kseriod參考檔案drivers/input/serio/serio.c,
參考Kernel Source Code, Serio Linux Kernel是對“Serial I/O “周邊的支援模組,只要有使用到Serial I/O的輸入裝置(Input Device),例如:AT keyboard, PS/2 mouse, joysticks搖桿..etc 都屬此類.
也因此,屬於Serio的輸入裝置,底層就可以包括RS232(Com Port),使用i8042 Controller晶片的ATPS/2 鍵盤/滑鼠,使用ct82c710 Controller晶片的QuickPort滑鼠…etc. Serio底層可支援的Controller很豐富,在此只列舉部分底層控制晶片,有興趣的開發者請自行參考Linux Kernel Source.
啟動時,會在Kernel Init執行到init Call時呼叫serio_init函式,並執行“serio_task = kthread_run(serio_thread, NULL, “kseriod”);”產生kseriod Kernel Thread,以函式serio_threadKernel Thread的執行函式
static int 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());
return 0;
}
只要serio_queue_event被呼叫到(像是Event SERIO_RESCAN_PORT/SERIO_RECONNECT_CHAIN/SERIO_REGISTER_PORT/SERIO_ATTACH_DRIVER/SERIO_RECONNECT_PORT)就會把Add新的EventLinked List Queue,並喚醒 kseriod Kernel Thread處理對應Serio Event.
如下為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產生的Kernel Thread,是透過執行“workqueue = create_singlethread_workqueue(“kmmcd”);”產生的kmmcd Kernel 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,
啟動時,會在Kernel Init執行到init Call時呼叫kswapd_init函式,並進入kswapd_run,執行“pgdat->kswapd = kthread_run(kswapd, pgdat, “kswapd%d”, nid);”產生kswapd Kernel Thread,並以函式kswapd (宣告:static int kswapd(void *p) )Kernel Thread的執行函式如下為函式kswapd_init的內容,swap_setup (in mm/swap.c)之後,會透過for_each_node_state (in include/linux/nodemask.h),
static int __init kswapd_init(void)
{
int nid;
swap_setup();
for_each_node_state(nid, N_HIGH_MEMORY)
kswapd_run(nid);
hotcpu_notifier(cpu_callback, 0);
return 0;
}
其中 for_each_node_state宣告如下,
#define for_each_node_state(__node, __state) \
for_each_node_mask((__node), node_states[__state])
Linux而言,在多核心的架構下,可以支援以Node方式去Group CPU,也就是說每個Node可以擁有一個以上的處理器,而在這就會選擇 NodeHIGH_MEMORY State有被設定的Node. 也就是說,如果該CPU NodeHIGH_MEMORY State有被設定,就會執行kswapd_run,並把該Node Id帶入成為nid的值. (也就是最後kswapd後面的數字).
筆者對Linux Kernel HighMemory的理解是,當所配置的實體記憶體大於Linux Kernel虛擬記憶體範圍(通常在ARM上是 0xc0000000-0xffffffff1GB的範圍),而因為受限於 Memory Mapped I/O或實際上的Kernel需要,有部分所需的實體記憶體無法被Mapped到這1GB虛擬記憶體空間中,此時就會需要Kernel Enable High Memory的能力目前在ARM Linux Kernel中也有支援High Memory,並可針對High Memory需求配置2nd-level PageTables. 在沒開啟High MemoryLinux Kernel, Node StatesN_HIGH_MEMORY 的值會等於 N_NORMAL_MEMORY,也就是會讓Normal MemoryHigh Memory的設定一致一般而言,在考慮系統效能時,並不建議開啟High Memory能力前提也是硬體設計時,也要避免遇到這樣的問題,如果選擇把Kernel Space加大(例如從1G/3G變成2G/2G),則會限制到需要大量記憶體應用程式的執行.
這有一篇關於Linux Kernel 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]
而在kswapd Kernel Thread的函式kswapd,主要進行縮減 Slab Cache (例如檔案系統快取),並在Free Pages數量過低時,把記憶體Swap Out到磁碟中,會透過函式balance_pgdat對每個處理器Node(這定義是針對多核心的內化設計.)的記憶體區域(Zone)進行記憶體釋放,直到所有記憶體區域的Free Pages都達到High WaterMark (透過呼叫high_wmark_pages確認),也就是“free_pages > high_wmark_pages(zone)”.
參考include/linux/mmzone.h,有關Memory Zone Water Marker相關巨集宣告如下
#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])
最後,附帶補充一下,Linux Kernel本身支援在每次透過Page Allocate配置記憶體時(Source Codemm/page_alloc.c),透過核心的OOM模組(Source Code mm/oom_kill.c),評估目前記憶體資源是不是快耗盡,如果是就會透過函式mem_cgroup_out_of_memory呼叫select_bad_process選擇一個透過badness計算後,要被終止的行程,然後呼叫函式oom_kill_process,最後在函式__oom_kill_task中發送SIGKILL Signal強制終止行程執行.
Android本身也有在Linux Kernel中加入 Low Memory Killer模組(並關閉Linux Kernel原本的OOM),啟動時會透過函式register_shrinker(in mm/vmscan.c)註冊自己的lowmem_shrinker服務.
kstriped參考檔案drivers/md/dm-stripe.c,
會在執行dm_stripe_init,透過“kstriped = create_singlethread_workqueue(“kstriped”);”產生執行 WorkQueue kernel Thread,並在函式stripe_ctr中透過“INIT_WORK(&sc->kstriped_ws, trigger_event);”設定函式 trigger_eventWorkQueueWork函式,並設定相關Strip所需參數在函式trigger_event,會再呼叫dm_table_event(in drivers/md/dm-table.c)然後執行event_fn.
MD (Multiple Device)主要用以支援把多個裝置虛擬為單一裝置,一般會應用在Software RAID 或 LVM (Logical Volume Management)的功能上.同時,參考Linux Kernel文件Documentation/device-mapper/striped.txt, dm-stripe (in Device-Mapper)主要用以支援像是RAID0Striped儲存裝置,可以把要寫入的資料,分割為不同的Chunks循序且循環的寫入到多個底層的儲存裝置中,由於是把單筆資料分割為不同的Chunks個別對多個裝置寫入,因此可以大幅改善儲存媒體寫入的效率. (相對於只對單一裝置寫入,把一筆資料分割後同時對多個裝置寫入可以縮短寫入等待的時間).
目前Linux KernelSoftware RAID 可支援軟體把多個磁碟Partition整合為一個邏輯磁碟,並支援像是RAID1,4 5,藉此避免硬碟損壞時造成的資料損失.不過在重視效能的環境中,Hardware RAID還是比較有效率的.
如下為kstriped在筆者環境中產生的行程資訊
USER PID PPID VSIZE RSS WCHAN PC NAME
root 23 2 0 0 c004b2c4 00000000 S kstriped
Android本身也有支援對Device-Mapper的操作,細節就不在本文範圍中,各位可以參考2.3 Source Code “gingerbread/system/vold/Devmapper.cpp”.
kjournald參考檔案fs/jbd/journal.c ,會透過執行 “t = kthread_run(kjournald, journal, “kjournald”);”,產生支援 Ext3 檔案系統JournalingKernel Thread. 或參考檔案fs/jbd2/journal.c,會透過執行“t = kthread_run(kjournald2, journal, “jbd2/%s”,journal->j_devname);”,產生支援Ext4OCFS2檔案系統,並可延伸支援更大空間64 bits block numbers機制的Journaling Kernel Thread.
以支援Ext3kjournald來說,主要負責兩類工作,
1,Commit: 用以把Joirnaling FileSystem所產生的MetaData寫回對應的檔案系統位置用以正式的把資料完成寫入動作,並可釋出MetaData的空間.
2,CheckPoint: 會由這個Thread執行查核點的動作,Journaling Log內容回寫,以便這些空間可以重複利用.
啟動流程為,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 產生kjournald Kernel Thread,之後進入 “wait_event(journal->j_wait_done_commit, journal->j_task != NULL);” 等待kjournald Kernel Thread產生完畢,呼叫“wake_up(&journal->j_wait_done_commit);” 讓函式journal_start_thread結束執行.
有關kjournald會被Timer WakeUp起來執行Commit的流程,其中Interval的設定如下 “journal->j_commit_interval = (HZ * JBD_DEFAULT_MAX_COMMI
T_AGE);” (in fs/jbd/journal.c), HZ為每秒觸發的Tick中斷次數(在筆者環境為100). 同時參考檔案 “include/linux/jbd.h” 中有關JBD_DEFAULT_MAX_COMMI
T_AGE的設定為5. (“#define JBD_DEFAULT_MAX_COMMIT_AGE 5″),也就是說kjournald Kernel Thread會被Timer喚醒的Interval值為5.
函式kjournald (原型為 “static int kjournald(void *arg)”),Kernel Thread的主函式,會執行如下的動作,
1,設定Timer (呼叫“setup_timer(&journal->j_commit_timer, commit_timeout,(unsigned long)current);”),固定週期透過Callback函式commit_timeout喚醒kjournald Kernel 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_FREEZE bit是否為1 (TIF_FREEZE=19). (例如可以透過函式freeze_taskTask發出freeze request ).
2-4-1,若上述成立,就會進入函式refrigerator (in kernel/freezer.c),並進入for(;;)迴圈中,直到frozen(current)不成立,才會結束迴圈,把函式執行完畢.
2-5,反之,2-4條件不成立,就會等待journal->j_wait_commit Wait Event,或判斷是否可以進入等待,例如還有待回寫的Log沒有執行完畢 (“if (journal->j_commit_sequence != journal->j_commit_request) “),或有執行中的Transaction且現在的系統Tick值已經大於等於原本kjournald所設定的Timer要觸發TimeOut的時間值 (也就是說已經發生過TimeOut),若上述條件成立,就會讓系統繼續執行下去,而不進入等待.
2-6,kjournald處於執行狀態 (有可能是因為 TimeOut或是journal->j_wait_commit Wait Event喚醒),透過比對現在的Tick值是否大於等於Timer TimeOut(transaction->t_expires),確認是否為透過 Timer喚醒.
2-7,重複回到2-1,繼續kjournald Kernel Thread的執行.
參考下圖,為整個kjournald Kernel Thread與 Timer運作的示意圖.

events參考檔案kernel/workqueue.c,
events Kernel Thread主要提供核心延後工作執行的方式,如前述的實作介紹,有的應用會透過自己建立的WorkQueue來實現這樣的設計,或是也可以透過Global WorkQueue來達成目的,在啟動過程中會透過函式 init_workqueues來初始化 keventd_wq (也就是keventd WorkQueue).參考如下程式碼,
void __init init_workqueues(void)
{
..
keventd_wq = create_workqueue(“events”);
.
}
當有需要使用預設的WorkQueue,可以透過函式schedule_work,把要處理的工作放到Global Work Queue,這函式參考實作如下
int schedule_work(struct work_struct *work)
{
return queue_work(keventd_wq, work);
}
如果希望放到Global WorkQueue並加入Delayed,可以透過函式schedule_delayed_work ,參考實作如下
int schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work(keventd_wq, dwork, delay);
}
或透過函式schedule_work_on 把工作指派到指定的處理器上,參考實作如下
int schedule_work_on(int cpu, struct work_struct *work)
{
return queue_work_on(cpu, keventd_wq, work);
}
也可以透過函式schedule_delayed_work_on 指定在所要的處理器上,並附帶Delayed,參考實作如下
int schedule_delayed_work_on(int cpu,
struct delayed_work *dwork, unsigned long delay)
{
return queue_delayed_work_on(cpu, keventd_wq, dwork, delay);
}
有興趣的開發者,也可以參考這篇Linux Kernel Korner文章 “Kernel Korner – 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決定當Dirty Pages超過多少比率時,便執行回寫到檔案系統的動作.(在筆者的環境中為10%).
在運作時,會透過函式 start_one_pdflush_thread產生 pdflush Krnel Thread,參考如下程式碼
static void start_one_pdflush_thread(void)
{
struct task_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產生新的pdflush Kernel Thread. (總量不超過MAX_PDFLUSH_THREADS).
Migration參考檔案kernel/stop_machine.c,
在多核心的架構下,Migration在意義上主要是讓運作在處理器#A的行程可以轉移到處理器#B或其它處理器上.但在目前筆者使用的Linux Kernel,Migration主要為當處理器進行CPU Down流程時,負責執行Callback函式,讓屬於要停止的處理器上的Tasks可以轉移到還能持續運作的處理器上.
Migration Kernel Thread主要的任務為在多核心的架構下,支援Stop CPU的行為,也因此可以視這個Kernel ThreadStopper Kernel Thread,
當處理器進行CPU Down,就會透過這個Kernel Thread呼叫所指定Callback Functions.
如之前介紹到的其它模組,在多核心的處理器上,會產生對應的 Migration Kernel Thread,例如: [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,migration Kernel Thread執行函式migration_cpu_stop來進行Task轉移的工作.
2,CPUAllowed Bitmask被修改,且該Task正位於不被允許的處理器上
例如,當使用者透過set_cpus_allowed_ptr(in kernel/sched.c)修改TaskCPU Allowed Bitmask,會執行 “if (cpumask_test_cpu(task_cpu(p), new_mask)) “確認目前所在的處理器是否符合新的Bitmask設定,若所在的處理器並非允許執行的處理器,就會執行“stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg);”透過函式stop_one_cpu,migration Kernel 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_NEWIDLE flag是否成立,若成立就會執行“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 ,migration Kernel Thread執行函式active_load_balance_cpu_stop,Task從目前最忙碌的處理器移到Idle的處理器上有關處理器Task Balance的動作,值得討論的事項很多,後續有機會再加以詳細說明.
4,當處理器被關閉時 (在有支援HotPlug的環境下,進行CPU Down)
例如:進行CPU Down時會透過函式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 (in kernel/stop_machine.c)呼叫stop_cpus (in kernel/stop_machine.c),進入__stop_cpus (in kernel/stop_machine.c),執行函式cpu_stop_queue_work,把工作透過list_add_tail(&work->list, &stopper->works); 放到stopper->works Queue,migration Kernel Thread執行函式take_cpu_down (in kernel/cpu.c)完成CPU Down的流程.
上述四個條件,是在目前Linux Kernel,會透過 migration Kernel Thread執行的情況.
在啟動過程中,會呼叫函式cpu_stop_init,並以啟動的處理器作為Boot CPU來呼叫函式cpu_stop_cpu_callback,執行CPU_UP_PREPARE的動作,會以函式cpu_stopper_thread做為migration Kernel Thread的執行起點,並以函式kthread_bind設定該Thread只能在目前產生該Kernel Thread的處理器上執行可以參考如下程式碼.
static int __cpuinit cpu_stop_cpu_callback(struct notifier_block *nfb,
unsigned long action, void *hcpu)
{
unsigned int cpu = (unsigned long)hcpu;
struct cpu_stopper *stopper = &per_cpu(cpu_stopper, cpu);
struct task_struct *p;
switch (action & ~CPU_TASKS_FROZEN) {
case CPU_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))
return notifier_from_errno(PTR_ERR(p));
get_task_struct(p);
kthread_bind(p, cpu);
sched_set_stop_task(cpu, p);
stopper->thread = p;
break;
.…..
migration Kernel Thread會在CPU_UP_PREPARE事件中產生,並在CPU_ONLINE事件被WakeUp與設定“stopper->enabled = true;”.
在有支援CPU HotPlug的環境中,會有額外的CPU_UP_CANCELEDCPU_POST_DEAD事件,會在透過函式kthread_stopmigration Kernel Thread結束.
而在函式cpu_stopper_thread,就會進入一個goto的迴圈中,確認stopper->works Queue中是否有被指派的工作,若有就會執行該工作對應的Callback函式.
其他常見的Kernel Thread還包括suspend,cqueue,aio,mtdblockd,hid_compat,rpciod,mmcqd….etc,就不在這多做介紹,有興趣的開發者請自行參閱Linux Kernel Source Code.
結語
本文基本上以 kthreadd Task為起點,探討了一部分以此為基礎的核心模組,然而由於涉及的領域很廣泛,筆者主要以自己認為值得加以說明的模組進行介紹,對核心解析有熱情的開發者,建議可以自行進一步的Hacking.
有關Linux Kernel啟動流程的介紹,就以本文告結.

















0 意見:

張貼留言