一文带你搞懂Linux内核之内核线程(二)超详细~

您所在的位置:网站首页 nt内核和linux内核 一文带你搞懂Linux内核之内核线程(二)超详细~

一文带你搞懂Linux内核之内核线程(二)超详细~

2022-05-30 08:47| 来源: 网络整理| 查看: 265

虽然讲解完了内核线程的创建过程,但是似乎又少点什么,那么下面我们来看两个细节:内核线程执行处理函数和内核线程上下文切换细节:

7.内核线程执行处理函数细节

内核线程执行到处理函数要从fork说起:

7.1 fork准备调度上下文

上面fork 对于创建内核线程已经注释的很清楚,这是为内核线程第一次被调度执行做准备。

7.2 使用调度上下文

当内核线程被唤醒,在合适的时机被调度时,会执行如下内核路径:

会将内核线程的 p->thread.cpu_context.pc 恢复到pc,然后就执行了ret_from_fork:

首先调用schedule_tail对前一个进程进程收尾工作,然后就判断x19寄存器的值是否为0, 其实有一个细节在copy_thread中首先就对p->thread.cpu_context做了清零操作

上面copy_thread中我们已经看到对 p->thread.cpu_context.x19 设置为了线程执行函数,调度的时候,设置进了x19 中,接着ret_from_fork将 x20赋值到x0, 就做了内核线程参数的传递动作,接着就执行958行跳转到了线程执行函数中执行了,于是新创建的内核线程才开始真正的欢乐执行。

【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)    

8.内核线程上下文切换细节

现在来说下内核线程进行上下文切换时的技术细节:

8.1 关于mm_struct的借用

我们知道内核线程比较特殊没有用户地址空间的概念,共享内核地址空间,而mm_struct结构专门用来描述用户地址空间的,我们知道对于arm64架构来说,有两个页表基址寄存器ttbr0_el1和ttbr1_el1, ttbr0_el1用来存放用户地址空间的页表基地址,在每次调度上下文切换的时候从tsk->mm->pgd加载,ttbr1_el1是内核地址空间的页表基地址,内核初始化完成之后存放swapper_pg_dir的地址。

所以,当切换下一个任务为内核线程的时候不需要切换用户地址空间:

内核线程的tsk->mm永远为空,因为它没有用户地址空间的概念,但是他在调度的时候需要借用前一个用户任务的active_mm赋值到自己的active_mm,为什么要这样做呢?

这个问题可能很多人都想搞明白,那就还从地址空间切换说起:我们知道对于用户任务来说(用户进程或者线程),他们的tsk->mm = tsk->active_mm,并且在fork的时候已经分配好mm_struct结构,而且申请好了私有的pgd页赋值到tsk->mm->pgd。如果是user1 -> user2 这样的切换,因为是两个不同的用户进程,所以必须切换地址空间,但是如果是user1 -> kernel1 ->user1 这样的情况会是怎样?首先我们知道的是:user1 -> kernel1的时候不需要切换地址空间,但是需要做kernel1->active_mm = user1->active_mm的处理,而当 kernel1 ->user1切换时,情况就不一样了,这个时候next->mm!= NULL, 所有会走下面的逻辑switch_mm_irqs_off(prev->active_mm, next->mm, next):

switch_mm中,prev=prev->active_mm 而next= next->mm,在我们上面分析的场景user1 -> kernel1 ->user1,则prev= kernel1->active_mm =user1->active_mm=user1->mm,而next= user1->mm,可以发现两者相等,所以这种情况下是不需要切换地址空间的。 以下场景都不会导致地址空间切换:user1 -> kernel1 -> kernel2 -> kernel3 ->user1 user1 -> kernel1 -> user1 -> kernel2 -> kernel3

下图给出了地址空间切换图示:

我们只关注内核线程的切换情况,从Ub->ka->kb->Ub切换过程中,都不需要切换地址空间。

8.2 内核线程虚拟地址转换情况

下面我们来看下,内核线程虚拟地址转换的情况,我们都知道,对于用户任务,调度时会切换地址空间,即是将tsk->mm->pgd放到ttbr0_el1(对于arm64来说)中,我们访问用户虚拟地址的时候,mmu通过ttbr0_el1查询各级页表最终找到物理地址(当然mmu首先会从tlb中查询页表项查询不到才进行多级页表遍历),那么对于内核线程怎么办,它可没有tsk->mm结构,那么它是如何进程地址转换的呢?

答案就是:内核线程共享内核地址空间,也只能访问内核地址空间,使用swapper_pg_dir去查询页表就可以,而对于arm64来说swapper_pg_dir在内核初始化的时候被加载到ttbr1_le1中,一旦内核线程访问内核虚拟地址,则mmu就会从ttbr1_le1指向的页表基地址开始查询各级页表,进行正常的虚实地址转换。当然,上面是arm64这种架构的处理,它有两个页表基地址寄存器,其他很多处理器如x86, riscv处理器架构都只有一个页表基址寄存器,如x86的cr3,那么这个时候怎么办呢?答案是:使用内核线程借用的prev->active_mm来做,实际上前一个用户任务(记住:不一定是上一个,有可能上上个任务才是用户任务)的active_mm=mm,当切换到前一个用户任务的时候就会将tsk->mm->pgd放到cr3, 对于x86这样的只有一个页表基址寄存器的处理器架构来说,tsk->mm->pgd存放的是整个虚拟地址空间的页表基地址,在fork的时候会将主内核页表的pgd表项拷贝到tsk->mm->pgd对于表项中(有兴趣可以查看fork的copy_mm相关代码,对于arm64这样的架构没有做内核页表同步)。

9. 内核中创建内核线程用例

下面我们来看下,内核中创建内核线程为系统服务的用例,我们只提及不讲解具体的服务逻辑。

用例1:linux系统中,当内存不足时,会唤醒kswapd内核线程来进行异步内存回收,下面我们来看他的创建过程:

用例2:Linux软中断是下半部的一种机制,一般对效率要求较高的场景会使用到,如网卡收发包,每当上半部执行完了会执行到软中断,软中断会抢占进程上下文执行,但是如果软中断处理太频繁,会导致高优先级的进程得不到执行,所以在软中断执行的时候会对执行次数和执行时间做限制,会将超过限制的软中断处理推到ksoftirqd来执行。

我们来看下ksoftirqd内核线程的创建:

可以看到这里虽然没有使用kthread_run这样的api创建内核线程,但是还是和kthread_run实现一样将内核线程创建信息添加到kthread_create_list链表 然后唤醒kthreadd来创建内核线程,最后会绑定到对应的cpu上去。

10.实践环节

前面我们分析了内核线程的创建过程,也分析了很多的源代码,最后我们来实战一下,来使用内核的api来创建内核线程为我们服务(这里我们创建一个内核线程,然后每隔一秒打印一串字符 :I am kernel thread: 小写字母循环)。

内核模块代码:kthread_demo.c

Makefile代码:

测试:



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3