Merge branch 'feat/thread-based-alarm' into 'master'

feat: implement alarm(2) using thread-based POSIX timer machinery

See merge request redox-os/relibc!1109
This commit is contained in:
Jeremy Soller
2026-04-01 08:40:24 -06:00
13 changed files with 243 additions and 31 deletions
+3
View File
@@ -66,6 +66,9 @@ pub(crate) struct timer_internal_t {
pub caller_thread: crate::pthread::OsTid,
// relibc handles it_interval, not the kernel
pub next_wake_time: itimerspec,
// When non-zero, timer_routine delivers SIGALRM via kill(process_pid, sig)
// instead of rlct_kill (thread-specific). Used by alarm().
pub process_pid: platform::types::pid_t,
}
/// See <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/time.h.html>.
+2 -23
View File
@@ -24,7 +24,7 @@ use crate::{
},
out::Out,
platform::{
self, ERRNO, Pal, Sys,
self, ERRNO, Pal, PalSignal, Sys,
types::{
c_char, c_int, c_long, c_short, c_uint, c_ulonglong, c_void, gid_t, off_t, pid_t,
size_t, ssize_t, suseconds_t, time_t, uid_t, useconds_t,
@@ -137,28 +137,7 @@ pub unsafe extern "C" fn access(path: *const c_char, mode: c_int) -> c_int {
/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/alarm.html>.
#[unsafe(no_mangle)]
pub extern "C" fn alarm(seconds: c_uint) -> c_uint {
// TODO setitimer is unimplemented on Redox and obsolete
let timer = sys_time::itimerval {
it_value: timeval {
tv_sec: time_t::from(seconds),
tv_usec: 0,
},
..Default::default()
};
let mut otimer = sys_time::itimerval::default();
let errno_backup = platform::ERRNO.get();
let secs =
if unsafe { sys_time::setitimer(sys_time::ITIMER_REAL, &raw const timer, &raw mut otimer) }
< 0
{
0
} else {
otimer.it_value.tv_sec as c_uint + if otimer.it_value.tv_usec > 0 { 1 } else { 0 }
};
platform::ERRNO.set(errno_backup);
secs
Sys::alarm(seconds)
}
/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/chdir.html>.
+24 -2
View File
@@ -6,7 +6,7 @@ use core::{
use super::{
super::{
PalSignal,
types::{c_int, pid_t},
types::{c_int, c_uint, pid_t, time_t},
},
Sys, e_raw,
};
@@ -15,8 +15,10 @@ use crate::{
header::{
bits_time::timespec,
signal::{SA_RESTORER, SI_QUEUE, sigaction, siginfo_t, sigset_t, sigval, stack_t},
sys_time::itimerval,
sys_select::timeval,
sys_time::{self, itimerval},
},
platform,
};
impl PalSignal for Sys {
@@ -68,6 +70,26 @@ impl PalSignal for Sys {
Ok(())
}
/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/alarm.html>.
fn alarm(seconds: c_uint) -> c_uint {
let timer = itimerval {
it_value: timeval {
tv_sec: time_t::from(seconds),
tv_usec: 0,
},
..Default::default()
};
let mut otimer = itimerval::default();
let errno_backup = platform::ERRNO.get();
let secs = if Self::setitimer(sys_time::ITIMER_REAL, &timer, Some(&mut otimer)).is_err() {
0
} else {
otimer.it_value.tv_sec as c_uint + if otimer.it_value.tv_usec > 0 { 1 } else { 0 }
};
platform::ERRNO.set(errno_backup);
secs
}
fn sigaction(
sig: c_int,
act: Option<&sigaction>,
+2
View File
@@ -21,6 +21,8 @@ pub trait PalSignal: Pal {
fn setitimer(which: c_int, new: &itimerval, old: Option<&mut itimerval>) -> Result<()>;
fn alarm(seconds: c_uint) -> c_uint;
fn sigaction(sig: c_int, act: Option<&sigaction>, oact: Option<&mut sigaction>) -> Result<()>;
unsafe fn sigaltstack(ss: Option<&stack_t>, old_ss: Option<&mut stack_t>) -> Result<()>;
+1
View File
@@ -1298,6 +1298,7 @@ impl Pal for Sys {
timer_st.next_wake_time = itimerspec::default();
timer_st.thread = ptr::null_mut();
timer_st.caller_thread = caller_thread;
timer_st.process_pid = 0;
timer_buf
};
+113 -2
View File
@@ -8,11 +8,15 @@ use crate::{
bits_time::timespec,
errno::{EINVAL, ENOSYS},
signal::{
SIG_BLOCK, SIG_DFL, SIG_IGN, SIG_SETMASK, SIG_UNBLOCK, SS_DISABLE, SS_ONSTACK,
sigaction, siginfo_t, sigset_t, sigval, stack_t, ucontext_t,
SIG_BLOCK, SIG_DFL, SIG_IGN, SIG_SETMASK, SIG_UNBLOCK, SIGALRM, SIGEV_SIGNAL,
SS_DISABLE, SS_ONSTACK, sigaction, sigevent, siginfo_t, sigset_t, sigval, stack_t,
ucontext_t,
},
sys_time::{ITIMER_REAL, itimerval},
time::{itimerspec, timer_internal_t},
},
out::Out,
sync::Mutex,
};
use core::mem::offset_of;
use redox_protocols::protocol::ProcKillTarget;
@@ -20,6 +24,17 @@ use redox_rt::signal::{
PosixStackt, SigStack, Sigaction, SigactionFlags, SigactionKind, Sigaltstack, SignalHandler,
};
/// Wrapper for timer_t that implements Send (the timer_t pointer is a process-
/// wide mmap'd allocation that outlives any single thread).
struct AlarmTimer(timer_t);
// SAFETY: The timer_t pointer refers to an mmap'd timer_internal_t that is
// only accessed under the ALARM_TIMER mutex lock.
unsafe impl Send for AlarmTimer {}
/// Process-global singleton timer used by alarm(). Protected by a mutex to
/// ensure only one alarm is active at a time (POSIX requirement).
static ALARM_TIMER: Mutex<Option<AlarmTimer>> = Mutex::new(None);
const _: () = {
#[track_caller]
const fn assert_eq(a: usize, b: usize) {
@@ -245,4 +260,100 @@ impl PalSignal for Sys {
}
Ok(info.si_signo)
}
/// Thread-based alarm() that Recycles the existing POSIX timer machinery
/// (timerfd + eventfd + pthread) with process-level signal delivery.
///
/// Internally works with a timespec to allow sub-second timers in the
/// future (e.g. ualarm) as i've been asked, though the public API only exposes whole seconds.
fn alarm(seconds: c_uint) -> c_uint {
alarm_timespec(timespec {
tv_sec: seconds as time_t,
tv_nsec: 0,
})
}
}
/// Internal helper that arms/disarms the process-global alarm timer.
/// Accepts a full timespec so sub-second timers (ualarm) can reuse this later.
/// Returns the number of seconds remaining on the previous alarm (rounded up),
/// or 0 if there was no previous alarm.
///
/// TODO: This implementation does not survive `exec()`. POSIX requires that a
/// pending alarm be preserved across exec (the timer continues counting down
/// in the new process image as i understand).
fn alarm_timespec(duration: timespec) -> c_uint {
let mut guard = ALARM_TIMER.lock();
// Determine remaining time on any existing alarm
let remaining = if let Some(ref alarm) = *guard {
let mut cur = itimerspec::default();
if Sys::timer_gettime(alarm.0, Out::from_mut(&mut cur)).is_ok() {
let secs = cur.it_value.tv_sec as c_uint;
if cur.it_value.tv_nsec > 0 {
secs + 1 // POSIX: round up
} else {
secs
}
} else {
0
}
} else {
0
};
let disarm = duration.tv_sec == 0 && duration.tv_nsec == 0;
if disarm {
// alarm(0): cancel any pending alarm
if let Some(ref alarm) = *guard {
let zero = itimerspec::default();
let _ = Sys::timer_settime(alarm.0, 0, &zero, None);
}
return remaining;
}
// Lazily create the singleton timer if it doesn't exist yet
if guard.is_none() {
let evp = sigevent {
sigev_value: sigval {
sival_ptr: core::ptr::null_mut(),
},
sigev_signo: SIGALRM as c_int,
sigev_notify: SIGEV_SIGNAL,
sigev_notify_function: None,
sigev_notify_attributes: core::ptr::null_mut(),
};
let mut timer_id: timer_t = core::ptr::null_mut();
if Sys::timer_create(
crate::header::time::CLOCK_REALTIME,
&evp,
Out::from_mut(&mut timer_id),
)
.is_err()
{
return remaining;
}
// Enable process-wide signal delivery instead of thread-specific
let timer_st = unsafe { &mut *(timer_id as *mut timer_internal_t) };
timer_st.process_pid = Sys::getpid();
*guard = Some(AlarmTimer(timer_id));
}
let timer_id = guard
.as_ref()
.expect("alarm timer must exist after lazy init")
.0;
// Arm the timer as a one-shot (no interval)
let spec = itimerspec {
it_value: duration,
it_interval: timespec::default(),
};
let _ = Sys::timer_settime(timer_id, 0, &spec, None);
remaining
}
+9 -4
View File
@@ -9,7 +9,7 @@ use crate::{
time::timer_internal_t,
},
out::Out,
platform::{Pal, Sys, sys::event, types::c_void},
platform::{Pal, PalSignal, Sys, sys::event, types::c_void},
};
use core::{
mem::{MaybeUninit, size_of},
@@ -42,9 +42,14 @@ pub extern "C" fn timer_routine(arg: *mut c_void) -> *mut c_void {
fun(timer_st.evp.sigev_value);
}
} else if timer_st.evp.sigev_notify == SIGEV_SIGNAL {
if unsafe { Sys::rlct_kill(timer_st.caller_thread, timer_st.evp.sigev_signo as _) }
.is_err()
{
// process_pid != 0 => process-wide delivery (used by alarm())
// process_pid == 0 => thread-specific delivery (used by timer_create)
let result = if timer_st.process_pid != 0 {
Sys::kill(timer_st.process_pid, timer_st.evp.sigev_signo)
} else {
unsafe { Sys::rlct_kill(timer_st.caller_thread, timer_st.evp.sigev_signo as _) }
};
if result.is_err() {
break;
}
}
+1
View File
@@ -159,6 +159,7 @@ EXPECT_NAMES=\
time/timegm \
time/tzset \
unistd/access \
unistd/alarm \
unistd/constants \
unistd/confstr \
unistd/dup \
@@ -0,0 +1,4 @@
alarm(0) baseline: ok
alarm(1) fires: ok
alarm(0) cancel: ok
alarm re-arm returns remaining: ok
@@ -0,0 +1,4 @@
alarm(0) baseline: ok
alarm(1) fires: ok
alarm(0) cancel: ok
alarm re-arm returns remaining: ok
+80
View File
@@ -0,0 +1,80 @@
/*
* Tests for alarm(2) - POSIX: https://pubs.opengroup.org/onlinepubs/9799919799/functions/alarm.html
*
* Verifies:
* 1. alarm(0) with no pending alarm returns 0
* 2. alarm(1) delivers SIGALRM after ~1 second (tested via pause())
* 3. alarm(0) cancels a pending alarm
* 4. re-arming alarm returns the remaining seconds from the previous alarm
*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "test_helpers.h"
static volatile sig_atomic_t alarm_count = 0;
static void handler(int sig) {
(void)sig;
alarm_count++;
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
int r = sigaction(SIGALRM, &sa, NULL);
ERROR_IF(sigaction, r, == -1);
/* alarm(0) with no existing alarm must return 0 */
unsigned prev = alarm(0);
UNEXP_IF(alarm, (int)prev, != 0);
puts("alarm(0) baseline: ok");
/* alarm(1): SIGALRM must fire; pause() blocks until the signal arrives */
alarm_count = 0;
alarm(1);
pause();
if (alarm_count != 1) {
fprintf(stderr, "SIGALRM did not fire (count=%d)\n", (int)alarm_count);
return EXIT_FAILURE;
}
puts("alarm(1) fires: ok");
/* alarm(0) must cancel a pending alarm and return remaining secs > 0 */
alarm_count = 0;
alarm(10);
unsigned remaining = alarm(0);
if (remaining == 0) {
fprintf(stderr, "alarm(0) cancel: expected remaining > 0\n");
return EXIT_FAILURE;
}
/* Wait longer than original alarm; SIGALRM must NOT fire */
sleep(2);
if (alarm_count != 0) {
fprintf(stderr, "SIGALRM fired after alarm(0) cancel\n");
return EXIT_FAILURE;
}
puts("alarm(0) cancel: ok");
/*
* Re-arming: arm with 10s, sleep 1s, then re-arm with 3s.
* The return value must be the remaining time (~9s) which is > 0.
*/
alarm_count = 0;
alarm(10);
sleep(1);
remaining = alarm(3);
if (remaining == 0) {
fprintf(stderr, "re-arm: expected remaining > 0, got 0\n");
return EXIT_FAILURE;
}
alarm(0); /* disarm so SIGALRM doesn't fire after the test */
puts("alarm re-arm returns remaining: ok");
return EXIT_SUCCESS;
}