What Is CPU Affinity in Linux? – Linux Device Drivers Course Online

CPU Affinity in Linux: Querying and Setting the CPU Affinity Mask

Linux Kernel Programming Course | CPU Scheduler Series – Part 2 | Lecture 2

In this lecture of our free Linux kernel programming course, we study CPU affinity in Linux — the mechanism that decides which CPU cores a thread is allowed to run on. By default the scheduler is free to place a runnable thread on any online core. That freedom is great for throughput, but embedded and real-time engineers often need control: pinning an interrupt-handling thread to one core, reserving a core for a latency-critical task, or keeping cache-hot data local. Mastering the CPU affinity mask gives you exactly that control, and it is a skill you will use constantly in any free linux device drivers course project and in professional embedded systems work.

What You Will Learn

  • What the CPU affinity mask is and how the kernel stores it per thread
  • Querying and setting affinity from the shell with taskset
  • Setting CPU affinity in Linux programmatically with sched_setaffinity()
  • The CPU_SET macro family and a complete working C example
  • CPU isolation with isolcpus, nohz_full and the cgroup v2 cpuset controller
  • Real-world affinity use cases, mistakes and best practices

Prerequisites

Lecture 1 of this series (scheduler tracing), basic C programming, and a multi-core Linux system. Learners from our free embedded systems course can follow along on a Raspberry Pi or any multi-core development board.

What Exactly Is the CPU Affinity Mask?

Every thread in Linux carries a per-thread attribute: a bitmask in which bit N set to 1 means “this thread may run on CPU N“. The scheduler consults this mask on every placement decision — a thread is never migrated to a core whose bit is 0. A child process inherits its parent’s mask across fork() and it is preserved across execve().

Affinity Mask Example on a 4-Core System (mask = 0x6)
CPU 3 CPU 2 CPU 1 CPU 0
0
not allowed
1
allowed
1
allowed
0
not allowed

Binary 0110 = hex 0x6 → the thread may run only on CPU 1 and CPU 2

Querying and Setting Affinity from the Shell: taskset

The taskset utility (util-linux package) is the quickest way to work with affinity. Query a running process by PID:

taskset -p 1234
# pid 1234's current affinity mask: f

# Human-readable CPU list instead of hex:
taskset -cp 1234
# pid 1234's current affinity list: 0-3

Launch a program restricted to specific cores:

# Run on CPUs 1 and 2 only
taskset -c 1,2 ./my_worker

# Change affinity of an already-running process to CPU 3
sudo taskset -cp 3 1234

You can also read the mask directly from procfs — useful on minimal embedded rootfs images that lack taskset:

grep Cpus_allowed_list /proc/1234/status
# Cpus_allowed_list:   0-3

Setting CPU Affinity Programmatically: sched_setaffinity()

For production code you set affinity from inside the program using the Linux-specific system calls sched_setaffinity() and sched_getaffinity(), together with the cpu_set_t type and its helper macros:

Macro Purpose
CPU_ZERO(&set) Clear the set (start empty)
CPU_SET(n, &set) Add CPU n to the set
CPU_CLR(n, &set) Remove CPU n from the set
CPU_ISSET(n, &set) Test whether CPU n is in the set
CPU_COUNT(&set) Number of CPUs in the set

Complete working example

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    cpu_set_t mask;

    /* Build a mask allowing only CPU 1 and CPU 2 */
    CPU_ZERO(&mask);
    CPU_SET(1, &mask);
    CPU_SET(2, &mask);

    /* pid 0 means "the calling thread" */
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
        perror("sched_setaffinity");
        exit(EXIT_FAILURE);
    }

    /* Verify by reading it back */
    CPU_ZERO(&mask);
    if (sched_getaffinity(0, sizeof(mask), &mask) == -1) {
        perror("sched_getaffinity");
        exit(EXIT_FAILURE);
    }

    printf("Thread now allowed on %d CPU(s): ", CPU_COUNT(&mask));
    for (int i = 0; i < CPU_SETSIZE; i++)
        if (CPU_ISSET(i, &mask))
            printf("%d ", i);
    printf("\nCurrently running on CPU %d\n", sched_getcpu());

    return 0;
}

Compile and run:

gcc -O2 -Wall affinity_demo.c -o affinity_demo
./affinity_demo
# Thread now allowed on 2 CPU(s): 1 2
# Currently running on CPU 1

Permissions: a thread can always restrict its own affinity. Changing another process’s affinity requires the same UID or the CAP_SYS_NICE capability. In multithreaded programs, prefer pthread_setaffinity_np() which targets a specific pthread rather than a kernel TID.

Beyond Affinity: True CPU Isolation

Pinning your thread to CPU 3 does not stop other tasks from also running on CPU 3. For hard latency requirements you must additionally keep everything else off that core:

1. Boot-time isolation

# Kernel command line (e.g. in GRUB or bootargs)
isolcpus=nohz,domain,3 nohz_full=3 rcu_nocbs=3

This removes CPU 3 from the general scheduler domains (no load balancing onto it), enables tickless operation on it, and offloads RCU callbacks. Only tasks you explicitly pin there will run there.

2. Runtime partitioning with cgroup v2 cpuset

# Create a partition owning CPU 3 exclusively (cgroup v2)
sudo mkdir /sys/fs/cgroup/rt_partition
echo 3 | sudo tee /sys/fs/cgroup/rt_partition/cpuset.cpus
echo root | sudo tee /sys/fs/cgroup/rt_partition/cpuset.cpus.partition

# Move your latency-critical task into it
echo $TASK_PID | sudo tee /sys/fs/cgroup/rt_partition/cgroup.procs

The modern cpuset controller in cgroup v2 replaces the legacy cpuset filesystem you may see in older tutorials — always use the v2 interface on current kernels (6.x), as it is the default on all recent distributions.

Real-World Use Cases

  • Network packet processing: pin the NIC interrupt and the polling thread to the same core to keep packet data cache-hot.
  • Industrial control on embedded Linux: isolate one core for the control loop; leave the rest for UI and logging.
  • Benchmarking: pin the benchmark to a fixed core so results are not distorted by migrations.
  • Asymmetric (big.LITTLE) ARM systems: steer heavy threads to performance cores and background work to efficiency cores.

Common Mistakes and Troubleshooting

  • EINVAL from sched_setaffinity: the mask contains no online CPU, or intersects an isolated/offline set. Check /sys/devices/system/cpu/online.
  • Pinning everything to CPU 0: a classic mistake that serializes your whole application. Pin only what needs pinning.
  • Assuming affinity means exclusivity: it does not — combine with isolation techniques above when you need a private core.
  • Hardcoding core numbers: query core counts and topology at runtime; the same binary may run on 2-core and 16-core targets.

Performance Considerations and Best Practices

  • Cache locality is the main win: a thread staying on one core reuses warm L1/L2 caches; migration throws that away.
  • On NUMA servers, pair CPU affinity with memory policy so a thread’s memory lives on the local node.
  • Over-constraining hurts: the fewer cores a thread may use, the fewer chances the scheduler has to run it promptly. Measure with the tracing tools from Lecture 1 before and after.
  • Document every pinning decision — affinity choices that made sense on one hardware generation can degrade the next.

Key Takeaways

  • The CPU affinity mask is a per-thread bitmask of allowed CPUs, inherited across fork and exec.
  • taskset handles shell-level work; sched_setaffinity() / pthread_setaffinity_np() handle programmatic control.
  • Affinity restricts your thread; isolation (isolcpus, cgroup v2 cpuset partitions) restricts everyone else.
  • Always verify the effect with tracing rather than assuming a performance win.

Conclusion

You can now inspect and control exactly where every thread in your system executes — from a one-line taskset command to a fully isolated real-time core. This is a foundational skill in this free Linux kernel development course and across our free embedded systems course content, because CPU placement decisions directly shape latency and throughput. In the next lecture we control how urgently a thread runs, by querying and setting scheduling policy and priority.

Frequently Asked Questions (FAQ)

1. Does CPU affinity survive across fork() and exec()?

Yes. Children inherit the parent’s mask and it persists across execve(). Set the mask before forking workers to affect the whole pool.

2. What is the difference between taskset and sched_setaffinity()?

taskset is a command-line wrapper around the same system call. Use taskset for operations and quick tests; use the syscall or the pthread wrapper inside applications.

3. Can I set affinity for a single thread instead of a whole process?

Yes. Affinity is per-thread in Linux. Pass a specific TID to sched_setaffinity(), or better, use pthread_setaffinity_np() on the pthread handle.

4. Does pinning a thread to a core guarantee low latency?

No. Other tasks and interrupts can still use that core. Combine affinity with isolcpus/nohz_full or a cgroup v2 cpuset partition, plus a real-time scheduling policy (next lecture).

5. How do I see which core a thread is on right now?

Programmatically with sched_getcpu(); interactively, press f in top and enable the P (last-used CPU) column, or check /proc/PID/stat field 39.

6. Is CPU affinity relevant on single-core embedded systems?

The mask exists but has no effect with one core. The isolation and priority techniques in this free linux kernel programming course still matter there.

7. What happens if I hot-unplug a CPU that a thread is pinned to?

The kernel forcibly moves the thread to a remaining online CPU; your carefully chosen placement is lost. Re-apply affinity after hotplug events.

Continue Your Free Linux Kernel Development Course

Next up: querying and setting a thread’s scheduling policy and priority.

← Previous Lecture
Next Lecture →

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *