Initial commit
This commit is contained in:
154
srcs/RobotNet10/Commons/RobotNet10.Realtime/CpuAffinity.cs
Normal file
154
srcs/RobotNet10/Commons/RobotNet10.Realtime/CpuAffinity.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
228
srcs/RobotNet10/Commons/RobotNet10.Realtime/LinuxNative.cs
Normal file
228
srcs/RobotNet10/Commons/RobotNet10.Realtime/LinuxNative.cs
Normal 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);
|
||||
}
|
||||
|
||||
116
srcs/RobotNet10/Commons/RobotNet10.Realtime/MemoryLock.cs
Normal file
116
srcs/RobotNet10/Commons/RobotNet10.Realtime/MemoryLock.cs
Normal 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,
|
||||
}
|
||||
|
||||
158
srcs/RobotNet10/Commons/RobotNet10.Realtime/README.md
Normal file
158
srcs/RobotNet10/Commons/RobotNet10.Realtime/README.md
Normal 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)
|
||||
|
||||
142
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeClock.cs
Normal file
142
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeClock.cs
Normal 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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
160
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeScheduler.cs
Normal file
160
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeScheduler.cs
Normal 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,
|
||||
}
|
||||
|
||||
239
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeTimer.cs
Normal file
239
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeTimer.cs
Normal 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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user