Skip to content
On this page

6. 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.

  1. Installieren Sie die snap version von Eclipse im Ubuntu Software Center.
  2. Starten Sie Eclipse und erstellen Sie einen neuen Workspace.
  3. Unter Help -> Install new Software wählen Sie --All Available Sites--.
  4. 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
    
  5. Erstellen Sie ein neues C Projekt mod_hello.
  6. Wählen Sie Makefile Project -> Empty Project und Linux GCC.
  7. 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.

  8. Öffnen Sie Project Settings -> C/C++ General -> Preprocessor Include Paths, Macros etc.
    1. Unter GNU C wählen Sie CDT User Setting Entries -> Add.
    2. Wählen Sie den Typ Preprocessor Macro File.
    3. 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.
    4. Unter dem Tab Providers ändern Sie bei CDT GCC Built-in Compiler Settings die Commandline auf folgenden Wert: ${COMMAND} ${FLAGS} -E -P -v -dD "${INPUTS}" -nostdinc -iwithprefix include
  9. Öffnen Sie nun Project Settings -> C/C++ General -> Paths and Symbols
    1. 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
    
    1. unter Symbols erstellen Sie ein neues Symbol __KERNEL__ (2 Unterstriche) mit dem Wert 1.
    2. ... zusätzlich setzen Sie __GNU__ auf 1.
  10. 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 per depmod indexiert wurden (vgl. Abschnitt "Treiber Laden" in Lab 2). Im Gegensatz zu insmod lädt modprobe falls nötig und noch nicht geladen auch alle vom angegebenen Modul benötigten Module in der richtigen Reihenfolge.[^1]
  • 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 per modprobe -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-Macros module_init() und module_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 mit dmesg 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 Variable KERNEL_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-Command uname -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 simples make 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 per dmesg angezeigt werden, oder z.B. die letzten 25 Zeilen z.B. per dmesg | tail -25 . Stattdessen könnte auch das Ende des Syslog-Files betrachtet werden, per tail -25 /var/log/syslog .
  • Entfernen Sie das Modul wieder per sudo rmmod mod_hello. Die entsprechende Meldung aus der per module_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 File Makefile-de1-soc und ändern/ergänzen in diese

  • Wechseln Sie zur Sicherheit noch einmal in den Kernel Source und führen Sie einen make modules mit entsprechendem export 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 unter linux-stable/ liegt per:
      bash
      ls ../../linux-stable/
      
      Oder geben Sie alternativ als KERNEL_SRC gleich den korrekten absoluten Pfad zu diesem an!
  • Die restlichen Variablen ARCH=... und CROSS_COMPILE=... kennen Sie ja bestens! Derart im Makefile exportiert, müssen diese nicht auf der make-Kommandozeile angeben werden!

    • Achtung: am Ende dieser beiden Zeilen darf kein Leerschlag sein! (Ansonsten werden diese Umge­bungs­variablen 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 Kernelmodule mod_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 ver­mie­den 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 Resche­du­ling 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-Sema­phore 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 per printk() 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, und

  • passen 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 mit Ctrl-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