本文以ARM架构为例,讲解linux的内核线程是如何创建的。
Linux内核在完成初始之后,会把控制权交给应用程序。只有当硬件中断、软中断、异常等发生时,CPU才会从用户空间切换到内核空间来执行相应的处理,完成后又回来用户空间。
如果内核需要周期性地做一些事情(比如页面的换入换出,磁盘高速缓存的刷新等),又该怎么办呢?内核线程(内核进程)可以解决这个问题。
内核线程(kernel thread)是由内核自己创建的线程,也叫做守护线程(deamon)。在终端上用命令"ps -Al"列出的所有进程中,名字以k开关以d结尾的往往都是内核线程,比如kthreadd、kswapd。
内核线程与用户线程的相同点是:
不同之处在于:
在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(®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;
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, ®s, 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,让别的线程进来运行。
在内核线程创建过程中还有两个有趣的细节值得说一下:
[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]