Ein Grundbaustein des Linux-Kernels ist das Kernel-Objekt. Über das Sys-Filesystem setzen Sie es mit wenigen Zeilen Code als Vermittler zwischen Userland und Kernel ein.
Everything is a file – das ist eine der genialen Design-Ideen der Unix-Eltern aus den späten 60er-Jahren des vorigen Jahrhunderts [1]. Die Zugriffsfunktionen Lesen und Schreiben genügen, um damit intuitiv nicht nur Dateien anzusprechen, sondern auch Motoren, LEDs oder Sensoren – also Peripherie. Dieser Idee folgend haben sich schließlich virtuelle Dateisysteme ausgebildet, allen voran das Proc-Filesystem, das aktuelle Informationen insbesondere zu Rechenprozessen in Form eines hierarchisch organisierten Dateibaums offeriert.
Auf diese Weise genügen beispielsweise die bekannten Konsolenkommandos »cat« und »echo«, um jederzeit Scheduling- oder Speicherstatistiken abzurufen oder Rechenprozessen CPU-Kerne zuzuordnen. Man spricht übrigens deshalb von einem virtuellen Dateisystem, weil die Daten nicht auf einem Hintergrundspeicher wie einer SSD abgelegt sind, sondern dynamisch beim Zugriff erzeugt werden und sich damit im Hauptspeicher befinden. Technisch ist das nicht immer ganz trivial.
Im Fahrwasser des Proc-Filesystems entstand das Sys-Filesystem (Sysfs), das – ebenfalls als virtuelles Dateisystem implementiert – unter anderem Informationen zum Kernel, zur Peripherie, zu Gerätetreibern und Kernel-Subsystemen parat hält. Das Sys-Filesystem strukturiert die gesamte Hardware und sortiert Peripherie nach unterschiedlichen Kriterien wie Klassen (USB, PCI, Input, GPIO) oder Geräten. Zugehörige Attributdateien ermöglichen die Konfiguration von Kernel und Hardware.
Der Blick hinter die Kulissen und damit in den Linux-Quellcode offenbart, dass es sich beim Sys-Filesystem um die Außendarstellung einer der zentralen Datenstrukturen des Kernels handelt, nämlich der »struct kobject«. Kernel-Objekte (Kobjects) lassen sich damit nicht nur intuitiv auslesen, sondern ebenso intuitiv vom Systemadministrator modifizieren. Da sich die Verwendung von Kobjects nicht auf spezifische Subsysteme beschränkt, leiten kreative Köpfe ihre eigenen Objekte von Kobjects ab, verwalten sie mithilfe des integrierten Referenzzählers und visualisieren dann die Objektzustände im Sys-Filesystem.
Basics
Als ein Grundbaustein des Linux-Kernels haben Kobjects einen Namen. Ihr spezifischer Typ »struct ktype« legt ihr Verhalten (mögliche Zugriffsfunktionen über das Sys-Filesystem) sowie weitere Attribute fest (Abbildung 1). Die im Kernel hierarchisch organisierten Kobjects sind Elternobjekten zugeordnet; umgekehrt kann ein Kobject mehrere Kindobjekte unter sich haben. Diese Struktur reflektiert das Sys-Filesystem, das beim Booten eines Linux-Rechners automatisch im Verzeichnis »/sys« eingehängt wird.
Abgesehen davon durchlaufen Kobjects verschiedene Zustände. Im initialisierten Zustand (»state_initialized«) lässt sich ein Kobject einsetzen. Der Aufruf »kobject_add()« macht es im Sys-Filesystem sichtbar (»state_in_sysfs«). Sobald per Uevent dem Userland das Hinzufügen signalisiert wurde, befindet sich das Kobject im Zustand »state_add_uevent_send()«. Ein Entfernen des Objekts löst ebenfalls ein Uevent aus (»state_remove_uevent_sent«). Zu guter Letzt lassen sich die Uevents auch unterdrücken, was der Zustand »uevent_suppress« signalisiert.
Ein Referenzzähler hält fest, wie viele Instanzen das Objekt nutzen. Bei der Erzeugung wird der Zähler auf 1 gesetzt. Erreicht er später durch Aufruf von »kobject_put()« wieder 0, wird das Objekt im Kernel freigegeben. Dabei kommt die über die Struktur »struct ktype« mit dem Kobject verknüpfte Release-Funktion zum Einsatz.
Für den Umgang mit mehreren semantisch zusammengehörigen Kernel-Objekten definiert Linux Ksets. Die entsprechende Datenstruktur »struct kset« ist direkt von der »struct kobject« abgeleitet, enthält also selbst ein Kobject (Abbildung 2). Ksets verwalten gleichartige Objekte, also solche mit identischen Attributdateien, auf die folglich in identischer Weise zugegriffen werden soll.
Im Sys-Filesystem repräsentiert ein eigenes Verzeichnis ein solches Kset – daher das eingebettete Kobject. Unterhalb siedeln sich dann die Ordner für die zugehörigen Objekte an. Der Beispielcode in den Linux-Quellen [2] legt das Verzeichnis »/sys/kernel/kset_example/« an. Darunter entsteht jeweils ein eigenes Verzeichnis für die drei Objekte »foo«, »bar« und »baz« mit dem Namen des jeweiligen Objekts (Abbildung 3). In jedem dieser Verzeichnisse liegen drei Attributdateien, die identische Namen tragen und sich in gleicher Weise beschreiben und auslesen lassen.
Um hier mit dem Code zu experimentieren, kopieren Sie ihn in ein Verzeichnis, legen dort zusätzlich das Makefile aus Listing 1 ab und ersetzen in dessen zweiter Zeile »sysfs_linuxmag.o« durch »kset-example.o«. Ein nachfolgendes »make« generiert das Kernel-Modul. Ein darauffolgendes »insmod kset-example.ko« lädt es, sofern die Software zur Generierung von Kernel-Code auf dem Rechner installiert ist. Die Einträge finden sich unter »/sys/kernel/kset-example/«.
Listing 1
Makefile
ifneq ($(KERNELRELEASE),) obj-m := sysfs_linmag.o else KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules endif clean: rm -rf *.ko *.mod *.mod.* *.o modules.order rm -rf Module.symvers .*.cmd *.dwo
Fallstrick voraus
In direktem Zusammenhang zu den Kernel-Objekten steht das Uevent-Subsystem zur Benachrichtigung des Userlands über Ereignisse im Kernel. Zu diesen Events zählen das Erzeugen und Zerstören eines Kernel-Objekts sowie alle Zustandsänderungen zwischen diesen beiden Ereignissen.
Konkret: Ein Gerätetreiber assoziiert mit einem Gerät (Device) ein Kernel-Objekt. Beim Erkennen eines neuen Geräts entsteht das Kobject, was ein Uevent auslöst. Ändert das Gerät seinen Zustand, erhalten über das Kernel-Objekt – und damit verknüpft über das Uevent-Subsystem – alle interessierten Applikationen eine Benachrichtigung. Dasselbe geschieht, sobald man das Gerät (und damit das Kernel-Objekt im Treiber) entfernt.
Uevents werden über die Funktion »kobject_uevent()« unter Angabe des Ereignistyps ausgelöst. Zu den entsprechenden Ereignissen zählen unter anderem das Hinzufügen oder Entfernen von Kernel-Objekten (»KOBJ_ADD«, »KOBJ_REMOVE«), Änderungen am Zustand (»KOBJ_CHANGE«), das (De-)Aktivieren von Geräten oder CPU-Kernen (»KOBJ_ONLINE«, »KOBJ_OFFLINE«) oder auch die Verknüpfung zwischen Treiber und Gerät (»KOBJ_BIND«, »KOBJ_UNBIND«).
Achtung, hier gibt es noch einen Fallstrick: Das Auslösen von Ereignissen funktioniert nur, wenn Kobjects auch einem Kset zugeordnet sind und das zum Kset gehörende Kobject das Elter des auslösenden Objekts ist. Das global definierte Kobject »kernel_kobj« gehört beispielsweise keinem Kset an, sodass dessen Kindobjekte auch kein Uevent auslösen können.
Verfolgen kann man Uevents übrigens sehr gut über das den meisten Distributionen beiliegende Werkzeug Udevadm. Rufen Sie es mit der Option »monitor« auf, protokolliert es sämtliche Events (Abbildung 4).
Alles easy
Programmtechnisch ist der Umgang mit den Kernel-Objekten einfach. Ein Kernel-Modul alloziert Speicher für ein »struct kobject« und initialisiert die Struktur durch Aufruf der Funktion »kobject_init()«, unter anderem mit einem Namen. Ein nachfolgendes »kobject_add()« erstellt im Sys-Filesystem ein Verzeichnis mit dem Objektnamen.
Zusätzlich lassen sich per »sysfs_create_file()« Attributdateien zum Abfragen und Setzen der Geräteeigenschaften anlegen. Ein späteres Entladen des Kernel-Moduls rollt die Aktionen zurück: Die Attributdateien werden gelöscht (»sysfs_remove_file()«), das Verzeichnis entfernt (»kobject_del()«) und schließlich der Referenzzähler durch Aufruf von »kobject_put()« dekrementiert.
Zum Anlegen der Attributdateien nimmt die Datenstruktur »struct kobj_attribute« neben Zugriffsrechten auch die Adressen von Callback-Funktionen auf. Am einfachsten lässt sich die Datenstruktur mithilfe des Makros »__ATTR()« initialisieren. Ihm übergeben Sie als ersten Parameter den Namen, als zweiten die Zugriffsrechte und als dritten und vierten die Namen der Zugriffsfunktionen. Die so initialisierte Datenstruktur schieben Sie nebst der Adresse des Kernel-Objekts, für das die Eigenschaften gelten sollen, als Parameter in die Funktion »sysfs_create_file()«.
Alles ASCII
Der Charme dieser Architektur liegt in der in aller Regel sehr simplen Implementierung der Callback-Funktionen sowie dem intuitiv möglichen Zugriff auf die Informationen aus dem Userland heraus. So genügt ein »cat«, um die Information aus dem Kernel (typischerweise in ASCII) auszulesen. Über ein schlichtes »echo« lassen sich – entsprechende Zugriffsrechte vorausgesetzt – Konfigurationen vornehmen.
Die beim Zugriff per Cat und Echo aktivierten Callbacks tragen im Kernel die Namen »show« und »store«. Sie lassen sich deshalb so einfach implementieren, weil der Kernel dazu nur das Füllen eines Speicherbereichs mit den auszugebenden Daten erwartet. Um den eigentlichen Transfer zwischen Kernel- und Userspace und die damit verbundene Fehlerbehandlung müssen Sie sich dabei nicht kümmern.
Das bringt allerdings die Einschränkung mit sich, dass sich über dieses Interface keine großen Datenmengen transferieren lassen, da der übergebene Buffer eine endliche Größe hat. In der Praxis spielt diese Nebenbedingung aber kaum eine Rolle.
Gruppenarbeit
Es gibt weitere Vereinfachungen. So eröffnet die Funktion »sysfs_create_group()« die Möglichkeit, mehrere semantisch zusammengehörige Attributdateien gleichzeitig anzulegen. Dazu übergeben Sie der Funktion neben der Adresse des Kobjects ein Objekt des Typs »struct kobj_attribute_group()«, das ein Feld von »kobj_attribute«-Strukturen bündelt. Beim Erzeugen entstehen alle Attributdateien in der Gruppe als eine Einheit, sprich: unterhalb eines Verzeichnisses. Anders als Ksets, die ganze Kernel-Objekte gruppieren, erledigt diese Funktion das nur für die Attributdateien der Objekte.
Des Weiteren implementiert die Funktion »kobject_create_and_add()« das Erzeugen, Initialisieren und Aktivieren eines Kernel-Objekts inklusive der Fehlerbehandlung in einem Zug. Das ist sehr praktisch, wie Zeile 29 in Listing 2 demonstriert. Das aus dem Quellcode »sysfs_linmag.c« generierte Modul »sysfs_linmag.ko« implementiert das Sysfs-Verzeichnis »/sys/kernel/linux-magazin/« und unterhalb davon die Attributdatei »linmag_file« mit den Zugriffsfunktionen »linmag_show()« und »linmag_store()«. Beim lesenden Zugriff auf die Attributdatei wird ein Zugriffszähler ausgegeben, der sich durch einen Schreibzugriff auf einen beliebigen Wert setzen lässt.
Listing 2
sysfs_linmag.c
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>
static struct kobject *lm_kobject;
static int access_count;
static ssize_t linmag_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf) {
access_count++;
return sprintf(buf, "%d times accessed\n",
access_count);
}
static ssize_t linmag_store(struct kobject *kobj,
struct kobj_attribute *attr, const char *buf,
size_t count) {
int ret;
ret = kstrtoint(buf, 10, &access_count);
if (ret < 0)
return ret;
return count;
}
static struct kobj_attribute linmag_attribute =
__ATTR(linmag_file,0664,linmag_show,linmag_store);
static int __init lm_module_init(void) {
int ret;
lm_kobject = kobject_create_and_add(
"linux-magazin", kernel_kobj);
if (!lm_kobject)
return -ENOMEM;
ret = sysfs_create_file(lm_kobject,
&linmag_attribute.attr);
if (ret)
kobject_put(lm_kobject); // Objekt freigeben
return ret;
}
static void __exit lm_module_exit(void) {
sysfs_remove_file(lm_kobject,
&linmag_attribute.attr);
kobject_put(lm_kobject); // Objekt freigeben
}
module_init(lm_module_init);
module_exit(lm_module_exit);
MODULE_LICENSE("GPL");
Der Quellcode lässt sich mithilfe des Makefiles aus Listing 1 übersetzen, laden und testen (Abbildung 5). Listing 3 zeigt eine Variante des Quellcodes, die ein Kset einführt und damit das Signalisieren von Zustandsänderungen per Uevent ermöglicht.
Listing 3
sysfs_uevent.c
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>
#include <linux/slab.h>
static struct kobject *kobj;
static int access_count;
static struct kset *linmag_kset;
static void linmag_kobj_release(
struct kobject *kobj) {
printk("kobject: (%p): %s\n", kobj, __func__);
kfree(kobj);
}
static struct kobj_type linmag_kobj_ktype = {
.release = linmag_kobj_release,
.sysfs_ops = &kobj_sysfs_ops,
};
static ssize_t linmag_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf) {
access_count++;
return sprintf(buf, "%d times accessed\n",
access_count);
}
static ssize_t linmag_store(struct kobject *kobj,
struct kobj_attribute *attr, const char *buf,
size_t count) {
int ret;
ret = kstrtoint(buf, 10, &access_count);
if (ret < 0)
return ret;
return count;
}
static struct kobj_attribute linmag_attribute =
__ATTR(linmag_file,0664,linmag_show,linmag_store);
static int __init lm_module_init(void) {
int ret;
linmag_kset = kset_create_and_add("kset_linmag",
NULL, kernel_kobj);
if (!linmag_kset)
return -ENOMEM;
kobj = kzalloc(sizeof(*kobj), GFP_KERNEL);
if (!kobj) {
kset_unregister(linmag_kset);
return -ENOMEM;
}
kobject_init(kobj, &linmag_kobj_ktype);
kobj->kset = linmag_kset;
ret = kobject_add(kobj, &linmag_kset->kobj,
"linux-magazin");
if (ret) {
kobject_put(kobj);
kset_unregister(linmag_kset);
return ret;
}
ret = kobject_uevent(kobj, KOBJ_ADD);
if (ret < 0) {
printk("kobject_uevent( %p, %d) failed with:
%d\n", kobj, KOBJ_ADD, ret);
kobject_put(kobj);
kset_unregister(linmag_kset);
return ret;
}
ret = sysfs_create_file(kobj,
&linmag_attribute.attr);
if (ret) {
kobject_put(kobj); // Objekt freigeben
kset_unregister(linmag_kset);
}
return ret;
}
static void __exit lm_module_exit(void) {
kset_unregister(linmag_kset);
sysfs_remove_file(kobj, &linmag_attribute.attr);
kobject_put(kobj); // Objekt freigeben
}
module_init(lm_module_init);
module_exit(lm_module_exit);
MODULE_LICENSE("GPL");
In einem Gerätetreiber sind Kernel-Objekte übrigens nicht direkt sichtbar. Sie verbergen sich in von »struct kobject« abgeleiteten Datenstrukturen wie der »struct cdev«, die über die Funktionen »cdev_alloc()« und »cdev_add()« verhackstückt wird.
Relevanter sind aber die Funktionen »class_create()« und »device_create()«. Erstere erzeugt ein Kernel-Objekt und legt im Sys-Filesystem ein zugehöriges Verzeichnis an, Letztere erstellt unterhalb des Verzeichnisses einen Unterordner mit dem Namen des Geräts. Dort befindet sich unter anderem die Attributdatei »dev«, die die aus Major- und Minor-Nummer bestehende Gerätenummer enthält.
Sobald diese Datei angelegt wird, informiert das Uevent-Subsystem den sogenannten Udev-Daemon darüber. Der wiederum erstellt auf Basis der in der Datei hinterlegten Daten unterhalb des Verzeichnisses »/dev« die zugehörige Gerätedatei. Ein Gerätetreiber nutzt das Kernel-Objekt also, um damit die unterstützten Geräte zu repräsentieren und über das Sys-Filesystem Applikationen die Interaktion zu ermöglichen.
Fazit
Als Bindeglied zwischen Kernel und Userland ermöglichen Kobjects somit neben dem Datenaustausch durch Lesen und Schreiben auch die Signalisierung von Zustandsänderungen. Allerdings taugt die Technik nicht zum Austausch großer Datenmengen oder zur Kommunikation innerhalb des Kernels selbst. Hier ist der direkte Zugriff auf die Daten beziehungsweise Funktionen deutlich effizienter als der Umweg über das Datei-Interface. (jlu)
Die Autoren
Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von Open Source. Jürgen Quade, Professor an der Hochschule Niederrhein, führt auch für Unternehmen Schulungen zu den Themen Treiberprogrammierung und Embedded Linux durch.
Infos
- Dennis M. Ritchie, “The Evolution of the Unix Time-sharing System”, Bell Laboratories, Murray Hill, NJ, 07974, September 1979: https://www.bell-labs.com/usr/dmr/www/hist.html
- Beispielcode zu Kset im Linux-Quellcode: https://github.com/torvalds/linux/blob/master/samples/kobject/kset-example.c











