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).
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).
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.
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.
|
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.
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«:
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
-
Bitcoin Energy Consumption Index: https://digiconomist.net/bitcoin-energy-consumption
-
Device Power Management Basics: Linux-Kernel-Dokumentation, https://www.kernel.org/doc/html/v5.2/driver-api/pm/devices.html
-
Runtime Power Management Framework for I/O Devices: Kernel-Dokumentation, https://www.kernel.org/doc/Documentation/power/runtime_pm.txt
-
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/










