You really have to define 'good enough'. E.g. Can handle 1000 Hz interrupt rate, and the time from firing of the interrupt timer to the time your first instruction executes is xxx microseconds. (this is important if you are doing e.g. velocity estimates from position information -- you need accurate 'dt'; therefore this time should be minimized. There are other ways to deal with jitter, but minimizing this is best.)
I turned BSD 4.3 unix into RT on a VAX 780 by using an external KW-11P clock on highest priority interrupt, have the RT code run in the kernel, and otherwise, timesharing was still going while we were running the robot -- although it was pretty sloooow. There was a big memory pool in kernel space that stored robot variables, so when the robot fell over, we would press a button which would 'freeze' the circular buffer, and then the user-space code would do an ioctl() to receive a copy of that memory pool (it was too hard to share the same pool between kernel / user space -- or maybe I was too lazy, and the solution found was 'good enough' ;-) )
>Is RT Linux a good (or at least a good enough) option for realtime OS's
Soft realtime, yes. It does keep scheduling latency under control, enabling e.g. pro audio with low latency (very small buffer). This is unlike without PREEMPT_RT, where with buffers under 20ms XRUNs happen frequently.
Hard realtime, no. It can't guarantee a thing, as the kernel is complicated (has millions of lines of code) and thus unpredictable.
Look at seL4 instead if your deadlines are hard. It has been designed for realtime, and comes with formal proofs of worst case execution time.
I turned BSD 4.3 unix into RT on a VAX 780 by using an external KW-11P clock on highest priority interrupt, have the RT code run in the kernel, and otherwise, timesharing was still going while we were running the robot -- although it was pretty sloooow. There was a big memory pool in kernel space that stored robot variables, so when the robot fell over, we would press a button which would 'freeze' the circular buffer, and then the user-space code would do an ioctl() to receive a copy of that memory pool (it was too hard to share the same pool between kernel / user space -- or maybe I was too lazy, and the solution found was 'good enough' ;-) )
You can see this in action here: https://youtu.be/mG_ZKXo6Rlg?t=34 yes, I'm sitting 'driving' the bot.