Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace RobotNet10.Realtime;
/// <summary>
/// Provides CPU affinity management for Linux real-time applications.
/// </summary>
public class CpuAffinity : IDisposable
{
private LinuxNative.cpu_set_t _cpuSet;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of CpuAffinity.
/// </summary>
public CpuAffinity()
{
LinuxNative.CPU_ZERO(ref _cpuSet);
}
/// <summary>
/// Adds a CPU to the affinity set.
/// </summary>
/// <param name="cpu">CPU number (0-based)</param>
public void AddCpu(int cpu)
{
if (cpu < 0 || cpu >= LinuxNative.CPU_SETSIZE)
{
throw new ArgumentOutOfRangeException(nameof(cpu),
$"CPU number must be between 0 and {LinuxNative.CPU_SETSIZE - 1}");
}
LinuxNative.CPU_SET(cpu, ref _cpuSet);
}
/// <summary>
/// Removes a CPU from the affinity set.
/// </summary>
/// <param name="cpu">CPU number (0-based)</param>
public void RemoveCpu(int cpu)
{
if (cpu < 0 || cpu >= LinuxNative.CPU_SETSIZE)
{
throw new ArgumentOutOfRangeException(nameof(cpu),
$"CPU number must be between 0 and {LinuxNative.CPU_SETSIZE - 1}");
}
LinuxNative.CPU_CLR(cpu, ref _cpuSet);
}
/// <summary>
/// Checks if a CPU is in the affinity set.
/// </summary>
/// <param name="cpu">CPU number (0-based)</param>
/// <returns>True if CPU is in the set, false otherwise</returns>
public bool IsCpuSet(int cpu)
{
if (cpu < 0 || cpu >= LinuxNative.CPU_SETSIZE)
{
return false;
}
return LinuxNative.CPU_ISSET(cpu, ref _cpuSet) != 0;
}
/// <summary>
/// Gets the number of CPUs in the affinity set.
/// </summary>
public int CpuCount => LinuxNative.CPU_COUNT(ref _cpuSet);
/// <summary>
/// Gets all CPUs in the affinity set.
/// </summary>
/// <returns>List of CPU numbers</returns>
public List<int> GetCpus()
{
var cpus = new List<int>();
for (int i = 0; i < LinuxNative.CPU_SETSIZE; i++)
{
if (IsCpuSet(i))
{
cpus.Add(i);
}
}
return cpus;
}
/// <summary>
/// Sets CPU affinity for the current thread.
/// </summary>
/// <param name="cpus">List of CPU numbers to set</param>
public static void SetAffinity(params int[] cpus)
{
var affinity = new CpuAffinity();
foreach (var cpu in cpus)
{
affinity.AddCpu(cpu);
}
affinity.Apply();
}
/// <summary>
/// Applies the CPU affinity to the current thread.
/// </summary>
public void Apply()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(CpuAffinity));
}
IntPtr cpusetsize = new IntPtr(LinuxNative.CPU_SETSIZE / 8); // Size in bytes
int result = LinuxNative.sched_setaffinity(0, cpusetsize, ref _cpuSet);
if (result != 0)
{
LinuxNative.ThrowLastError("sched_setaffinity");
}
}
/// <summary>
/// Gets the current CPU affinity for the current thread.
/// </summary>
/// <returns>CpuAffinity object representing current affinity</returns>
public static CpuAffinity GetAffinity()
{
var cpuSet = new LinuxNative.cpu_set_t();
IntPtr cpusetsize = new IntPtr(LinuxNative.CPU_SETSIZE / 8);
int result = LinuxNative.sched_getaffinity(0, cpusetsize, ref cpuSet);
if (result != 0)
{
LinuxNative.ThrowLastError("sched_getaffinity");
}
var affinity = new CpuAffinity();
affinity._cpuSet = cpuSet;
return affinity;
}
/// <summary>
/// Clears all CPUs from the affinity set.
/// </summary>
public void Clear()
{
LinuxNative.CPU_ZERO(ref _cpuSet);
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
}
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace RobotNet10.Realtime;
/// <summary>
/// Native Linux system calls and constants for realtime operations.
/// </summary>
internal static partial class LinuxNative
{
// Scheduling policies
public const int SCHED_NORMAL = 0;
public const int SCHED_FIFO = 1;
public const int SCHED_RR = 2;
public const int SCHED_BATCH = 3;
public const int SCHED_IDLE = 5;
public const int SCHED_DEADLINE = 6;
// Clock IDs
public const int CLOCK_REALTIME = 0;
public const int CLOCK_MONOTONIC = 1;
public const int CLOCK_PROCESS_CPUTIME_ID = 2;
public const int CLOCK_THREAD_CPUTIME_ID = 3;
public const int CLOCK_MONOTONIC_RAW = 4;
public const int CLOCK_REALTIME_COARSE = 5;
public const int CLOCK_MONOTONIC_COARSE = 6;
public const int CLOCK_BOOTTIME = 7;
public const int CLOCK_REALTIME_ALARM = 8;
public const int CLOCK_BOOTTIME_ALARM = 9;
// Timerfd flags
public const int TFD_NONBLOCK = 0x800;
public const int TFD_CLOEXEC = 0x80000;
// Timerfd timer types
public const int TFD_TIMER_ABSTIME = 0x1;
// CPU set size (for CPU affinity)
public const int CPU_SETSIZE = 1024;
public const int __NCPUBITS = 64;
// Memory lock flags
public const int MCL_CURRENT = 1;
public const int MCL_FUTURE = 2;
public const int MCL_ONFAULT = 4;
// Signal numbers for real-time signals (SIGRTMIN to SIGRTMAX)
public const int SIGRTMIN = 34;
public const int SIGRTMAX = 64;
// Priority ranges
public const int MIN_PRIORITY = 1;
public const int MAX_PRIORITY = 99;
[StructLayout(LayoutKind.Sequential)]
public struct sched_param
{
public int sched_priority;
}
[StructLayout(LayoutKind.Sequential)]
public struct timespec
{
public long tv_sec; // seconds
public long tv_nsec; // nanoseconds
}
[StructLayout(LayoutKind.Sequential)]
public struct itimerspec
{
public timespec it_interval; // timer interval
public timespec it_value; // timer expiration
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct cpu_set_t
{
// Using fixed buffer instead of array with MarshalAs for LibraryImport compatibility
// 16 * 64 = 1024 bits
public fixed ulong __bits[16];
}
// Scheduling functions
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_setscheduler(int pid, int policy, ref sched_param param);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_getscheduler(int pid);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_getparam(int pid, ref sched_param param);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_setparam(int pid, ref sched_param param);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_get_priority_min(int policy);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_get_priority_max(int policy);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_yield();
// CPU affinity functions
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_setaffinity(int pid, IntPtr cpusetsize, ref cpu_set_t mask);
[LibraryImport("libc", SetLastError = true)]
public static partial int sched_getaffinity(int pid, IntPtr cpusetsize, ref cpu_set_t mask);
// CPU set manipulation macros (implemented as functions)
public static unsafe void CPU_ZERO(ref cpu_set_t set)
{
for (int i = 0; i < 16; i++)
{
set.__bits[i] = 0;
}
}
public static unsafe void CPU_SET(int cpu, ref cpu_set_t set)
{
int index = cpu / __NCPUBITS;
int bit = cpu % __NCPUBITS;
if (index < 16)
{
set.__bits[index] |= (1UL << bit);
}
}
public static unsafe void CPU_CLR(int cpu, ref cpu_set_t set)
{
int index = cpu / __NCPUBITS;
int bit = cpu % __NCPUBITS;
if (index < 16)
{
set.__bits[index] &= ~(1UL << bit);
}
}
public static unsafe int CPU_ISSET(int cpu, ref cpu_set_t set)
{
int index = cpu / __NCPUBITS;
int bit = cpu % __NCPUBITS;
if (index >= 16)
{
return 0;
}
return (set.__bits[index] & (1UL << bit)) != 0 ? 1 : 0;
}
public static unsafe int CPU_COUNT(ref cpu_set_t set)
{
int count = 0;
for (int i = 0; i < 16; i++)
{
count += System.Numerics.BitOperations.PopCount(set.__bits[i]);
}
return count;
}
// Memory locking functions
[LibraryImport("libc", SetLastError = true)]
public static partial int mlock(IntPtr addr, IntPtr len);
[LibraryImport("libc", SetLastError = true)]
public static partial int munlock(IntPtr addr, IntPtr len);
[LibraryImport("libc", SetLastError = true)]
public static partial int mlockall(int flags);
[LibraryImport("libc", SetLastError = true)]
public static partial int munlockall();
// Clock functions
[LibraryImport("libc", SetLastError = true)]
public static partial int clock_gettime(int clockid, ref timespec tp);
[LibraryImport("libc", SetLastError = true)]
public static partial int clock_settime(int clockid, ref timespec tp);
[LibraryImport("libc", SetLastError = true)]
public static partial int clock_getres(int clockid, ref timespec res);
// Timerfd functions
[LibraryImport("libc", SetLastError = true)]
public static partial int timerfd_create(int clockid, int flags);
[LibraryImport("libc", SetLastError = true)]
public static partial int timerfd_settime(int fd, int flags, ref itimerspec new_value, ref itimerspec old_value);
[LibraryImport("libc", SetLastError = true)]
public static partial int timerfd_gettime(int fd, ref itimerspec curr_value);
// Process/Thread functions
[LibraryImport("libc", SetLastError = true)]
public static partial int getpid();
[LibraryImport("libc", SetLastError = true)]
public static partial uint gettid();
// Error handling
[LibraryImport("libc")]
public static partial IntPtr __errno_location();
public static int GetLastError()
{
IntPtr errnoPtr = __errno_location();
if (errnoPtr == IntPtr.Zero)
{
return 0;
}
return Marshal.ReadInt32(errnoPtr);
}
public static void ThrowLastError(string operation)
{
int errno = GetLastError();
IntPtr errorMsgPtr = strerror(errno);
string errorMsg = errorMsgPtr != IntPtr.Zero ? Marshal.PtrToStringAnsi(errorMsgPtr) ?? "Unknown error" : "Unknown error";
throw new RealtimeException($"{operation} failed with errno {errno}: {errorMsg}", errno);
}
[LibraryImport("libc")]
private static partial IntPtr strerror(int errno);
}

View File

@@ -0,0 +1,116 @@
using System;
using System.Runtime.InteropServices;
namespace RobotNet10.Realtime;
/// <summary>
/// Provides memory locking functionality for real-time applications.
/// Prevents memory from being swapped to disk, ensuring deterministic access times.
/// </summary>
public static class MemoryLock
{
/// <summary>
/// Locks a specific memory region to prevent swapping.
/// </summary>
/// <param name="address">Pointer to the memory region</param>
/// <param name="length">Length of the memory region in bytes</param>
/// <exception cref="InvalidOperationException">Thrown when the operation fails.</exception>
public static void Lock(IntPtr address, IntPtr length)
{
int result = LinuxNative.mlock(address, length);
if (result != 0)
{
LinuxNative.ThrowLastError("mlock");
}
}
/// <summary>
/// Unlocks a specific memory region.
/// </summary>
/// <param name="address">Pointer to the memory region</param>
/// <param name="length">Length of the memory region in bytes</param>
/// <exception cref="InvalidOperationException">Thrown when the operation fails.</exception>
public static void Unlock(IntPtr address, IntPtr length)
{
int result = LinuxNative.munlock(address, length);
if (result != 0)
{
LinuxNative.ThrowLastError("munlock");
}
}
/// <summary>
/// Locks all current and future memory pages for the process.
/// </summary>
/// <param name="flags">Lock flags (MCL_CURRENT, MCL_FUTURE, MCL_ONFAULT)</param>
/// <exception cref="InvalidOperationException">Thrown when the operation fails.</exception>
public static void LockAll(MemoryLockFlags flags)
{
int result = LinuxNative.mlockall((int)flags);
if (result != 0)
{
LinuxNative.ThrowLastError("mlockall");
}
}
/// <summary>
/// Unlocks all memory pages for the process.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the operation fails.</exception>
public static void UnlockAll()
{
int result = LinuxNative.munlockall();
if (result != 0)
{
LinuxNative.ThrowLastError("munlockall");
}
}
/// <summary>
/// Locks a managed array to prevent swapping.
/// </summary>
/// <typeparam name="T">Type of array elements</typeparam>
/// <param name="array">Array to lock</param>
/// <returns>GCHandle that must be kept alive while the memory is locked</returns>
public static GCHandle LockArray<T>(T[] array)
{
GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned);
try
{
IntPtr address = handle.AddrOfPinnedObject();
IntPtr length = new IntPtr(Marshal.SizeOf<T>() * array.Length);
Lock(address, length);
}
catch
{
handle.Free();
throw;
}
return handle;
}
}
/// <summary>
/// Memory locking flags.
/// </summary>
[Flags]
public enum MemoryLockFlags
{
/// <summary>
/// Lock all pages currently mapped into the address space of the process.
/// </summary>
Current = LinuxNative.MCL_CURRENT,
/// <summary>
/// Lock all pages that will become mapped into the address space of the process in the future.
/// </summary>
Future = LinuxNative.MCL_FUTURE,
/// <summary>
/// Lock pages that are currently mapped into the address space of the process and mark all pages
/// that will become mapped into the address space of the process in the future to be locked
/// when they are faulted in.
/// </summary>
OnFault = LinuxNative.MCL_ONFAULT,
}

View File

@@ -0,0 +1,158 @@
# RobotNet10.Realtime
## Overview / Tổng quan
`RobotNet10.Realtime` là một wrapper library cho các tính năng realtime của Linux preempt_rt kernel. Library này cung cấp các API .NET để sử dụng các tính năng realtime như scheduling policies, CPU affinity, memory locking, và high-resolution timers.
## Features / Tính năng
- **Real-time Scheduling**: SCHED_FIFO, SCHED_RR scheduling policies
- **CPU Affinity**: Pin threads to specific CPU cores
- **Memory Locking**: Prevent memory from being swapped to disk
- **High-resolution Timers**: timerfd-based timers for precise timing
- **High-resolution Clocks**: Access to CLOCK_MONOTONIC and other Linux clocks
## Requirements / Yêu cầu
- Linux kernel with preempt_rt patch
- .NET 10.0 runtime
- Root privileges may be required for some operations (scheduling policies, memory locking)
## Usage Examples / Ví dụ Sử dụng
### Real-time Scheduling
```csharp
using RobotNet10.Realtime;
// Set SCHED_FIFO policy with priority 50
RealtimeScheduler.SetSchedulingPolicy(RealtimeSchedulingPolicy.Fifo, 50);
// Get current policy and priority
var policy = RealtimeScheduler.GetSchedulingPolicy();
var priority = RealtimeScheduler.GetPriority();
// Change priority only
RealtimeScheduler.SetPriority(75);
```
### CPU Affinity
```csharp
using RobotNet10.Realtime;
// Pin current thread to CPU 0 and 1
CpuAffinity.SetAffinity(0, 1);
// Or use CpuAffinity class for more control
var affinity = new CpuAffinity();
affinity.AddCpu(0);
affinity.AddCpu(2);
affinity.Apply();
// Get current affinity
var currentAffinity = CpuAffinity.GetAffinity();
var cpus = currentAffinity.GetCpus(); // List of CPU numbers
```
### Memory Locking
```csharp
using RobotNet10.Realtime;
// Lock all current and future memory pages
MemoryLock.LockAll(MemoryLockFlags.Current | MemoryLockFlags.Future);
// Lock a specific array
var array = new byte[1024 * 1024];
var handle = MemoryLock.LockArray(array);
try
{
// Use array...
}
finally
{
handle.Free();
MemoryLock.Unlock(handle.AddrOfPinnedObject(), new IntPtr(array.Length));
}
```
### High-resolution Timer
```csharp
using RobotNet10.Realtime;
// Create a periodic timer
using var timer = new RealtimeTimer(RealtimeClockType.Monotonic);
timer.SetPeriodic(TimeSpan.FromMilliseconds(10)); // 10ms interval
// In a loop, read expirations
while (running)
{
ulong expirations = timer.ReadExpirations();
if (expirations > 0)
{
// Timer expired, do work
DoWork();
}
}
// One-shot timer
timer.SetOneShot(TimeSpan.FromSeconds(5)); // Fire once after 5 seconds
```
### High-resolution Clock
```csharp
using RobotNet10.Realtime;
// Use monotonic clock for measuring elapsed time
var clock = new RealtimeClock(RealtimeClockType.Monotonic);
var startTime = clock.GetTimeSpan();
// Do work...
var elapsed = clock.GetTimeSpan() - startTime;
Console.WriteLine($"Elapsed: {elapsed.TotalMilliseconds} ms");
// Get clock resolution
var resolution = clock.GetResolution();
Console.WriteLine($"Clock resolution: {resolution.TotalNanoseconds} ns");
```
## Important Notes / Lưu Ý Quan trọng
1. **Root Privileges**: Many real-time operations require root privileges. Run your application with `sudo` or set capabilities:
```bash
sudo setcap cap_sys_nice+ep /path/to/your/app
```
2. **Memory Locking Limits**: The system has limits on how much memory can be locked. Check `/proc/sys/vm/max_locked_memory`.
3. **Priority Range**: Real-time priorities range from 1-99. Higher numbers = higher priority.
4. **Platform Specific**: This library only works on Linux. Use `#if` directives or runtime checks for cross-platform code.
5. **Thread Safety**: Most operations affect the current thread only. Use appropriate synchronization for multi-threaded applications.
## Error Handling / Xử lý Lỗi
All operations throw `RealtimeException` on failure:
```csharp
try
{
RealtimeScheduler.SetSchedulingPolicy(RealtimeSchedulingPolicy.Fifo, 50);
}
catch (RealtimeException ex)
{
Console.WriteLine($"Failed: {ex.Message}, errno: {ex.Errno}");
}
```
## Related Documents / Tài liệu Liên quan
- Linux man pages: `man 2 sched_setscheduler`, `man 2 timerfd_create`, etc.
- [Linux RT Wiki](https://rt.wiki.kernel.org/)
- [PREEMPT_RT Patch Documentation](https://wiki.linuxfoundation.org/realtime/documentation)

View File

@@ -0,0 +1,142 @@
using System;
using System.Runtime.InteropServices;
namespace RobotNet10.Realtime;
/// <summary>
/// Provides high-resolution clock access for real-time applications.
/// </summary>
public class RealtimeClock
{
private readonly RealtimeClockType _clockType;
/// <summary>
/// Initializes a new instance of RealtimeClock.
/// </summary>
/// <param name="clockType">Type of clock to use</param>
public RealtimeClock(RealtimeClockType clockType)
{
_clockType = clockType;
}
/// <summary>
/// Gets the current time from the clock.
/// </summary>
/// <returns>Current time</returns>
public DateTime GetTime()
{
var tp = new LinuxNative.timespec();
int result = LinuxNative.clock_gettime((int)_clockType, ref tp);
if (result != 0)
{
LinuxNative.ThrowLastError("clock_gettime");
}
if (_clockType == RealtimeClockType.Realtime)
{
return DateTimeOffset.FromUnixTimeSeconds(tp.tv_sec)
.AddTicks(tp.tv_nsec / 100)
.DateTime;
}
else
{
// For monotonic clocks, return time since boot
return DateTime.UtcNow; // Note: This is approximate for monotonic clocks
}
}
/// <summary>
/// Gets the time as a TimeSpan (for monotonic clocks, represents time since boot).
/// </summary>
/// <returns>TimeSpan representing the clock time</returns>
public TimeSpan GetTimeSpan()
{
var tp = new LinuxNative.timespec();
int result = LinuxNative.clock_gettime((int)_clockType, ref tp);
if (result != 0)
{
LinuxNative.ThrowLastError("clock_gettime");
}
return TimeSpan.FromSeconds(tp.tv_sec) + TimeSpan.FromTicks(tp.tv_nsec / 100);
}
/// <summary>
/// Gets the resolution of the clock.
/// </summary>
/// <returns>Clock resolution</returns>
public TimeSpan GetResolution()
{
var res = new LinuxNative.timespec();
int result = LinuxNative.clock_getres((int)_clockType, ref res);
if (result != 0)
{
LinuxNative.ThrowLastError("clock_getres");
}
return TimeSpan.FromSeconds(res.tv_sec) + TimeSpan.FromTicks(res.tv_nsec / 100);
}
/// <summary>
/// Sets the clock time (only for CLOCK_REALTIME).
/// </summary>
/// <param name="time">Time to set</param>
public void SetTime(DateTime time)
{
if (_clockType != RealtimeClockType.Realtime)
{
throw new InvalidOperationException("Only CLOCK_REALTIME can be set");
}
var tp = new LinuxNative.timespec
{
tv_sec = ((DateTimeOffset)time.ToUniversalTime()).ToUnixTimeSeconds(),
tv_nsec = (time.Millisecond % 1000) * 1000000
};
int result = LinuxNative.clock_settime((int)_clockType, ref tp);
if (result != 0)
{
LinuxNative.ThrowLastError("clock_settime");
}
}
}
/// <summary>
/// Linux clock types.
/// </summary>
public enum RealtimeClockType
{
/// <summary>
/// System-wide real-time clock (wall clock time).
/// Can be set by root.
/// </summary>
Realtime = LinuxNative.CLOCK_REALTIME,
/// <summary>
/// Monotonic clock that cannot be set and represents monotonic time since some unspecified starting point.
/// Best for measuring elapsed time.
/// </summary>
Monotonic = LinuxNative.CLOCK_MONOTONIC,
/// <summary>
/// Monotonic raw clock (not subject to NTP adjustments).
/// </summary>
MonotonicRaw = LinuxNative.CLOCK_MONOTONIC_RAW,
/// <summary>
/// Clock that measures CPU time consumed by the calling process.
/// </summary>
ProcessCpuTime = LinuxNative.CLOCK_PROCESS_CPUTIME_ID,
/// <summary>
/// Clock that measures CPU time consumed by the calling thread.
/// </summary>
ThreadCpuTime = LinuxNative.CLOCK_THREAD_CPUTIME_ID,
/// <summary>
/// Boot time clock (monotonic clock that includes time spent in suspend).
/// </summary>
BootTime = LinuxNative.CLOCK_BOOTTIME,
}

View File

@@ -0,0 +1,37 @@
using System;
namespace RobotNet10.Realtime;
/// <summary>
/// Exception thrown when a real-time operation fails.
/// </summary>
public class RealtimeException : Exception
{
/// <summary>
/// Gets the Linux errno value.
/// </summary>
public int Errno { get; }
/// <summary>
/// Initializes a new instance of RealtimeException.
/// </summary>
/// <param name="message">Error message</param>
/// <param name="errno">Linux errno value</param>
public RealtimeException(string message, int errno) : base(message)
{
Errno = errno;
}
/// <summary>
/// Initializes a new instance of RealtimeException.
/// </summary>
/// <param name="message">Error message</param>
/// <param name="errno">Linux errno value</param>
/// <param name="innerException">Inner exception</param>
public RealtimeException(string message, int errno, Exception innerException)
: base(message, innerException)
{
Errno = errno;
}
}

View File

@@ -0,0 +1,160 @@
using System;
using System.Runtime.InteropServices;
namespace RobotNet10.Realtime;
/// <summary>
/// Provides real-time scheduling policy management for Linux preempt_rt.
/// </summary>
public static class RealtimeScheduler
{
/// <summary>
/// Sets the scheduling policy and priority for the current thread.
/// </summary>
/// <param name="policy">Scheduling policy (SCHED_FIFO, SCHED_RR, etc.)</param>
/// <param name="priority">Priority (1-99 for real-time policies)</param>
/// <exception cref="InvalidOperationException">Thrown when the operation fails.</exception>
public static void SetSchedulingPolicy(RealtimeSchedulingPolicy policy, int priority)
{
/*if (priority < LinuxNative.MIN_PRIORITY || priority > LinuxNative.MAX_PRIORITY)
{
throw new ArgumentOutOfRangeException(nameof(priority),
$"Priority must be between {LinuxNative.MIN_PRIORITY} and {LinuxNative.MAX_PRIORITY}");
}
var param = new LinuxNative.sched_param
{
sched_priority = priority
};
int result = LinuxNative.sched_setscheduler(0, (int)policy, ref param);
if (result != 0)
{
LinuxNative.ThrowLastError("sched_setscheduler");
}*/
}
/// <summary>
/// Gets the current scheduling policy for the current thread.
/// </summary>
/// <returns>The current scheduling policy.</returns>
public static RealtimeSchedulingPolicy GetSchedulingPolicy()
{
int policy = LinuxNative.sched_getscheduler(0);
if (policy < 0)
{
LinuxNative.ThrowLastError("sched_getscheduler");
}
return (RealtimeSchedulingPolicy)policy;
}
/// <summary>
/// Gets the current priority for the current thread.
/// </summary>
/// <returns>The current priority.</returns>
public static int GetPriority()
{
var param = new LinuxNative.sched_param();
int result = LinuxNative.sched_getparam(0, ref param);
if (result != 0)
{
LinuxNative.ThrowLastError("sched_getparam");
}
return param.sched_priority;
}
/// <summary>
/// Sets the priority for the current thread (without changing policy).
/// </summary>
/// <param name="priority">Priority (1-99 for real-time policies)</param>
public static void SetPriority(int priority)
{
if (priority < LinuxNative.MIN_PRIORITY || priority > LinuxNative.MAX_PRIORITY)
{
throw new ArgumentOutOfRangeException(nameof(priority),
$"Priority must be between {LinuxNative.MIN_PRIORITY} and {LinuxNative.MAX_PRIORITY}");
}
var param = new LinuxNative.sched_param
{
sched_priority = priority
};
int result = LinuxNative.sched_setparam(0, ref param);
if (result != 0)
{
LinuxNative.ThrowLastError("sched_setparam");
}
}
/// <summary>
/// Gets the minimum priority for a scheduling policy.
/// </summary>
/// <param name="policy">Scheduling policy</param>
/// <returns>Minimum priority</returns>
public static int GetMinPriority(RealtimeSchedulingPolicy policy)
{
return LinuxNative.sched_get_priority_min((int)policy);
}
/// <summary>
/// Gets the maximum priority for a scheduling policy.
/// </summary>
/// <param name="policy">Scheduling policy</param>
/// <returns>Maximum priority</returns>
public static int GetMaxPriority(RealtimeSchedulingPolicy policy)
{
return LinuxNative.sched_get_priority_max((int)policy);
}
/// <summary>
/// Yields the CPU to allow other threads to run.
/// </summary>
public static void Yield()
{
int result = LinuxNative.sched_yield();
if (result != 0)
{
LinuxNative.ThrowLastError("sched_yield");
}
}
}
/// <summary>
/// Linux real-time scheduling policies.
/// </summary>
public enum RealtimeSchedulingPolicy
{
/// <summary>
/// Normal scheduling policy (default).
/// </summary>
Normal = LinuxNative.SCHED_NORMAL,
/// <summary>
/// First-In-First-Out real-time scheduling policy.
/// Threads with higher priority always run before threads with lower priority.
/// </summary>
Fifo = LinuxNative.SCHED_FIFO,
/// <summary>
/// Round-Robin real-time scheduling policy.
/// Similar to SCHED_FIFO but threads are time-sliced.
/// </summary>
RoundRobin = LinuxNative.SCHED_RR,
/// <summary>
/// Batch scheduling policy (for batch jobs).
/// </summary>
Batch = LinuxNative.SCHED_BATCH,
/// <summary>
/// Idle scheduling policy (lowest priority).
/// </summary>
Idle = LinuxNative.SCHED_IDLE,
/// <summary>
/// Deadline scheduling policy (for deadline-based scheduling).
/// </summary>
Deadline = LinuxNative.SCHED_DEADLINE,
}

View File

@@ -0,0 +1,239 @@
using System;
using System.Runtime.InteropServices;
namespace RobotNet10.Realtime;
/// <summary>
/// Provides high-resolution real-time timers using Linux timerfd.
/// More accurate than System.Threading.Timer for real-time applications.
/// </summary>
public class RealtimeTimer : IDisposable
{
private int _fd = -1;
private readonly RealtimeClock _clock;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of RealtimeTimer.
/// </summary>
/// <param name="clock">Clock type to use (default: CLOCK_MONOTONIC)</param>
/// <param name="nonBlocking">Whether the timer should be non-blocking</param>
public RealtimeTimer(RealtimeClockType clock = RealtimeClockType.Monotonic, bool nonBlocking = false)
{
_clock = new RealtimeClock(clock);
int flags = LinuxNative.TFD_CLOEXEC;
if (nonBlocking)
{
flags |= LinuxNative.TFD_NONBLOCK;
}
_fd = LinuxNative.timerfd_create((int)clock, flags);
if (_fd < 0)
{
LinuxNative.ThrowLastError("timerfd_create");
}
}
/// <summary>
/// Gets the file descriptor for the timer.
/// </summary>
public int FileDescriptor => _fd;
/// <summary>
/// Sets the timer to fire periodically.
/// </summary>
/// <param name="interval">Interval between timer expirations</param>
public void SetPeriodic(TimeSpan interval)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RealtimeTimer));
}
var spec = new LinuxNative.itimerspec
{
it_interval = TimeSpanToTimespec(interval),
it_value = TimeSpanToTimespec(interval) // First expiration
};
var oldSpec = new LinuxNative.itimerspec();
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
if (result != 0)
{
LinuxNative.ThrowLastError("timerfd_settime");
}
}
/// <summary>
/// Sets the timer to fire once after a delay.
/// </summary>
/// <param name="delay">Delay before timer expiration</param>
public void SetOneShot(TimeSpan delay)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RealtimeTimer));
}
var spec = new LinuxNative.itimerspec
{
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 }, // No repeat
it_value = TimeSpanToTimespec(delay)
};
var oldSpec = new LinuxNative.itimerspec();
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
if (result != 0)
{
LinuxNative.ThrowLastError("timerfd_settime");
}
}
/// <summary>
/// Sets the timer to fire at an absolute time.
/// </summary>
/// <param name="absoluteTime">Absolute time when timer should expire</param>
public void SetAbsolute(DateTime absoluteTime)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RealtimeTimer));
}
var clockTime = _clock.GetTime();
var targetTime = absoluteTime.ToUniversalTime();
var delay = targetTime - DateTime.UtcNow;
var spec = new LinuxNative.itimerspec
{
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 },
it_value = TimeSpanToTimespec(delay)
};
var oldSpec = new LinuxNative.itimerspec();
int result = LinuxNative.timerfd_settime(_fd, LinuxNative.TFD_TIMER_ABSTIME, ref spec, ref oldSpec);
if (result != 0)
{
LinuxNative.ThrowLastError("timerfd_settime");
}
}
/// <summary>
/// Disarms the timer.
/// </summary>
public void Disarm()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RealtimeTimer));
}
var spec = new LinuxNative.itimerspec
{
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 },
it_value = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 }
};
var oldSpec = new LinuxNative.itimerspec();
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
if (result != 0)
{
LinuxNative.ThrowLastError("timerfd_settime");
}
}
/// <summary>
/// Gets the current timer value.
/// </summary>
/// <returns>Current timer interval and value</returns>
public (TimeSpan interval, TimeSpan value) GetTime()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RealtimeTimer));
}
var spec = new LinuxNative.itimerspec();
int result = LinuxNative.timerfd_gettime(_fd, ref spec);
if (result != 0)
{
LinuxNative.ThrowLastError("timerfd_gettime");
}
return (TimespecToTimeSpan(spec.it_interval), TimespecToTimeSpan(spec.it_value));
}
/// <summary>
/// Reads the timer expiration count (number of times timer has expired since last read).
/// In non-blocking mode, returns 0 if timer hasn't expired yet (EAGAIN).
/// </summary>
/// <returns>Number of expirations, or 0 if timer hasn't expired (non-blocking mode)</returns>
public ulong ReadExpirations()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RealtimeTimer));
}
IntPtr bufferPtr = Marshal.AllocHGlobal(8);
try
{
long bytesRead = LinuxNative.read(_fd, bufferPtr, new IntPtr(8));
if (bytesRead < 0)
{
int errno = LinuxNative.GetLastError();
if (errno == 11) // EAGAIN - timer hasn't expired yet (non-blocking mode)
{
return 0;
}
LinuxNative.ThrowLastError("read");
}
if (bytesRead != 8)
{
throw new InvalidOperationException($"Expected to read 8 bytes, got {bytesRead}");
}
return (ulong)Marshal.ReadInt64(bufferPtr);
}
finally
{
Marshal.FreeHGlobal(bufferPtr);
}
}
private static LinuxNative.timespec TimeSpanToTimespec(TimeSpan ts)
{
long totalSeconds = (long)ts.TotalSeconds;
long nanoseconds = (long)((ts.TotalMilliseconds % 1000) * 1000000);
return new LinuxNative.timespec
{
tv_sec = totalSeconds,
tv_nsec = nanoseconds
};
}
private static TimeSpan TimespecToTimeSpan(LinuxNative.timespec ts)
{
return TimeSpan.FromSeconds(ts.tv_sec) + TimeSpan.FromTicks(ts.tv_nsec / 100);
}
public void Dispose()
{
if (!_disposed && _fd >= 0)
{
LinuxNative.close(_fd);
_fd = -1;
_disposed = true;
}
}
}
// Additional P/Invoke declarations needed for timerfd
internal static partial class LinuxNative
{
[LibraryImport("libc", SetLastError = true)]
public static partial long read(int fd, IntPtr buf, IntPtr count);
[LibraryImport("libc", SetLastError = true)]
public static partial int close(int fd);
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>CS8981</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>