Affinité des interruptions (IRQ)#

Pourquoi#

Isoler des cœurs CPU (chapitres précédents) ne sert à rien si les interruptions matérielles critiques, le PPS et la réception réseau, continuent d’être traitées n’importe où, en concurrence avec d’autres tâches. Ce chapitre route explicitement chaque interruption vers un cœur dédié.

Comment#

Script de routage#

mkdir -p /opt/affinity
cat > /opt/affinity/affinity.sh <<'EOF'
#!/bin/bash
# Routage matériel des interruptions :
#   - PPS (GPIO)  -> Core 3, priorité RT FIFO 99
#   - end0 (NIC)  -> Core 2, priorité RT 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
    # rx-usecs/tx-usecs à 0 désactive la coalescence d'interruptions de la
    # carte réseau (par défaut, le contrôleur retarde volontairement la
    # remontée d'une IRQ pour regrouper plusieurs paquets et réduire la
    # charge CPU) : ici, on préfère une IRQ par paquet, immédiate, quitte à
    # augmenter le nombre d'interruptions, puisque ce cœur 2 lui est
    # entièrement dédié et n'a rien d'autre à faire
    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

Le masque smp_affinity est un bitmask hexadécimal : 4 correspond au cœur 2, 8 au cœur 3.

Service systemd et persistance#

cat > /etc/systemd/system/affinity.service <<'EOF'
[Unit]
Description=Routage materiel des interruptions (PPS core3, reseau 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

Une renégociation du lien Ethernet réinitialise les masques, une règle udev rejoue le script automatiquement :

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

Erreurs à éviter#

Le thread IRQ de l’interface réseau ne porte pas le nom de l’interface. Sur Raspberry Pi 5, le pilote de la puce réseau enregistre son interruption sous un nom interne historique (eth%d) avant le renommage de l’interface en end0 par udev. Le thread kernel s’appelle donc irq/<N>-eth%d, pas irq/<N>-end0. Un script qui chercherait pgrep -f "irq/.*-end0" ne trouvera jamais rien. Toujours cibler par numéro d’IRQ (déterminé via /proc/interrupts), jamais par nom d’interface supposé.

  • irqbalance toujours installé. S’il n’a pas été purgé au chapitre Mises à jour, il va continuellement redistribuer les IRQ entre cœurs, annulant l’effet de ce script au fil du temps.
  • Oublier la règle udev de persistance. Sans elle, une simple déconnexion/reconnexion du câble réseau (ou un NetworkManager qui redémarre l’interface) réinitialise le masque smp_affinity à sa valeur par défaut, et l’IRQ réseau repart en libre circulation entre cœurs.

Vérifier#

# Cartographie en direct des IRQ (Ctrl+C pour quitter)
watch -n 1 -d 'grep -E "pps|end0" /proc/interrupts'

# Affinité effective
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
# Doit retourner respectivement "3" et "2"

# Confirmer qu'aucun thread userland ordinaire n'est planifié sur 2 ou 3
ps -eLo psr,comm | awk '$1==2 || $1==3' | sort -u
# Ne doit lister que les threads kernel IRQ concernés (et idle)

📚 Pour aller plus loin

Le noyau PREEMPT_RT (chapitre Noyau temps réel) rend chaque gestionnaire d’interruption “threadé”, c’est-à-dire ordonnançable individuellement, avec sa propre priorité SCHED_FIFO. C’est ce mécanisme qui permet ici de donner à l’IRQ du PPS la priorité 99 (la plus haute possible sur ce système), garantissant qu’elle préempte absolument tout le reste dès qu’une impulsion arrive, y compris les autres threads temps réel comme celui de chrony (priorité 98, cf. chapitre Chrony).