feat(kernel): lazy user FPU: off until first FP insn (illegal-insn trap -> zeroed regs, retry), save on dirty trap, restore on switch, off for threads without + kernel; spawntest per-thread FP check
This commit is contained in:
165
crates/adapter-riscv/src/trap/fp.rs
Normal file
165
crates/adapter-riscv/src/trap/fp.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//! Floating point for user threads, lazily. A thread starts with the FPU
|
||||||
|
//! off (`sstatus.FS`); its first floating-point instruction traps as an
|
||||||
|
//! illegal instruction, and it is given zeroed registers and the FPU and
|
||||||
|
//! retries. From then on its registers are saved when a trap finds them
|
||||||
|
//! changed (FS dirty) and loaded whenever it is switched to; a thread
|
||||||
|
//! without them runs with the FPU off, so it never sees another's. The
|
||||||
|
//! kernel itself runs with the FPU off.
|
||||||
|
|
||||||
|
use core::arch::asm;
|
||||||
|
use domain::thread::{FpState, SavedContext};
|
||||||
|
|
||||||
|
use super::TrapFrame;
|
||||||
|
|
||||||
|
const FS: usize = 3 << 13;
|
||||||
|
const FS_OFF: usize = 0;
|
||||||
|
const FS_CLEAN: usize = 2 << 13;
|
||||||
|
const FS_DIRTY: usize = 3 << 13;
|
||||||
|
|
||||||
|
/// Turns the FPU off for the kernel; what the trap frame says the thread
|
||||||
|
/// returns with is unchanged.
|
||||||
|
pub(super) fn kernel_off() {
|
||||||
|
// SAFETY: clears sstatus.FS only.
|
||||||
|
unsafe { asm!("csrc sstatus, {0}", in(reg) FS) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hardware_on() {
|
||||||
|
// SAFETY: sets sstatus.FS to (at least) clean, so the FP registers can
|
||||||
|
// be read and written; `kernel_off` turns it off again.
|
||||||
|
unsafe { asm!("csrs sstatus, {0}", in(reg) FS_CLEAN) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On a trap from a thread: its registers into its context if it changed
|
||||||
|
/// them, and the frame then says they are saved.
|
||||||
|
pub(super) fn save_if_dirty(frame: &mut TrapFrame, ctx: &mut SavedContext) {
|
||||||
|
if frame.sstatus & FS != FS_DIRTY {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(state) = ctx.fp_mut() {
|
||||||
|
hardware_on();
|
||||||
|
save(state);
|
||||||
|
frame.sstatus = (frame.sstatus & !FS) | FS_CLEAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returning to a thread: its registers into the FPU, or the FPU off.
|
||||||
|
pub(super) fn load_for(frame: &mut TrapFrame, ctx: &SavedContext) {
|
||||||
|
frame.sstatus &= !FS;
|
||||||
|
match ctx.fp() {
|
||||||
|
Some(state) => {
|
||||||
|
hardware_on();
|
||||||
|
load(state);
|
||||||
|
frame.sstatus |= FS_CLEAN;
|
||||||
|
}
|
||||||
|
None => frame.sstatus |= FS_OFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An illegal instruction from a thread with the FPU off and no registers
|
||||||
|
/// yet: it gets them, zeroed, and retries. Whether that was it (otherwise
|
||||||
|
/// the instruction is a real fault).
|
||||||
|
pub(super) fn first_use(frame: &mut TrapFrame, ctx: &mut SavedContext) -> bool {
|
||||||
|
if frame.sstatus & FS != FS_OFF || ctx.fp().is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let state = ctx.enable_fp();
|
||||||
|
hardware_on();
|
||||||
|
load(state);
|
||||||
|
// sepc still names the instruction, so it runs again.
|
||||||
|
frame.sstatus = (frame.sstatus & !FS) | FS_CLEAN;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$op` (fsd or fld) for every FP register, at `{base}` + 8 each.
|
||||||
|
macro_rules! fp_registers {
|
||||||
|
($op:literal) => {
|
||||||
|
concat!(
|
||||||
|
$op,
|
||||||
|
" f0, 0({base})\n",
|
||||||
|
$op,
|
||||||
|
" f1, 8({base})\n",
|
||||||
|
$op,
|
||||||
|
" f2, 16({base})\n",
|
||||||
|
$op,
|
||||||
|
" f3, 24({base})\n",
|
||||||
|
$op,
|
||||||
|
" f4, 32({base})\n",
|
||||||
|
$op,
|
||||||
|
" f5, 40({base})\n",
|
||||||
|
$op,
|
||||||
|
" f6, 48({base})\n",
|
||||||
|
$op,
|
||||||
|
" f7, 56({base})\n",
|
||||||
|
$op,
|
||||||
|
" f8, 64({base})\n",
|
||||||
|
$op,
|
||||||
|
" f9, 72({base})\n",
|
||||||
|
$op,
|
||||||
|
" f10, 80({base})\n",
|
||||||
|
$op,
|
||||||
|
" f11, 88({base})\n",
|
||||||
|
$op,
|
||||||
|
" f12, 96({base})\n",
|
||||||
|
$op,
|
||||||
|
" f13, 104({base})\n",
|
||||||
|
$op,
|
||||||
|
" f14, 112({base})\n",
|
||||||
|
$op,
|
||||||
|
" f15, 120({base})\n",
|
||||||
|
$op,
|
||||||
|
" f16, 128({base})\n",
|
||||||
|
$op,
|
||||||
|
" f17, 136({base})\n",
|
||||||
|
$op,
|
||||||
|
" f18, 144({base})\n",
|
||||||
|
$op,
|
||||||
|
" f19, 152({base})\n",
|
||||||
|
$op,
|
||||||
|
" f20, 160({base})\n",
|
||||||
|
$op,
|
||||||
|
" f21, 168({base})\n",
|
||||||
|
$op,
|
||||||
|
" f22, 176({base})\n",
|
||||||
|
$op,
|
||||||
|
" f23, 184({base})\n",
|
||||||
|
$op,
|
||||||
|
" f24, 192({base})\n",
|
||||||
|
$op,
|
||||||
|
" f25, 200({base})\n",
|
||||||
|
$op,
|
||||||
|
" f26, 208({base})\n",
|
||||||
|
$op,
|
||||||
|
" f27, 216({base})\n",
|
||||||
|
$op,
|
||||||
|
" f28, 224({base})\n",
|
||||||
|
$op,
|
||||||
|
" f29, 232({base})\n",
|
||||||
|
$op,
|
||||||
|
" f30, 240({base})\n",
|
||||||
|
$op,
|
||||||
|
" f31, 248({base})\n",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The FPU's registers into `state` (the FPU must be on).
|
||||||
|
fn save(state: &mut FpState) {
|
||||||
|
let base = state.registers.as_mut_ptr();
|
||||||
|
let control: u64;
|
||||||
|
// SAFETY: `base` points at 32 u64s, written in place; frcsr only reads.
|
||||||
|
unsafe {
|
||||||
|
asm!(fp_registers!("fsd"), base = in(reg) base, options(nostack));
|
||||||
|
asm!("frcsr {0}", out(reg) control, options(nostack));
|
||||||
|
}
|
||||||
|
state.control = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `state` into the FPU's registers (the FPU must be on).
|
||||||
|
fn load(state: &FpState) {
|
||||||
|
let base = state.registers.as_ptr();
|
||||||
|
// SAFETY: `base` points at 32 u64s, only read; fscsr writes fcsr.
|
||||||
|
unsafe {
|
||||||
|
asm!(fp_registers!("fld"), base = in(reg) base, options(nostack, readonly));
|
||||||
|
asm!("fscsr {0}", in(reg) state.control, options(nostack));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod fast;
|
mod fast;
|
||||||
|
mod fp;
|
||||||
mod sched;
|
mod sched;
|
||||||
mod vector;
|
mod vector;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const CAUSE_INTERRUPT_BIT: usize = 1 << 63;
|
|||||||
const CAUSE_S_SOFTWARE: usize = 1;
|
const CAUSE_S_SOFTWARE: usize = 1;
|
||||||
const CAUSE_S_TIMER: usize = 5;
|
const CAUSE_S_TIMER: usize = 5;
|
||||||
const CAUSE_S_EXTERNAL: usize = 9;
|
const CAUSE_S_EXTERNAL: usize = 9;
|
||||||
|
const CAUSE_ILLEGAL_INSTRUCTION: usize = 2;
|
||||||
const CAUSE_ECALL_U: usize = 8;
|
const CAUSE_ECALL_U: usize = 8;
|
||||||
/// Set in `sstatus` when the trap came from supervisor mode.
|
/// Set in `sstatus` when the trap came from supervisor mode.
|
||||||
const SSTATUS_SPP: usize = 1 << 8;
|
const SSTATUS_SPP: usize = 1 << 8;
|
||||||
@@ -29,6 +31,8 @@ pub fn init() {
|
|||||||
let addr = trap_vector as *const () as usize;
|
let addr = trap_vector as *const () as usize;
|
||||||
core::arch::asm!("csrw stvec, {0}", in(reg) addr);
|
core::arch::asm!("csrw stvec, {0}", in(reg) addr);
|
||||||
core::arch::asm!("csrw sscratch, zero");
|
core::arch::asm!("csrw sscratch, zero");
|
||||||
|
// The kernel never uses the FPU; user threads get it lazily.
|
||||||
|
core::arch::asm!("csrc sstatus, {0}", in(reg) 3usize << 13);
|
||||||
// SIE stays clear; interrupts are enabled only by sret into userspace.
|
// SIE stays clear; interrupts are enabled only by sret into userspace.
|
||||||
let sie_bits = (1usize << 5) | (1usize << 9) | (1usize << 1);
|
let sie_bits = (1usize << 5) | (1usize << 9) | (1usize << 1);
|
||||||
core::arch::asm!("csrs sie, {0}", in(reg) sie_bits);
|
core::arch::asm!("csrs sie, {0}", in(reg) sie_bits);
|
||||||
@@ -90,6 +94,14 @@ extern "C" fn trap_handler_rust(frame: &mut TrapFrame) {
|
|||||||
}
|
}
|
||||||
crate::smp::service_ipi();
|
crate::smp::service_ipi();
|
||||||
crate::smp::lock();
|
crate::smp::lock();
|
||||||
|
// The thread's FP registers are saved before anything can switch away.
|
||||||
|
if let Some(ctx) = kern()
|
||||||
|
.current_thread()
|
||||||
|
.and_then(|t| kern().thread_context_mut(t))
|
||||||
|
{
|
||||||
|
fp::save_if_dirty(frame, ctx);
|
||||||
|
}
|
||||||
|
fp::kernel_off();
|
||||||
if scause & CAUSE_INTERRUPT_BIT != 0 {
|
if scause & CAUSE_INTERRUPT_BIT != 0 {
|
||||||
handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT);
|
handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT);
|
||||||
} else {
|
} else {
|
||||||
@@ -163,6 +175,11 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CAUSE_ILLEGAL_INSTRUCTION
|
||||||
|
if kern()
|
||||||
|
.current_thread()
|
||||||
|
.and_then(|t| kern().thread_context_mut(t))
|
||||||
|
.is_some_and(|ctx| fp::first_use(frame, ctx)) => {}
|
||||||
_ => {
|
_ => {
|
||||||
let uart = crate::hw::uart::Uart::new();
|
let uart = crate::hw::uart::Uart::new();
|
||||||
uart.write_str("FAULT: scause=");
|
uart.write_str("FAULT: scause=");
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFra
|
|||||||
frame.sstatus = ctx.status();
|
frame.sstatus = ctx.status();
|
||||||
frame.sstatus &= !(1 << 8); // SPP: return to U-mode
|
frame.sstatus &= !(1 << 8); // SPP: return to U-mode
|
||||||
frame.sstatus |= 1 << 5; // SPIE
|
frame.sstatus |= 1 << 5; // SPIE
|
||||||
|
super::fp::load_for(frame, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) {
|
fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) {
|
||||||
|
|||||||
@@ -173,6 +173,57 @@ fn encrypted_home(fs: &mut Storage, home_fs: &mut Storage) -> Result<(), &'stati
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Floating-point work that only comes out right if nobody else's
|
||||||
|
/// registers get in: a sum over `ROUNDS`, yielding (so switching threads)
|
||||||
|
/// all the way.
|
||||||
|
fn float_work(step: f64) -> u64 {
|
||||||
|
const ROUNDS: u32 = 2000;
|
||||||
|
let mut sum = 0.0f64;
|
||||||
|
let mut x = core::hint::black_box(step);
|
||||||
|
for _ in 0..ROUNDS {
|
||||||
|
sum += x * 0.5;
|
||||||
|
x += step;
|
||||||
|
syscall::yield_now();
|
||||||
|
}
|
||||||
|
sum.to_bits()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `float_work(step)` gives when nothing interferes.
|
||||||
|
fn float_expected(step: f64) -> u64 {
|
||||||
|
let mut sum = 0.0f64;
|
||||||
|
let mut x = step;
|
||||||
|
for _ in 0..2000 {
|
||||||
|
sum += x * 0.5;
|
||||||
|
x += step;
|
||||||
|
}
|
||||||
|
sum.to_bits()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The other thread's answer, once it has one.
|
||||||
|
static FLOAT_RESULT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
|
||||||
|
|
||||||
|
fn float_thread(_: &'static core::sync::atomic::AtomicU64) -> ! {
|
||||||
|
FLOAT_RESULT.store(float_work(0.25), core::sync::atomic::Ordering::Release);
|
||||||
|
syscall::exit_thread()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two threads computing with floats at once each get their own registers
|
||||||
|
/// (the kernel saves and restores them on every switch).
|
||||||
|
fn floating_point_is_kept_per_thread() -> Result<(), &'static str> {
|
||||||
|
if !libkos::thread::spawn(&FLOAT_RESULT, float_thread) {
|
||||||
|
return Err("cannot start a thread");
|
||||||
|
}
|
||||||
|
let mine = float_work(1.75);
|
||||||
|
while FLOAT_RESULT.load(core::sync::atomic::Ordering::Acquire) == 0 {
|
||||||
|
syscall::yield_now();
|
||||||
|
}
|
||||||
|
let theirs = FLOAT_RESULT.load(core::sync::atomic::Ordering::Acquire);
|
||||||
|
if mine != float_expected(1.75) || theirs != float_expected(0.25) {
|
||||||
|
return Err("floating-point registers leaked between threads");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// How many requests the `flaky` answering now has served.
|
/// How many requests the `flaky` answering now has served.
|
||||||
fn flaky() -> Result<usize, &'static str> {
|
fn flaky() -> Result<usize, &'static str> {
|
||||||
syscall::request(FLAKY, [flaky::op::WHO, 0, 0, 0]).map(|r| r[0]).map_err(|_| "flaky does not answer")
|
syscall::request(FLAKY, [flaky::op::WHO, 0, 0, 0]).map(|r| r[0]).map_err(|_| "flaky does not answer")
|
||||||
@@ -246,6 +297,9 @@ fn check() -> Result<(), &'static str> {
|
|||||||
revoke_stops_a_running_app(died)?;
|
revoke_stops_a_running_app(died)?;
|
||||||
libkos::println!("spawntest: revoking stops a running app, allowing lets it run: ok");
|
libkos::println!("spawntest: revoking stops a running app, allowing lets it run: ok");
|
||||||
|
|
||||||
|
floating_point_is_kept_per_thread()?;
|
||||||
|
libkos::println!("spawntest: floating point is kept per thread: ok");
|
||||||
|
|
||||||
restarted_after_crashes()?;
|
restarted_after_crashes()?;
|
||||||
libkos::println!("spawntest: a crashed server is restarted, a crash loop backs off: ok");
|
libkos::println!("spawntest: a crashed server is restarted, a crash loop backs off: ok");
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
//! Saved register state of a thread.
|
//! Saved register state of a thread.
|
||||||
|
|
||||||
|
/// A thread's floating-point registers and their control word. A thread
|
||||||
|
/// has none until its first floating-point instruction (see
|
||||||
|
/// `SavedContext::enable_fp`): until then it cannot see another thread's.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct FpState {
|
||||||
|
pub registers: [u64; 32],
|
||||||
|
pub control: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FpState {
|
||||||
|
pub const ZERO: Self = Self {
|
||||||
|
registers: [0; 32],
|
||||||
|
control: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SavedContext {
|
pub struct SavedContext {
|
||||||
registers: [usize; 32],
|
registers: [usize; 32],
|
||||||
pc: usize,
|
pc: usize,
|
||||||
status: usize,
|
status: usize,
|
||||||
|
fp: FpState,
|
||||||
|
fp_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SavedContext {
|
impl SavedContext {
|
||||||
@@ -12,6 +30,8 @@ impl SavedContext {
|
|||||||
registers: [0; 32],
|
registers: [0; 32],
|
||||||
pc: 0,
|
pc: 0,
|
||||||
status: 0,
|
status: 0,
|
||||||
|
fp: FpState::ZERO,
|
||||||
|
fp_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,4 +68,39 @@ impl SavedContext {
|
|||||||
pub fn registers_mut(&mut self) -> &mut [usize; 32] {
|
pub fn registers_mut(&mut self) -> &mut [usize; 32] {
|
||||||
&mut self.registers
|
&mut self.registers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Its floating-point registers, if it has used any.
|
||||||
|
pub fn fp(&self) -> Option<&FpState> {
|
||||||
|
self.fp_enabled.then_some(&self.fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fp_mut(&mut self) -> Option<&mut FpState> {
|
||||||
|
self.fp_enabled.then_some(&mut self.fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives it floating-point registers, all zero.
|
||||||
|
pub fn enable_fp(&mut self) -> &mut FpState {
|
||||||
|
self.fp = FpState::ZERO;
|
||||||
|
self.fp_enabled = true;
|
||||||
|
&mut self.fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_thread_has_no_fp_until_it_is_given_zeroed_registers() {
|
||||||
|
let mut c = SavedContext::empty();
|
||||||
|
assert!(c.fp().is_none() && c.fp_mut().is_none());
|
||||||
|
c.enable_fp().registers[3] = 7;
|
||||||
|
assert_eq!(c.fp().unwrap().registers[3], 7);
|
||||||
|
c.enable_fp();
|
||||||
|
assert_eq!(
|
||||||
|
c.fp(),
|
||||||
|
Some(&FpState::ZERO),
|
||||||
|
"enabling again starts from zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::error::KernelError;
|
|||||||
mod context;
|
mod context;
|
||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
pub use context::SavedContext;
|
pub use context::{FpState, SavedContext};
|
||||||
pub use table::{MAX_THREADS, ThreadTable};
|
pub use table::{MAX_THREADS, ThreadTable};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -320,6 +320,15 @@ set it stays masked.
|
|||||||
- A saved reply is named only within the thread that saved it: it cannot
|
- A saved reply is named only within the thread that saved it: it cannot
|
||||||
be passed on, and another thread's number means nothing.
|
be passed on, and another thread's number means nothing.
|
||||||
|
|
||||||
|
## Floating point
|
||||||
|
|
||||||
|
A thread starts without floating-point registers, and the FPU off. Its
|
||||||
|
first floating-point instruction traps; the kernel gives it registers, all
|
||||||
|
zero, turns the FPU on and runs the instruction again. From then on its
|
||||||
|
registers are saved when a trap finds them changed and loaded whenever it
|
||||||
|
runs; a thread without them runs with the FPU off, so it can never read
|
||||||
|
another's. The kernel itself never uses the FPU.
|
||||||
|
|
||||||
## Budgets
|
## Budgets
|
||||||
|
|
||||||
Every process has a budget: frames (its pages, page tables and regions'
|
Every process has a budget: frames (its pages, page tables and regions'
|
||||||
|
|||||||
Reference in New Issue
Block a user