1622: Introduce `timer_*` support  r=asomers a=blt

This commit adds support for the signal timer mechanism in POSIX, the mirror to timerfd on Linux. I wasn't _quite_ sure of how to fit into the project organization but hopefully this patch isn't too far off.

Resolves #1424

Signed-off-by: Brian L. Troutwine <brian@troutwine.us>

1623: Remove cc dependency on DragonFly r=asomers a=rtzoeller

f5ee22db48 removed the need for this dependency.

Co-authored-by: Brian L. Troutwine <brian@troutwine.us>
Co-authored-by: Ryan Zoeller <rtzoeller@rtzoeller.com>
This commit is contained in:
bors[bot] 2021-12-31 20:49:28 +00:00 committed by GitHub
commit ee0543f126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 432 additions and 104 deletions

View File

@ -27,6 +27,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
(#[1619](https://github.com/nix-rust/nix/pull/1619))
- Added the `TxTime` sockopt and control message.
(#[1564](https://github.com/nix-rust/nix/pull/1564))
- Added POSIX per-process timer support
(#[1622](https://github.com/nix-rust/nix/pull/1622))
### Changed
### Fixed

View File

@ -76,9 +76,6 @@ uio = []
users = ["features"]
zerocopy = ["fs", "uio"]
[target.'cfg(target_os = "dragonfly")'.build-dependencies]
cc = "1"
[dev-dependencies]
assert-impl = "0.1"
lazy_static = "1.2"

View File

@ -201,3 +201,18 @@ feature! {
#[allow(missing_docs)]
pub mod timerfd;
}
#[cfg(all(
any(
target_os = "freebsd",
target_os = "illumos",
target_os = "linux",
target_os = "netbsd"
),
feature = "time",
feature = "signal"
))]
feature! {
#![feature = "time"]
pub mod timer;
}

View File

@ -1085,6 +1085,11 @@ mod sigevent {
pub fn sigevent(&self) -> libc::sigevent {
self.sigevent
}
/// Returns a mutable pointer to the `sigevent` wrapped by `self`
pub fn as_mut_ptr(&mut self) -> *mut libc::sigevent {
&mut self.sigevent
}
}
impl<'a> From<&'a libc::sigevent> for SigEvent {

View File

@ -5,6 +5,129 @@ use libc::{timespec, timeval};
#[cfg_attr(target_env = "musl", allow(deprecated))] // https://github.com/rust-lang/libc/issues/1848
pub use libc::{time_t, suseconds_t};
#[cfg(any(
all(feature = "time", any(target_os = "android", target_os = "linux")),
all(
any(
target_os = "freebsd",
target_os = "illumos",
target_os = "linux",
target_os = "netbsd"
),
feature = "time",
feature = "signal"
)
))]
pub(crate) mod timer {
use crate::sys::time::TimeSpec;
use bitflags::bitflags;
#[derive(Debug, Clone, Copy)]
pub(crate) struct TimerSpec(libc::itimerspec);
impl TimerSpec {
pub const fn none() -> Self {
Self(libc::itimerspec {
it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
})
}
}
impl AsMut<libc::itimerspec> for TimerSpec {
fn as_mut(&mut self) -> &mut libc::itimerspec {
&mut self.0
}
}
impl AsRef<libc::itimerspec> for TimerSpec {
fn as_ref(&self) -> &libc::itimerspec {
&self.0
}
}
impl From<Expiration> for TimerSpec {
fn from(expiration: Expiration) -> TimerSpec {
match expiration {
Expiration::OneShot(t) => TimerSpec(libc::itimerspec {
it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: *t.as_ref(),
}),
Expiration::IntervalDelayed(start, interval) => TimerSpec(libc::itimerspec {
it_interval: *interval.as_ref(),
it_value: *start.as_ref(),
}),
Expiration::Interval(t) => TimerSpec(libc::itimerspec {
it_interval: *t.as_ref(),
it_value: *t.as_ref(),
}),
}
}
}
/// An enumeration allowing the definition of the expiration time of an alarm,
/// recurring or not.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Expiration {
/// Alarm will trigger once after the time given in `TimeSpec`
OneShot(TimeSpec),
/// Alarm will trigger after a specified delay and then every interval of
/// time.
IntervalDelayed(TimeSpec, TimeSpec),
/// Alarm will trigger every specified interval of time.
Interval(TimeSpec),
}
#[cfg(any(target_os = "android", target_os = "linux"))]
bitflags! {
/// Flags that are used for arming the timer.
pub struct TimerSetTimeFlags: libc::c_int {
const TFD_TIMER_ABSTIME = libc::TFD_TIMER_ABSTIME;
}
}
#[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "dragonfly", target_os = "illumos"))]
bitflags! {
/// Flags that are used for arming the timer.
pub struct TimerSetTimeFlags: libc::c_int {
const TFD_TIMER_ABSTIME = libc::TIMER_ABSTIME;
}
}
impl From<TimerSpec> for Expiration {
fn from(timerspec: TimerSpec) -> Expiration {
match timerspec {
TimerSpec(libc::itimerspec {
it_interval:
libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: ts,
}) => Expiration::OneShot(ts.into()),
TimerSpec(libc::itimerspec {
it_interval: int_ts,
it_value: val_ts,
}) => {
if (int_ts.tv_sec == val_ts.tv_sec) && (int_ts.tv_nsec == val_ts.tv_nsec) {
Expiration::Interval(int_ts.into())
} else {
Expiration::IntervalDelayed(val_ts.into(), int_ts.into())
}
}
}
}
}
}
pub trait TimeValLike: Sized {
#[inline]
fn zero() -> Self {

175
src/sys/timer.rs Normal file
View File

@ -0,0 +1,175 @@
//! Timer API via signals.
//!
//! Timer is a POSIX API to create timers and get expiration notifications
//! through queued Unix signals, for the current process. This is similar to
//! Linux's timerfd mechanism, except that API is specific to Linux and makes
//! use of file polling.
//!
//! For more documentation, please read [timer_create](https://pubs.opengroup.org/onlinepubs/9699919799/functions/timer_create.html).
//!
//! # Examples
//!
//! Create an interval timer that signals SIGALARM every 250 milliseconds.
//!
//! ```no_run
//! use nix::sys::signal::{self, SigEvent, SigHandler, SigevNotify, Signal};
//! use nix::sys::timer::{Expiration, Timer, TimerSetTimeFlags};
//! use nix::time::ClockId;
//! use std::convert::TryFrom;
//! use std::sync::atomic::{AtomicU64, Ordering};
//! use std::thread::yield_now;
//! use std::time::Duration;
//!
//! const SIG: Signal = Signal::SIGALRM;
//! static ALARMS: AtomicU64 = AtomicU64::new(0);
//!
//! extern "C" fn handle_alarm(signal: libc::c_int) {
//! let signal = Signal::try_from(signal).unwrap();
//! if signal == SIG {
//! ALARMS.fetch_add(1, Ordering::Relaxed);
//! }
//! }
//!
//! fn main() {
//! let clockid = ClockId::CLOCK_MONOTONIC;
//! let sigevent = SigEvent::new(SigevNotify::SigevSignal {
//! signal: SIG,
//! si_value: 0,
//! });
//!
//! let mut timer = Timer::new(clockid, sigevent).unwrap();
//! let expiration = Expiration::Interval(Duration::from_millis(250).into());
//! let flags = TimerSetTimeFlags::empty();
//! timer.set(expiration, flags).expect("could not set timer");
//!
//! let handler = SigHandler::Handler(handle_alarm);
//! unsafe { signal::signal(SIG, handler) }.unwrap();
//!
//! loop {
//! let alarms = ALARMS.load(Ordering::Relaxed);
//! if alarms >= 10 {
//! println!("total alarms handled: {}", alarms);
//! break;
//! }
//! yield_now()
//! }
//! }
//! ```
use crate::sys::signal::SigEvent;
use crate::sys::time::timer::TimerSpec;
pub use crate::sys::time::timer::{Expiration, TimerSetTimeFlags};
use crate::time::ClockId;
use crate::{errno::Errno, Result};
use core::mem;
/// A Unix signal per-process timer.
#[derive(Debug)]
#[repr(transparent)]
pub struct Timer(libc::timer_t);
impl Timer {
/// Creates a new timer based on the clock defined by `clockid`. The details
/// of the signal and its handler are defined by the passed `sigevent`.
pub fn new(clockid: ClockId, mut sigevent: SigEvent) -> Result<Self> {
let mut timer_id: mem::MaybeUninit<libc::timer_t> = mem::MaybeUninit::uninit();
Errno::result(unsafe {
libc::timer_create(
clockid.as_raw(),
sigevent.as_mut_ptr(),
timer_id.as_mut_ptr(),
)
})
.map(|_| {
// SAFETY: libc::timer_create is responsible for initializing
// timer_id.
unsafe { Self(timer_id.assume_init()) }
})
}
/// Set a new alarm on the timer.
///
/// # Types of alarm
///
/// There are 3 types of alarms you can set:
///
/// - one shot: the alarm will trigger once after the specified amount of
/// time.
/// Example: I want an alarm to go off in 60s and then disable itself.
///
/// - interval: the alarm will trigger every specified interval of time.
/// Example: I want an alarm to go off every 60s. The alarm will first
/// go off 60s after I set it and every 60s after that. The alarm will
/// not disable itself.
///
/// - interval delayed: the alarm will trigger after a certain amount of
/// time and then trigger at a specified interval.
/// Example: I want an alarm to go off every 60s but only start in 1h.
/// The alarm will first trigger 1h after I set it and then every 60s
/// after that. The alarm will not disable itself.
///
/// # Relative vs absolute alarm
///
/// If you do not set any `TimerSetTimeFlags`, then the `TimeSpec` you pass
/// to the `Expiration` you want is relative. If however you want an alarm
/// to go off at a certain point in time, you can set `TFD_TIMER_ABSTIME`.
/// Then the one shot TimeSpec and the delay TimeSpec of the delayed
/// interval are going to be interpreted as absolute.
///
/// # Disabling alarms
///
/// Note: Only one alarm can be set for any given timer. Setting a new alarm
/// actually removes the previous one.
///
/// Note: Setting a one shot alarm with a 0s TimeSpec disable the alarm
/// altogether.
pub fn set(&mut self, expiration: Expiration, flags: TimerSetTimeFlags) -> Result<()> {
let timerspec: TimerSpec = expiration.into();
Errno::result(unsafe {
libc::timer_settime(
self.0,
flags.bits(),
timerspec.as_ref(),
core::ptr::null_mut(),
)
})
.map(drop)
}
/// Get the parameters for the alarm currently set, if any.
pub fn get(&self) -> Result<Option<Expiration>> {
let mut timerspec = TimerSpec::none();
Errno::result(unsafe { libc::timer_gettime(self.0, timerspec.as_mut()) }).map(|_| {
if timerspec.as_ref().it_interval.tv_sec == 0
&& timerspec.as_ref().it_interval.tv_nsec == 0
&& timerspec.as_ref().it_value.tv_sec == 0
&& timerspec.as_ref().it_value.tv_nsec == 0
{
None
} else {
Some(timerspec.into())
}
})
}
/// Return the number of timers that have overrun
///
/// Each timer is able to queue one signal to the process at a time, meaning
/// if the signal is not handled before the next expiration the timer has
/// 'overrun'. This function returns how many times that has happened to
/// this timer, up to `libc::DELAYTIMER_MAX`. If more than the maximum
/// number of overruns have happened the return is capped to the maximum.
pub fn overruns(&self) -> i32 {
unsafe { libc::timer_getoverrun(self.0) }
}
}
impl Drop for Timer {
fn drop(&mut self) {
if !std::thread::panicking() {
let result = Errno::result(unsafe { libc::timer_delete(self.0) });
if let Err(Errno::EINVAL) = result {
panic!("close of Timer encountered EINVAL");
}
}
}
}

View File

@ -28,10 +28,10 @@
//! // We wait for the timer to expire.
//! timer.wait().unwrap();
//! ```
use crate::sys::time::TimeSpec;
use crate::sys::time::timer::TimerSpec;
pub use crate::sys::time::timer::{Expiration, TimerSetTimeFlags};
use crate::unistd::read;
use crate::{errno::Errno, Result};
use bitflags::bitflags;
use libc::c_int;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
@ -77,93 +77,6 @@ libc_bitflags! {
}
}
bitflags! {
/// Flags that are used for arming the timer.
pub struct TimerSetTimeFlags: libc::c_int {
const TFD_TIMER_ABSTIME = libc::TFD_TIMER_ABSTIME;
}
}
#[derive(Debug, Clone, Copy)]
struct TimerSpec(libc::itimerspec);
impl TimerSpec {
pub const fn none() -> Self {
Self(libc::itimerspec {
it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
})
}
}
impl AsRef<libc::itimerspec> for TimerSpec {
fn as_ref(&self) -> &libc::itimerspec {
&self.0
}
}
impl From<Expiration> for TimerSpec {
fn from(expiration: Expiration) -> TimerSpec {
match expiration {
Expiration::OneShot(t) => TimerSpec(libc::itimerspec {
it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: *t.as_ref(),
}),
Expiration::IntervalDelayed(start, interval) => TimerSpec(libc::itimerspec {
it_interval: *interval.as_ref(),
it_value: *start.as_ref(),
}),
Expiration::Interval(t) => TimerSpec(libc::itimerspec {
it_interval: *t.as_ref(),
it_value: *t.as_ref(),
}),
}
}
}
impl From<TimerSpec> for Expiration {
fn from(timerspec: TimerSpec) -> Expiration {
match timerspec {
TimerSpec(libc::itimerspec {
it_interval:
libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: ts,
}) => Expiration::OneShot(ts.into()),
TimerSpec(libc::itimerspec {
it_interval: int_ts,
it_value: val_ts,
}) => {
if (int_ts.tv_sec == val_ts.tv_sec) && (int_ts.tv_nsec == val_ts.tv_nsec) {
Expiration::Interval(int_ts.into())
} else {
Expiration::IntervalDelayed(val_ts.into(), int_ts.into())
}
}
}
}
}
/// An enumeration allowing the definition of the expiration time of an alarm,
/// recurring or not.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Expiration {
OneShot(TimeSpec),
IntervalDelayed(TimeSpec, TimeSpec),
Interval(TimeSpec),
}
impl TimerFd {
/// Creates a new timer based on the clock defined by `clockid`. The
/// underlying fd can be assigned specific flags with `flags` (CLOEXEC,
@ -181,7 +94,7 @@ impl TimerFd {
///
/// - one shot: the alarm will trigger once after the specified amount of
/// time.
/// Example: I want an alarm to go off in 60s and then disables itself.
/// Example: I want an alarm to go off in 60s and then disable itself.
///
/// - interval: the alarm will trigger every specified interval of time.
/// Example: I want an alarm to go off every 60s. The alarm will first
@ -225,13 +138,11 @@ impl TimerFd {
/// Get the parameters for the alarm currently set, if any.
pub fn get(&self) -> Result<Option<Expiration>> {
let mut timerspec = TimerSpec::none();
let timerspec_ptr: *mut libc::itimerspec = &mut timerspec.0;
Errno::result(unsafe { libc::timerfd_gettime(self.fd, timerspec_ptr) }).map(|_| {
if timerspec.0.it_interval.tv_sec == 0
&& timerspec.0.it_interval.tv_nsec == 0
&& timerspec.0.it_value.tv_sec == 0
&& timerspec.0.it_value.tv_nsec == 0
Errno::result(unsafe { libc::timerfd_gettime(self.fd, timerspec.as_mut()) }).map(|_| {
if timerspec.as_ref().it_interval.tv_sec == 0
&& timerspec.as_ref().it_interval.tv_nsec == 0
&& timerspec.as_ref().it_value.tv_sec == 0
&& timerspec.as_ref().it_value.tv_nsec == 0
{
None
} else {
@ -259,7 +170,7 @@ impl TimerFd {
pub fn wait(&self) -> Result<()> {
while let Err(e) = read(self.fd, &mut [0u8; 8]) {
if e != Errno::EINTR {
return Err(e)
return Err(e);
}
}
@ -270,9 +181,7 @@ impl TimerFd {
impl Drop for TimerFd {
fn drop(&mut self) {
if !std::thread::panicking() {
let result = Errno::result(unsafe {
libc::close(self.fd)
});
let result = Errno::result(unsafe { libc::close(self.fd) });
if let Err(Errno::EBADF) = result {
panic!("close of TimerFd encountered EBADF");
}

View File

@ -41,6 +41,17 @@ mod test_sendfile;
mod test_stat;
mod test_time;
mod test_unistd;
#[cfg(all(
any(
target_os = "freebsd",
target_os = "illumos",
target_os = "linux",
target_os = "netbsd"
),
feature = "time",
feature = "signal"
))]
mod test_timer;
use std::os::unix::io::RawFd;
use std::path::PathBuf;

91
test/test_timer.rs Normal file
View File

@ -0,0 +1,91 @@
use nix::sys::signal::{
sigaction, SaFlags, SigAction, SigEvent, SigHandler, SigSet, SigevNotify, Signal,
};
use nix::sys::timer::{Expiration, Timer, TimerSetTimeFlags};
use nix::time::ClockId;
use std::convert::TryFrom;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};
const SIG: Signal = Signal::SIGALRM;
static ALARM_CALLED: AtomicBool = AtomicBool::new(false);
pub extern "C" fn handle_sigalarm(raw_signal: libc::c_int) {
let signal = Signal::try_from(raw_signal).unwrap();
if signal == SIG {
ALARM_CALLED.store(true, Ordering::Release);
}
}
#[test]
fn alarm_fires() {
// Avoid interfering with other signal using tests by taking a mutex shared
// among other tests in this crate.
let _m = crate::SIGNAL_MTX.lock();
//
// Setup
//
// Create a handler for the test signal, `SIG`. The handler is responsible
// for flipping `ALARM_CALLED`.
let handler = SigHandler::Handler(handle_sigalarm);
let signal_action = SigAction::new(handler, SaFlags::SA_RESTART, SigSet::empty());
let old_handler =
unsafe { sigaction(SIG, &signal_action).expect("unable to set signal handler for alarm") };
// Create the timer. We use the monotonic clock here, though any would do
// really. The timer is set to fire every 250 milliseconds with no delay for
// the initial firing.
let clockid = ClockId::CLOCK_MONOTONIC;
let sigevent = SigEvent::new(SigevNotify::SigevSignal {
signal: SIG,
si_value: 0,
});
let mut timer = Timer::new(clockid, sigevent).expect("failed to create timer");
let expiration = Expiration::Interval(Duration::from_millis(250).into());
let flags = TimerSetTimeFlags::empty();
timer.set(expiration, flags).expect("could not set timer");
//
// Test
//
// Determine that there's still an expiration tracked by the
// timer. Depending on when this runs either an `Expiration::Interval` or
// `Expiration::IntervalDelayed` will be present. That is, if the timer has
// not fired yet we'll get our original `expiration`, else the one that
// represents a delay to the next expiration. We're only interested in the
// timer still being extant.
match timer.get() {
Ok(Some(exp)) => {
assert!(matches!(
exp,
Expiration::Interval(..) | Expiration::IntervalDelayed(..)
))
}
_ => panic!("timer lost its expiration"),
}
// Wait for 2 firings of the alarm before checking that it has fired and
// been handled at least the once. If we wait for 3 seconds and the handler
// is never called something has gone sideways and the test fails.
let starttime = Instant::now();
loop {
thread::sleep(Duration::from_millis(500));
if ALARM_CALLED.load(Ordering::Acquire) {
break;
}
if starttime.elapsed() > Duration::from_secs(3) {
panic!("Timeout waiting for SIGALRM");
}
}
// Replace the old signal handler now that we've completed the test. If the
// test fails this process panics, so the fact we might not get here is
// okay.
unsafe {
sigaction(SIG, &old_handler).expect("unable to reset signal handler");
}
}