首页 > 学院 > 开发设计 > 正文

linux内核线程 [创建]

2019-11-14 09:17:33
字体:
来源:转载
供稿:网友

本文以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状态。经过简化的代码如下:

12345678staticnoinline 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,定义如下:

123456789101112131415pid_t kernel_thread(int(*fn)(void *), void *arg, unsignedlong flags){structpt_regs regs;memset(&regs, 0,sizeof(regs));regs.ARM_r4 = (unsignedlong)arg;regs.ARM_r5 = (unsignedlong)fn;regs.ARM_r6 = (unsignedlong)kernel_thread_exit;regs.ARM_r7 = SVC_MODE | PSR_ENDSTATE | PSR_ISETSTATE;regs.ARM_pc = (unsignedlong)kernel_thread_helper;regs.ARM_cpsr = regs.ARM_r7 | PSR_I_BIT;returndo_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);}

它的第一个参数是线程所要执行的函数的指针,第二个参数是线程的参数,第三个是线程属性。

在kernel_thread()函数中先是准备一些寄存器的值,并保存起来。然后执行了do_fork()来复制task_struct内容,并建立起自己的内核栈。在kernel_thread() > do_fork() > copy_PRocess() > copy_thread()函数调用中,有一个很重要的操作需要留意一下:

123456789101112intcopy_thread(unsignedlong clone_flags, unsignedlong stack_start,unsignedlong stk_sz, struct task_struct *p,struct pt_regs *regs){structthread_info *thread= task_thread_info(p);......memset(&thread->cpu_context, 0,sizeof(structcpu_context_save));thread->cpu_context.sp = (unsignedlong)childregs;thread->cpu_context.pc = (unsignedlong)ret_from_fork;......return0;}

注意这里把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()的地址,这个函数是用汇编写的:

1234567891011externvoid 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线程:

12345678910111213141516staticint __init kernel_init(void* unused){......init_post();}staticnoinline intinit_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线程:

1234567891011int kthreadd(void *unused){for(;;) {if(list_empty(&kthread_create_list))schedule();while(!list_empty(&kthread_create_list)) {create_kthread(create);}}return0;}

可见,在每一次循环里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()其实是不会被运行到的:
12345678staticnoinline 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>}

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表