How Do Linux Scheduling Policies Work? – Best Linux Device Drivers Course

Linux Scheduling Policies Explained: SCHED_NORMAL, SCHED_FIFO, SCHED_RR and More

Part of our free Linux kernel development course — understand how the kernel decides which thread runs next

Level
Beginner Friendly
Kernel
6.x / 7.x (Latest)
Cost
100% Free

Welcome to another lecture in our free Linux kernel development course at EmbeddedPathashala. In this lesson we study Linux scheduling policies — the rules the kernel uses to decide which thread gets the CPU, for how long, and who can interrupt whom. If you are preparing for embedded Linux interviews or following any free Linux device drivers course, scheduling policies appear again and again, because drivers, kernel threads, and user applications all live under the scheduler’s control.

Everything in this lecture is verified against modern kernels (6.6 and later, including the 7.x series), so you are learning the current behavior, not outdated information from old books.

What You Will Learn

What a scheduling policy is
SCHED_NORMAL (SCHED_OTHER)
SCHED_FIFO vs SCHED_RR
SCHED_BATCH and SCHED_IDLE
SCHED_DEADLINE
Nice values vs real-time priority
chrt and nice commands
Interview Q&A

Prerequisites

You only need two things to follow this lecture from our free Linux kernel development course:

  • Basic knowledge of processes and threads on Linux (covered earlier in this free embedded systems course).
  • Any Linux machine or virtual machine where you can run terminal commands.

What Is a Scheduling Policy?

On Linux, the unit of scheduling is the thread (the kernel calls it a task). At any moment, many threads are runnable but only a limited number of CPU cores exist. A scheduling policy is the rulebook attached to each thread that tells the kernel two things:

  • Selection rule: among all runnable threads, who should run next?
  • Preemption rule: when should the currently running thread be forced to give up the CPU?

Every thread on your system carries exactly one policy at a time. The policy plus a priority number fully describes how aggressively that thread competes for CPU time. This is why Linux scheduling policies matter so much in embedded systems: choosing the wrong policy for a motor-control thread or an audio thread directly causes missed deadlines and glitches.

The Two Worlds: Normal vs Real-Time Policies

Think of Linux scheduling policies as two separate worlds that never mix fairly. Real-time threads always win against normal threads. Inside each world, different rules apply.

Linux Scheduling Policies — Priority Worlds
HIGHEST: SCHED_DEADLINE (deadline world — beats everything below)
REAL-TIME WORLD: SCHED_FIFO and SCHED_RR (RT priority 1–99)
NORMAL WORLD: SCHED_NORMAL and SCHED_BATCH (nice value −20 to +19)
LOWEST: SCHED_IDLE (runs only when nothing else wants the CPU)

A key rule to memorize: a normal thread can never preempt a runnable real-time thread. Real-time here means soft real-time. Mainline Linux is a general-purpose OS, not a hard RTOS — although with the PREEMPT_RT support that was merged into the mainline kernel (from version 6.12 onward you can enable it as a standard config option on major architectures), Linux gets very close to deterministic hard real-time behavior.

SCHED_NORMAL: The Default Policy for Everything

SCHED_NORMAL (called SCHED_OTHER in POSIX and userspace headers) is the policy of nearly every thread on your machine: your shell, browser, compilers, and most daemons. Its design goal is fairness — every thread should receive a share of CPU time proportional to its weight.

The priority knob in this world is the nice value, ranging from −20 (strongest) to +19 (weakest), with 0 as default. Each nice step changes the CPU share by roughly 10%, which is a significant amount.

What changed in modern kernels: older books describe CFS (Completely Fair Scheduler) as the engine behind SCHED_NORMAL. Since kernel 6.6, the fair class engine is EEVDF (Earliest Eligible Virtual Deadline First). Your programs and the nice value semantics stay the same — only the internal algorithm changed. We cover EEVDF in detail in the next lecture of this free Linux kernel development course.

# Start a build with lower priority (nice +10)
nice -n 10 make -j8

# Change priority of a running process (PID 4321) to nice +5
renice -n 5 -p 4321

# Only root can boost priority below 0
sudo renice -n -5 -p 4321

SCHED_FIFO: The Aggressive Real-Time Policy

SCHED_FIFO (First In, First Out) is the most aggressive soft real-time policy. A SCHED_FIFO thread has, in effect, an unlimited timeslice. Once it starts running, it keeps the CPU until one of these events happens:

  • It voluntarily blocks (waits for I/O, sleeps, waits on a lock).
  • It calls sched_yield(), stops, or exits.
  • A higher-priority real-time thread becomes runnable and preempts it.

Note carefully: another SCHED_FIFO thread at the same priority cannot preempt it. Hardware and software interrupts, however, always remain superior and will interrupt even the highest-priority SCHED_FIFO thread.

A classic thought experiment: on a single-CPU system, a SCHED_FIFO priority-99 thread stuck in an infinite busy loop will effectively freeze the whole machine, because no normal thread — including your shell — can ever run again. This is why real-time privileges require root or the CAP_SYS_NICE capability.

// Set the calling thread to SCHED_FIFO, priority 50
#include <sched.h>
#include <stdio.h>

int main(void)
{
    struct sched_param sp = { .sched_priority = 50 };

    if (sched_setscheduler(0, SCHED_FIFO, &sp) == -1) {
        perror("sched_setscheduler");
        return 1;
    }
    printf("Now running as SCHED_FIFO prio 50\n");
    /* time-critical work here */
    return 0;
}

SCHED_RR: Round Robin for Fair Real-Time Sharing

SCHED_RR (Round Robin) is a moderately aggressive real-time policy. It follows exactly the same rules as SCHED_FIFO, with one extra exit condition: each thread receives a finite timeslice (100 ms by default on Linux, tunable via /proc/sys/kernel/sched_rr_timeslice_ms). When the timeslice expires, the thread moves to the back of the queue for its priority level, and the next thread at the same priority runs.

So the practical difference: if you have three real-time threads at the same priority doing CPU-heavy work, choose SCHED_RR so they rotate. With SCHED_FIFO, the first one would monopolize the CPU.

# Check the current round-robin timeslice (milliseconds)
cat /proc/sys/kernel/sched_rr_timeslice_ms

# Launch a program under SCHED_RR at priority 30
sudo chrt --rr 30 ./my_rt_app

# Inspect the policy and priority of a running thread
chrt -p 4321

SCHED_BATCH and SCHED_IDLE: The Background Policies

SCHED_BATCH is designed for non-interactive batch jobs like nightly compilation, video encoding, or data crunching. It lives in the normal (nice value) world but the scheduler assumes the thread does not care about latency, so it preempts other threads less often and gets slightly longer runs, improving cache behavior and throughput.

SCHED_IDLE is the floor of the whole system: an idle-policy thread runs only when no other thread wants the CPU. Its effective priority sits even below nice +19. It is perfect for tasks like search indexers or telemetry uploaders that should never disturb real work.

# Run a heavy backup job that should never disturb the system
chrt --idle 0 tar czf /backup/home.tar.gz /home

# Run an encode job as a batch task
chrt --batch 0 ffmpeg -i input.mp4 output.mkv

Do not confuse the SCHED_IDLE policy with the per-CPU idle task (PID 0, historically called the swapper). The idle task is a special kernel thread that runs when a CPU truly has nothing to do; SCHED_IDLE is a policy you can assign to your own low-importance threads.

SCHED_DEADLINE: The Modern Top of the Hierarchy

Older material often stops at FIFO/RR, but modern kernels include SCHED_DEADLINE, which outranks all FIFO and RR threads. Instead of a priority number, you declare three parameters — runtime, deadline, and period — for example: “my thread needs 3 ms of CPU within every 10 ms window.” The kernel then uses Earliest Deadline First (EDF) plus an admission test: if the CPU cannot guarantee your reservation, the request is rejected upfront. This is extremely useful for periodic embedded workloads such as sensor sampling loops.

# Run a periodic task: 3ms runtime, 10ms deadline, 10ms period
# (values are in nanoseconds)
sudo chrt --deadline \
     --sched-runtime  3000000 \
     --sched-deadline 10000000 \
     --sched-period   10000000 \
     0 ./sensor_loop

Comparison Table of All Linux Scheduling Policies

Linux Scheduling Policies at a Glance (Kernel 6.x / 7.x)
Policy World Priority Knob Timeslice Typical Use
SCHED_DEADLINE Deadline (highest) runtime / deadline / period Reserved budget Periodic control loops
SCHED_FIFO Real-time RT prio 1–99 (99 highest) Effectively infinite Latency-critical audio, motor control
SCHED_RR Real-time RT prio 1–99 (99 highest) Finite (default 100 ms) Multiple equal-priority RT workers
SCHED_NORMAL Normal (fair) Nice −20 to +19 Dynamic (EEVDF virtual deadline) Everything by default
SCHED_BATCH Normal (fair) Nice −20 to +19 Longer runs, less preemption Compiles, encoding, batch jobs
SCHED_IDLE Lowest of all Below nice +19 Runs only when CPU is free Indexers, scavenger tasks

The Priority Scale: Nice Values vs Real-Time Priority

Two separate numbering systems exist, and beginners often mix them up:

  • Nice value (normal world): −20 to +19. Lower is stronger. Non-real-time threads always have real-time priority 0, so they can never compete with any RT thread.
  • Real-time priority (RT world): 1 to 99. Higher is stronger. Used by SCHED_FIFO and SCHED_RR.

Yes, the directions are opposite — this is a favorite interview trap. Also remember that internally the kernel stores a single unified priority in the task structure, but for userspace the two scales above are what you specify.

# See policy (CLS) and priorities of everything on the system
ps -eo pid,tid,cls,rtprio,ni,comm | head -20

# CLS column meaning:
#  TS  = SCHED_NORMAL (time sharing)
#  FF  = SCHED_FIFO
#  RR  = SCHED_RR
#  B   = SCHED_BATCH
#  IDL = SCHED_IDLE
#  DLN = SCHED_DEADLINE

Common Mistakes and Troubleshooting

  • Running a busy loop under SCHED_FIFO: can hang a single-CPU embedded board. Always ensure RT threads block regularly, and keep an emergency SSH session at a higher RT priority during development.
  • Confusing nice −20 with RT priority: nice −20 is still a normal thread; any SCHED_FIFO priority-1 thread beats it.
  • Forgetting privileges: switching to FIFO/RR/DEADLINE needs root or CAP_SYS_NICE; otherwise sched_setscheduler() fails with EPERM.
  • Hitting RT throttling: by default the kernel reserves a small slice of every second for non-RT tasks (see /proc/sys/kernel/sched_rt_runtime_us). If your RT thread mysteriously pauses, this safety valve is why.
  • Assuming hard real-time: stock policies give soft real-time. For hard determinism, enable PREEMPT_RT (built into mainline configs since 6.12) and measure latency with tools like cyclictest.

Best Practices

  • Start every design with SCHED_NORMAL; escalate to RT policies only when measurements show you need them.
  • Prefer SCHED_RR over SCHED_FIFO when several RT threads share one priority level.
  • For strictly periodic work, prefer SCHED_DEADLINE — its admission control protects you from overcommitting the CPU.
  • Keep RT priorities low (e.g., 10–50) and leave headroom; priority 99 competes with critical kernel threads like watchdogs.
  • Document every RT thread in your project: policy, priority, and why.

Key Takeaways

  • Linux scheduling policies split into the deadline world, real-time world (FIFO/RR, priority 1–99), the normal fair world (nice −20 to +19), and SCHED_IDLE at the bottom.
  • SCHED_FIFO has an effectively infinite timeslice; SCHED_RR adds a finite timeslice (default 100 ms).
  • Real-time on stock Linux means soft real-time; interrupts always preempt even priority-99 threads.
  • Modern kernels (6.6+) run the normal world on EEVDF instead of CFS, and offer SCHED_DEADLINE above FIFO/RR.
  • Use chrt, nice, and ps -eo cls,rtprio,ni to inspect and control policies from the shell.

Interview Questions and FAQ

Q1. What is the difference between SCHED_FIFO and SCHED_RR?

Both are soft real-time policies with priority 1–99, but SCHED_RR gives each thread a finite timeslice (default 100 ms) and rotates among equal-priority threads; SCHED_FIFO has no timeslice and runs until it blocks, exits, yields, or is preempted by a higher-priority RT thread.

Q2. Can a nice −20 thread preempt a SCHED_FIFO priority-1 thread?

No. All normal-world threads have real-time priority 0, so even the strongest nice value never competes with the weakest real-time thread.

Q3. Is Linux a hard real-time operating system?

Stock Linux is a general-purpose OS offering soft real-time. With the PREEMPT_RT configuration (part of mainline since kernel 6.12) it achieves near-deterministic latencies suitable for many hard real-time use cases, but formal hard-RT guarantees still require careful system design.

Q4. What preempts a SCHED_FIFO priority-99 thread?

Hardware and software interrupts, SCHED_DEADLINE threads, and stop-class kernel machinery. No FIFO/RR or normal thread can preempt it.

Q5. What is the default timeslice of SCHED_RR and can it be changed?

100 ms by default; it can be read and tuned via /proc/sys/kernel/sched_rr_timeslice_ms.

Q6. Which policy should a periodic 1 kHz control loop use?

SCHED_DEADLINE is ideal: declare runtime/deadline/period and let the kernel’s admission control guarantee the budget. If unavailable, a moderate SCHED_FIFO priority with careful blocking is the fallback.

Q7. What does each nice level change in practice?

Each step of the nice value corresponds to roughly a 10% change in CPU bandwidth share relative to the neighboring level, which adds up quickly across the −20 to +19 range.

References

Continue Your Free Linux Kernel Development Course

Next lecture: the modern Linux CPU scheduler internals — EEVDF, scheduling classes, and sched_ext.

← Previous Lecture
Next Lecture →

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *