加入收藏 | 设为首页 | 会员中心 | 我要投稿 广州站长网 (https://www.020zz.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

linux内核线程详解,Linux内核线程

发布时间:2022-12-16 15:33:50 所属栏目:Linux 来源:未知
导读: 本文以ARM架构为例,讲解linux的内核线程是如何创建的。
Linux内核在完成初始之后,会把控制权交给应用程序。只有当硬件中断、软中断、异常等发生时,CPU才会从用户空间切换到内核空间来执

本文以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进程linux 线程,所以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,让别的线程进来运行。

在内核线程创建过程中还有两个有趣的细节值得说一下:

虽然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()其实是不会被运行到的:

[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]

(编辑:广州站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!