每一种技术的出现必然是因为某种需求。正因为人的本性是贪婪的,所以科技的创新才能日新月异。
从英特尔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(任务切换)标志,标志设置的时机如下所示:
Device not available
的异常。从上面可以看出,只有执行浮点运算的时候才需要保存FPU、MMX和XMM相关寄存器。假设进程A正在使用协处理器,当进程A切换到进程B的时候,内核设置TS标志,且把浮点寄存器保存到进程A的任务状态段(TSS)中。如果进程B没有使用协处理器,内核不需要恢复浮点寄存器的内容。但只要进程B想要执行浮点运算或多媒体指令,CPU就会发出Device not available
异常,这个异常对应的处理程序就会把浮点寄存器中的值加载到进程B的TSS段中。
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个标志:
我们在分析进程切换的时候,知道主要的工作都是在__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()完成保存这些寄存器的基本工作,如下所示:
新进程恢复执行的时候,浮点寄存器不能立即恢复。但是通过__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标志,表示使用了浮点运算单元。
当然了,内核中也可以使用FPU、MMX或SSE/SSE2硬件单元(虽然,大部分时候没有意义)。这样做的话,应该避免干扰当前用户进程执行的任何浮点运算。因此:
之后,如果用户进程再执行协处理器指令的时候,math_state_restore()就会像进程切换时那样,恢复这些寄存器的内容。
但是,需要特别指出的是,如果当前用户进程正在使用协处理器时,kernel_fpu_begin()的执行时间相当长,甚至抵消了使用FPU、MMX或SSE/SSE2这些硬件单元带来的加速效果。事实上,内核只在几处地方使用它们,通常是搬动或清除大内存块或当计算校验的时候。
本文分享自 嵌入式ARM和Linux 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!