Linux內核線程

本文以ARM架構爲例,講解linux的內核線程是如何創建的。

Linux內核在完成初始之後,會把控制權交給應用程序。只有當硬件中斷、軟中斷、異常等發生時,CPU纔會從用戶空間切換到內核空間來執行相應的處理,完成後又回來用戶空間。

如果內核需要週期性地做一些事情(比如頁面的換入換出,磁盤高速緩存的刷新等),又該怎麼辦呢?內核線程(內核進程)可以解決這個問題。

內核線程(kernel thread)是由內核自己創建的線程,也叫做守護線程(deamon)。在終端上用命令"ps -Al"列出的所有進程中,名字以k開關以d結尾的往往都是內核線程,比如kthreadd、kswapd。

內核線程與用戶線程的相同點是:

  • 都由do_fork()創建,每個線程都有獨立的task_struct和內核棧;
  • 都參與調度,內核線程也有優先級,會被調度器平等地換入換出。

不同之處在於:

  • 內核線程只工作在內核態中;而用戶線程則既可以運行在內核態,也可以運行在用戶態;
  • 內核線程沒有用戶空間,所以對於一個內核線程來說,它的0~3G的內存空間是空白的,它的current->mm是空的,與內核使用同一張頁表;而用戶線程則可以看到完整的0~4G內存空間。

在Linux內核啓動的最後階段,系統會創建兩個內核線程,一個是init,一個是kthreadd。其中init線程的作用是運行文件系統上的一系列"init"腳本,並啓動shell進程,所以init線程稱得上是系統中所有用戶進程的祖先,它的pid是1。kthreadd線程是內核的守護線程,在內核正常工作時,它永遠不退出,是一個死循環,它的pid是2。

內核初始化工作的最後一部分是在函數rest_init()中完成的。在這個函數中,主要做了4件事情,分別是:創建init線程,創建kthreadd線程,執行schedule()開始調度,執行cpu_idle()讓CPU進入idle狀態。經過簡化的代碼如下:

  [c]
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
schedule();
cpu_idle();
}
[/c]

內核線程的創建過程比較曲折,讓我們一步一步來看。

創建內核線程的入口函數是kernel_thread,定義如下:

  [c]
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);
}
[/c]

它的第一個參數是線程所要執行的函數的指針,第二個參數是線程的參數,第三個是線程屬性。

在kernel_thread()函數中先是準備一些寄存器的值,並保存起來。然後執行了do_fork()來複制task_struct內容,並建立起自己的內核棧。在kernel_thread() > do_fork() > copy_process() > copy_thread()函數調用中,有一個很重要的操作需要留意一下:

  [c]
int
copy_thread(unsigned long clone_flags, unsigned long stack_start,
unsigned long stk_sz, struct task_struct *p, struct pt_regs *regs)
{
struct thread_info *thread = task_thread_info(p);
......
memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
thread->cpu_context.sp = (unsigned long)childregs;
thread->cpu_context.pc = (unsigned long)ret_from_fork;
......
return 0;
}
[/c]

注意這裏把cpu_context中保存的pc寄存器值設爲ret_from_fork函數的地址,這在後面調度的時候會用到。

注:前面的這兩段代碼中都有設置pc寄存器,但是所設的內容是不同的:在kernel_thread()中設置的regs.ARM_*值最後會被壓入內核棧,是在context_switch完成之後待要運行的目標代碼;而在copy_thread()中設置的sp和pc則是thread_info結構中cpu_context的值,是在context_switch()過程中要用的。

rest_init()中兩次調用過kernel_thread()之後,就分別創建好了init和kthreadd內核線程的運行上下文,並已經加入了運行隊列,隨時可以運行了。

接下來在schedule()裏面最終會運行到switch_to()做上下文切換,這個函數的實現細節在此前的文章中已經講過,不再贅述,這裏只說我們的場景。在switch_to()完成之後,新線程的sp寄存器已經切換到線程自己的棧上,新線程的pc則成了ret_from_fork。

接下來新線程就跳轉到ret_from_fork()函數繼續執行。ret_from_fork()是用匯編代碼來寫的,用於fork系統調用(軟中斷)完成後的收尾工作。中斷的收尾工作最後都會要完成一件事情,就是恢復原先運行的“用戶”程序狀態,即彈出設置內核棧上所保存的各寄存器值。而我們此前保存在這裏的pc寄存器指向的是函數kernel_thread_helper()的地址,這個函數是用匯編寫的:

  [c]
extern void kernel_thread_helper(void);
asm( ".pushsection .text\n"
" .align\n"
" .type kernel_thread_helper, #function\n"
"kernel_thread_helper:\n"
" 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");
[/c]

這段代碼把pc值設爲r5,在kernel_thread()中我們已經把r5設爲線程的目標函數的值,而返回地址寄存器lr被設爲r6,即此前設置的kernel_thread_exit()函數地址。

所以,接下來內核線程將會被正式啓動,如果線程退出(即線程函數運行結束)的話,kernel_thread_exit()會做掃尾工作。

到這裏,我們已經講完了內核線程啓動的整個過程。最後我們看一下剛剛啓動起來的兩個內核線程都做了哪些事情:

init線程:

  [c]
static int __init kernel_init(void * unused)
{
......
init_post();
}
static noinline int init_post(void) __releases(kernel_lock)
{
......
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");

panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
[/c]

在init線程中,將運行完"/sbin/init"、"/etc/init"和"/bin/init"三個腳本,並啓動shell。run_init_process("/bin/sh")並不會返回,init線程就停在這裏,以後所有的應用程序進程都將從/bin/sh克隆,而sh來自init內核線程,所以init線程最終成爲所有用戶進程的祖先。

kthreadd線程:

  [c]
int kthreadd(void *unused)
{
for (;;) {
if (list_empty(&kthread_create_list))
schedule();
while (!list_empty(&kthread_create_list)) {
create_kthread(create);
}
}
return 0;
}
[/c]

可見,在每一次循環裏kthreadd只做兩件事:如果有其它的內核線程需要創建,就調用create_kthread()來逐個創建;如果沒有就調用schedule()把自己換出CPU,讓別的線程進來運行。

在內核線程創建過程中還有兩個有趣的細節值得說一下:

  1. 雖然init線程是在kthreadd之前創建的,pid也比較小,但是在schedule()的時候,最先被選中先運行的是kthreadd。這不會有任何影響,因爲kthreadd總會讓出CPU,init線程一定能啓動。
  2. 進程號PID的分配是從0開始的,但是在"ps"命令中看不到0號進程。這是因爲0號pid被分給了“啓動”內核進程,就是完成了系統引導工作的那個進程。在函數rest_init()中,0號進程在創建完成了init和kthreadd兩個內核線程之後,調用schedule()使得pid=1和2的兩個線程得以啓動,但是pid=0的線程並不參與調度,所以這個進程就再也得不到運行了。如下所示,在我們前面已經看到過的這段代碼中,schedule()不會返回,最後一行的cpu_idle()其實是不會被運行到的:
  [c]
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
schedule();
cpu_idle();
}
[/c]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章