Fix irq-set-prio not working in a PLC shell

6 min read · Published November 2, 2026

When `irq-set-prio eth0 55` runs in the PLC shell and the fieldbus jitter does not move, one of five causes is at work: a stub command, no threaded handler behind the interrupt, a real-time ceiling below 55, the wrong interface, or a setting lost at restart. Match the interrupt's IRQ number to a handler thread, read the priority before and after the command, and the branch names itself along with its fix.

irq-set-prio eth0 55 goes into the PLC shell, an OK line comes back, and the fieldbus cycle jitter sits exactly where it was. The command ran. The question is what it attached to. A scheduling priority sticks only to something the scheduler can order, and on a Linux-class controller an interrupt runs in a schedulable thread only when its driver registered one. Work five causes in a fixed order: a stub command, no thread to prioritize, a real-time ceiling below 55, the wrong interface, and a setting lost at restart. The thread check splits that list in under 60 seconds, and every branch has one fix.

What priority 55 has to attach to

Real-time priorities run from 0 to 99, and the higher number runs first. That scale is the entire meaning of the 55 in the command. A stock kernel starts a threaded interrupt handler at 50, half of the 99-point scale, so 55 asks for one step above the default.

An interrupt gets a schedulable thread in one of two ways. The driver registered a threaded handler, supplying both a hard-interrupt handler and a thread function, which is the pattern the kernel's own generic IRQ documentation lays out. Or the controller image is built for full preemption, in which case every interrupt is threaded whether its driver asked or not. Where eth0's interrupt is serviced in hard-interrupt context, there is no scheduling entity behind it, and a priority command against it cannot apply whatever the shell prints back. That image difference is why the same command works on one controller and does nothing on another.

Symptoms and what each points to

  • The shell answers garbage arguments exactly as it answered 55. Points to a stub: the command lives in the parser and goes nowhere.
  • Jitter is unchanged, and no thread in the thread list carries the interface's IRQ number. Points to a missing threaded handler.
  • A handler thread exists, reads priority 50 before the command and 50 after. Points to the privilege ceiling.
  • The command sticks, the thread reads 55, and jitter is unchanged. Points to the wrong interface: you raised the priority of a port that carries no process data.
  • The value reads 55 in this session and 50 after a runtime restart or a power cycle. Points to a volatile setting applied at the wrong point in startup.

The checks in order

Work top to bottom. Each check is one or two commands, about 30 seconds each, and each closes a branch.

  1. Test the command with an impossible argument: irq-set-prio eth0 999. The scale stops at 99, so a real parser rejects 999. A stub answers 999 exactly as it answered 55, and that is your answer.
  2. Resolve the interface to its interrupt lines: cat /proc/interrupts, and read the lines whose device column names the interface. Record every number in column 1. A multi-queue network controller lists one line per queue, and each queue has its own interrupt and its own thread. Fixing one queue and leaving the rest is the classic partial improvement.
  3. Confirm the thread exists: ps -eo pid,class,rtprio,comm, and match the recorded IRQ number inside a thread name, which starts with irq/. No matching thread, stop. This image cannot honor the command, and the fix is an image decision.
  4. Read the priority before and after. Issue the command, rerun the ps line, compare the rtprio column. 50 before and 55 after means the command worked and the problem lives elsewhere. 50 and 50 means the request was refused, so go to step 5.
  5. Read the ceiling as the user the runtime runs as: ulimit -r, plus the rtprio lines in /etc/security/limits.conf. A ceiling of 50 against a target of 55 is refused at the operating system layer.
  6. Restart the runtime, then power cycle, rereading the priority each time. The value dying at the runtime restart points at startup ordering. Dying at the power cycle points at a command that only ever ran interactively.

Example: eth0 targeted at 55 under a ceiling of 50

Inputs:

  • Target priority 55, from the shell command irq-set-prio eth0 55.
  • Stock handler priority 50, the kernel default for a threaded interrupt.
  • Real-time scale 0 to 99, set by the Linux scheduler.
  • Ceiling 50, read with ulimit -r for the user the runtime runs as.
  • A fieldbus cycle of 1 ms, so a priority fix has to show up as jitter moving within a few seconds of measurement.

Rule: the request takes effect only when the target priority is at or below the ceiling.

Arithmetic: the interrupt table resolves the interface to its line, and the thread list shows the handler thread at rtprio 50. The command is issued, and the thread still reads 50. Compare: 55 > 50, so the operating system refused the request. The ceiling and the target cannot both hold.

Fix and result: the runtime user's rtprio limit goes to 99 in /etc/security/limits.conf, the runtime restarts so the new limit loads, and the command is reissued. Now 55 is at or below 99, and the ps line reads rtprio 55. Decision: a true 55 reading with jitter unchanged moves you to the interface check in step 2. A reading still stuck at 50 under a ceiling of 99 moves you to the stub check in step 1.

The fix for each cause

Work in a planned stop, and treat any kernel or image change on a certified controller as a vendor case decided in writing, never a self-served swap.

Confirmed cause Fix
Stub command Set the scheduling at the operating system level with chrt -f 55 -p against the thread's process ID, and open a vendor case quoting the firmware revision and the shell's exact response.
No threaded handler Image-level. Request the supported real-time image from the vendor.
Ceiling below target Raise the runtime user's rtprio limit to 99, restart the runtime so the limit loads, reissue the command.
Wrong interface Confirm the carrying port by MAC address and link counters, reissue against that port, and enforce persistent interface naming.
Volatile setting Move the command into a startup hook ordered after the network driver loads. The hook rereads the priority and logs what it sees.

A runtime configured against one device while the process data rides another is the same class of fault as a PLC runtime device mismatch, and you check it the same way: follow the traffic, not the name. Persisting a per-machine value like the priority is the same job as setting machine-specific NetVar IP and port values the supported way: the setting belongs in the machine's startup configuration, written once and applied in the right order.

Where 55 should sit

Before locking the value, list every real-time thread on the controller with the same ps line, then place the frame-handling thread deliberately:

  • Above the cyclic task when the task cannot start its work until frames arrive. Late frame delivery then shows up as an outranked interrupt thread.
  • Below the cyclic task when the task is compute-bound and frame handling tolerates deferral of a few microseconds.
  • Never tied with another critical thread, and never pinned at the top of the scale to be safe. The gaps between your real-time priorities are the design; fill them on purpose. The cyclic task's own position comes from tracing PLC execution order across task layers.

Keep the fieldbus stack's threads within a few numbers of each other so the stack finishes what it starts.

Recommendation: Run the thread check first, always. A thread reading 50 before and after the command is a ceiling problem: set the runtime user's rtprio limit to 99, restart the runtime, reissue, reread. No thread at all is an image decision that belongs to the vendor. Whatever branch you land on, the command ends up in a startup hook that runs after the network driver and logs the priority it reads back.

Questions

What does the number 55 mean in irq-set-prio eth0 55?

It is a real-time scheduling priority on the 0 to 99 scale, where the higher number runs first. A stock kernel starts a threaded interrupt handler at 50, the middle of that scale, so 55 is one deliberate step above the default: enough to outrank the stock position, small enough to leave room above it. That space above 55 is where your cyclic task and the rest of the fieldbus stack belong.

Why not set every real-time thread to 99 to be safe?

Two things go wrong at 99. Threads sharing one SCHED_FIFO priority run to completion in arrival order, so a cluster of threads pinned at 99 serializes instead of sharing the core. And a stock kernel throttles real-time work to 95 percent of each second by default, holding the remainder in reserve for everything else, so a machine loaded to that budget loses time whatever the priorities say. Place threads with gaps between them.

Does the real-time priority ceiling apply to the root user?

The ceiling from ulimit -r binds the user the runtime runs as. A process holding CAP_SYS_NICE, which root carries by default, is exempt from it. If the runtime runs as root with that capability intact and the thread still reads 50 after the command, skip the privilege branch and work the thread and stub checks instead.

Where should the command live so it survives a reboot?

In a startup hook that runs after the network driver loads, never before it. Have the hook reread the priority and log the value, because a hook that fires too early fails silently and leaves you comparing jitter against a setting that never applied. Look up the IRQ number at run time as well: interface names and line numbering can change between boots unless the controller enforces persistent naming.

Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.

Keep reading

Build your signAsk for a quote