How to Set Thread Scheduling Policy and Priority in Linux – chrt, System Calls and Pthreads
Part 4 of the CPU scheduler module in our free Linux kernel development course – practical, copy-paste-ready ways to query and change any thread’s policy and priority
Intermediate
CPU Scheduling – Lesson 4
100% Hands-On
Theory is done – now we get our hands dirty. In this lesson of the free Linux kernel development course you will learn every practical way to set thread scheduling policy and priority in Linux: from the shell with chrt and nice, from C code with the scheduling system calls, per-thread with the pthreads API, and even the modern sched_setattr() path used for SCHED_DEADLINE.
These skills are used daily in real embedded projects – boosting a UART driver’s IRQ thread, pinning a control loop at a real-time priority, or demoting a logger so it never disturbs the data path. If you are following our free Linux device drivers course or free embedded systems course, bookmark this page; you will come back to it often.
Topics Covered in This Lesson
nice / renice
sched_setscheduler()
sched_getscheduler()
pthread_setschedparam()
sched_setattr()
CAP_SYS_NICE
What You Will Learn
- Query and change policy/priority from the command line with
chrt,nice, andrenice - Write a C program that reads and sets a thread’s scheduling policy using the classic system calls
- Set per-thread attributes correctly with the pthreads API
- Use
sched_setattr()for SCHED_DEADLINE and modern attribute control - Understand the permissions (capabilities and rlimits) required to raise priority
Prerequisites
Lessons 1–3 of this series, a Linux machine with GCC installed, and sudo access for the real-time experiments. All code below is self-contained and compiles on any modern distribution.
Method 1: The Command Line – chrt, nice, renice
chrt (from util-linux) is the fastest way to inspect or change a thread’s real-time attributes:
# Query policy and priority of an existing thread/process
chrt -p 1234
# Launch a program under SCHED_FIFO, priority 20 (needs privilege)
sudo chrt -f 20 ./my_control_loop
# Launch under SCHED_RR, priority 10
sudo chrt -r 10 ./my_worker
# Change a RUNNING thread to SCHED_OTHER (back to normal)
sudo chrt -o -p 0 1234
# Launch under SCHED_BATCH or SCHED_IDLE (no privilege needed)
chrt -b 0 make -j8
chrt -i 0 ./background_indexer
For the fair policies, the nice value is managed with nice (at launch) and renice (afterwards):
# Start a job politely at nice +10
nice -n 10 tar czf backup.tgz /data
# Lower the priority of an already-running process (no root needed to increase niceness)
renice +15 -p 1234
# Raising priority (more negative nice) requires privilege
sudo renice -5 -p 1234
Method 2: The Classic System Calls in C
The portable POSIX interface consists of sched_getscheduler(), sched_setscheduler(), and friends. Here is a complete, original example that prints its own policy, switches itself to SCHED_RR, and verifies the change:
/* sched_demo.c - query and set our own scheduling policy
* Build : gcc -Wall sched_demo.c -o sched_demo
* Run : sudo ./sched_demo (RT policy needs privilege)
*/
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <string.h>
static const char *policy_name(int p)
{
switch (p) {
case SCHED_OTHER: return "SCHED_OTHER";
case SCHED_FIFO: return "SCHED_FIFO";
case SCHED_RR: return "SCHED_RR";
case SCHED_BATCH: return "SCHED_BATCH";
case SCHED_IDLE: return "SCHED_IDLE";
default: return "UNKNOWN";
}
}
int main(void)
{
struct sched_param sp;
int policy;
/* --- 1. Query current policy and priority (pid 0 = calling thread) --- */
policy = sched_getscheduler(0);
if (policy < 0) {
perror("sched_getscheduler");
return EXIT_FAILURE;
}
if (sched_getparam(0, &sp) < 0) {
perror("sched_getparam");
return EXIT_FAILURE;
}
printf("Before: policy=%s rt_priority=%d\n",
policy_name(policy), sp.sched_priority);
/* --- 2. Switch to SCHED_RR at a modest priority --- */
memset(&sp, 0, sizeof(sp));
sp.sched_priority = 15; /* valid range: 1..99 */
if (sched_setscheduler(0, SCHED_RR, &sp) < 0) {
fprintf(stderr, "sched_setscheduler: %s "
"(run with sudo or grant CAP_SYS_NICE)\n",
strerror(errno));
return EXIT_FAILURE;
}
/* --- 3. Verify --- */
policy = sched_getscheduler(0);
sched_getparam(0, &sp);
printf("After : policy=%s rt_priority=%d\n",
policy_name(policy), sp.sched_priority);
/* Useful helpers: valid priority range for a policy */
printf("SCHED_RR priority range on this system: %d..%d\n",
sched_get_priority_min(SCHED_RR),
sched_get_priority_max(SCHED_RR));
return EXIT_SUCCESS;
}
Key details worth memorizing:
- Passing
pid 0means “the calling thread”. Passing a TID targets that specific thread – another confirmation that the KSE is the thread. - For the fair policies,
sched_prioritymust be 0; the knob there is the nice value, changed viasetpriority()ornice(). - Always check the valid range with
sched_get_priority_min()/max()instead of hard-coding 1–99.
Method 3: Per-Thread Control with Pthreads
In a multi-threaded program you usually want one thread real-time (the sampling loop) while the rest stay normal. The pthreads API handles this cleanly, with one classic trap: by default a new thread inherits the creator’s policy, and your explicit attributes are ignored unless you set the inherit mode:
/* rt_thread.c - create ONE real-time thread among normal threads
* Build : gcc -Wall rt_thread.c -o rt_thread -lpthread
* Run : sudo ./rt_thread
*/
#include <stdio.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
static void *rt_worker(void *arg)
{
int policy;
struct sched_param sp;
pthread_getschedparam(pthread_self(), &policy, &sp);
printf("worker: policy=%s priority=%d\n",
policy == SCHED_FIFO ? "SCHED_FIFO" : "other",
sp.sched_priority);
/* real work: a bounded, well-behaved loop that blocks regularly */
for (int i = 0; i < 3; i++) {
/* ... time-critical work here ... */
usleep(1000); /* always give the CPU back! */
}
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_attr_t attr;
struct sched_param sp = { .sched_priority = 30 };
pthread_attr_init(&attr);
/* THE critical line: without it, the attrs below are ignored */
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, &sp);
if (pthread_create(&tid, &attr, rt_worker, NULL) != 0) {
perror("pthread_create (need privilege for SCHED_FIFO)");
return 1;
}
pthread_join(tid, NULL);
pthread_attr_destroy(&attr);
return 0;
}
You can also retarget a thread that is already running with pthread_setschedparam(tid, policy, &sp) – handy when the decision to go real-time happens after startup.
Method 4: sched_setattr() – The Modern Interface
sched_setattr() supersedes the older calls: one system call carries policy, nice, RT priority, and the deadline parameters. It is the only way to request SCHED_DEADLINE. Glibc does not wrap it, so we invoke it via syscall():
/* dl_demo.c - request SCHED_DEADLINE: 2ms runtime every 10ms
* Build : gcc -Wall dl_demo.c -o dl_demo
* Run : sudo ./dl_demo
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/sched.h>
#include <linux/sched/types.h>
static int sched_setattr(pid_t pid, struct sched_attr *attr,
unsigned int flags)
{
return syscall(SYS_sched_setattr, pid, attr, flags);
}
int main(void)
{
struct sched_attr attr;
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 2 * 1000 * 1000; /* 2 ms in ns */
attr.sched_deadline = 10 * 1000 * 1000; /* 10 ms in ns */
attr.sched_period = 10 * 1000 * 1000; /* 10 ms in ns */
if (sched_setattr(0, &attr, 0) < 0) {
perror("sched_setattr (admission control may refuse)");
return 1;
}
printf("Running under SCHED_DEADLINE: 2ms / 10ms guaranteed\n");
/* periodic work would go here; yield ends each instance */
sched_yield();
return 0;
}
If the kernel cannot guarantee the requested budget (too many deadline threads already admitted), the call fails with EBUSY – guarantees are refused rather than silently broken.
Permissions: Who May Raise Priority?
| Operation | Unprivileged user | Requirement otherwise |
|---|---|---|
| Increase nice (be more polite) | Allowed | – |
| Decrease nice (demand more CPU) | Denied by default | CAP_SYS_NICE or RLIMIT_NICE |
| Set SCHED_FIFO / SCHED_RR | Denied by default | CAP_SYS_NICE or RLIMIT_RTPRIO > 0 |
| Set SCHED_DEADLINE | Denied | CAP_SYS_NICE + admission control must pass |
| Set SCHED_BATCH / SCHED_IDLE | Allowed | – |
Production tip: instead of running your whole application as root, grant just the capability to the binary: sudo setcap cap_sys_nice+ep ./my_app.
Common Mistakes and Best Practices
- Forgetting PTHREAD_EXPLICIT_SCHED – the most common pthreads scheduling bug: your attributes are silently ignored and the thread inherits the parent’s policy.
- Hard-coding priority 99 – leave headroom below kernel-critical threads; pick the lowest priority that meets your latency goal.
- Real-time threads that never block – design every RT loop to sleep, wait on an event, or yield each cycle.
- Ignoring return values – priority changes fail for permission and admission reasons; always check and log
errno. - Testing only as root – verify the deployed capability/rlimit setup, not just your development machine.
Interview Questions and Answers
Q1: How do you make exactly one thread of a process real-time?
A: Create it with a pthread attribute object: set PTHREAD_EXPLICIT_SCHED, then the policy (e.g. SCHED_FIFO) and priority, and create the thread with those attributes – or call pthread_setschedparam() on the running thread. Other threads remain SCHED_OTHER.
Q2: Why might sched_setscheduler() fail with EPERM?
A: The caller lacks CAP_SYS_NICE and has no RLIMIT_RTPRIO allowance. Raising priority is a privileged operation because it can starve other users’ work.
Q3: What is special about sched_setattr() compared to sched_setscheduler()?
A: It carries an extended attribute structure supporting all policies including SCHED_DEADLINE with runtime/deadline/period, plus flags. It is the modern superset interface.
Q4: What does pid 0 mean in the scheduling system calls?
A: The calling thread itself. Any other value is interpreted as a thread ID, allowing per-thread control – consistent with the thread being the KSE.
Q5: Why can a SCHED_DEADLINE request be rejected even with root privileges?
A: Admission control. The kernel sums the declared budgets of all deadline threads; if adding yours would exceed available capacity, the request fails rather than jeopardizing existing guarantees.
FAQ
Can I change another process’s priority?
Yes, with sufficient privilege: chrt -p, renice, or the system calls all accept a target PID/TID.
Does nice work on SCHED_FIFO threads?
No. Nice affects only the fair policies. FIFO/RR use the 1–99 real-time priority instead.
How do I return a thread to normal scheduling?
chrt -o -p 0 <tid> from the shell, or sched_setscheduler(tid, SCHED_OTHER, ¶m) with priority 0 from code.
Is any of this different on kernel 6.6+ (EEVDF)?
No. All interfaces in this lesson are unchanged; EEVDF replaced only the internal fair-class algorithm.
Where can I learn the surrounding topics for free?
Right here – this article belongs to the free Linux kernel development course at EmbeddedPathashala, alongside the free Linux device drivers course and free embedded systems course.
Key Takeaways
chrt,nice, andrenicecover most day-to-day needs from the shell.sched_getscheduler()/sched_setscheduler()are the portable C interface; pid 0 targets the calling thread.- Pthreads scheduling attributes require
PTHREAD_EXPLICIT_SCHEDto take effect. sched_setattr()is the modern interface and the only path to SCHED_DEADLINE.- Raising priority needs CAP_SYS_NICE or rlimits; prefer
setcapover running as root.
Continue the Free Linux Kernel Development Course
Coming up next: visualizing scheduler behavior – watching context switches and CPU placement live with perf and tracing tools.

2 Comments