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


相关文章
|
5月前
|
安全 网络协议 Linux
深入理解Linux内核模块:加载机制、参数传递与实战开发
本文深入解析了Linux内核模块的加载机制、参数传递方式及实战开发技巧。内容涵盖模块基础概念、加载与卸载流程、生命周期管理、参数配置方法,并通过“Hello World”模块和字符设备驱动实例,带领读者逐步掌握模块开发技能。同时,介绍了调试手段、常见问题排查、开发规范及高级特性,如内核线程、模块间通信与性能优化策略。适合希望深入理解Linux内核机制、提升系统编程能力的技术人员阅读与实践。
495 1
|
5月前
|
Ubuntu Linux
Ubuntu 23.04 用上 Linux 6.2 内核,预计下放到 22.04 LTS 版本
Linux 6.2 带来了多项内容更新,修复了 AMD 锐龙处理器设备在启用 fTPM 后的运行卡顿问题,还增强了文件系统。
|
5月前
|
Ubuntu Linux
Ubuntu 23.10 现在由Linux内核6.3提供支持
如果你想在你的个人电脑上测试一下Ubuntu 23.10的最新开发快照,你可以从官方下载服务器下载最新的每日构建ISO。然而,请记住,这是一个预发布版本,所以不要在生产机器上使用或安装它。
|
5月前
|
传感器 监控 Ubuntu
10 月发布,Ubuntu 23.10 已升级到 Linux Kernel 6.3 内核
硬件方面,Linux 6.3 引入了在 HID 中引入了原生的 Steam Deck 控制器接口,允许罗技 G923 Xbox 版赛车方向盘在 Linux 上运行;改善 8BitDo Pro 2 有线控制器的行为;并为一系列华硕 Ryzen 主板添加传感器监控。
|
5月前
|
Ubuntu Linux
Ubuntu24.04LTS默认采用Linux 6.8内核,实验性版本可通过PPA获得
IT之家提醒,当下的 Ubuntu 23.10 也是一个“短期支持版本”,该版本将在今年 7 月终止支持,而今年 4 月推出的 Ubuntu 24.04 LTS 长期支持版本将获得 5 年的更新支持。
|
5月前
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
371 0
|
5月前
|
Web App开发 缓存 Rust
|
5月前
|
Ubuntu 安全 Linux
Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
本地攻击者可以利用上述漏洞,攻击 Ubuntu 22.10、Ubuntu 22.04、Ubuntu 20.04 LTS 发行版,导致拒绝服务(系统崩溃)或执行任意代码。
|
5月前
|
Ubuntu 机器人 物联网
Linux Ubuntu 22.04 LTS 测试版实时内核已可申请
请注意,在启用实时内核后您需要手动配置 grub 以恢复到原始内核。更多内容请参考: