Appearance
1. Kernel Module
Virtual Memory
Lesen Sie folgenden Artikel zum Thema Virtual Memory und Paging:
Visual Studio Code
Ein Kernelmodul kan Problemlos as Textfile in C in einem einfachen Editor (Notepad++ / gedit / nano etc.) entwickelt werden.
Für manche Funktionsaufrufe und Refactoring von Sourcecode ist allerdings eine IDE in vielen Fällen hilfreich.
Eine Option ist Visual Studio Code (vscode
, https://code.visualstudio.com/) zu verwenden. Ein Setup für die Entwicklung für Kernelmodule finden Sie hier: https://gitlab.fhnw.ch/ebssd/kernel-module. Nach einem git clone des repos müssen die Pfade für Toolchain gemäss README.md
allerdings noch angepasst werden.
Eclipse Setup für Kernel Programmierung
Alternativ ist Eclipse mit dem CDT Plugin eine State-of-the-art IDE für Entwicklung mit C und C++. Im Gegensatz zu VS-Code ist Eclipse einiges "schwerer" und etwas träger. Allerdings bietet es wesentlich umfangreichere Features insbesondere für Refacotring.
- Installieren Sie die
snap
version von Eclipse im Ubuntu Software Center. - Starten Sie Eclipse und erstellen Sie einen neuen Workspace.
- Unter
Help -> Install new Software
wählen Sie--All Available Sites--
. - Im Filter geben Sie ein
C++
. Selektieren und installieren Sie folgende Pakete:C/C++ Autotools support C/C++ Development Tools C/C++ Development Tools SDK C/C++ GCC Cross Compiler Support C/C++ GDB Hardware Debugging C/C++ Remote Launch
- Erstellen Sie ein neues C Projekt
mod_hello
. - Wählen Sie
Makefile Project -> Empty Project
undLinux GCC
. - Fügen Sie ein neues File
mod_hello.c
hinzu mit dem Inhalt den Sie hier finden mod_hello.c.Eclipse meldet
Syntax Error
Im jetztigen Zustand meldet Eclipse im Source Code fehlerhafte Syntax, mit der folgenden Konfiguration werden die entsprechenden Resourcen verlinkt, so dass diese Fehlermeldungen verschwinden werden.
- Öffnen Sie
Project Settings -> C/C++ General -> Preprocessor Include Paths, Macros etc.
- Unter
GNU C
wählen SieCDT User Setting Entries -> Add
. - Wählen Sie den Typ
Preprocessor Macro File
. - Auf der rechten Seite
File System Path
und verwenden Sie<pfad zum kernel source>/include/linux/kconfig.h
sowie auch<pfad zum kernel source>/include/asm-generic/param.h
. - Unter dem Tab
Providers
ändern Sie beiCDT GCC Built-in Compiler Settings
die Commandline auf folgenden Wert:${COMMAND} ${FLAGS} -E -P -v -dD "${INPUTS}" -nostdinc -iwithprefix include
- Unter
- Öffnen Sie nun
Project Settings -> C/C++ General -> Paths and Symbols
- Unter
GNU C
/Includes
fügen Sie hinzu ..
<pfad zum kernel source>/include/ <pfad zum kernel source>/include/uapi/ <pfad zum kernel source>/arch/arm/include/ <pfad zum kernel source>/arch/arm/include/uapi
- unter
Symbols
erstellen Sie ein neues Symbol__KERNEL__
(2 Unterstriche) mit dem Wert1
. - ... zusätzlich setzen Sie
__GNU__
auf1
.
- Unter
- Führen Sie aus
Project -> C/C++ Index -> Rebuild
.
Nach erstellen eines einfachen Makefiles, sollte die IDE keine Fehler mehr melden. Überprüfen Sie ob die Kernel Headers gefunden werden, via CTRL+Click
auf printk
im Source.
Weitere Informationen finden Sie hier: https://github.com/eclipse-cdt/cdt/tree/main/FAQ#whats-the-best-way-to-set-up-the-cdt-to-navigate-linux-kernel-source
Kernelmodule
Der Begriff "Module" ist in der Softwarewelt mehrdeutig:
- Einerseits kann man darunter z.B. ein C Module verstehen, also eine C Quelldatei, welche mittels C-Compiler in ein Objektfile
*.o
übersetzt und zusammen mit ev. weiteren Objektfiles und Libraries zu einem Executable gelinkt wird, d.h. zu einem ausführbaren Programm. - Andererseits versteht man unter einem "Module" auch ein "Linux Kernel Module" (kurz
LKM
) , also eine spezielle Objektdatei, welche aus einem oder mehreren übersetzen und gelinkten C-Modulen generiert wird und dynamisch zum laufenden Kernel zugeladen werden kann.
Zwischen Kernel-Modules und Usermode-Programmen bestehen einige gravierende Unterschiede:
Ein LKM darf keine Funktionen aus Usermode-Libraries verwenden, weshalb Usermode Libraries (wie z.B. die
libc
) weder statisch noch dynamisch einem Kernelmodule zugelinkt werden darf! (Aus Usermode-Libraries werden ja Systemcalls aufgerufen, was aus dem geladenen Kernelmodule d.h. im Kernelmode nicht zulässig ist).Hingegen darf ein LKM direkt beliebige exportierte Funktionen des Kernels oder exportierte Funktionen bereits geladener Kernel-Module verwenden, denn der Kernel samt allen statischen Treibern plus allen nachgeladenen Kernel-Modules laufen ja im gleichen virtuellen Adressraum im Kernel-Space.
Folglich werden auch die Headerfiles der libc oder anderer Usermode-Libraries nicht verwendet. Statt dessen werden die Kernel-Headerfiles aus
<kernelsource>/include
verwendet, in welchen die vom Kernel exportierte Funktionen, Variablen, Kernel-Datentypen und Kernel-Macros deklariert sind.Das Laden eines Kernel-Modules geschieht hierbei...
- Entweder per
insmod <modulpfad>
wobei der relative oder absolute Pfad zum Kernel-Module inklusive Dateierweiterung.ko
(für "Kernel Object") angegeben werden muss, - Oder per
modprobe <modulname>
ohne Pfadangabe und ohne File-Extension, womit nur Kernelmodule gefunden werden, welche ordnungsgemäss unter/lib/modules/<kernelversion>/
installiert und perdepmod
indexiert wurden (vgl. Abschnitt "Treiber Laden" in Lab 2). Im Gegensatz zuinsmod
lädt modprobe falls nötig und noch nicht geladen auch alle vom angegebenen Modul benötigten Module in der richtigen Reihenfolge.[^1]
- Entweder per
Das Auflisten aller dynamisch geladenen Module erfolgt per
lsmod
. (Versuchen Sie's auf dem Hostsystem!)Das Entladen eines Kernel-Modules kann bei Bedarf per
rmmod <modulname>
geschehen oder alternativ permodprobe -r <modulname>
für rekursives Entladen auch der abhängigen Module.Ein "Linux Kernel Module" hat keine
main()
-Funktion und auch kein main-Thread, welcher über die Lebensdauer des geladenen Moduls läuft. Hingegen muss jedes Kernel-Module eine Funktion zwecks Initialisierung des Moduls beim laden sowie eine zwecks "Aufräumen" beim Entladen des Moduls bereitstellen. Im Quellcode eines Modules werden diese beiden Funktionen über di e C-Macrosmodule_init()
undmodule_exit()
dem Module-Loader (ein Bestandteil des Kernels) bekannt gemacht.Die mit
modul_init()
angegebene Funktion ist also nicht während der gesamten Lebensdauer des Modules aktiv sondern nur während der Modul-Initialisierung!Weiter haben Kernelmodules auch keine Standard-Ein-/Ausgabe, denn ein Kernel-Module läuft ja nie unter der Kontrolle einer Shell. Jedoch kann per Kernelfunktion
printk()
eine Text-Meldung in den Kernel-Log Ringpuffer geschrieben werden, dessen Inhalt erscheint ja z.B. auf der (seriellen) Konsole oder kann mitdmesg
auch nachträglich oder an anderen Konsolen angezeigt werden. Meist kopiert der Syslog-Daemon diesen Kernel-Log auch ins Syslog-File (/var/log/syslog
).
Einfaches Kernelmodul
- Studieren Sie den Inhalt des folgenden minimales Kernel-Modules:
c
#include <linux/module.h> /* Needed by all modules */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Darth Vader <anakin.skywalker@orbit.com>");
MODULE_DESCRIPTION("The ultimative linux kernel module");
static int __init hello_start(void) {
printk("mod_hello: Loading hello module...\n");
printk("mod_hello: Hello universe!\n");
return 0;
}
static void __exit hello_end(void) { printk("mod_hello: see you.\n"); }
module_init(hello_start);
module_exit(hello_end);
- und kopieren Sie diesen in einen neuen Projektordner nach:
~/esl/workspace/mod_hello/mod_hello.c
Die Kompilation eines Kernelmodules darf natürlich nicht wie gewohnt per einfachem gcc
-Aufruf erfolgen, denn die mit <...>
angegebenen Include-Dateien sollen ja nicht im üblichen System-Include-Path gesucht werden, sondern im include-Directory des Kernel-Source-Verzeichnisses. Weiter soll der Output ja im relozierbaren "Kernel Object Format" *.ko
erzeugt werden und nicht im üblichen ELF
-Format und es dürfen auch keine Standard C Libraries automatisch zugelinkt werden.
Glücklicherweise ist dies mit einem einfachen "Trick" ohne viel Aufwand realisierbar: Hierzu wird ein Makefile erstellt, in welchem einerseits das aus der Quelldatei zu erstellende Objektfile mod_hello.o
in der Variable obj-m
definiert wird. Weiter wird in der Default-Build Rule (all) nochmals make aufgerufen, diesmal aber so, das das Makefile im Kernel-Source-Verzeichnis abgearbeitet wird:
Einfaches Makefile
Makefile
obj-m = mod_hello.o
KERNEL_SRC = /lib/modules/$(shell uname -r)/build
all:
make -C $(KERNEL_SRC) M=$(PWD) modules
rm -f *.o *.mod.c modules.order Module.symvers
clean:
make -C $(KERNEL_SRC) M=$(PWD) clean
# indents in rules above have to be TAB characters! Check&correct after copy&paste out of PDF!
- Über die Variable obj-m wird also das Objektfile des zu generierenden Modules angegeben
*.o
, (Aber Achtung, nicht das Kernel-Output-File*.ko
). - Da wie oben erwähnt der Buildvorgang via
Makefile
aus dem Kernelsource-Verzeichnis erfolgen soll, muss unter der Default-Rule 'all' nochmals einen Aufruf von 'make' erfolgen, wobei diesmal mit Option-C
das Kernelsource-Verzeichniss übergeben wird (via VariableKERNEL_SRC
). Dadurch wird make nun vorgängig ins referenzierte Kernel-Verzeichnis wechseln (d.h. nach/lib/modules/<kernelversion>/build/
) und das dortige Makefile abarbeiten.[^2] - Über eine weitere Option
M=$(PWD)
wird dem Kernel-Makefile mitgeteilt, dass ein externes Modul kompiliert werden soll:M
wird hierzu einfach auf dem Pfad zu unserer Quelldatei gesetzt. (PWD
ist eine Shell-Umgebungsvariable, welche zum gegenwärtig aktive Verzeichnis zeigt)[^3] - Entsprechend muss also in obigem Makefile die Variable
KERNEL_SRC
so gesetzt werden, dass diese in ein Verzeichnis zeigt, welches das aktuelle Kernel-Makefile samt Build-Scripts und Kernel-Includefiles enthält! (/lib/modules/$(shell uname -r)/build
zeigt also auf jene des installierten resp. aktiven Host-Kernels, denn der Shell-Commanduname -r
ermittelt die Releasnummer des laufenden Kernels). - mit der Anweisung
@rm .... >/dev/null
werden alle unnötige Files wieder kommentarlos entfernt.
- Generieren Sie das Kernel-Module
mod_hello
durch ein simplesmake
im betreffenden Verzeichnis! - Falls dies misslingt, fehlt vermutlich noch das Ubuntu-Package linux-headers-generic (also installieren!) Es werden nämlich ja nicht die gesamten Kernel-Sourcen benötigt sondern nur die Kernel-Headerfiles sowie natürlich das Kernel-Makefile, welches auch in diesem Package ist!
- Testen Sie das erstellte Kernel-Module auf dem Hostsystem indem Sie dieses per
sudo insmod mod_hello.ko
laden - es sollte damit kommentarlos und fehlerlos geladen werden! - Die
printk()
-Anweisungen im Quellcode sind wie erwähnt bloss im Kernel-Log ersichtlich. Dieser kann einfach perdmesg
angezeigt werden, oder z.B. die letzten 25 Zeilen z.B. perdmesg | tail -25
. Stattdessen könnte auch das Ende des Syslog-Files betrachtet werden, pertail -25 /var/log/syslog
. - Entfernen Sie das Modul wieder per
sudo rmmod mod_hello
. Die entsprechende Meldung aus der permodule_exit()
angegebenen Funktion muss wiederum im Kernel-Log ersichtlich sein!
Fehlt das Macro-Statement MODULE_LICENSE("GPL")
im Modul, wird der Kernel "beschmutzt", was er prompt mit einer Meldung "tainted" (also "unrein") meldet - der Kernel ist ja GPL und erwartet dies auch von allen zugeladenen Modulen! Das Modul würde aber trotzdem korrekt ausgeführt.
Nun soll das Modul natürlich auch noch für unser Zielsystem cross-compiliert werden!
Hierzu kopieren Sie das
Makefile
auf ein FileMakefile-de1-soc
und ändern/ergänzen in dieseWechseln Sie zur Sicherheit noch einmal in den Kernel Source und führen Sie einen
make modules
mit entsprechendemexport ARCH=arm
export CROSS_COMPILE=<PFAD ZU IHRER TOOLCHAIN MIT PREFIX>
aus.
Makefile
# KERNEL_SRC = /lib/modules/$(shell uname -r)/build
KERNEL_SRC = ../../linux-stable
export ARCH = arm
export CROSS_COMPILE = arm-linux-gnueabihf-
Die ursprüngliche Variable KERNEL_SRC kommentieren Sie also per
#
aus und lassen diese über einen relativen Pfad auf Ihr Kernel-Quellverzeichnis des Target-Kernels zeigen!- Kontrollieren Sie aus dem Projektverzeichnis
mod_hello
ob bei Ihnen das Kernel-Quellverzeichnis tatsächlich zwei Ebenen höher unterlinux-stable/
liegt per:bashOder geben Sie alternativ alsls ../../linux-stable/
KERNEL_SRC
gleich den korrekten absoluten Pfad zu diesem an!
- Kontrollieren Sie aus dem Projektverzeichnis
Die restlichen Variablen
ARCH=
... undCROSS_COMPILE=
... kennen Sie ja bestens! Derart imMakefile
exportiert, müssen diese nicht auf dermake
-Kommandozeile angeben werden!- Achtung: am Ende dieser beiden Zeilen darf kein Leerschlag sein! (Ansonsten werden diese Umgebungsvariablen falsch definiert und in der Folge die CPU-Architektur resp. der Compilerprefix als ungültig definiert).
- Ergänzen Sie zudem die Rule
all
sodass nach dem Builden das erstellte Kernelmodulemod_hello.ko
ins/tmp
-Verzeichnis des Zielsystems kopiert wird. (vgl. Makefiles aus letzten Versuchen) - Rufen Sie nun
make -f Makefile-de1-soc
auf, worauf das Modul fehlerlos cross-kompiliert und auf das Zielsystem kopiert werden sollte. - Testen Sie den Erfolg auf dem Zielsystem (mittels
<insmod pfad_zum_kernnelmodule>
)
Linux Timer System
"Jiffies", "Timer Wheel" und "HZ" vs. "Tickless Kernel" und "High Resolution Timer"
Früher wurde bekanntlich der Linux-Systemtimer so konfiguriert, dass dieser Timer-Interrupts in einem fixen Zeitintervall von 10ms erzeugt, also mit einer Interruptrate von 100Hz resp. 100 "Jiffies" pro Sek.
Aus dieser Timer-Interruptroutine wurde dann einerseits die Systemzeit in einer Zählervariable "Jiffies" nachgeführt, andererseits auch das zeitgesteuerte Rescheduling von Prozessen angestossen - z.B. zwecks Time-Slicing (also das Resheduling bei lange laufenden Prozessen), geplante Delays (z.B. wenn ein Prozess die sleep()
-Funktion aufruft) sowie System-Timeouts (z.B. Netzwerk-Timeouts oder Warten mittels Semaphore mit Timeout) [^4].
Derartige "zeitlich geplanten Ereignisse" wurden über ein "Time Wheel" realisiert, d.h. über Jiffies-Tabellen, in welche die zu einem bestimmten Zeitpunkt wieder-aufzuweckenden Prozesse eingetragen werden und abgelaufene jeweils wieder entfernt.
Die erreichbare Genauigkeit und Zeitauflösung war damit höchstens ein Jiffie genau, also bestenfalls 10ms, oder bloss Nx10ms bei N wartenden rechenintensiven Prozessen auf einem Single-Core System!
Mit der zunehmenden Anzahl Prozesse und zunehmendem Multimedia-Bedarf moderner Desktop-Systeme (also weichen Echtzeit-Anforderungen d.h. "soft real-time requirements") taugte dieser Lösungsansatz als wie schlechter. Auch das erhöhen der Timer-Interrupt-Rate auf 250Hz oder gar 1000Hz brachte nur mässigen Erfolg. Hingegen stieg damit der Systemverwaltungs-"Overhead" beträchtlich aufgrund der steigenden Anzahl von Timer-Interrupts, Context-Switches, Prozess-Reshedulings, Cash-Flushes etc, woraus insgesamt eine niedrigere nutzbare Rechenleistung trotz höherem Energieverbrauch resultierte, was speziell bei batteriebetriebenen Systemen wie Notebooks oder Mobile-Devices sehr Nachteilig war.
Die Lösung für dieses Problem brachten Thomas Gleixner und Ingo Molnar mit dem Kernel-Patch: "High-res timers, tickless/dyntick and dynamic HZ" mit der Kernel-Version 2.6.17 [^5]:
Dieser "Patch" ersetzte das zuvor fix konfigurierte Timer-Interrupt-Intervall durch eine freies Timer-Intervall (Tickless Kernel, Dynamic HZ), wodurch das Rescheduling zu beliebigen Zeitpunkten und somit "exakt" zum nächsten gewünschten Zeitpunkt stattfinden konnte. Gleichzeitig werden dadurch auch unnötige Timer-Interrupts vermieden, sodass der Prozessor auch längere Zeit in einem energiesparenden Ruhezustand (C1..C7) verbleiben kann - was Linux den Durchbruch auch bei batteriebetriebenen Geräten ermöglichte.
Keine einfache Aufgabe, zumal dies mit nur einem einzigen Hardware-Timer effizient realisiert werden, sowie die Kompatibilität erhalten bleiben sollte (kompatibel zu Jiffies und HZ). Über "Variable Time Wheels" können seit dem effizient sowohl Ereignisse geplant oder auch wieder vorzeitig entfernt werden, was insbesondere bei Timeouts erforderlich ist (Timeouts laufen ja normalerweise nicht aus und sollten deshalb auch keine unnötigen Timer-Interrupts generieren...).
Die durchschnittliche Interrupt-Rate sowie z.B. die Resheduling-Rate von Prozessen kann mittels Programm powertop
schön visualisiert werden! (Leider aufgrund von Kernel-Versionsabhängigkeiten nicht immer korrekt...)
Installieren Sie das Ubuntu-Package powertop und führen Sie darauf in einer Shell 'sudo powertop' aus und warten Sie eine Weile ... , worauf Sie die grössten "Energiefresser" sehen sollten....
Aufgabe
Welcher Prozess, Interrupt-Quelle oder "Gerät" hat die höchste Rate?
Beachten Sie nach Taste '→' in welchem Stromsparmodi sich der Prozessors wie oft befindet (C0..C7)!
Im Vergleich hierzu generiert übrigens ein schlankes Display-loses System (wie z.B. ein Debian-basierter NAS-Server mit ARM-Prozessor) im Leerlauf bloss noch wenige Interrupts pro Sekunde!
Aber auch ohne powertop
lässt sich zumindest die Interrupt-Rate bei jedem Linux-System einfach bestimmen - indem Sie 2x hintereinander (z.B. mit 10s Abstand) die Interrupt-Counter aller Interrupt-Quellen per cat /proc/interrupts
auslesen und die Counter-Werte jeweils voneinander subtrahieren[^6].
Aufgabe
Welche Timer-Interrupt-Rate (gp timer) hat das Wandoard im Leerlauf?[^7]
- Per
cat /proc/timer_list
erhalten Sie weitere Informationen betreffend der verfügbaren "Kernel Timer": wenn der Wert von resolution = 1ns beträgt, sind die High Resolution Timer aktiv. Sie sollten diesen Wert aber nicht allzu ernst nehmen - betreffend der tatsächlich erreichbaren Zeitauflösung ist folgender Wert glaubwürdiger: min_delta_ns auf Host[^8]: .............. = ........ us , auf Target: ................. = ........ us
Interrupts im Linux Kernel
Interrupt-Routinen, softirqs, tasklets und Kernel-Threads
Interrupt-Routinen (ISRs) können unter Linux wie unter Windows nur im Kernel-Mode d.h. nur direkt im Kernel oder in Kernel-Modulen implementiert werden, also nicht in normalen Programmen im Usermode.
Interrupt-Routinen werden zudem unter Linux "Non-Nested" abgearbeitet, d.h. eine laufende ISR wird aus Effizienzgründen nie von anderen Interrupts unterbrochen sondern jeweils nacheinander abgearbeitet. Damit ist klar, dass die "Worst Case"-Latenzzeit zwischen Auftreten eines Interrupt-Ereignisses und dessen Bearbeitung (in der zugehörigen Interrupt-Routine) vom Auftreten und Laufzeit aller Interrupt-Routinen abhängt! Aus diesem Grund müssen lange Ausführungszeiten in Interrupt-Routinen unbedingt vermieden werden - und zwar bei allen ISRs - eine einzige nicht-kooperative ISR kann also fatale Folgen haben!
In einer Interrupt-Routine selbst soll deshalb nur das tatsächlich zeitkritische erledigt werden ("top half"). Der weniger kritische Teil der Ereignisbehandlung ("bottom half") soll hingegen aus dem Interrupt-Kontext in einen Kernel-Context ausgelagert werden und erst nach beenden der ISR und allen anderen gerade anstehenden ISRs ausgeführt werden. Während der Ausführung derartiger "bottom-half" Codefragmente ist dann die Bearbeitung von Interrupts wieder zugelassen.
Der "bottom-half" Teil einer Ereignis-Bearbeitung kann je nach Bedarf und Umfang unterschiedlich implementiert werden:
- Softirqs oder Tasklets: Diese werden normalerweise unmittelbar nach der ISR, also noch vor dem Rücksprung in den unterbrochenen Thread ausgeführt - in welchem Fall kein Rescheduling wie bei den Prozessen nötig ist. Sie erzeugen deshalb im Normalfall sehr wenig System-Overhead. Abgesehen von der Unterbrechbarkeit durch Interrupts bestehen jedoch ähnliche Restriktionen wie bei Interrupt-Routinen, d.h. sie haben eine höhere Priorität als alle (Kernel- oder Usermode-) Threads und dürfen selbst wiederum nicht blockieren: ein Warten per Aufruf von z.B. einer Sleep-Funktion oder das Warten auf eine Semaphore oder verriegeln kritischer Abschnitte per Mutex etc. ist also nicht zulässig![^9]
- Kernel-Threads: diese bieten ein vergleichbares Spektrum von Möglichkeiten wie die normalen Threads aus Usermode-Prozessen - ausser natürlich, dass diese keine Usermode-Libraries verwenden dürfen sondern bloss Kernel-Funktionen und -Makros sowie "unprotected" laufen! Kernel-Threads haben deshalb wie Usermode-Threads eine Priorität, dürfen vom Linux-Scheduler beliebig unterbrochen werden und dürfen selbst auch blockieren. Z.B. indem sie auf eine Kernel-Semaphore warten oder z.B. die Kernel-Funktion
sleep()
aufrufen - aber natürlich nicht via länger dauernde Warteschlaufen... - Eine weitere Möglichkeit wäre die "bottom-half" Bearbeitung im Usermode (statt wie oben im Kernel) durchzuführen, was Sie im nächsten Versuch kennen lernen (dabei wird ein Usermode-Thread, welcher per blocking read() oder write() Systemcall auf einen Char-Device in einem Treiber blockiert wird deblockiert...).
Übrigens, beim Linux Real-time Patch, mit welchem die Echtzeitfähigkeit von Linux ja verbessert werden kann, werden (fast) alle ISRs, Softirqs und Tasklets einfach zu "normalen" Kernel-Threads umfunktioniert. Die kurzen "echten" ISRs aktivieren (deblockieren) dann bloss noch diese "Interrupt-Kernel-Threads". Da diesen somit eine jeweils individuelle Ausführungsprioritäten zugewiesen werden kann, können die weniger dringlichen problemlos durch die dringlicheren oder auch durch hoch priorisierte Usermode-Threads unterbrochen werden - womit eine Grundvoraussetzung für Hard-Realtime-Systeme erreicht wird. Natürlich erfordert diese Verarbeitung aufgrund der Zunahme von Context Swiches einen Mehrbedarf an CPU-Ressourcen und damit auch einen höherem Energiebedarf, weshalb dieser Ansatz derzeit (noch) nicht standardmässig im Mainline Kernel integriert wird.
Latenzzeit testen
Im folgenden soll über einen "High Tesolution Timer" die Latenzzeit zwischen dem Timer-Interrupt-Ereignis und der "bottom-half"-Bearbeitung ermittelt werden.
Dies sollen Sie über nachfolgendes Kernel-Module mod_hrtimer
ermitteln, in welchem...
Ein High Resolution Timer gestartet wird,
- Welcher jeweils im 100ms-Intervall eine Callback Function aufrufen lässt - wobei es sich effektiv um ein Softirq handelt, welcher aus der System-Timer-Interruptroutine aktiviert wird.
- In dieser Callback Funktion wird der Aufrufzeitpunkt per
printk()
protokolliert, - Sowie eine Statistik über die Latenzzeit alle Aufrufzeitpunkte nachgeführt. Diese wird am Schluss der Versuchsreihe nach
NR_ITERATIONS
Versuchen ebenfalls perprintk()
ausgegeben.
Zum Vergleich startet das Modul zusätzlich einen Kernel-Thread, welcher ebenfalls Meldungen betreffend dessen effektiven Aufrufzeitpunkt ausgibt.
Studieren Sie den gesamten Sourcecode, wozu Sie sich am Besten von unten nach oben durcharbeiten!
Erstellen Sie ein Projektverzeichnis
~/esl/workspace/mod_hrtimer/
in welches Sie die beiden Makefiles aus dem Projektverzeichnis
mod_hello
hineinkopieren, undpassen Sie diese Makefiles an das zu übersetzenden Kernel-Modul an (insbesondere Variable
obj-m
)und erstellen Sie nachfolgendes Modul
mod_hrtimer.c
Danach builden Sie das Modul für das Hostsystem und testen dieses:
Bestimmen Sie durch wiederholtes Laden/Entladen des Modules und nachfolgendes
dmesg|tail -25
den gemessenen Bereich der Latenzzeiten:- Der Timer Callback-Funktion auf dem Hostsystem (min/max): .................. us
- Sowie der im Kernel-Task ausgeführten
sleep()
-Funktion (ev. das Module geeignet modifizieren, sodass diese Latenzzeiten ebenfalls ausgegeben oder statisch ermittelt werden!) .................. us - Merkwürdigerweise werden unter hoher Systemlast die Latenzzeiten erheblich kürzer, z.B. während dem Kompilieren des Kernels oder einfach per "sinnlosem"
while true; do echo >/dev/null; done
in einer anderen Shell (Abbruch mitCtrl-C
). Wie erklären Sie sich das?
Ermitteln Sie die Latenzzeiten nach cross-compileren auch auf dem Wandoard:
Wenn Sie noch Zeit und Lust haben, ermitteln Sie auch noch den Einfluss bei zusätzlicher Prozessor-Last d.h. vielen Context-Switches und/oder hoher IO-Last. (Z.B. Kernel kompilieren mit
make
-Option-j 10
)
mod_hrtimer.c
c
/*
* File: mod_hrtimer.c
* Autor: Matthias Meier
* Aim: simple latency test of high res timers
*
* Based on an article "Kernel APIs, Part 3: Timers and lists in the 2.6 kernel"
* by M.Tim Jones:
* http://www.ibm.com/developerworks/linux/library/l-timers-list/
*
* Remarks:
* - The timer subsystem is documented here:
* http://www.kernel.org/doc/htmldocs/device-drivers/
* - For a simple test without additional cpu-load enter:
* insmod mod_hrtimer.ko; sleep 3; dmesg | tail -25 ; rmmod mod_hrtimer
* - For additional CPU load use 'hackbench' or (less heavy) by parallel kernel
* compile (eg. make -j 20)
*/
#include <linux/delay.h>
#include <linux/hrtimer.h>
#include <linux/kthread.h>
#include <linux/module.h>
MODULE_AUTHOR("M. Tim Jones (IBM)");
MODULE_AUTHOR("Matthias Meier <matthias.meier@fhnw.ch>");
MODULE_LICENSE("GPL");
#define INTERVAL_BETWEEN_CALLBACKS (100 * 1000000LL) // 100ms (scaled in ns)
#define NR_ITERATIONS 20
static struct hrtimer hr_timer;
static ktime_t ktime_interval;
static s64 starttime_ns;
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer) {
static int n = 0;
static int min = 1000000000, max = 0, sum = 0;
int latency;
s64 now_ns = ktime_to_ns(ktime_get());
hrtimer_forward(&hr_timer, hr_timer._softexpires,
ktime_interval); // next call relative to expired timestamp
// calculate some statistics values...
n++;
latency = now_ns - starttime_ns - n * INTERVAL_BETWEEN_CALLBACKS;
sum += latency / 1000;
if (min > latency) min = latency;
if (max < latency) max = latency;
printk("mod_hrtimer: my_hrtimer_callback called after %dus.\n",
(int)(now_ns - starttime_ns) / 1000);
if (n < NR_ITERATIONS)
return HRTIMER_RESTART;
else {
printk(
"mod_hrtimer: my_hrtimer_callback: statistics latences over %d hrtimer "
"callbacks: "
"min=%dus, max=%dus, mean=%dus\n",
n, min / 1000, max / 1000, sum / n);
return HRTIMER_NORESTART;
}
}
static int init_module_hrtimer(void) {
printk("mod_hrtimer: installing module...\n");
// define a ktime variable with the interval time defined on top of this file
ktime_interval = ktime_set(0, INTERVAL_BETWEEN_CALLBACKS);
// init a high resolution timer named 'hr_timer'
hrtimer_init(&hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
// set the callback function for this hr_timer
hr_timer.function = &my_hrtimer_callback;
// get the current time as high resolution timestamp, convert it to ns
starttime_ns = ktime_to_ns(ktime_get());
// activate the high resolution timer including callback function...
hrtimer_start(&hr_timer, ktime_interval, HRTIMER_MODE_REL);
printk(
"mod_hrtimer: started timer callback function to fire every %lldns "
"(current jiffies=%ld, HZ=%d)\n",
INTERVAL_BETWEEN_CALLBACKS, jiffies, HZ);
return 0;
}
static void cleanup_module_hrtimer(void) {
int ret;
ret = hrtimer_cancel(&hr_timer);
if (ret) printk("mod_hrtimer: The timer was still in use...\n");
printk("mod_hrtimer: HR Timer module uninstalling\n");
}
module_init(init_module_hrtimer);
module_exit(cleanup_module_hrtimer);
[^1]: Der Dateipfad zum Modul sowie die Abhängigkeiten zu anderen Modulen findet modprobe in den Files /lib/modules/<kernelversion>/modules
, welche per depmod
beim Installieren der Module erzeugt wurden. [^2]:
Auf dem Hostsystem sind unter diesem Verzeichnis nicht wirklich alle Kernel-Sourcen, sondern bloss das Kernel-
Makefile, sowie einige Build-Scripts und die Kernel-Headerfiles - zwecks Kompilation von Kernel-Modulen.
[^3]: vgl. <KERELSRC>/Documentation/kbuild/modules.txt
* resp. https://www.kernel.org/doc/Documentation/kbuild/modules.txt [^4]: Kurze Delays im Bereich unter 1ms wurden hingegen durch Verzögerungsschalufen realisiert. [^5]: Announce: High-res timers, tickless/dyntick and dynamic HZ: https://lkml.org/lkml/2006/6/18/113 [^6]: Ein periodisches Ausführen eines Commands ist auch per 'watch' möglich, z.B.: watch -n 10 "cat /proc/interrupts"
[^7]: Eine mit powertop
vergleichbare Ausgabe (nur) der "Timer-Konsumenten", ist nach einschalten der "Timer-Statistik" per *echo 1 >/proc/timer_stats*
möglich per cat /proc/timer_stats | sort -nr | head -n 20
[^8]: Im CPU-Standby (C-States) funktionieren die lapic timer auf dem Host nicht, sondern nur noch der HPET-Timer. [^9]:
Ein softirq darf zwar nicht blockieren, jedoch darf er wiederum neue sofirqs aktivieren. Bei einer grossen Anzahl anstehender softirqs, bestünde ausserdem die Gefahr, dass die Usermode-Prozesse gar keine CPU-Ressourcen mehr erhalten. Gesetztenfalls werden die softirqs in hierfür eigens bereitstehende Kernel-Threads ausgelagert, welche gleich priorisiert sind wie die Usermode-Prozesse. Ein ps -elf | grep ksoftirqd
zeigt für jede vorhandene CPU den bereitstehenden ksoftirqd
Kernel-Thread(s) und wieviel CPU-Zeit diese seit Systemstart schon erforderten. Zwischen softirqs
und tasklets
besteht übrigens nur ein kleiner Unterschied im Zusammenhang mit Mehrprozessor-Systemen: dar gleiche softirq
darf gleichzeitig auf mehreren Prozessoren laufen, weshalb softirqs generell "reentrant" sein müssen und nicht-lokalen Datenzugriff via "locks" regeln müssen. Das gleiche tasklet läuft hingegen immer nur einmal, weshalb tasklets nicht "reentrant" sein müssen.
Ergänzende Infos zu Realtime unter Linux
- Video: Präsentation RT Patches Mainline Linux (2019) https://embedded-recipes.org/2019/talks/rt-is-about-to-make-it-to-mainline-now-what