Skip to main content

ax_runtime/thread/
runtime_impl.rs

1use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
2
3use ax_task::runtime::cpu::{LocalIrqState, PreemptGuardToken};
4
5use super::*;
6
7static SCHED_SWITCH_TRACE_HOOK: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
8static SCHED_SWITCH_TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
9
10unsafe fn membarrier_ipi_memory_barrier(_arg: *mut ()) {
11    core::sync::atomic::fence(Ordering::SeqCst);
12}
13
14unsafe fn membarrier_ipi_refresh_run_queue(_arg: *mut ()) {
15    ax_task::sync::membarrier::refresh_current_membarrier_run_queue()
16        .unwrap_or_else(|error| panic!("membarrier rq refresh failed in IPI: {error}"));
17}
18
19/// Allocation-free scheduler-switch capture hook installed by an OS layer.
20///
21/// Capture must not acquire task/rq locks. Its optional returned notification
22/// runs in incoming switch completion after those locks have been released.
23pub type SchedSwitchTraceHook = fn(SchedSwitchRecord) -> Option<fn()>;
24
25/// Installs the process-wide scheduler-switch diagnostic consumer.
26///
27/// Reinstalling the same function is harmless; replacing a live consumer is an
28/// invariant violation because switches may concurrently execute the hook.
29pub fn install_sched_switch_trace_hook(hook: SchedSwitchTraceHook) {
30    let hook = hook as *mut ();
31    match SCHED_SWITCH_TRACE_HOOK.compare_exchange(
32        core::ptr::null_mut(),
33        hook,
34        Ordering::AcqRel,
35        Ordering::Acquire,
36    ) {
37        Ok(_) => {}
38        Err(installed) => assert_eq!(installed, hook, "scheduler trace hook already installed"),
39    }
40}
41
42/// Publishes whether the scheduler-switch trace site has active consumers.
43pub fn publish_sched_switch_trace_gate(enabled: bool) {
44    SCHED_SWITCH_TRACE_ENABLED.store(enabled, Ordering::Release);
45}
46
47struct ArceOsTaskRuntime;
48
49impl_task_runtime! {
50    impl TaskRuntime for ArceOsTaskRuntime {
51        unsafe fn task_system_handle() -> TaskSystemHandle {
52            runtime_task_system_handle()
53        }
54
55        unsafe fn current_cpu_owner_handles() -> CurrentCpuOwnerHandles {
56            // SAFETY: the ax-task caller already owns a migration pin. The
57            // callback captures both paired endpoints from that same CPU area
58            // in one transaction.
59            unsafe { with_current_cpu_pin(current_cpu_owner_handles) }
60        }
61
62        unsafe fn current_cpu_remote_handle() -> CpuRemoteHandle {
63            // SAFETY: the ax-task caller keeps the scheduler-owned current
64            // thread fixed. Bootstrap cached this CPU's Arc-backed endpoint
65            // before online publication and retains its TaskSystem owner.
66            unsafe { scheduler_current_cpu_remote_handle() }
67        }
68
69        fn current_thread_identity() -> ThreadIdentityV1 {
70            scheduler_current_thread_identity()
71        }
72
73        fn current_thread_publication() -> CurrentThreadPublication {
74            scheduler_current_thread_publication()
75        }
76
77        fn current_preemption_pending() -> bool {
78            cpu_local::current_preemption_pending().unwrap_or_else(|error| {
79                panic!("current preemption state is unavailable: {error}")
80            })
81        }
82
83        unsafe fn cpu_remote_handle(cpu: RuntimeCpuId) -> CpuRemoteHandle {
84            cpu_remote(cpu).map_or(CpuRemoteHandle::NONE, |cpu| {
85                // SAFETY: TaskSystem owns this Arc-backed CpuRemote endpoint
86                // through shutdown and the lookup preserves its CPU identity.
87                unsafe {
88                    CpuRemoteHandle::from_raw((cpu as *const CpuRemote).expose_provenance())
89                }
90            })
91        }
92
93        unsafe fn current_cpu_id() -> RuntimeCpuId {
94            // SAFETY: the TaskRuntime caller retains a migration pin for the
95            // complete owner-CPU observation.
96            let cpu = unsafe { cpu_local::current_cpu_index() }
97                .unwrap_or_else(|error| panic!("task runtime CPU index is invalid: {error}"));
98            RuntimeCpuId::new(cpu.as_u32())
99        }
100
101        fn prepare_cpu_online(cpu: RuntimeCpuId) -> RuntimeStatus {
102            // SAFETY: this hook runs on the IRQ-excluded owner CPU before
103            // scheduler publication.
104            if cpu != unsafe { Self::current_cpu_id() } {
105                return RuntimeStatus::InvalidArgument;
106            }
107            crate::clock_event_runtime::init_timer();
108            RuntimeStatus::Success
109        }
110
111        fn prepare_cpu_offline(cpu: RuntimeCpuId) -> RuntimeStatus {
112            // SAFETY: this hook runs on the IRQ-excluded owner CPU after
113            // remote admission has closed.
114            if cpu != unsafe { Self::current_cpu_id() } {
115                return RuntimeStatus::InvalidArgument;
116            }
117            #[cfg(feature = "paging")]
118            if let Err(error) = crate::kernel_mapping::retry_kernel_tlb_reclaims() {
119                error!("failed to retry kernel TLB quarantine before CPU offline: {error}");
120                return RuntimeStatus::Platform;
121            }
122            // No recoverable work may follow this publication: it installs the
123            // safe root, clears the CPU-local active handle, and releases the
124            // logical address-space lease in one direction.
125            release_current_active_address_space();
126            crate::clock_event_runtime::take_current_clock_event_offline();
127            RuntimeStatus::Success
128        }
129
130        fn local_irq_save_and_disable() -> LocalIrqState {
131            let was_enabled = ax_hal::asm::irqs_enabled();
132            ax_hal::asm::disable_irqs();
133            // SAFETY: the provider restores only the boolean state encoded by
134            // this implementation's matching restore operation.
135            unsafe { LocalIrqState::from_raw(usize::from(was_enabled)) }
136        }
137
138        unsafe fn local_irq_restore(state: LocalIrqState) {
139            if state.into_raw() != 0 {
140                ax_hal::asm::enable_irqs();
141            } else {
142                ax_hal::asm::disable_irqs();
143            }
144        }
145
146        fn irq_guard_enter() -> IrqGuardToken {
147            #[cfg(any(test, feature = "host-test"))]
148            {
149                // SAFETY: host-test mode models one balanced runtime IRQ token.
150                unsafe { IrqGuardToken::from_raw(1) }
151            }
152            #[cfg(not(any(test, feature = "host-test")))]
153            {
154                if crate::guard::inherits_hardirq_cpu_owner() {
155                    return IrqGuardToken::NONE;
156                }
157                crate::guard::enter_irq();
158                // SAFETY: enter_irq established the matching live guard state.
159                unsafe { IrqGuardToken::from_raw(1) }
160            }
161        }
162
163        unsafe fn irq_guard_exit(token: IrqGuardToken) {
164            #[cfg(not(any(test, feature = "host-test")))]
165            if !token.is_none() {
166                crate::guard::exit_irq("task runtime");
167            }
168            #[cfg(any(test, feature = "host-test"))]
169            let _ = token;
170        }
171
172        fn preempt_guard_enter() -> PreemptGuardToken {
173            #[cfg(any(test, feature = "host-test"))]
174            {
175                // SAFETY: host-test mode models one balanced runtime preemption token.
176                unsafe { PreemptGuardToken::from_raw(1) }
177            }
178            #[cfg(not(any(test, feature = "host-test")))]
179            {
180                match crate::guard::enter_lock_preempt() {
181                    Some(token) => {
182                        // SAFETY: the architecture owner identifies the live
183                        // depth established by enter_lock_preempt.
184                        unsafe { PreemptGuardToken::from_raw(token.into_raw()) }
185                    }
186                    None => PreemptGuardToken::NONE,
187                }
188            }
189        }
190
191        unsafe fn preempt_guard_exit(token: PreemptGuardToken) {
192            assert!(
193                !token.is_none(),
194                "inherited owner scope passed to ordinary preemption exit"
195            );
196            #[cfg(not(any(test, feature = "host-test")))]
197            {
198                let token = unsafe { cpu_local::PreemptionToken::from_raw(token.into_raw()) }
199                .expect("task preemption token must retain its architecture owner");
200                crate::guard::exit_preempt(token);
201            }
202        }
203
204        unsafe fn preempt_guard_exit_irq_return(token: PreemptGuardToken) {
205            assert!(
206                !token.is_none(),
207                "inherited owner scope passed to IRQ-return preemption exit"
208            );
209            #[cfg(not(any(test, feature = "host-test")))]
210            {
211                let token = unsafe { cpu_local::PreemptionToken::from_raw(token.into_raw()) }
212                .expect("IRQ-return token must retain its architecture owner");
213                crate::guard::exit_preempt_from_irq_return(token);
214            }
215        }
216
217        fn hardirq_enter() {
218            #[cfg(feature = "irq-time-accounting")]
219            crate::irq_time::enter();
220        }
221
222        fn hardirq_exit() {
223            #[cfg(feature = "irq-time-accounting")]
224            crate::irq_time::exit();
225        }
226
227        fn publish_local_scheduler_work() -> bool {
228            #[cfg(any(test, feature = "host-test"))]
229            {
230                false
231            }
232            #[cfg(not(any(test, feature = "host-test")))]
233            {
234                crate::guard::publish_local_scheduler_work()
235            }
236        }
237
238        fn finish_context_switch_tail() -> bool {
239            finish_runtime_context_switch_tail()
240        }
241
242        fn finish_initial_context_switch() {
243            crate::guard::finish_initial_context_switch();
244        }
245
246        fn scheduler_frame_guard_enter(
247            origin: ax_task::runtime::switch::RuntimeScheduleOrigin,
248            entry: ax_task::runtime::switch::RuntimeSchedulerEntry,
249        ) -> RuntimeSchedulerFrameEnterResult {
250            crate::guard::enter_scheduler_frame_guard(origin, entry)
251        }
252
253        fn scheduler_frame_guard_exit(
254            return_to: ax_task::runtime::switch::RuntimeSchedulerReturn,
255            needs_reschedule: bool,
256        ) -> bool {
257            crate::guard::exit_scheduler_frame_guard(return_to, needs_reschedule)
258        }
259
260        fn in_hard_irq() -> bool {
261            #[cfg(any(test, feature = "host-test"))]
262            {
263                false
264            }
265            #[cfg(not(any(test, feature = "host-test")))]
266            {
267                ax_hal::irq::in_irq_context()
268            }
269        }
270
271        fn validate_schedule_context(
272            origin: ax_task::runtime::switch::RuntimeScheduleOrigin,
273        ) -> RuntimeStatus {
274            crate::guard::validate_schedule_context(origin)
275        }
276
277        fn validate_owner_cpu_context() -> RuntimeStatus {
278            crate::guard::validate_owner_cpu_context()
279        }
280
281        fn monotonic_now() -> ax_task::time::MonotonicInstant {
282            ax_task::time::MonotonicInstant::from_nanos(
283                ax_hal::time::monotonic_time_nanos(),
284            )
285            .expect("platform monotonic clock exceeded the signed ktime domain")
286        }
287
288        fn rq_clock_sample() -> ax_task::runtime::cpu::RqClockSample {
289            #[cfg(feature = "qperf-metrics")]
290            let clock_started_ns = ax_hal::time::monotonic_time_nanos();
291            // SAFETY: ax-task holds the target runqueue IRQ-save lock, which
292            // pins its owner CPU for this complete local clock sample.
293            let clock_ns = unsafe { ax_hal::time::scheduler_clock_source() }
294                .unwrap_or_else(|error| {
295                    panic!("current scheduler clock source is unavailable: {error}")
296                });
297            #[cfg(feature = "qperf-metrics")]
298            let irq_time_started_ns = ax_hal::time::monotonic_time_nanos();
299            #[cfg(feature = "irq-time-accounting")]
300            let irq_time_ns = crate::irq_time::total_current();
301            #[cfg(feature = "qperf-metrics")]
302            {
303                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
304                    15,
305                    clock_started_ns,
306                    irq_time_started_ns,
307                );
308            }
309            #[cfg(all(feature = "qperf-metrics", feature = "irq-time-accounting"))]
310            {
311                let irq_time_finished_ns = ax_hal::time::monotonic_time_nanos();
312                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
313                    16,
314                    irq_time_started_ns,
315                    irq_time_finished_ns,
316                );
317            }
318            #[cfg(feature = "irq-time-accounting")]
319            return ax_task::runtime::cpu::RqClockSample::new(
320                ax_task::sched::SchedulerTimestamp::from_nanos(clock_ns),
321                irq_time_ns,
322            );
323            #[cfg(not(feature = "irq-time-accounting"))]
324            ax_task::runtime::cpu::RqClockSample::without_irq_time_accounting(
325                ax_task::sched::SchedulerTimestamp::from_nanos(clock_ns),
326            )
327        }
328
329        fn publish_scheduler_deadline(update: ax_task::runtime::cpu::SchedulerDeadlineUpdate) {
330            crate::clock_event_runtime::publish_local_scheduler_deadline(update);
331        }
332
333        fn publish_scheduler_runtime_deadline(
334            update: ax_task::runtime::cpu::SchedulerRuntimeDeadline,
335        ) {
336            crate::clock_event_runtime::publish_local_scheduler_runtime_deadline(update);
337        }
338
339        fn idle_exit_restart_scheduler_tick() {
340            crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(
341                crate::clock_event_runtime::monotonic_now(),
342            );
343        }
344
345        fn notify_scheduler_cpu(cpu: RuntimeCpuId) -> RuntimeStatus {
346            #[cfg(any(feature = "ipi", feature = "wake-ipi"))]
347            {
348                let cpu_id = cpu.as_u32() as usize;
349                if cpu_id >= ax_hal::cpu_num() {
350                    return RuntimeStatus::InvalidArgument;
351                }
352                match ax_ipi::notify_cpu(ax_hal::irq::CpuId(cpu_id)) {
353                    Ok(notification) => {
354                        if notification == ax_ipi::IpiNotification::Sent {
355                            #[cfg(feature = "qperf-metrics")]
356                            record_scheduler_ipi_send();
357                        }
358                        RuntimeStatus::Success
359                    }
360                    Err(ax_hal::irq::IrqError::InvalidCpu) => RuntimeStatus::InvalidArgument,
361                    Err(ax_hal::irq::IrqError::CpuOffline) => RuntimeStatus::NotInitialized,
362                    Err(ax_hal::irq::IrqError::Busy) => RuntimeStatus::Busy,
363                    Err(ax_hal::irq::IrqError::NoMemory) => RuntimeStatus::NoMemory,
364                    Err(ax_hal::irq::IrqError::Unsupported) => RuntimeStatus::Unsupported,
365                    Err(_) => RuntimeStatus::Platform,
366                }
367            }
368            #[cfg(not(any(feature = "ipi", feature = "wake-ipi")))]
369            {
370                let _ = cpu;
371                RuntimeStatus::Unsupported
372            }
373        }
374
375        fn wait_for_interrupt() {
376            // Linux keeps the idle task non-preemptible from do_idle() through
377            // tick_nohz_idle_exit() and enters schedule_idle() only afterwards.
378            // Keep the same ownership across the IRQ-enabled WFI window: an
379            // interrupt may publish need-resched, but its return path cannot
380            // switch away before this scope restores the stopped tick.
381            let idle_exit_guard = crate::task::sync::PreemptGuard::new();
382            ax_hal::asm::disable_irqs();
383            unsafe {
384                // SAFETY: local IRQs remain disabled through the immediately
385                // following task-work and clockevent recheck, matching Linux
386                // `current_clr_polling_and_test()`.
387                ax_task::runtime::cpu::finish_current_cpu_idle_polling()
388            }
389            .expect("idle handoff requires an initialized current CPU");
390            let mut now = crate::clock_event_runtime::monotonic_now();
391            let mut needs_reschedule = ax_task::runtime::cpu::current_cpu_needs_resched()
392                .expect("idle handoff requires an initialized current CPU");
393            if needs_reschedule
394                || crate::clock_event_runtime::local_clock_event_has_immediate_work(now)
395            {
396                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(now);
397                ax_hal::asm::enable_irqs();
398                drop(idle_exit_guard);
399                return;
400            }
401
402            // Linux NOHZ removes the periodic scheduler tick only after idle
403            // polling is withdrawn. Task deadlines remain selected by the
404            // same physical clockevent and are reprogrammed in this IRQ-off
405            // transaction.
406            crate::clock_event_runtime::stop_current_scheduler_tick_for_idle();
407            now = crate::clock_event_runtime::monotonic_now();
408            needs_reschedule = ax_task::runtime::cpu::current_cpu_needs_resched()
409                .expect("idle handoff requires an initialized current CPU");
410            if needs_reschedule
411                || crate::clock_event_runtime::local_clock_event_has_immediate_work(now)
412            {
413                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(now);
414                ax_hal::asm::enable_irqs();
415                drop(idle_exit_guard);
416                return;
417            }
418
419            ax_hal::asm::wait_for_irqs_disabled();
420
421            // A non-scheduling interrupt may leave the CPU in the idle loop,
422            // in which case the tick stays stopped just as in Linux do_idle().
423            // Work that makes the idle thread yield restarts the tick before
424            // the scheduler can select a non-idle thread.
425            let irq_guard = crate::task::sync::IrqSaveGuard::new();
426            now = crate::clock_event_runtime::monotonic_now();
427            needs_reschedule = ax_task::runtime::cpu::current_cpu_needs_resched()
428                .expect("idle wake requires an initialized current CPU");
429            if needs_reschedule
430                || crate::clock_event_runtime::local_clock_event_has_immediate_work(now)
431            {
432                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(now);
433            }
434            drop(irq_guard);
435            drop(idle_exit_guard);
436        }
437
438        fn allocate_stack(_request: StackRequest) -> RuntimeHandleResult {
439            match allocate_runtime_stack(_request) {
440                Ok(handle) => RuntimeHandleResult::success(handle.into_raw()),
441                Err(status) => RuntimeHandleResult::failure(status),
442            }
443        }
444
445        fn deallocate_stack(_stack: StackHandle) {
446            assert_eq!(
447                deallocate_runtime_stack(_stack),
448                RuntimeStatus::Success,
449                "reclaimable task stack destruction failed"
450            );
451        }
452
453        fn allocate_kernel_tls() -> RuntimeHandleResult {
454            allocate_runtime_tls()
455        }
456
457        fn deallocate_tls(_tls: TlsHandle) {
458            assert_eq!(
459                deallocate_runtime_tls(_tls),
460                RuntimeStatus::Success,
461                "reclaimable task TLS destruction failed"
462            );
463        }
464
465        fn create_kernel_context(_request: KernelContextRequest) -> RuntimeHandleResult {
466            create_runtime_context(_request)
467        }
468
469        fn create_user_context(_request: UserContextRequest) -> RuntimeHandleResult {
470            create_user_runtime_context(_request)
471        }
472
473        fn bind_context_thread(binding: ContextThreadBinding) -> RuntimeStatus {
474            bind_runtime_context_thread(binding)
475        }
476
477        fn destroy_context(_context: ExecutionContextHandle) {
478            assert_eq!(
479                destroy_runtime_context(_context),
480                RuntimeStatus::Success,
481                "task context remained live after scheduler switch tail"
482            );
483        }
484
485        fn destroy_address_space(
486            address_space: AddressSpaceHandle,
487        ) -> AddressSpaceDestroyOutcome {
488            destroy_runtime_address_space(address_space)
489        }
490
491        fn arm_address_space_reclaim(
492            address_space: AddressSpaceHandle,
493        ) -> AddressSpaceReclaimArmOutcome {
494            arm_runtime_address_space_reclaim(address_space)
495        }
496
497        fn address_space_membarrier_state(
498            address_space: AddressSpaceHandle,
499        ) -> AddressSpaceMembarrierState {
500            runtime_address_space_membarrier_state(address_space)
501        }
502
503        fn update_address_space_membarrier_state(
504            address_space: AddressSpaceHandle,
505            registration: MembarrierRegistration,
506            phase: MembarrierRegistrationPhase,
507        ) -> AddressSpaceMembarrierState {
508            update_runtime_address_space_membarrier_state(address_space, registration, phase)
509        }
510
511        fn synchronize_membarrier_cpu(
512            cpu: RuntimeCpuId,
513            action: RuntimeMembarrierAction,
514        ) -> RuntimeStatus {
515            let cpu = cpu.as_u32() as usize;
516            let callback = match action {
517                RuntimeMembarrierAction::MemoryBarrier => membarrier_ipi_memory_barrier,
518                RuntimeMembarrierAction::RefreshRunQueue => {
519                    membarrier_ipi_refresh_run_queue
520                }
521            };
522            #[cfg(feature = "ipi")]
523            {
524                // SAFETY: both callbacks are fixed, allocation-free hard-IRQ
525                // operations and carry no argument lifetime.
526                match unsafe {
527                    crate::ipi_delivery::run_on_cpu_sync(cpu, callback, core::ptr::null_mut())
528                } {
529                    Ok(()) => RuntimeStatus::Success,
530                    Err(ax_hal::irq::IrqError::CpuOffline) => RuntimeStatus::Busy,
531                    Err(ax_hal::irq::IrqError::InvalidCpu) => RuntimeStatus::InvalidArgument,
532                    Err(_) => RuntimeStatus::Platform,
533                }
534            }
535            #[cfg(not(feature = "ipi"))]
536            {
537                // A UP runtime executes the same hard-call ABI locally. SMP
538                // membarrier requires the explicit `ipi` capability.
539                let current = unsafe { Self::current_cpu_id() }.as_u32() as usize;
540                if cpu != current {
541                    return RuntimeStatus::Unsupported;
542                }
543                // SAFETY: the selected fixed callback takes no argument.
544                unsafe { callback(core::ptr::null_mut()) };
545                RuntimeStatus::Success
546            }
547        }
548
549        unsafe fn switch_context(plan: RuntimeSwitchPlan) {
550            // SAFETY: the TaskRuntime contract passes one committed move-only
551            // switch transaction under the active scheduler baton.
552            unsafe { switch_runtime_context(plan) };
553        }
554
555        fn flush_tlb_local(_start: usize, _size: usize) {
556            ax_hal::asm::flush_tlb(None);
557        }
558
559        fn trace_sched_switch(record: SchedSwitchRecord) -> Option<fn()> {
560            if !SCHED_SWITCH_TRACE_ENABLED.load(Ordering::Acquire) {
561                return None;
562            }
563            let hook = SCHED_SWITCH_TRACE_HOOK.load(Ordering::Acquire);
564            if hook.is_null() {
565                return None;
566            }
567            // SAFETY: installation accepts exactly this function-pointer type,
568            // and the process-wide hook is never replaced or removed.
569            let hook = unsafe { core::mem::transmute::<*mut (), SchedSwitchTraceHook>(hook) };
570            hook(record)
571        }
572
573        fn emergency_console_write(message: &str) {
574            ax_hal::console::write_bytes(message.as_bytes());
575        }
576
577        fn fatal_invariant(code: u32, argument: usize) -> ! {
578            panic!("ax-task invariant {code} failed with argument {argument:#x}")
579        }
580    }
581}