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

来源:互联网 发布:office mac 迅雷下载 编辑:程序博客网 时间:2024/05/20 08:00

http://loda.hala01.com/2011/09/android-%E7%AD%86%E8%A8%98-linux-kernel-smp-symmetric-multi-processors-%E9%96%8B%E6%A9%9F%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90-part4-linux-%E5%A4%9A%E6%A0%B8%E5%BF%83%E5%95%9F%E5%8B%95%E6%B5%81/


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 的工作.

若有進一步興趣的開發者,也可以參考這篇文章http://book.chinaunix.net/special/ebook/oreilly/Understanding_Linux_Network_Internals/0596002556/understandlni-CHP-9-SECT-3.html .

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啟動流程的介紹,就以本文告結.


原创粉丝点击