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.shThe 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.serviceAn 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-rulesPitfalls 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 toend0byudev. The kernel thread is therefore calledirq/<N>-eth%d, notirq/<N>-end0. A script that looks forpgrep -f "irq/.*-end0"will never find anything. Always target by IRQ number (determined via/proc/interrupts), never by assumed interface name.
irqbalancestill 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
NetworkManagerrestarting the interface) resets thesmp_affinitymask 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_FIFOpriority. This mechanism is what lets the PPS IRQ be given priority99here (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 (priority98, see the Chrony chapter).