276 lines
9.0 KiB
Rust
276 lines
9.0 KiB
Rust
//! Harts. Each hart has its own kernel stack, and its hart id doubles as
|
|
//! its kernel `CoreId`. The kernel is entered under one lock for now (stage
|
|
//! 1 of multi-core; the kernel itself stays a sequential program), so
|
|
//! userspace runs in parallel and the kernel does not.
|
|
//!
|
|
//! Harts talk through IPIs carrying two kinds of request:
|
|
//! - reschedule: the kernel made work for this hart, or took its thread
|
|
//! away; handled under the lock on the way out of the kernel.
|
|
//! - leave root R: a page table this hart may be using is changing or about
|
|
//! to be freed; the hart switches to the kernel's root and flushes its
|
|
//! TLB. This needs no lock, so it is serviced even while a hart spins
|
|
//! waiting for the lock or idles, and the sender (which holds the lock)
|
|
//! can wait for the acknowledgement without deadlocking.
|
|
|
|
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
|
|
|
use crate::mmu;
|
|
|
|
/// Must match linker.ld.
|
|
pub const MAX_HARTS: usize = 8;
|
|
/// A guard page and a 512 KiB stack (boot builds big tables on it). Must
|
|
/// match linker.ld.
|
|
pub const STACK_SLOT: usize = 4096 + 0x8_0000;
|
|
|
|
const SIP_SSIP: usize = 1 << 1;
|
|
|
|
/// The hart running this code, from which kernel stack it is on.
|
|
pub fn hart_id() -> usize {
|
|
let sp: usize;
|
|
// SAFETY: reading sp has no side effects.
|
|
unsafe { core::arch::asm!("mv {0}, sp", out(reg) sp) };
|
|
(sp - stacks_start()) / STACK_SLOT
|
|
}
|
|
|
|
pub fn stacks_start() -> usize {
|
|
unsafe extern "C" {
|
|
static _stacks_start: u8;
|
|
}
|
|
// SAFETY: only the symbol's address is taken.
|
|
unsafe { &_stacks_start as *const u8 as usize }
|
|
}
|
|
|
|
static BOOT_HART: AtomicUsize = AtomicUsize::new(0);
|
|
static NUM_HARTS: AtomicUsize = AtomicUsize::new(1);
|
|
/// Physical address of the kernel's own root table.
|
|
static KERNEL_ROOT: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
/// The root table each hart translates with.
|
|
static ACTIVE_ROOT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
|
|
static RESCHEDULE: [AtomicBool; MAX_HARTS] = [const { AtomicBool::new(false) }; MAX_HARTS];
|
|
static LEAVE_ROOT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
|
|
static LEFT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
|
|
|
|
pub fn boot_hart() -> usize {
|
|
BOOT_HART.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn num_harts() -> usize {
|
|
NUM_HARTS.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn kernel_root() -> usize {
|
|
KERNEL_ROOT.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Records which hart booted and how many there are, and where the
|
|
/// kernel's root table is.
|
|
pub fn init(boot: usize, harts: usize, kernel_root: usize) {
|
|
BOOT_HART.store(boot, Ordering::Relaxed);
|
|
NUM_HARTS.store(harts, Ordering::Relaxed);
|
|
KERNEL_ROOT.store(kernel_root, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Called by `mmu::activate`.
|
|
pub fn note_active_root(root: usize) {
|
|
if let Some(r) = ACTIVE_ROOT.get(hart_id()) {
|
|
r.store(root, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
// ---- the kernel lock ----
|
|
|
|
static NEXT_TICKET: AtomicU32 = AtomicU32::new(0);
|
|
static SERVING: AtomicU32 = AtomicU32::new(0);
|
|
|
|
/// Takes the kernel lock, first come first served. Leave-root requests
|
|
/// are serviced while waiting.
|
|
pub fn lock() {
|
|
let ticket = NEXT_TICKET.fetch_add(1, Ordering::Relaxed);
|
|
while SERVING.load(Ordering::Acquire) != ticket {
|
|
service_ipi();
|
|
core::hint::spin_loop();
|
|
}
|
|
}
|
|
|
|
pub fn unlock() {
|
|
SERVING.fetch_add(1, Ordering::Release);
|
|
}
|
|
|
|
// ---- what lock-free fast paths may know ----
|
|
//
|
|
// Published under the kernel lock just before it is released, read without
|
|
// it. A fast path that reads them while another hart holds the lock simply
|
|
// takes effect before that hart's operation.
|
|
|
|
/// Per core: highest waiting priority + 1, 0 if its queue is empty.
|
|
static WAITING: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
|
|
/// Per hart: the running thread's process id + 1 and priority + 1.
|
|
static RUNNING_PID: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
|
|
static RUNNING_PRIORITY: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
|
|
|
|
pub fn publish_waiting(core: usize, highest: Option<u8>) {
|
|
if let Some(w) = WAITING.get(core) {
|
|
w.store(highest.map_or(0, |p| p as usize + 1), Ordering::Release);
|
|
}
|
|
}
|
|
|
|
pub fn publish_running(pid: Option<u16>, priority: Option<u8>) {
|
|
let me = hart_id();
|
|
if let (Some(p), Some(q)) = (RUNNING_PID.get(me), RUNNING_PRIORITY.get(me)) {
|
|
p.store(pid.map_or(0, |x| x as usize + 1), Ordering::Release);
|
|
q.store(priority.map_or(0, |x| x as usize + 1), Ordering::Release);
|
|
}
|
|
}
|
|
|
|
/// The process the thread running on this hart belongs to.
|
|
pub fn running_pid() -> Option<usize> {
|
|
let v = RUNNING_PID.get(hart_id())?.load(Ordering::Acquire);
|
|
v.checked_sub(1)
|
|
}
|
|
|
|
/// Whether a Yield by the running thread would change nothing: no thread
|
|
/// waits on this core at its priority or above.
|
|
pub fn yield_is_noop() -> bool {
|
|
let me = hart_id();
|
|
let (Some(w), Some(p)) = (WAITING.get(me), RUNNING_PRIORITY.get(me)) else {
|
|
return false;
|
|
};
|
|
let mine = p.load(Ordering::Acquire);
|
|
mine != 0 && w.load(Ordering::Acquire) < mine
|
|
}
|
|
|
|
/// Whether this hart has an IPI or a reschedule request it has not acted
|
|
/// on; fast paths must then take the slow path.
|
|
pub fn has_pending_request() -> bool {
|
|
let sip: usize;
|
|
// SAFETY: reading sip has no side effects.
|
|
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
|
|
sip & SIP_SSIP != 0
|
|
|| RESCHEDULE
|
|
.get(hart_id())
|
|
.is_some_and(|r| r.load(Ordering::SeqCst))
|
|
}
|
|
|
|
// ---- IPIs ----
|
|
|
|
/// The lock-free half of an IPI: acknowledges it and, if asked, leaves the
|
|
/// root being changed. Safe to call at any time, any number of times.
|
|
pub fn service_ipi() {
|
|
let sip: usize;
|
|
// SAFETY: reading sip has no side effects.
|
|
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
|
|
if sip & SIP_SSIP == 0 {
|
|
return;
|
|
}
|
|
// Clear first: a request posted after this raises SSIP again.
|
|
// SAFETY: clearing our own pending software interrupt.
|
|
unsafe { core::arch::asm!("csrc sip, {0}", in(reg) SIP_SSIP) };
|
|
let me = hart_id();
|
|
let (Some(leave), Some(left)) = (LEAVE_ROOT.get(me), LEFT.get(me)) else {
|
|
return;
|
|
};
|
|
let root = leave.swap(0, Ordering::SeqCst);
|
|
if root != 0 {
|
|
if ACTIVE_ROOT.get(me).map(|r| r.load(Ordering::SeqCst)) == Some(root) {
|
|
mmu::activate(mmu::page_table::table_at(kernel_root()));
|
|
}
|
|
// SAFETY: flushing translations has no other effect.
|
|
unsafe { core::arch::asm!("sfence.vma zero, zero") };
|
|
left.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
/// Asks `hart` to look at its run queue again, on its way out of the
|
|
/// kernel.
|
|
pub fn kick(hart: usize) {
|
|
if let Some(r) = RESCHEDULE.get(hart) {
|
|
r.store(true, Ordering::SeqCst);
|
|
sbi_send_ipi(hart);
|
|
}
|
|
}
|
|
|
|
/// Consumes this hart's reschedule request.
|
|
pub fn take_reschedule() -> bool {
|
|
RESCHEDULE
|
|
.get(hart_id())
|
|
.is_some_and(|r| r.swap(false, Ordering::SeqCst))
|
|
}
|
|
|
|
/// Makes every other hart that translates through `root` leave it and
|
|
/// flush, and waits until each has. Called with the kernel lock held, so
|
|
/// no hart can start using `root` meanwhile.
|
|
pub fn shoot_down(root: usize) {
|
|
let me = hart_id();
|
|
for hart in 0..num_harts() {
|
|
if hart == me || ACTIVE_ROOT[hart].load(Ordering::SeqCst) != root {
|
|
continue;
|
|
}
|
|
let seen = LEFT[hart].load(Ordering::SeqCst);
|
|
LEAVE_ROOT[hart].store(root, Ordering::SeqCst);
|
|
sbi_send_ipi(hart);
|
|
while LEFT[hart].load(Ordering::SeqCst) == seen {
|
|
core::hint::spin_loop();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- SBI ----
|
|
|
|
const SBI_EXT_IPI: usize = 0x735049;
|
|
const SBI_EXT_HSM: usize = 0x48534D;
|
|
|
|
fn sbi_send_ipi(hart: usize) {
|
|
// SAFETY: an SBI call; a0 and a1 come back clobbered.
|
|
unsafe {
|
|
core::arch::asm!(
|
|
"ecall",
|
|
inlateout("a0") 1usize => _,
|
|
inlateout("a1") hart => _,
|
|
in("a6") 0usize,
|
|
in("a7") SBI_EXT_IPI,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Whether `hart` exists, per SBI HSM `hart_get_status`.
|
|
fn sbi_hart_exists(hart: usize) -> bool {
|
|
let error: isize;
|
|
// SAFETY: an SBI call; a0 and a1 come back clobbered.
|
|
unsafe {
|
|
core::arch::asm!(
|
|
"ecall",
|
|
inlateout("a0") hart => error,
|
|
lateout("a1") _,
|
|
in("a6") 2usize,
|
|
in("a7") SBI_EXT_HSM,
|
|
);
|
|
}
|
|
error == 0
|
|
}
|
|
|
|
/// The number of harts, counting up from hart 0 until one is missing.
|
|
pub fn probe_harts() -> usize {
|
|
(0..MAX_HARTS)
|
|
.take_while(|h| sbi_hart_exists(*h))
|
|
.count()
|
|
.max(1)
|
|
}
|
|
|
|
/// Starts `hart` at the physical address `entry` with the MMU off.
|
|
pub fn sbi_hart_start(hart: usize, entry: usize) -> bool {
|
|
let error: isize;
|
|
// SAFETY: an SBI call; a0 and a1 come back clobbered.
|
|
unsafe {
|
|
core::arch::asm!(
|
|
"ecall",
|
|
inlateout("a0") hart => error,
|
|
inlateout("a1") entry => _,
|
|
in("a2") 0usize,
|
|
in("a6") 0usize,
|
|
in("a7") SBI_EXT_HSM,
|
|
);
|
|
}
|
|
error == 0
|
|
}
|