Deep-dive into Linux CPU scheduling, interrupt handling, and CPU isolation for latency-critical workloads.
Linux implements multiple scheduling classes with strict priority ordering:
Priority (highest to lowest):
SCHED_DEADLINE > SCHED_FIFO/RR > SCHED_NORMAL/BATCH/IDLE
| | |
Real-time Real-time Normal
(EDF-based) (static prio) (EEVDF/CFS)
| Policy | Use Case | Priority | Preemption |
|---|---|---|---|
SCHED_DEADLINE |
Hard RT: audio DSP, video frames, control loops | Highest | By deadline |
SCHED_FIFO |
Soft RT: deterministic latency, no time slice | Static 1-99 | Only by higher prio |
SCHED_RR |
Soft RT: round-robin among same priority | Static 1-99 | Time quantum + higher prio |
SCHED_NORMAL |
Default: interactive and batch | Dynamic (nice) | EEVDF virtual deadline |
SCHED_BATCH |
CPU-intensive: encoders, builds | Dynamic (nice) | Disfavored at wakeup |
SCHED_IDLE |
Background: backups, maintenance | Lowest | Only when idle |
# Set deadline parameters: runtime, deadline, period (all in nanoseconds)
# Guarantee: runtime CPU time within deadline, repeating every period
chrt -d --sched-runtime 5000000 \
--sched-deadline 10000000 \
--sched-period 16666666 0 ./process
# Example: Video processing at 60 FPS (16.67ms period)
# - 5ms runtime guaranteed
# - Must complete within 10ms of period start
# - Period repeats every 16.67ms
chrt -d --sched-runtime 5000000 \
--sched-deadline 10000000 \
--sched-period 16666666 0 ffmpeg -i input.mp4 -c:v libx264 output.mp4
# Audio processing: 10ms buffer, 2ms processing time
chrt -d --sched-runtime 2000000 \
--sched-deadline 8000000 \
--sched-period 10000000 0 jackd -d alsa
# Check current deadline parameters
cat /proc/<pid>/sched | grep -E "dl_runtime|dl_deadline|dl_period"Constraint: runtime <= deadline <= period
Admission control: Kernel rejects if total deadline utilization exceeds capacity:
sum(runtime_i / period_i) <= total_cpu_capacity
When to use SCHED_DEADLINE:
- Hard real-time with known WCET (worst-case execution time)
- Periodic tasks: audio, video, control loops
- Need guaranteed CPU regardless of other load
# SCHED_FIFO: Run until blocks or preempted by higher priority
chrt -f 80 ./latency_critical_app
# SCHED_RR: Same as FIFO but with time quantum rotation
chrt -r 50 ./soft_realtime_app
# Check time quantum for RR (default 100ms)
cat /proc/sys/kernel/sched_rr_timeslice_ms
# View process scheduling policy
chrt -p <pid>
# Priority range: 1 (lowest RT) to 99 (highest RT)
# Priority 99 reserved for kernel migration threads
# Use 1-98 for applications
# Set from within application (requires CAP_SYS_NICE)
# C: sched_setscheduler(0, SCHED_FIFO, ¶m)
# Python: os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(80))SCHED_FIFO vs SCHED_RR:
| Aspect | SCHED_FIFO | SCHED_RR |
|---|---|---|
| Time slice | Unlimited | Default 100ms |
| Same-priority handling | FIFO queue | Round-robin |
| Use case | Single critical path | Multiple RT tasks |
Warning: RT tasks at priority 99 can starve kernel threads. Use priority 1-90 for applications.
# SCHED_BATCH: CPU-intensive, non-interactive
# Scheduler assumes CPU-bound, applies wakeup penalty
chrt -b 0 make -j$(nproc)
nice -n 19 chrt -b 0 ffmpeg -i video.mp4 -c:v libx265 output.mp4
# SCHED_IDLE: Only runs when system truly idle
# Gets ~1.4% CPU even under load (starvation protection)
chrt -i 0 updatedb
chrt -i 0 /usr/bin/btrfs-backup.sh
# Combine with ionice for full background mode
ionice -c 3 chrt -i 0 rsync -av /data /backupSCHED_BATCH use cases:
- Video/audio encoding
- Database maintenance (VACUUM, compaction)
- Build systems, CI/CD
- Log analyzers, ETL jobs
SCHED_IDLE use cases:
- Index building (updatedb, mlocate)
- Backup scripts
- Garbage collection
- System maintenance
EEVDF (Earliest Eligible Virtual Deadline First) replaced CFS as the default scheduler.
# Check kernel version
uname -r # 6.6+ uses EEVDF
# Key behavior differences from CFS:
# - Virtual deadline instead of vruntime alone
# - Bounded latency guarantees
# - Simpler tuning (fewer heuristics)
# - Tasks can request shorter time slices
# Time slice range: 100us to 100ms
# Controlled via sched_setattr() syscall with sched_runtime parameter
# Lag tracking: positive lag = owed CPU time, negative = exceeded share
# Sleep handling: lag decays over virtual time (prevents sleep exploit)EEVDF vs CFS behavior:
| Aspect | CFS | EEVDF |
|---|---|---|
| Selection | Lowest vruntime | Earliest eligible deadline |
| Latency bound | Heuristic | Algorithmic |
| Sleep bonus | Explicit heuristics | Lag decay |
| Tuning knobs | Many (wakeup_granularity, etc.) | Fewer, cleaner |
Tunable parameters (6.6+):
# Minimum time slice (default 3ms)
sysctl kernel.sched_min_granularity_ns=3000000
# Migration cost threshold
sysctl kernel.sched_migration_cost_ns=500000
# Disable autogroup for servers (group by session otherwise)
sysctl kernel.sched_autogroup_enabled=0# cgroup v1: Set quota and period
echo 250000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us # 250ms quota
echo 100000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us # 100ms period
# Result: 2.5 CPUs worth of time (250ms / 100ms)
# cgroup v2: Single file
echo "250000 100000" > /sys/fs/cgroup/mygroup/cpu.max
# Docker equivalent
docker run --cpus=2.5 myimage
docker run --cpu-quota=250000 --cpu-period=100000 myimage
# Kubernetes equivalent
resources:
limits:
cpu: "2500m" # 2.5 cores
# Check throttling stats (cgroup v1)
cat /sys/fs/cgroup/cpu/mygroup/cpu.stat
# nr_periods: total periods elapsed
# nr_throttled: periods with throttling
# throttled_time: total ns throttled
# cgroup v2
cat /sys/fs/cgroup/mygroup/cpu.statThrottling pathology diagnosis:
# If you see throttling with low CPU usage:
# - Common in multi-threaded apps (JVM GC, Go runtime)
# - All threads share the quota within period
# - Burst consumption exhausts quota early in period
# Symptom: nr_throttled >> 0 but %CPU < limit
cat /sys/fs/cgroup/cpu/*/cpu.stat | grep throttled
# Solutions:
# 1. Increase period (allows more burst)
echo 250000 > cpu.cfs_period_us # Longer period
echo 625000 > cpu.cfs_quota_us # Same ratio, more headroom
# 2. Remove limits, use requests only (K8s)
resources:
requests:
cpu: "2500m"
# No limits specified
# 3. Use cpu.cfs_burst_us (kernel 5.14+)
echo 100000 > cpu.cfs_burst_us # Allow 100ms burst accumulationRed flags for CFS throttling:
- JVM STW GC pauses causing 100% quota consumption
- High throttled_time with low average CPU
- Latency spikes correlating with period boundaries
# vmstat: r column = processes waiting for CPU + currently running
vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 5 0 0 123456 12345 234567 0 0 0 0 1000 500 50 5 45 0 0
# Interpretation:
# r > num_CPUs = CPU saturated
# r - num_CPUs = actual queue depth
# b > 0 = processes blocked on I/O (not CPU)
# Get CPU count for comparison
nproc
# Real queue depth (subtract running processes)
# r=12 on 8-core = 4 processes waiting
# BPF tool: precise run queue length distribution
runqlen 10 1 # 10 samples, 1 second
# Or with bcc:
/usr/share/bcc/tools/runqlenRun queue thresholds:
| Ratio (r / nproc) | Status | Action |
|---|---|---|
| < 1 | Healthy | None |
| 1-2 | Elevated | Monitor |
| 2-4 | Saturated | Investigate |
| > 4 | Severely saturated | Immediate action |
Load average interpretation:
uptime
# 10:00:00 up 30 days, load average: 4.50, 3.20, 2.10
# Load = runnable + uninterruptible (D state) processes
# Includes I/O waiters, not just CPU demand
# Per-CPU load
awk '{print $1/'"$(nproc)"'}' /proc/loadavg
# Rule of thumb:
# load/nproc < 0.7 : Healthy headroom
# load/nproc = 1.0 : Fully utilized, no headroom
# load/nproc > 2.0 : Investigate immediately
# Caveat: load includes D-state processes
# High load + low CPU% = I/O bound, not CPU bound# System-wide context switches
vmstat 1
# cs column = context switches/second
# Per-process context switches
pidstat -w 1
# cswch/s = voluntary (waiting for resource)
# nvcswch/s = involuntary (time slice expired)
# Example output:
# PID cswch/s nvcswch/s Command
# 1234 150.00 50.00 myapp
# High voluntary = I/O bound or lock contention
# High involuntary = CPU bound, being preempted
# Detailed per-thread
pidstat -w -t 1
# Process lifetime totals
cat /proc/<pid>/status | grep ctxt
# voluntary_ctxt_switches: 12345
# nonvoluntary_ctxt_switches: 6789
# perf stat for specific workload
perf stat -e context-switches,cpu-migrations ./myappContext switch thresholds:
| Rate | Assessment | Notes |
|---|---|---|
| < 10K/s | Normal | Typical server |
| 10K-50K/s | Elevated | Check if justified |
| 50K-100K/s | High | Likely performance impact |
| > 100K/s | Critical | Thread thrashing likely |
Cost per context switch: 1-10 microseconds depending on cache state
# Identify context switch causes
perf sched record -a sleep 5
perf sched latency --sort max
# High involuntary switches indicate:
# - Too many threads for available CPUs
# - Short time slices under contention
# - Aggressive RT processes preempting
# High voluntary switches indicate:
# - Frequent I/O or syscalls
# - Lock contention (mutex, futex)
# - Over-synchronization# Check NUMA topology
numactl --hardware
# node 0 cpus: 0-15
# node 1 cpus: 16-31
# node distances:
# node 0 1
# 0: 10 21
# 1: 21 10
# Monitor cross-node scheduling
perf stat -e node-loads,node-stores ./myapp
# CPU migrations (cross-socket is expensive)
perf stat -e cpu-migrations ./myapp
# Per-process NUMA stats
numastat -p <pid>
# Check scheduler domains
cat /proc/sys/kernel/sched_domain/cpu0/domain*/name
# SMT, MC, NUMA
# Memory locality
cat /proc/<pid>/numa_maps | head
# Shows where memory pages are allocated vs accessedNUMA scheduling pathologies:
# Symptom: High memory latency despite free local memory
# Diagnosis:
numastat -p <pid>
# Look for high "other_node" counts
# Fix 1: Bind to NUMA node
numactl --cpunodebind=0 --membind=0 ./myapp
# Fix 2: Disable auto NUMA balancing if it's hurting
sysctl kernel.numa_balancing=0
# Fix 3: CPU pinning to keep memory local
taskset -c 0-15 ./myapp # Stay on node 0
# Verify NIC NUMA affinity
cat /sys/class/net/eth0/device/numa_node
# Schedule network processing on same nodeCross-node migration cost:
- Same socket, different core: ~50-100ns cache miss
- Different socket: ~100-300ns + cache invalidation
# Record scheduler events (warning: high overhead)
perf sched record -a -- sleep 10
# Or for specific command:
perf sched record -- ./myapp
# Latency report: per-task scheduling delays
perf sched latency
# Shows: max, avg scheduler latency per task
# Task | Runtime ms | Switches | Average delay ms | Maximum delay ms |
# -----------------------------------------------------------------
# stress:12345 | 8000.000 ms | 1234 | avg: 0.050 ms | max: 15.000 ms |
# Sort by worst offenders
perf sched latency --sort max
# Timeline view (since kernel 4.10)
perf sched timehist
# Columns: time, cpu, task, wait time, sch delay, runtime
# wait time = time sleeping
# sch delay = time runnable but not running (queue time)
# Add CPU visualization and migrations
perf sched timehist -MVw
# -M = show migrations
# -V = CPU visualization
# -w = wakeup events
# Summary only
perf sched timehist --summary
# CPU map view (context switches across CPUs)
perf sched map --compact
# Shows which task ran on which CPU at what time
# * = CPU with event, . = idle CPU
# Replay for analysis (experimental)
perf sched replayInterpreting perf sched latency output:
# Good: max delay < 1ms for interactive
# Warning: max delay > 10ms
# Critical: max delay > 100ms
# High switches + low runtime = thrashing
# Low switches + high runtime = batch workload (normal)
BPF-based scheduler analysis (lower overhead):
# Run queue latency distribution
runqlat 10 1 # From bcc-tools
# Shows histogram of time spent waiting in run queue
# Run queue length over time
runqlen 10 1
# Off-CPU time analysis (why tasks aren't running)
offcputime -p <pid> 10# View current IRQ assignments
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 27: 1234567 0 0 0 IR-PCI-MSI eth0-TxRx-0
# 28: 0 2345678 0 0 IR-PCI-MSI eth0-TxRx-1
# Check NIC queue to IRQ mapping
ls /sys/class/net/eth0/queues/
# rx-0 rx-1 rx-2 rx-3 tx-0 tx-1 tx-2 tx-3
# View IRQ affinity (hex bitmask)
cat /proc/irq/27/smp_affinity
# 00000001 = CPU 0
# Set IRQ to specific CPU (hex mask)
# CPU 0 = 1, CPU 1 = 2, CPU 2 = 4, CPU 3 = 8
echo 2 > /proc/irq/27/smp_affinity # CPU 1
echo f > /proc/irq/27/smp_affinity # CPUs 0-3
# List format (easier)
echo 0-3 > /proc/irq/27/smp_affinity_list
# Per-NIC queue affinity
cat /sys/class/net/eth0/queues/rx-0/rps_cpus
# Stop irqbalance for manual control
systemctl stop irqbalance
systemctl disable irqbalanceMulti-queue NIC strategy:
# Ideal: 1 queue per CPU, spread across NUMA node
# Check number of queues
ethtool -l eth0
# Combined: 8 (current), max 16
# Increase queues if needed
ethtool -L eth0 combined 16
# Spread IRQs across CPUs on same NUMA node as NIC
cat /sys/class/net/eth0/device/numa_node
# 0
# Pin each queue IRQ to different CPU on node 0
for i in {27..34}; do
cpu=$((i - 27))
echo $cpu > /proc/irq/$i/smp_affinity_list
done
# Verify distribution
grep eth0 /proc/interruptsReceive Side Scaling (RSS) tuning:
# Check RSS hash settings
ethtool -x eth0
# Shows indirection table and hash key
# Configure flow distribution
ethtool -X eth0 equal 8 # Spread across 8 queues evenly
# Set hash fields (for better distribution)
ethtool -N eth0 rx-flow-hash tcp4 sdfn # src/dst IP + src/dst port# Identify softirq load
top
# Look for ksoftirqd/N at high CPU%
# Or high %si in CPU line
# Detailed softirq breakdown
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3
# HI: 0 0 0 0
# TIMER: 12345678 12345678 12345678 12345678
# NET_TX: 1234 5678 9012 3456
# NET_RX: 567890123 567890123 567890123 567890123 <- Network heavy
# BLOCK: 123456 123456 123456 123456
# IRQ_POLL: 0 0 0 0
# TASKLET: 0 0 0 0
# SCHED: 1234567 1234567 1234567 1234567
# HRTIMER: 0 0 0 0
# RCU: 12345678 12345678 12345678 12345678
# Watch rate of change
watch -d 'cat /proc/softirqs'
# If NET_RX dominates: network packet storm
# If TIMER dominates: too many timers (check application)
# If RCU dominates: RCU callback backlogDiagnosing NET_RX softirq storms:
# Check softnet stats
cat /proc/net/softnet_stat
# Column meanings (per CPU):
# 1: packets processed
# 2: packets dropped (buffer full)
# 3: time_squeeze (ran out of budget)
# Columns 2,3 non-zero = problem
# Column 2: increase buffer
# Column 3: increase softirq budget
# Increase softirq budget
sysctl net.core.netdev_budget=600 # Default 300
sysctl net.core.netdev_budget_usecs=4000 # Default 2000
# Increase receive buffer
sysctl net.core.netdev_max_backlog=10000 # Default 1000RCU callback storms:
# Check RCU callback queue
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# Look for high cblist length
# If using nohz_full, ensure rcu_nocbs is set
cat /proc/cmdline | grep rcu_nocbs
# Monitor RCU grace periods
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata# View current coalescing settings
ethtool -c eth0
# rx-usecs: 50
# rx-frames: 0
# rx-usecs-irq: 0
# tx-usecs: 50
# Disable coalescing for lowest latency
ethtool -C eth0 rx-usecs 0 rx-frames 1
ethtool -C eth0 tx-usecs 0 tx-frames 1
# Enable adaptive coalescing (driver adjusts automatically)
ethtool -C eth0 adaptive-rx on adaptive-tx on
# High throughput settings (batching)
ethtool -C eth0 rx-usecs 100 rx-frames 64
ethtool -C eth0 tx-usecs 100 tx-frames 64
# Check for packet drops indicating too much coalescing
ethtool -S eth0 | grep -i dropCoalescing decision matrix:
| Workload | rx-usecs | rx-frames | Notes |
|---|---|---|---|
| Low latency trading | 0 | 1 | Every packet fires IRQ |
| Interactive (web) | 20-50 | 8-16 | Balance |
| Bulk transfer | 100-250 | 64-128 | Batch for throughput |
| Adaptive | adaptive-rx on | - | Driver decides |
Latency impact:
rx-usecs=0: ~5-10us interrupt latencyrx-usecs=50: up to 50us delayrx-usecs=250: up to 250us delay
Warning: Low coalescing = more interrupts = higher CPU usage = potential DoS vulnerability.
# /proc/interrupts format:
# IRQ# CPU0 CPU1 ... Type Source
# 0: 123 0 IO-APIC 2-edge timer
# 27: 123456 234567 PCI-MSI 524288-edge eth0-TxRx-0
# Interrupt types:
# IO-APIC-edge: Edge-triggered, single signal
# IO-APIC-level: Level-triggered, sustained signal
# PCI-MSI: Message Signaled Interrupts (modern, preferred)
# IR-PCI-MSI: Interrupt Remapping MSI (with IOMMU)
# Red flags in /proc/interrupts:
# 1. All interrupts on CPU0 (irqbalance not running)
# 2. One NIC queue handling 10x more than others
# 3. Extremely high interrupt rate (>100K/s per CPU)
# Analyze interrupt rate
while true; do
cat /proc/interrupts | grep eth0 >> /tmp/irq.log
sleep 1
done
# Then: awk to calculate delta per second
# Or use sar
sar -I ALL 1 10
# /proc/softirqs analysis
# Expected pattern: relatively even distribution across CPUs
# Red flags:
# 1. One CPU handling 90%+ of NET_RX (affinity problem)
# 2. NET_RX >> NET_TX (asymmetric load or receive-heavy)
# 3. RCU counts vastly different across CPUs
# Quick health check
awk 'NR==1 || /NET_RX|NET_TX|TIMER/' /proc/softirqsInterrupt to CPU mapping best practices:
# 1. Pin NIC IRQs to same NUMA node as NIC
# 2. Pin application to same NUMA node
# 3. Keep housekeeping on CPU 0 (or dedicated housekeeping CPUs)
# 4. For low-latency: isolate CPUs from IRQs entirely
# Example: 8-core dual-socket
# Node 0: CPU 0-3, Node 1: CPU 4-7
# NIC on Node 0
# Strategy:
# CPU 0: housekeeping, irqbalance
# CPU 1-3: NIC IRQs + application
# CPU 4-7: isolated for latency-critical threads# Kernel boot parameters (GRUB_CMDLINE_LINUX)
# Basic isolation:
isolcpus=2-7
# Full isolation (recommended combination):
isolcpus=managed_irq,domain,2-7 nohz_full=2-7 rcu_nocbs=2-7
# Complete low-latency setup:
isolcpus=managed_irq,domain,2-7 nohz_full=2-7 rcu_nocbs=2-7 \
irqaffinity=0-1 rcu_nocb_poll skew_tick=1 \
intel_pstate=disable nosoftlockup
# Parameter breakdown:
# isolcpus=managed_irq,domain,2-7
# managed_irq: kernel manages IRQ affinity (keeps off isolated)
# domain: remove from scheduler load balancing domains
# 2-7: CPU list to isolate
# nohz_full=2-7
# Disable scheduler tick when only one runnable task
# Requires: CONFIG_NO_HZ_FULL=y
# rcu_nocbs=2-7
# Offload RCU callbacks to kthreads on non-isolated CPUs
# Automatically implied by nohz_full on recent kernels
# Apply changes
update-grub && rebootAdditional isolation parameters:
# irqaffinity=0-1
# Default IRQ affinity (housekeeping CPUs only)
# rcu_nocb_poll
# RCU callback kthreads poll instead of waking (reduces IPI)
# skew_tick=1
# Offset timer ticks across CPUs (reduces lock contention)
# intel_pstate=disable
# Use acpi-cpufreq (more deterministic than intel_pstate)
# nosoftlockup
# Disable soft lockup detector (fires on isolated CPUs)
# nmi_watchdog=0
# Disable NMI watchdog (periodic interrupt)# Pin to isolated CPUs using taskset
taskset -c 2-7 ./latency_critical_app
# Pin with cgroups (cpuset)
mkdir /sys/fs/cgroup/cpuset/realtime
echo 2-7 > /sys/fs/cgroup/cpuset/realtime/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/realtime/cpuset.mems
echo $$ > /sys/fs/cgroup/cpuset/realtime/tasks
./latency_critical_app
# Pin with systemd
# /etc/systemd/system/myapp.service
[Service]
CPUAffinity=2 3 4 5 6 7
ExecStart=/usr/bin/myapp
# Docker
docker run --cpuset-cpus="2-7" myimage
# Kubernetes (requires CPU manager)
# kubelet: --cpu-manager-policy=static
resources:
requests:
cpu: "4"
limits:
cpu: "4"
# Pod gets dedicated CPUs if Guaranteed QoS
# Verify pinning
taskset -p <pid>
# pid XXXXX's current affinity mask: fc (binary: 11111100 = CPUs 2-7)Thread-level pinning (for multi-threaded apps):
# Find all threads
ls /proc/<pid>/task/
# Pin each thread
for tid in /proc/<pid>/task/*; do
taskset -p -c 2 $(basename $tid)
done
# Or in application code:
# pthread_setaffinity_np() in C
# os.sched_setaffinity() in Python# 1. Check isolcpus effective
cat /sys/devices/system/cpu/isolated
# 2-7
# 2. Verify scheduler domains
cat /proc/schedstat | head -20
# Isolated CPUs should show minimal activity
# 3. Check no unexpected processes on isolated CPUs
ps -eo pid,comm,psr | awk '$3 >= 2 && $3 <= 7 {print}'
# Should only show your pinned processes
# 4. Monitor timer interrupts on isolated CPUs
watch -d 'cat /proc/interrupts | grep -E "LOC|RES"'
# LOC (Local timer) should be nearly static on isolated CPUs
# Expect ~1 tick every 1-2 seconds with nohz_full
# 5. Trace interrupts hitting isolated CPUs
perf record -e irq:irq_handler_entry -C 2-7 -- sleep 10
perf report
# Should be minimal
# 6. BPF tracing for interrupt latency
cat > /tmp/trace.bt << 'EOF'
tracepoint:irq:irq_handler_entry /cpu >= 2 && cpu <= 7/ {
@[cpu, args->name] = count();
}
EOF
bpftrace /tmp/trace.bt
# 7. Measure actual jitter
cyclictest -m -p 80 -t 1 -a 2 -i 1000 -l 10000
# -m: mlockall
# -p 80: priority 80 (SCHED_FIFO)
# -t 1: 1 thread
# -a 2: pin to CPU 2
# -i 1000: 1ms interval
# -l 10000: 10000 loops
# Good result: Max latency < 50us
# Acceptable: Max latency < 100us
# Problem: Max latency > 1msCommon verification failures:
# Problem: Still seeing timer ticks
# Check: Only ONE runnable task per isolated CPU
ps -eo psr,comm | sort | uniq -c | sort -rn
# Problem: RCU callbacks still running
# Check: rcu_nocbs parameter effective
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# Isolated CPUs should show "nocb" flag
# Problem: Kernel threads on isolated CPUs
# Fix: kthread_cpus=0-1 boot parameter
cat /proc/cmdline | grep kthread_cpus
# Problem: IRQs hitting isolated CPUs
# Check: /proc/irq/*/smp_affinity
for irq in /proc/irq/*/smp_affinity; do
mask=$(cat $irq 2>/dev/null)
# Convert to binary, check if bits 2-7 set
done# Designate CPUs 0-1 as housekeeping
# Boot parameters:
GRUB_CMDLINE_LINUX="isolcpus=managed_irq,domain,2-7 \
nohz_full=2-7 rcu_nocbs=2-7 irqaffinity=0-1 \
kthread_cpus=0-1"
# Move kernel threads to housekeeping CPUs
for pid in $(pgrep -f '\[.*\]'); do
taskset -p -c 0-1 $pid 2>/dev/null
done
# Move irqbalance to housekeeping
taskset -c 0-1 irqbalance
# Move all IRQs to housekeeping
for irq in /proc/irq/*/smp_affinity; do
echo 3 > $irq 2>/dev/null # 0x3 = CPUs 0,1
done
# Systemd: restrict system slice
mkdir -p /etc/systemd/system/system.slice.d/
cat > /etc/systemd/system/system.slice.d/cpuset.conf << 'EOF'
[Slice]
AllowedCPUs=0-1
EOF
systemctl daemon-reload
# Verify nothing unexpected on isolated CPUs
watch 'ps -eo pid,psr,comm | awk "\$2 >= 2"'Housekeeping workload placement:
| Task | CPUs | Notes |
|---|---|---|
| systemd, logging | 0-1 | System services |
| irqbalance | 0-1 | IRQ distribution |
| sshd, monitoring | 0-1 | Management traffic |
| Kernel threads | 0-1 | kworker, ksoftirqd |
| Network IRQs | 0-1 | Unless app processes packets |
| Application | 2-7 | Isolated, pinned |
# Definition:
# Voluntary: Task blocks (I/O, lock, sleep)
# Involuntary: Task preempted (time slice expired, higher priority)
# Per-process monitoring
pidstat -w 1 5
# PID cswch/s nvcswch/s Command
# 1234 150.00 50.00 myapp
# 5678 20.00 200.00 cpu_intensive
# Process lifetime totals
cat /proc/<pid>/status | grep ctxt
# voluntary_ctxt_switches: 12345
# nonvoluntary_ctxt_switches: 6789
# perf stat for specific workload
perf stat -e context-switches ./myapp
# Or detailed:
perf stat -e 'sched:sched_switch' ./myapp
# Ratio interpretation:
# High voluntary, low involuntary = I/O bound (normal)
# Low voluntary, high involuntary = CPU bound, contention
# Both high = thread thrashing or over-synchronizationPattern recognition:
Pattern 1: High voluntary, low involuntary
cswch/s: 5000 nvcswch/s: 50
Diagnosis: I/O bound or lock contention
Action: Profile I/O, check lock patterns
Pattern 2: Low voluntary, high involuntary
cswch/s: 100 nvcswch/s: 2000
Diagnosis: CPU bound, too many threads
Action: Reduce thread count, increase priority
Pattern 3: Both extremely high
cswch/s: 10000 nvcswch/s: 10000
Diagnosis: Thread thrashing, over-synchronization
Action: Thread pool, batch work, reduce sync points
Pattern 4: Both low
cswch/s: 10 nvcswch/s: 5
Diagnosis: Healthy, efficient execution
Action: None needed
Reducing unnecessary context switches:
# 1. Thread pools instead of spawn-per-task
# 2. Batch small I/O operations
# 3. Use async I/O (io_uring, epoll)
# 4. Reduce lock granularity
# 5. Use lock-free structures where possible
# 6. Pin threads to reduce cache invalidation# Measure migrations
perf stat -e cpu-migrations ./myapp
# Watch migrations in real-time
perf sched record -a -- sleep 5
perf sched timehist -M
# Per-process migration count
cat /proc/<pid>/sched | grep nr_migrations
# Migration cost by type:
# Same core (HT): ~free (shared L1/L2)
# Same socket, different core: 50-100ns (L3 hit)
# Different socket: 100-300ns + potential cache flush
# NUMA migration impact
numastat -p <pid>
# numa_miss: pages accessed from remote node
# numa_foreign: pages intended for this node, allocated elsewhereMeasuring actual migration cost:
# BPF: track migration latency
cat > /tmp/migrate.bt << 'EOF'
tracepoint:sched:sched_migrate_task {
@migrations[args->pid] = count();
@from_to[args->orig_cpu, args->dest_cpu] = count();
}
END {
print(@migrations);
print(@from_to);
}
EOF
bpftrace /tmp/migrate.bt
# Memory bandwidth test with/without migration
# Without migration (pinned):
numactl --cpunodebind=0 --membind=0 ./membw_test
# With migration (unpinned):
./membw_test
# Compare throughputCross-NUMA migration red flags:
# High remote memory access
perf stat -e node-loads,node-stores,node-load-misses ./myapp
# node-load-misses >> 0 indicates cross-node access
# Memory bandwidth saturation
perf stat -e LLC-load-misses,LLC-store-misses ./myapp
# Process moving between NUMA nodes
watch -n 1 'cat /proc/<pid>/sched | grep -E "nr_migrations|numa_faults"'When to pin:
# Pin when:
# 1. Latency-critical and need cache warmth
# 2. NUMA-aware application with local memory
# 3. Real-time requirements
# 4. Application understands its memory layout
# Example: Database with NUMA-aware memory allocator
numactl --cpunodebind=0 --membind=0 ./database
# Example: Network application pinned to NIC's NUMA node
nic_node=$(cat /sys/class/net/eth0/device/numa_node)
numactl --cpunodebind=$nic_node ./network_appWhen NOT to pin:
# Don't pin when:
# 1. Many short-lived processes (let scheduler load balance)
# 2. Unknown/variable workload characteristics
# 3. Oversubscribed system (more threads than cores)
# 4. Application already NUMA-optimized internally
# The scheduler is usually smarter:
# - Tracks cache hotness
# - Balances load automatically
# - Adapts to changing conditionsHybrid approach:
# Soft affinity: prefer but allow migration
# Linux lacks native soft affinity, but:
# 1. Use cpuset with multiple CPUs
mkdir /sys/fs/cgroup/cpuset/myapp
echo 0-3 > /sys/fs/cgroup/cpuset/myapp/cpuset.cpus
# Process can migrate within 0-3, but not outside
# 2. NUMA-aware allocation without CPU pinning
numactl --preferred=0 ./myapp
# Prefer node 0 memory, but allow CPU migration
# 3. Use scheduler hints
# SCHED_BATCH: hint that task is CPU-bound
chrt -b 0 ./myapp
# Scheduler will try to minimize migrationsMonitoring pinning effectiveness:
# Track if pinned process stays put
while true; do
psr=$(ps -o psr= -p <pid>)
echo "$(date +%s) CPU: $psr"
sleep 0.1
done > /tmp/cpu_track.log
# Analyze
sort /tmp/cpu_track.log | uniq -c
# Should show single CPU if properly pinned
# Check for involuntary migration despite pinning
# (indicates pinning not working)
cat /proc/<pid>/status | grep nonvoluntaryvmstat 1 # r column > nproc = saturated
mpstat -P ALL 1 # Per-CPU utilization
uptime # Load average / nproc
runqlat 10 1 # Run queue latency distribution (BPF)
perf sched latency # Per-task scheduler delaysvmstat 1 # cs column (system-wide)
pidstat -w 1 # Per-process cswch/nvcswch
perf stat -e context-switches # Specific workload
cat /proc/<pid>/status # Lifetime totalscat /proc/interrupts # IRQ distribution
cat /proc/softirqs # Softirq counts
watch -d 'cat /proc/net/softnet_stat' # Network softnet
sar -I ALL 1 # Interrupt rate
mpstat -P ALL 1 # %irq, %soft columnsnumactl --hardware # Topology
numastat -p <pid> # Per-process NUMA stats
perf stat -e node-loads # Cross-node memory
cat /sys/class/net/*/device/numa_node # NIC NUMA affinitycat /sys/devices/system/cpu/isolated # Isolated CPUs
cyclictest -m -p 80 -t 1 -a <cpu> # Latency test
cat /proc/interrupts | grep LOC # Timer interrupts
ps -eo pid,psr,comm # Process placementIs CPU saturated? (vmstat r > nproc)
├─ Yes → Scale out or optimize
└─ No
├─ Check context switches (pidstat -w)
│ ├─ High involuntary → Too many threads, reduce or pin
│ └─ High voluntary → I/O or lock contention
├─ Check scheduler latency (perf sched latency)
│ └─ Max delay > 10ms → Priority/isolation issue
├─ Check interrupt distribution (/proc/interrupts)
│ └─ Unbalanced → Configure IRQ affinity
└─ Check CFS throttling (cpu.stat)
└─ throttled > 0 → Increase quota or period
Latency requirement < 100us?
├─ Yes → Full isolation: isolcpus + nohz_full + rcu_nocbs
└─ No
├─ Latency requirement < 1ms?
│ ├─ Yes → isolcpus + careful IRQ placement
│ └─ No → Standard pinning (taskset/cgroups)
└─ Interactive workload?
├─ Yes → EEVDF default is usually fine
└─ No → SCHED_BATCH for throughput
Hard real-time with known WCET?
├─ Yes → SCHED_DEADLINE
└─ No
├─ Soft real-time, need deterministic latency?
│ ├─ Yes → SCHED_FIFO (single path) or SCHED_RR (multiple)
│ └─ No → Continue
├─ Interactive/mixed workload?
│ ├─ Yes → SCHED_NORMAL (default EEVDF)
│ └─ No → Continue
├─ CPU-intensive batch job?
│ ├─ Yes → SCHED_BATCH
│ └─ No → Continue
└─ Background maintenance?
└─ Yes → SCHED_IDLE
| Metric | Healthy | Warning | Critical |
|---|---|---|---|
| Run queue (r/nproc) | < 1 | 1-2 | > 2 |
| Load average (/nproc) | < 0.7 | 0.7-1.5 | > 2 |
| Context switches/s | < 10K | 10K-50K | > 100K |
| Scheduler delay (max) | < 1ms | 1-10ms | > 100ms |
| CFS throttle ratio | 0% | 1-10% | > 25% |
| Softirq %CPU | < 5% | 5-20% | > 30% |
| Cross-NUMA migrations | < 100/s | 100-1000/s | > 1000/s |
| Timer interrupts (isolated CPU) | ~1/s | 10/s | > 100/s |
- EEVDF Scheduler - Kernel Docs
- Completing the EEVDF scheduler - LWN
- CFS Bandwidth Control - Kernel Docs
- The container throttling problem - Dan Luu
- perf sched for Linux CPU scheduler analysis - Brendan Gregg
- Linux Run Queue Latency - Brendan Gregg
- Low Latency Tuning Guide - Erik Rigtorp
- CPU Isolation with isolcpus, nohz_full, rcu_nocbs - Red Hat
- IRQ Affinity and Network Performance - Red Hat
- Monitoring and Tuning the Linux Networking Stack - Packagecloud
- SCHED_DEADLINE - Kernel Docs
- sched_ext Schedulers - GitHub
- Interrupt Coalescing - Jakub Kicinski
- Understanding Interrupts and Softirqs - Netdata
- eBPF & Tracing - runqlat, sched_ext custom schedulers, CPU tracing
- Containers & K8s - CFS bandwidth throttling detection, container CPU limits
- Kernel Tuning - EEVDF tuning, sched_* sysctls, CPU governor
- Memory Subsystem - NUMA memory placement, migration costs
- Latency Analysis - Scheduling latency impact on tail latency