Linux内核13_1-进程切换是对FPU单元的处理_X86

简介: Linux内核13_1-进程切换是对FPU单元的处理_X86

每一种技术的出现必然是因为某种需求。正因为人的本性是贪婪的,所以科技的创新才能日新月异。


1 简介


从英特尔80486DX开始,算术浮点单元(FPU)就已经被集成到CPU中了。但是之所以还继续使用数学协处理器,是因为以前使用专用芯片进行浮点运算,所以这算是旧习惯的沿用吧。为了与旧CPU架构模型兼容,指令的使用方式与整数运算一样,只是使用了转义指令,也就是在原有的指令基础上加上前缀,组成新的指令,这些前缀的范围是0xd8-0xdf。使用这些指令可以操作CPU中的浮点寄存器。很显然,使用这些浮点运算指令的进程在进程切换的时候,需要保存属于它的硬件上下文中的浮点寄存器内容。

随后的奔腾系列处理器,因特尔引入了一组新的汇编指令。它们被称为MMX指令,用来支持多媒体应用的加速执行。MMX指令也是作用于FPU单元的浮点寄存器。这样的设计缺点是,内核开发者无法混合使用转义浮点指令和MMX指令;优点是内核开发者可以使用相同的进程切换代码来保存浮点单元和MMX的状态。

MMX指令之所以能够加速多媒体应用的执行,是因为它们在处理器中专门引入了单指令多数据(SIMD)指令流水线。奔腾III扩展了SIMD指令:引入了SSE扩展(单指令多数据流扩展),包含8个128位的寄存器,称为XMM寄存器,通过它们可以大大增加浮点数的处理。这些寄存器是独立的,和FPU和MMX寄存器没有重叠,所以SSE扩展和FPU/MMX指令可以混合使用。奔腾4又又引入了新的扩展:SSE2扩展,是在SSE基础上的扩展,支持更高精度的浮点数。SSE2扩展和SSE扩展使用相同的XMM寄存器。

X86微处理器不会自动在TSS中保存FPU、MMX和XMM寄存器。但是,从硬件上,支持内核只保存所需要的寄存器。具体方法就是在cr0寄存器中包含一个TS(任务切换)标志,标志设置的时机如下所示:

  • 每次执行硬件上下文切换,TS标志被置。
  • 每次执行ESCAPE、MMX、SSE或SSE2指令,同时TS标志被置,则控制单元就会发出Device not available的异常。

从上面可以看出,只有执行浮点运算的时候才需要保存FPU、MMX和XMM相关寄存器。假设进程A正在使用协处理器,当进程A切换到进程B的时候,内核设置TS标志,且把浮点寄存器保存到进程A的任务状态段(TSS)中。如果进程B没有使用协处理器,内核不需要恢复浮点寄存器的内容。但只要进程B想要执行浮点运算或多媒体指令,CPU就会发出Device not available异常,这个异常对应的处理程序就会把浮点寄存器中的值加载到进程B的TSS段中。


2 FPU相关数据结构


Linux内核是使用什么数据结构表示FPU、MMX和XMM这些需要保存的寄存器值呢?基于x86架构的Linux内核使用i387_union类型的变量thread.i387存储这些值,该变量位于进程描述符中。i387_union如下所示:

union i387_union {
    struct i387_fsave_struct fsave;
    struct i387_fxsave_struct fxsave;
    struct i387_soft_struct soft;
};

如代码所示,这个联合体包含三个不同类型的数据结构。没有协处理器的CPU模型使用i387_soft_struct类型数据结构,这是Linux为了兼容那些使用软件模拟协处理器的旧芯片。故我们在此,不做过多描述。带有协处理器和MMX单元的CPU模型使用i387_fsave_struct数据类型。带有SSE和SSE2扩展的CPU模型使用i387_fxsave_struct数据类型。


除此之外,进程描述符还包含另外2个标志:

  • TS_USEDFPU标志
    位于thread_info描述符的status成员中。表示正在进行的进程是否使用FPU、MMX或XMM寄存器。
  • PF_USED_MATH标志
    位于task_struct描述符中的flags成员中。表示存储在thread.i387中的数据是否有意义。该标志被清除的时候有两种情况:
  • 调用execve()系统调用,启动新进程的时候。因为控制单元绝不会再返回到之前的程序中,所以存储在thread.i387中的数据就没有了意义。
  • 用户态正在执行的程序开始执行信号处理程序的时候。因为信号处理程序相对于正在执行的程序流来说是异步的,此时的浮点寄存器对于信号处理程序也就没有了意义。但是需要内核为进程保存thread.i387中的浮点寄存器值,等到信号处理程序终止时再恢复这些寄存器值。也就是说,允许信号处理程序使用协处理器。


2 保存FPU寄存器


我们在分析进程切换的时候,知道主要的工作都是在__switch_to()宏中完成的。而在__switch_to()宏中,执行__unlazy_fpu宏,并将先前进程的进程描述符作为参数进行传递。这个宏会检查旧进程的TS_USEDFPU标志:如果标志被设置,说明旧进程使用了FPU、MMX、SSE或SSE2指令。因此,内核必须保存相关的硬件上下文,如下所示:

if (prev->thread_info->status & TS_USEDFPU)
    save_init_fpu(prev);

函数save_init_fpu()完成保存这些寄存器的基本工作,如下所示:

  1. 将FPU寄存器的内容保存到旧进程的描述符中,然后重新初始化FPU。如果CPU还使用了SSE/SSE2扩展,还需要保存XMM寄存器的内容,然后重新初始化SSE/SSE2单元。下面的2条GNU伪汇编语言实现:
asm volatile( "fxsave %0 ; fnclex"
    : "=m" (prev->thread.i387.fxsave) );
  1. 开启SSE/SSE2扩展,使用下面这条汇编语言:
asm volatile( "fnsave %0 ; fwait"
    : "=m" (prev->thread.i387.fsave) );
  1. 清除旧进程的TS_USEDFPU标志:
prev->thread_info->status &= ~TS_USEDFPU;
  1. 设置cr0协处理器的TS标志。使用stts()宏实现,该宏转换成汇编语言如下所示:
movl %cr0, %eax
orl $8,%eax
movl %eax, %cr0

4 加载FPU寄存器


新进程恢复执行的时候,浮点寄存器不能立即恢复。但是通过__unlazy_fpu()宏已经设置了cr0协处理器中的TS标志。所以,新进程第一次尝试执行ESCAPE、MMX或SSE/SSE2指令的时候,控制单元就会发出Device not available异常,内核中相关的异常处理程序就会执行math_state_restore()函数加载浮点寄存器等。新进程被标记为当前进程。

void math_state_restore()
{
    asm volatile ("clts");  /* 清除TS标志 */
    if (!(current->flags & PF_USED_MATH))
        init_fpu(current);
    restore_fpu(current);
    current->thread.status |= TS_USEDFPU;
}

该函数还会清除TS标志,以至于后来再执行FPU、MMX或SSE/SSE2指令的时候,不会再发出Device not available异常。如果PF_USED_MATH标志等于0,说明thread.i387中的内容没有意义了,init_fpu()就会复位thread.i387,并设置当前进程的PF_USED_MATH为1。然后,restore_fpu()就会把正确的值加载到FPU寄存器中。这个加载过程需要调用汇编指令fxrstor或frstor,使用哪一个取决于CPU是否支持SSE/SSE2扩展。最后,设置TS_USEDFPU标志,表示使用了浮点运算单元。


5 在内核中使用FPU、MMX和SSE/SSE2单元


当然了,内核中也可以使用FPU、MMX或SSE/SSE2硬件单元(虽然,大部分时候没有意义)。这样做的话,应该避免干扰当前用户进程执行的任何浮点运算。因此:

  • 在使用协处理器之前,内核必须调用kernel_fpu_begin(),继而调用save_init_fpu(),保存用户进程的浮点相关寄存器的内容。然后,清除cr0协处理器中的TS标志。
  • 使用完了之后,内核必须调用kernel_fpu_end(),设置TS标志。

之后,如果用户进程再执行协处理器指令的时候,math_state_restore()就会像进程切换时那样,恢复这些寄存器的内容。

但是,需要特别指出的是,如果当前用户进程正在使用协处理器时,kernel_fpu_begin()的执行时间相当长,甚至抵消了使用FPU、MMX或SSE/SSE2这些硬件单元带来的加速效果。事实上,内核只在几处地方使用它们,通常是搬动或清除大内存块或当计算校验的时候。


相关文章
|
2天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
19 4
linux进程管理万字详解!!!
|
1天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
15 4
|
2天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
4天前
|
消息中间件 存储 Linux
|
5天前
|
缓存 运维 Linux
深入探索Linux内核:CPU拓扑结构探测
【10月更文挑战第18天】在现代计算机系统中,CPU的拓扑结构对性能优化和资源管理至关重要。了解CPU的核心、线程、NUMA节点等信息,可以帮助开发者和系统管理员更好地调优应用程序和系统配置。本文将深入探讨如何在Linux内核中探测CPU拓扑结构,介绍相关工具和方法。
8 0
|
2天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
6 0
|
5月前
|
监控 Linux 应用服务中间件
探索Linux中的`ps`命令:进程监控与分析的利器
探索Linux中的`ps`命令:进程监控与分析的利器
118 13
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
144 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
3月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。