Pinning HLDS to a CPU Core with taskset

November 6, 2025 Daemon666 8 min read 4 visualizações

By default the Linux scheduler is free to bounce your HLDS process from core to core. For a single-threaded, latency-sensitive game server that is mildly wasteful — every migration means a cold cache on the new core — and on a box running several servers it is genuinely harmful, because instances end up fighting over the same cores while others sit idle. taskset fixes both by pinning each server to a core you choose. Here is how to use it correctly.

1. See your cores first

Know how many cores you have and how they are numbered before pinning:

nproc
lscpu | grep -E 'CPU\(s\)|Thread|Core'

Cores are numbered from 0. On a 4-core box you have 0,1,2,3. Note whether hyper-threading is on — sibling threads share a physical core, so pinning two busy servers to two siblings is not the same as two real cores.

2. Launch a server pinned to a core

Wrap the launch command with taskset -c and the core number:

taskset -c 2 ./hlds_run -game cstrike +ip 0.0.0.0 +port 27015 +map de_dust2 +maxplayers 32

-c 2 confines the process (and its threads) to core 2. Everything the server spawns inherits that affinity. Reserve core 0 for the OS and interrupts where you can, and give servers cores 1 and up.

3. Pin a server that is already running

You do not have to restart to pin. Find the PID and set affinity live with -p:

pgrep -a hlds
taskset -cp 2 12345      # 12345 = the hlds_linux PID

Read the current affinity back the same way without a core list:

taskset -cp 12345
# pid 12345's current affinity list: 2

Note that the real game process is often hlds_linux under the hlds_run wrapper — pin the actual hlds_linux PID, not just the shell wrapper.

4. Why pinning helps

Two concrete wins:

  • Cache locality — a server that stays on one core keeps its working set warm in that core's L1/L2 cache. A migration to another core starts cold and costs cycles right when the server is busy.
  • Deterministic packing — when you run many servers on one box, pinning server A to core 1, B to core 2, C to core 3 guarantees they never pile onto the same core and starve each other. Left to the scheduler, two servers can end up on one core while a third sits empty.

The effect is small for a single lightly-loaded server and large for a dense multi-server box — that is where taskset earns its keep.

5. Pair affinity with IRQ placement

Pinning the game to a core only helps if that core is not also being hammered by network interrupts. On a busy server the NIC generates a lot of hardware interrupts, and if those land on the same core as your server they compete for it. Two complementary moves keep them apart. First, dedicate a core (or the OS default core 0) to interrupts and keep your servers off it. Second, on multi-queue NICs you can steer the interrupts explicitly by writing an affinity mask to each queue's IRQ:

grep eth0 /proc/interrupts        # find the NIC's IRQ numbers
cat /proc/irq/<irq>/smp_affinity_list

The principle is simple: let one core handle NIC interrupts and the OS, and give each game server its own separate core. That way the server's core spends its time on the game loop and nothing else — which is the entire point of pinning in the first place.

6. Make it stick across restarts

Affinity set with taskset -cp is lost when the process restarts, so bake it into how the server launches. Either prefix the launch command in your start script with taskset -c N, or if you run under systemd use the built-in directive:

# in the [Service] section of your unit file
CPUAffinity=2

systemd then pins the service to core 2 every start, no wrapper needed. Reload with systemctl daemon-reload and restart the unit.

Common mistakes

  • Pinning the wrapper, not the gamehlds_run is a shell script; confirm the actual hlds_linux PID carries the affinity.
  • Pinning everything to core 0 — core 0 also handles OS work and interrupts. Leave it for the system; use cores 1+ for servers.
  • Treating HT siblings as real cores — two hyper-threads share one physical core; pinning two busy servers there does not double throughput.
  • Expecting a big single-server gain — the win is cache locality and packing, not a magic FPS boost on an idle box.
  • Losing affinity on restart-cp is runtime-only. Put taskset in the launch command or use CPUAffinity=.

Verification

Confirm the pin took and holds. Read affinity back with taskset -cp <pid> — it should list exactly the core you set. Then run top, press 1 for per-core view, and watch under load: the server's CPU should appear on your chosen core and stay there instead of hopping between cores. On a multi-server box, confirm each instance sits on its own core with none doubled up. Pair this with the performance governor so the pinned core also runs at full clock, and with a fast single-thread CPU so that core is worth pinning to.

Colaboradores: Daemon666 ✦
Compartilhar: