Aus Linux-Magazin 11/2019

Kernel- und Treiberprogrammierung mit dem Linux-Kernel – Folge 106

© psdesign1, Fotolia

Energie sparen ist ein Gebot der Stunde – auch im Linux-Kernel. Wir zeigen, wie Linux das freiwillige Stromsparen organisiert und wie Programmierer dazu ihren Beitrag leisten.

Früher ging es um Geschwindigkeit, heute rückt im Zug des Klimawandels zunehmend Energieeffizienz ins Blickfeld der Informatik. Besonders krass wird das beim Bitcoin-Mining sichtbar, bei dem angeblich eine einzelne Überweisung bereits die unglaubliche Menge von rund 600 kWh Strom verbraucht [1], oder bei Rechenzentren, deren Unterhalt so viel Energie benötigt wie eine Kleinstadt.

Konsequenterweise bauen die Hardwarehersteller in ihre Geräte diverse Stromspartechniken ein, die die Informatiker nur noch zu den richtigen Zeitpunkten (de-)aktivieren müssen: Power-Management (PM) nennt sich der zugehörige Fachterminus.

Nicht erst seit dem Auftreten mobiler Endgeräte wie Smartphones implementiert Linux ein Power-Management-Subsystem (PM-Subsystem), in das sich die Gerätetreiber einklinken und fleißig Komponenten schlafen legen oder bei Bedarf aufwecken.

Klappe zu

Die Linux-Implementierung unterscheidet zwei Arten von Power-Management: das System-PM (Zwang) und das Runtime-PM (freiwillig). Ersteres zwingt alle Komponenten dazu, sofort alle Aktivitäten einzustellen und in einen Stromsparzustand zu wechseln. Klappe zu: Sobald der Anwender das Notebook zusammenklappt, arbeitet Linux hektisch den System-PM-Code ab, bevor es das Display abstellt und den Lüfter herunterdreht.

Das Runtime-PM dagegen versucht bereits im Normalbetrieb möglichst effizient mit Energie umzugehen. Hat eine Komponente gerade nichts zu tun, ist also inaktiv, dann versetzt der Kernel sie, falls möglich, in den Stromsparmodus, neuhochdeutsch: suspendiert sie. Im Folgenden geht es um dieses Runtime-Power-Management.

Das Realisieren von Power-Management in Software ist kompliziert. Geräte bieten nicht nur unterschiedliche Stromsparzustände an (was allerdings der Programmierer schon mit dem Hardware-Ingenieur ausklamüsert), sondern sind vor allem miteinander verwoben. Es wäre ärgerlich, wenn das System einen USB-Hub in den Stromsparmodus versetzt, an dem eine gerade aktiv genutzte Videokamera hängt. Des Weiteren kommt noch hinzu, dass je nach Betriebszustand möglicherweise eine ganze Gruppe von Geräten, zum Beispiel Eingabegeräte, nicht benötigt werden.

Aufgrund dieser Anforderungen und ihrer Komplexität zieht sich daher im Linux-Kernel das PM durch diverse Subsysteme. Den Hauptansatzpunkt stellt dabei das im Code verankerte Geräte- oder besser Objektmodell dar, das für den Anwender durch das Sys-Filesystem sichtbar ist. Dieses Objektmodell sortiert die Hardware gemäß ihrer Kopplung über unterschiedliche Bussysteme, über die Gerätetypen (etwa: Partition oder Disk, Maus oder Event) und auch über Geräteklassen wie beispielsweise Blockgerät, DMA, Drucker, Watchdog oder Eingabegerät.

Wer einen Linux-Gerätetreiber schreibt, kategorisiert also bei der Treiberinitialisierung sein Gerät (»struct device«) und ordnet es einem Bussystem sowie einer Geräteklasse zu (Abbildung 1).

Abbildung 1: Power-Management-Funktionen sind über Unterstrukturen mit dem Gerät verknüpft.

Abbildung 1: Power-Management-Funktionen sind über Unterstrukturen mit dem Gerät verknüpft.

Linux ermöglicht es, das Power-Management innerhalb der unterschiedlichen Kategorien zu implementieren, also auf Ebene des Bussystems, des Gerätetyps, der Geräteklasse oder eben auch direkt auf Geräteebene. Darüber hinaus kennt Linux noch die Kategorie Power-Domain. Sie fasst Geräte zusammenfasst, die mehr oder minder an einem Stromversorgungsabschnitt hängen, also an einem Taktgeber oder einem Multiplexer, sprich: die energietechnisch mit einer gemeinsamen Quelle verbunden sind.

Mit diesen Ebenen oder Kategorien (Power-Domain, Gerätetyp, Klasse, Bustyp und Gerät) lässt sich eine Reihe von 23 Callbacks (»struct dev_pm_ops«) verbinden. Die Callbacks selbst sind für alle Kategorien gleich. Die ersten 20 Callbacks sind für System-PM vorgesehen, die letzten drei für Runtime-PM (Listing 1).

Listing 1

Callbacks für das Runtime-PM

struct dev_pm_ops {
  [...]
  int (*runtime_suspend)(struct device *dev);
  int (*runtime_resume)(struct device *dev);
  int (*runtime_idle)(struct device *dev);
};

Sind Callbacks bei einem Gerät – im Code durch »struct device« repräsentiert – auf unterschiedlichen Kategorien oder Ebenen eingehängt, gibt es eine Mehrfachauswahl: Vielleicht existieren PM-Callbacks für die Geräteklasse, aber auch für den zum Gerät gehörigen Treiber?

Linux löst dieses Dilemma sehr pragmatisch: Es triggert grundsätzlich nur eine der mit dem Gerät verknüpften Callback-Gruppen, und zwar die in der Hierarchie höher angesiedelte. Dabei priorisiert es seine Wahl folgendermaßen: 1.  PM-Domain, 2.  Gerätetyp, 3.  Klasse, 4.  Bus und 5.  Treiber (Abbildung 1). Dem Programmierer der Callbacks steht es allerdings frei, die niedriger priorisierten Callbacks innerhalb seiner Callback-Funktion aufzurufen.

Der Kernel ruft Callbacks typischerweise immer im Prozess-Kontext auf, Interrupts sind dann folglich freigegeben. Falls bei dem Vorgang, Stromsparmodi zu ändern, Interrupts keine Rolle spielen, teilt man dem PM-Subsystem mit, dass der Aufruf im Interrupt-Kontext möglich ist.

Schäfchen zählen

Ob und wann der Kernel einen der Runtime-Callbacks aufruft, regeln zwei Zähler: einer für die Zugriffe (»usage_count«) und einer für die nachgelagerten Systeme (»child_count«). Änderungen an diesen Zählern nehmen die Hilfsfunktionen »pm_runtime_get()« und »pm_runtime_put()« vor, die beispielsweise von den Funktionen »driver_open()« und »driver_close()« des zugehörigen Gerätetreibers aufgerufen werden (Abbildung 2).

Abbildung 2: Das Power-Management-Subsystem aktiviert das Stromsparen bei den Geräten.

Abbildung 2: Das Power-Management-Subsystem aktiviert das Stromsparen bei den Geräten.

Nimmt bei einem Aufruf von »pm_runtime_put()« der dekrementierte Zugriffszähler den Wert »0« an und ist zugleich der Zähler für die nachgelagerten Geräte ebenfalls »0«, erfolgt – falls implementiert – ein Aufruf des Callbacks »runtime_idle()«. Er prüft dann, ob das Gerät einen Stromsparmodus annehmen kann. Lässt sich das Gerät tatsächlich in einen Stromsparmodus versetzen oder ist »runtime_idle()« nicht implementiert, wird ein entsprechender Suspend-Auftrag ins PM-Subsystem eingespult.

Der im Treiber realisierte Callback »runtime_suspend()« überführt dann das Gerät oder die zugehörigen Subsysteme in den Stromsparzustand. Bei Erfolg – das Gerät befindet sich im Zustand »suspended« – gibt er »0« zurück (Abbildung 3). Ist es aber zurzeit nicht sinnvoll oder möglich Strom zu sparen, gibt die Methode »-EBUSY« oder »-EAGAIN« zurück; der Gerätestatus verbleibt im Zustand »active«. Bei allen anderen Rückgabewerten geht der Kernel von einem schwerwiegenden Fehler und einem nicht eindeutig definierten Gerätezustand aus.

Abbildung 3: Komponenten nehmen einen der beiden Zustände »suspended« oder »active« ein.

Abbildung 3: Komponenten nehmen einen der beiden Zustände »suspended« oder »active« ein.

Aufwachen bitte

Signalisiert eine Applikation durch das Antriggern der Treiberfunktion »driver_open()«, dass sie ein Gerät benötigt, ruft »driver_open()« seinerseits »pm_runtime_get()« auf. Befindet sich das Gerät noch im Zustand »suspended«, aktiviert das PM-Subsystem es durch Aufruf des Callbacks »runtime_resume()« wieder und überführt es in den betriebsbereiten Zustand. Gibt »runtime_resume()« den Wert »0« zurück, geht der Kernel von einem Erfolg aus. Ansonsten verbleibt aus Sicht des Kernels das Gerät im Zustand »suspended«, und der Programmierer muss den Status gegebenenfalls selbst aktualisieren.

Wer einen Gerätetreiber realisiert, implementiert zur Unterstützung von Runtime-PM zumindest zwei der drei Callbacks aus Listing 1. Der optionale Callback »runtime_idle()« wird in der Tat nur selten implementiert.

Außerdem muss sich der Gerätetreiber in das Runtime-PM-Subsystem einklinken, indem die Datenstruktur »struct pm_dev_ops« für jedes Gerät (»struct device«) an eine der Unterstrukturen (Abbildung 1) gebunden wird. Bei vielen Gerätetreibern handelt es sich dabei um das Bus-Objekt, wobei Linux für die Geräte, die nicht über einen Standardbus wie PCI oder USB angeschlossen sind, den Platform-Bus zur Verfügung stellt.

Da initial das Runtime-Power-Management für das Gerät ausgeschaltet und der Gerätestatus auf »suspended« gesetzt ist, passt der Treibercode den Zustand an (»pm_runtime_set_active()«) und gibt das Runtime-PM durch den Aufruf von »pm_runtime_enable()« frei. Das klappt allerdings nur dann, wenn sich kein übergeordnetes Gerät im Zustand »suspended« befindet – unabhängig davon, ob es PM-Runtime aktiviert hat oder nicht.

Kein Auto

Unter den diversen weiteren Funktionalitäten des PM-Subsystems (siehe Tabelle 1) soll noch Autosuspend erwähnt werden: Überraschenderweise ist keineswegs »auto« drin, wo »auto« draufsteht. Anders als zu vermuten wäre, kann man hier keine Zeitspanne spezifizieren, nach deren Ablauf das PM-Subsystem von sich aus ein Gerät suspendiert.

Tabelle 1

Hilfsfunktionen des Runtime-Power-Management-Subsystems (Auswahl)

Name

Funktion

»pm_schedule_suspend()«

Gerät bei nächster Gelegenheit schlafen legen

»pm_request_resume()«

Gerät aktivieren

»pm_runtime_get_noresume()«

Zugriffszähler inkrementieren, Gerät schlafen lassen

»pm_runtime_get()«

Zugriffszähler inkrementieren, Gerät aktivieren

»pm_runtime_put()«

Zugriffszähler dekrementieren, eventuell Stromsparmodus aktivieren

»pm_request_idle()«

Gerät auf Inaktivität überprüfen

»pm_runtime_put_noidle()«

Zugriffszähler dekrementieren, Gerät aktiv lassen

»pm_runtime_put_autosuspend()«

Zugriffszähler dekrementieren, verzögertes Schlafenlegen antriggern

»pm_runtime_disable()«

Runtime-PM für das Gerät deaktivieren

»pm_runtime_enable()«

Runtime-PM für das Gerät aktivieren

»pm_runtime_set_active()«

Gerät als »active« markieren

»pm_runtime_set_suspended()«

Gerät als »suspended« markieren

»pm_runtime_allow()«

Runtime-PM für das Gerät erlauben

»pm_runtime_forbid()«

Runtime-PM für das Gerät verbieten

»pm_runtime_active()«

Gibt »1« zurück, falls der Gerätezustand »active« ist

»pm_runtime_suspended()«

Gibt »1« zurück, falls Gerät im Zustand »suspended« ist

Vielmehr bedeutet Autosuspend, dass ein Aufruf von »pm_runtime_suspend()« nicht direkt zum Aufruf des im Treiber realisierten Callbacks »runtime_suspend()« führt, sondern erst um »autosuspend_delay« Millisekunden zeitverzögert. Das soll ein übereifriges hin und her Switchen zwischen den Zuständen »suspended« und »active« verhindern, denn auch der Zustandswechsel verbraucht Energie, am Ende hätte man mehr Strom verbraucht als eingespart. Autosuspend wird übrigens durch Aufruf der Hilfsfunktion »pm_ runtime_use_autosuspend()« während der Treiberinitialisierung aktiviert.

Listing 2 zeigt die Basisimplementierung von Runtime-Power-Management in einem Treiber (ohne Lese- und Schreibfunktionen). Die Implementierung der Runtime-PM-Callbacks rahmt ein »#ifdef CONFIG_PM« ein. Die vom PM-Subsystem zur Verfügung gestellten Hilfsfunktionen benötigen das Define nicht, bei ihnen ist die bedingte Kompilierung bereits in der Header-Datei realisiert.

Listing 2

Unterstützung von Runtime-PM im Treiber

001 // SPDX-License-Identifier: GPL-2.0
002 //
003 #include <linux/module.h>
004 #include <linux/fs.h>
005 #include <linux/cdev.h>
006 #include <linux/platform_device.h>
007 #include <linux/pm_runtime.h>
008
009 static dev_t foo_dev_number;
010 static struct cdev *driver_object;
011 struct class *foo_class;
012 static struct device *foo_dev;
013
014 static int driver_open(struct inode* dev_file, struct file* instance)
015 {
016   dev_info(foo_dev,"driver_open()\n");
017   pm_runtime_get(foo_dev); // aktivieren
018   return 0;
019 }
020
021 static int driver_close(struct inode* dev_file, struct file* instance)
022 {
023   dev_info(foo_dev,"driver_close()\n");
024   pm_runtime_put(foo_dev); // suspend.
025   return 0;
026 }
027
028 #ifdef CONFIG_PM /* Power-Management */
029 static int foo_runtime_suspend(struct device *dev)
030 {
031   /* Stromsparmodus einschalten,
032    * Geraet schlafen legen...
033    */
034   dev_info(dev,"foo_runtime_suspend\n");
035   return 0;
036 }
037
038 static int foo_runtime_resume(struct device *dev)
039 {
040   /* Stromsparmodus ausschalten,
041    * Geraet reaktivieren...
042    */
043   dev_info(dev,"foo_runtime_resume\n");
044   return 0;
045 }
046
047 static struct dev_pm_ops foo_dev_pm_ops ={
048   .runtime_suspend= foo_runtime_suspend,
049   .runtime_resume = foo_runtime_resume
050 };
051 #endif
052
053 static struct device_driver foo_driver = {
054   .name = "foo_driver",
055   .bus = &platform_bus_type,
056 #ifdef CONFIG_PM /* Power-Management */
057   .pm = &foo_dev_pm_ops
058 #endif
059 };
060
061 static struct file_operations foo_fops = {
062   .open = driver_open,
063   .release = driver_close
064 };
065
066 static int __init foo_init(void)
067 {
068   if(alloc_chrdev_region(&foo_dev_number,0,1,"foo")<0)
069     return -EIO;
070   driver_object = cdev_alloc();
071   if(driver_object==NULL)
072     goto free_device_number;
073   driver_object->owner = THIS_MODULE;
074   driver_object->ops = &foo_fops;
075   if(cdev_add(driver_object,foo_dev_number,1))
076     goto free_cdev;
077   foo_class = class_create(THIS_MODULE, "foo");
078   if(IS_ERR(foo_class)) {
079     pr_err("foo: no udev support\n");
080     goto free_cdev;
081   }
082   foo_dev = device_create(foo_class,NULL,foo_dev_number, NULL, "%s","foo");
083   foo_dev->driver = &foo_driver;
084
085   pm_runtime_allow(foo_dev);
086   pm_runtime_enable(foo_dev);
087
088   dev_info(foo_dev,"foo_init(%p)\n",foo_dev);
089   return 0;
090 free_cdev:
091   kobject_put(&driver_object->kobj);
092 free_device_number:
093   unregister_chrdev_region(foo_dev_number,1);
094   return -EIO;
095 }
096
097 static void __exit foo_exit(void)
098 {
099   dev_info(foo_dev,"foo_exit(%p)\n",foo_dev);
100   device_destroy(foo_class,foo_dev_number);
101   class_destroy(foo_class);
102   cdev_del(driver_object);
103   unregister_chrdev_region(foo_dev_number, 1);
104   return;
105 }
106
107 module_init(foo_init);
108 module_exit(foo_exit);
109 MODULE_LICENSE("GPL v2");

Das Vermeiden der bedingten Kompilierung ist übrigens auch der Grund für die Existenz des (hier nicht verwendeten) Makros »SET_RUNTIME_PM_OPS( suspend, resume, idle)«, das die Adressen der Callbacks in die Datenstruktur »dev_pm_ops« einträgt. Abbildung 4 zeigt das zugehörige Makefile sowie das Generieren und Laden des Moduls.

Abbildung 4: Das Runtime-Power-Management l&auml;sst sich leicht mit Bordmitteln testen.

Abbildung 4: Das Runtime-Power-Management lässt sich leicht mit Bordmitteln testen.

Das Schlafenlegen und das Aufwecken lässt sich auf zweierlei Art triggern: entweder durch den Zugriff auf die zum Treiber gehörige Gerätedatei »/dev/foo« oder über die entsprechenden Dateien im Sys-Filesystem.

Benutzerschnittstelle

Nach dem Laden des Treibers gibt es im Sys-Filesystem einen neuen Ordner. Wurde eine neue Klasse »foo« erzeugt, findet sich ein Eintrag unter »/sys/class/foo/«. Die dort abgelegten symbolischen Links zeigen auf die Verzeichnisse, die die Geräte repräsentieren. Falls ein Gerät »foo« genannt wurde, lautet folglich ein Verzeichnisname »/sys/devices/virtual/foo/foo/«.

Hier findet sich neben einer Reihe von Dateien insbesondere das hier relevante Verzeichnis »power/« (Abbildung 5). Dort liegen die Dateien, über die sich das Power-Management des Geräts kontrollieren lässt. Ein Blick in die Datei »runtime_status« zeigt, dass sich das Gerät initial im Zustand »suspended« befindet. Um es zu aktivieren, schreibt man ein »on« in die Datei »control«:

Abbildung 5: Das Sys-Filesystem bietet gute Kontrolle &uuml;ber das Runtime-Power-Management.

Abbildung 5: Das Sys-Filesystem bietet gute Kontrolle über das Runtime-Power-Management.

echo "on" > control

Dieses Kommando bewirkt, dass das PM-Subsystem die Funktion »foo_runtime_resume()« aufruft. Da im Beispiel-Quellcode Debug-Ausgaben im Systemlog erfolgen, erscheint dort die Meldung »foo_runtime_resume()«, wie sich über den Aufruf »tail -f /var/log/kern.log« unschwer nachvollziehen lässt. Um das Gerät wieder schlafen zu legen, schreibt man ein »auto« in die Datei »control«.

Der Zustand der beiden Variablen »usage_count« und »child_count« lässt sich bei Bedarf über »runtime_usage« und »runtime_active_kids« ebenfalls im Sys-Filesystem kontrollieren.

Wer ein Freund von Statistiken ist, kann sich die Zeiten ausgeben lassen, in denen das Gerät suspendiert (Datei »runtime_suspended_time«) beziehungsweise aktiv war (Datei »runtime_active_time«). Ins Verhältnis gesetzt bekommt der Programmierer ein gutes Gefühl für den Erfolg seiner Energiesparmaßnahmen.

Gutes Gewissen

An dieser Stelle sei noch einmal betont: Power-Management ist keineswegs ein Gimmick. Verantwortungsvolle Informatiker und Programmierer achten bei Konzeption und Implementierung auf die – insbesondere bei der hohen Verbreitung von Linux – nicht unbedeutenden energietechnischen Auswirkungen.

Mehr Informationen zum Power-Management unter Linux finden sich im Übrigen in der Linux Kernel-Dokumentation ([2], [3]). Eine Kern-Technik-Folge in einer der nächsten Ausgaben wird dann das System-Power-Management thematisieren. Wen die Neugier packt, was sich in den letzten sieben Jahren alles im Bereich Stromsparen und Linux-Kernel getan hat, der kann diesbezüglich noch einmal ebenso nostalgisch wie kostenlos online in der Kern-Technik-Folge 61 vom März 2012 blättern [4]. (jlu)

106

Infos

  1. Bitcoin Energy Consumption Index: https://digiconomist.net/bitcoin-energy-consumption

  2. Device Power Management Basics: Linux-Kernel-Dokumentation, https://www.kernel.org/doc/html/v5.2/driver-api/pm/devices.html

  3. Runtime Power Management Framework for I/O Devices: Kernel-Dokumentation, https://www.kernel.org/doc/Documentation/power/runtime_pm.txt

  4. Runtime-Power-Management im Linux-Kernel, Eva-Katharina Kunst, Jürgen Quade: “Kern-Technik, Folge 61”, LM 03/2012, S. 88, https://www.linux-magazin.de/ausgaben/2012/03/kernel-management/

Der Autor

Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von Open Source. Jürgen Quade, Professor an der Hochschule Niederrhein, bietet auch für Unternehmen Schulungen zu den Themen Treiberprogrammierung und Embedded Linux an.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben