Wer in die Verlegenheit kommt, ein Rettungssystem zu brauchen, vermisst schnell die Annehmlichkeiten einer automatischen Hardware-Erkennung. Die beiden exklusiv in diesem Artikel vorgestellten universellen Skripte scannen angeschlossene Geräte und listen übersichtlich die passenden Kernelmodule auf.
Die große Vielfalt der heute eingesetzten Hardware erfordert eine kaum übersichtlichere Anzahl an Treibern und Modulen im Linux-Kernel. Funktioniert die automatische Hardware-Erkennung einmal nicht wie gewohnt, etwa weil die installierte Distribution nicht mehr startet oder eine neu eingebaute Komponente nicht automatisch erkennt, muss der Administrator selbst nach den passenden Kernelmodulen suchen.
Prinzipiell stellen der Kernel und die Modullisten alle nötigen Informationen bereit, um den zum jeweiligen Gerät gehörigen Treiber zu finden angesichts generischer Treiber für ganze Geräteklassen kann die Suche allerdings sehr kompliziert sein.
Die Skripte »pcidetect« für PCI und »usb-detect« für USB von [1] erleichtern den Umgang mit Rettungssystemen, Mini-Distributionen, Rescue-CDs und Embedded-Linux-Installationen, indem sie auflisten, welche Treiber direkt im Kernel einkompiliert sind, welche Module eventuell nachzuladen sind und welche Hardware vom aktuell laufenden Kernel gar nicht unterstützt wird.
Kernel-Interfaces
Die Hardware-Erkennung für PCI, USB und Firewire ist mit ein wenig Hintergrundwissen keine Hexerei und lässt sich mit nahezu jeder beliebigen Programmiersprache realisieren der Autor wählte die Bash für die beiden Beispiel-Skripte von [1]. Alle nötigen Informationen zur Hardware wie Hersteller, Gerät und Geräteklasse stellt der Kernel per SysFS oder Proc zur Verfügung.
Am einfachsten funktioniert die Hardware-Erkennung mit SysFS, da hier anders als beim Proc-Interface keine Binärdaten zu verarbeiten sind.
Bei Sysfs gibt es für jedes PCI-Gerät unterhalb von »/sys/bus/pci/devices« einen symbolischen Link mit der jeweiligen PCI-ID, der auf das entsprechende Geräteverzeichnis unterhalb von »/sys/devices/pci*« zeigt. Der Zugriff über »/sys/bus/pci/devices« gestaltet sich einfacher, weil hier alle PCI-Geräte aller PCI-Busse zusammengefasst sind, während es unterhalb von »/sys/devices« für jeden PCI-Bus ein separates Verzeichnis mit den Geräten gibt.
Das Geräteverzeichnis enthält alle für die Hardware-Erkennung nötigen Daten: »vendor« verrät die Vendor-ID des Geräteherstellers in Hexadezimal-Schreibweise, »device« die Product-ID und »class« die Geräteklasse nach [3]. Die Zeilen 11 bis 16 von Listing 1 lesen die für die Hardwarebestimmung nötigen Daten aus der SysFS-Struktur ein.
Unterhalb von »/proc/bus/pci« sind die Pseudodateien der Geräte nach PCI-Controllern sortiert aufgelistet. Jede Gerätedatei enthält alle Angaben in konzen-trierter Form, die SysFS einzeln bereitstellt. Die Zeilen 18 bis 20 in Listing 1 zeigen den Abschnitt aus dem PCI-Hardware-Erkennungsskript »pcidetect« von [1], der Vendor- und Product-ID sowie den Class Code ermittelt. Eine komplette Beschreibung der Pseudodatei liefert der Artikel aus [4].
PCI-ID-Datenbank
Für die Klartextausgabe von Hersteller und Namen der PCI-Geräte benötigt das Hardware-Erkennungsskript die PCI-ID-Datenbank von [2], die auch der Linux-Kernel und »lspci« verwenden. Die PCI-Datenbank lässt sich sehr einfach parsen, Listing 2 zeigt den entsprechenden Abschnitt aus »pcidetect«. Die Variable »PCIIDCMD« enthält den Befehl zum Ein-lesen der PCI-ID-Datenbank, entweder ein einfaches »cat« der Datei oder einen entsprechenden »wget«-Aufruf.
In der Datei »pci.db« besteht jeder Hersteller- oder Geräte-Eintrag aus einer Zeile, in der die einzelnen Felder per Tabulator getrennt sind. Um jeweils eine komplette Zeile abzuarbeiten, setzt Zeile 1 von Listing 2 die Trennzeichen-Variable »IFS« auf den Zeilenumbruch. Um die Zeilen der Datenbank in ihre Felder aufzuspalten, schreibt Zeile 3 des Listings ein Tabulatorzeichen in die Variable »IFS«. Der »set«-Aufruf in Zeile 4 übernimmt dann die Trennung.
Die Abfrage in Zeile 8, ob die vierte Spalte eine »0« enthält, dient der Qualitätssicherung: Die PCI-Datenbank kann von jedermann erweitert werden, solche neuen Einträge werden mit einer »1« in Spalte 4 gekennzeichnet. Verifizierte Einträge bekommen dann eine »0«. Die Vendor-ID bei Herstellereinträgen und die kombinierte Vendor- und Device-ID bei Geräte-Einträgen sowie die Beschreibung speichern die Zeilen 9 und 13.
|
Listing 1: PCI-IDs |
|---|
01 if [ -e /sys/bus/pci/devices ]; then
02 PCIDevices=/sys/bus/pci/devices/*
03 Method="sysfs"
04 elif [ -e /proc/bus/pci ]; then
05 PCIDevices=/proc/bus/pci/??/*
06 Method="proc"
07 fi
08
09 for device in $PCIDevices; do
10 if [ "$Method" = "sysfs" ]; then
11 read Vendor < ${device}/vendor
12 Vendor=${Vendor:2:4}
13 read Device < ${device}/device
14 Device=${Device:2:4}
15 read Class < ${device}/class
16 Class=${Class:2:4}
17 elif [ "$Method" = "proc" ]; then
18 Vendor=`hexdump -s 0 -n 2 -e '1/2 "%04x"' $device`
19 Device=`hexdump -s 2 -n 2 -e '1/2 "%04x"' $device`
20 Class=`hexdump -s 10 -n 2 -e '1/2 "%04x"' $device`
21 fi
|
|
Listing 2: PCI-ID-Datenbank |
|---|
01 IFS="${Newline}"
02 for z in `eval ${PCIIDCMD}`; do
03 IFS="${Tab}"
04 set -- $z
05
06 case "$1" in
07 v)
08 if [ "$4" = "0" ]; then
09 declare v${2}=$3
10 fi
11 ;;
12 d)
13 declare d${2}=$3
14 ;;
15 esac
16 done
|
Variable Variablennamen
Die Speicherung der PCI-ID-Datenbank bereitet in der Bash große Probleme: Sowohl Vendor- als auch Device-ID sind 16 Bit groß, die Bash erlaubt aber keine zweidimensionalen Arrays und nur 16-Bit-Indizes, Hashes gibt es auch nicht. Die Lösung sind variable Variablennamen. Die Zuweisung erfolgt (Listing 2, Zeilen 9 und 13) nach dem Muster:
declare prefix${name}=$value
Variablen von Herstellereinträgen erhalten ein »v« für Vendor als Prefix im Variablennamen, gefolgt von der Vendor-ID als vierstelliger hexadezimaler Zahl, Geräte-Einträge beginnen mit einem »d«, gefolgt von der Vendor- und Device-ID. Diese Verarbeitungsmethode hat gegenüber einem »grep« für jedes Gerät den Vorteil, dass die PCI-ID-Datenbank nur einmal eingelesen werden muss dafür ist der Speicherverbrauch mit 8 bis 9 MByte jedoch enorm, zudem dauert es je nach Rechner bis zu einer Minute, bis das Skript abgearbeitet ist.
Module zuordnen
Ähnlich wie die PCI-ID-Datenbank wird auch die Datei »modules.pcimap« des aktuellen Kernels eingelesen. Sie enthält eine Auflistung aller Kernelmodule und der vom jeweiligen Modul unterstützten Geräte in Form der Vendor- und Device-ID sowie des Class Code bei generischen Treibern wie Sound- oder Firewire-Controller. Listing 3 enthält den Code-Abschnitt, der die »modules.pcimap« parst und ähnlich der PCI-ID-Datenbank in variablen Variablennamen ablegt.
Die Funktion von Listing 3 entspricht der von Listing 2, nur dass diesmal die einzelnen Datenfelder mit Leerzeichen, nicht mit Tabulatoren getrennt sind. Eine weitere Besonderheit ist die Auswertung des Class Code in Zeile 6, falls Vendor- und Device-ID jeweils den Wert »-1« besitzen. Zeile 8 ist für generische Treiber eines Herstellers verantwortlich.
Die Abfrage in Zeile 14 enthält ein seltenes Konstrukt, »${!id}«. Es handelt sich um die Rückgabefunktion des Werts einer Variablen mit variablem Namen: Steht unmittelbar hinter der geschweiften Klammer ein Ausrufezeichen, interpretiert die Bash alle nachfolgenden Zeichen bis zur schließenden Klammer als den Namen jener Variablen, deren Wert als Variablenname für den ganzen Ausdruck verwendet wird. Enthält die Variable »id« den Wert »3c59x«, liefert »${!id}« den Inhalt der Variablen »3c59x«, die Schreibweise »${!id}« entspricht also in diesem Fall »${3c59x}«.
Innereien des Kernels
Um festzustellen, für welche PCI-Geräte noch eine Treiberunterstützung fehlt, muss das Hardware-Erkennungsskript die in den Kernel einkompilierten Treiber ermitteln. Dazu bedient es sich der Datei »System.map«. Jeder Treiber fügt dem Kernel ein Symbol mit folgendem Muster hinzu:
__devicestr_vendor-/device-id
Listing 4 zeigt den Code-Abschnitt des Hardware-Erkennungs-Skripts, der die Datei »System.map« einliest und die Vendor- sowie Device-ID der vom Kernel unterstützten Geräte wie gehabt in Variablen mit variablem Namen speichert. Die Felder der einzelnen Zeilen der »System.map« sind mit Tabulatoren unterteilt. Die einzige Besonderheit gegenüber Listing 2 und 3 ist, dass in Listing 4 der Symboleintrag anhand der Unterstriche noch einmal zerlegt und lediglich für jedes unterstützte Gerät eine »1« gespeichert wird. Anders als bei den Variablen für die Module beginnen die Namen der Kernelvariablen mit k.
Nachdem Hersteller- und Gerätebezeichnung, die Kernelmodule und ihre Zuständigkeiten sowie die vom Kernel direkt unterstützten Geräte eingelesen und gespeichert wurden, bleibt noch die Ausgabe für den Benutzer (siehe Abbildung 1) in Listing 5, es ist die unmittelbare Fortsetzung von Listing 1. In den Zeilen 1 bis 6 von Listing 5 werden die Namen der Varia-blen zusammengesetzt, um die Bezeichnung von Hersteller und Ge-rät, die nötigen Module, die generische Module, die Kernelunterstützung sowie Module für die jeweilige Geräteklasse zu ermitteln.
Die restlichen Zeilen von Listing 5 bergen an-sonsten keine Be-sonder-heiten mehr, das Skript greift einfach über die variablen Variablennamen auf die entsprechenden Variablen mit den Kernelmodulen oder Gerätebeschreibungen zu und gibt sie aus. Einzig Zeile 10 erscheint auf den ersten Blick etwas kompliziert, hier prüft das Skript, ob es Einträge für die Geräte bei den regulären Kernelmodulen, den Hersteller-spezifischen Modulen oder denen für ganze Geräteklassen gibt.
|
Listing 4: |
|---|
01 IFS="${Newline}"
02 for z in `cat $SYSTEMMAP`; do
03 IFS="${Tab} "
04 set -- $z
05
06 if [ "${3:0:12}" = "__devicestr_" ]; then
07 IFS="_"
08 set -- $3
09 declare k${4}=1
10 fi
11 done
|
|
Listing 5: Ausgabe |
|---|
01 v="v${Vendor}"
02 d="d${Vendor}${Device}"
03 m="m${Vendor}${Device}"
04 g="m${Vendor}"
05 k="k${Vendor}${Device}"
06 c="c${Class}"
07
08 echo "Hersteller: ${!v}${Tab}[0x${Vendor}]"
09 echo "Gerät: ${!d}${Tab}[0x${Device}]"
10 if [ -n "${!m}" -o -n "${!g}" -o -n "${!c}" ]; then
11 set -- ${!m} ${!g} ${!c}
12 if [ "$#" -gt "1" ]; then
13 echo "Kernel-Module: $*"
14 else
15 echo "Kernel-Modul: $1"
16 fi
17 elif [ -n "${!k}" ]; then
18 echo "Vom Kernel unterstützt"
19 else
20 echo "Nicht unterstützt"
21 fi
22 echo
23 done
|
Ausblick auf USB und Firewire
Auch die Hardware-Erkennung für USB-Geräte unterscheidet sich vom PCI-Skript nur minimal, abgesehen von der wesentlich umständlicher formatierten und schlechter zerlegbaren USB-ID-Liste greift das USB-Erkennungsskript von [1] lediglich auf andere Pseudodateien aus »/sys« und »/proc« zu. Selbst eine Firewire-Hardware-Erkennung ist nach dem gleichen Strickmuster möglich, auch wenn der Autor hier kein spezielles Skript anbietet.
Die einfache Auflistung der einzelnen unterstützten Geräte und ihrer Kernel-module ist nur ein erster Schritt, um sich etwa auf einem Router oder Embedded-Linux-System einen Überblick zu verschaffen. Mit geringfügigen Änderungen, indem man zum Beispiel die PCI- und USB-ID-Datenbank entfernt und statt der Ausgabe der Kernelmodule automatisch ein »modprobe« durchführt, taugt die Hardware-Erkennung auch als Start-skript, um beim Hochfahren alle nötigen Kernelmodule zu laden.
|
Infos |
|---|
|
[1] Hardware-Erkennungsskripte für PCI und USB: [https://www.linux-magazin.de/Service/Listings/2005/09/hwdetect] [2] PCI-ID-Datenbank: [http://pciids.sf.net/pci.db] [3] PCI-Headerdatei mit Vendor-, Device- und Class-Definitionen: [http://www.pcidatabase.com/pci_c_header.php] [4] Eva-Katharina Kunst, Jürgen Quade, “Kerntechnik Folge 3: PCI-Geräte programmieren”: Linux-Magazin 10/03, S. 81 |
Copyright © 2005 Linux New Media AG






