use std::collections::VecDeque;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use super::engine::{RtEngine, RtStats, RxVerdict, WatchdogVerdict};
use super::image::{IoImage, Validity, WatchdogState};
use super::layout::Layout;
use super::sched;
use super::RtError;
use crate::eth::poll::{poll_readable_into, wait_readable};
use crate::eth::AfPacketTransport;
use crate::eth::{EthTransport, MacAddr, TransportError, MAX_FRAME_LEN};
const MAX_RX_PER_PASS: usize = 64;
const MAX_NO_PROGRESS_ITERATIONS: u32 = 1000;
pub struct RtConfig {
pub iface: String,
pub our_mac: MacAddr,
pub cpu_mac: MacAddr,
pub layout: Layout,
pub image: Arc<IoImage>,
pub stats: Arc<RtStats>,
pub problem_indicator: Arc<AtomicBool>,
pub cpu_pin: Option<usize>,
pub rt_priority: Option<u8>,
pub lock_memory: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RtEvent {
WatchdogExpired,
SocketError(String),
SchedWarning(String),
Exited,
}
struct RtShared {
stop: AtomicBool,
running: AtomicBool,
events: Mutex<VecDeque<RtEvent>>,
event_fd: OwnedFd,
wake_fd: OwnedFd,
stats: Arc<RtStats>,
}
impl RtShared {
fn push_event(&self, event: RtEvent) {
let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
events.push_back(event);
signal_eventfd(self.event_fd.as_raw_fd());
}
fn request_stop(&self) {
self.stop.store(true, Ordering::Release);
signal_eventfd(self.wake_fd.as_raw_fd());
}
}
struct ExitGuard<'a>(&'a RtShared);
impl Drop for ExitGuard<'_> {
fn drop(&mut self) {
self.0.push_event(RtEvent::Exited);
self.0.running.store(false, Ordering::Release);
}
}
pub struct RtHandle {
shared: Arc<RtShared>,
thread: Mutex<Option<JoinHandle<()>>>,
}
impl Drop for RtHandle {
fn drop(&mut self) {
self.stop();
}
}
impl RtHandle {
pub fn stop(&self) {
self.shared.request_stop();
}
pub fn join(&self, timeout: Duration) -> Result<(), RtError> {
let deadline = Instant::now() + timeout;
loop {
let mut slot = self.thread.lock().unwrap_or_else(|e| e.into_inner());
match slot.as_ref() {
None => return Ok(()),
Some(handle) if handle.is_finished() => {
let handle = slot.take().expect("checked Some just above");
drop(slot);
let _ = handle.join(); return Ok(());
}
Some(_) => drop(slot),
}
if Instant::now() >= deadline {
return Err(RtError::Stopped);
}
std::thread::sleep(Duration::from_millis(1));
}
}
pub fn event_fd(&self) -> RawFd {
self.shared.event_fd.as_raw_fd()
}
pub fn take_event(&self) -> Option<RtEvent> {
let mut events = self.shared.events.lock().unwrap_or_else(|e| e.into_inner());
let event = events.pop_front();
if events.is_empty() {
drain_eventfd(self.shared.event_fd.as_raw_fd());
}
event
}
pub fn stats(&self) -> Arc<RtStats> {
self.shared.stats.clone()
}
pub fn is_running(&self) -> bool {
self.shared.running.load(Ordering::Acquire)
}
}
pub struct RtRunner;
impl RtRunner {
pub fn spawn(cfg: RtConfig) -> Result<RtHandle, RtError> {
let transport = AfPacketTransport::open(&cfg.iface)?;
transport.attach_filter(&crate::eth::bpf::rt_filter())?;
Self::spawn_with_transport(cfg, transport)
}
pub fn spawn_with_transport<T: EthTransport + 'static>(
cfg: RtConfig,
transport: T,
) -> Result<RtHandle, RtError> {
let timer = new_timerfd()?;
let shared = Arc::new(RtShared {
stop: AtomicBool::new(false),
running: AtomicBool::new(true),
events: Mutex::new(VecDeque::with_capacity(16)),
event_fd: new_eventfd()?,
wake_fd: new_eventfd()?,
stats: cfg.stats.clone(),
});
let thread_shared = Arc::clone(&shared);
let thread = std::thread::Builder::new()
.name("pnio-rt".to_string())
.spawn(move || {
let _exit = ExitGuard(&thread_shared);
run_loop(cfg, transport, timer, &thread_shared);
})?;
Ok(RtHandle {
shared,
thread: Mutex::new(Some(thread)),
})
}
}
#[derive(Clone, Copy)]
enum Pending {
Data(Validity),
Validity(Validity),
}
struct TickGrid {
period: Duration,
start: Option<Instant>,
ticks: u64,
}
impl TickGrid {
fn new(period: Duration) -> Self {
Self {
period,
start: None,
ticks: 0,
}
}
fn on_read(&mut self, now: Instant, expirations: u64) -> u64 {
match self.start {
None => {
self.start = Some(now);
self.ticks = 1;
0
}
Some(start) => {
self.ticks = self.ticks.saturating_add(expirations);
let offset = Duration::from_nanos(
(self.period.as_nanos() as u64).saturating_mul(self.ticks.saturating_sub(1)),
);
match start.checked_add(offset) {
Some(expected) => now.saturating_duration_since(expected).as_nanos() as u64,
None => 0,
}
}
}
}
fn ticks(&self) -> u64 {
self.ticks
}
}
fn run_loop<T: EthTransport>(cfg: RtConfig, transport: T, timer: OwnedFd, shared: &RtShared) {
let RtConfig {
our_mac,
cpu_mac,
layout,
image,
stats,
problem_indicator,
cpu_pin,
rt_priority,
lock_memory,
..
} = cfg;
if let Some(cpu) = cpu_pin {
if let Err(e) = sched::set_affinity(&[cpu]) {
shared.push_event(RtEvent::SchedWarning(format!("CPU pin {cpu}: {e}")));
}
}
if let Some(priority) = rt_priority {
if let Err(e) = sched::set_fifo(priority) {
shared.push_event(RtEvent::SchedWarning(format!(
"SCHED_FIFO priority {priority}: {e}"
)));
}
}
if lock_memory {
if let Err(e) = sched::lock_memory() {
shared.push_event(RtEvent::SchedWarning(format!("mlockall: {e}")));
}
sched::prefault_stack();
}
let period = layout.input_cr.period();
let mut snapshot = vec![0u8; layout.input_cr.data_length];
let mut rx_buf = [0u8; MAX_FRAME_LEN];
let mut engine = RtEngine::new(
layout,
our_mac,
cpu_mac,
Arc::clone(&stats),
Arc::clone(&problem_indicator),
);
let socket_fd = transport.raw_fd();
let mut fds = [timer.as_raw_fd(), shared.wake_fd.as_raw_fd(), 0];
let nfds = match socket_fd {
Some(fd) => {
fds[2] = fd;
3
}
None => 2,
};
let mut ready = [false; 3];
let mut grid = TickGrid::new(period);
let mut pending: Option<Pending> = None;
let mut no_progress: u32 = 0;
if let Err(e) = arm_timerfd(timer.as_raw_fd(), period) {
shared.push_event(RtEvent::SocketError(format!("timerfd arm: {e}")));
return;
}
while !shared.stop.load(Ordering::Acquire) {
if let Err(e) = poll_readable_into(&fds[..nfds], &mut ready[..nfds], None) {
shared.push_event(RtEvent::SocketError(format!("poll: {e}")));
break;
}
if ready[1] {
drain_eventfd(shared.wake_fd.as_raw_fd()); }
let mut progressed = false;
let mut ticked = false;
if ready[0] {
match read_timer(timer.as_raw_fd()) {
Err(e) => {
shared.push_event(RtEvent::SocketError(format!("timerfd read: {e}")));
break;
}
Ok(0) => {} Ok(expirations) => {
ticked = true;
progressed = true;
let now = Instant::now();
let is_first_read = grid.ticks() == 0;
let lateness = grid.on_read(now, expirations);
stats.tick_lateness.record(lateness);
let ticks = grid.ticks();
if let Some(p) = pending.take() {
retry_pending(p, &engine, &image, &stats, &mut pending);
}
if engine.check_watchdog(now) == WatchdogVerdict::Expired {
engine.mark_outputs_stale();
shared.push_event(RtEvent::WatchdogExpired);
let validity = Validity {
provider_run: engine.provider_run(),
primary: engine.primary(),
watchdog: WatchdogState::Expired,
last_rx_age: engine.last_rx().map(|t| now.saturating_duration_since(t)),
cycle: ticks,
};
if !image.rt_set_validity(validity) {
stats
.output_publish_deferred
.fetch_add(1, Ordering::Relaxed);
pending = Some(Pending::Validity(validity));
}
}
if !image.rt_snapshot_inputs(&mut snapshot) {
stats.input_snapshot_reused.fetch_add(1, Ordering::Relaxed);
}
let engine_expirations = if is_first_read {
1
} else {
u32::try_from(expirations).unwrap_or(u32::MAX)
};
let frame = engine.on_tick(engine_expirations, &snapshot);
if let Err(e) = transport.send(frame) {
shared.push_event(RtEvent::SocketError(format!("send: {e}")));
break;
}
stats.cycle_work.record(now.elapsed().as_nanos() as u64);
}
}
}
let drain = if nfds == 3 { ready[2] } else { ticked };
if drain {
let mut drained_frame = false;
if !drain_rx(
&transport,
&mut rx_buf,
socket_fd,
&mut engine,
&image,
&stats,
grid.ticks(),
&mut pending,
shared,
&mut drained_frame,
) {
break;
}
progressed = progressed || drained_frame;
}
if progressed {
no_progress = 0;
} else {
no_progress = no_progress.saturating_add(1);
if no_progress >= MAX_NO_PROGRESS_ITERATIONS {
shared.push_event(RtEvent::SocketError("poll made no progress".to_string()));
break;
}
}
}
}
fn drain_should_continue(has_fd: bool, readable: bool, got_frame: bool) -> bool {
if has_fd {
readable
} else {
got_frame
}
}
#[allow(clippy::too_many_arguments)]
fn drain_rx<T: EthTransport>(
transport: &T,
rx_buf: &mut [u8; MAX_FRAME_LEN],
socket_fd: Option<RawFd>,
engine: &mut RtEngine,
image: &IoImage,
stats: &RtStats,
ticks: u64,
pending: &mut Option<Pending>,
shared: &RtShared,
processed_frame: &mut bool,
) -> bool {
let has_fd = socket_fd.is_some();
for _ in 0..MAX_RX_PER_PASS {
if let Some(fd) = socket_fd {
match wait_readable(fd, Some(Duration::ZERO)) {
Ok(true) => {}
Ok(false) => break, Err(e) => {
shared.push_event(RtEvent::SocketError(format!("poll: {e}")));
return false;
}
}
}
let got_frame = match transport.recv_into(rx_buf, Some(Duration::ZERO)) {
Ok(None) => false,
Ok(Some(n)) => {
*processed_frame = true;
let now = Instant::now();
let prev_rx = engine.last_rx();
if let RxVerdict::Accepted { .. } = engine.on_frame(&rx_buf[..n], now) {
if let Some(prev) = prev_rx {
stats
.rx_interval
.record(now.saturating_duration_since(prev).as_nanos() as u64);
}
let validity = Validity {
provider_run: engine.provider_run(),
primary: engine.primary(),
watchdog: WatchdogState::Ok,
last_rx_age: Some(Duration::ZERO),
cycle: ticks,
};
if image.rt_publish(engine.rx_csdu(), validity) {
*pending = None;
} else {
stats
.output_publish_deferred
.fetch_add(1, Ordering::Relaxed);
*pending = Some(Pending::Data(validity));
}
}
true
}
Err(TransportError::FrameTooLong { .. }) => {
stats.rx_invalid.fetch_add(1, Ordering::Relaxed);
*processed_frame = true;
true
}
Err(e) => {
shared.push_event(RtEvent::SocketError(format!("recv: {e}")));
return false;
}
};
if !drain_should_continue(has_fd, true, got_frame) {
break;
}
}
true
}
fn retry_pending(
p: Pending,
engine: &RtEngine,
image: &IoImage,
stats: &RtStats,
pending: &mut Option<Pending>,
) {
let taken = match p {
Pending::Data(v) => image.rt_publish(engine.rx_csdu(), v),
Pending::Validity(v) => image.rt_set_validity(v),
};
if !taken {
stats
.output_publish_deferred
.fetch_add(1, Ordering::Relaxed);
*pending = Some(p);
}
}
fn new_timerfd() -> Result<OwnedFd, RtError> {
let raw = unsafe {
libc::timerfd_create(
libc::CLOCK_MONOTONIC,
libc::TFD_NONBLOCK | libc::TFD_CLOEXEC,
)
};
if raw < 0 {
return Err(RtError::Io(std::io::Error::last_os_error()));
}
Ok(unsafe { OwnedFd::from_raw_fd(raw) })
}
fn arm_timerfd(fd: RawFd, period: Duration) -> Result<(), RtError> {
if period.is_zero() {
return Err(RtError::Io(std::io::Error::from_raw_os_error(libc::EINVAL)));
}
let spec = libc::itimerspec {
it_interval: to_timespec(period),
it_value: to_timespec(period),
};
let ret = unsafe { libc::timerfd_settime(fd, 0, &spec, ptr::null_mut()) };
if ret < 0 {
return Err(RtError::Io(std::io::Error::last_os_error()));
}
Ok(())
}
fn to_timespec(d: Duration) -> libc::timespec {
libc::timespec {
tv_sec: d.as_secs() as _,
tv_nsec: d.subsec_nanos() as _,
}
}
fn read_timer(fd: RawFd) -> io::Result<u64> {
let mut buf = [0u8; 8];
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n == 8 {
Ok(u64::from_ne_bytes(buf))
} else if n < 0 {
let e = io::Error::last_os_error();
match e.raw_os_error() {
Some(libc::EAGAIN) => Ok(0), _ => Err(e),
}
} else {
Ok(0)
}
}
fn new_eventfd() -> Result<OwnedFd, RtError> {
let raw = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
if raw < 0 {
return Err(RtError::Io(std::io::Error::last_os_error()));
}
Ok(unsafe { OwnedFd::from_raw_fd(raw) })
}
fn signal_eventfd(fd: RawFd) {
let one: u64 = 1;
let _ = unsafe { libc::write(fd, &one as *const u64 as *const libc::c_void, 8) };
}
fn drain_eventfd(fd: RawFd) {
let mut buf = [0u8; 8];
let _ = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cm::{validate, ConnectReq, DeviceModel};
use crate::eth::{MacAddr, MockTransport};
use crate::rt::image::{Freshness, IoImage};
use crate::rt::layout::Layout;
use crate::testutil::{golden, golden_rt};
use std::sync::Arc;
use std::time::{Duration, Instant};
const CPU: MacAddr = MacAddr([0xec, 0x1c, 0x5d, 0x61, 0xe7, 0x3f]);
const DEV: MacAddr = MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
fn layout() -> Layout {
let model = DeviceModel::pnet_sample(DEV);
let req = ConnectReq::parse(&golden("connect_req")[142..]).unwrap();
Layout::from_ar(&validate(&req, &model).unwrap(), &model).unwrap()
}
fn cfg(image: Arc<IoImage>, stats: Arc<RtStats>) -> RtConfig {
let mut layout = layout();
layout.input_cr.cycle_step = 160; layout.output_cr.cycle_step = 160;
layout.output_cr.watchdog = Duration::from_millis(15);
RtConfig {
iface: String::new(),
our_mac: DEV,
cpu_mac: CPU,
layout,
image,
stats,
problem_indicator: Arc::new(AtomicBool::new(false)),
cpu_pin: None,
rt_priority: None,
lock_memory: false,
}
}
#[test]
fn tick_grid_anchors_on_first_read_and_does_not_count_its_setup_expirations() {
let period = Duration::from_millis(1);
let mut grid = TickGrid::new(period);
let t0 = Instant::now();
assert_eq!(grid.on_read(t0, 3), 0);
assert_eq!(grid.ticks(), 1);
assert_eq!(grid.on_read(t0 + period, 1), 0);
assert_eq!(grid.ticks(), 2);
let late = t0 + 2 * period + Duration::from_micros(250);
assert_eq!(grid.on_read(late, 1), 250_000);
assert_eq!(grid.ticks(), 3);
let later = t0 + 4 * period + Duration::from_micros(10);
assert_eq!(grid.on_read(later, 2), 10_000);
assert_eq!(grid.ticks(), 5);
let early = t0 + 5 * period - Duration::from_millis(1);
assert_eq!(grid.on_read(early, 1), 0);
assert_eq!(grid.ticks(), 6);
}
#[test]
fn runner_ticks_sends_and_consumes_with_a_mock_transport() {
let image = Arc::new(IoImage::new(&layout()));
let stats = Arc::new(RtStats::default());
let mock = MockTransport::new();
mock.push_rx(golden_rt("echo_cpu_8001"));
let mock = Arc::new(mock);
image.write_inputs(1, 1, &[0x5a]).unwrap();
let h = RtRunner::spawn_with_transport(
cfg(image.clone(), stats.clone()),
SharedMock(mock.clone()),
)
.unwrap();
std::thread::sleep(Duration::from_millis(60));
h.stop();
h.join(Duration::from_secs(1)).unwrap();
let sent = mock.sent();
assert!(sent.len() >= 8 && sent.len() <= 20, "sent {}", sent.len());
assert_eq!(&sent[0][12..18], &[0x81, 0x00, 0xc0, 0x00, 0x88, 0x92]);
assert_eq!(sent[0][20 + 3], 0x5a); let qb0 = image.read_outputs(2, 1, |b, _| b[0]).unwrap();
assert_eq!(qb0, 0x01);
assert_eq!(image.validity().freshness(), Freshness::Stale); assert!(stats.snapshot().tx >= 8);
assert_eq!(stats.snapshot().rx_accepted, 1);
assert_eq!(stats.snapshot().watchdog_expirations, 1);
assert_eq!(stats.snapshot().missed_ticks, 0);
assert_eq!(stats.cycle_work.count(), stats.snapshot().tx);
assert_eq!(stats.tick_lateness.count(), stats.snapshot().tx);
assert_eq!(stats.rx_interval.count(), 0); assert_eq!(h.take_event(), Some(RtEvent::WatchdogExpired));
assert_eq!(h.take_event(), Some(RtEvent::Exited));
}
#[test]
fn repeated_golden_frame_yields_one_rx_interval_sample() {
let image = Arc::new(IoImage::new(&layout()));
let stats = Arc::new(RtStats::default());
let mock = MockTransport::new();
mock.push_rx(golden_rt("echo_cpu_8001"));
mock.push_rx(golden_rt("echo_cpu_8001"));
let mock = Arc::new(mock);
let h =
RtRunner::spawn_with_transport(cfg(image, stats.clone()), SharedMock(mock)).unwrap();
std::thread::sleep(Duration::from_millis(60));
h.stop();
h.join(Duration::from_secs(1)).unwrap();
assert_eq!(stats.snapshot().rx_accepted, 2);
assert_eq!(stats.rx_interval.count(), 1);
assert!(stats.rx_interval.max_ns() > 0);
}
#[test]
fn oversized_frame_counts_rx_invalid_and_the_thread_keeps_ticking() {
let image = Arc::new(IoImage::new(&layout()));
let stats = Arc::new(RtStats::default());
let mock = MockTransport::new();
mock.push_rx(vec![0u8; MAX_FRAME_LEN + 1]);
let mock = Arc::new(mock);
let h =
RtRunner::spawn_with_transport(cfg(image, stats.clone()), SharedMock(mock)).unwrap();
std::thread::sleep(Duration::from_millis(30));
let tx_before = stats.snapshot().tx;
std::thread::sleep(Duration::from_millis(30));
let tx_after = stats.snapshot().tx;
h.stop();
h.join(Duration::from_secs(1)).unwrap();
assert_eq!(stats.snapshot().rx_invalid, 1);
assert!(tx_before > 0, "runner never ticked");
assert!(
tx_after > tx_before,
"runner stopped ticking after the bad frame"
);
assert!(!h.is_running());
}
#[test]
fn stop_is_prompt_and_join_times_out_cleanly() {
let image = Arc::new(IoImage::new(&layout()));
let h = RtRunner::spawn_with_transport(
cfg(image, Arc::new(RtStats::default())),
MockTransport::new(),
)
.unwrap();
let t = Instant::now();
h.stop();
h.join(Duration::from_secs(1)).unwrap();
assert!(t.elapsed() < Duration::from_millis(200));
assert!(!h.is_running());
}
#[test]
fn sched_warning_is_reported_not_fatal() {
let image = Arc::new(IoImage::new(&layout()));
let mut c = cfg(image, Arc::new(RtStats::default()));
c.rt_priority = Some(80); let h = RtRunner::spawn_with_transport(c, MockTransport::new()).unwrap();
std::thread::sleep(Duration::from_millis(20));
let was_running = h.is_running();
h.stop();
h.join(Duration::from_secs(1)).unwrap();
let first = h.take_event();
let got_warning = matches!(first, Some(RtEvent::SchedWarning(_)));
assert!(
got_warning || was_running,
"expected a SchedWarning, or at least a running thread before stop; \
got {first:?} (was_running={was_running})"
);
}
#[test]
fn dropping_the_handle_stops_the_thread() {
let image = Arc::new(IoImage::new(&layout()));
let stats = Arc::new(RtStats::default());
let h = RtRunner::spawn_with_transport(cfg(image, stats.clone()), MockTransport::new())
.unwrap();
std::thread::sleep(Duration::from_millis(40)); drop(h); std::thread::sleep(Duration::from_millis(30));
let tx_after_drop = stats.snapshot().tx;
assert!(tx_after_drop > 0, "runner never ticked");
std::thread::sleep(Duration::from_millis(30));
assert_eq!(
stats.snapshot().tx,
tx_after_drop,
"thread kept sending after the handle was dropped"
);
}
#[test]
fn drain_should_continue_pins_the_policy() {
assert!(drain_should_continue(true, true, false));
assert!(drain_should_continue(true, true, true));
assert!(drain_should_continue(false, true, true));
assert!(!drain_should_continue(false, true, false));
}
struct SharedMock(Arc<MockTransport>);
impl crate::eth::EthTransport for SharedMock {
fn send(&self, f: &[u8]) -> Result<(), crate::eth::TransportError> {
self.0.send(f)
}
fn recv_into(
&self,
buf: &mut [u8],
timeout: Option<Duration>,
) -> Result<Option<usize>, crate::eth::TransportError> {
self.0.recv_into(buf, timeout)
}
}
}