Linux Thread Scheduling Policy and Priority: Query and Set It the Right Way
Part of the free Linux kernel development course on EmbeddedPathashala — learn chrt, sched_setattr(), and pthread scheduling APIs on modern kernels
Intermediate
6.x (EEVDF era)
100% Free
Every thread on a Linux system carries two attributes that decide how the CPU scheduler treats it: a scheduling policy and a priority. In this lesson of our free Linux kernel development course, you will learn how to query and change the Linux thread scheduling policy and priority from user space — first with the chrt utility, then programmatically with the sched_setattr() system call and the pthread wrapper APIs.
This tutorial is written against modern kernels (6.x series onwards), where the old CFS fair scheduler has been replaced by EEVDF (Earliest Eligible Virtual Deadline First). The user-facing APIs described here work the same way on both, which is exactly why they are worth learning properly. If you are following our free embedded systems course track, this lesson builds directly on the CPU scheduler internals lessons.
What You Will Learn
- Which scheduling policies exist on Linux today and when each one is used
- How to query any thread’s scheduling policy and priority with
chrt - How to change policy and priority from the shell, safely
- How to do the same programmatically with
sched_setattr()andsched_getattr() - How the pthread wrapper APIs (
pthread_setschedparam()and friends) fit in - Which privileges and resource limits gate real-time priorities
- Common mistakes that make real-time threads freeze an entire machine
Prerequisites
You should be comfortable with basic C programming and compiling programs with gcc. It helps if you have already gone through the earlier lessons of this free Linux kernel development course covering scheduling classes and the EEVDF scheduler, but this lesson can also be read standalone. Any Linux machine or virtual machine with a recent distribution works; no special hardware is needed.
Linux Scheduling Policies: A Quick Refresher
Before touching the APIs, let us be clear about what we are actually setting. Linux groups every thread under one of the following policies:
| Policy | Type | Priority Range | Typical Use |
|---|---|---|---|
SCHED_OTHER |
Normal (EEVDF) | 0 (uses nice −20 to +19) | Default for almost everything |
SCHED_BATCH |
Normal (EEVDF) | 0 (uses nice) | CPU-heavy background jobs |
SCHED_IDLE |
Normal (EEVDF) | 0 | Runs only when CPU is otherwise idle |
SCHED_FIFO |
Soft real-time | 1 to 99 | Run-to-completion latency-critical work |
SCHED_RR |
Soft real-time | 1 to 99 | Like FIFO but with time slices at equal priority |
SCHED_DEADLINE |
Hard-ish real-time | Uses runtime/deadline/period, not a priority number | Periodic tasks with strict deadlines |
The key mental model: real-time policies (SCHED_FIFO, SCHED_RR, SCHED_DEADLINE) always win over the normal policies. A single runnable FIFO thread at priority 1 will starve every SCHED_OTHER thread on that CPU until it blocks or yields. That power is exactly why the kernel gates it behind privileges, as we will see shortly.
| stop class (kernel internal, migration/hotplug) |
| deadline class — SCHED_DEADLINE |
| rt class — SCHED_FIFO / SCHED_RR (prio 1–99) |
| fair class (EEVDF) — SCHED_OTHER / SCHED_BATCH / SCHED_IDLE |
| idle class — the per-CPU idle task (swapper) |
Querying Scheduling Policy and Priority with chrt
The quickest way to inspect a thread’s scheduling policy and priority is the chrt utility from util-linux. Internally it wraps the same system calls we will use in C, so it is a great first tool.
# Query the policy and priority of a process by PID
$ chrt -p 1
pid 1's current scheduling policy: SCHED_OTHER
pid 1's current scheduling priority: 0
# Query a specific thread (TID works the same as PID here)
$ chrt -p 2712
pid 2712's current scheduling policy: SCHED_FIFO
pid 2712's current scheduling priority: 50
Want to see every kernel thread running with a real-time policy on your box? A one-liner with ps does it:
# cls = scheduling class, rtprio = real-time priority
$ ps -eLo pid,tid,cls,rtprio,comm | awk '$3=="FF" || $3=="RR"'
On most systems you will spot threaded interrupt handlers (names like irq/125-xhci_hcd) sitting at FIFO priority 50 — we cover why in the kernel-side lesson of this free Linux device drivers course.
Setting Scheduling Policy and Priority from the Shell
Changing the policy is just as simple — but raising a thread to a real-time policy needs either root or the CAP_SYS_NICE capability.
# Launch a new program under SCHED_FIFO at priority 30
$ sudo chrt -f 30 ./my_latency_sensitive_app
# Change an already-running thread to SCHED_RR at priority 20
$ sudo chrt -r -p 20 2712
# Drop a runaway RT thread back to the normal policy
$ sudo chrt -o -p 0 2712
# Run a background job under SCHED_IDLE so it never disturbs anyone
$ chrt -i 0 ./nightly_rebuild.sh
Notice the last command needs no sudo: lowering yourself is always allowed; raising yourself is what is restricted.
Who is allowed to go real-time?
- Root / CAP_SYS_NICE: can set any policy and priority on any thread.
- Unprivileged users: limited by the
RLIMIT_RTPRIOresource limit. Check it withulimit -r. If it reads 0, you cannot enter FIFO/RR at all. - systemd systems: you can grant a service real-time rights cleanly with
LimitRTPRIO=andAmbientCapabilities=CAP_SYS_NICEin its unit file instead of running the whole thing as root.
Doing It in C: sched_getattr() and sched_setattr()
For programs, the modern interface is the Linux-specific pair sched_getattr(2) and sched_setattr(2). These are a superset of the older sched_getscheduler(2) / sched_setscheduler(2) calls and are the only pair that also supports SCHED_DEADLINE. Depending on your glibc version you may need to invoke them via syscall(); recent glibc exposes wrappers.
Here is a small, complete demo you can compile and run. It prints its own current policy, switches itself to SCHED_FIFO priority 10, and verifies the change:
// sched_attr_demo.c
// Build : gcc -O2 -Wall sched_attr_demo.c -o sched_attr_demo
// Run : sudo ./sched_attr_demo
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/sched.h>
#include <linux/sched/types.h>
static int my_sched_setattr(pid_t pid, struct sched_attr *attr,
unsigned int flags)
{
return syscall(SYS_sched_setattr, pid, attr, flags);
}
static int my_sched_getattr(pid_t pid, struct sched_attr *attr,
unsigned int size, unsigned int flags)
{
return syscall(SYS_sched_getattr, pid, attr, size, flags);
}
static const char *policy_name(int policy)
{
switch (policy) {
case SCHED_OTHER: return "SCHED_OTHER";
case SCHED_BATCH: return "SCHED_BATCH";
case SCHED_IDLE: return "SCHED_IDLE";
case SCHED_FIFO: return "SCHED_FIFO";
case SCHED_RR: return "SCHED_RR";
case SCHED_DEADLINE: return "SCHED_DEADLINE";
default: return "unknown";
}
}
int main(void)
{
struct sched_attr attr;
/* Step 1: query our own current attributes (pid 0 == self) */
memset(&attr, 0, sizeof(attr));
if (my_sched_getattr(0, &attr, sizeof(attr), 0) < 0) {
perror("sched_getattr");
exit(EXIT_FAILURE);
}
printf("Before: policy=%s prio=%u nice=%dn",
policy_name(attr.sched_policy),
attr.sched_priority, attr.sched_nice);
/* Step 2: switch ourselves to SCHED_FIFO, priority 10 */
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
attr.sched_policy = SCHED_FIFO;
attr.sched_priority = 10;
if (my_sched_setattr(0, &attr, 0) < 0) {
perror("sched_setattr (need root or CAP_SYS_NICE?)");
exit(EXIT_FAILURE);
}
/* Step 3: verify */
memset(&attr, 0, sizeof(attr));
if (my_sched_getattr(0, &attr, sizeof(attr), 0) < 0) {
perror("sched_getattr");
exit(EXIT_FAILURE);
}
printf("After : policy=%s prio=%un",
policy_name(attr.sched_policy), attr.sched_priority);
return 0;
}
Run it twice — once as a normal user and once with sudo — and watch the EPERM failure turn into success. That single experiment teaches the permission model better than any paragraph can.
The pthread wrapper APIs
When you are working with POSIX threads rather than raw TIDs, the pthread library exposes friendlier wrappers over the same kernel machinery:
#include <pthread.h>
struct sched_param sp = { .sched_priority = 20 };
int policy;
/* Set: make a specific pthread run under SCHED_RR at prio 20 */
pthread_setschedparam(tid, SCHED_RR, &sp);
/* Get: read back what a pthread currently has */
pthread_getschedparam(tid, &policy, &sp);
/* Or bake it into the thread at creation time */
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, &sp);
pthread_create(&tid, &attr, worker_fn, NULL);
One trap worth calling out: by default a new pthread inherits the creator’s scheduling attributes and silently ignores whatever you put in the attribute object. You must set PTHREAD_EXPLICIT_SCHED, as shown above, for your policy and priority to actually apply.
| chrt(1) utility | pthread_setschedparam(3) | your C code |
| ↓ all funnel into ↓ | ||
| sched_setattr(2) / sched_getattr(2) system calls | ||
| ↓ | ||
| Kernel scheduler core — updates the thread’s task_struct scheduling fields | ||
Real-World Use Cases
- Audio pipelines: JACK and PipeWire run their processing threads under
SCHED_FIFOto avoid audible dropouts. - Industrial and robotics control loops: a periodic control thread under
SCHED_DEADLINEgets a mathematically guaranteed CPU budget every cycle. - Embedded data acquisition: a sensor-drain thread at FIFO priority ensures a full FIFO buffer in the hardware never overflows because the reader got scheduled late.
- Build servers: compile farms run heavy jobs under
SCHED_BATCHorSCHED_IDLEso interactive users never feel them.
Common Mistakes and Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
sched_setattr fails with EPERM |
No CAP_SYS_NICE and RLIMIT_RTPRIO is 0 |
Run privileged, or raise the rtprio limit in /etc/security/limits.conf |
| Machine freezes after starting an RT thread | A busy-looping FIFO thread is starving everything | Make the thread block/sleep; keep RT throttling (sched_rt_runtime_us) enabled while developing |
| pthread priority silently ignored | Default inherit-sched behaviour | Call pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED) |
sched_setattr fails with E2BIG/EINVAL |
Wrong attr.size or garbage fields |
memset the struct to zero and set size = sizeof(attr) before filling it |
| Child processes unexpectedly real-time | Policy is inherited across fork() |
Set the SCHED_RESET_ON_FORK flag so children fall back to SCHED_OTHER |
Best Practices
- Use the lowest real-time priority that solves your problem. Priority 99 is not a badge of importance; it competes with critical kernel threads.
- Real-time threads must spend most of their life blocked (on a device, futex, or timer). A CPU-bound FIFO loop is a denial of service against your own machine.
- Prefer
SCHED_DEADLINEfor genuinely periodic work — the kernel then admission-controls the workload and refuses over-commitment instead of letting you starve the system. - Grant real-time rights through capabilities or systemd unit directives instead of running whole applications as root.
- Log the result of every policy-change call. Silent
EPERMfailures in production are painful to diagnose after the fact.
Performance and Security Considerations
Performance: changing the policy of a running thread is cheap, but the consequences are global. One misplaced FIFO thread affects interrupt-handling latency, RCU progress, and every normal task on that CPU. Combine RT policies with CPU affinity (previous lesson) so latency-critical work lives on dedicated cores, away from general load.
Security: real-time scheduling is effectively a resource-exhaustion capability, which is why the kernel guards it with CAP_SYS_NICE and RLIMIT_RTPRIO. In containers, be aware that granting these rights lets a workload starve neighbours on the same host unless CPU bandwidth control (next lesson, cgroups v2) fences it in.
Key Takeaways
- Every Linux thread has a scheduling policy and priority; real-time policies always preempt the normal (EEVDF-managed) ones.
chrtis your query-and-set tool at the shell;sched_getattr()/sched_setattr()are the full-featured system calls in C.- pthread wrappers work fine, but remember
PTHREAD_EXPLICIT_SCHED. - Raising priority requires privilege; lowering never does.
- With great FIFO comes great responsibility — design RT threads to block.
Conclusion
Scheduling policy and priority are the primary knobs user space has for telling the Linux scheduler what matters. You now know how to inspect them, change them from the shell, and change them from C code using both the raw system calls and the pthread wrappers — along with the permission model that guards real-time access. These skills come up constantly in embedded Linux work, from tuning audio latency to keeping a sensor pipeline honest on a busy SoC.
In the next lesson of this free Linux kernel development course, we move inside the kernel: how kernel threads get their scheduling policy set with the modern sched_set_fifo() family (the old sched_setscheduler_nocheck() approach you may see in older books no longer works from modules), and why threaded interrupt handlers all run at FIFO priority 50.
FAQ
What is the difference between SCHED_FIFO and SCHED_RR?
Both are real-time policies with priorities 1–99 and both preempt all normal threads. The difference appears only between threads of equal real-time priority: a FIFO thread runs until it blocks, yields, or is preempted by a higher priority, while RR threads at the same priority rotate on a time slice.
Can a normal user set a real-time priority on Linux?
Only if the RLIMIT_RTPRIO soft limit allows it (check with ulimit -r) or the process holds CAP_SYS_NICE. Lowering priority or switching to SCHED_IDLE/SCHED_BATCH never needs privilege.
Which API should I use in new code: sched_setscheduler() or sched_setattr()?
Prefer sched_setattr(). It covers everything the older call does and is the only interface that can configure SCHED_DEADLINE parameters (runtime, deadline, period).
Does priority 0 mean lowest priority?
No. For normal policies the static priority is always 0 and relative importance is expressed via nice values. For real-time policies the valid range is 1–99, where 99 is the highest.
Why did my whole system hang when I ran a SCHED_FIFO loop?
A CPU-bound FIFO thread starves every normal thread on that CPU, including your desktop. The kernel’s RT throttling normally reserves a small slice for non-RT work, but on isolated or misconfigured systems even that safety net may be off. Always design RT threads to block.
Is SCHED_DEADLINE better than SCHED_FIFO for periodic tasks?
Usually, yes. You declare a runtime, deadline, and period, and the kernel performs admission control: it refuses configurations that cannot be guaranteed, instead of letting priorities fight it out.
Do these APIs change with the EEVDF scheduler in kernel 6.6+?
No. EEVDF replaced the internal algorithm of the fair scheduling class, but the user-facing policies, priorities, system calls, and tools shown in this tutorial are unchanged.
How do I make children of my RT process run as normal tasks?
Set the SCHED_RESET_ON_FORK flag when configuring the parent (chrt supports it via -R). Every child created with fork() then starts under SCHED_OTHER with nice 0.
Continue the Free Linux Kernel Development Course
Next up: setting scheduling policy on kernel threads, and CPU bandwidth control with cgroups v2.
