深入理解进程切换

您所在的位置:网站首页 进程切换的原因 深入理解进程切换

深入理解进程切换

2024-07-04 19:58:28| 来源: 网络整理| 查看: 265

深入理解进程切换

进程切换(process switch)是操作系统的核心任务之一,用于在不同进程之间进行 CPU 时间的共享和分配。当一个进程在运行时,它占用了 CPU,并占用了其他诸如内存等资源。当操作系统需要执行另一个进程时,就需要进行进程切换。进程切换涉及到保存当前进程的上下文信息,包括 CPU 寄存器、程序计数器、栈指针等,以及恢复调度执行下一个进程所需的上下文信息。

在 Linux 操作系统中,进程切换的实现源码可以分为两个部分:进程调度和上下文切换。进程调度负责决定当前应该将哪个进程分配给 CPU 执行;上下文切换则是在进程切换时,保存当前进程的上下文信息,并恢复调度执行下一个进程所需的上下文信息。

进程调度的代码主要位于 kernel/sched/ 目录下,包括了进程调度算法以及实现。而进程切换则需要涉及到进程的 PCB(进程控制块)和线程的 TCB(线程控制块),以及 CPU 的寄存器状态和内核栈等上下文数据。在 x86 架构的处理器上,进程切换的具体实现涉及到 task_switch 函数、switch_to 宏以及 switch_to_asm 汇编函数等。在 AArch64 等不同架构的处理器上,对应的汇编代码可能有所不同,但目的是一致的。

基本上,当要切换到一个新进程时,CPU 必须保存当前进程(即上下文切换)。这包括将所有寄存器值保存到当前进程的 PCB 或 TCB 中,以及将内核栈保存到该进程的堆栈中。此外,还需要修改进程的状态标志、计数器和时间戳等信息。然后 CPU 将切换到新进程的 kernel 栈,并从其 PCB 或 TCB 中读取并加载该进程的 CPU 寄存器状态和堆栈指针等信息,以便开始执行新进程。

进程切换的 Linux 源码实现涉及到进程调度和上下文切换两个部分。以下是对进程切换源码的解读:

进程调度:在 Linux 内核源码中,进程调度的相关代码位于 kernel/sched 目录下。其中,调度策略相关的代码在 kernel/sched/core.c 文件中实现,包括 Linux 中比较常用的 CFS(Completely Fair Scheduler)调度算法等。此外,进程状态相关的代码则位于 include/linux/sched.h 文件中。

上下文切换:在 Linux 内核源码中,进程的上下文切换具体实现的代码主要位于 arch/x86/kernel/process.c 和 arch/arm64/kernel/process.c 等架构相关的文件中。例如,在 x86 架构的处理器上,有一个名为 task_switch() 的函数,它会保存当前进程上下文,并加载下一个进程的上下文。在这个函数中,会使用一些汇编代码来完成 CPB(process control block)的管理,如将当前进程的寄存器状态保存到 PCB 中,然后从新的 PCB 中加载下一个进程的寄存器状态等。

进程调度

进程调度的核心代码实现参考 kernel/sched/ 目录文件,主要包含以下几个部分:

调度算法:Linux 中实现了多种不同的进程调度算法,如 CFS(Completely Fair Scheduler)、O(1) 调度算法、实时调度算法等,并且各个算法之间可以配置和切换,由用户指定默认调度器。

调度队列:调度算法的实现需要用到调度队列,它通过双向链表的数据结构来管理所有进程。Linux 中有就绪队列、休眠队列、实时队列等不同类型的队列,它们存储着不同状态的进程。

进程状态:Linux 中的进程状态有很多种,如 TASK_RUNNING(运行中)、TASK_INTERRUPTIBLE(可中断的)、TASK_UNINTERRUPTIBLE(不可中断的)、TASK_STOPPED(已停止的)等。进程在不同状态下会被放置到不同类型的调度队列中,以便进行合适的调度。

上下文切换

上下文切换的核心代码实现参考 arch/x86/kernel/process.c 或者 arch/arm64/kernel/process.c 等架构相关的文件,主要包含以下几个部分:

进程控制块(Process Control Block, PCB):PCB 是 Linux 内核用来存储和管理进程信息的重要数据结构。在进程切换时,需要将当前进程的 PCB 中保存的上下文信息保存到内存中。

内核栈:内核栈用来保存进程的运行状态,进程切换时需要把当前进程的内核栈保存到当前进程的 PCB 中,并从新进程的 PCB 中恢复对应的内核栈信息。

寄存器状态:CPU 中包含了多个寄存器,进程切换时需要将当前进程的寄存器状态保存到当前进程的 PCB 中,并从新进程的 PCB 中恢复寄存器状态。

进程上下文切换:在 Linux 中,进程上下文切换是通过 switch_to 宏实现的。该宏会将当前进程的上下文信息保存到 PCB 中,并从新进程的 PCB 中读取上下文信息,完成进程切换的操作。

需要注意的是,不同架构的处理器可能会有不同的实现,但其核心原理相同。以上仅为进程切换的核心代码实现的简单介绍。

Linux 进程切换的流程和关键代码

Linux 内核中进程上下文切换的核心代码主要在 kernel/sched/core.c 和 arch/*/kernel/process.c 等文件中实现。

1. 进程切换的触发

进程切换的触发通常有以下几种情况:

当前进程的时间片用完了;当前进程主动让出 CPU 时间片,调用了 schedule() 或 yield() 函数;当前进程因等待某个事件(如 I/O 操作)而进入睡眠状态;新的进程创建或唤醒,需要切换到该进程并执行。 2. 进程上下文切换的步骤

Linux 内核中进程上下文切换的步骤主要包括以下几个部分:

保存当前进程的寄存器信息和堆栈信息;保存当前进程的上下文信息(task_struct 结构体);切换进程地址空间(如果需要);切换内核堆栈;恢复下一个进程的上下文信息;恢复下一个进程的堆栈和寄存器信息;跳转到下一个进程的执行点。 3. 进程切换的关键代码

在 kernel/sched/core.c 文件中,进程切换的关键函数包括 schedule()、__schedule()、switch_to() 等。其中,schedule() 函数会首先根据进程的优先级选择一个最合适的进程并调用 __schedule() 函数来执行具体的上下文切换操作。__schedule() 函数则会根据当前进程状态和下一个进程状态的不同,执行不同的操作,比如调度进程、睡眠进程、等待事件等。最后,switch_to() 函数则是用于执行实际的上下文切换操作,并切换到下一个进程的执行点。

在 arch/*/kernel/process.c 文件中,不同架构的 Linux 内核则会有不同的上下文切换代码实现。在 x86 架构的代码实现中,使用了 switch_to() 宏来完成具体的上下文切换操作,包括保存当前进程的寄存器信息,切换堆栈和地址空间,恢复下一个进程的寄存器信息并跳转到下一个进程的执行点等。其他架构的代码实现方式会有所不同,但都会涉及到类似的步骤和操作。

content_switch()

content_switch() 是 Linux 内核中用于进行进程上下文切换的函数,其源码位于 kernel/sched/core.c 文件中。下面对该函数的源码进行分析:

static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf) { prepare_task_switch(rq, prev, next); /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ arch_start_context_switch(prev); /* * kernel -> kernel lazy + transfer active * user -> kernel lazy + mmgrab() active * * kernel -> user switch + mmdrop() active * user -> user switch */ if (!next->mm) { // to kernel enter_lazy_tlb(prev->active_mm, next); next->active_mm = prev->active_mm; if (prev->mm) // from user mmgrab(prev->active_mm); else prev->active_mm = NULL; } else { // to user membarrier_switch_mm(rq, prev->active_mm, next->mm); /* * sys_membarrier() requires an smp_mb() between setting * rq->curr / membarrier_switch_mm() and returning to userspace. * * The below provides this either through switch_mm(), or in * case 'prev->active_mm == next->mm' through * finish_task_switch()'s mmdrop(). */ 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; } } rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); prepare_lock_switch(rq, next, rf); /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); }

这段代码是 Linux 内核中的 context_switch 函数的实现代码,用于在进程之间进行上下文切换。它接受三个参数:当前CPU运行的调度队列 rq、当前正在运行任务 prev 和即将运行的任务 next,以及一个用于标记上下文切换的一些标志 rq_flags *rf。

函数首先会调用 prepare_task_switch 函数准备切换进程,其中会调用一些处理器实现相关的函数。然后通过 arch_start_context_switch(prev) 函数启动新的进程上下文,并且在必要的情况下会利用 hypervisor 等技术进行处理器状态的转移和页面表的重载等操作。

接下来需要根据即将运行的进程 next 是否具有自己的地址空间(mm)来采用不同的上下文切换策略。当 next 没有 mm 时,表示即将切换到一个内核线程,此时调用 enter_lazy_tlb() 函数来刷新 TLB 并在之后的某个时间点再真正地切换进程上下文。同时需要将 prev 进程使用的地址空间设置为 next 进程使用的地址空间,并对 prev 的地址空间进行引用计数增加的处理。当 next 有 mm 时,表示即将切换到一个用户进程,此时会调用 membarrier_switch_mm() 函数来确保内存屏障相关的操作已经完成,然后再调用 switch_mm_irqs_off() 函数切换进程,并对相应的地址空间进行引用计数的更新。

最后,函数调用 prepare_lock_switch() 来准备新进程的锁,然后调用 switch_to() 函数切换进程上下文并更新时钟。最终,函数会返回调用 finish_task_switch() 函数的结果。

需要注意的是,这段代码中还涉及了一些 Linux 内核中的体系结构相关的实现,例如 arch_start_context_switch()、enter_lazy_tlb()、switch_mm_irqs_off() 等函数,它们可能在不同的体系结构和不同的内核版本中具有不同的实现方式。

switch_to()

switch_to() 函数是 Linux 内核中进行进程上下文切换的关键函数之一,其实现在不同的体系结构和内核版本中会有所差别,但其基本功能都相似。其作用就是将当前正在运行的进程(prev)的状态保存到其内核栈中,并将要运行的进程(next)的状态从其内核栈中恢复。此外,还需要更新进程的调度信息和时钟等。

x86

在 x86 体系结构中,switch_to() 函数的实现分为两部分:汇编实现和 C 语言实现。汇编实现主要用于完成进程状态的保存和恢复,包括将处理器状态、段寄存器、控制寄存器、调试寄存器等保存到 prev 进程的内核栈中,并从 next 进程的内核栈中恢复这些寄存器的值等。而 C 语言实现则负责更新进程的调度信息和时钟等。

ENTRY(__switch_to_asm) UNWIND_HINT_FUNC /* * Save callee-saved registers * This must match the order in inactive_task_frame */ pushq %rbp pushq %rbx pushq %r12 pushq %r13 pushq %r14 pushq %r15 /* switch stack */ movq %rsp, TASK_threadsp(%rdi) // 保存旧进程的栈顶 movq TASK_threadsp(%rsi), %rsp // 恢复新进程的栈顶 /* restore callee-saved registers */ popq %r15 popq %r14 popq %r13 popq %r12 popq %rbx popq %rbp jmp __switch_to END(__switch_to_asm)

这是x86 体系结构下 switch_to() 函数的汇编实现。该函数__switch_to_asm,是 switch_to() 函数的一个帮助函数。需要注意修改任务结构体中的 TASK_threadsp 指向旧的进程栈的指针,以便在之后的上下文切换过程中使用旧的内核栈。

具体实现分为以下几个步骤:

首先保存调用该函数时,rbp、rbx、r12、r13、r14、r15 这些 callee-saved 寄存器的值。这些寄存器的值在接下来的操作中会被修改,因此需要在下一步恢复它们的值。

然后,通过 movq 指令将当前进程的栈顶地址保存到其任务结构体的 TASK_threadsp 偏移处,表示该进程的内核栈已经切换到了新的栈,而且栈顶指针也已经更新为 rdi 所指向的进程的内核栈顶。接着,通过 movq 指令将待运行进程的栈顶地址从其任务结构体的 TASK_threadsp 偏移处读出并移动到 %rsp 寄存器中,使得下一步的操作将从新进程的内核栈中执行。

最后,通过 popq 指令恢复之前保存的 callee-saved 寄存器的值,以及 jmp 指令跳转到 C 语言实现的 __switch_to 函数中。

总之,这段汇编代码实现了 x86 体系结构下 switch_to() 函数的核心功能,即切换进程的内核栈,并将处理器状态从旧进程的栈中保存到 TASK_threadsp 中的指针所指向的地方,从新进程的栈中恢复处理器状态。

ARM

在 ARM 体系结构中,switch_to() 函数的实现也会涉及到进程状态的保存和恢复,以及进程调度信息和时钟的更新。在实现上需要考虑更多的体系结构特定的细节。例如,在 ARM64 中,需要对不同的 CPU 模式(EL0/EL1/EL2/EL3)进行处理,同时还需要考虑异常处理、虚拟化等方面的问题。

ENTRY(cpu_switch_to) mov x10, #THREAD_CPU_CONTEXT add x8, x0, x10 mov x9, sp stp x19, x20, [x8], #16 stp x21, x22, [x8], #16 stp x23, x24, [x8], #16 stp x25, x26, [x8], #16 stp x27, x28, [x8], #16 stp x29, x9, [x8], #16 str lr, [x8] add x8, x1, x10 ldp x19, x20, [x8], #16 ldp x21, x22, [x8], #16 ldp x23, x24, [x8], #16 ldp x25, x26, [x8], #16 ldp x27, x28, [x8], #16 ldp x29, x9, [x8], #16 ldr lr, [x8] mov sp, x9 msr sp_el0, x1 ret ENDPROC(cpu_switch_to) NOKPROBE(cpu_switch_to)

这段汇编代码实现了 ARM64 (aarch64) 体系架构下 cpu_switch_to() 函数。 具体实现分为以下几个步骤:

首先,将偏移量 THREAD_CPU_CONTEXT 加到旧进程的任务结构体指针 x0 中,获取旧进程的 cpu_context 结构体的地址。将栈顶地址 sp 存入 x9 寄存器中,以备后续保存。

接着,使用 stp 指令依次将 x19 - x28 寄存器的值保存到旧进程的 cpu_context 中。这些寄存器中的值在接下来的操作中会被修改,因此需要在下一步恢复它们的值。

使用 stp 指令将 x29(栈基址) 和 x9(栈顶) 的值保存到旧进程的 cpu_context 结构体中,并使用 str 指令将 lr 寄存器中的值(存放 cpu_switch_to() 函数的返回地址)保存到旧进程的 cpu_context 中。

将偏移量 THREAD_CPU_CONTEXT 加到新进程的任务结构体指针 x1 中,获取新进程的 cpu_context 结构体的地址。

使用 ldp 指令依次将 x19 - x28 寄存器的值从新进程的 cpu_context 中恢复。

使用 ldp 指令将 x29(栈基址) 和 x9(栈顶) 的值从新进程的 cpu_context 中恢复,并使用 ldr 指令将旧进程的 lr 寄存器中的值 (函数返回地址) 从新进程的 cpu_context 中恢复。

从 x9 恢复栈顶的值,使用 msr 指令将新进程的任务结构体地址放入 sp_el0 中,以实现从内核栈切换到新进程的用户模式栈。

最后,使用 ret 指令返回到 cpu_switch_to() 函数的调用点。



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭