常见上下文切换场景


CPU运行指令的过程中,根据应用或者操作系统的需要,经常会改变指令的执行流,同时根据需要在不同的上下文之间切换。

一、函数调用

函数调用是用户主动发起的指令流和上下文改变。普通的转移指令只改变指令流不改变上下文,函数调用则通过ABI约定实现了一定的上下文变化。函数调用通常伴随着栈帧的变化,此外部分寄存器也会发生变化。根据ABI的约定,像$s0-$s8这样约定由被调用者保存(Callee Save)的寄存器在函数调用前后保持不变,而通用暂存器、参数寄存器等则不保证维持调用前的值。

不同指令系统实现函数调用的方式有所不同。LoongArch采用比较典型的RISC做法,硬件仅仅提供一个机制(bl或者jirl指令),用于在改变指令流的同时保存一个返回地址到通用寄存器,其余的都由软件来约定和实现。X86指令系统中则有比较复杂的硬件支持,其函数调用指令call指令有多种形式,硬件可以执行权限检查、保存返回地址到栈上、修改CS和IP寄存器、设置标志位等处理逻辑,但是参数的传递方式还是由软件约定。Sparc指令系统则为了减少函数调用时寄存器准备的开销,引入了体系结构可见的寄存器窗口机制。它的通用寄存器包括8个全局寄存器和2-32个窗口,每个窗口包括16个寄存器。任意时刻,指令可以访问8个全局寄存器、8个输入寄存器、8个局部寄存器、8个输出寄存器,其中前两个由当前窗口提供,输出寄存器由相邻窗口的输入寄存器提供。Sparc提供专门的save和restore指令来移动窗口,调用函数执行save指令,让当前函数的输出寄存器变成被调用函数的输入寄存器,消除了多数情况下准备调用参数的过程,函数返回时则执行restore指令恢复原窗口。这个技术看起来非常巧妙,然而它会给寄存器重命名等现代流水线技术带来很大的实现困难,现在常常被人们当作指令系统过度优化的反面案例。

二、异常与中断

之前有总结过异常控制流。通常异常和中断的处理对用户程序来说是透明的,相关软硬件需要保证处理前后原来执行中的代码看到的CPU状态保持一致。这意味着开始异常和中断处理程序之前需要保存所有可能被破坏的、原上下文可见的CPU状态,并在处理完返回原执行流之前恢复。需要保存的上下文包括异常处理代码的执行可能改变的寄存器(如Linux内核自身不用浮点部件,因此只需要处理通用整数寄存器而无须处理浮点寄存器)、发生异常的地址、处理器状态寄存器、中断屏蔽位等现场信息以及特定异常的相关信息(如触发存储访问异常的地址)。异常和中断的处理代码通常在内核态执行,如果它们触发前处理器处于用户态,硬件会自动切换到内核态。这种情况下通常栈指针也会被重新设置为指向内核态代码所使用的栈,以便隔离不同特权等级代码的运行信息。

对于非特别高频的异常或者中断,操作系统往往会统一简化处理,直接保存所有可能被内核修改的上下文状态,然后调用相应的处理函数,最后再恢复所有状态。因为大部分情况下处理函数的逻辑比较复杂,所以算起开销比例来这么做的代价也可以接受。例如,3A5000处理器的Linux内核中,所有中断都采用统一的入口处理代码,它的主要工作就是保存所有的通用整数寄存器和异常现场信息,除此之外只有少量指令用于切换中断栈、调用实际中断处理函数等代码。入口处理的指令总共只有几十条,而一个有实际用处的中断处理过程一般至少有数百条指令,其中还包括一些延迟比较长的IO访问。

对于发生频率很高的异常或者中断,我们希望它的处理效率尽量高。从异常和中断处理的各个环节都可以设法降低开销。例如,可以通过专用入口或者向量中断技术来降低确定异常来源和切换指令流的开销。此外,不同的指令系统用不同的方法来降低上下文保存恢复的开销。例如TLB管理,存储管理(MMU)中介绍了LoongArch中TLB重填的做法:设置专门的异常入口,利用便签寄存器来快速获得可用的通用寄存器,以及提供两个专门的指令(lddir和ldpte)来进一步加速从内存页表装入TLB表项的过程。X86指令系统选择完全用硬件来处理,成功的情况不会发出异常。MIPS指令系统则采用预留两个通用寄存器的办法。TLB重填异常处理只用这两个寄存器,因此没有额外的保存恢复代价(但所有的应用程序都牺牲了两个宝贵的通用寄存器)。

三、系统调用

之前总结过系统调用与API。系统调用是操作系统内核为用户态程序实现的子程序。系统调用的上下文切换场景和函数调用比较类似,和普通调用相比主要多了特权等级的切换。一些系统调用(如gettimeofday系统调用)只返回一些内核知道但用户程序不知道的信息。系统调用要满足安全性和兼容性两方面的要求。安全性方面,在面对错误甚至恶意的应用时,内核应该是健壮的,应能保证自身的安全;兼容性方面,操作系统内核应该能够运行已有的应用程序,这也要求系统调用应该是兼容的,轻易移除一个系统调用是无法接受的。

Linux内核中,每个系统调用都被分配了一个整数编号,称为调用号。调用号的定义与具体指令系统相关,X86和MIPS对同一函数的调用号可能不同。Linux/LoongArch系统的调用号定义可以从内核源码include/uapi/asm-generic/unistd.h获得。

因为涉及特权等级的切换,系统调用通常被当作一种用户发起的特殊异常来处理。例如在LoongArch指令系统中,执行SYSCALL指令会触发系统调用异常。异常处理程序通过调用号查表找到内核中相应的实现函数。与所有异常一样,系统调用在返回时使用ERTN指令来同时完成跳转用户地址和返回用户态的操作。

类似于一般的函数调用,系统调用也需要进行参数的传递。应该尽可能使用寄存器进行传递,这可以避免在核心态空间和用户态空间之间进行不必要的内容复制。在LoongArch指令系统中,系统调用的参数传递有以下约定:

  • 1)调用号存放在$a7寄存器中。
  • 2)至多7个参数通过$a0~$a6寄存器进行传递。
  • 3)返回值存放在$a0/$a1寄存器。
  • 4)系统调用保存$s0-$s8寄存器的内容,不保证保持参数寄存器和暂存寄存器的内容。

为了保障安全性,内核必须对用户程序传入的数组索引、指针和缓冲区长度等可能带来安全风险的参数进行检查。从用户空间复制数据时,应用程序提供的指针可能是无效的,直接在内核使用可能导致内核崩溃。因此,Linux内核使用专用函数copy_to_user()copy_from_user()来完成与用户空间相关的复制操作。它们为相应的访存操作提供了专门的异常处理代码,避免内核因为用户传入的非法值而发生崩溃。

四、进程

为了支持多道程序并发执行,操作系统引入了进程的概念。进程是程序在特定数据集合上的执行实例,一般由程序、数据集合和进程控制块三部分组成。进程控制块包括很多信息,它记录每个进程运行过程中虚拟内存地址、打开文件、锁和信号等资源的情况。操作系统通过分时复用、虚拟内存等技术让每个进程都觉得自己拥有一个独立的CPU和独立的内存地址空间。切换进程时需要切换进程上下文。进程上下文包括进程控制块记录的各种信息。

进程的上下文切换主要由软件来完成。发生切换的时机主要有两种,一是进程主动调用某些系统调用时因出现无法继续运行的情况(如等待IO完成或者获得锁)而触发切换,二是进程分配到的时间片用完了或者有更高优先级的就绪进程要抢占CPU导致的切换。切换工作的实质是实现对CPU硬件资源的分时复用。操作系统把当前进程的运行上下文信息保存到内存中,再把选中的下一个进程的上下文信息装载到CPU中。特定时刻只能由一个进程使用的处理器状态信息,包括通用寄存器、eflags等用户态的专有寄存器以及当前程序计数器(PC)、处理器模式和状态、页表基址(例如X86指令系统的CR3寄存器和LoongArch的PGD寄存器)等控制信息,都需要被保存起来,以便下次运行时恢复到同样的状态。如果一些不支持共享的硬件状态信息在内存里有最新备份,切换时可以采用直接丢弃的方法。例如,有些指令系统的TLB不能区分不同进程的页表项(早期的X86指令系统就是如此),那么在进程切换时需要把已有的表项设为无效,避免被新的进程错误使用。而可以共享的硬件状态信息(如Cache等),以及用内存保存的上下文信息(如页表等),则不需要处理。

不同的硬件支持可能导致不同的效率。TLB是否可以区分来自不同进程的页表项就是一个例子。不能区分时,每次切换进程的时候必须使所有的硬件TLB表项无效,每次进程开始运行时都需要重新从内存获取页表项。而LoongArch等指令系统的TLB则支持用某种进程标记(LoongArch中是ASID)来区分不同进程的页表项,可以避免这种开销。随着指令系统的发展,需要切换的信息也在增加,引发了一些新的硬件支持需求。例如,除了常规的整数和浮点通用寄存器,很多现代处理器增加了数十个位宽很大(X86 AVX扩展可达512位)的向量寄存器。由于无条件保存所有寄存器的代价比较大,操作系统常常会采用某种按需保存的优化,比如不为没有用到向量的进程保存向量状态。但这需要指令系统提供一定的支持。在MIPS和LoongArch指令系统中,浮点和向量部件都可以通过控制寄存器来关闭,在关闭部件后使用相关指令会触发异常,这样操作系统就能有效地实现按需加载。

历史上也有些指令系统曾尝试为进程切换提供更多硬件支持。例如,X86指令系统提供了专门的TS(Task State)段和硬件自动保存进程上下文的机制,适当设置之后进程切换可以由硬件完成。但由于硬件机制不够灵活而且性能收益不明显,包括Linux和Windows在内的多数操作系统都没有使用这个机制。

五、线程

线程是程序代码的一个执行路径。一个进程可以包含多个线程,这些线程之间共享内存空间和打开文件等资源,但逻辑上拥有独立的寄存器状态和栈。现代系统的线程一般也支持线程私有存储区(Thread Local Storage,简称TLS)。例如,GCC编译器支持用__thread int number;这样的语句来定义一个线程私有的全局变量,不同线程看到的number地址是不一样的。

线程可以由操作系统内核管理,也可以由用户态的线程库管理,或者两者混合。线程的实现方式对切换开销有很大的影响。例如,Linux系统中最常用的线程库NPTL( POSIX Thread Library)采用内核和用户1:1的线程模型,每个用户级线程对应一个内核线程。除了不切换地址空间,线程的切换和进程的大部分流程一致,都需要进入和退出核心态,经历至少两次用户态和核心态上下文的切换。因此,对一些简单测试来说,Linux中进程和线程切换的速度差异可能不太明显。而Go语言提供的协程(goroutines)可以被看作一种用户级实现的轻量级线程,它的切换不需要通过内核,一些测试表明,其切换开销可比NPTL小一半以上。当然,进程和线程切换不仅仅有执行切换代码的直接开销,还有因为TLB、Cache等资源竞争导致的间接开销,在数据集比较大的时候,进程和线程的实际切换代价差异也可能较大。

同样,适当的硬件支持也有助于提升线程切换效率。例如,LoongArch的ABI将一个通用寄存器用作专门的$tp寄存器,用来高效访问TLS空间。切换线程时只需要将$tp指向新线程的TLS,访问TLS的变量时用$tp和相应的偏移就能实现访问每个线程一份的变量。相比之下,Linux/MIPS系统则依赖系统调用set_thread_area来设置当前线程的TLS指针,将它保存到内核的线程数据结构中;用户程序用rdhwr指令来读取当前的线程指针,这个指令会产生一个异常来陷入内核读取TLS指针。相比之下,这样的实现效率会低很多。

六、虚拟机

线程把一份CPU计算资源虚拟成多份独立的CPU计算资源,进程把CPU和物理内存的组合虚拟成多份独立的虚拟CPU和虚拟内存组合。更进一步,我们可以把一台物理计算机虚拟成多台含CPU、内存和各种外设的虚拟计算机。虚拟机可以更好地隔离不同的服务运行环境,更充分地利用越来越丰富的物理机资源,更方便地迁移和管理,因此得到了广泛的应用,成为云计算的基础技术。

虚拟机的运行上下文包括CPU、内存和外设的状态。在虚拟机内部会发生函数调用、中断和异常、线程和进程等各种内部的上下文切换,它们的处理和物理机的相应场景类似。但在虚拟机无法独立处理的情况下会退出虚拟机运行状态,借助宿主机的虚拟化管理软件来完成任务。虚拟机和宿主机之间的切换需要保存和恢复所有可能被修改的虚拟机相关状态信息。例如对于CPU的状态信息,之前几种场景需要保存恢复的主要是用户可访问的寄存器,而虚拟机切换时可能还需要保存各种特权态资源,包括众多控制寄存器。如果系统支持在一台物理计算机上虚拟化出多个虚拟机,在物理资源少于虚拟机个数的时候,只能通过保存和恢复相关资源来维持每个虚拟机都独占资源的效果。

虚拟机可以完全由软件实现。例如,开源的QEMU虚拟机软件能够虚拟出各种架构的CPU和众多设备,如在一台龙芯电脑上虚拟出一台X86 PC设备并运行Windows操作系统。在宿主机指令系统和被模拟的客户机指令系统不同时,QEMU采用二进制翻译技术把客户机应用动态翻译成等价功能的宿主机指令。不过,这种情况下QEMU虚拟的客户机运行速度比较低,一般不到宿主机的10%。

在客户机和宿主机指令系统相同时,已经有一些成熟的技术可通过适当的硬件支持来大大提升虚拟化效率。龙芯和大部分现代的高性能处理器都支持虚拟机扩展,在处理运行模式、系统态资源、内存虚拟化和IO虚拟化等方面提供硬件支持,使得虚拟机可以实现和物理机相似的性能。

七、六种上下文切换场景的对比

以下对以上六种上下文切换的场景进行了对比总结。函数调用和系统调用是用户主动发起的,因此可以通过ABI约定来避免不必要的保存恢复。其他几种场景通常都要达到对应用程序透明的效果,因此切换后可能被修改的状态都应该被保存和恢复。
在这里插入图片描述

【参考书籍】
计算机体系结构基础》第3版
计算机体系结构量化研究方法》第5版

<think>好的,用户想了解操作系统的上下文切换流程。我得先回忆一下相关知识,确保自己理解正确。上下文切换是多任务处理中的关键部分,但具体步骤可能有点复杂,需要分步骤解释清楚。 首先,用户可能对操作系统的基本概念有一定了解,但上下文切换可能涉及一些底层细节,比如寄存器、进程状态等。我需要用通俗易懂的语言,避免过于技术化的术语,同时保持准确性。 接下来,我应该从触发上下文切换的原因开始,比如中断、系统调用或者时间片用完。然后,分步骤描述保存当前进程的上下文、选择下一个进程、恢复新进程的上下文等流程。可能需要提到内核栈和PCB(进程控制块)的作用,这些都是关键点。 还要注意区分进程上下文和线程上下文切换,虽然用户可能没有问,但简要说明可以增加回答的全面性。另外,上下文切换的开销和优化方法也是值得提及的,这样用户能理解其重要性。 需要检查是否有遗漏的步骤,比如是否涉及内存管理单元(MMU)或者缓存的问题,但可能这些属于更高级的内容,暂时不需要深入。保持回答结构清晰,按步骤讲解,并确保每个部分逻辑连贯。 最后,用总结的方式回顾整个流程,帮助用户巩固理解。确保使用中文,并且数学公式正确,如果有用到的话,比如提到时间片轮转调度算法时可能需要公式,但根据用户的问题,可能不需要涉及太多数学表达式。按照系统指令,行内公式用$...$,独立公式用$$...$$,但这个问题可能更多是文字描述,公式使用较少,但如果有的话要正确应用。</think>好的!我将分步骤为您介绍操作系统上下文切换的流程,并尽量保持清晰和系统性: --- ### **一、上下文切换的定义** **上下文切换(Context Switch)** 是操作系统在多任务环境中,将CPU的执行权从一个进程(或线程)转移到另一个进程(线程)的过程。 核心目标:**保存当前运行任务的执行状态**,并**恢复新任务的执行状态**,使多个任务能够“看似并行”运行。 --- ### **二、触发上下文切换常见场景** 1. **主动触发** - 进程主动调用系统调用(如 `fork()`, `sleep()`, `I/O请求`)。 - 进程执行完毕,主动释放CPU。 2. **被动触发** - **时间片耗尽**:CPU分配给进程的时间片用完(如时间片轮转调度)。 - **中断发生**:硬件中断(如时钟中断、I/O完成中断)或异常(如缺页异常)。 --- ### **三、上下文切换的核心流程** #### **步骤1:保存当前进程的上下文** - **什么是上下文?** 进程的运行时状态,包括: - **寄存器内容**:如程序计数器(PC)、栈指针(SP)、通用寄存器(eax, ebx等)。 - **进程控制块(PCB)**:存储进程状态(运行/就绪/阻塞)、内存映射、权限等信息。 - **其他资源**:如浮点寄存器、MMU状态(内存管理单元)。 - **保存方式** 操作系统将当前进程的寄存器内容压入其**内核栈**,并将PCB更新为“就绪”或“阻塞”状态。 #### **步骤2:选择下一个运行进程** - **调度器(Scheduler)** 根据调度算法(如优先级、轮转)从就绪队列中选择一个进程。 - 例如:$$ \text{Next\_PID} = \text{Scheduler}(\text{Ready\_Queue}) $$ #### **步骤3:恢复新进程的上下文** - 从新进程的PCB中加载其寄存器值和内存映射信息。 - 将新进程的内核栈内容弹出到寄存器中,恢复其执行状态。 - 更新CPU的**程序计数器(PC)**,跳转到新进程被中断的代码位置继续执行。 #### **步骤4:切换地址空间(可选)** - 如果新旧进程属于不同的地址空间(如不同进程),需切换**页表基址寄存器**(如x86的CR3),更新内存映射。 --- ### **四、关键数据结构** 1. **进程控制块(PCB)** - 存储进程唯一标识符(PID)、状态、寄存器快照、内存信息等。 2. **内核栈(Kernel Stack)** - 每个进程拥有独立的内核栈,用于保存中断/系统调用时的上下文。 --- ### **五、上下文切换的开销** - **直接开销** - 寄存器保存/恢复、缓存失效(如TLB刷新)、调度算法计算时间。 - **间接开销** - CPU流水线停滞、缓存局部性被破坏。 --- ### **六、进程 vs 线程的上下文切换** 1. **进程切换** - 需要切换地址空间(页表),开销较大。 2. **线程切换** - 同一进程内的线程共享地址空间,只需切换寄存器/栈,开销较小。 --- ### **七、优化方法** 1. **减少切换频率**:增大时间片、使用更高效的调度算法。 2. **惰性保存**:仅保存必要的寄存器。 3. **硬件加速**:利用TLB标签(ASID)避免全量刷新。 --- ### **总结流程** ```plaintext 中断/系统调用 → 保存当前进程上下文 → 调度新进程 → 恢复新进程上下文 → 跳转执行 ``` 希望以上内容能帮助您理解上下文切换的机制!如果有具体细节需要展开,请随时提出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值