本文以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状态。经过简化的代码如下:
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();
}
内核线程的创建过程比较曲折,让我们一步一步来看。
创建内核线程的入口函数是kernel_thread,定义如下:
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);
}
它的第一个参数是线程所要执行的函数的指针,第二个参数是线程的参数,第三个是线程属性。
在kernel_thread()函数中先是准备一些寄存器的值,并保存起来。然后执行了do_fork()来复制task_struct内容,并建立起自己的内核栈。在kernel_thread()> do_fork() > copy_process() >copy_thread()函数调用中,有一个很重要的操作需要留意一下:
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;
}
注意这里把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()的地址,这个函数是用汇编写的:
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");
这段代码把pc值设为r5,在kernel_thread()中我们已经把r5设为线程的目标函数的值,而返回地址寄存器lr被设为r6,即此前设置的kernel_thread_exit()函数地址。
所以,接下来内核线程将会被正式启动,如果线程退出(即线程函数运行结束)的话,kernel_thread_exit()会做扫尾工作。
到这里,我们已经讲完了内核线程启动的整个过程。最后我们看一下刚刚启动起来的两个内核线程都做了哪些事情:
init线程:
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.");
}
在init线程中,将运行完"/sbin/init"、"/etc/init"和"/bin/init"三个脚本,并启动shell。run_init_process("/bin/sh")并不会返回,init线程就停在这里,以后所有的应用程序进程都将从/bin/sh克隆,而sh来自init内核线程,所以init线程最终成为所有用户进程的祖先。
kthreadd线程:
int kthreadd(void *unused)
{
for (;;) {
if (list_empty(&kthread_create_list))
schedule();
while (!list_empty(&kthread_create_list)) {
create_kthread(create);
}
}
return 0;
}
可见,在每一次循环里kthreadd只做两件事:如果有其它的内核线程需要创建,就调用create_kthread()来逐个创建;如果没有就调用schedule()把自己换出CPU,让别的线程进来运行。
在内核线程创建过程中还有两个有趣的细节值得说一下:
- 虽然init线程是在kthreadd之前创建的,pid也比较小,但是在schedule()的时候,最先被选中先运行的是kthreadd。这不会有任何影响,因为kthreadd总会让出CPU,init线程一定能启动。
- 进程号PID的分配是从0开始的,但是在"ps"命令中看不到0号进程。这是因为0号pid被分给了“启动”内核进程,就是完成了系统引导工作的那个进程。在函数rest_init()中,0号进程在创建完成了init和kthreadd两个内核线程之后,调用schedule()使得pid=1和2的两个线程得以启动,但是pid=0的线程并不参与调度,所以这个进程就再也得不到运行了。如下所示,在我们前面已经看到过的这段代码中,schedule()不会返回,最后一行的cpu_idle()其实是不会被运行到的:
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();
}