From f3bd6ed8116b89cbe6dfb3c942048dae09375685 Mon Sep 17 00:00:00 2001 From: Vincent Dagonneau Date: Tue, 3 Mar 2020 16:30:30 +0100 Subject: [PATCH] Adding an implementation and some basic tests for timerfd. Removed support for timerfd on Android as it seems to have been deprecated? See https://android.googlesource.com/platform/development/+/73a5a3b/ndk/platforms/android-20/include/sys/timerfd.h or https://github.com/rust-lang/libc/issues/1589 Removed the public status of `TimerSpec`, as it should not be exposed to the user. Implemented `FromRawFd` for `TimerFd` as it already implements `AsRawFd`. Addressed comments from the latest code review: - Removed upper bound assertions on timer expirations in tests. - Made the main example runnable and added code to show how to wait for the timer. - Refactored `ClockId` to use `libc_enum`. - Added comments for all public parts of the module. - Wrapped to 80 cols. - Changed the size of the buffer in the tests to the minimum required. * Ran rustfmt. * Added a `From` implementation for `libc::timespec` -> `TimeSpec`. * Reworked the example with the new changes and changed the timer from 5 to 1 second. * Added a constructor for a 0-initialized `TimerSpec`. * Added a new method to get the timer configured expiration (based on timerfd_gettime). * Added an helper method to unset the expiration of the timer. * Added a `wait` method to actually read from the timer. * Renamed `settime` into just `set`. * Refactored the tests and added a new one that tests both the `unset` and the `get` method. Modified CHANGELOG. --- CHANGELOG.md | 2 + src/sys/mod.rs | 3 + src/sys/time.rs | 5 + src/sys/timerfd.rs | 272 +++++++++++++++++++++++++++++++++++++++ test/sys/mod.rs | 2 + test/sys/test_timerfd.rs | 61 +++++++++ 6 files changed, 345 insertions(+) create mode 100644 src/sys/timerfd.rs create mode 100644 test/sys/test_timerfd.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9205945ef2..d53b7f8e95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). (#[1259](https://github.com/nix-rust/nix/pull/1259)) - Added support for `Ipv4PacketInfo` and `Ipv6PacketInfo` to `ControlMessage` for iOS and Android. (#[1265](https://github.com/nix-rust/nix/pull/1265)) +- Added support for `TimerFd`. + (#[1261](https://github.com/nix-rust/nix/pull/1261)) ### Changed - Changed `fallocate` return type from `c_int` to `()` (#[1201](https://github.com/nix-rust/nix/pull/1201)) diff --git a/src/sys/mod.rs b/src/sys/mod.rs index 1b2d537076..bf7f541204 100644 --- a/src/sys/mod.rs +++ b/src/sys/mod.rs @@ -102,3 +102,6 @@ pub mod wait; #[cfg(any(target_os = "android", target_os = "linux"))] pub mod inotify; + +#[cfg(target_os = "linux")] +pub mod timerfd; diff --git a/src/sys/time.rs b/src/sys/time.rs index 51baa9e10f..973a3526c7 100644 --- a/src/sys/time.rs +++ b/src/sys/time.rs @@ -60,6 +60,11 @@ const TS_MAX_SECONDS: i64 = ::std::isize::MAX as i64; const TS_MIN_SECONDS: i64 = -TS_MAX_SECONDS; +impl From for TimeSpec { + fn from(ts: timespec) -> Self { + Self(ts) + } +} impl AsRef for TimeSpec { fn as_ref(&self) -> ×pec { diff --git a/src/sys/timerfd.rs b/src/sys/timerfd.rs new file mode 100644 index 0000000000..3086309e62 --- /dev/null +++ b/src/sys/timerfd.rs @@ -0,0 +1,272 @@ +//! Timer API via file descriptors. +//! +//! Timer FD is a Linux-only API to create timers and get expiration +//! notifications through file descriptors. +//! +//! For more documentation, please read [timerfd_create(2)](http://man7.org/linux/man-pages/man2/timerfd_create.2.html). +//! +//! # Examples +//! +//! Create a new one-shot timer that expires after 1 second. +//! ``` +//! # use std::os::unix::io::AsRawFd; +//! # use nix::sys::timerfd::{TimerFd, ClockId, TimerFlags, TimerSetTimeFlags, +//! # Expiration}; +//! # use nix::sys::time::{TimeSpec, TimeValLike}; +//! # use nix::unistd::read; +//! # +//! // We create a new monotonic timer. +//! let timer = TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty()) +//! .unwrap(); +//! +//! // We set a new one-shot timer in 1 seconds. +//! timer.set( +//! Expiration::OneShot(TimeSpec::seconds(1)), +//! TimerSetTimeFlags::empty() +//! ).unwrap(); +//! +//! // We wait for the timer to expire. +//! timer.wait().unwrap(); +//! ``` +use crate::sys::time::TimeSpec; +use crate::unistd::read; +use crate::{errno::Errno, Error, Result}; +use bitflags::bitflags; +use libc::c_int; +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; + +/// A timerfd instance. This is also a file descriptor, you can feed it to +/// other interfaces consuming file descriptors, epoll for example. +#[derive(Debug, Clone, Copy)] +pub struct TimerFd { + fd: RawFd, +} + +impl AsRawFd for TimerFd { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl FromRawFd for TimerFd { + unsafe fn from_raw_fd(fd: RawFd) -> Self { + TimerFd { fd } + } +} + +libc_enum! { + /// The type of the clock used to mark the progress of the timer. For more + /// details on each kind of clock, please refer to [timerfd_create(2)](http://man7.org/linux/man-pages/man2/timerfd_create.2.html). + #[repr(i32)] + pub enum ClockId { + CLOCK_REALTIME, + CLOCK_MONOTONIC, + CLOCK_BOOTTIME, + CLOCK_REALTIME_ALARM, + CLOCK_BOOTTIME_ALARM, + } +} + +libc_bitflags! { + /// Additional flags to change the behaviour of the file descriptor at the + /// time of creation. + pub struct TimerFlags: c_int { + TFD_NONBLOCK; + TFD_CLOEXEC; + } +} + +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 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 for TimerSpec { + fn as_ref(&self) -> &libc::itimerspec { + &self.0 + } +} + +impl From 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 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, + /// NONBLOCK). + pub fn new(clockid: ClockId, flags: TimerFlags) -> Result { + Errno::result(unsafe { libc::timerfd_create(clockid as i32, flags.bits()) }) + .map(|fd| Self { fd }) + } + + /// Sets 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 disables 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 disables the alarm + /// altogether. + pub fn set(&self, expiration: Expiration, flags: TimerSetTimeFlags) -> Result<()> { + let timerspec: TimerSpec = expiration.into(); + Errno::result(unsafe { + libc::timerfd_settime( + self.fd, + flags.bits(), + timerspec.as_ref(), + std::ptr::null_mut(), + ) + }) + .map(drop) + } + + /// Get the parameters for the alarm currently set, if any. + pub fn get(&self) -> Result> { + 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 + { + None + } else { + Some(timerspec.into()) + } + }) + } + + /// Remove the alarm if any is set. + pub fn unset(&self) -> Result<()> { + Errno::result(unsafe { + libc::timerfd_settime( + self.fd, + TimerSetTimeFlags::empty().bits(), + TimerSpec::none().as_ref(), + std::ptr::null_mut(), + ) + }) + .map(drop) + } + + /// Wait for the configured alarm to expire. + /// + /// Note: If the alarm is unset, then you will wait forever. + pub fn wait(&self) -> Result<()> { + loop { + if let Err(e) = read(self.fd, &mut [0u8; 8]) { + match e { + Error::Sys(Errno::EINTR) => continue, + _ => return Err(e), + } + } else { + break; + } + } + + Ok(()) + } +} diff --git a/test/sys/mod.rs b/test/sys/mod.rs index 3dc430eef1..c4391c72fa 100644 --- a/test/sys/mod.rs +++ b/test/sys/mod.rs @@ -41,3 +41,5 @@ mod test_pthread; target_os = "netbsd", target_os = "openbsd"))] mod test_ptrace; +#[cfg(any(target_os = "android", target_os = "linux"))] +mod test_timerfd; diff --git a/test/sys/test_timerfd.rs b/test/sys/test_timerfd.rs new file mode 100644 index 0000000000..24fb2ac002 --- /dev/null +++ b/test/sys/test_timerfd.rs @@ -0,0 +1,61 @@ +use nix::sys::time::{TimeSpec, TimeValLike}; +use nix::sys::timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags}; +use std::time::Instant; + +#[test] +pub fn test_timerfd_oneshot() { + let timer = TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty()).unwrap(); + + let before = Instant::now(); + + timer + .set( + Expiration::OneShot(TimeSpec::seconds(1)), + TimerSetTimeFlags::empty(), + ) + .unwrap(); + + timer.wait().unwrap(); + + let millis = before.elapsed().as_millis(); + assert!(millis > 900); +} + +#[test] +pub fn test_timerfd_interval() { + let timer = TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty()).unwrap(); + + let before = Instant::now(); + timer + .set( + Expiration::IntervalDelayed(TimeSpec::seconds(1), TimeSpec::seconds(2)), + TimerSetTimeFlags::empty(), + ) + .unwrap(); + + timer.wait().unwrap(); + + let start_delay = before.elapsed().as_millis(); + assert!(start_delay > 900); + + timer.wait().unwrap(); + + let interval_delay = before.elapsed().as_millis(); + assert!(interval_delay > 2900); +} + +#[test] +pub fn test_timerfd_unset() { + let timer = TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty()).unwrap(); + + timer + .set( + Expiration::OneShot(TimeSpec::seconds(1)), + TimerSetTimeFlags::empty(), + ) + .unwrap(); + + timer.unset().unwrap(); + + assert!(timer.get().unwrap() == None); +}