skip to content

You pin a latency-sensitive Linux process to CPUs 2 and 3 with taskset. Does that reserve those CPUs for it, and what happens to the affinity when the process forks or calls exec?

level: middleimportance: should knowfreq 35%

answer

  1. it constrains one side only
  2. the CPUs are not yours afterwards
  3. other tasks never hear about your mask
  4. children start where the parent was allowed
  5. the mask outlives the program image

basics

~20 s

Setting CPU affinity restricts where a task may run; it does not reserve anything. Every other task on the system can still be scheduled onto CPUs 2 and 3. Children inherit the mask across fork, and it survives exec.

solid answer

~50 s

CPU affinity is a **restriction on the task, not a reservation of the CPUs**. `taskset -c 2,3` sets the affinity mask via `sched_setaffinity(2)`, which tells the scheduler this task may only be placed on CPUs 2 and 3 — it says nothing to any other task, so the rest of the system keeps being scheduled there, along with interrupt handling and kernel work. If you actually want those CPUs dedicated, you have to exclude everyone else: a cpuset that confines other tasks, the `isolcpus=` boot parameter to keep the general scheduler off them, and steering device interrupts elsewhere. On inheritance, the mask propagates: a child created by `fork(2)` inherits its parent's affinity mask, and the mask is preserved across `execve(2)`. That is why pinning a wrapper script silently pins everything it launches, which is sometimes exactly what you wanted and sometimes a surprise nobody documented.

code

bash · 8 lines
bash
# confine a process to CPUs 2 and 3 — and everything it spawns
taskset -c 2,3 ./latency-sensitive-server

# read back the mask of a running process
taskset -c -p 4242

# widen it again at runtime
taskset -c -p 0-7 4242

go deeper

for a junior

Know that CPU affinity lists which CPUs a task is allowed to run on, that taskset sets and reads it, and that it is a restriction rather than a guarantee.

for a middle

Explain that the mask constrains only the pinned task while everything else still runs there, and state the inheritance rules: children inherit across fork and the mask survives exec.

for a senior

Show what real dedication requires — excluding other tasks via cpusets or isolcpus, steering interrupt affinity, and reducing tick jitter — and treat an unexpectedly narrow inherited mask as a live diagnostic hypothesis.

for a principal

Own the tradeoff at fleet scale: hardcoded CPU numbers do not survive heterogeneous hardware, and partitioning cores costs utilisation that must be justified by a measured latency requirement.

## What the mask actually says Every task on Linux carries a **CPU affinity mask**: the set of logical CPUs the scheduler is permitted to place it on. By default the mask contains every online CPU, so the scheduler is free to run the task anywhere and to migrate it when load changes. ```bash taskset -c 2,3 ./latency-sensitive-server # launch confined to CPUs 2 and 3 taskset -c -p 4242 # show the mask of a running PID taskset -c -p 0-3 4242 # change it on a running PID ``` Underneath, this is `sched_setaffinity(2)` / `sched_getaffinity(2)`. The kernel enforces it as a hard constraint: the task will *never* be run on a CPU outside its mask, even if every CPU in the mask is saturated and the rest of the machine is idle. That is the first thing people get wrong in the other direction — pinning can make a task slower, because it forfeits the scheduler's ability to move it to a free CPU. ## Restriction, not reservation The crucial asymmetry: the mask is a property of *this* task. Nothing about it tells the scheduler to keep other work off CPUs 2 and 3. Everything else on the box — with a default all-CPUs mask — is still eligible to run there, and will, whenever the load balancer decides those CPUs look inviting. On top of that, device interrupts, softirqs and kernel worker threads land on CPUs according to their own affinity settings, entirely unaffected by yours. So `taskset -c 2,3` gives you: "my process runs only here." It does not give you: "only my process runs here." Achieving the second requires pushing everyone else out: - **Confine the others.** Put the rest of the system's tasks in a cpuset that excludes CPUs 2 and 3, so the exclusion is enforced from both sides. - **`isolcpus=`** as a kernel boot parameter removes CPUs from the general scheduler's load balancing, so nothing gets placed there unless it is explicitly pinned. - **`nohz_full=`** reduces the periodic timer tick on isolated CPUs, cutting a source of jitter for a thread that runs uninterrupted. - **Interrupt steering.** Point device IRQ affinity at the other CPUs, or the isolated core still gets interrupted by network and storage completions. An interviewer asking this is usually checking whether you know that real isolation is a whole-system configuration, not a one-line command. ## Inheritance across fork and exec Two precise rules, both worth stating without hedging: - A child created by `fork(2)` **inherits** its parent's affinity mask. - The mask is **preserved across `execve(2)`** — replacing the program image does not reset it. Together these mean affinity flows down a process tree by default. Practical consequences: - `taskset -c 0,1 ./start.sh` confines the script *and* every binary it launches, plus their children, indefinitely. - A service manager that pins a supervisor pins the whole supervised tree unless something resets the mask explicitly. - A container or a job runner that inherited a narrow mask from whatever started it can end up with a large multi-threaded process crammed onto two CPUs, presenting as inexplicable CPU saturation while most of the machine is idle. When a process seems capped at a strange multiple of one CPU's worth of time, checking its affinity mask is a fast, cheap thing to rule out. Note that affinity, like the nice value, is a **per-thread** attribute on Linux. A threaded application can set different masks for different threads, and a runtime that pins its own worker threads will override whatever mask it inherited. ## Why pin at all The legitimate reasons are about locality and jitter, not about priority: - **Cache and NUMA locality.** A thread that stays on one CPU keeps its working set in that core's caches, and staying on one NUMA node keeps memory accesses local. Migration throws both away. - **Reduced migration jitter.** For latency-sensitive loops, an unpredictable migration is a tail-latency event. - **Deliberate partitioning.** Separating interrupt-handling CPUs from application CPUs on a busy network host. And the costs are real: you give up load balancing, you can pin two hot threads onto one CPU by accident, and a mask that hardcodes CPU numbers becomes wrong the moment the workload moves to a machine with a different topology or a different number of online CPUs. ## What an interviewer is checking One conceptual point and one factual point. The concept: affinity restricts the task and reserves nothing, so "pinning for isolation" is incomplete without excluding everything else. The fact: masks are inherited by children and survive exec, which is why a pinned wrapper quietly pins an entire process tree.

  • What would you actually configure to dedicate two CPUs to one process, given that affinity alone does not do it?
    Exclude everyone else from those CPUs rather than just confining one task: keep the general scheduler off them with the isolcpus boot parameter or a cpuset that covers the rest of the system, steer device interrupt affinity to other CPUs, and consider nohz_full to cut tick jitter. Then pin the process there. Isolation is a whole-machine configuration.
  • A containerised service looks CPU-starved at exactly two cores' worth of usage on a 32-core host. What cheap check would you run first?
    Read the process's affinity mask. An inherited narrow mask — from a wrapper, a job runner, or whatever started the parent — is preserved across fork and exec, so a large threaded process can be silently confined to two CPUs while the rest of the machine idles. It is a one-command check that rules out a whole class of mystery.
  • Can pinning a process make it slower?
    Yes. The mask is a hard constraint, so a pinned task waits for one of its permitted CPUs even when others are idle — you have traded the scheduler's load balancing for locality. If you pin two busy threads to the same CPU, or pin onto CPUs that also handle heavy interrupt load, throughput drops while the machine looks under-used.
  • Is CPU affinity a per-process or per-thread attribute on Linux?
    Per-thread. Each thread has its own mask, and a runtime can pin its worker threads individually regardless of what the process inherited. Tools that operate on a PID act on that thread unless told to cover all of them, which is why a partial pinning attempt can leave some threads confined and others free.

saying these in an interview costs you the question

  • Says taskset reserves the listed CPUs for that process
  • Thinks other processes are excluded from a pinned CPU
  • Believes affinity is reset when the process calls exec
  • Assumes children do not inherit the parent's mask
  • Claims pinning always improves performance

context