Skip to main content

ax_task/runtime/switch/
dispatch.rs

1use core::marker::PhantomData;
2
3use crate::{
4    runtime::{
5        context::{
6            RuntimeCpuPin, RuntimeIrqGuard, RuntimeSchedulerFrameGuard, runtime_current_cpu_mut,
7            runtime_task_system, validate_schedule_context,
8        },
9        switch::{
10            RuntimeScheduleOrigin, RuntimeSchedulerEntry, SchedSwitchRecord, ScheduleDecision,
11            SchedulerOutcome,
12        },
13        task_runtime,
14    },
15    sched::system::{CurrentExitPermit, SchedulerRequestScope},
16    thread::{TaskError, ThreadId, ThreadState, current::current_thread_handle},
17};
18
19/// Runs one scheduler decision at a task/IRQ-return safe point.
20///
21/// The typed outcome distinguishes a completed decision, an in-flight park
22/// handshake, and bounded owner-work backpressure. It never clears
23/// `need_resched` before entering the scheduler.
24///
25/// # Errors
26///
27/// Returns [`TaskError::UnsafeContext`] in hard IRQ context and object-handle
28/// errors when runtime initialization is incomplete or inconsistent.
29pub fn schedule_current_cpu() -> Result<SchedulerOutcome, TaskError> {
30    schedule_current_cpu_with_entry(RuntimeSchedulerEntry::Task)
31}
32
33/// Services the final preemption-guard exit without exposing a preemptible
34/// window before the scheduler owns its CPU-local baton.
35///
36/// # Safety
37///
38/// `entry` must match the caller's exact runtime context. The caller must own
39/// one final lock-preemption depth and must satisfy the raw IRQ-state contract
40/// documented by [`RuntimeSchedulerEntry`].
41pub unsafe fn schedule_current_cpu_from_preempt_exit(
42    entry: RuntimeSchedulerEntry,
43) -> Result<SchedulerOutcome, TaskError> {
44    if !matches!(
45        entry,
46        RuntimeSchedulerEntry::PreemptExit | RuntimeSchedulerEntry::IrqReturn
47    ) {
48        return Err(TaskError::UnsafeContext);
49    }
50    schedule_current_cpu_with_entry(entry)
51}
52
53/// Services the final task-context IRQ publication guard exit without
54/// restoring IRQs before the scheduler owns its CPU-local baton.
55///
56/// # Safety
57///
58/// The caller must own the final runtime IRQ-guard depth, have entered it from
59/// ordinary task context with IRQs enabled, and retain raw IRQ exclusion.
60pub unsafe fn schedule_current_cpu_from_irq_guard_exit() -> Result<SchedulerOutcome, TaskError> {
61    schedule_current_cpu_with_entry(RuntimeSchedulerEntry::IrqGuardExit)
62}
63
64fn schedule_current_cpu_with_entry(
65    mut entry: RuntimeSchedulerEntry,
66) -> Result<SchedulerOutcome, TaskError> {
67    let original_entry = entry;
68    loop {
69        let request_scope = scheduler_request_scope(entry, original_entry);
70        let mut scheduler_frame =
71            RuntimeSchedulerFrameGuard::enter(RuntimeScheduleOrigin::Preempt, entry)?;
72        let system = scheduler_frame.task_system();
73        let current_publication = scheduler_frame.current_thread_publication();
74        let (mut outcome, no_switch_request_pending) = {
75            let mut cpu = runtime_current_cpu_mut(&mut scheduler_frame)?;
76            // SAFETY: RuntimeSchedulerFrameGuard owns the IRQ-off scheduler baton.
77            let current_state = unsafe { cpu.scheduler_current_lifecycle_state() };
78            let outcome = if !cpu.scheduler_request_pending(request_scope) && !cpu.has_remote_work()
79            {
80                if current_state == Some(ThreadState::Parking) {
81                    SchedulerOutcome::ParkingDeferred
82                } else {
83                    SchedulerOutcome::Quiescent
84                }
85            } else {
86                // SAFETY: the publication was captured by this scheduler
87                // frame's runtime transaction and remains current here.
88                let current = unsafe { current_publication.borrow_current()? };
89                // SAFETY: `scheduler_frame` owns the IRQ-off scheduler baton.
90                unsafe {
91                    system.schedule_if_requested_in_scheduler_frame(
92                        cpu.as_mut(),
93                        &current,
94                        request_scope,
95                    )?
96                }
97            };
98            let request_pending = match outcome.decision() {
99                Some(decision) if decision.requires_context_switch() => None,
100                Some(_) | None => Some(cpu.scheduler_request_pending(request_scope)),
101            };
102            (outcome, request_pending)
103        };
104        if let Some(decision) = outcome.decision_mut() {
105            execute_switch_plan(&mut scheduler_frame, decision);
106        }
107        let needs_reschedule = if let Some(request_pending) = no_switch_request_pending {
108            request_pending
109        } else {
110            scheduler_frame.scheduler_request_pending(request_scope)?
111        };
112        let repeat = preempt_schedule_needs_repeat(&outcome, needs_reschedule);
113        drop(scheduler_frame);
114        if !repeat {
115            return Ok(outcome);
116        }
117        entry = match entry {
118            RuntimeSchedulerEntry::IrqReturn | RuntimeSchedulerEntry::IrqReturnContinuation => {
119                RuntimeSchedulerEntry::IrqReturnContinuation
120            }
121            RuntimeSchedulerEntry::Task
122            | RuntimeSchedulerEntry::PreemptExit
123            | RuntimeSchedulerEntry::IrqGuardExit => RuntimeSchedulerEntry::Task,
124        };
125    }
126}
127
128fn scheduler_request_scope(
129    entry: RuntimeSchedulerEntry,
130    original_entry: RuntimeSchedulerEntry,
131) -> SchedulerRequestScope {
132    if matches!(
133        original_entry,
134        RuntimeSchedulerEntry::PreemptExit | RuntimeSchedulerEntry::IrqGuardExit
135    ) {
136        // Linux preempt-enable and task-context IRQ-exit loops continue only
137        // while ordinary need-resched remains set. Rewriting the continuation
138        // entry to `Task` must not promote the next pass to a lazy-consuming
139        // explicit scheduling point.
140        return SchedulerRequestScope::Immediate;
141    }
142    match entry {
143        RuntimeSchedulerEntry::Task => SchedulerRequestScope::All,
144        RuntimeSchedulerEntry::PreemptExit
145        | RuntimeSchedulerEntry::IrqReturn
146        | RuntimeSchedulerEntry::IrqGuardExit
147        | RuntimeSchedulerEntry::IrqReturnContinuation => SchedulerRequestScope::Immediate,
148    }
149}
150
151fn preempt_schedule_needs_repeat(outcome: &SchedulerOutcome, needs_reschedule: bool) -> bool {
152    needs_reschedule && !outcome.parking_deferred()
153}
154
155/// Yields the calling thread and executes the resulting context switch.
156pub fn yield_current_cpu() -> Result<(), TaskError> {
157    #[cfg(feature = "qperf-metrics")]
158    let scheduler_started_ns = task_runtime::monotonic_now().as_nanos();
159    let mut scheduler_frame = RuntimeSchedulerFrameGuard::enter(
160        RuntimeScheduleOrigin::Yield,
161        RuntimeSchedulerEntry::Task,
162    )?;
163    #[cfg(feature = "qperf-metrics")]
164    let scheduler_frame_entered_ns = task_runtime::monotonic_now().as_nanos();
165    let system = scheduler_frame.task_system();
166    #[cfg(feature = "qperf-metrics")]
167    let scheduler_dispatch_started_ns;
168    let mut outcome = {
169        let mut cpu = runtime_current_cpu_mut(&mut scheduler_frame)?;
170        #[cfg(feature = "qperf-metrics")]
171        {
172            scheduler_dispatch_started_ns = task_runtime::monotonic_now().as_nanos();
173        }
174        // SAFETY: `scheduler_frame` owns the IRQ-off scheduler baton.
175        unsafe { system.yield_current_in_scheduler_frame(cpu.as_mut())? }
176    };
177    #[cfg(feature = "qperf-metrics")]
178    let scheduler_dispatch_finished_ns = task_runtime::monotonic_now().as_nanos();
179    if let Some(decision) = outcome.decision_mut() {
180        #[cfg(feature = "qperf-metrics")]
181        {
182            crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
183                7,
184                scheduler_started_ns,
185                scheduler_frame_entered_ns,
186            );
187            crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
188                8,
189                scheduler_frame_entered_ns,
190                scheduler_dispatch_started_ns,
191            );
192            crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
193                9,
194                scheduler_dispatch_started_ns,
195                scheduler_dispatch_finished_ns,
196            );
197            crate::diagnostics::counters::qperf_record_switch_phase_scheduler(
198                scheduler_started_ns,
199                scheduler_dispatch_finished_ns,
200            );
201        }
202        execute_switch_plan(&mut scheduler_frame, decision);
203    }
204    Ok(())
205}
206
207/// Exits the calling thread and switches to its replacement.
208pub fn exit_current_thread() -> Result<(), TaskError> {
209    let permit = prepare_current_exit()?;
210    commit_current_exit(permit)
211}
212
213/// A validated, thread-bound opportunity to publish exit completion.
214pub struct ExitPermit {
215    system: CurrentExitPermit,
216    _not_send: PhantomData<*mut ()>,
217}
218
219/// Validates scheduler-side exit prerequisites without changing the current
220/// thread's observable lifecycle.
221pub fn prepare_current_exit() -> Result<ExitPermit, TaskError> {
222    validate_schedule_context(RuntimeScheduleOrigin::Exit)?;
223    let current = current_thread_handle()?;
224    let mut irq = RuntimeIrqGuard::enter();
225    let system = runtime_task_system()?;
226    let mut cpu = runtime_current_cpu_mut(&mut irq)?;
227    let system = system.prepare_current_exit(cpu.as_mut(), &current)?;
228    Ok(ExitPermit {
229        system,
230        _not_send: PhantomData,
231    })
232}
233
234/// Commits a prepared scheduler exit and permanently leaves this context.
235///
236/// Any failure after completion became externally visible is a fatal runtime
237/// invariant; this function therefore has no recoverable return path.
238pub fn commit_current_exit(permit: ExitPermit) -> ! {
239    let thread = permit.system.thread();
240    let mut scheduler_frame =
241        RuntimeSchedulerFrameGuard::enter(RuntimeScheduleOrigin::Exit, RuntimeSchedulerEntry::Task)
242            .unwrap_or_else(|_| task_runtime::fatal_invariant(0x4558_0010, thread.as_u64() as _));
243    let system = scheduler_frame.task_system();
244    let mut decision = {
245        let mut cpu = runtime_current_cpu_mut(&mut scheduler_frame)
246            .unwrap_or_else(|_| task_runtime::fatal_invariant(0x4558_0013, thread.as_u64() as _));
247        // SAFETY: `scheduler_frame` owns the IRQ-off scheduler baton.
248        unsafe { system.commit_prepared_current_exit(cpu.as_mut(), permit.system) }
249    };
250    execute_switch_plan(&mut scheduler_frame, &mut decision);
251    // An exited context is never re-enqueued, so returning here indicates a
252    // broken architecture switch contract.
253    task_runtime::fatal_invariant(4, decision.previous().map_or(0, ThreadId::as_u64) as usize)
254}
255
256pub(crate) fn execute_switch_plan(
257    scheduler_frame: &mut RuntimeSchedulerFrameGuard,
258    decision: &mut ScheduleDecision,
259) {
260    if !decision.requires_context_switch() {
261        return;
262    }
263    #[cfg(feature = "qperf-metrics")]
264    let prepare_started_ns = task_runtime::monotonic_now().as_nanos();
265    let Some(previous) = decision.previous() else {
266        task_runtime::fatal_invariant(1, decision.next().as_u64() as usize);
267    };
268    let next = decision.next();
269    let previous_extension = {
270        let mut cpu = runtime_current_cpu_mut(scheduler_frame)
271            .unwrap_or_else(|_| task_runtime::fatal_invariant(6, next.as_u64() as usize));
272        let handoff = cpu
273            .as_mut()
274            .switch_handoff_mut()
275            .unwrap_or_else(|| task_runtime::fatal_invariant(6, next.as_u64() as usize));
276        if handoff.previous().id() != previous || handoff.incoming().id() != next {
277            task_runtime::fatal_invariant(6, next.as_u64() as usize);
278        }
279        handoff.previous().extension_view()
280    };
281    let plan = decision
282        .take_runtime_switch_plan()
283        .unwrap_or_else(|| task_runtime::fatal_invariant(6, next.as_u64() as usize));
284    #[cfg(feature = "qperf-metrics")]
285    let switch_validate_finished_ns = task_runtime::monotonic_now().as_nanos();
286    // Match Linux's sched_switch observation point: the trace runs while the
287    // previous extension is still the published current task and the switch
288    // decision is final. The rq baton may still be held, so notifications
289    // returned by capture belong to incoming switch completion.
290    let trace_wake = task_runtime::trace_sched_switch(SchedSwitchRecord {
291        cpu: scheduler_frame.cpu_id(),
292        previous_thread: previous.as_u64(),
293        next_thread: next.as_u64(),
294        timestamp_ns: decision.timestamp_ns(),
295        reason: decision.switch_reason() as u32,
296    });
297    if let Some(wake) = trace_wake {
298        let mut cpu = runtime_current_cpu_mut(scheduler_frame)
299            .unwrap_or_else(|_| task_runtime::fatal_invariant(6, next.as_u64() as usize));
300        cpu.as_mut()
301            .switch_handoff_mut()
302            .unwrap_or_else(|| task_runtime::fatal_invariant(6, next.as_u64() as usize))
303            .install_trace_wake(wake);
304    }
305    #[cfg(feature = "qperf-metrics")]
306    let switch_trace_finished_ns = task_runtime::monotonic_now().as_nanos();
307    if let Some(extension) = previous_extension {
308        // SAFETY: ThreadExtension construction guarantees callback validity;
309        // TaskSystem released every internal lock and the scheduler baton
310        // keeps local IRQs disabled.
311        unsafe {
312            (extension.ops().on_switch_out)(extension.data(), previous, decision.switch_reason())
313        };
314    }
315    #[cfg(feature = "qperf-metrics")]
316    let switch_out_hook_finished_ns = task_runtime::monotonic_now().as_nanos();
317    #[cfg(feature = "qperf-metrics")]
318    crate::diagnostics::counters::record_context_switch(decision.switch_reason());
319    #[cfg(feature = "qperf-metrics")]
320    let switch_accounting_finished_ns = task_runtime::monotonic_now().as_nanos();
321    #[cfg(feature = "qperf-metrics")]
322    let plan = {
323        crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
324            26,
325            prepare_started_ns,
326            switch_validate_finished_ns,
327        );
328        crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
329            27,
330            switch_validate_finished_ns,
331            switch_trace_finished_ns,
332        );
333        crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
334            28,
335            switch_trace_finished_ns,
336            switch_out_hook_finished_ns,
337        );
338        crate::diagnostics::counters::qperf_record_switch_scheduler_detail(
339            29,
340            switch_out_hook_finished_ns,
341            switch_accounting_finished_ns,
342        );
343        let mut plan = plan;
344        plan.set_qperf_prepare_started_ns(prepare_started_ns);
345        plan
346    };
347    // SAFETY: the scheduler committed both endpoint states before releasing
348    // its locks. Every context and address-space handle remains live, and
349    // local IRQs stay disabled while the runtime consumes the complete plan.
350    unsafe { task_runtime::switch_context(plan) };
351    scheduler_frame.refresh_current_cpu();
352    // SAFETY: the scheduler frame retains its IRQ-off baton across the
353    // architecture switch and through switch-tail completion.
354    if unsafe { complete_current_context_switch_tail_in_scheduler_frame(scheduler_frame) }.is_err()
355    {
356        task_runtime::fatal_invariant(5, 0);
357    }
358}
359
360/// Completes a fresh context's switch tail below its transferred scheduler
361/// baton.
362///
363/// # Safety
364///
365/// The caller must be the first instruction sequence of a freshly switched-in
366/// context and must retain the transferred IRQ-off scheduler baton until this
367/// function returns.
368pub(crate) unsafe fn complete_current_context_switch_tail(
369    pin: &mut impl RuntimeCpuPin,
370) -> Result<(), TaskError> {
371    let system = runtime_task_system()?;
372    // SAFETY: the caller retains the transferred scheduler baton.
373    unsafe { finish_switch_tail(system, pin) }
374}
375
376/// Completes the inherited switch tail below a live scheduler frame.
377///
378/// # Safety
379///
380/// `scheduler_frame` must retain the IRQ-off scheduler baton until this
381/// function returns.
382unsafe fn complete_current_context_switch_tail_in_scheduler_frame(
383    scheduler_frame: &mut RuntimeSchedulerFrameGuard,
384) -> Result<(), TaskError> {
385    let system = scheduler_frame.task_system();
386    // SAFETY: the frame retains the same IRQ-off baton through completion.
387    unsafe { finish_switch_tail(system, scheduler_frame) }
388}
389
390/// Completes the committed owner handoff under its caller's live scheduler baton.
391///
392/// # Safety
393/// `pin` must retain the IRQ-off baton for the CPU belonging to `system`.
394unsafe fn finish_switch_tail(
395    system: &crate::runtime::TaskSystem,
396    pin: &mut impl RuntimeCpuPin,
397) -> Result<(), TaskError> {
398    let completion = {
399        let mut cpu = runtime_current_cpu_mut(pin)?;
400        // SAFETY: forwarded from this helper's transferred-baton contract.
401        unsafe { system.complete_context_switch_in_scheduler_frame(cpu.as_mut())? }
402    };
403    completion.finish();
404    Ok(())
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn preempt_exit_continuation_does_not_consume_lazy_requests() {
413        assert_eq!(
414            scheduler_request_scope(
415                RuntimeSchedulerEntry::Task,
416                RuntimeSchedulerEntry::PreemptExit,
417            ),
418            SchedulerRequestScope::Immediate
419        );
420        assert_eq!(
421            scheduler_request_scope(
422                RuntimeSchedulerEntry::Task,
423                RuntimeSchedulerEntry::IrqGuardExit,
424            ),
425            SchedulerRequestScope::Immediate
426        );
427        assert_eq!(
428            scheduler_request_scope(RuntimeSchedulerEntry::Task, RuntimeSchedulerEntry::Task),
429            SchedulerRequestScope::All
430        );
431    }
432}