Interrupt (IRQ) affinity#

Why#

Isolating CPU cores (previous chapters) is pointless if the critical hardware interrupts, PPS and network reception, keep being handled anywhere, competing with other tasks. This chapter explicitly routes each interrupt to a dedicated core.

How#

Routing script#

mkdir -p /opt/affinity
cat > /opt/affinity/affinity.sh <<'EOF'
#!/bin/bash
# Hardware interrupt routing:
#   - PPS (GPIO)  -> Core 3, RT priority FIFO 99
#   - end0 (NIC)  -> Core 2, RT priority FIFO 90
set -u
NET_IF="end0"

PPS_IRQ=$(awk '/pps/ {print $1}' /proc/interrupts | sed 's/://')
NET_IRQ=$(awk -v iface="$NET_IF" '$0 ~ iface {print $1}' /proc/interrupts | sed 's/://' | head -n 1)

# --- PPS -> Core 3, FIFO 99 ---
if [ -n "$PPS_IRQ" ]; then
    echo 8 > /proc/irq/"$PPS_IRQ"/smp_affinity
    PPS_PID=$(pgrep -f "irq/${PPS_IRQ}-")
    [ -n "$PPS_PID" ] && chrt -f -p 99 "$PPS_PID"
fi

# --- end0 -> Core 2, FIFO 90 ---
if [ -n "$NET_IRQ" ]; then
    echo 4 > /proc/irq/"$NET_IRQ"/smp_affinity
    # Setting rx-usecs/tx-usecs to 0 disables the NIC's interrupt
    # coalescing (by default, the controller deliberately delays raising
    # an IRQ to batch several packets together and reduce CPU load): one
    # immediate IRQ per packet is preferred here instead, even if that
    # means more interrupts, since core 2 is entirely dedicated to it and
    # has nothing else to do
    ethtool -C "$NET_IF" rx-usecs 0 tx-usecs 0 2>/dev/null || true
    NET_PID=$(pgrep -f "irq/${NET_IRQ}-")
    [ -n "$NET_PID" ] && chrt -f -p 90 "$NET_PID"
fi
EOF
chmod +x /opt/affinity/affinity.sh

The smp_affinity mask is a hexadecimal bitmask: 4 maps to core 2, 8 to core 3.

systemd service and persistence#

cat > /etc/systemd/system/affinity.service <<'EOF'
[Unit]
Description=Hardware interrupt routing (PPS core3, network core2)
After=network.target

[Service]
Type=oneshot
ExecStart=/opt/affinity/affinity.sh
RemainAfterExit=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now affinity.service

An Ethernet link renegotiation resets the masks, a udev rule replays the script automatically:

cat > /etc/udev/rules.d/90-end0-affinity.rules <<'EOF'
ACTION=="change", SUBSYSTEM=="net", KERNEL=="end0", RUN+="/opt/affinity/affinity.sh"
EOF
udevadm control --reload-rules

Pitfalls to avoid#

The network interface’s IRQ thread doesn’t carry the interface’s name. On Raspberry Pi 5, the network chip’s driver registers its interrupt under a historical internal name (eth%d) before the interface gets renamed to end0 by udev. The kernel thread is therefore called irq/<N>-eth%d, not irq/<N>-end0. A script that looks for pgrep -f "irq/.*-end0" will never find anything. Always target by IRQ number (determined via /proc/interrupts), never by assumed interface name.

  • irqbalance still installed. If it wasn’t purged in the Updates chapter, it will keep continuously redistributing IRQs across cores, undoing this script’s effect over time.
  • Forgetting the persistence udev rule. Without it, a simple network cable unplug/replug (or NetworkManager restarting the interface) resets the smp_affinity mask to its default, and the network IRQ goes back to floating freely across cores.

Verify#

# Live IRQ mapping (Ctrl+C to quit)
watch -n 1 -d 'grep -E "pps|end0" /proc/interrupts'

# Effective affinity
cat /proc/irq/$(awk '/pps/ {print $1}' /proc/interrupts | tr -d ':')/smp_affinity_list
cat /proc/irq/$(awk '/end0/ {print $1}' /proc/interrupts | head -1 | tr -d ':')/smp_affinity_list
# Should return "3" and "2" respectively

# Confirm no ordinary userland thread is scheduled on core 2 or 3
ps -eLo psr,comm | awk '$1==2 || $1==3' | sort -u
# Should only list the relevant kernel IRQ threads (and idle)

📚 Going further

The PREEMPT_RT kernel (previous chapter) makes every interrupt handler “threaded”, that is, individually schedulable, with its own SCHED_FIFO priority. This mechanism is what lets the PPS IRQ be given priority 99 here (the highest possible on this system), guaranteeing it preempts absolutely everything else the moment a pulse arrives, including other real-time threads such as chrony’s (priority 98, see the Chrony chapter).