内核初始化工作的最后一部分是在函数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(); <del>cpu_idle();</del> }