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这些硬件单元带来的加速效果。事实上,内核只在几处地方使用它们,通常是搬动或清除大内存块或当计算校验的时候。


相关文章
|
3天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
21 4
|
7天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
28 6
|
4天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
41 8
|
6天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
26 9
|
5天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
23 6
|
6天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
22 5
|
6天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
7天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
6天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
8天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
25 6