taskset: Setting CPU Affinity in Linux
What You'll Learn
- How to pin a process to specific CPU cores with
taskset - The difference between launch-time pinning (
-c) and reassigning a running process (-p) - Practical patterns for benchmarking, core isolation, and NUMA
Quick Summary
- Pin at launch →
taskset -c 0,1 ./program - Pin a running process →
taskset -cp 0-3 <PID> - Check current state →
taskset -cp <PID> - The list form (
-c) is readable; the hex bitmask suits automation
Prerequisites
tasksetships with theutil-linuxpackage (preinstalled on most distros)- Core numbers start at
0. Count them withnproc - Changing a running process needs ownership of that process (or
root)
What is CPU affinity?
Conclusion: CPU affinity restricts which CPU cores a process or thread is allowed to run on.
tasksetis the command to view and change that setting.
By default the Linux scheduler is free to move a process to any idle core. Setting CPU affinity means the process can only run on the cores you specify.
The main reasons to pin are:
- Cache locality: avoid L1/L2 cache invalidation caused by core migration
- Core isolation: give a latency-sensitive process a dedicated core, free of interference
- Reproducible benchmarks: run on the same cores every time to reduce measurement noise
Affinity specifies a set of allowed cores. Narrow it to one core for hard pinning, or allow several and let the scheduler pick within that set.
Check core count and numbering
Conclusion: Before deciding where to pin, confirm the available core count with
nproc. Core numbers start at0.
$ nproc
8
If it prints 8, valid core numbers are 0–7. Specifying a nonexistent core like taskset -c 8 ... returns an error.
Use lscpu when you also need the physical/logical layout.
$ lscpu
Pin cores at launch (-c)
Conclusion: To pin as you start a command, use
taskset -c <core-list> <command>. The list form accepts0,2(individual) and0-3(range).
The basic form: start a new process pinned to specific cores from the very beginning.
# Launch program on cores 0 and 1 only $ taskset -c 0,1 ./program # Launch on the range of cores 0 through 3 $ taskset -c 0-3 ./program # Hard-pin to a single core (core 2) $ taskset -c 2 ./program
-c (--cpu-list) lets you list core numbers directly, so it is easy to read. Use , for individual cores, - for ranges, and mix both (for example 0,2,4-7).
Pass options to the launched command by appending them as usual.
taskset -c 0,1 stress-ng --cpu 2 works with no -- separator needed.
View and change a running process (-p)
Conclusion: For an already-running process, use
-p(--pid):taskset -cp <PID>to check,taskset -cp <core-list> <PID>to change.
Check the current affinity
$ taskset -cp 1234
pid 1234's current affinity list: 0-7
0-7 means "may run on all cores" (the default). With -c you get the list form; without it, a hex mask.
Reassign a running process
# Reassign PID 1234 to cores 0 and 1 $ taskset -cp 0,1 1234
pid 1234's current affinity list: 0-7 pid 1234's new affinity list: 0,1
Find the PID with ps or pgrep.
$ pgrep -f program
Mind the argument order
In -p mode the core list comes first, the PID second: taskset -cp 0,1 1234, not taskset -cp 1234 0,1. Reversing them makes taskset treat the PID as a mask and fail.
Hex bitmask vs list form
Conclusion: With
-cyou specify a core list; without it, a hex bitmask. In the mask, each bit maps to one core (bit 0 = core 0).
Natively, taskset specifies cores as a hexadecimal bitmask. -c is the option that swaps that for a human-readable list.
| Target cores | List form (-c) | Hex mask |
|---|---|---|
| Core 0 | 0 |
0x1 |
| Cores 0,1 | 0,1 |
0x3 |
| Core 1 only | 1 |
0x2 |
| Cores 0–3 | 0-3 |
0xf |
| Core 3 only | 3 |
0x8 |
The mask is easiest to read in binary. The least significant bit (rightmost) is core 0. 0x3 is 0b0011 (cores 0 and 1); 0x8 is 0b1000 (core 3).
# Launch with a mask (cores 0,1 = 0x3) $ taskset 0x3 ./program # Check with a mask (no -c) $ taskset -p 1234
pid 1234's current affinity mask: ff
For hand-typed commands the list form (-c) is safer. A single wrong hex digit points at a different core. Reach for the mask only when a script generates it.
Apply to all threads (-a)
Conclusion: To pin every thread of a multithreaded process, add
-a(--all-tasks). Without it, only the main thread is affected.
When operating on a running process with -p, by default affinity applies only to the given PID (the main thread) and does not propagate to existing child threads. Use -a to pin all threads in the process at once.
# Pin all threads of PID 1234 to cores 0-3 $ taskset -acp 0-3 1234
Even with -a, threads created after the change inherit the process affinity. -a applies to existing threads; future threads inherit anyway. Do not conflate the two.
Real-world use cases
Conclusion: The classic cases are reproducible benchmarking, isolating latency-sensitive processes, and keeping memory access local on NUMA.
Make benchmarks reproducible
Measuring on the same cores every time removes the cache-miss variance caused by core migration.
$ taskset -c 2,3 ./benchmark
Isolate latency-sensitive processes
Combine with the kernel boot parameter isolcpus, which removes cores from the OS scheduler, then place only your target process on them with taskset.
Keep memory local on NUMA
On NUMA systems, pinning to cores on the same node as the memory avoids remote memory access. When you also need to control memory allocation, numactl fits better (taskset only handles CPU placement).
The over-pinning trap
Pin too aggressively and a process cannot use other idle cores, which can make it slower. Pin only after measuring and confirming the benefit. Do not pin blindly.
Common errors and fixes
Conclusion: Most are a nonexistent core number, insufficient permissions, or wrong argument order.
sched_setaffinity: Invalid argument
You specified a core number that does not exist. Check the range with nproc.
# Only 8 cores (0-7) exist but core 8 was requested → error $ taskset -c 8 ./program
Operation not permitted
You are trying to change another user's process. Run as the process owner or use sudo.
$ sudo taskset -cp 0,1 1234
command not found: taskset
util-linux is missing (for example a minimal container). On Debian/Ubuntu, install it:
$ sudo apt install util-linux