Real-Time Scheduling SCHED_FIFO and SCHED_RR
Linux implements the two POSIX real-time scheduling policies —
SCHED_FIFOandSCHED_RR— as a single scheduling class,rt_sched_class, that sits above the fair (SCHED_OTHER) class in the kernel’s strict class hierarchy. A runnable real-time task therefore always preempts any ordinary task, no matter how “important” the fair task believes it is. Real-time tasks carry a static priority from 1 (low) to 99 (high) (sched(7)); the scheduler always runs the highest-priority runnable real-time task, and only chooses among tasks of equal priority using the policy:SCHED_FIFOlets a task run until it blocks, yields, or is preempted by something of higher priority (no time slice at all), whereasSCHED_RRrotates among equal-priority tasks everysched_rr_timeslice(100 ms by default on a 1000 Hz kernel). Because nothing below the real-time class runs while an RT task is runnable, a runaway, non-blocking RT task can monopolise a CPU forever — which is precisely why Linux ships with RT throttling enabled by default. Verified against Linux 6.12 LTS source (kernel/sched/rt.c,include/linux/sched/prio.h) as of 2026-06-03.
These are fixed-priority policies, the classical real-time model: the programmer assigns priorities, and the scheduler mechanically honours them. There is no notion of fairness, lag, or virtual runtime here — those belong to EEVDF. For deadline-driven work where you can state a (runtime, period, deadline) triple and want the kernel to compute schedulability, see SCHED_DEADLINE, which sits above the real-time class.
Mental Model
Think of the real-time class as 99 numbered shelves stacked from 1 at the bottom to 99 at the top. Each shelf holds a FIFO queue of runnable tasks at that priority. To pick the next task, the scheduler scans from the top shelf downward and takes the first task on the first non-empty shelf. A task only ever runs if every shelf above it is empty. SCHED_FIFO and SCHED_RR tasks share the same shelves and the same selection rule — they differ only in when a running task gives up its shelf position: a SCHED_FIFO task holds its spot until it voluntarily blocks or is preempted; a SCHED_RR task is sent to the back of its own shelf when its time quantum expires, letting an equal-priority sibling run.
flowchart TD PICK["pick_next_task_rt()<br/>on this CPU's rt_rq"] --> SCAN["sched_find_first_bit(array.bitmap)<br/>find highest set priority"] SCAN --> SHELF99["prio 99 queue<br/>(FIFO list)"] SCAN --> SHELF50["prio 50 queue"] SCAN --> SHELF1["prio 1 queue"] SHELF99 -->|non-empty wins| RUN["run head of highest<br/>non-empty queue"] SHELF50 -.->|only if 99 empty| RUN SHELF1 -.->|only if all above empty| RUN RUN --> FIFO{"policy?"} FIFO -->|SCHED_FIFO| HOLD["runs until block / yield /<br/>higher-prio preempt"] FIFO -->|SCHED_RR| TICK["on tick: decrement time_slice;<br/>at 0 → requeue at tail,<br/>reset to sched_rr_timeslice"]
The real-time run queue and its selection rule. What it shows: a per-priority array of FIFO queues with a companion bitmap; sched_find_first_bit reads the bitmap to locate the highest populated priority in O(1), and the head of that queue runs. The insight to take: priority is absolute — priority 50 never runs while priority 51 has a runnable task — and the only difference between the two policies is the bottom of the diagram: SCHED_FIFO keeps the shelf position indefinitely, SCHED_RR rotates within a shelf on a tick-driven quantum.
How the Real-Time Class Works
The per-CPU rt_rq and the priority array
Each CPU’s run queue (struct rq) embeds an rt_rq. The heart of the rt_rq is a struct rt_prio_array named active, defined in kernel/sched/sched.h (Linux 6.12) as:
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};MAX_RT_PRIO is 100 (include/linux/sched/prio.h, Linux 6.12), so there are 100 FIFO queues — one per internal real-time priority level 0…99 — and a bitmap with one bit per queue plus a trailing “delimiter” bit. When a real-time task becomes runnable it is appended to queue[prio] and the corresponding bit is set; when the queue empties the bit is cleared. Selecting the next task is then just sched_find_first_bit(array->bitmap) — an O(1) scan for the lowest set bit (which, because of the priority inversion in the internal numbering described below, is the highest user-visible priority) — followed by taking the head of that queue (rt.c, Linux 6.12). This is what makes fixed-priority scheduling constant-time regardless of how many real-time tasks are runnable, a property classical Unix schedulers lacked.
init_rt_rq() initialises every queue empty, clears all bits, and then sets the delimiter bit at index MAX_RT_PRIO so that sched_find_first_bit always terminates even when no real-time task is runnable; it also seeds highest_prio.curr = MAX_RT_PRIO-1 (i.e. 99, the lowest-priority sentinel) (rt.c init_rt_rq, Linux 6.12).
Internal vs. user-visible priority numbering — a deliberate inversion
There are two priority number spaces and they run in opposite directions, which is a perennial source of confusion. The userspace API (sched_priority in struct sched_param) uses 1 = lowest, 99 = highest, as documented in sched(7): “Processes scheduled under one of the real-time policies … have a sched_priority value in the range 1 (low) to 99 (high)” (sched(7)). Internally the kernel stores a task_struct.prio where smaller numbers are more urgent so that the same comparison logic works across all classes. The conversion lives in __normal_prio() (kernel/sched/syscalls.c, Linux 6.12):
prio = MAX_RT_PRIO - 1 - rt_prio; /* 100 - 1 - rt_priority */So a user priority of 99 becomes internal prio = 0 (most urgent), and user priority 1 becomes internal prio = 98. The fair (SCHED_OTHER) tasks occupy internal priorities MAX_RT_PRIO…MAX_PRIO-1, i.e. 100…139 — strictly numerically larger, hence strictly less urgent, than every real-time priority. That is the entire mechanism by which “real-time always beats fair”: it falls straight out of the number line (prio.h, Linux 6.12). The sched_get_priority_min(2)/sched_get_priority_max(2) calls return 1 and 99 for both SCHED_FIFO and SCHED_RR (sched_get_priority_max(2)).
SCHED_FIFO: run until you stop yourself
SCHED_FIFO is, in the words of sched(7), “a simple scheduling algorithm without time slicing.” A SCHED_FIFO thread “runs until either it is blocked by an I/O request, it is preempted by a higher priority thread, or it calls sched_yield(2)” (sched(7)). There is no quantum and no preemption by equal-priority peers — a SCHED_FIFO task at priority 50 will run to the exclusion of every other priority-50 task until it blocks. The list discipline is precisely specified:
- When a
SCHED_FIFOtask is preempted by a higher-priority task, “it will stay at the head of the list for its priority and will resume execution as soon as all threads of higher priority are blocked again” (sched(7)). It does not lose its place. - When a blocked
SCHED_FIFOtask becomes runnable again, “it will be inserted at the end of the list for its priority” (sched(7)). - When a running task’s priority is changed via
sched_setscheduler(2)/sched_setparam(2): raising it places the task “at the end of the list for its new priority”; an unchanged value leaves “its position in the run list … unchanged”; lowering it places it “at the front of the list for its new priority” (sched(7)). The asymmetry (front on lower, back on higher) exists so a task that raises its own priority cannot use the change to leapfrog peers it just joined.
sched_yield(2) under SCHED_FIFO moves the caller to the tail of its priority list, giving equal-priority siblings a turn — the one cooperative knob a SCHED_FIFO programmer has.
SCHED_RR: round-robin within a priority
SCHED_RR is “a simple enhancement of SCHED_FIFO” — identical in every respect except that “each thread is allowed to run only for a maximum time quantum” (sched(7)). When a SCHED_RR task exhausts its quantum it is moved to the tail of its priority’s queue and the next equal-priority task runs; tasks of different priority are still strictly ordered. The default quantum is the kernel variable sched_rr_timeslice, initialised from the macro RR_TIMESLICE in include/linux/sched/rt.h (Linux 6.12):
#define RR_TIMESLICE (100 * HZ / 1000) /* 100ms */With the common server tick rate HZ = 1000 this evaluates to 100 jiffies, i.e. 100 ms; with HZ = 250 it is 25 jiffies, still 100 ms of wall-clock. The value is tunable at runtime through /proc/sys/kernel/sched_rr_timeslice_ms (exposed via the sysctl_sched_rr_timeslice table in rt.c). Userspace can query the quantum for a specific task with sched_rr_get_interval(2), which “writes into the timespec structure … the round-robin time quantum for the process” (sched_rr_get_interval(2)).
Mechanically, the per-tick accounting happens in task_tick_rt() (rt.c): for a SCHED_RR task it decrements p->rt.time_slice; while the slice remains positive nothing happens, but on reaching zero it resets time_slice back to sched_rr_timeslice and, only if the task has a queue-mate at the same priority, requeues it at the tail and sets the reschedule flag. A lone SCHED_RR task at its priority therefore behaves exactly like SCHED_FIFO — there is no one to round-robin with, so the quantum expiry is a no-op beyond the reset.
Configuring Real-Time Tasks
From C: sched_setscheduler(2)
#include <sched.h>
#include <string.h>
struct sched_param param;
memset(¶m, 0, sizeof(param));
param.sched_priority = 80; /* user priority 1..99 */
/* Switch the calling thread (pid 0 == self) to SCHED_FIFO at priority 80. */
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1)
perror("sched_setscheduler"); /* EPERM if unprivileged */Line by line: sched_priority is the only meaningful field of struct sched_param, and for the RT policies it must be in 1…99 (the kernel rejects out-of-range values with EINVAL; in __sched_setscheduler the check is attr->sched_priority > MAX_RT_PRIO-1, and it also enforces that a non-zero priority is given iff the policy is a real-time policy — rt_policy(policy) != (attr->sched_priority != 0) (syscalls.c, Linux 6.12)). Passing pid == 0 targets the caller. The call fails with EPERM unless the caller has CAP_SYS_NICE or the requested priority is within the thread’s RLIMIT_RTPRIO soft limit. To make the policy change not survive fork() or execve()-style resets in newer code, the SCHED_RESET_ON_FORK flag can be OR-ed into the policy argument.
From the shell: chrt(1)
# Run a command as SCHED_FIFO priority 80
$ chrt --fifo 80 ./latency_sensitive_app
# Run as SCHED_RR priority 10
$ chrt --rr 10 ./worker
# Inspect an existing thread's policy and priority
$ chrt -p 1234
pid 1234's current scheduling policy: SCHED_FIFO
pid 1234's current scheduling priority: 80
# Change a running thread back to normal (SCHED_OTHER, priority must be 0)
$ chrt --other -p 0 1234chrt(1) is the util-linux front-end to sched_setscheduler(2) (chrt(1)). chrt -m prints the valid priority range for each policy on the running kernel (1–99 for SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER/SCHED_BATCH/SCHED_IDLE). Note that SCHED_OTHER must be given priority 0 — supplying any other value is the classic chrt EINVAL.
Privilege and the RLIMIT_RTPRIO ceiling
Since Linux 2.6.12, an unprivileged thread can set real-time policies only up to its RLIMIT_RTPRIO soft limit: “the RLIMIT_RTPRIO resource limit defines a ceiling on an unprivileged thread’s static priority for the SCHED_RR and SCHED_FIFO policies” (sched(7)). A thread with CAP_SYS_NICE bypasses the limit entirely. This is why audio daemons (PipeWire, JACK) ship /etc/security/limits.d rules granting members of an audio/realtime group a non-zero rtprio — without it, the daemon’s request for SCHED_FIFO priority returns EPERM.
Failure Modes
A runaway RT task hangs the machine
The defining hazard of fixed-priority real-time scheduling is that, because nothing in a lower class runs while an RT task is runnable, a single non-blocking RT task starves everything below it. sched(7) states it plainly: “A nonblocking infinite loop in a thread scheduled under the SCHED_FIFO, SCHED_RR, or SCHED_DEADLINE policy can potentially block all other threads from accessing the CPU forever” (sched(7)). On a single-CPU system this means the shell you would use to kill the offending task never gets to run; on SMP it pins one CPU and, if the task migrates or there are several such tasks, can take the whole machine down. Linux’s defence is RT throttling — by default sched_rt_runtime_us = 950000 of every sched_rt_period_us = 1000000, reserving 5% of each period for non-RT tasks “so that a run-away real-time tasks will not lock up the machine but leave a little time to recover it” (sched-rt-group, Linux 6.12). The mechanism, its caveats, and how to disable it are covered in Real-Time Throttling and the RT Bandwidth Limit.
Priority inversion: an RT task blocked by a lower-priority lock holder
A higher-priority RT task that needs a lock held by a lower-priority task is, transitively, at the mercy of whatever can preempt that holder — a medium-priority task can then keep the holder (and thus the high-priority waiter) off the CPU indefinitely. This is the priority inversion problem, the bug that famously reset the Mars Pathfinder lander. Linux’s answer is priority inheritance via rt_mutex and PI-futexes; the full story, including the inheritance algorithm, is in Real-Time Priorities and Priority Inversion.
Mis-set priority and CPU pinning interactions
Two subtler traps: (1) A SCHED_FIFO task spinning at priority 99 can starve per-CPU kernel threads (kworker, RCU callbacks, the migration thread) that themselves run as RT — leading to RCU stalls and watchdog warnings rather than a clean hang. Best practice is to leave headroom (run application RT work in the middle of the range, e.g. 1–50) and to pin RT work to dedicated cores (see CPU Isolation isolcpus and nohz_full). (2) Because real-time tasks ignore nice values entirely, renice has no effect on them — a common surprise when an operator tries to tame a runaway RT process with nice.
Alternatives and When to Choose Them
SCHED_OTHER(EEVDF) — the default for almost everything. Choose it unless you have a genuine bounded-latency requirement; fixed-priority RT is a sharp tool that trades system robustness for determinism. See The EEVDF Scheduler.SCHED_RRvs.SCHED_FIFO— preferSCHED_RRwhen several tasks legitimately share one priority and you want time-sharing among them; preferSCHED_FIFOwhen a single task per priority should run to completion. If you only ever have one task per priority, the two are indistinguishable.SCHED_DEADLINE— choose when you can characterise work as (runtime every period, by this deadline) and want the kernel to admission-control the set so it provably meets deadlines. UnlikeSCHED_FIFO/RR,SCHED_DEADLINEis not fixed-priority and refuses over-subscription. See SCHED_DEADLINE and Earliest Deadline First and The Constant Bandwidth Server and Admission Control.PREEMPT_RTkernel — orthogonal to policy choice: it makes the kernel itself preemptible so that even a high-priority RT task is not blocked by long non-preemptible kernel sections. You still useSCHED_FIFO/RR;PREEMPT_RTlowers the worst-case latency before your RT task runs. See The PREEMPT_RT Real-Time Kernel.
Production Notes
Real-world RT deployments treat priority as a scarce, carefully budgeted resource. Audio servers (JACK, PipeWire) run their processing graph as SCHED_FIFO at a modest priority and rely on PI-futex-backed pthread_mutex_t with PTHREAD_PRIO_INHERIT to avoid inversion when the real-time thread takes a lock shared with a non-RT setup thread. Industrial and robotics stacks (e.g. ROS 2 real-time configurations) combine SCHED_FIFO with mlockall(2) to lock pages (a page fault in an RT task is an unbounded delay) and CPU isolation to keep housekeeping work off the RT cores. The kernel’s own machinery — the migration/N stop-class thread sits above even RT, and several per-CPU kthreads run as SCHED_FIFO priority 1 — which is why a careless application grabbing priority 99 across all CPUs can deadlock the scheduler’s internals and not merely its own peers. The standard diagnostic when an RT workload “freezes” the box is to check whether throttling fired (/proc/sched_debug, or simply that the box recovered after ~50 ms per second), and whether mlockall and core isolation were actually applied.
See Also
- Real-Time Priorities and Priority Inversion — the inversion problem and priority inheritance (the necessary companion to using these policies safely)
- Real-Time Throttling and the RT Bandwidth Limit — the safety valve that keeps a runaway RT task from killing the box
- SCHED_DEADLINE and Earliest Deadline First — the deadline-driven RT policy that sits above this class
- Scheduling Classes and the sched_class Interface — the strict-priority class stack that puts
rt_sched_classabove fair - Per-CPU Run Queues and struct rq — the
rqthat embeds thert_rq - Nice Values Weights and Priority Scaling — why
nicedoes nothing to RT tasks - The PREEMPT_RT Real-Time Kernel — making the kernel itself preemptible for bounded latency
- Futex and OS Synchronization Primitives — the userspace locking primitive whose PI variant backs
PTHREAD_PRIO_INHERIT - Linux Process Scheduling MOC — parent map