osal_rs/posix/system.rs
1/***************************************************************************
2 *
3 * osal-rs
4 * Copyright (C) 2026 Antonio Salsi <passy.linux@zresa.it>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
18 *
19 ***************************************************************************/
20
21//! System-level control and timing for POSIX.
22//!
23//! [`System`] provides the scheduler-adjacent operations that don't belong
24//! to any single primitive: starting/stopping the "run loop", timing
25//! (`CLOCK_MONOTONIC`-based), and querying/suspending the threads spawned
26//! through this crate's [`crate::os::Thread`] API. Unlike FreeRTOS, POSIX has
27//! no real scheduler to hand control to, so [`System::start`] just spins
28//! until [`System::stop`] is called from another thread.
29//!
30//! # Examples
31//!
32//! ```
33//! use osal_rs::os::*;
34//! use std::sync::Arc;
35//!
36//! // Something else must call `System::stop()` for `start()` to return.
37//! let mut stopper = Thread::new("stopper", 1024, 1);
38//! stopper.spawn_simple(|| {
39//! System::delay(10);
40//! System::stop();
41//! Ok(Arc::new(()))
42//! }).unwrap();
43//!
44//! System::start(); // blocks here until `stop()` runs above
45//! ```
46
47use core::ffi::c_long;
48use core::ops::Deref;
49use core::time::Duration;
50use std::sync::atomic::{AtomicBool, Ordering};
51
52use alloc::vec::Vec;
53
54use crate::os::ThreadFn;
55use crate::posix::ffi::{
56 CLOCK_MONOTONIC, PTHREAD_ONCE_INIT, _SC_AVPHYS_PAGES, _SC_PAGESIZE, clock_gettime, nanosleep, pthread_once, pthread_once_t, pthread_self, sched_yield, sysconf, timespec,
57};
58use crate::posix::thread::{Thread, all_registered_threads, registered_thread_count};
59use crate::posix::types::{BaseType, TickType, UBaseType};
60use crate::traits::{SystemFn, ThreadMetadata, ThreadState, ToTick};
61use crate::utils::OsalRsBool;
62
63static RUN: AtomicBool = AtomicBool::new(true);
64
65/// Snapshot returned by [`System::get_all_thread`]: every thread spawned
66/// through this crate's [`crate::os::Thread`] API (plus the calling thread
67/// itself), and the total elapsed run time at the moment of the snapshot.
68/// Derefs to `&[ThreadMetadata]` for convenient iteration.
69///
70/// # Examples
71///
72/// ```
73/// use osal_rs::os::*;
74///
75/// let state = System::get_all_thread();
76/// // The calling thread is always included.
77/// assert!(!state.is_empty());
78/// ```
79#[derive(Debug, Clone)]
80pub struct SystemState {
81 /// Metadata for every thread spawned through this crate's
82 /// [`crate::os::Thread`] API, plus the calling thread itself.
83 pub tasks: Vec<ThreadMetadata>,
84 /// Total elapsed run time, in milliseconds, at the moment of the
85 /// snapshot (see [`System::get_tick_count`]).
86 pub total_run_time: u32,
87}
88
89impl Deref for SystemState {
90 type Target = [ThreadMetadata];
91
92 fn deref(&self) -> &Self::Target {
93 &self.tasks
94 }
95}
96
97/// Namespace for system-level operations (scheduler control, timing, thread
98/// introspection) - see the [module docs](self) for an overview and a
99/// runnable example. Zero-sized: never instantiated, only used as
100/// `System::function(...)`.
101pub struct System;
102
103impl System {
104 /// Blocks like [`System::delay`], but accepts any [`ToTick`] duration
105 /// (e.g. a [`core::time::Duration`]) instead of a raw tick count.
106 ///
107 /// # Examples
108 ///
109 /// ```
110 /// use osal_rs::os::*;
111 /// use core::time::Duration;
112 ///
113 /// let before = System::get_tick_count();
114 /// System::delay_with_to_tick(Duration::from_millis(10));
115 /// assert!(System::get_tick_count() >= before);
116 /// ```
117 #[inline]
118 pub fn delay_with_to_tick(ticks: impl ToTick) {
119 Self::delay(ticks.to_ticks());
120 }
121
122 /// Blocks like [`System::delay_until`], but accepts any [`ToTick`]
123 /// increment (e.g. a [`core::time::Duration`]) instead of a raw tick
124 /// count.
125 ///
126 /// # Examples
127 ///
128 /// ```
129 /// use osal_rs::os::*;
130 /// use core::time::Duration;
131 ///
132 /// let mut previous = System::get_tick_count();
133 /// System::delay_until_with_to_tick(&mut previous, Duration::from_millis(5));
134 /// ```
135 #[inline]
136 pub fn delay_until_with_to_tick(previous_wake_time: &mut TickType, time_increment: impl ToTick) {
137 Self::delay_until(previous_wake_time, time_increment.to_ticks());
138 }
139
140 fn monotonic_now() -> Duration {
141 let mut ts = timespec::default();
142 unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts) };
143
144 Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
145 }
146
147 fn start_time() -> Duration {
148 static mut ONCE: pthread_once_t = PTHREAD_ONCE_INIT;
149 static mut START_TIME: Duration = Duration::ZERO;
150
151 extern "C" fn init() {
152 unsafe {
153 START_TIME = System::monotonic_now();
154 }
155 // Without this, a caller landing within nanoseconds of the
156 // epoch capture above would measure 0 elapsed ms (millisecond
157 // resolution) on its very first read, since `pthread_once`
158 // blocks every other racing caller until `init` returns.
159 // Paid once per process, lazily, only if timing is ever used.
160 System::delay(1);
161 }
162
163 unsafe {
164 pthread_once(&raw mut ONCE, Some(init));
165 START_TIME
166 }
167 }
168
169 fn elapsed() -> Duration {
170 Self::monotonic_now().checked_sub(Self::start_time()).unwrap_or_default()
171 }
172}
173
174impl SystemFn for System {
175 /// Spins until [`System::stop`] is called from another thread. There is
176 /// no real scheduler on POSIX to hand control to, so this is just a busy
177 /// loop over an atomic flag - unlike FreeRTOS, where the equivalent call
178 /// never returns.
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use osal_rs::os::*;
184 /// use std::sync::Arc;
185 ///
186 /// let mut stopper = Thread::new("stopper", 1024, 1);
187 /// stopper.spawn_simple(|| {
188 /// System::delay(10);
189 /// System::stop();
190 /// Ok(Arc::new(()))
191 /// }).unwrap();
192 ///
193 /// System::start(); // blocks here until `stop()` runs above
194 /// ```
195 fn start() {
196 loop {
197 if !RUN.load(Ordering::Acquire) {
198 break;
199 }
200 System::delay_with_to_tick(Duration::from_millis(500));
201 }
202 }
203
204 /// Suspends every currently `Ready`/`Running` thread spawned through
205 /// this crate's [`crate::os::Thread`] API (see
206 /// [`crate::os::ThreadFn::suspend`]).
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use osal_rs::os::*;
212 /// use std::sync::Arc;
213 ///
214 /// let mut worker = Thread::new("worker", 1024, 1);
215 /// worker.spawn_simple(|| {
216 /// System::delay(200);
217 /// Ok(Arc::new(()))
218 /// }).unwrap();
219 ///
220 /// System::delay(10); // give it a moment to start running
221 /// System::suspend_all();
222 /// assert!(System::resume_all() >= 1);
223 /// ```
224 fn suspend_all() {
225 for tm in all_registered_threads() {
226 if let Ok(t) = Thread::new_with_handle(tm.thread, tm.name.as_str(), tm.stack_depth, tm.current_priority) {
227 if tm.state == ThreadState::Ready || tm.state == ThreadState::Running {
228 t.suspend();
229 }
230 }
231 }
232 }
233
234 /// Resumes every currently `Suspended` thread spawned through this
235 /// crate's [`crate::os::Thread`] API, returning how many were resumed.
236 ///
237 /// See [`System::suspend_all`] for a complete example.
238 fn resume_all() -> BaseType {
239 let mut count = 0;
240
241 for tm in all_registered_threads() {
242 if let Ok(t) = Thread::new_with_handle(tm.thread, tm.name.as_str(), tm.stack_depth, tm.current_priority) {
243 if tm.state == ThreadState::Suspended {
244 t.resume();
245 count += 1;
246 }
247 }
248 }
249
250 count
251 }
252
253 /// Signals [`System::start`]'s spin loop to return. See
254 /// [`System::start`] for a complete example.
255 fn stop() {
256 RUN.store(false, Ordering::Release);
257 }
258
259 /// Returns the number of ticks elapsed since the first time any of
260 /// [`System::get_tick_count`]/[`System::get_current_time`] was called
261 /// in this process (that first call defines tick `0`).
262 ///
263 /// # Examples
264 ///
265 /// ```
266 /// use osal_rs::os::*;
267 ///
268 /// let before = System::get_tick_count();
269 /// System::delay(5);
270 /// assert!(System::get_tick_count() >= before);
271 /// ```
272 fn get_tick_count() -> TickType {
273 Self::elapsed().as_millis().min(TickType::MAX as u128) as TickType
274 }
275
276 /// Same reference point as [`System::get_tick_count`], but returned as a
277 /// [`Duration`] instead of a raw tick count.
278 ///
279 /// # Examples
280 ///
281 /// ```
282 /// use osal_rs::os::*;
283 ///
284 /// let before = System::get_current_time();
285 /// System::delay(5);
286 /// assert!(System::get_current_time() >= before);
287 /// ```
288 fn get_current_time() -> Duration {
289 let mut ts = timespec::default();
290
291 unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts) };
292
293 Duration::from_micros((ts.tv_sec * 1000 * 1000 + ts.tv_nsec / 1000) as u64)
294 }
295
296 /// Deprecated alias for [`System::get_current_time`]; kept for source
297 /// compatibility with code written before the rename.
298 #[inline]
299 fn get_current_time_ms() -> Duration {
300 Self::get_current_time()
301 }
302
303 /// Converts a [`Duration`] to POSIX ticks (milliseconds); see
304 /// `crate::posix::duration` for the same conversion via [`ToTick`].
305 ///
306 /// # Examples
307 ///
308 /// ```
309 /// use osal_rs::os::*;
310 /// use core::time::Duration;
311 ///
312 /// assert_eq!(System::get_from_tick(&Duration::from_millis(250)), 250);
313 /// ```
314 fn get_from_tick(duration: &Duration) -> TickType {
315 duration.as_millis().min(TickType::MAX as u128) as TickType
316 }
317
318 /// Deprecated alias for [`System::get_from_tick`]; kept for source
319 /// compatibility with code written before the rename.
320 #[inline]
321 fn get_ms_from_tick(duration: &Duration) -> TickType {
322 Self::get_from_tick(duration)
323 }
324
325 /// Number of threads known to the system: every thread spawned through
326 /// this crate's [`crate::os::Thread`] API, plus the calling thread
327 /// itself.
328 ///
329 /// # Examples
330 ///
331 /// ```
332 /// use osal_rs::os::*;
333 ///
334 /// // Just the calling thread: nothing else has been spawned yet.
335 /// assert_eq!(System::count_threads(), 1);
336 /// ```
337 fn count_threads() -> usize {
338 // +1 for the calling thread itself, which `get_all_thread()` below
339 // always reports even when it wasn't spawned through this crate's API.
340 1 + registered_thread_count()
341 }
342
343 /// Returns a [`SystemState`] snapshot of every thread known to the
344 /// system, mirroring [`System::count_threads`]'s "+1 for the caller"
345 /// accounting.
346 ///
347 /// # Examples
348 ///
349 /// ```
350 /// use osal_rs::os::*;
351 ///
352 /// let state = System::get_all_thread();
353 /// assert_eq!(state.len(), System::count_threads());
354 /// ```
355 fn get_all_thread() -> SystemState {
356 let mut tasks = all_registered_threads();
357
358 // Mirror `count_threads()`'s +1: report the calling thread even when
359 // it wasn't spawned through this crate's API. Skip it if the caller
360 // is itself a registered thread (e.g. a spawned worker calling this
361 // from within its own thread function), to avoid double-counting.
362 let caller = unsafe { pthread_self() };
363 if !tasks.iter().any(|metadata| metadata.thread == caller) {
364 tasks.push(Thread::get_metadata_from_handle(caller));
365 }
366
367 SystemState {
368 tasks,
369 total_run_time: Self::get_tick_count().min(TickType::MAX) as u32,
370 }
371 }
372
373 /// Blocks the calling thread for `ticks` (milliseconds on this
374 /// backend), automatically resuming the sleep if interrupted by a
375 /// signal before it elapsed.
376 ///
377 /// # Examples
378 ///
379 /// ```
380 /// use osal_rs::os::*;
381 ///
382 /// let before = System::get_tick_count();
383 /// System::delay(20);
384 /// assert!(System::get_tick_count() - before >= 20);
385 /// ```
386 fn delay(ticks: TickType) {
387 let mut req = timespec {
388 tv_sec: (ticks / 1000) as c_long,
389 tv_nsec: ((ticks % 1000) as c_long) * 1_000_000,
390 };
391
392 loop {
393 let mut rem = timespec::default();
394
395 if unsafe { nanosleep(&req, &mut rem) } == 0 {
396 break;
397 }
398
399 // Interrupted by a signal before `req` elapsed: `rem` holds the
400 // time still left to sleep, so resume with that. If the kernel
401 // left `rem` untouched (a real error, not EINTR), it'll be zero
402 // and the loop exits instead of spinning forever.
403 if rem.tv_sec == 0 && rem.tv_nsec == 0 {
404 break;
405 }
406
407 req = rem;
408 }
409 }
410
411 /// Blocks until `*previous_wake_time + time_increment` (absolute ticks),
412 /// then advances `*previous_wake_time` by `time_increment` - a fixed
413 /// period loop that doesn't drift with the time spent doing work each
414 /// iteration, unlike calling [`System::delay`] with the same increment
415 /// every time.
416 ///
417 /// # Examples
418 ///
419 /// ```
420 /// use osal_rs::os::*;
421 ///
422 /// let before = System::get_tick_count();
423 /// let mut previous = before;
424 /// System::delay_until(&mut previous, 20);
425 ///
426 /// assert_eq!(previous, before + 20);
427 /// assert!(System::get_tick_count() >= previous);
428 /// ```
429 fn delay_until(previous_wake_time: &mut TickType, time_increment: TickType) {
430 let next_wake_time = previous_wake_time.saturating_add(time_increment);
431 let now = Self::get_tick_count();
432
433 if next_wake_time > now {
434 Self::delay(next_wake_time - now);
435 }
436
437 *previous_wake_time = next_wake_time;
438 }
439
440 /// Returns [`OsalRsBool::True`] once at least `time` has elapsed since
441 /// `timestamp` (both measured against [`System::get_current_time`]'s
442 /// clock).
443 ///
444 /// # Examples
445 ///
446 /// ```
447 /// use osal_rs::os::*;
448 /// use osal_rs::utils::OsalRsBool;
449 /// use core::time::Duration;
450 ///
451 /// let start = System::get_current_time();
452 /// assert_eq!(System::check_timer(&start, &Duration::from_millis(500)), OsalRsBool::False);
453 ///
454 /// System::delay(20);
455 /// assert_eq!(System::check_timer(&start, &Duration::from_millis(10)), OsalRsBool::True);
456 /// ```
457 fn check_timer(timestamp: &Duration, time: &Duration) -> OsalRsBool {
458 let elapsed = Self::get_current_time().checked_sub(*timestamp).unwrap_or_default();
459
460 if elapsed >= *time {
461 OsalRsBool::True
462 } else {
463 OsalRsBool::False
464 }
465 }
466
467 /// Yields the processor (`sched_yield(2)`) if `higher_priority_task_woken`
468 /// is non-zero, a no-op otherwise. On FreeRTOS this triggers a context
469 /// switch to a just-woken higher-priority task from within an ISR; POSIX
470 /// has no real interrupt context, so this exists purely for API
471 /// compatibility.
472 ///
473 /// # Examples
474 ///
475 /// ```
476 /// use osal_rs::os::*;
477 ///
478 /// System::yield_from_isr(1); // yields
479 /// System::yield_from_isr(0); // no-op
480 /// ```
481 fn yield_from_isr(higher_priority_task_woken: BaseType) {
482 if higher_priority_task_woken != 0 {
483 unsafe {
484 sched_yield();
485 }
486 }
487 }
488
489 /// Identical to [`System::yield_from_isr`] under a different name,
490 /// matching FreeRTOS's `portEND_SWITCHING_ISR` naming convention.
491 ///
492 /// # Examples
493 ///
494 /// ```
495 /// use osal_rs::os::*;
496 ///
497 /// System::end_switching_isr(1);
498 /// ```
499 fn end_switching_isr(switch_required: BaseType) {
500 if switch_required != 0 {
501 unsafe {
502 sched_yield();
503 }
504 }
505 }
506
507 /// No-op on POSIX: there is no real interrupt/scheduler state to guard,
508 /// unlike FreeRTOS where this disables interrupts/the scheduler.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// use osal_rs::os::*;
514 ///
515 /// System::critical_section_enter();
516 /// System::critical_section_exit();
517 /// ```
518 fn critical_section_enter() {}
519
520 /// See [`System::critical_section_enter`].
521 fn critical_section_exit() {}
522
523 /// ISR-context counterpart of [`System::critical_section_enter`]; always
524 /// returns `0` (nothing to restore) since it's a no-op on POSIX.
525 ///
526 /// # Examples
527 ///
528 /// ```
529 /// use osal_rs::os::*;
530 ///
531 /// let saved = System::critical_section_enter_from_isr();
532 /// System::critical_section_exit_from_isr(saved);
533 /// ```
534 fn critical_section_enter_from_isr() -> UBaseType {
535 0
536 }
537
538 /// See [`System::critical_section_enter_from_isr`].
539 fn critical_section_exit_from_isr(_: UBaseType) {}
540
541 /// POSIX processes don't have a fixed heap the way FreeRTOS does (the
542 /// allocator can keep extending it via `mmap`/`brk`), so this reports
543 /// available physical memory as the closest analogue.
544 ///
545 /// # Examples
546 ///
547 /// ```
548 /// use osal_rs::os::*;
549 ///
550 /// assert!(System::get_free_heap_size() > 0);
551 /// ```
552 fn get_free_heap_size() -> usize {
553 // POSIX processes don't have a fixed heap the way FreeRTOS does (the
554 // allocator can keep extending it via mmap/brk), so this reports
555 // available physical memory as the closest analogue.
556 let page_size = unsafe { sysconf(_SC_PAGESIZE) };
557 let avail_pages = unsafe { sysconf(_SC_AVPHYS_PAGES) };
558
559 if page_size <= 0 || avail_pages <= 0 {
560 0
561 } else {
562 (page_size as usize).saturating_mul(avail_pages as usize)
563 }
564 }
565}