Skip to main content

strat9_kernel/arch/x86_64/
percpu.rs

1//! Per-CPU data (x86_64)
2//!
3//! Minimal per-CPU tracking for SMP bring-up. This keeps CPU identity,
4//! online state, and per-CPU kernel stack top pointers.
5
6use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
7
8/// Maximum number of CPUs supported for now.
9pub const MAX_CPUS: usize = 32;
10
11/// Offsets used by the SYSCALL entry (must match `PerCpuArch` layout).
12/// Note: cpu_index is at offset 0 (8 bytes).
13pub const USER_RSP_OFFSET: usize = 8;
14pub const KERNEL_RSP_OFFSET: usize = 16;
15
16/// Minimal per-CPU block accessed from assembly via GS base.
17///
18/// **Layout invariant** (`#[repr(C)]`):
19/// - `cpu_index` at offset  0 (8 bytes) : read by assembly as `gs:[0]`
20/// - `user_rsp`  at offset  8 (8 bytes)
21/// - `kernel_rsp` at offset 16 (8 bytes)
22///
23/// `AtomicU64` is `#[repr(C, align(8))]` and has the same size/alignment as
24/// `u64`, so the offsets declared in `USER_RSP_OFFSET`/`KERNEL_RSP_OFFSET` are
25/// preserved. The inline assembly in `current_cpu_index()` reads the raw 8
26/// bytes from `gs:[0]`, which are the inner `u64` of `AtomicU64`.
27#[repr(C)]
28pub struct PerCpuArch {
29    /// CPU index for this block. Written once during init via `AtomicU64::store`
30    /// (no raw-pointer cast needed), then only ever read. `AtomicU64` provides
31    /// proper `UnsafeCell` interior mutability so reads through `&PerCpuArch`
32    /// are not UB even though the field was written after the struct was placed
33    /// in a `static`.
34    pub cpu_index: AtomicU64, // offset 0 : read by assembly: `mov rax, gs:[0]`
35    pub user_rsp: AtomicU64,   // offset 8
36    pub kernel_rsp: AtomicU64, // offset 16
37}
38
39const _: () = assert!(core::mem::offset_of!(PerCpuArch, user_rsp) == USER_RSP_OFFSET);
40const _: () = assert!(core::mem::offset_of!(PerCpuArch, kernel_rsp) == KERNEL_RSP_OFFSET);
41
42/// Per-CPU state.
43#[repr(C)]
44pub struct PerCpu {
45    pub arch: PerCpuArch,
46    present: AtomicBool,
47    online: AtomicBool,
48    apic_id: AtomicU32,
49    kernel_stack_top: AtomicU64,
50    tlb_ready: AtomicBool,
51    /// Preemption-disable depth counter.
52    /// When > 0, `maybe_preempt()` and `yield_task()` are no-ops on this CPU.
53    pub preempt_count: AtomicU32,
54    /// Fast signal-pending hint for the current CPU.
55    ///
56    /// Set by `send_signal` when a signal targets the task currently running
57    /// on this CPU. Checked by the syscall dispatcher to avoid the expensive
58    /// `current_task_clone()` + scheduler lock when no signal is pending.
59    ///
60    /// AtomicU32 (not AtomicBool) for better cache-line performance.
61    ///
62    /// NOTE TODO : the extra bits are reserved for future fast-pending flags.
63    pub signal_pending: AtomicU32,
64}
65
66impl PerCpu {
67    /// Creates a new instance.
68    pub const fn new() -> Self {
69        Self {
70            arch: PerCpuArch {
71                cpu_index: AtomicU64::new(0),
72                user_rsp: AtomicU64::new(0),
73                kernel_rsp: AtomicU64::new(0),
74            },
75            present: AtomicBool::new(false),
76            online: AtomicBool::new(false),
77            apic_id: AtomicU32::new(0),
78            kernel_stack_top: AtomicU64::new(0),
79            tlb_ready: AtomicBool::new(false),
80            preempt_count: AtomicU32::new(0),
81            signal_pending: AtomicU32::new(0),
82        }
83    }
84
85    /// Performs the apic id operation.
86    pub fn apic_id(&self) -> u32 {
87        self.apic_id.load(Ordering::Acquire)
88    }
89
90    /// Performs the online operation.
91    pub fn online(&self) -> bool {
92        self.online.load(Ordering::Acquire)
93    }
94}
95
96static CPU_COUNT: AtomicUsize = AtomicUsize::new(0);
97static PERCPU: [PerCpu; MAX_CPUS] = [const { PerCpu::new() }; MAX_CPUS];
98
99/// Set to `true` once the BSP has called `init_gs_base`.
100///
101/// Used by `current_cpu_index()` to skip the serialising `rdmsr` null-guard
102/// on every hot-path call once GS is permanently initialised.
103///
104/// **Invariant**: APs always call `init_gs_base` *before* the first call to
105/// `current_cpu_index()` on that AP (see `smp_main` in `smp.rs`).  Once
106/// `BSP_GS_INITIALIZED` is true, any CPU that could possibly call
107/// `current_cpu_index()` must already have a valid GS base, so the direct
108/// `gs:[0]` read on the fast path is safe.
109static BSP_GS_INITIALIZED: AtomicBool = AtomicBool::new(false);
110
111/// Initialize the boot CPU (BSP) entry.
112pub fn init_boot_cpu(apic_id: u32) -> usize {
113    let cpu = &PERCPU[0];
114    cpu.present.store(true, Ordering::Release);
115    cpu.online.store(true, Ordering::Release);
116    cpu.apic_id.store(apic_id, Ordering::Release);
117    // AtomicU64::store through shared ref : no unsafe needed, UnsafeCell provides
118    // interior mutability so this write is well-defined.
119    cpu.arch.cpu_index.store(0, Ordering::Release);
120    cpu.tlb_ready.store(false, Ordering::Release);
121    CPU_COUNT.store(1, Ordering::Release);
122    0
123}
124
125/// Register a new CPU by APIC ID, returning its CPU index.
126pub fn register_cpu(apic_id: u32) -> Option<usize> {
127    for (idx, cpu) in PERCPU.iter().enumerate() {
128        if cpu
129            .present
130            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
131            .is_ok()
132        {
133            cpu.online.store(false, Ordering::Release);
134            cpu.apic_id.store(apic_id, Ordering::Release);
135            cpu.arch.cpu_index.store(idx as u64, Ordering::Release);
136            cpu.tlb_ready.store(false, Ordering::Release);
137            CPU_COUNT.fetch_add(1, Ordering::AcqRel);
138            return Some(idx);
139        }
140    }
141    None
142}
143
144/// Mark a CPU as online by APIC ID.
145pub fn mark_online_by_apic(apic_id: u32) -> Option<usize> {
146    for (idx, cpu) in PERCPU.iter().enumerate() {
147        if cpu.present.load(Ordering::Acquire) && cpu.apic_id.load(Ordering::Acquire) == apic_id {
148            cpu.online.store(true, Ordering::Release);
149            cpu.tlb_ready.store(false, Ordering::Release);
150            // Re-confirm index in arch block (no-op if already set, safe to repeat).
151            cpu.arch.cpu_index.store(idx as u64, Ordering::Release);
152            return Some(idx);
153        }
154    }
155    None
156}
157
158/// Set the per-CPU kernel stack top for the given CPU index.
159pub fn set_kernel_stack_top(index: usize, rsp: u64) {
160    if let Some(cpu) = PERCPU.get(index) {
161        cpu.kernel_stack_top.store(rsp, Ordering::Release);
162    }
163}
164
165/// Get the per-CPU kernel stack top for the given CPU index.
166pub fn kernel_stack_top(index: usize) -> Option<u64> {
167    PERCPU
168        .get(index)
169        .map(|cpu| cpu.kernel_stack_top.load(Ordering::Acquire))
170}
171
172/// Set the per-CPU SYSCALL kernel RSP (used by syscall entry).
173pub fn set_kernel_rsp_for_cpu(index: usize, rsp: u64) {
174    if let Some(cpu) = PERCPU.get(index) {
175        cpu.arch.kernel_rsp.store(rsp, Ordering::Release);
176    }
177}
178
179/// Set the per-CPU SYSCALL kernel RSP for the current CPU.
180pub fn set_kernel_rsp_current(rsp: u64) {
181    let cpu_index = current_cpu_index();
182    set_kernel_rsp_for_cpu(cpu_index, rsp);
183}
184
185/// Initialize GS base for this CPU to point at its per-CPU block.
186///
187/// Sets `IA32_GS_BASE` (0xC000_0101, current GS base) to
188/// `&PERCPU[cpu_index].arch` for kernel execution, and initializes
189/// `IA32_KERNEL_GS_BASE` (0xC000_0102) to 0 as the initial user GS base.
190///
191/// For the BSP (cpu_index == 0) this also sets `BSP_GS_INITIALIZED`, enabling
192/// the fast (non-serialising) path in `current_cpu_index()`.
193///
194/// **Ordering for APs**: in `smp_main`, `init_gs_base` is called before the
195/// first `current_cpu_index()` can execute on that AP, so it is safe to take
196/// the fast path on the AP after the BSP flag is visible.
197pub fn init_gs_base(cpu_index: usize) {
198    let base = &PERCPU[cpu_index].arch as *const PerCpuArch as u64;
199    // IA32_GS_BASE = 0xC000_0101, IA32_KERNEL_GS_BASE = 0xC000_0102.
200    // Keep GS_BASE on kernel per-CPU for Ring 0; seed KERNEL_GS_BASE with 0
201    // so the first Ring 0->3 transition can restore a non-kernel user GS.
202    crate::arch::x86_64::wrmsr(0xC000_0101, base);
203    crate::arch::x86_64::wrmsr(0xC000_0102, 0);
204
205    if cpu_index == 0 {
206        // Release ordering: all prior init writes must be visible before any
207        // CPU sees the flag and takes the fast path in current_cpu_index().
208        BSP_GS_INITIALIZED.store(true, Ordering::Release);
209    }
210}
211
212/// Find a CPU index by APIC ID.
213pub fn cpu_index_by_apic(apic_id: u32) -> Option<usize> {
214    for (idx, cpu) in PERCPU.iter().enumerate() {
215        if cpu.present.load(Ordering::Acquire) && cpu.apic_id.load(Ordering::Acquire) == apic_id {
216            return Some(idx);
217        }
218    }
219    None
220}
221
222/// Get the total number of CPUs that have been registered.
223pub fn cpu_count() -> usize {
224    CPU_COUNT.load(Ordering::Acquire)
225}
226
227/// Get the total number of CPUs (public alias for OSTD compatibility).
228pub fn get_cpu_count() -> usize {
229    cpu_count()
230}
231
232// ========== Preemption helpers ==========
233
234/// Increment the preemption-disable depth for the current CPU.
235/// When depth > 0, the scheduler will not preempt this CPU.
236///
237/// Safe to call from any Ring-0 context **after** `init_gs_base` has run on
238/// this CPU.  If called before GS is initialised (early boot), `current_cpu_index`
239/// returns 0, which is always the BSP slot.  On the BSP itself that is correct;
240/// on an AP before its GS base is set the call is a no-op because the AP's
241/// scheduler is not yet running : incrementing slot 0 would be wrong, so we
242/// verify the GS index matches the slot we are about to touch.
243#[inline]
244pub fn preempt_disable() {
245    let idx = current_cpu_index();
246    // SAFETY: idx is clamped to [0, MAX_CPUS-1] by current_cpu_index().
247    // Additional guard: if GS is not yet set on this CPU, cpu_index will be
248    // the BSP's slot (0).  Skip if the slot's stored cpu_index disagrees with
249    // idx : that means GS isn't set here yet.
250    if PERCPU[idx].arch.cpu_index.load(Ordering::Relaxed) as usize == idx {
251        PERCPU[idx].preempt_count.fetch_add(1, Ordering::Relaxed);
252    }
253}
254
255/// Decrement the preemption-disable depth for the current CPU.
256/// Must be paired with exactly one prior call to `preempt_disable`.
257///
258/// Includes an underflow guard: if the counter is already 0 (mismatched call),
259/// the decrement is skipped and a warning is emitted rather than letting u32
260/// wrap to u32::MAX and disabling the scheduler permanently.
261#[inline]
262pub fn preempt_enable() {
263    let idx = current_cpu_index();
264    if PERCPU[idx].arch.cpu_index.load(Ordering::Relaxed) as usize == idx {
265        // Load first: prevent wrapping from 0 to u32::MAX.
266        let prev = PERCPU[idx].preempt_count.load(Ordering::Relaxed);
267        if prev > 0 {
268            PERCPU[idx].preempt_count.fetch_sub(1, Ordering::Relaxed);
269        } else {
270            // Mismatched preempt_enable : log and leave count at 0.
271            log::warn!("preempt_enable: underflow on CPU {} (mismatched call)", idx);
272        }
273    }
274}
275
276/// Returns `true` if preemption is currently allowed on this CPU
277/// (preempt_count == 0).
278#[inline]
279pub fn is_preemptible() -> bool {
280    let idx = current_cpu_index();
281    if PERCPU[idx].arch.cpu_index.load(Ordering::Relaxed) as usize != idx {
282        return true; // Early boot: no scheduler active, preemption is allowed.
283    }
284    PERCPU[idx].preempt_count.load(Ordering::Relaxed) == 0
285}
286
287/// Get the APIC ID for a given CPU index, or `None` if not present.
288pub fn apic_id_by_cpu_index(index: usize) -> Option<u32> {
289    PERCPU
290        .get(index)
291        .filter(|cpu| cpu.present.load(Ordering::Acquire))
292        .map(|cpu| cpu.apic_id.load(Ordering::Acquire))
293}
294
295/// Resolve current CPU index via a GS-relative load from offset 0.
296///
297/// **Hot path** (after `init_gs_base` has run on the BSP): a single
298/// non-serialising `mov rax, gs:[0]` : no MSR, no pipeline stall.
299///
300/// **Slow/early-boot path** (before `init_gs_base` is called the first time):
301/// reads `IA32_GS_BASE` via `rdmsr` as a null-guard; if the GS base is 0
302/// (hardware reset value, before our `wrmsr`) the function returns 0 (BSP
303/// slot) without touching GS.  Once the BSP calls `init_gs_base`, the slow
304/// path is never taken again.
305///
306/// **Corrupt-GS defence**: the returned index is clamped to [0, MAX_CPUS-1]
307/// so a bogus GS value can never produce an out-of-bounds array access.
308///
309/// **Invariant for APs**: `smp_main` always calls `init_gs_base` before the
310/// first `current_cpu_index()` on each AP, so the hot path is safe for APs.
311#[inline]
312pub fn current_cpu_index() -> usize {
313    // SAFETY:
314    // Hot path : `gs:[0]` reads `PerCpuArch::cpu_index` (AtomicU64, repr(C),
315    // offset 0).  Valid because `BSP_GS_INITIALIZED` is only set after the BSP's
316    // GS base has been written, and APs always write their GS base before
317    // executing any code that calls this function.
318    //
319    // Slow path : `rdmsr` is a privileged Ring-0 instruction, always valid here.
320    // The derference of `gs_base` is guarded by the != 0 check.
321    unsafe {
322        if BSP_GS_INITIALIZED.load(Ordering::Acquire) {
323            // Fast path: GS is valid on every CPU that can run kernel code now.
324            let idx: u64;
325            core::arch::asm!(
326                "mov {idx}, gs:[0]",
327                idx = out(reg) idx,
328                options(nostack, preserves_flags, readonly),
329            );
330            return (idx as usize).min(MAX_CPUS - 1);
331        }
332
333        // Slow path: early boot : GS not yet set on any CPU.
334        // `rdmsr` reads IA32_GS_BASE (0xC000_0101) as a null-guard.
335        let lo: u32;
336        let hi: u32;
337        core::arch::asm!(
338            "rdmsr",
339            in("ecx")  0xC000_0101u32,
340            out("eax") lo,
341            out("edx") hi,
342            options(nostack, preserves_flags),
343        );
344        let gs_base = (lo as u64) | ((hi as u64) << 32);
345        if gs_base == 0 {
346            return 0; // GS not yet initialised : early boot, return BSP slot.
347        }
348
349        // GS is set but BSP_GS_INITIALIZED wasn't yet visible (narrow race
350        // between wrmsr and the store; take the GS-relative read here too).
351        let idx: u64;
352        core::arch::asm!(
353            "mov {idx}, gs:[0]",
354            idx = out(reg) idx,
355            options(nostack, preserves_flags, readonly),
356        );
357        (idx as usize).min(MAX_CPUS - 1)
358    }
359}
360
361/// Alias for current_cpu_index.
362#[inline]
363pub fn current_cpu_index_fast() -> usize {
364    current_cpu_index()
365}
366
367/// Resolve current CPU index from GS base (compatibility).
368pub fn cpu_index_from_gs() -> Option<usize> {
369    Some(current_cpu_index())
370}
371
372/// Access the per-CPU array (read-only).
373pub fn percpu() -> &'static [PerCpu; MAX_CPUS] {
374    &PERCPU
375}
376
377/// Mark current CPU as ready to handle TLB shootdown IPIs.
378pub fn mark_tlb_ready_current() {
379    let idx = current_cpu_index();
380    PERCPU[idx].tlb_ready.store(true, Ordering::Release);
381}
382
383/// Returns true iff CPU `index` is online and ready for TLB shootdown.
384pub fn tlb_ready(index: usize) -> bool {
385    PERCPU
386        .get(index)
387        .map(|cpu| {
388            cpu.present.load(Ordering::Acquire)
389                && cpu.online.load(Ordering::Acquire)
390                && cpu.tlb_ready.load(Ordering::Acquire)
391        })
392        .unwrap_or(false)
393}
394
395// ========== Fast signal-pending hint ==========
396
397/// Set the `signal_pending` flag for the current CPU.
398///
399/// Called by `send_signal` when a signal targets the task running on this CPU.
400/// The syscall dispatcher checks this flag to avoid the expensive scheduler
401/// lock when no signal is pending.
402#[inline]
403pub fn set_signal_pending_current() {
404    let idx = current_cpu_index();
405    PERCPU[idx].signal_pending.store(1, Ordering::Release);
406}
407
408/// Test and clear the `signal_pending` flag for the current CPU.
409///
410/// Returns `true` if a signal was marked pending since the last check.
411/// This is a one-shot check : the flag is cleared atomically.
412#[inline]
413pub fn test_and_clear_signal_pending_current() -> bool {
414    let idx = current_cpu_index();
415    PERCPU[idx].signal_pending.swap(0, Ordering::AcqRel) != 0
416}