进程上下文切换

上下文切换动作,来保存上一个进程的“上下文”,恢复下一个进程的“上下文”,主要包括进程地址空间切换和处理器状态切换

注:这里的上下文实际上是指进程运行时最小寄存器的集合。

如果切换的next进程不是同一个进程,才进行切换:

__schedulei  f (likely(prev != next)) {      ...context_switch  //进程上下文切换}

进程地址空间切换

进程地址空间切换就是切换虚拟地址空间,使得切换之后,当前进程访问的是属于自己的虚拟地址空间(包括用户地址空间和内核地址空间),本质上是切换页表基地址寄存器

进程地址空间切换让进程产生独占系统内存的错觉,因为切换完地址空间后,当前进程可以访问属于它的海量的虚拟地址空间(内核地址空间各个进程共享,用户地址空间各个进程私有),而实际上物理地址空间只有一份。

下面给出源代码分析:

context_switch
->/*¦* kernel -> kernel   lazy + transfer active¦*   user -> kernel   lazy + mmgrab() active¦*¦* kernel ->   user   switch + mmdrop() active¦*   user ->   user   switch¦*/if (!next->mm) {                                // to kernelenter_lazy_tlb(prev->active_mm, next);next->active_mm = prev->active_mm;if (prev->mm)                           // from usermmgrab(prev->active_mm);elseprev->active_mm = NULL;} else {                                        // to user...switch_mm_irqs_off(prev->active_mm, next->mm, next);if (!prev->mm) {                        // from kernel/* will mmdrop() in finish_task_switch(). */rq->prev_mm = prev->active_mm;prev->active_mm = NULL;}            }                    

以上代码是判断是否next进程是内核线程,如果是则不需要进行地址空间切换(实际上指的是用户地址空间),因为内核线程总是运行在内核态访问的是内核地址空间,而内核地址空间是所有的进程共享的。在arm64架构中,内核地址空间是通过ttbr1_el1来访问,而它的主内核页表在内核初始化的时候已经填充好了,也就是我们常说的swapper_pg_dir页表,后面所有对内核地址空间的访问,无论是内核线程也好还是用户任务,统统通过swapper_pg_dir页表来访问,而在内核初始化期间swapper_pg_dir页表地址已经加载到ttbr1_el1中。

需要说明一点的是:这里会做“借用” prev->active_mm的处理,借用的目的是为了避免切换属于同一个进程的地址空间。举例说明:Ua  ->  Ka  ->  Ua   ,Ua表示用户进程,  Ka表示内核线程,当进行这样的切换的时候,Ka 借用Ua地址空间,Ua  ->  Ka不需要做地址空间切换,而Ka  ->  Ua按理来说需要做地址空间切换,但是由于切换的还是Ua 地址空间,所以也不需要真正的切换(判断了Ka->active_mm == Ua->active_mm ),当然还包括切换的是同一个进程的多个线程的情况,这留给大家思考。

下面来看下真正的地址空间切换:

 switch_mm_irqs_off(prev->active_mm, next->mm, next);->switch_mm  //arch/arm64/include/asm/mmu_context.h-> if (prev != next) __switch_mm(next);->check_and_switch_context(next)-> ...  //asid处理-> cpu_switch_mm(mm->pgd, mm)->cpu_do_switch_mm(virt_to_phys(pgd),mm)-> unsigned long ttbr1 = read_sysreg(ttbr1_el1);  unsigned long asid = ASID(mm);                 unsigned long ttbr0 = phys_to_ttbr(pgd_phys);  ...write_sysreg(ttbr1, ttbr1_el1);   //设置asid到ttbr1_el1isb();                            write_sysreg(ttbr0, ttbr0_el1);   //设置mm->pgd 到ttbr0_el1

上面代码是做真正的地址空间切换,实际的切换很简单,并没有那么复杂和玄乎,仅仅设置页表基地址寄存器即可,当然这里还涉及到了为了防止频繁无效tlb的ASID的设置。

主要做的工作就是设置next进程的ASID到ttbr1_el1, 设置mm->pgd 到ttbr0_el1,仅此而已

需要注意的是:1.写到ttbr0_el1的值是进程pgd页表的物理地址。2.虽然做了这样的切换,但是这个时候并不能访问到next的用户地址空间,因为还处在主调度器上下文中,属于内核态,访问的是内核空间。

而一旦返回了用户态,next进程就能正常访问自己地址空间内容:

  • 访问一个用户空间的虚拟地址va,首先通过va和记录在ttbr1_el1的asid查询tlb,如果找到相应表项则获得pa进行访问。

  • 如果tlb中没有找到,通过ttbr0_el1来遍历自己的多级页表,找到相应表项则获得pa进行访问。

  • 如果发生中断异常等访问内核地址空间,直接通过ttbr1_el1即可完成访问。

  • 访问没有建立页表映射的合法va,发生缺页异常来建立映射关系,填写属于进程自己的各级页表,然后访问。

  • 访问无法地址,发生缺页杀死进程等等。

处理器状态切换

来切换下一个进程的执行流,上一个进程执行状态保存,让下一个进程恢复执行状态。

处理器状态切换而后者让进程产生独占系统cpu的错觉,使得系统中各个任务能够并发(多个任务在多个cpu上运行)或分时复用(多个任务在一个cpu上运行)cpu资源。

下面给出代码:

context_switch
->(last) = __switch_to((prev), (next))-> fpsimd_thread_switch(next) //浮点寄存器切换...last = cpu_switch_to(prev, next); 

处理器状态切换会做浮点寄存器等切换,最终调用cpu_switch_to做真正切换。

cpu_switch_to  //arch/arm64/kernel/entry.S
SYM_FUNC_START(cpu_switch_to)mov     x10, #THREAD_CPU_CONTEXTadd     x8, x0, x10mov     x9, spstp     x19, x20, [x8], #16             // store callee-saved registersstp     x21, x22, [x8], #16stp     x23, x24, [x8], #16stp     x25, x26, [x8], #16stp     x27, x28, [x8], #16stp     x29, x9, [x8], #16str     lr, [x8]add     x8, x1, x10ldp     x19, x20, [x8], #16             // restore callee-saved registersldp     x21, x22, [x8], #16ldp     x23, x24, [x8], #16ldp     x25, x26, [x8], #16ldp     x27, x28, [x8], #16ldp     x29, x9, [x8], #16ldr     lr, [x8]mov     sp, x9msr     sp_el0, x1ptrauth_keys_install_kernel x1, x8, x9, x10scs_save x0, x8scs_load x1, x8ret
SYM_FUNC_END(cpu_switch_to)

这里传递过来的是x0为prev进程的进程描述符(struct task_struct)地址, x1为next的进程描述符地址。会就将prev进程的 x19-x28,fp,sp,lr保存到prev进程的tsk.thread.cpu_context中,next进程的这些寄存器值从next进程的tsk.thread.cpu_context中恢复到相应寄存器。这里还做了sp_el0设置为next进程描述符的操作,为了通过current宏找到当前的任务。

需要注意的是:

  1. mov     sp, x9  做了切换进程内核栈的操作。

  2. ldr     lr, [x8] 设置了链接寄存器,然后ret的时候会将lr恢复到pc从而真正完成了执行流的切换。

精美图示

这里给出了进程切换的图示(以arm64处理器为例),这里从prev进程切换到next进程。


进程再次被调度

当进程重新被调度的时候,从原来的调度现场恢复执行。

关于lr地址的设置

1)如果切换的next进程是刚fork的进程,它并没有真正的这些调度上下文的存在,那么lr是什么呢?这是在fork的时候设置的:

do_fork...copy_thread //arch/arm64/kernel/process.c->memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));p->thread.cpu_context.pc = (unsigned long)ret_from_fork;p->thread.cpu_context.sp = (unsigned long)childregs;

设置为了ret_from_fork的地址,当然这里也设置了sp等调度上下文(这里将进程切换保存的寄存器称之为调度上下文)。

SYM_CODE_START(ret_from_fork)bl      schedule_tailcbz     x19, 1f                         // not a kernel threadmov     x0, x20blr     x19
1:      get_current_task tskb       ret_to_user
SYM_CODE_END(ret_from_fork)

刚fork的进程,从cpu_switch_to的ret指令执行后返回,lr加载到pc。

于是执行到ret_from_fork:这里首先调用schedule_tail对前一个进程做清理工作,然后判断是否为内核线程如果是执行内核线程的执行函数,如果是用户任务通过ret_to_user返回到用户态。

2)如果是之前已经被切换过的进程,lr为cpu_switch_to调用的下一条指令地址(这里实际上是__schedule函数中调用barrier()的指令地址)。

关于__switch_to的参数和返回值

 switch_to(prev, next, prev)
->  ((last) = __switch_to((prev), (next)))

这里做处理器状态切换时,传递了两个参数,返回了一个参数:

prev和next很好理解就是 就是前一个进程(当前进程)和下一个进程的 task_struct结构指针,那么last是什么呢?

一句话:返回的last是当前重新被调度的进程的上一个进程的 task_struct结构指针

如:A ->B ->千山万水->D -> A 上面的切换过程:A切换到B 然后经历千山万水再从D -> A,这个时候A重新被调度时,last即为D的 task_struct结构指针。

获得当前重新被调度进程的前一个进程是为了回收前一个进程资源,见后面分析。

关于finish_task_switch

进程被重新调度时无论是否为刚fork出的进程都会走到finish_task_switch这个函数,下面我们来看它做了什么事情:

主要工作为:检查回收前一个进程资源,为当前进程恢复执行做一些准备工作

finish_task_switch
->finish_lock_switch->raw_spin_unlock_irq   //使能本地中断
->if (mm) mmdrop(mm)  //有借有还  借用的mm现在归还->if (unlikely(prev_state == TASK_DEAD)) {        //前一个进程是死亡状态put_task_stack(prev);    //如果内核栈在task_struct中   释放内核栈                                      put_task_struct_rcu_user(prev);  //释放前一个进程的task_struct占用内存}

可以看到进程被重新调度时首先需要做的主要是:

  • 重新使能本地中断 ,进程被重新调度时,本地cpu中断是被重新打开的!!!

  • 如果有借用mm的情况,现在归还 如果前一个是内核线程,在进程地址空间切换时“借用了”某个进程的mm_struct,现在切换到了下一个进程,理应归还,归还做的是递减借用的mm_struct的引用计数,引用计数为0就会释放mm_struct占用的内存。

  • 对于上一个死亡的进程现在回收最后的资源, 注意这里是递减引用计数,当引用计数为0时才会真正释放。

总结

主调度器可以说Linux内核进程管理中的核心组件,进程管理的其他部分如抢占、唤醒、睡眠等都是围绕它来运作。在原子上下文不能发生调度,说的就是调用主调度器,但是可以设置抢占标志以至于在最近的抢占点发生调度,如中断中唤醒高优先级进程的场景。主调度器所做的工作就是让出cpu,内核很多场景可以直接或间接调用它,而大体上可以分为两种情况:即为主动调度和抢占式调度。主调度器做了两件事情:选择下一个进程和进程进程上下文切换。选择下一个进程解决选择合适高优先级进程的问题。进程进程上下文切换又分为地址空间切换和处理器状态切换,前者让进程产生独自占用系统内存的错觉,而后者让进程产生独自占用系统cpu的错觉,让系统各个进程有条不紊的共享内存和cpu等资源。

- END -


看完一键三连在看转发,点赞

是对文章最大的赞赏,极客重生感谢你

推荐阅读

硬核致敬Linux !30岁生日快乐!

后端技术趋势指南|如何选择自己的技术方向

Linux调度系统全景指南(上篇)

深入理解Linux内核之主调度器相关推荐

  1. 深入理解Linux内核之主调度器(下)

    4.进程上下文切换 接前文:深入理解Linux内核之主调度器(上) 前面选择了一个合适进程作为下一个进程,接下来做重要的上下文切换动作,来保存上一个进程的"上下文"恢复下一个进程的 ...

  2. 【Linux 内核】CFS 调度器 ④ ( 调度子系统组件模块 | 主调度器、周期性调度器 | 调度器类 )

    文章目录 一.调度子系统组件模块 二.主调度器.周期性调度器 三.调度器类 一.调度子系统组件模块 调度器 需要对 被调度的进程 进行 排序 和 调度管理 , 进程管理过程需要 调度器 的 组件模块 ...

  3. 【Linux 内核】CFS 调度器 ① ( CFS 完全公平调度器概念 | CFS 调度器虚拟时钟 Virtual Runtime 概念 | 四种进程优先级 | 五种调度类 )

    文章目录 一.CFS 调度器概念 ( 完全公平调度器 ) 二.CFS 调度器虚拟时钟概念 ( Virtual Runtime ) 三.进程优先级 ( 调度优先级 | 静态优先级 | 正常优先级 | 实 ...

  4. 【Linux 内核】CFS 调度器 ③ ( 计算进程 “ 虚拟运行时间 “ )

    文章目录 一.计算进程 " 虚拟运行时间 " 一.计算进程 " 虚拟运行时间 " 在上一篇博客 [Linux 内核]CFS 调度器 ② ( CFS 调度器 &q ...

  5. 【Linux 内核】CFS 调度器 ⑥ ( CFS 调度器就绪队列 cfs_rq | Linux 内核调度实体 sched_entity | “ 红黑树 “ 数据结构 rb_root_cached )

    文章目录 一.CFS 调度器就绪队列 cfs_rq 二.Linux 内核调度实体 sched_entity 三." 红黑树 " 数据结构 rb_root_cached 一.CFS ...

  6. 【Linux 内核】CFS 调度器 ⑤ ( CFS 调度器类 fair_sched_class 源码 | next 赋值 | enqueue_task 赋值 | dequeue_task 赋值 )

    文章目录 一.调度器类 sched_class 简介 二.CFS 调度器类源码 三.next 赋值 四.enqueue_task 赋值 五.dequeue_task 赋值 一.调度器类 sched_c ...

  7. 【Linux 内核】CFS 调度器 ② ( CFS 调度器 “ 权重 “ 概念 | CFS 调度器调度实例 | 计算进程 “ 实际运行时间 “ )

    文章目录 一.CFS 调度器 " 权重 " 概念 二.CFS 调度器调度实例 ( 计算进程 " 实际运行时间 " ) 一.CFS 调度器 " 权重 & ...

  8. 【Linux 内核】实时调度类 ③ ( 实时调度类 rt_sched_class 源码 | 调度类 sched_class 源码 )

    文章目录 一.调度类 ( 停机调度类 | 限期调度类 | 实时调度类 | 公平调度类 | 空闲调度类 ) 二. 实时调度类 rt_sched_class 源码 一.调度类 ( 停机调度类 | 限期调度 ...

  9. Linux进程核心调度器之主调度器schedule--Linux进程的管理与调度(十九)

    日期 内核版本 架构 作者 GitHub CSDN 2016-06-30 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 ...

最新文章

  1. Fcrackzip——简介、安装、使用
  2. Python爬虫入门教程 26-100 知乎文章图片爬取器之二
  3. ac2100 反弹shell无法粘贴_记一次突破反弹shell
  4. 只需五步学会Maven 3.6.1OR 3.6.3及其他版本的下载安装与配置【图文详解】
  5. python raise语句_Python异常处理,告别xxxxError!
  6. 卡尔曼滤波原理(2)
  7. datagrid 溢出文本显示省略号
  8. 红米k20适配android q,比谷歌还快,红米K20 PRO首发安卓Q稳定版,只有华为心里苦...
  9. 360浏览器打不开html5文件,电脑360浏览器打不开网页原因-浏览器打不开解决方法 - 系统家园...
  10. 猜数游戏--python(设定猜测次数)
  11. 中心极限定理与大数定律的区别
  12. 密码框后的小眼睛怎么写?
  13. 群晖218J安装mysql_群晖218j和218+选择哪个?
  14. 电脑右下角没有USB图标,不能退出
  15. 一步一步实现中后台管理平台模板-08-登录页和用户信息保存
  16. python calu()_python使用配置文件过程详解
  17. 各位集美兄得看过来! 利用AI给青春有你2的选手们做数据分析挖掘(一):基于PaddlePaddle爬虫选手信息
  18. 腾达n4怎么设置虚拟服务器,win10系统电脑设置腾达n4无线路由器的具体教程
  19. init: cannot execve(‘XXX’):Permission denied问题
  20. QT CREATOR出现no Qt platform plugin问题

热门文章

  1. JDK并发包-同步控制
  2. STM32的ADC采集的引脚汇总
  3. Workbooks 对象的 Open 方法参数说明
  4. expect工具使用
  5. oracle-sql优化-通过分组和缓存减少不必要的读
  6. XMNetworking 网络库的设计与使用
  7. C#检查json格式是否合法
  8. day1---python的基础特性
  9. DOCTYPE html PUBLIC 指定了 HTML 文档遵循的文档类型定义
  10. 2012.5.2 学习记录:RadGrid单元格操作