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