浮点服务
在支持浮点寄存器的开发板配置上,内核允许线程使用这些寄存器。
备注
浮点服务目前仅适用于基于以下架构的开发板:支持浮点扩展的 ARM Cortex-M SoC、Intel x86 架构、SPARC 架构,以及支持浮点扩展的 ARCv2 SoC。提供的服务因架构而异。
内核不支持 ISR 使用浮点寄存器。
概念
内核可以配置为仅提供应用所需的浮点服务。支持下述三种工作模式。此外,还可根据需要包含或省略内核对 SSE 寄存器的支持。
也可以在运行时管理线程是否参与浮点上下文保存。k_float_enable() 为线程启用上下文保存,并接受 K_FP_REGS 和 K_SSE_REGS 等选项来选择要保存的寄存器组;k_float_disable() 则再次关闭上下文保存。对于未实现浮点上下文管理的目标,这些函数返回 -ENOTSUP;如果请求因其他原因无效而无法执行,则返回 -EINVAL。
不使用浮点寄存器模式
此模式适用于应用中没有线程使用浮点寄存器的情况,也是内核默认的浮点服务模式。
如果线程使用任何浮点寄存器,内核都会产生致命错误并终止该线程。
非共享浮点寄存器模式
此模式适用于应用中只有一个线程使用浮点寄存器的情况。
在 x86 平台上,内核会初始化浮点寄存器,使所有线程均可使用它们(ARM Cortex-M 和 ARCv2 平台跳过此初始化)。发生上下文切换时,浮点寄存器保持不变。
备注
如果两个或更多线程尝试使用浮点寄存器,则行为未定义,因为内核不会尝试检测或阻止多个线程使用这些寄存器。
共享浮点寄存器模式
此模式适用于应用中有两个或更多线程使用浮点寄存器的情况。内核根据底层 CPU 架构,支持以下一种或多种线程类别:
非使用者:不能使用任何浮点寄存器的线程
FPU 使用者:能够使用标准浮点寄存器的线程
SSE 使用者:能够同时使用标准浮点寄存器和 SSE 寄存器的线程
内核初始化浮点寄存器并启用访问,使任何线程均可使用它们;随后在上下文切换时保存和恢复这些寄存器,以确保每个 FPU 或 SSE 使用者的计算不受其他使用者影响。
ARM Cortex-M 架构(带浮点扩展)
备注
共享浮点寄存器模式是 ARM Cortex-M 默认的浮点服务模式。
对于带浮点扩展的 ARM Cortex-M 架构,启用共享浮点寄存器模式后,内核将 所有 线程视为 FPU 使用者。这意味着任何线程均可访问浮点寄存器。线程首次访问浮点寄存器时,ARM 内核会自动检测到该线程正在使用它们。
可使用以下方法之一,预先标记打算使用浮点寄存器的线程。
对于静态创建的 ARM 线程,可向
K_THREAD_DEFINE传入K_FP_REGS选项进行预先标记。对于动态创建的 ARM 线程,可向
k_thread_create()传入K_FP_REGS选项进行预先标记。
通过 K_FP_REGS 选项预先标记线程,可指示基于 MPU 的栈保护机制正确配置线程保护区的大小,以始终保证能够检测栈溢出,并在线程创建时为其启用延迟压栈。
在线程上下文切换期间,如果被换出的线程使用了浮点寄存器,ARM 内核会保存 被调用者保存 的浮点寄存器。此外,调用者保存 的浮点寄存器会保存在线程栈上。如果被换入的线程使用了浮点寄存器,内核会恢复该线程的 被调用者保存 的浮点寄存器,并从线程栈中恢复 调用者保存 的浮点上下文。因此,对于不使用浮点寄存器的线程,内核不会保存或恢复其浮点上下文。
每个打算使用浮点寄存器的线程必须额外提供 72 字节栈空间,用于保存被调用者保存的浮点上下文。
ARM Cortex-M 架构上的 Zephyr 应用目前已启用 延迟压栈,以在浮点上下文处于活动状态时尽量降低中断延迟。
未启用基于 MPU 的栈保护机制时,Zephyr 应用始终启用延迟压栈。启用该栈保护机制时,延迟压栈遵循以下规则:
如果 ARM 线程不再需要使用浮点寄存器,可以调用 k_float_disable(),指示内核在线程上下文切换时不再保存或恢复其浮点上下文。
ARM64 架构
备注
共享浮点寄存器模式是 ARM64 默认的浮点服务模式。编译器可以自由使用浮点/SIMD 寄存器优化代码,memcpy 等库函数也会使用这些寄存器。
在 ARM64(Aarch64)架构上,内核根据具体情况将各线程视为 FPU 使用者。上下文切换采用“延迟保存”算法,仅在确有必要时才更新浮点寄存器。例如,从 FPU 使用者切换到非使用者线程,随后又切回原 FPU 使用者时,不会 保存这些寄存器。
支持 ISR 使用 FPU 寄存器,但不推荐这样做。当 ISR 使用浮点或 SIMD 寄存器时,访问会触发陷阱,当前 FPU 使用者的上下文会保存在线程对象中,随后在禁用中断的情况下恢复 ISR,以防其他 IRQ 中断该 ISR 并可能请求使用 FPU。由于 ISR 没有持久的寄存器上下文,也就没有保存 ISR 浮点上下文的机制,因此需要禁用 IRQ。
启用共享浮点寄存器模式后,每个线程对象增大 512 字节。
ARCv2 架构
在 ARCv2 架构上,内核将各线程视为非使用者或 FPU 使用者,且必须通过以下方法之一标记线程。
对于静态创建的 ARC 线程,可向
K_THREAD_DEFINE传入K_FP_REGS选项进行标记。对于动态创建的 ARC 线程,可向
k_thread_create()传入K_FP_REGS进行标记。
如果 ARC 线程不再需要使用浮点寄存器,可以调用 k_float_disable(),指示内核在线程上下文切换时不再保存或恢复其浮点上下文。
在线程上下文切换期间,如果被换出的线程使用了浮点寄存器,ARC 内核会保存 被调用者保存 的浮点寄存器。此外,调用者保存 的浮点寄存器会保存在线程栈上。如果被换入的线程使用了浮点寄存器,内核会恢复该线程的 被调用者保存 的浮点寄存器,并从线程栈中恢复 调用者保存 的浮点上下文。因此,对于不使用浮点寄存器的线程,内核不会保存或恢复其浮点上下文。加载和存储浮点寄存器需要额外的 16 字节(单精度浮点硬件)或 32 字节(双精度浮点硬件)栈空间。
RISC-V 架构
在 RISC-V 架构上,内核根据具体情况将各线程视为 FPU 使用者,并按需分配 FPU 访问权。上下文切换采用“延迟保存”算法,仅在确有必要时才更新浮点寄存器。例如,从 FPU 使用者切换到非使用者线程(或在其调度时间段内未访问 FPU 的 FPU 使用者),随后又切回原 FPU 使用者时,不会 保存 FPU 寄存器。
支持 ISR 使用 FPU 寄存器,但不推荐这样做。当 ISR 使用浮点或 SIMD 寄存器时,访问会触发陷阱,当前 FPU 使用者的上下文会保存在线程对象中,随后在禁用中断的情况下恢复 ISR,以防其他 IRQ 中断该 ISR 并可能请求使用 FPU。由于 ISR 没有持久的寄存器上下文,也就没有保存 ISR 浮点上下文的机制,因此需要禁用 IRQ。
作为优化,如果某个“活跃 FPU 使用者”线程的浮点上下文曾因其他线程使用 FPU 而被保存,那么再次调度该线程时会提前恢复其浮点上下文。线程若在最近一次被调度执行期间、被换出之前使 FPU 状态变为“脏”,就会被归类为活跃 FPU 使用者。因此,如果线程在其调度时间段内未修改 FPU 状态,随后其他线程取得了 FPU 的使用权,那么第一个线程将采用按需处理方式,直到再次尝试访问 FPU 时才会恢复其浮点上下文。如果该线程在被换出之前修改了 FPU,那么再次被换入时很可能继续使用它,提前恢复浮点上下文可避免原本会产生的异常陷阱开销。
启用共享浮点寄存器模式后,每个线程对象增大 136 字节(单精度浮点硬件)或 264 字节(双精度浮点硬件)。
SPARC 架构
在 SPARC 架构上,内核将各线程视为非使用者或 FPU 使用者,且必须通过以下方法之一标记线程:
对于静态创建的线程,可向
K_THREAD_DEFINE传入K_FP_REGS选项进行标记。对于动态创建的线程,可向
k_thread_create()传入K_FP_REGS进行标记。
在退出中断处理程序时进行线程上下文切换,如果被换出的线程启用了 FPU,SPARC 内核会保存 所有 浮点寄存器。浮点寄存器保存在线程栈上。当恢复线程上下文时,当且仅当此前保存上下文时保存了浮点寄存器,才会恢复它们。浮点寄存器的保存和恢复是同步进行的,不采用延迟机制。调用 ISR 时始终禁用 FPU(与 CONFIG_FPU_SHARING 无关)。
尚未实现通过 k_float_disable() 禁用浮点功能。
使用 CONFIG_FPU_SHARING 时,每个 FPU 使用者线程需要 136 字节栈空间来加载和存储浮点寄存器。不使用 CONFIG_FPU_SHARING 时,无需额外栈空间。
x86 架构
在 x86 架构上,内核根据具体情况将各线程视为非使用者、FPU 使用者或 SSE 使用者。上下文切换采用“延迟保存”算法,仅在确有必要时才更新浮点寄存器。例如,从 FPU 使用者切换到非使用者线程,随后又切回原 FPU 使用者时,不会 保存这些寄存器。下表列出了线程为正确保存寄存器所需提供的额外栈空间。
线程类型 |
浮点寄存器使用情况 |
所需额外栈空间 |
|---|---|---|
协作式 |
任意 |
0 字节 |
抢占式 |
无 |
0 字节 |
抢占式 |
FPU |
108 字节 |
抢占式 |
SSE |
464 字节 |
线程首次访问浮点寄存器时,x86 内核会自动检测到该线程正在使用它们。如果内核配置为支持 SSE 寄存器,就将线程标记为 SSE 使用者;如果不支持 SSE 寄存器,则标记为 FPU 使用者。如果这种行为会将 FPU 使用者线程标记为 SSE 使用者,或者应用希望避免自动标记线程带来的异常处理开销,可以使用以下方法之一预先标记线程。
对于静态创建的 x86 线程,可向
K_THREAD_DEFINE传入K_FP_REGS或K_SSE_REGS选项进行预先标记。对于动态创建的 x86 线程,可向
k_thread_create()传入K_FP_REGS或K_SSE_REGS选项进行预先标记。已创建的 x86 线程可在启动后,向
k_float_enable()传入K_FP_REGS或K_SSE_REGS选项来预先标记自身。
如果 x86 线程很少使用浮点寄存器,可以调用 k_float_disable(),取消其 FPU 使用者或 SSE 使用者标记。这样可避免内核在不需要时仍为上下文切换保存浮点寄存器内容。线程再次需要使用浮点寄存器时,可调用 k_float_enable() 将自身重新标记为 FPU 使用者或 SSE 使用者。
实现
执行浮点运算
只要内核配置正确,线程无需特殊编码即可使用浮点运算。
以下代码展示函数如何使用浮点运算,在计算一系列整数值的平均值时避免溢出问题。
int average(int *values, int num_values)
{
double sum;
int i;
sum = 0.0;
for (i = 0; i < num_values; i++) {
sum += *values;
values++;
}
return (int)((sum / num_values) + 0.5);
}
使用建议
应用需要执行浮点运算时,使用内核浮点服务。
配置选项
要配置非共享浮点寄存器模式,启用 CONFIG_FPU 配置选项,并保持 CONFIG_FPU_SHARING 配置选项禁用。
要配置共享浮点寄存器模式,同时启用 CONFIG_FPU 和 CONFIG_FPU_SHARING 配置选项。此外,应按照上述说明,确保每个使用浮点寄存器的线程都有足够的额外栈空间,以便在上下文切换时保存浮点寄存器值。
对于 x86,使用 CONFIG_X86_SSE 配置选项启用对 SSEx 指令的支持。