Aus Linux-Magazin 01/2016

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

© psdesign1, Fotolia

Ein ausgeklügeltes Framework verwaltet das Abarbeiten asynchroner Codesequenzen im Linux-Kernel: Kworker-Threads. Was sie sind, wie sie arbeiten und wer die Arbeitstiere für seine Zwecke einspannen darf, erklärt die neueste Folge der Kern-Technik.

Wer neugierig auf die Liste der Rechenprozesse seines Linux-Systems schaut, erkennt eine inflationäre Menge von Kernelthreads mit dem Namen »kworker« (Abbildung 1). Die gehören zum “Concurrency Managed Workqueue”-Framework [1] – kurz Workqueues – und dienen dem Kernel dazu, Codesequenzen asynchron ablaufen zu lassen. Interrupt-Service-Routinen nutzen die Möglichkeit gerne, um ihre zeitlich unkritischen Sequenzen über Workqueues auszuführen. Aber auch andere Kernelmodule setzen Workqueues ein, wenn sie vom sonstigen Ablauf entkoppelt zum Beispiel Berechnungen zu absolvieren haben.

Abbildung 1: Eine Armada von Worker-Threads kümmert sich um die Abarbeitung asynchroner Codesequenzen.

Abbildung 1: Eine Armada von Worker-Threads kümmert sich um die Abarbeitung asynchroner Codesequenzen.

Workqueues erleichtern dem Programmierer die Arbeit, indem sie eine fertigte Umgebung zum Abarbeiten von Codesequenzen im Kernelkontext bereitstellen. Der Entwickler erzeugt, startet und verwaltet also nicht selbst einen Kernelthread, sondern greift im Sinne eines Designpattern auf eine vorgefertigte Komponente zurück.

Linux macht nicht nur das Designpattern als solches nutzbar und hilft eine Workqueue zu erzeugen, sondern beherbergt bereits eine Reihe von Workqueues, denen der Programmierer die abzuarbeitenden Sequenzen einfach nur zu übergeben braucht. Genau das sind die eingangs erwähnten »kworker« -Threads. Sie gehören zu einem ausgeklügelten Subsystem, das die Anzahl der Threads auf Basis der vorhandenen Ressourcen und der benötigten Threads laufend anpasst.

Let’s work!

Um einem Kworker-Thread eine Aufgabe zu übertragen, legt der Entwickler ein Datenobjekt vom Typ »struct work« an und initialisiert es wie in Abbildung 2 mit der Adresse der Codesequenz (Funktion). Dazu nutzt er zur Compilezeit das Makro »DECLARE_WORK()« oder alternativ zur Laufzeit die Funktion »init_work()« . Das initialisierte Work-Objekt bekommt der Kernel im einfachsten (und zugleich klassischen) Fall durch Aufruf von »schedule_work()« übergeben.

Abbildung 2: Übergebene Work-Objekte sammelt der Kernel in einem Workqueue-Pool.

Abbildung 2: Übergebene Work-Objekte sammelt der Kernel in einem Workqueue-Pool.

Der Kernel wiederum aktiviert einen Kworker-Thread, der die im Work-Objekt abgelegte Funktion bei nächster Gelegenheit aufruft. Dabei übergibt er ihr die Adresse des Work-Objekts. Dies kann der Programmierer nutzen, um der eigenen Funktion einen zusätzlichen Parameter zu übermitteln. Hierfür haben die Kernelentwickler im Work-Objekt extra einen Platz reserviert.

Probe-Arbeiten

Listing 1 zeigt die drei Schritte in einem vollständigen Beispiel: Die asynchron abzuarbeitende Funktion beginnt in Zeile 4. Die Zeile 11 legt das Work-Objekt mit dem Namen »work« an und initialisiert es dabei mit der Adresse der asynchronen Funktion. Die Übergabe an den Kernel und damit die Aktivierung passiert in Zeile 15. Um das Beispiel überschaubar zu halten, ruft es »schedule_work()« im Rahmen der Funktion »mod_init()« einmal auf. Das Work-Objekt bleibt aber grundsätzlich gültig und lässt sich – mit den weiter unten aufgeführten Einschränkungen – beliebig häufig dem Kernel übergeben.

Aus Listing 1 wird mit Hilfe des Makefile aus Listing 2 das Kernelmodul »worker.ko« generiert. Das Kommando »insmod worker.ko« lädt es in den Kernel. Ob dies gelungen ist, kann der Anwender mit Hilfe des Kommandos »tail« prüfen, wie im unteren Teil von Abbildung 1 zu sehen ist. Dabei gibt die im Rahmen eines »kworker« -Thread abgearbeitete Codesequenz »work_queue_function()« beim Aufruf im Syslog die aktuelle Zeit in Jiffies und die Prozess-ID genau jenes Worker-Thread aus, der die Codesequenz abarbeitet.

In der Abbildung 1 überträgt Linux die Funktion des Beispielmoduls dem »kworker« -Thread mit PID 8709. Anhand der Erklärung in Abbildung 3 wird mit »[kworker/3:1]« ) deutlich, dass der Thread auf dem CPU-Kern 3 fixiert (gebunden) ist und dass es sich auf diesem CPU-Kern um den zweiten (die Zählung beginnt bei 0) von insgesamt drei »kworker« -Threads handelt.

Abbildung 3: So setzen sich die Bezeichnungen der Worker-Threads zusammen.

Abbildung 3: So setzen sich die Bezeichnungen der Worker-Threads zusammen.

Wie immer muss der Entwickler auch bei Workqueues vor dem Entladen von Code aufräumen. Konkret: Er wartet mit dem Entladen, bis alle an die Workqueue übergebenen Aufträge entweder abgearbeitet oder aus den Auftragslisten entfernt sind. Dies stellen die Funktionen »flush_scheduled_work()« und »cancel_work_sync()« sicher (Zeile 26).

Listing 1

Einfaches Workqueue-Beispiel (worker.c)

01 #include <linux/module.h>
02 #include <linux/sched.h>
03
04 static void work_queue_function( struct work_struct *data )
05 {
06   pr_info( "work_queue_function( jiffies: %ld ) %d\n",
07     jiffies, current->pid );
08   return;
09 }
10
11 static DECLARE_WORK(work, work_queue_function);
12
13 static int __init mod_init(void)
14 {
15   if( schedule_work( &work )==0 ) {
16     pr_info( "schedule_work failed ...\n");
17   } else {
18     pr_info( "schedule_work successful ...\n");
19   }
20   return 0;
21 }
22
23 static void __exit mod_exit(void)
24 {
25   pr_info("mod_exit called\n");
26   flush_scheduled_work();
27 }
28
29 module_init( mod_init );
30 module_exit( mod_exit );
31 MODULE_LICENSE("GPL");

Listing 2

Makefile

01 ifneq ($(KERNELRELEASE),)
02 obj-m := worker.o
03
04 else
05 KDIR  := /lib/modules/$(shell uname -r)/build
06 PWD   := $(shell pwd)
07
08 default:
09       $(MAKE)   -C $(KDIR)    M=$(PWD) modules
10 endif
11
12 clean:
13       rm -rf .*.cmd *.mod.o modules.order Module.symvers
14       rm -rf *.o .*.dwo *.ko *.mod.c

Aufgaben delegieren

Technisch betrachtet besteht die Aufgabe von »schedule_work()« darin, das Work-Objekt an eine Aufgabenliste anzuhängen und den Worker-Thread – sollte er gerade schlafen – aufzuwecken. Der Worker-Thread ruft die in den Work-Objekten seiner Aufgabenliste angegebenen Funktionen nacheinander auf und löscht dann die Adressen der Work-Objekte aus seiner Liste. Sind alle Funktionen aufgerufen und ist die Liste damit leer, legt er sich wieder schlafen (Abbildung 4).

Abbildung 4: Die asynchrone Funktionsbearbeitung läuft in drei Schritten ab.

Abbildung 4: Die asynchrone Funktionsbearbeitung läuft in drei Schritten ab.

Verbote und Gefahren

Es ist untersagt, Work-Objekte mehrfach einzuhängen. Ebenso darf niemand im Rahmen der Work-Funktion das Work-Objekt erneut (und damit die Work-Funktion rekursiv) an die Auftragsliste der Workqueue eintragen. Außerhalb der Work-Funktion und nach dem Abarbeiten darf man das Work-Objekt jedoch beliebig weiterverwenden.

Der beschriebene Mechanismus ist sehr simpel, birgt aber auch Gefahren: Das Hauptproblem bildet die sequenzielle Abarbeitung, die insbesondere auf Multicore-Maschinen ineffizient ist und zu großen Verzögerungen führen kann. Ein einzelner Auftrag, der beispielsweise ein lang dauerndes Sleep aufruft, legt alle Arbeitswilligen lahm. Während ein Thread für jede CPU das Effizienzproblem etwas reduziert, behebt dies das Problem der zeitlichen Verzögerung nicht.

Das hat 2010 dazu geführt, dass die Torvalds-Community die Kernelthreads mit dem Namen »keventd« durch die Armada der »kworker« -Threads ersetzt hat. Die Kernelentwickler Tejun Heo und Florian Mickler starten nicht einfach wie früher einen Worker-Thread pro CPU, sondern bieten diverse unterschiedliche Workqueues an, deren Worker-Threads mehrfach instanziert werden (Tabelle 1).

Tabelle 1

Vordefinierte Workqueue-Pools

Poolname bound/unbound Funktion
struct workqueue_struct *system_wq; bound normale Abarbeitung
struct workqueue_struct *system_highpri_wq; bound priorisierte Abarbeitung
struct workqueue_struct *system_long_wq; bound für rechenintensive Aufträge
struct workqueue_struct *system_unbound_wq; unbound Abarbeitung unabhängig vom CPU-Kern
struct workqueue_struct *system_freezable_wq; bound verzögerte Abarbeitung erlaubt
struct workqueue_struct *system_power_efficient_wq; unbound auf Energieverbrauch optimierte Abarbeitung
struct workqueue_struct *system_freezable_power_efficient_wq; unbound energieeffiziente und verzögerte Abarbeitung

Gebunden – oder auch nicht

Die Entwickler unterscheiden zwischen den gebundenen (bound) und ungebundenen (unbound) Workqueues. Erstere legt Linux pro CPU-Kern mindestens ein Mal an, die anderen sind unabhängig von der Anzahl der Rechnerkerne, kommen aber ebenfalls mehrfach vor. Die »system_wq« ist die normale Workqueue, die ein aufgerufenes »schedule_work()« benutzt. Der Name »system_highpri_wq« ist irreführend. Hier eingehängte Funktionen arbeitet der Kernel beileibe nicht mit einer Realzeitpriorität ab. Nur der altbekannte Nice-Level ist erhöht und führt zu einer geringfügigen Bevorzugung gegenüber den übrigen Tasks.

Die Workqueue »system_long_wq« ist für länger arbeitende Funktionen gedacht. Die Workqueue »system_freezable_wq« erlaubt es, eingehängte Funktionen erst nach einem Aufwecken zu starten, sollte das System in einen Schlafzustand übergehen. Ähnlich funktioniert es mit »system_power_efficient_wq« . Hier kann es schon mal zu einer Verzögerung kommen, wenn das Energie einspart. »system_freezable_power_efficient_wq« ist – wie der Name schon besagt – die Kombination aus beiden.

Die Inflation an Kernelthreads führt allerdings zum nächsten Problem: Many-Core-Maschinen, also solche mit mehreren Hundert Prozessorkernen, können das Linux-Limit bei den Prozess-Identifikationsnummern (rund 32 000) knacken. Daher haben die Entwickler eine sehr flexible Architektur implementiert, die auf Basis der freien Ressourcen auf der einen und der Nachfrage auf der anderen Seite die Kworker-Threads vorhält. Diese Architektur beruht auf so genannten Worker-Pools (Abbildung 2).

Einem Worker-Pool sind ein oder mehrere Worker-Threads zugeordnet. Gibt es mehr Aufträge im Pool als zugeordnete Worker-Threads, erzeugt Linux – falls ausreichend Ressourcen zur Verfügung stehen – zusätzliche Worker-Threads. Gibt es umgekehrt zu viele schlafende Worker-Threads, entfernt das System sie wieder. Diese Architektur stellt sicher, dass Aufträge nicht zu lange bis zur Abarbeitung warten.

Als Konsequenz kennen Worker-Threads drei Zustände: Sie sind »idle« , haben also zurzeit keinen Auftrag und schlafen daher. Oder sie sind als »running« unterwegs und bearbeiten einen Auftrag, oder aber sie befinden sich im Zustand »managing« . Ein Worker-Thread in diesem Zustand überprüft den Pool (Auftragsliste und vorhandene Threads) und generiert oder entfernt zum Pool gehörende Worker-Threads [2]. Pro Pool befindet sich nie mehr als ein Thread in dem Zustand »managing« .

Welcher Pool und welche CPU ist steuerbar

Um das System nutzbar zu machen, stellen die Entwickler eine Reihe neuer Kernelfunktionen bereit. Über sie lässt sich eine abzuarbeitende Funktion in der Auftragsliste der ausgewählten Workqueue, genauer genommen des Workqueue-Pools, einhängen (Listing 3). Neben dem Workqueue-Pool kann der Benutzer häufig noch die CPU spezifizieren, auf der die Codesequenz ablaufen soll. Das ist natürlich nur für Bound-Workqueues interessant. Mit diesem Wissen lässt sich Zeile 15 in Listing 1 durch

if( queue_work( system_power_efficient_wq, &work )==0 ) {

ersetzen, um die Stromsparfunktion bei der Abarbeitung einer asynchronen Funktion zu unterstützen.

Die Funktionen aus Listing 3 zeigen außerdem, dass das Workqueue-Subsystem auch eine zeitverzögerte Abarbeitung ermöglicht. Dazu wird mit dem Makro »DECLARE_DELAYED_WORK()« ein Objekt erzeugt, das die Verzögerung als Wert in Jiffies abspeichern kann. Dieses Objekt sowie eine relative Verzögerungszeit bekommt die Funktion »queue_delayed_work()« übergeben. Sie stößt die Abarbeitung nach Ablauf der spezifizierten Zeit an.

Listing 4 zeigt einen Ausschnitt, bei dem die asynchron abzuarbeitende Funktion eine halbe Sekunde verzögert startet. Da die eigentlich veraltete Zeiteinheit Jiffie des Linux-Kernels je nach Plattform unterschiedlich lange ausfällt, weichen die meisten Kernelprogrammierer auf das Define »HZ« aus (Zeile 5). HZ entspricht der Anzahl Jiffies, die innerhalb einer Sekunde vergehen.

Listing 3

Funktionen zur Auftragsvergabe

01 bool queue_work(struct workqueue_struct *wq, struct work_struct *work);
02 bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);
03 bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);
04 bool mod_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay);

Listing 4

Codefragment zur verzögerten Abarbeitung

01 static DECLARE_DELAYED_WORK( work_obj, work_queue_function );
02 [...]
03 static int __init mod_init( void )
04 {
05     if (queue_delayed_work(system_wq, &work_obj, HZ/2)) {
06         pr_info( "queue_delayed_work successful ...\n");
07     } else {
08         [...]

Work und Worker

Das seit 2005 im Kernel verbaute Workqueue-System ist für Multicore-Systeme optimiert – der Name “Concurrency Managed Workqueue” deutet es an. Wer unkritische einfache Arbeit zu erledigen hat, findet darin das optimale Betriebsmittel, besonders wenn er den richtigen Workqueue-Pool wählt. So integriert er seinen Code sauber in den Kernel. Für ganz wichtige oder sehr eilige Berechnungen implementieren Kernelentwickler aber weiterhin eigene Kernelthreads – nur die lassen sich priorisieren.

Infos

  1. Workqueue-Dokumentation im Kernel: https://www.kernel.org/doc/Documentation/workqueue.txt
  2. Vita Loginova, “Multitasking in the Linux Kernel. Workqueues”: http://kukuruku.co/hub/nix/multitasking-in-the-linux-kernel-workqueues

Der Autor

Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von Open Source. Jürgen Quade, Professor an der Hochschule Niederrhein, hat mit “Embedded Linux lernen mit dem Raspberry Pi” letztes Jahr sein drittes Linux-Buch veröffentlicht. Das gemeinsame Buch “Linux-Treiber entwickeln” ist jüngst in vierter Auflage erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 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
Nach oben