Appearance
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":
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 Systemkonfigurationen 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, permisc_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 Systemcallclose()
wieder schliessen kann. Ein Aufruf vonopen()
auf unseren Devicenode führt also den Systemcallopen()
auf dem VFS-Layer aus, welcher wiederum gemäss der Treiberregistrierung die inmy_fops
definierte Funtionmy_open()
ausführt... - Unsere minimale Implementation von
my_write()
soll vorerst bloss die vom Usermode-Programm perwrite()
übergebenen Daten in einer modulinternen Variablemydata[]
speichern. - Und
my_read()
diese inmydata[]
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()
sowiecopy_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-Systemcallmmap()
möglich wäre und z.B. beim dynamisch Linken von Libraries genutzt wird! - Gerade die
read()
- undwrite()
-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 Funktionenmy_read()
undmy_write()
)!
- Bauen Sie die obigen Codefragmente an geeigneter Stelle in das Module
mod_hrtimer_dev
ein, also die Definition des Structsmy_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 pergedit
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:bashzugewiesene Major-Nummer:cat /proc/devices
- Leider erstellt
devtmpfs
(resp.udev
odermdev
) noch nicht automatisch einen Devicenode unter/dev/
. Erstellen Sie deshalb manuell den fehlenden Devicenode mit schreib-leserecht im aktiven Verzeichnis:bash(fürsudo mknod -m 666 mod_hrtimer_dev c <major> 0
<major>
den zuvor ermittelten Wert!) - Und testen Sie die Funktionalität, indem Sie per
echo
einwrite()
und percat
einread()
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 Variablemydata[]
geschrieben wird, was mit der Kernel-Funktionsprintf()
machbar ist. Diese Funktion ist äquivalent zur gleichnamigen libc-Libraryfunktion (s.man sprintf
), schreibt also in ein übergebenes char-Array und nicht wieprintf()
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 eingeschleust und mit Kernelrecht ausgeführt werden! Um dies zu vermeiden, verwenden Sie stattsprintf()
die Funktionsnprintf()
, 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 percat
. - 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 nachCtrl-C
perdmesg -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. Dieseread()
-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 namenswq
(also im Modulrumpf im statischen Variablenbereich):cstatic 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 inmy_hrtimer_callback()
gleich nach Beenden der gesamten Messreihe (nach erstellen des Strings) per:cwake_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 SystemsignalSIGINT
deblockiert – im Falle z.B. der Usermode-Prozess perkill
oderCtrl-C
zum Beenden aufgefordert wird. - Vervollständigen Sie das Kernel-Module noch weiter, sodass...
- in
my_read()
noch vor demwait_event_interruptible()
der Event-Timer neu getriggert wird (durch aktualisieren der Variable starttime_ns und Aufruf vonhrtimer_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:
Aktion | User 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:cstatic 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:cdevice_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 folgendem 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
- Buch LINUX Device Drivers 3rd Edition, O'Reilly 2008 (auch kostenlos als PDF downloadbar!)
- Linux API Reference auf http://www.kernel.org/doc/htmldocs/ (oder nach zufügen einiger Ubuntu-Packages aus dem Kernel-Verzeichnis per 'make ... htmldocs | pdfdocs' generierbar)
- Buch Linux Treiber entwickeln, Anhang C Makros und Funktionen des Kernels kurz gefasst : https://ezs.kr.hs-niederrhein.de/TreiberBuch/html/a9387.html
- http://www.freesoftwaremagazine.com/articles/drivers_linux?page=0%2C6
- https://www.ibm.com/developerworks/linux/library/l-timers-list/
- http://www.linux-tutorial.info/modules.php?name=MContent&pageid=83
- http://lxr.free-electrons.com/
man udev
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);