If you are building embedded Linux applications, sooner or later you will care about Linux scheduling: why one thread runs before another, why a background job doesn’t freeze your UI, and why a control loop occasionally misses a beat. This lecture is part of our free Linux kernel development course and free embedded Linux course, and it lays the foundation for everything else we cover on scheduling in this chapter.
Linux schedules threads, not processes. A process with five threads has five independent schedulable entities, each of which may be running a different policy with a different priority. Keeping that distinction straight is the single most useful mental model you can carry into the rest of this free linux device drivers course module.
What You Will Learn
- Why the Linux scheduler operates on threads and not on processes
- The three events that trigger the scheduler to run
- How the Completely Fair Scheduler (CFS) shares CPU time among timeshare threads
- The three timeshare policies: SCHED_NORMAL, SCHED_BATCH, and SCHED_IDLE
- How nice values bias CPU entitlement, and how to read and change them
- How to query and set a thread’s scheduling policy from C
Prerequisites
- Comfortable with C and POSIX threads (pthreads) basics
- A Linux machine or embedded board running a modern kernel (5.x or later)
- gcc and glibc development headers installed
Why the Scheduler Even Needs to Run
A CPU core can only execute one thread at a time. The Linux scheduler’s job is to decide which ready thread gets that core next. It does not wake up at random moments — it runs at three specific triggers:
Every thread carries a scheduling policy. Policies fall into two families: timeshare policies, which aim for fairness, and real-time policies, which aim for determinism. This lecture covers the timeshare family; the next lecture in this free linux kernel development course covers real-time policies in depth.
The Completely Fair Scheduler (CFS)
Since Linux 2.6.23, timeshare threads are scheduled by the Completely Fair Scheduler. Unlike older schedulers, CFS does not hand out fixed timeslices. Instead it tracks, per thread, a virtual runtime — roughly, how much CPU time the thread has already received relative to its fair share. The scheduler always picks the ready thread with the smallest virtual runtime. A thread that has run a lot accumulates a larger virtual runtime and drops down the queue; a thread that has been waiting accumulates a smaller one and rises to the front. This self-balancing approach is why CFS adapts well to a mix of interactive and CPU-heavy workloads without any manual timeslice tuning.
The Three Timeshare Policies
| Policy | Also Known As | Behaviour |
|---|---|---|
| SCHED_NORMAL | SCHED_OTHER | Default policy. Nearly every user-space thread on a Linux system uses this. |
| SCHED_BATCH | — | Same fairness model as SCHED_NORMAL but scheduled in larger chunks, trading responsiveness for fewer context switches and less cache churn. Good fit for background compilation or batch data processing. |
| SCHED_IDLE | — | Lowest possible priority. Runs only when nothing else is ready — ideal for a housekeeping task you never want to compete with real work. |
Niceness: Biasing CPU Entitlement
Within SCHED_NORMAL and SCHED_BATCH, you can bias a thread’s fair share using its nice value. The scale runs from -20 (least nice, most CPU-hungry) to 19 (most nice, most willing to step aside), with 0 as the default. Lowering a thread’s niceness (making it less nice, more demanding) requires the CAP_SYS_NICE capability, normally held only by root.
A subtlety worth remembering for interviews and real debugging alike: the classic nice(2) API and the nice/renice shell commands are documented in terms of processes, but the value actually belongs to the thread. On Linux you can target an individual thread by passing its TID (thread ID) wherever a PID is expected.
Reading and Setting Policy From Code
Two API pairs exist. One operates on a PID and affects a process’s main thread:
struct sched_param {
int sched_priority;
/* ... */
};
int sched_setscheduler(pid_t pid, int policy,
const struct sched_param *param);
int sched_getscheduler(pid_t pid);
The other operates on a pthread_t and can target any thread in the process:
int pthread_setschedparam(pthread_t thread, int policy,
const struct sched_param *param);
int pthread_getschedparam(pthread_t thread, int *policy,
struct sched_param *param);
For timeshare policies, sched_priority must be 0 — priority only has meaning for real-time policies, which we cover next lecture.
Original Demo: Inspecting and Setting Niceness
Let’s write a small original program, ep_nice_demo, that reports its own scheduling policy and nice value, then raises its niceness (making itself more cooperative) before doing a short CPU-bound loop.
/* ep_nice_demo.c
* Reports current policy/nice value, then raises niceness.
* Build: gcc -O2 -o ep_nice_demo ep_nice_demo.c
* Run: ./ep_nice_demo
*/
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
#include <sys/resource.h>
#include <errno.h>
static const char *policy_name(int p)
{
switch (p) {
case SCHED_OTHER: return "SCHED_NORMAL";
case SCHED_BATCH: return "SCHED_BATCH";
case SCHED_IDLE: return "SCHED_IDLE";
default: return "unknown";
}
}
int main(void)
{
int policy = sched_getscheduler(0);
errno = 0;
int nice_before = getpriority(PRIO_PROCESS, 0);
printf("ep_nice_demo: policy=%s nice=%d\n",
policy_name(policy), nice_before);
if (setpriority(PRIO_PROCESS, 0, 10) == -1) {
perror("setpriority");
return 1;
}
int nice_after = getpriority(PRIO_PROCESS, 0);
printf("ep_nice_demo: nice value raised to %d (more cooperative)\n",
nice_after);
volatile long sum = 0;
for (long i = 0; i < 200000000L; i++)
sum += i;
printf("ep_nice_demo: busy loop done, sum=%ld\n", sum);
return 0;
}
Build and run it, and confirm the change from a second terminal:
$ gcc -O2 -o ep_nice_demo ep_nice_demo.c
$ ./ep_nice_demo &
[1] 18422
ep_nice_demo: policy=SCHED_NORMAL nice=0
ep_nice_demo: nice value raised to 10 (more cooperative)
$ ps -o pid,ni,cls,cmd -p 18422
PID NI CLS CMD
18422 10 TS ./ep_nice_demo
The CLS column reading TS confirms the timeshare (SCHED_NORMAL/CFS) class, and NI shows the raised nice value of 10 taking effect while the thread is still running.
setpriority() with a negative value and getting EPERM. Lowering niceness below 0 needs CAP_SYS_NICE — either run as root or grant the capability with sudo setcap cap_sys_nice+ep ./ep_nice_demo.
Best Practices
- Leave threads on SCHED_NORMAL unless you have a concrete, measured reason to change policy.
- Use SCHED_BATCH for long-running batch/background work where you want fewer context switches, not lower priority.
- Use SCHED_IDLE for true “only when the system is otherwise quiet” housekeeping — not as a substitute for a positive nice value.
- Prefer TID-targeted calls (
pthread_setschedparam) when you need to change one thread inside a multi-threaded process, not the whole process.
Summary
The Linux scheduler operates on threads, wakes up on blocking calls, timeslice exhaustion, and interrupt-driven unblocks, and — for the vast majority of threads — hands scheduling decisions to CFS under the SCHED_NORMAL policy. SCHED_BATCH and SCHED_IDLE cover background and lowest-priority work, and nice values let you fine-tune CPU entitlement within the timeshare world. In the next lecture of this free embedded systems course we move to the deterministic side: real-time policies SCHED_FIFO and SCHED_RR.
FAQ
What is the difference between a process and a thread for Linux scheduling purposes?
The Linux scheduler picks among threads, not processes. A multi-threaded process is really a group of independently schedulable threads that happen to share an address space; each can have its own policy, priority, and nice value.
What does CFS stand for and what problem does it solve?
CFS stands for Completely Fair Scheduler. It replaced fixed timeslice scheduling in Linux 2.6.23 and instead tracks each thread’s virtual runtime, always running the thread that has received the least CPU time relative to its fair share.
What is the default Linux scheduling policy?
SCHED_NORMAL, also called SCHED_OTHER. The overwhelming majority of threads on any Linux system use this policy.
What is the nice value range in Linux?
Nice values range from -20 (highest CPU entitlement) to 19 (lowest CPU entitlement), with 0 as the default.
Do I need root privileges to change a thread’s nice value?
Only to lower niceness (increase CPU entitlement). Raising your own niceness requires no special privilege; lowering it below your current value requires CAP_SYS_NICE.
When should I use SCHED_BATCH instead of SCHED_NORMAL?
Use SCHED_BATCH for CPU-bound, non-interactive work such as compilation or bulk data processing, where you want fewer context switches and better cache behaviour rather than lower priority.
Can a nice value be applied to real-time threads?
No. Nice values only affect SCHED_NORMAL and SCHED_BATCH threads. Real-time threads use a separate static priority, covered in the next lecture.
Continue the Free Linux Kernel Development Course
Next up: real-time scheduling with SCHED_FIFO and SCHED_RR — deterministic execution for time-critical embedded threads.
Next Lecture Course Index
2 Comments