Skip to content
On this page

7. Character Devices

Im Folgenden soll Ihrem Kernel-Module vom letzten Versuch zusätzlich ein "Character-Device-Interface" zugefügt werden, sodass Usermode-Programme (also normalen Anwendungen) mit diesem Treiber kommunizieren können ... gem. dem UNIX Motto "das Filesystem ist alles" also über File-Systemcalls auf einen "eigenen" Devicenode und kernelseitig weiter über den VFS-Layer zum eigenen "Device Driver":

Device Driver

Kopieren Ihr Verzeichnis mod_hrtimer auf mod_hrtimer_dev und korrigieren Sie sowohl den Quelldateiname sowie die Makefiles entsprechend. Nur falls Ihr Kernelmodule nicht korrekt läuft, finden Sie im Anhang dieses Versuchs eine Vorlage.

Character Device Interface regiestrieren

Wir erinnern uns: wird aus einer Anwendung ein Character Device Node über den Systemcall open() "geöffnet" und darüber via read(), write(), ioctl(), etc. kommuniziert, werden diese Library-Aufrufe jeweils über die gleichnamigen (File-)Systemcalls im Kernel über den VFS-Treiber (Virtual File System Switch) an jenen Treiber weitergeleitet, welcher sich unter der im Devicenode angegebenen Major-Node-Nummer registriert hat. (Natürlich könnte man auch gepuffert via C Streames verfahren ...)

Einen derartigen "Treiber mit Device-Interface" wollen wir nun selbst erstellen: Die Zuweisung des Treibers findet ja über die im Device-Node definierte Major-Node-Nummer statt, der Treiber muss sich folglich bei dessen Initialisierung unter der gewünschten Major-Nummer registrieren!

Etliche Major-Nummern sind fix reserviert, denn bei früheren Unix- und Linux-Installationen wurden die Device-Nodes schon bei der Systeminstallation fix unter /dev erstellt und zu diesen Systemkonfi­gu­ra­tionen soll auch ein aktueller Kernel noch rückwärtskompatibel sein. (s. <Kernelsource>/Documentation/devices.txt)

In den wenigen Treibern mit fixer Major-Node-Nummer wird deshalb die Major-Id jeweils im Treiber hardcodiert. (z.B. der Devicenode /dev/console mit fixer Major=5 sowie Minor=1).

Die allermeisten Device-Treiber reservieren jedoch nunmehr keine fixe Major-Nummer mehr ^[Prinzipiell würden aktuelle Linux-Releases auch Major-Nummern > 255 unterstützen]:

  • Übergibt ein Character-Device Driver bei der Registrierung des Deviceinterfaces per register_chrdev() als Major-Nummern den Wert 0, so registriert er sich unter einer dynamisch zugewiesenen Major Nummer. So registriert kann der Treiber den gesamten Minor-Bereich (0..255) frei nutzen (z.B. zwecks Zuweisung verschiedener gleichartigen "Ressourcen" wie z.B. Schnittstellen oder Partitionsnummern etc). Dynamisch zugewiesene Devicenodes vergibt der Kernel von 254 rückwärts (vgl. cat /proc/devices auf dem Hostsystem).

  • Benötigt hingegen ein Treiber nur einen Minor-Teilbereich sollte er sich mit register_chrdev_region() registrieren, oder falls er den Minor-Bereich überhaupt nicht benötigt, per misc_register(), wodurch die Treiber-Identifikation im VFS-Layer nicht nur durch die Major-Nr sondern auch durch den zugewiesenen Minor-Node-Nummer Teilbereich erfolgt.

Obwohl es etwas verschwenderisch ist, einen ganzen Minor-Bereich zu reservieren, registrieren wir der Einfachheit halber unseren Treiber per register_chrdev() und erstellen den Devicenode vorerst bloss manuell per mknod Command (also nicht hotplug-mässig per udev, mdev oder via devtmpfs) ...

Bei der Treiber-Registrierung mittels register_chrdev() werden gleichzeitig auch die vom Character-Device-Treiber zur Verfügung gestellten Filefunktionen beim Virtual-File-System-Layer (VFS) registriert.

Hierzu übergibt man der Funktion register_chrdev() die Adresse einer initialisiert Struct-Variable vom Typ file_operations, dessen Datentyp in <linux/fs.h> definiert ist. In dieser Struct-Variable werden die Adressen der zur Verfügung gestellten Filefunktionen definiert, z.B:

c
static struct file_operations my_fops = {
  .owner = THIS_MODULE,
  .read = my_read,
  .write = my_write,
  .open = my_open,
  .release = my_release,
};

Im obigen Beispiel wird also eine Variable my_fops vom Typ struct file_operations statisch definiert und in deren Felder read, write, open und release die Adressen der C-Funktionen my_read(), ..., my_release() zugewiesen. Denn ein Funktionsname ohne nachfolgende Parameter-Klammern stehtfür die Anfangsadresse der betreffenden Funktion, führt also keinen Funktionsaufruf durch - die erstellte struct-Variable ist also effektiv eine Sprungtabelle resp. Vectortabelle!

Daneben wären noch ca. 20 weitere File-Funktionen über diesen Struct definierbar, wie ioctl, flush, mmap, lseek, ... Da im obigen Beispiel diese nicht angegeben wurden, würde diesen folglich NULL zugewiesen. Durchgeführt wird diese Registrierung normalerweise in der module_init Funktion (vgl. letzter Versuch) wobei mit register_chardev() gleichzeitig auch die dynamisch angeforderte Major-Nummer registriert wird:

c
major = register_chrdev(0, "mod_hrtimer_dev", &my_fops);
if (major < 0) {
	printk("mod_hr_timer: error, cannot register the character device\n");
	return major;
}

Dabei muss major als modulglobale statische integer-Variable angelegt werden, denn die zugewiesenen Major-Nummer wird beim Entladen des Treiber (in der module_exit-Funktion) wieder benötigt zwecks:

c
unregister_chrdev(major, "mod_hrtimer_dev");

Weiter müssen natürlich die angegeben Filefunktionen zumindest minimal implementiert werden, sodass...

  • Ein Usermode-Programm den zugehörigen Devicenode per Systemcall open() öffen sowie per Systemcall close() wieder schliessen kann. Ein Aufruf von open() auf unseren Devicenode führt also den Systemcall open() auf dem VFS-Layer aus, welcher wiederum gemäss der Treiberregistrierung die in my_fops definierte Funtion my_open() ausführt...
  • Unsere minimale Implementation von my_write() soll vorerst bloss die vom Usermode-Programm per write() übergebenen Daten in einer modulinternen Variable mydata[] speichern.
  • Und my_read() diese in mydata[] gespeicherten Daten wieder zurückliefern - nach belieben an das gleiche oder an ein beliebig anderes Usermode-Programm, welches diesen "Device" öffnet.
c
static char mydata[100];

static int my_open(struct inode *inode, struct file *filp) {
	return 0;	// SUCCESS zurueckmelden
}

static int my_release(struct inode *inode, struct file *filp) {
	return 0;	// SUCCESS zurückmelden
}

static ssize_t my_read(struct file *filp, char *buf, size_t count, loff_t *f_pos) {
	if (strnlen(mydata,sizeof(mydata))<count) 	// mehr Daten angefordert als in mydata[] vorhanden?
		count = strnlen(mydata,sizeof(mydata)); 	// wenn ja, count entsprechend dezimieren
	__copy_to_user (buf, mydata, count); 	// Daten aus Kernel- in Userspace kopieren
	mydata[0]=0;	// und lokale Daten "loeschen" womit naechstes read() EOF
	return count;	// Zurueckmelden wieviele Bytes effektiv geliefert werden
}

static ssize_t my_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos) {
	if (sizeof(mydata)<count) 	// mydata[] Platz fuer gelieferte Daten?
		count = sizeof(mydata); 	// wenn nicht entsprehchend begrenzen!
	__copy_from_user(mydata, buf, count);	// Daten aus User- in Kernel-space kopieren
	return count;	// Zurückmelden wieviele Bytes effektiv konsumiert wurden
}

  • Studieren Sie die Inhalte dieser Funktionen und beachten Sie dabei insbesondere folgendes:
    • Die Parameterliste der definierten File-Funktionen ist nicht identisch mit jenen der zugehörigen Systemcalls, da die implementierten Funktion ja nicht direkt sondern via VFS-Layer aufgerufen werden (Erst im VFS-Layer wird ja die Verzweigung zum entsprechenden Treiber vorgenommen, die File-Permissions überprüft, über den Zustand der geöffneter File-Handles Buch geführt, etc. etc.)
    • die Funktionen copy_to_user() sowie copy_from_user() sind im Includefile <linux/uaccess.h> deklariert. Diese kopieren die Nutzdaten zwischen Kernel- und Userspace: wird aus einem Usermode-Prozess ein Systemcall aufgerufen, läuft dieser User-Thread nach Context-Switch in den Kernelmode weiter, jedoch nun mit höheren CPU-Privilegien und zusätzlichem Kernel-Paging, sodass beliebig auf den Virtual-Memory-Bereich des Kernels und des Prozesses zugegriffen werden kann. Die erwähnten Funktionen kopieren also bloss die referenzierten Daten zwischen diesen beiden Bereichen auf möglichst effiziente Weise (d.h. CPU-spezifisch!). Die Daten werden dabei effektiv hin- und her-kopiert und nicht nur "gepaged" wie es mit dem File-Systemcall mmap() möglich wäre und z.B. beim dynamisch Linken von Libraries genutzt wird!
    • Gerade die read()- und write()-Filefunktionen eines Treibers sind also extrem sicherheitskritisch, weshalb ein Buffer-Overflow in diesen Funktionen unbedingt verhindert werden muss - sowohl in der Usermode-Applikation und erst recht im Kernel! Es darf also höchstens soviel Speicherplatz kopiert werden, wie quellenseitig Daten vorhanden sind resp. Zielseitig effektiv Speicherplatz reserviert resp. angefordert wurde, weshalb der Übergabeparameter count jeweils überprüft und bei Bedarf korrigiert werden muss (vgl. obige Funktionen my_read() und my_write())!
  • Bauen Sie die obigen Codefragmente an geeigneter Stelle in das Module mod_hrtimer_dev ein, also die Definition des Structs my_fopts, die Filefunktionen, die Registrierung und De-Registrierung dieses Device-Interfaces, die Definition der beiden modul-globalen Variablen sowie das Includen von <linux/fs.h> . (Entweder per gedit oder mittels Eclipse analog dem in V4 erstellten C Makefile Project).
  • Übersetzen Sie das Modul für das Hostsystem und korrigieren Sie etwelche Fehler (Tipp: es könnte ja z.B. an der falschen Code-Reihenfolge liegen! Bevor in C etwas referenziert werden darf, muss es definiert oder zumindest deklariert werden. Oder z.B. an fehlenden Include-Files...)
  • Nach Laden des Modules per inspizieren Sie auf folgende Weise, welche Major-Id Ihrem Character Device Treiber namens mod_hrtimer_dev zugewiesen wurde:
    bash
    cat /proc/devices
    
    zugewiesene Major-Nummer:
  • Leider erstellt devtmpfs (resp. udev oder mdev) noch nicht auto­matisch einen Devicenode unter /dev/ . Erstellen Sie deshalb manuell den fehlenden Devicenode mit schreib-leserecht im aktiven Verzeichnis:
    bash
    sudo  mknod   -m 666  mod_hrtimer_dev  c  <major>  0
    
    (für <major> den zuvor ermittelten Wert!)
  • Und testen Sie die Funktionalität, indem Sie per echo ein write() und per cat ein read() durchführen: bash echo 'hoffentlich klapps!' >mod_hrtimer_dev cat mod_hrtimer_dev

    Major Nummer

    Übrigens ist natürlich überhaupt nicht gewährleistet, dass Ihr Kernel-Modul beim Laden immer die gleiche Major-Nummer erhält - insbesondere wenn die Lade-Reihenfolge der Module ändert!

Latenzzeit-Statistik über Device-Interface ausgeben

Nun soll der Treiber noch so abgeändert werden, dass ein read() auf auf den Devicenode statt der Daten aus write() die in mod_hrtimer_dev gemessene Timer-Latenzzeit-Statistik zurückliefert (also die berechnete Statistik über das Device-Interface übermittelt werden kann und nicht wie beim Letzten Versuch bloss in den Kernel-Log schreibt!).

  • Ändern Sie hierzu die Timer Callback-Funktion my_hrtimer_callback(), sodass die Latenzzeit-Statistik nach Abschluss der Messreihe zusätzlich in die modulglobale Variable mydata[] geschrieben wird, was mit der Kernel-Funktion sprintf() machbar ist. Diese Funktion ist äquivalent zur gleichnamigen libc-Libraryfunktion (s. man sprintf), schreibt also in ein übergebenes char-Array und nicht wie printf() auf stdout resp. printk() in den Kernel-Log.
  • Ohne zusätzliche Vorsichtsmassnahme bestünde aber damit die Gefahr, dass über das Ende der Variable mydata[] hinaus geschrieben wird, was ein Buffer-Overflow im Kernel und im (noch) günstigsten Fall eine "Kernel-Panik" bewirken würde - im ungünstigsten Fall könnte so vielleicht ein Virus einge­schleust und mit Kernelrecht ausgeführt werden! Um dies zu vermeiden, verwenden Sie statt sprintf() die Funktion snprintf(), bei welcher die Maximalzahl zu schreibender Zeichen angegeben werden kann! Re-compilieren Sie den Treiber und testen Sie nach entladen und neu laden des Treibers per cat .
  • Auf der Console sollte nun die Ausgabe doppelt erscheinen: von kprintf() als Console-Log sowie über das Deviceinterface und cat. Den Console-Log können Sie nach Ctrl-C per dmesg -n 1 abschalten - oder Sie loggen sich vom Hostsystem aus per ssh ein (der Console-Log erscheint nur auf der seriellen Console).

Blocking Read

In einem letzten Schritt soll nun my_read() noch so erweitert werden, dass diese Funktion jeweils....

  • Zuerst eine neue Latenzzeit-Messreihe per Timer-Callback-Funktion startet,
  • Danach wartet, bis die neue Messreihe in der Timer-Callback-Funktion ganz durchgeführt ist,
  • Und danach die neuen Statistik-Werte aus dem Array mydata[] in den Userspace kopiert und zurückspringt. Diese read()-Funktion soll also....
  • Eine gewisse Zeit blockieren, weshalb man von einem "blocking read" spricht.
  • Dies soll durch ein passives warten erfolgen, d.h. nicht durch unsinniges "verschwenden" von Prozessor-Zeit in einer Warteschlaufe oder durch unnötig vieler Task-Switches z.B. durch Aufrufe von msleep() in einer while-Schlaufe...
  • Die korrekte Lösung hierfür ist eine (binäre) Semaphore, was im Linux-Kernel mittels einer Wait Queue einfach und effizient realisiert werden kann: der wartende Task stellt sich hierbei in die betreffende Wait Queue und bleib dort solange passiv blockiert, bis er aus unserer Timer-Callback-Funktion wieder geweckt, und damit de-blockiert wird.
  • Inkludieren Sie <linux/wait.h> und erstellen Sie über folgendes Makro modul global eine Wait Queue namens wq (also im Modulrumpf im statischen Variablenbereich):
    c
    static DECLARE_WAIT_QUEUE_HEAD(wq);
    
  • Die Funktion my_read() wird ja von einem Userspace-Thread via Deviceinterface und VFS aufgerufen. Gleich zu Beginn dieser Funktion blockieren Sie den Thread wie folgt: wait_event_interruptible(wq, triggered);
  • Danach, also wenn der Thread deblockiert wurde, setzen Sie die Variable triggered zurückgesetzt. Deklarieren Sie diese Variable modulglobal als int (also als static int triggered=0 im Modulrumpf).
  • Das Aufweckenresp. Signalisieren der Wait Queue (wq) muss in der Callback-Funktion geschehen, also in my_hrtimer_callback() gleich nach Beenden der gesamten Messreihe (nach erstellen des Strings) per:
    c
    wake_up_interruptible_sync(&wq);
    
  • Unmittelbar zuvor setzen Sie auch die modulglobale Variable triggered=1;
  • Und als Vorbereitung für die nächste Messreihe setzen die Statistik-Variablen (n, min, max, sum) auf ihren Anfangswert zurück.

    Beenden / Signalisieren

    durch Verwendung der interruptible Variante von wait_event(), wird der blockierte Thread nicht nur beim Signanlisiseren der "Wait Queue" sondern auch über ein Systemsignal SIGINT deblockiert – im Falle z.B. der Usermode-Prozess per kill oder Ctrl-C zum Beenden aufgefordert wird.

  • Vervollständigen Sie das Kernel-Module noch weiter, sodass...
  • in my_read() noch vor dem wait_event_interruptible() der Event-Timer neu getriggert wird (durch aktualisieren der Variable starttime_ns und Aufruf von hrtimer_start() wie bei module_init)
  • und zudem in my_hrtimer_callback() nach Beenden und speichern der Messresultate als Vorbereitung für die nächste Messreihe die lokalen Statistik-Variablen zurückgesetzt wird.
  • Erstellen und testen Sie das Modul – es sollte nun bei cat /dev/mod_hrtimer_dev alle 20 x 100ms = 2s eine Zeile ausgeben!
  • Testen Sie dieses auch auf dem Zielsystem - nach cross-kompilieren per ..................................... , sowie laden des Modules und erstellen des richtigen Deivcenodes!
  • Vervollständige Sie zur Übersicht noch folgende Tabelle:
AktionUser functions (unbuffer / buffered)Kernel function (in mod_hrtimer_dev)
load module
open device
read device
write device
close device
remove module

Blockieren

Wie im letzten Versuch erklärt, darf die Timer-Callback-Funktion keinesfalls blockieren, denn diese läuft ja als Tasklet und nicht als (vollwertiger) Kernel-Thread (vgl. letzter Versuch). Ein Aufruf von wait_event() oder wait_event_interruptible() etc. wäre hier also unzulässig! Hingegen darf aus dieser ein signalisieren der Waitqueue erfolgen, sofern selbiges nicht blockiert. Die Funktion wake_up_interruptible_sync() verhindert eben dieses.

Ansteuerung ohne Major Node Nummer

Da wir im erstellten Character Device Interface ja bloss die File-Funktionen read() und write() implementierten, hätten wir selbige Funktionen ebensogut unter einer Sysfs Class registrieren können. Derart wäre kein Devicenode und auch keine Major-Node-Nummer-Registrierung nötig gewesen... (vgl. GPIO-Ansteuerung unter /sys/class/gpio/in einem früheren Versuch).

Devicenode automatisch erstellen lassen

in Problem ist natürlich, dass die bezogene Major-Nummer ja variieren kann: ändert die Anzahl oder die Lade-Reihenfolge der Module mit dynamischer Major-Nummer, stimmt die Major-Node-Nummer im manuell definierten Devicenode nicht mehr mit jenem in der Treiberregistrierung überein und es würde in der Folge irrtümlich mit einem falschen Treiber kommuniziert, z.B. mit dem RTC-Treiber! Um den Devicenode automatisch (in unserem Fall durch devtmpfs) erstellen zu lassen, muss das Kernel-Module zusätzlich beim "Kernel Device Model" registriert werden (das Kernelmodule erscheint damit unter /sys/class/ ... und der Devicenode automatisch unter /dev/... ).

  • Includen Sie hierzu <linux/device.h> und definieren Sie z.B. folgende modulglobale Pointervariable:
    c
    static struct class *mydev_class;
    
  • In der module_init Funktion, z.B. nach dem Registrieren des Device-Interfaces, erzeugen und registrieren Sie wie folgt eine eigene Sysfs Geräte-Klasse:
    c
    mydev_class = class_create("mod_hrtimer_dev");
    device_create(mydev_class, NULL, MKDEV(major,0), NULL, "mod_hrtimer_dev");
    
  • In der modul_exit Funktion de-registrieren und entfernen Sie diese wieder:
    c
    device_destroy(mydev_class,MKDEV(major,0));
    class_unregister(mydev_class);
    class_destroy(mydev_class);
    

Der Class-Name sowie der Device-Name kann dabei beliebig sein, sinnvollerweise aber ähnlich oder identisch wie der Modulename.

Damit wird der gewünschte Devicenode /dev/mod_hrtimer_dev automatisch von devtmpfs (oder udev oder mdev) generiert – defaultmässig mit Dateirecht 600 root root weshalb nur Prozesse unter UID root Zugriff darauf haben.

Sollen auch Prozesse unter anderen UIDs auf den Devicenode zugreifen, müssten die Permissions des Devicenodes nachträglich (also nach Laden des Treiber) angepasst werden - denn im Treiber ist dies nicht möglich.^[Ob das automatische Generieren von Devicenodes genrell im Kernel oder im Userspace durch ein Dienstprogramm erfolgen soll, wurde in der Kernel-Community lange kontrovers dikutiert. Man einigte sich darauf, dass das Erstellen der Devicenodes automatisch vom Kernel via devtmpfs möglich sein soll, wodurch die Devicenodes beim Booten früh vorhanden sind. Das Ändern der Zugriffsrechte soll hingegen aus flexibilitätsgründen aus dem Userspace erfolgen - üblicherweise per udev-Daemon oder auf schlanken auf Busybox basierenden Embedded Systemen z.B. per mdev Daemon oder einfachem 'chmod' Command aus dem Bootscript... ] Bei den meisten Linux-Distributionen ist hierfür der udevd oder der systemd-udevd Daemon zuständig, welcher standardmässig die Regeln unter /usr/lib/udev/rules.d/ berücksichtigt. Relativ einfach können aber auch eigene so genannte udev rules unter /etc/udev/rules.d/ ergänzt werden.

Beispielsweise würde durch zufügen z.B. der Datei /etc/udev/rules.d/99-ebssd-mod-hrtimer.rules mit folgen­dem Inhalt unseren Devicenode beim nächsten Module-Load auf Zugriffsrecht 666 (rw-rw-rw-) korrigieren:

bash
SUBSYSTEM=="mod_hrtimer_dev", MODE="0666"

Auf einem schlanken Embedded System ohne udev-Daemon resp. ohne systemd könnten derartige Korrekturen alternativ auch einfach mittels chmod und chown z.B. einfach aus dem Bootscript nach Laden des Kernelmodules oder durch Starten des busybox mdev Daemon und Zufügen einer mdev Regel in /etc/mdev.conf erfolgen (vgl. busybox/examples/mdev.conf).

Literaturhinweise und Quellen

Vorlage für den Versuch

(Vorlage für mod_hrtimer_dev.c – oder eigene Lösung aus V8 verwenden!)

c
/*
 * File:  Vorlage__mod_hrtimer_dev.c
 * Autor: Matthias Meier
 * Aim:   template for the char device interface (it measures the highres timer
 * latency by executing a timer callback )
 */

#include <linux/delay.h>
#include <linux/hrtimer.h>
#include <linux/kthread.h>
#include <linux/module.h>

#include <linux/fs.h>       // struct file_operations, register_chrdev()
#include <linux/uaccess.h>  // copy_from_user(), copy_to_user()
#include <linux/wait.h>     // wait_event_interruptible()

MODULE_AUTHOR("Matthias Meier <matthias.meier@fhnw.ch>");
MODULE_AUTHOR("please add your name here if you change this file!!");
MODULE_LICENSE("GPL");

#define MY_MODULE_NAME "mod_hrtimer_dev"

#define INTERVAL_BETWEEN_CALLBACKS (50 * 1000000LL)  // 50ms  (scaled in ns)
#define NR_ITERATIONS 40

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(MY_MODULE_NAME ": my_hrtimer_callback called after %dus.\n", (int)
  // (now_ns - starttime_ns)/1000 );

  if (n < NR_ITERATIONS)
    return HRTIMER_RESTART;
  else {
    printk(MY_MODULE_NAME
           ": my_hrtimer_callback latency over the last %d hrtimer callbacks: "
           "min=%dus, max=%dus, mean=%dus\n",
           n, min / 1000, max / 1000, sum / n);

    min = 1000000000, max = 0, sum = 0;
    n = 0;
    return HRTIMER_NORESTART;
  }
}

//-----------------------------------------------------------------------------------------

static int init_module_hrtimer(void) {
  printk("mod_hrtimer_dev: installing module...\n");

  /* define a ktime variable with the defined interval on top */
  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 high resolution timestamp of starttime_ns and convert to [ns] */
  starttime_ns = ktime_to_ns(ktime_get());

  /* start the high resolution timer which calls the callback function after
   * some time */
  hrtimer_start(&hr_timer, ktime_interval, HRTIMER_MODE_REL);

  printk(
      MY_MODULE_NAME
      ": initially started hrtimer to fire my_hrtimer_callback() 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(MY_MODULE_NAME ": active timer cancelled, ");

  printk(MY_MODULE_NAME ": module uninstalled.\n");
}

module_init(init_module_hrtimer);
module_exit(cleanup_module_hrtimer);