| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> 进程管理(十九)----linux内核进程上下文(二) -> 正文阅读 |
|
[系统运维]进程管理(十九)----linux内核进程上下文(二) |
我们知道当调用schedule函数进行主动调度时,首先会调用通过调度类找到下一个要被调度的进程,然后将当前进程切换状态放入对应调度类的调度队列里面,等待再次被唤醒。而对于被调度的这个队列我们就要对其进行上下文切换,上一章节我们学习了上下文切换的时候的基本原理后,本章主要是学习在最新的内核上基于ARM架构学习完整的进程上下文切换的过程,本文的内核版本号为linux4.9.88。 1 context_switch代码分析在操作系统中把当前正在运行的进程挂起并恢复以前挂起的某个进程的执行,这个过程叫进程切换或者进程上下文,linux内核实现进程切换的核心代码在kernel/sched/core.c中有一个context_switch函数,该函数用来完成具体的进程切换,代码如下
虽然每个进程都可以拥有属于自己的进程空间,但是所有进程必须共享CPU寄存器等资源,所以在进程切换的时候必须把next进程在上一次挂起时保持的寄存器重新装载到CPU里,进程恢复执行前必须装入CPU寄存器的数据为硬件上下文,其主要包括以下两部分
2 切换前的准备工作在进程切换之前, 首先执行调用每个体系结构都必须定义的prepare_task_switch挂钩, 这使得内核执行特定于体系结构的代码, 为切换做事先准备. 大多数支持的体系结构都不需要该选项,该接口基本没有做什么事情,暂时不介绍。 对于内核线程,没有空间切换,直接使用上一个进程的内核空间即可。但是如果是用户进程则调用switch_mm_irqs_off完成用户地址空间切换,switch_mm_irqs_off(或switch_mm)与体系结构相关。 对于ARM64的cpu,每个cpu core都有两个寄存器来指示当前运行在该CPU core上的进程实体的地址空间,这两个寄存器分别是ttbr0_el1(用户地址空间)和ttbr1_el1(内核地址空间)。由于所有的进程共享内核地址空间,因此所谓地址空间切换也就是切换ttbr0_el1而已。 TTBR0指示了进程PGD页表基址,PGD指示了PTE页表基址,PTE指示了物理地址PA。每个进程的PGD不同,因而不同进程虚拟内存对于的物理地址就隔离开了。进程切换switch_mm实质上就是完成TTBR0寄存器的改写。Linux4.99内核调用switch_mm_irqs_off切换用户进程空间,对于没有定义该函数的架构,则调用的是switch_mm。X86体系架构定义了switch_mm_irqs_off函数,ARM体系架构没有定义。 对于swapper的进程,其使用cpu_set_reserved_ttbr0接口如下 那么check_and_switch_context完成了进程地址空间的切换,这包括两部分内容:
如果next进程发生迁移,在一个新的CPU上执行,则需要flush I-Cache(Instructions Cache)。如下图所示,对于ARM SMP架构来说每个core都有独立的I-Cache和D-Cache(哈佛结构L1 Cache),因而新进程第一次运行到某Core时需要将I-Cache内容全部刷新。 在看代码之前,我们要学习基础的知识,ASID指示了每个TLB entry所属的进程,这样可以保证不同进程之间的TLB entry不会互相干扰,因而避免了切换进程时将TLB刷新的问题。所以ASID作用避免了进程切换时TLB的频繁刷新。 实际上,ARM TLB包含了Global和process-specific表项,即全集类型的TLB和进程独有类型的TLB
为了支持进程独有类型的TLB,ARM架构出现了一个硬件解决方案,叫进程地址空间ID(address space ID,ASID),TLB可以识别哪些TLB属于某个进程,其方案如下:
check_and_switch_context函数前面部分主要实现了ASID相关的内容,详细的内容参考进程切换分析(2):TLB处理
地址空间切换过程中,会清空tlb,防止当前进程虚拟地址转化过程中命中上一个进程的tlb表项,一般会将所有的tlb无效,但是这会导致很大的性能损失,因为新进程被切换进来的时候面对的是全新的空的tlb,造成很大概率的tlb miss,需要重新遍历多级页表,所以arm64在tlb表项中增加了非全局(nG)位区分内核和进程的页表项,使用ASID区分不同进程的页表项,来保证可以在切换地址空间的时候可以不刷tlb。 cpu_switch_mm调用cpu_do_switch_mm完成进程地址空间切换,该实现在汇编arch/arm64/mm/proc.S中完成
3 switch_to函数处理完TLB和页表基地址后,还需要进行栈空间切换,这样next进程才能开始运行,这个正是switch_to的目的。
函数一共有3个参数,prev表示将要被调度出去的进程prev,next表示将要被调度进来的进程,这里有一个几个困惑
所以当切换回A进程的时候,该cpu上(也不一定是A调用switch_to切换到B进程的那个CPU)执行的上一个task是谁?这就是第三个参数的含义,实际上这个参数的名字就是last,也就是如何恢复到上一次被调度的片段处。 具体的切换发生在arch/arm64/kernel/entry.S文件中的cpu_switch_to,代码如下: cpu_switch_to要如何保存现场呢?要保存那些通用寄存器,那么就需要符合ARM64标准的过程调用,对于该过程用到了task_struct数据结构里的一个thread_struct的数据结构,用于存放和具体架构相关的信息,对于ARM64定义在arch/arm64/include/asm/processor.h中 这个结构体主要保存CPU的部分状态(寄存器),用来存储内核态切换时的硬件上下文。
为什么cpu_context数据结构只包含X19X28寄存器,而没有X0X18寄存器?这是跟ARM64架构函数调用的标准和规范有关
cpu_context数据结构定义如下:
进程上下文切换的过程如下图所示,在切换的过程中,将进程硬件上下文的重要的寄存器保存到prev进程的cpu_context数据结构中,进程上下文的包括X19~X28寄存器,FP寄存器,SP寄存器,PC寄存器,然后将next进程描述符的cpu_contex的x19-x28,fp,sp,pc恢复到相应寄存器中,而且将next进程的进程描述符task_struct地址存放在sp_el0中,用于通过current找到当前进程,这样就完成了处理器的状态切换。 那么内核是如何恢复到用户空间去执行呢?我们知道用户空间通过异常/中断进入到内核空间的时候都需要保存现场,也就是保存发生异常/中断的所有的通用寄存器,内核会将现场保存到每个进程特有的进程内核栈中,当异常/中断处理完毕后会返回到用户空间,返回到之前保存的现场,用户程序继续执行。 当进程重新被调度的时候,从原来的调度的现场恢复执行,如以切换的next进程刚好是fork进程,那么它的lr是什么呢?这个在fork的时候设置的 设置为了ret_from_fork的地址,当然这里也设置了sp等调度上下文(这里将进程切换保存的寄存器称之为调度上下文)。 刚fork的进程,从cpu_switch_to的ret指令执行后返回,lr加载到pc。于是执行到ret_from_fork:这里首先调用schedule_tail对前一个进程做清理工作,然后判断是否为内核线程如果是执行内核线程的执行函数,如果是用户任务通过ret_to_user返回到用户态。 4 finish_task_switchA保存内核栈和寄存器,切换至B,此时prev = A, next = B,该状态会保存在栈里,等下次调用A的时候再恢复。然后调用B的finish_task_switch()继续执行下去,返回B的队列rq,该函数主要是完成任务切换后的清理工作,其注释已经解释的比较明白了 可以看到进程被重新调度时首先需要做的主要是:
返回来,我们看当进程 A 在内核里面执行 switch_to 的时候,内核态的指令指针也是指向这一行的。但是在 switch_to 里面,将寄存器和栈都切换到成了进程 B 的,唯一没有变的就是指令指针寄存器。当 switch_to 返回的时候,指令指针寄存器指向了下一条语句 finish_task_switch。 但这个时候的 finish_task_switch 已经不是进程 A 的 finish_task_switch 了,而是进程 B 的 finish_task_switch 了。这样合理吗?我们如何知道进程B当时切换走到时候,执行到哪呢?恢复到B进程一定执行到这里吗? 当年 B 进程被别人切换走的时候,也是调用 __schedule,也是调用到 switch_to,被切换成为 C 进程的,所以,B 进程当年的下一个指令也是 finish_task_switch,这就说明指令指针指到这里是没有错的。 5 总结进程管理中最重要的一步要进行进程上下文切换,其中主要有两大步骤:
有了这两步的切换过程保证了进程运行的有条不紊,当然切换的过程是在内核空间完成,这对于进程来说是透明的。我们以一个系统调用为例
6 参考文档 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/18 13:50:12- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |