Dos & Don'ts Entwicklung einer Linux-Realtime-Applikation

Von Diplom-Ingenieur (FH) Andreas Klinger 9 min Lesedauer

Anbieter zum Thema

Wie entwickelt man echtzeitfähige Linux-Anwendungen richtig? Dieser Artikel zeigt zentrale Anforderungen, typische Fehlerquellen und konkrete Maßnahmen – von Interrupts über Scheduling bis zur Speicherverwaltung – für stabile Realtime-Performance.

Was gilt es bei der Entwicklung einer Echtzeit-Anwendung mit Linux zu beachten? Und was sollte man dabei keinesfalls tun?(Bild:  KI-generiert / DALL-E)
Was gilt es bei der Entwicklung einer Echtzeit-Anwendung mit Linux zu beachten? Und was sollte man dabei keinesfalls tun?
(Bild: KI-generiert / DALL-E)

Das Ereignis, auf welches in Echtzeitsystemen deterministisch reagiert werden soll, ist der Interrupt. Daher kommt ihm eine zentrale Bedeutung zu.

Interruptbehandlung - threaded(Bild:  Andreas Klinger)
Interruptbehandlung - threaded
(Bild: Andreas Klinger)

Beim RT-Preemption-Patch erfolgt die Interruptbehandlung fast aller Interrupts in zwei Stufen: Ein IRQ-Handler wird im Hardware-Interrupt-Context und ein Threaded-IRQ-Handler wird durch einen Kernel-Thread aufgerufen. (Die wichtigste Ausnahme von diesem Schema bildet der Timer-Interrupt, welcher auch beim RT-Patch nur im Hardware-Interrupt-Context aufgerufen wird).

Der im Hardware-Interrupt-Context aufgerufene Handler besteht aus einer Default-Funktion, welche lediglich zurückgibt, dass der zugehörige Threaded-IRQ-Handler aufgerufen werden soll. Dieser Handler kann durch den Treiber-Entwickler auch ersetzt werden, beispielsweise um bei einem Shared-Interrupt diesen zu Clearen.

SEMINAR-TIPP

Embedded-Linux-Woche

Embedded-Linux-Woche

In den Kursen der Embedded-Linux-Woche führen unsere Referenten Sie Schritt für Schritt in die Linux-Welt ein. Fortgeschrittene können sich im Seminarangebot "Echtzeit-Linux und Systemprogrammierung" vom einfachen Embedded-Linux-User zum systemnahen Echtzeit-Entwickler weiterbilden. In nur fünf Tagen erarbeiten Sie sich fundiertes Fachwissen um den Linux-Kernel, Prozessverwaltung, Tracing, Ressoucenverwaltung, Hardware-Anbindung und vieles mehr, so dass Sie sich schon bald als waschechten Linux-Experten bezeichnen können!

Weitere Details und Termine

Die eigentliche Interrupt-Service-Routine vom ursprünglichen Treiber wird durch einen Kernel-Thread aufgerufen. Wenn dieser aufgeweckt wurde, wird er entsprechend seiner Policy gescheduled (Default: SCHED_FIFO mit Priorität 50). Wenn dies erfolgt, dann ruft er die Interrupt-Service-Routine auf. Durch diese Veränderung an fast allen bestehenden Treibern wird die Interrupt-Arbeit in den Scheduler in den mittleren Prioritätsbereich der Realtime-Policy verlagert.

Dadurch öffnet sich ein Prioritätsfenster im Bereich von 51 bis 98 welches für hochpriorisierte Interrupts und Applikationen (eigentliche Echtzeit-System) durch den Entwickler einstellbar genutzt werden kann. Priorität 99 wird für diverses Houskeeping (z. B. Task-Migration) genutzt und sollte daher gemieden werden.

Scheduling

Wer unterbricht wen im RT-Patch?(Bild:  Andreas Klinger)
Wer unterbricht wen im RT-Patch?
(Bild: Andreas Klinger)

Die Aufgabe des Entwicklers ist es nun das Prioritätsfenster für die Echtzeit-Applikation über den Threaded-Interrupts zu nutzen. Dazu wird mit dem Programm chrt die Priorität des echtzeitrelevanten Interrupts hochgehoben genauso wie diejenige der Echtzeitapplikation auf über 50 eingestellt wird, um vor den restlichen Interrupts dranzukommen.

SoftIRQs

threaded SoftIRQ - threaded Interrupt raised(Bild:  Andreas Klinger)
threaded SoftIRQ - threaded Interrupt raised
(Bild: Andreas Klinger)

Im Linux-Kernel existiert ein Mechanismus namens SoftIRQ. Dieser dient dazu die Interrupt-Service-Routinen zu entlasten. SoftIRQs werden erst nach Abarbeitung von Hardware-Interrupts aber vor dem eigentlichen Scheduler aufgerufen und während sie rechnen, sind weitere Interrupts enabled. Dies ist auch der entscheidende Unterschied zum Hardware-Interrupt: Weitere anliegende Interrupts kommen während der Ausführung des SoftIRQs durch und sind nicht implizit maskiert.

Bei Aktivierung des RT-Patches muss sich dieses Verhalten ändern, da sie ja vor dem Scheduling und dann vor dem Echtzeit-Interrupt und dessen Applikation rechnen würden. Im RT-Patch werden die aus einem Threaded-Interrupt heraus ablaufbereit markierten (geraised) SoftIRQs direkt wie eine Funktion aufgerufen. Damit laufen sie durch den Scheduler verwaltet im Kontext des Threaded-Interrupts.

Threaded SoftIRQ - Hardware-Interrupt raised order ksoftirqd running(Bild:  Andreas Klinger)
Threaded SoftIRQ - Hardware-Interrupt raised order ksoftirqd running
(Bild: Andreas Klinger)

Wird der SoftIRQ jedoch aus einem Hardware-Interrupt heraus geraised, so darf er nicht direkt aufgerufen werden, da die SoftIRQs ja genau dazu erfunden wurden, um Interruptarbeit außerhalb des Interruptkontextes mit implizit gesperrten Interrupts zu machen. In diesem Fall werden die SoftIRQs an den Kernel-Thread mit dem Namen ksoftirqd delegiert und dieser ruft die SoftIRQ-Funktionen auf. Dieser Kernel-Thread hat keine Echtzeit-Priorität (Policy: SCHED_OTHER) und daher fallen diese SoftIRQs in ihrer Wichtigkeit deutlich nach unten.

Für SoftIRQs besteht weiterhin die Einschränkung, dass diese sich auf einem Core nicht gegenseitig unterbrechen, sondern nur sequentiell auf einem Core ausgeführt werden können. Daher werden auch SoftIRQs, welche geraised werden, wenn schon andere SoftIRQs geraised sind, auch in den ksoftirq-Thread eingereiht. Ein Workaround für diese Fälle besteht darin, diese SoftIRQs, wenn (und auch nur dann) sie echtzeitrelevant sind, auf separate Cores zu pinnen. Werden echtzeitrelevante Treiber neu entwickelt, dann empfiehlt es sich auf SoftIRQs zu verzichten und stattdessen Kernel-Threads zu verwenden.

Jetzt Newsletter abonnieren

Verpassen Sie nicht unsere besten Inhalte

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung. Die Einwilligungserklärung bezieht sich u. a. auf die Zusendung von redaktionellen Newslettern per E-Mail und auf den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern (z. B. LinkedIn, Google, Meta).

Aufklappen für Details zu Ihrer Einwilligung

RT-Throttling

In Linux existiert ein Mechanismus, der es verhindert, dass Realtime-Tasks das System durch eine Busy-Loop lahmlegen können. Dieser Mechanismus wird als Realtime-Throttling bezeichnet.

RT-Throttling(Bild:  Andreas Klinger)
RT-Throttling
(Bild: Andreas Klinger)

Dabei wird den Realtime-Tasks insgesamt eine maximale Runtime zugestanden. Ist diese erreicht, wird das Realtime-Scheduling unterbrochen und für die bis zur Zeitperiode verbleibende Zeit dürfen normale Tasks rechnen. Die Default-Einstellung für die Runtime ist 950 ms und für die Periode 1 s. Daher sind sporadische Latenzen bis zu 50 ms ein typisches Symptom dafür, dass das RT-Throttling zugeschlagen hat.

In einem Echtzeit-System kann es nun vorkommen, dass ein niedrig priorisierter RT-Task übermäßig lange (länger als Runtime) rechnet und ausgerechnet, wenn das RT-Throttling-Zeitfenster zuschlägt, gerne rechnen möchte. Das würde bedeuten, dass der niedrig priorisierte indirekt den hoch priorisierten ausbremst. Daher sollte man das RT-Throttling für Echtzeitsysteme ausschalten. Dies kann mit folgendem Aufruf erfolgen:

echo -1 > /proc/sys/kernel/sched_rt_runtime_us

Besonderheiten in der Entwicklung

Im Laufe der Echtzeit-Entwicklung stellte sich heraus, dass nicht alle Mechanismen, welche aus der Entwicklung im Userspace bekannt und empfohlen sind, auch für das Echtzeitsystem geeignet sind. Im Gegenteil verursachen manche Mechanismen unerwartete Latenzen.

Datei memory-mappen(Bild:  Andreas Klinger)
Datei memory-mappen
(Bild: Andreas Klinger)

Grundsätzlich sollen alle IPC-Mechanismen, welche auf der herkömmlichen Waitqueue basieren, nicht verwendet werden. Dazu gehört der Aufruf von poll(), select() und Co. genauso wie die Signale. Alternativ kann zur Benachrichtigung die Conditional Variable aus der NPTL-Library genutzt werden, wenn diese auch mit einem Priority-Inheritance-Mutex verwendet wird.

Posix-Timer beruhen ebenso auf Signalen und Timer-File-Descriptors auf Waitqueues. Daher ist für zeitlich abhängige Vorgänge empfohlen, den nanosleep() und den clock_nanosleep() (mit CLOCK_MONOTONIC_RAW) zu verwenden. Benötigt man ein fixes Zeitraster, kann das Flag TIMER_ABSTIME zum Einsatz kommen.

Deutschlands Leitkongress der Embedded-Softwarebranche

Bewerben Sie sich als Sprecher

Embedded Software Engineering Kongress

Gestalten Sie den ESE Kongress aktiv mit! Es gibt viele gute Gründe, warum Sie sich mit Ihrem eigenen Vortrag oder Seminar am Programm beteiligen sollten. Teilen Sie Ihr Wissen, Ihre Erfahrung und Ihre Erkenntnisse mit Ihren Branchenkollegen. Nutzen Sie den ESE Kongress als bewährte Plattform, um sich als Software-Experte einen Namen zu machen und Ihren Marktwert zu steigern, und präsentieren Sie Ihre Lösungen einem hochwertigen Fachpublikum.

Speicherverwaltung

Memory-Mapping wird im Linux-Kernel vielfältig genutzt. Beispielsweise um Programm- und Librarycode sowie -daten in das RAM zu mappen, dynamischen Speicher zu allozieren sowie um Heap und Stack zu setzen. Das physische Kopieren der Daten vom Massenspeicher oder von einem anderen Speicherbereich (Copy-On-Write-Memory) passiert allerdings erst beim erstmaligen Zugriff. Beim lesenden Zugriff wird im Falle von Copy-On-Write-Memory auf schon anderswo vorhandenen Speicher mit gleichem Inhalt zugegriffen.

Wird nun der Speicher tatsächlich benötigt, löst die MMU eine Exception namens Page-Fault aus. Diese wird durch das Betriebssystem abgefangen und aufgelöst, indem die betreffende Speicherseite in den Adressraum des betreffenden Prozesses kopiert wird. Wir unterscheiden dabei zwischen Major- und Minor-Page-Faults. Beim Major-Fault sind Disk-IO-Operationen notwendig, während beim Minor-Page-Fault nur Speicher kopiert wird (Copy-On-Write-Memory). In beiden Fällen entsteht aus Sicht des ausführenden Tasks eine unerwartete Latenz beim Zugriff auf den Speicher. Um dies zu vermeiden, sind einige Maßnahmen erforderlich.

Speicher soll gelockt werden, so dass einmalig im RAM vorhandener Speicher nicht mehr ausgelagert wird. Dies bewerkstelligt der folgende C-Aufruf:

mlockall(MCL_CURRENT | MCL_FUTURE);

Speicher sollte durch die C-Library auch nicht durch den mmap()-Mechanismus irgendwo im RAM gemapped, sondern vom Heap genommen werden. Beim Heap ist genau bekannt, wo er liegt, nämlich direkt nach den Daten des Executables und er wird von dort bei Bedarf vergrößert (sogenannte Program-Break). Daher ist es für den Heap-Speicher einfach, ihn für zukünftige Allokationen resident im RAM vorzuhalten, was für mmap()-Speicher nicht möglich ist. Die folgende C-Zeile bewerkstelligt dies:

mallop(M_MMAP_MAX, 0);

Die C-Library versucht den Heap zu verkleinern, wenn er gerade nicht benötigt wird. Auch das sollte abgeschaltet werden:

mallopt(M_TRIM_THRESHOLD, -1);

Heap prefaulten(Bild:  Andreas Klinger)
Heap prefaulten
(Bild: Andreas Klinger)

Zu guter Letzt muss man noch dafür sorgen, dass sowohl der Heap als auch der Stack wirklich resident im RAM liegen. Dazu alloziert man im Heap die während des Programmlaufs benötigte Speichermenge und beschreibt (lesen reicht wegen Copy-On-Write-Memory nicht) in jede Page mindestens ein Byte, nachdem obige Einstellungen vorgenommen wurden. Diesen Speicher kann man gleich wieder freigeben und weiß dann sicher, dass bis zu dieser Allokationsgröße der Speicher bei zukünftigen Anforderungen bereits im RAM resident ist.

Stack prefaulten(Bild:  Andreas Klinger)
Stack prefaulten
(Bild: Andreas Klinger)

Das gleiche macht man mit dem Stack, indem man einfach eine Funktion erstellt, welche den maximal benötigten Stack anlegt und ebenso pageweise beschreibt.

Wie kann man feststellen, ob die ergriffenen Maßnahmen wirken? Dazu gibt es einige Diagnosemöglichkeiten. Mit dem Programm pmap sieht man die Speicherbereiche eines Prozesses und wie viel davon wirklich im RAM liegt (RSS - resident set size). Der Aufruf lautet:

pmap -x <pid>

Mit dem guten alten Programm ps kann man sich auch die Page-Faults ansehen, wenn man die Spalten entsprechend auswählt. Der Aufruf lautet:

ps -Leo pid,maj_flt,min_flt,cmd

Auch mit dem Programm perf kann man sich die Page-Faults anschauen. Beispielsweise:

perf stat -e page-faults ./myrtapp

Möchte man genauer sehen, in welchen Programmteilen Page-Faults entstehen, kann man das perf-Interface des Kernels auch im eingenen Programm nutzen. Siehe dazu die Manual-Page perf_event_open(2).

Im Linux gibt es einen "Heckenschützen". Die Rede ist vom Out-Of-Memory-Killer (OOM-Killer). Dieser wird aktiviert, wenn Speicher knapp wird und sucht sich mittels einer Score-Tabelle einen Prozess, der viel Speicher benötigt, und terminiert diesen. Man kann für einen Task diese Score-Tabelle beeinflussen und diesen somit aus der Liste der relevanten Tasks rausnehmen. Dies funktioniert im procfs und auch mit einem Programm namens choom. Der Programmaufruf lautet:

choom -p <pid> -n -1000

Hier ist die Rede von deterministischen Tasks, und sobald durch einen mehr oder weniger zufälligen Mechanismus Tasks aus dem System entfernt werden, wird man möglicherweise auch den Determinismus verlieren. Ansonsten wäre der terminierte Task ja von vornerein überflüssig gewesen, wenn er keine für das Gesamtsystem relevante Aufgabe hatte.

Energieeinstellungen

Mit Hilfe von Frequenzvariationen erlauben wir es unseren heutigen Systemen, sich an unterschiedliche Lastanforderungen anzupassen und damit die Leistungsaufnahme zu reduzieren. Wenn ein Echtzeit-Interrupt an unserem System anliegt und dieser behandelt werden soll, kann es passieren, dass das System bei der Anpassung der Frequenz eine höhere Latenz aufweist. Daher sollten diese Frequenzanpassungen vermieden werden. Dies passiert bei Linux durch Auswahl von Frequency-Governors. Wenn die Frequenz nicht reduziert werden soll, kann der Performance-Governor verwendet werden. Auf der Linux-Command-Line lautet der Aufruf:

cpufreq.governor=performance

Zu beachten ist, dass dann natürlich die Leistungsaufnahme auch ansteigt und es auch zu einer stärkeren Erwärmung des Systems kommt und bei manchen Systemen sogar zu einer Reduzierung der Lebensdauer des Microcontrollers. Hier sollten die Randbedingungen aus der Hardware beachtet werden.

Eine weitere und noch bedeutendere Quelle von unerwarteten Latenzen sind die CPU-C-States. Dabei handelt es sich um Schlafstadien, in welche die CPU versetzt werden kann, wenn nichts gerechnet werden soll. Beim Aufwachen aus diesen Stadien sind zum Teil immense Latenzen durch die Hardware verursacht zu erwarten. Von Linux aus kann man diese C-States auf der Kernel-Command-Line abschalten:

cpuidle.off=1

Auch hier ist wieder zu beachten, dass dadurch eine höhere Leistungsaufnahme und Wärmeentwicklung verursacht werden.

Kernel-Configuration

In der Linux-Kernel-Configuration ist der RT-Preemption-Patch zu aktivieren mit dem Schalter:

CONFIG_PREEMPT_RT_FULL=y

Außerdem sind alle Debug-Optionen unter Kernel hacking für das produktive System zu deaktivieren. Diese Schalter heißen CONFIG_DEBUG_*. Bei der Verwendung einer Linux-Distribution ist zu beachten, dass diese Schalter bei den meisten Distributionen default eingeschaltet sind.

SEMINAR-TIPP

Embedded-Linux-Woche

Embedded-Linux-Woche

In den Kursen der Embedded-Linux-Woche führen unsere Referenten Sie Schritt für Schritt in die Linux-Welt ein. Fortgeschrittene können sich im Seminarangebot "Echtzeit-Linux und Systemprogrammierung" vom einfachen Embedded-Linux-User zum systemnahen Echtzeit-Entwickler weiterbilden. In nur fünf Tagen erarbeiten Sie sich fundiertes Fachwissen um den Linux-Kernel, Prozessverwaltung, Tracing, Ressoucenverwaltung, Hardware-Anbindung und vieles mehr, so dass Sie sich schon bald als waschechten Linux-Experten bezeichnen können!

Weitere Details und Termine

Der Autor

Andreas Klinger wurde 2011, 2014 und 2018 als Referent auf dem ESE Kongress mit dem Best Speaker Award ausgezeichnet.(Bild:  IT-Klinger)
Andreas Klinger wurde 2011, 2014 und 2018 als Referent auf dem ESE Kongress mit dem Best Speaker Award ausgezeichnet.
(Bild: IT-Klinger)

Andreas Klinger ist selbständiger Trainer und Entwickler. Seit Abschluss des Studiums der Elektrotechnik im Jahre 1998 arbeitet er im Bereich der systemnahen Softwareentwicklung mit den Schwerpunkten Kernel-Treiber, Embedded-Linux und Echtzeit. Als Spezialist für Linux beschäftigt er sich mit dem internen Aufbau des Kernels, den Systemmechanismen sowie vor allem mit deren Einsatz in Embedded Systemen. Contributor zum Linux-Kernel und anderen Open-Source-Projekten.

Dieser Beitrag wurde mit freundlicher Genehmigung des Autors aus dem Tagungsband des ESE Kongress 2023 übernommen. (sg)

(ID:50384970)