Real-Time Configuration (RtConfig)
HORUS provides RtConfig, a builder API for configuring system-level real-time settings. It gives hard real-time robotics applications deterministic execution on top of HORUS IPC, which measures in the hundreds of nanoseconds over shared memory (see Performance Impact).
Cross-Platform Support
RtConfig is designed for development on any OS with production deployment on Linux:
| Platform | RtConfig Behavior | Use Case |
|---|---|---|
| Linux | Full RT features enabled | Production deployment |
| macOS | Graceful degradation (no RT) | Development & testing |
| Windows | Graceful degradation (no RT) | Development & testing |
On non-Linux platforms, RtConfig::apply() returns a degraded result and your code continues running normally - just without kernel-level RT guarantees. This allows you to develop and test on any OS.
use horus::prelude::*;
let config = RtConfig::builder()
.memory_locked(true)
.scheduler(RtScheduler::Fifo)
.priority(80)
.cpu_affinity(&[2, 3])
.build();
match config.apply() {
Ok(RtApplyResult::FullSuccess) => println!("Full RT enabled (Linux)"),
Ok(RtApplyResult::Degraded(reasons)) => {
// macOS/Windows, and also Linux without PREEMPT_RT or without
// the required capabilities: keeps running, minus the RT guarantees
println!("Running in degraded mode: {:?}", reasons);
}
Err(e) => eprintln!("Error: {}", e),
}
Overview
RtConfig controls kernel-level features that eliminate latency sources (Linux only):
| Feature | Purpose | Default |
|---|---|---|
memory_locked | Prevent page faults via mlockall() | false |
scheduler | RT scheduling class (Normal/Fifo) | Normal |
priority | RT scheduling priority (1-99) | None (unset) |
cpu_affinity | Pin to specific CPU cores | None |
Quick Start
use horus::prelude::*;
// Apply hard real-time configuration
let config = RtConfig::builder()
.memory_locked(true)
.scheduler(RtScheduler::Fifo)
.priority(80)
.cpu_affinity(&[2, 3]) // Pin to cores 2, 3
.build();
config.apply()?;
// Now all HORUS operations run with RT guarantees
let link: Topic<CmdVel> = Topic::new("cmd_vel")?; // see Performance Impact below
Common Configurations
Every configuration is built with RtConfig::builder() - HORUS ships no preset
constructors, so the chains below are the recipes to copy.
Hard Real-Time
For safety-critical applications with strict timing requirements:
// High priority, locked memory, pinned to isolated cores
let config = RtConfig::builder()
.memory_locked(true)
.scheduler(RtScheduler::Fifo)
.priority(80)
.cpu_affinity(&[2, 3])
.build();
config.apply()?;
This enables:
mlockall(MCL_CURRENT | MCL_FUTURE)- No page faultsSCHED_FIFOat the requested priority, clamped to the kernel's RT range - Preempts all normal processes- CPU affinity - No migration jitter. The core list is a preference order, not a set: the thread is pinned to the first usable core in it and the rest never enter the affinity mask, even though the confirmation line echoes the whole list. To spread work across cores, pin each node's thread separately rather than passing several cores to one
Lower-Priority Real-Time
For applications that want RT scheduling without locking memory (runs without
CAP_IPC_LOCK, but still needs CAP_SYS_NICE for SCHED_FIFO):
let config = RtConfig::builder()
.scheduler(RtScheduler::Fifo)
.priority(50)
.build();
config.apply()?;
Normal Operation
Default configuration with no RT features:
let config = RtConfig::default(); // or RtConfig::builder().build()
config.apply()?; // No RT modifications
Builder API
Every setter in one chain:
use horus::prelude::*;
let config = RtConfig::builder()
.memory_locked(true) // mlockall()
.scheduler(RtScheduler::Fifo) // SCHED_FIFO
.priority(90) // High priority (1-99)
.cpu_affinity(&[4, 5, 6, 7]) // Pin to cores 4-7
.build();
config.apply()?;
Builder Methods
| Method | Description |
|---|---|
memory_locked(bool) | Enable/disable mlockall() |
scheduler(RtScheduler) | Scheduling class |
priority(i32) | RT priority (1-99 for Fifo) |
cpu_affinity(&[usize]) | CPU cores to pin to |
warn_on_degradation(bool) | Log warnings when features degrade |
You must call .priority(n) for .scheduler(RtScheduler::Fifo) to take effect -
apply() skips scheduler setup entirely when no priority is set.
Scheduling Classes
RtScheduler comes from the prelude (use horus::prelude::*;):
| Variant | Linux Scheduler | Preemption | Use Case |
|---|---|---|---|
Normal | SCHED_OTHER | Time-sliced | General applications |
Fifo | SCHED_FIFO | Highest-priority-first | Hard RT control loops |
RtScheduler has exactly these two variants. SCHED_DEADLINE is reachable, but
only per-node via NodeBuilder::deadline_scheduler() - not through RtScheduler.
System Requirements
Capabilities Required
| Feature | Capability Needed |
|---|---|
memory_locked | CAP_IPC_LOCK (or unlimited memlock) |
scheduler(Fifo) | CAP_SYS_NICE |
priority > 0 | CAP_SYS_NICE |
cpu_affinity | None (always available) |
Grant Capabilities (Recommended)
# For development - grant capabilities to your binary
sudo setcap 'cap_sys_nice=ep cap_ipc_lock=ep' ./target/release/my_robot
# For production - run as RT user
sudo usermod -a -G realtime $USER
Alternative: let HORUS do it
horus setup-rt --check # see what is missing
horus setup-rt # write the limits file, offer the RT kernel
horus setup-rt performs the configuration described below — the limits file
and, on a standard kernel, the distribution's real-time kernel package. It asks
before each of those two changes and takes silence for no, so a run without a
terminal (CI, a provisioning script) changes nothing unless HORUS_ASSUME_YES=1
is set. horus setup-rt --undo removes the limits file only; a kernel package or
an isolcpus boot parameter has to be undone by hand, and --undo prints the
removal command for the distribution it detects. The manual steps that follow are
the same thing done by hand.
Alternative: Increase memlock limit
# In /etc/security/limits.conf
your_user soft memlock unlimited
your_user hard memlock unlimited
PREEMPT_RT Kernel
For a bounded worst case under load, use a PREEMPT_RT kernel:
Ubuntu/Debian
The package name differs between the two — Ubuntu has never shipped the arch-suffixed name:
# Ubuntu
sudo apt install linux-image-realtime
# Debian
sudo apt install linux-image-rt-amd64 # linux-image-rt-arm64 on 64-bit ARM
# Reboot and select RT kernel in GRUB
sudo reboot
horus setup-rt picks the right one for the distribution it finds and asks before
installing anything. It also knows Fedora (kernel-rt kernel-rt-core) and Arch
(linux-rt linux-rt-headers); on any other distribution it prints those commands
and installs nothing. See
horus setup-rt
for the full table.
Verify RT Kernel
uname -a | grep -i rt
# Should show: PREEMPT_RT
Kernel Tuning
# Isolate CPU cores for RT (add to kernel cmdline)
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
# Disable CPU frequency scaling
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
Integration with the Scheduler
RtConfig provides system-level configuration. Combine it with per-node timing
constraints on the scheduler for node-level budgets and deadlines:
use horus::prelude::*;
use std::time::Duration;
// 1. Configure system for RT
RtConfig::builder()
.memory_locked(true)
.scheduler(RtScheduler::Fifo)
.priority(80)
.cpu_affinity(&[2, 3])
.build()
.apply()?;
// 2. Create scheduler with safety-critical settings
let mut scheduler = Scheduler::new()
.tick_rate(1000_u64.hz())
.prefer_rt();
// 3. Add nodes with timing constraints
struct MotorControl { /* ... */ }
impl Node for MotorControl {
fn tick(&mut self) {
// Control loop body
}
}
scheduler.add(MotorControl { /* ... */ })
.order(0) // Execution order 0 (highest)
.budget(Duration::from_micros(50)) // 50µs WCET budget
.deadline(Duration::from_micros(500)) // 500µs deadline (2kHz)
.done()?;
scheduler.run()?;
RT is enabled scheduler-wide rather than per node: .prefer_rt() attempts RT and
degrades with a warning, .require_rt() panics if the system lacks RT
capabilities.
Performance Impact
Every HORUS topic is shared-memory backed, so there is no intra-process fast
path - publisher and subscriber go through the same SHM ring even inside a
single process. Measured latency on an Intel Core i7-10750H @ 2.60 GHz
(6C/12T), powersave governor, 100K iterations per scenario - without any
RT configuration applied (benchmarks/README.md):
| Path | Backend | p50 | p99 |
|---|---|---|---|
| 1 pub / 2 sub | SpmcShm | 198 ns | 298 ns |
| 1 pub / 4 sub | SpmcShm | 236 ns | 417 ns |
| 4 pub / 4 sub | PodShm | 304 ns | 1.5 µs |
The backend is chosen automatically from the topic's topology and message type; the names above label what each benchmark exercised, not something you select.
There is no published before/after RT-config comparison. What RtConfig changes
is the tail, not the median: memory_locked keeps page faults out of the hot
path, SCHED_FIFO stops normal-priority tasks preempting the control loop, and
cpu_affinity stops the kernel migrating the thread between cores. Measure your
own worst case on your own kernel and hardware.
Graceful Degradation
RtConfig degrades gracefully when features aren't available:
use horus::prelude::*;
let config = RtConfig::builder()
.memory_locked(true)
.scheduler(RtScheduler::Fifo)
.priority(80)
.cpu_affinity(&[2, 3])
.build();
match config.apply() {
Ok(RtApplyResult::FullSuccess) => {
println!("Full RT enabled");
}
Ok(RtApplyResult::Degraded(reasons)) => {
// System continues with available features
for reason in &reasons {
match reason {
RtDegradation::MemoryLockUnavailable(msg) => {
println!("Memory locking unavailable: {}", msg);
}
RtDegradation::SchedulerDegraded(msg) => {
println!("RT scheduler unavailable: {}", msg);
}
RtDegradation::PriorityClamped { requested, actual } => {
println!("Priority clamped: {} -> {}", requested, actual);
}
RtDegradation::AffinityUnavailable(msg) => {
println!("CPU affinity unavailable: {}", msg);
}
RtDegradation::NoPreemptRt => {
println!("PREEMPT_RT kernel not detected");
}
}
}
}
Err(e) => eprintln!("IO error: {}", e),
}
Use warn_on_degradation(true) to automatically log warnings. apply() never
fails on degradation - it always returns Ok(RtApplyResult::Degraded(..)), so
inspect the result if you need to treat degradation as fatal.
Verification
Query Kernel RT Capabilities
use horus::prelude::*;
let info = RtKernelInfo::detect();
println!("PREEMPT_RT: {}", info.preempt_rt);
println!("Kernel: {}", info.kernel_version);
println!("RT priority range: {}-{}", info.min_rt_priority, info.max_rt_priority);
println!("mlockall permitted: {}", info.mlockall_permitted);
Check Current Scheduler and Affinity
RtConfig does not expose a getter for the current thread's scheduler or
affinity - use the standard tools:
# Scheduling class (FF = SCHED_FIFO) and priority
ps -eo pid,cls,pri,comm | grep my_robot
# CPU affinity of a given PID
taskset -cp $$
Troubleshooting
"Operation not permitted" on mlockall
# Check memlock limit
ulimit -l
# If not unlimited, increase in /etc/security/limits.conf
# Or grant CAP_IPC_LOCK capability
"Operation not permitted" on SCHED_FIFO
# Grant CAP_SYS_NICE
sudo setcap 'cap_sys_nice=ep' ./target/release/my_robot
High latency despite RT config
- Verify PREEMPT_RT kernel:
uname -a | grep RT - Check CPU isolation:
cat /sys/devices/system/cpu/isolated - Disable CPU frequency scaling
- Check for other RT processes:
ps -eo pid,cls,pri,comm | grep -E "FF|RR"
CPU affinity not taking effect
# Verify isolated cores
cat /sys/devices/system/cpu/isolated
# Should show your isolated cores: 2-3
Best Practices
- Isolate CPU cores - Use kernel
isolcpusparameter - Use PREEMPT_RT kernel - Essential for hard RT guarantees
- Pin to isolated cores - Avoid contention with system tasks
- Pre-fault everything -
memory_locked(true)covers the heap; the stack is separate and pre-faulted viaScheduler::prefault_stack(bytes), notRtConfig - Monitor worst-case latency - Not just average
- Test under load - RT violations often appear under stress
Complete Example
use horus::prelude::*;
fn main() -> Result<()> {
// Configure system for hard real-time
let rt_config = RtConfig::builder()
.memory_locked(true)
.scheduler(RtScheduler::Fifo)
.priority(80)
.cpu_affinity(&[2, 3]) // Isolated cores
.warn_on_degradation(true)
.build();
match rt_config.apply()? {
RtApplyResult::FullSuccess => println!("Full RT configuration applied"),
RtApplyResult::Degraded(reasons) => {
println!("RT applied with degradation: {:?}", reasons);
}
}
// Create RT communication channel
let producer: Topic<CmdVel> = Topic::new("motor_cmd")?;
let consumer: Topic<CmdVel> = Topic::new("motor_cmd")?;
// Every HORUS topic is shared-memory backed, so both endpoints talk
// through the same SHM ring even in one process - RT config is what
// keeps the tail flat
loop {
let cmd = CmdVel::new(1.0, 0.5);
producer.send(cmd);
if let Some(received) = consumer.recv() {
// Process at 10kHz+ with deterministic timing
}
}
}
See Also
- Execution Modes - Scheduler presets and configuration
- Safety Monitor - WCET and deadline monitoring
- Deterministic Execution - Reproducible execution