Linux stellt mir eine Unmenge an Werkzeugen für die Fehlersuche zur Verfügung. Etliche davon kommen mir sowohl bei Total- als auch bei Partialausfällen zu gute. Andere bei Performanceproblemen. Einige sind so nützlich, dass ich sie immer wieder bei den unterschiedlichsten Problemen einsetze.
Da es mir schwerfällt, die einzelnen Werkzeuge bestimmten Kategorien zuzuordnen, stelle ich sie nachfolgend in alphabetischer Reihenfolge vor. Das erleichtert zumindest das Wiederfinden, wenn ich nur etwas nachschlagen möchte.
Abrechnungsprogramme spielen eine wichtige Rolle bei der Performanceoptimierung. Sie liefern einen einfachen Weg um herauszufinden, was ein Rechner macht, welche Anwendungen laufen, wieviel Systemzeit die Prozesse verbrauchen, wie stark sie das System belasten. Wenn ich die Programme und ihr Verhalten im Großen und Ganzen kenne, kann ich mir eine Strategie überlegen, um die Performance zu optimieren. Das Programmpaket acct liefert diese Abrechnungsdaten.
Natürlich belastet das Führen der Statistiken das System zusätzlich. Und Extra-Plattenplatz benötigen die Statistiken auch. Andererseits: wenn das System deutlich auf das Einschalten der Statistikerfassung reagiert, arbeitet es bereits in einem Bereich nahe der Belastungsgrenze und eine Analyse der Systemperformance und entsprechende Maßnahmen sind längst fällig.
Ist das Paket acct installiert, so wird das Accounting meist automatisch
beim Systemstart via accton
eingeschaltet.
Will ich es deaktivieren, reicht es nicht, es über das Startscript
auszuschalten, da cron in regelmäßigen Abständen die Komprimierung der
Protokolle anstößt und dabei das Accounting ab- und wieder angeschaltet wird.
Um das Accounting zu deaktivieren ändere ich bei Debian-Systemen in
/etc/default/acct die Variable ACCT_ENABLE
auf 0.
Die Abrechnungsdaten werte ich mit dem Programm sa
aus.
sa
gibt eine Tabelle aus mit einer Zeile pro Programm,
der Anzahl der Aufrufe des Programms in der ersten Spalte und den
folgenden Bezeichnungen in den anderen Spalten:
Ganz rechts, ohne Bezeichnung steht der Programmname.
Ist dieser mit einem Asterisk (‘*’) gekennzeichnet, ist das Programm als
Daemon gelaufen. Das heißt, es hat fork()
aufgerufen, aber nicht
exec()
.
Daemon-Prozesse sammeln durch ihre lange Laufzeit sehr viel CPU-Zeit an.
Die Tabelle ist nach der CPU-Zeit absteigend sortiert.
Programme, die nur einmalig gelaufen sind, werden in der Zeile
***other*
zusammengefasst.
Mit den folgenden Optionen kann ich die Ausgabe von sa modifizieren, die Handbuchseite kennt noch mehr davon:
***other*
zusammen.sa -s
.Bei jeglicher Art von Performance Tuning mache ich grundsätzlich je eine Bestandsaufnahme vor den Tuning-Maßnahmen und danach, um mich von der Wirkung des Tunings zu überzeugen. Bonnie++ ist ein Programm, mit dem ich die Performance der Lese- und Schreiboperationen im Dateisystem in Zahlen ausdrücken und damit vergleichen kann. Ich setze bonnie++ ein, wenn ich
hdparm
,
verifizieren will,Bonnie++ sollte niemals auf aktiven Produktionsmaschinen laufen, da die Performance durch die Tests sehr stark beeinträchtigt wird.
Das Programm gibt für jeden Test, den es durchführt, zwei Kennzahlen aus: die geschaffte Arbeit (je mehr, um so besser) und die dafür benötigte CPU-Zeit (je weniger, umso besser).
Die Tests teilen sich grob in zwei Abschnitte. In einem Abschnitt testet bonnie++ den I/O-Durchsatz mit großen Dateien, wie er ähnlich bei Datenbankanwendungen vorkommt. Im anderen Abschnitt geht es um das Erzeugen, Lesen und Löschen vieler kleiner Dateien, wie es auf Proxy-, Mail- und News-Servern vorkommt.
In den meisten Fällen bin ich daran interessiert, das I/O-Verhalten bei einzelnen Dateizugriffen zu beobachten. Bei bestimmten Problemen möchte ich jedoch den Einfluss von gleichzeitigen Dateizugriffen bewerten. Zu diesem Zweck kann ich mehrere bonnie++ Prozesse synchron starten.
Die Ausgabe von bonnie++ kommt, wie schon beim Vorgängerprogramm bonnie als
Text mit 80 Spalten.
Zusätzlich gibt bonnie++ die Daten als kommaseparierte Werte (CSV)
aus, die einfacher weiterverarbeitet werden und mehr als 80 Zeichen pro Zeile
einnehmen können.
Für diese CSV-Daten gibt es zwei Programme (bon_csv2html
, bon_csv2txt
),
die die Daten für die HTML-Ausgabe beziehungsweise das bekannte Textformat
aufbereiten.
Die Felder der CSV-Daten sind in den Handbuchseiten der beiden Programme
beschrieben.
bonnie++
leere Dateien an.
Es ist möglich, die maximale und minimale Größe der Dateien und die Anzahl
der Verzeichnisse durch Doppelpunkt getrennt gemeinsam mit der Anzahl
anzugeben.
Details stehen in den Handbuchseiten.bonnie++
als normaler Nutzer starten.
Starte ich es als root, gebe ich mit dieser Option einen anderen Benutzer
vor, um Fehler im Dateisystem zu vermeiden.fsync()
nach jedem Schreiben auf.-ys
verwenden, starten synchron.<RETURN>
eingegeben wurde.O_DIRECT
) für Massen-I/O-Tests verwenden.Um mehrere Prozesse mit bonnie++ synchron zu starten, kann ich wie folgt vorgehen:
$ bonnie++ -p3
$ bonnie++ [weitere Optionen] -ys > out1 &
$ bonnie++ [weitere Optionen] -ys > out2 &
$ bonnie++ [weitere Optionen] -ys > out3 &
Bonnie++ kann mitunter recht lange laufen, vor allem wenn es mit Option
-x
seine Tests wiederholt.
Mit SIGINT kann ich die Ausführung abbrechen, bonnie++ räumt dann wieder auf, das heißt, es entfernt die temporären Dateien und Verzeichnisse. Das kann etwas dauern. Durch wiederholtes Senden von SIGINT bricht es das Aufräumen ab.
SIGHUP wird ignoriert, das heißt, wenn bonnie++ im Hintergrund läuft, bricht es nicht nach Abmelden vom Terminal ab.
Busybox kombiniert Mini-Versionen vieler gebräuchlicher UNIX-Befehle in einem Programm. Es ist mit Blick auf Größenoptimierung und geringen Ressourcenverbrauch geschrieben. Für ein arbeitsfähiges System benötige ich nur noch ein /dev Verzeichnis mit den Gerätedateien, ein /etc Verzeichnis mit den Konfigurationsdateien und einen Linux-Kernel. Busybox ist extrem anpassungsfähig. Beim Kompilieren kann ich festlegen, welche Befehle hineinkommen und so die Funktionalität gegen die Programmgröße abwägen. Das macht es ideal für eingebettete Systeme und kleine Systemumgebungen.
Damit bin ich beim Punkt, warum man Busybox kennen sollte. Es befindet sich auf dem InitRamFS, dem ersten Dateisystem, das zusammen mit dem Kernel vom Bootloader geladen wird. Auf Live-CDs, beim Netboot und bei Rescue-Disks kann ich mit Busybox rechnen. Auch in kleineren Einzelgeräten, wie Routern, wird es oft eingesetzt.
Busybox ist ein Multi-Call-Binary. Das heißt, es verhält sich komplett anders, je nachdem, wie es aufgerufen wird. Sämtliche enthaltenen UNIX-Befehle kann ich auf zwei Arten aufrufen. Entweder ich gebe den Befehl als erstes Argument und danach dessen Argumente an:
$ /bin/busybox cp file file.bak
Oder ich erzeuge einen Link auf Busybox mit dem Namen des Befehls und rufe es über diesen Link auf:
$ /bin/busybox ln -s /bin/busybox ./cp
$ ./cp file file.bak
Hieße der Link stattdessen mv
, hätte busybox
die Datei nicht kopiert
sondern umbenannt.
Weil Busybox extrem konfigurierbar ist, möchte ich vielleicht wissen, welche Funktionen mein konkretes Binary kennt. Dazu rufe ich Busybox ohne Argumente auf:
$ busybox
BusyBox v1.18.5 (Ubuntu 1:1.18.5-1ubuntu4.1) \
multi-call binary.
Copyright (C) 1998-2009 Erik Andersen, Rob Landley, \
Denys Vlasenko
and others. Licensed under GPLv2.
See source distribution for full notice.
Usage: busybox [function] [arguments]...
or: busybox --list[-full]
or: function [arguments]...
BusyBox is a multi-call binary that combines \
many common Unix
utilities into a single executable. Most people \
will create a
link to busybox for each function they wish to \
use and BusyBox
will act like whatever it was invoked as.
Currently defined functions:
[, [[ , acpid, addgroup, adduser, adjtimex, ar, \
arping, ash, awk,
...
xargs, xz, xzcat, yes, zcat
Da die in Busybox implementierten Versionen der UNIX-Befehle von den
täglich benutzten in Details abweichen, muss ich die genauen Optionen wissen.
Neben der Handbuchseite, die ich in einer kleinen Umgebung oft
nicht zur Verfügung habe, hilft mir Busybox selbst.
Ich rufe den Befehl mit der Option --help
auf, welche die meisten der
implementierten Befehle verstehen:
$ busybox ln --help
BusyBox v1.18.5 (Ubuntu 1:1.18.5-1ubuntu4.1) \
multi-call binary.
Usage: ln [OPTIONS] TARGET... LINK|DIR
Create a link LINK or DIR/TARGET to the specified \
TARGET(s)
Options:
-s Make symlinks instead of hardlinks
-f Remove existing destinations
-n Don't dereference symlinks - treat like \
normal file
-b Make a backup of the target (if exists) \
before link operation
-S suf Use suffix instead of ~ when making \
backup files
Als Systemadministrator setze ich die Programmiersprache C selten direkt ein. Gut, zugegeben, ich nutze mein Verständnis dieser Programmiersprache, wenn ich die Ausgabe von strace auswerte. Aber das hätte ich auch ohne dieses Wissen hinbekommen. Wenn ich einen Fehler im Debugger suche, möchte ich die Sprache ebenfalls kennen, aber auch das mache ich als Systemadministrator sehr selten.
Ich nutze es, wenn ich einem Fehler in einem C-Programm nachspüre und keine Gelegenheit habe, mit dem Autor des Programms in Kontakt zu treten. Meist aber, wenn ich ein Werkzeug benötige, das Informationen direkt vom Kernel abgreift oder mit ihm interagiert. Dann ist C das Mittel der Wahl, weil es die Sprache ist, die im Kernel selbst verwendet wird, so dass ich die gleichen Datenstrukturen wie der Kernel in meinem Programm verwenden kann.
Natürlich kann ich mir damit gewaltig in den Fuß schießen, deshalb setze ich die Programmiersprache so defensiv wie möglich ein. Zum Beispiel, wenn ich in meiner bevorzugten Skriptsprache keine Entsprechung für die benötigte Systemfunktion habe. Oder, wenn ich ein Programm finde, das in etwa das macht, was ich will, und nur noch etwas modifiziert werden muss, wie das folgende Beispiel, das auf [ctDiedrich2012] zurückgeht.
Dieses Minimalprogramm überwacht Zugriffe auf die Dateien in einem
Verzeichnis mit der Systemfunktion fanotify und gibt die PID des Prozesses
aus, der auf die Datei zugreift, sowie den Dateideskriptor der Datei in diesem
Prozess.
Will ich mehr über den Prozess erfahren, kann ich mit lsof
alle von ihm
geöffneten Dateien herausbekommen, oder mit strace
seine Systemaufrufe
überwachen.
fnotify.c
1
/* fnotify.c - notify about file access */
2
#include <linux/fcntl.h>
3
#include <stdio.h>
4
#include <sys/fanotify.h>
5
6
int
main
(
int
arg
,
char
**
argv
)
{
7
struct
fanotify_event_metadata
ev
;
8
int
fa
=
fanotify_init
(
FAN_CLASS_NOTIF
,
9
O_RDONLY
|
O_LARGEFILE
);
10
if
(
fa
>=
0
)
{
11
int
m
=
fanotify_mark
(
fa
,
FAN_MARK_ADD
,
12
FAN_OPEN
|
FAN_EVENT_ON_CHILD
,
13
AT_FDCWD
,
argv
[
1
]);
14
if
(
m
)
{
15
perror
(
"error with 'fanotify_mark()'"
);
16
return
2
;
17
}
18
while
(
read
(
fa
,
&
ev
,
sizeof
(
ev
))
19
==
sizeof
(
ev
))
{
20
printf
(
"PID: %d, FD: %d, M: 0x%02llX
\n
"
,
21
ev
.
pid
,
ev
.
fd
,
ev
.
mask
);
22
}
23
}
24
else
{
25
perror
(
"error with 'fanotify_init()'"
);
26
return
1
;
27
}
28
return
0
;
29
}
Mit gcc -o fnotify fnotify.c
übersetze ich das Programm.
Für die Systemaufrufe benötigt das Programm die POSIX Capability CAP_SYS_ADMIN, die ich ihm gebe, wie in Kapitel 6 erläutert. Anschließend kann ich das Programm aufrufen und zum Test in einer anderen Konsole eine Datei in diesem Verzeichnis öffnen.
$ sudo setcap cap_sys_admin=ep fnotify
$ ./fnotify .
PID: 5021, FD: 4, M: 0x20
Mit lsof
schaue ich nach, welches Programm welche Datei geöffnet hat:
$ lsof -p5021
COMMAND... FD TYPE...NAME
less ... txt REG.../bin/less
...
less ... 4r REG.../home/.../code/fnotify.c
In diesem Beispiel habe ich in einer zweiten Konsole den Quelltext mit less
betrachtet, was mir lsof
in einer weiteren Konsole anzeigt.
Fanotify bietet noch weitergehende Möglichkeiten. So kann ich den Prozess blockieren und den Zugriff erlauben oder verbieten. Oder ich modifiziere die Datei vor dem Zugriff. Eine ausführliche Behandlung dieser Systemschnittstelle würde den Rahmen dieses Buches sprengen, aber es ist in manchen Fällen gut zu wissen, dass sich da noch etwas machen lässt.
Ein eher unscheinbares, aber sehr nützliches Werkzeug ist dd
.
Bei der Fehlersuche verwende ich es um
Um Dateien zwischen zwei Rechnern zu kopieren, verwende ich am
liebsten scp
oder rsync
.
In manchen Fällen, wenn beide Befehle nicht zur Verfügung stehen oder ich
Daten von einer Gerätedatei kopieren möchte, greife ich auf dd
zurück.
Bei kleinen Linux-Routern, die von einer read-only eingehängten Flash-Disk betrieben werden, sichere ich die Flash-Disk mit folgendem Befehl, von einem anderen Rechner:
$ ssh root@r-xyz dd if=/dev/sda | dd of=r-xyz.image
Mit SSH starte ich auf dem Router einen Prozess mit dd
, der den Inhalt von
/dev/sda, der ersten Festplatte, zur Standardausgabe schreibt. Die
Standardausgabe dieses Prozesses leite ich via SSH und Pipe an einen lokalen
Prozess mit dd
, der von Standardeingabe liest und in die Datei r-xyz.image
schreibt.
Partitionstabellen sichere ich, wenn ich mehrere Betriebssysteme auf einer Festplatte installieren will und damit rechnen muß, dass das Linux-System unbootbar wird, wenn ich ein anderes System installiere. Aber auch, wenn ich die Partitionen manipuliere, weil sich beispielsweise die Größe eines Festplattenimages geändert hat. Ebenso ist die Sicherung vor der Installation eines Bootloaders in der Partitionstabelle angezeigt.
# dd if=/dev/sda of=sda.image bs=512 count=1
Damit sichere ich die Partionstabelle.
# dd if=sda.image of=/dev/sda
Dieser Befehl stellt sie wieder her. Natürlich muss ich die Image-Datei an einem sicheren und erreichbaren Ort lagern.
Der dritte Anwendungsfall ist das “Reparieren” von defekten Festplattensektoren.
Moderne Festplatten haben ungenutzten Plattenplatz, mit denen sie defekte Sektoren ersetzen können. Allerdings nur, wenn auf diesen Sektor geschrieben wird.
# dd if=/dev/null of=/dev/sda1 bs=$BLOCKSIZE \
seek=$OFFSET count=1 oflag=direct,dsync
Mit diesem Befehl beschreibe ich gezielt einen Sektor der Festplatte.
Dabei ist $BLOCKSIZE
die Größe eines Sektors der Festplatte und $OFFSET
die
Differenz zwischen dem defekten Sektor und dem Anfang der Partition in
Sektoren.
Wenn die Plattenelektronik beim Schreiben bemerkt, dass der ursprüngliche
Block defekt ist, ersetzt sie diesen durch einen Reserveblock und die
Festplatte ist “repariert”.
Wie ich herausbekomme, welchen Sektor ich beschreiben muss, ist im Bad Block Howto in Englisch und in einem Artikel auf Pro-Linux.de in deutscher Sprache beschrieben.
Manche Fehler treten nur unter Last zutage. Genauso ist es beim Performance-Tuning. Ob meine Maßnahmen erfolgreich sind, sehe ich erst, wenn das System unter Last arbeitet. Damit ich nicht auf die Systembenutzer angewiesen bin, deren Last meist stochastisch und nicht vorhersagbar ist, benötige ich in diesem Fall ein Werkzeug, das mir eine geeignete Last erzeugen kann.
Früher habe ich dann schnell ein kleines Skript oder Programm gebastelt, das ungefähr die geforderte Last erzeugt, und dieses anschließend entsorgt.
Mit fio
brauche ich kein Programm mehr zu schreiben, sondern nur noch eine
Beschreibung für die gewünschte Last.
Und das kann fast jede erdenkliche Last sein.
Das Programm fio
kann in einem Job synchron oder asynchron schreiben, per
mmap() eingebundene Dateien bewegen, Netzlast simulieren oder einfach nur
CPU-Zeit verbrennen.
Und es kann mehrere Jobs gleichzeitig ausführen, so dass ich mir die
gewünschte Last sehr genau zusammenstellen kann.
Die Jobs, die fio
abarbeitet, werden in Dateien beschrieben, deren
Format den Ini-Dateien entspricht, mit Sektionen, die durch ihren Namen in
eckigen Klammern eingeleitet werden und Kommentaren, die mit ;
oder #
am Zeilenanfang beginnen.
Meist rufe ich fio
nur mit dem Namen der Job-Datei als einzigem Argument auf.
Habe ich nur einen einzigen Job, könnte ich dessen Parameter auch gleich auf
der Kommandozeile angeben.
Details zum Einsatz von fio
finde ich in der Handbuchseite, der Datei
HOWTO und den Beispiel-Job-Dateien im Verzeichnis examples/ bei der
Paketdokumentation.
Das Programm fuser
setze ich ein, wenn ich Informationen darüber
haben will, welche Prozesse bestimmte Dateien oder Netzwerksockets geöffnet
haben, um sie dann mit anderen Programmen zu untersuchen.
Zwar kann ich die ermittelten Prozesse gleich von fuser
beenden lassen,
aber in diesem Buch geht es vor allem um die Fehleranalyse und dafür wäre
dieses Vorgehen doch zu voreilig.
Was mich vor allem interessiert, sind die Prozesse und die Art und Weise,
wie diese die betreffenden Dateien verwenden.
Die Prozesse zeigt fuser
in einer Tabelle an.
In der ersten Spalte steht der Dateiname, dahinter die PID.
Mit der Option -v kann ich diese Ausgabe
erweitern, so dass fuser
für jeden Prozess den Benutzer (USER), die PID, den
Zugriff (ACCESS) und den Namen des Prozesses (COMMAND) anzeigt.
Die Spalte ACCESS interessiert mich am meisten für die Analyse. Diese kann die folgenden Merkmale haben:
Mit diesen Merkmalen bekomme ich heraus, wie ein Prozess eine Datei
verwendet.
Allerdings sehe ich das nur für Dateien, die im Dateisystem verlinkt sind.
Dateien, die zwar geöffnet, aber nicht mehr im Dateisystem verlinkt sind,
finde ich damit nicht, dafür benötige ich andere Programme, wie zum Beispiel
lsof
.
Mit der Option --mount
(-m
) bekomme ich von fuser
zumindest die
PID dieser Prozesse, und kann diese dann mit lsof
näher untersuchen.
Dazu muss ich folgende Besonderheit der Ausgabe von fuser
beachten.
Das Programm schreibt nur die PIDs an die Standardausgabe, alles andere kommt
über die Fehlerausgabe.
Damit kann ich die Ausgabe sehr bequem in Scripts weiterverarbeiten:
for p in $(fuser -m /); do
lsof -p $p
done
Will ich die Ausgabe allerdings in einem Pager betrachten, oder dokumentieren, so schreibe ich:
fuser -m / 2>&1 | less
Außer für Dateien in Dateisystemen kann ich mit fuser
auch die Prozesse
ermitteln, die bestimmte Sockets geöffnet haben.
Dazu wähle ich den entsprechenden Namensraum mit der Option --namespace SPACE
(-n SPACE
) aus.
Das Programm kennt die folgenden Namensräume:
Sockets werden nach dem folgenden Schema angegeben:
[locport][,[remhost][,[remport]]][/namespace]
Den Namensraum kann ich angeben, wenn die Angabe eindeutig ist und ich
diesen nicht explizit mit --namespace
angeben will.
Die Komma sind wichtig.
So zeigt fuser ssh/tcp
alle Prozesse, die mit
dem lokalen Port 22 arbeiten, während fuser ,,ssh/tcp
alle Prozesse mit
abgehender Verbindung zu diesem Port anzeigt.
Mit der Option -4
beziehungsweise -6
grenze ich die Ausgabe auf
die entsprechende Version des Internet-Protokolls ein.
Eher selten wende ich die Option --kill
(-k
) an, mit
der fuser
ein Signal (ohne weitere Angaben: SIGKILL
) an die
ermittelten Prozesse sendet.
Das Signal kann ich mit einem vorangestellten
Bindestrich (-
) angeben, eine Liste der Signale bekomme ich mit
--list-signals
(-l
).
Zum Beispiel könnte ich mit
fuser -k -HUP 22/tcp
alle SSH-Anmeldungen an diesem Rechner regulär beenden. War auch ich via SSH angemeldet, habe ich mich damit selbst hinausgeworfen.
Falls ich eine CD-ROM aushängen will, kann ich mit
fuser -k -m /media/cdrom
alle Prozesse, die auf die eingehängte CD-ROM zugreifen, beenden.
Falls unter /media/cdrom kein Dateisystem eingehängt war, werden alle Prozesse,
die das nächsthöhere Dateisystem (meist /) verwenden, beendet.
Das kommt einem unvermittelten Ausschalten des Rechners schon sehr nahe.
Darum gibt es, quasi als
Sicherheitsgurt für solche Fälle, die Option --ismountpoint
(-M
), mit der alle Aktionen nur dann ausgeführt werden, wenn der
angegebene Dateiname ein Mountpoint ist.
Außerdem kann ich mit der Option -w
das Senden des Signals auf Prozesse
einschränken, die eine Datei zum Schreiben geöffnet haben.
Das ist interessant, wenn ich ein Dateisystem
von read-write auf read-only umhängen will.
GDB ist ein mächtiges Werkzeug, das ich in extrem schwierigen Fällen einsetze. Vorzugsweise, wenn ich nachträglich die Ursache eines Programmabsturzes ermitteln will oder wenn ich einen Programmfehler vermute und finden will.
Um mit dem Debugger zu arbeiten benötige ich Zugriff auf die Quellen, aus
denen das Programm übersetzt wurde3.
Außerdem brauche ich die Symboltabellen des Programms damit der Debugger die
Maschinenbefehle des Binärprogramms den Quellcodezeilen zuordnen kann.
Diese gibt es, wenn beim Übersetzen des Programms mit dem
Compiler gcc
die Option -g
angegeben wurde.
Bei vielen Softwarepaketen
kann ich die Symboltabellen über das entsprechende Paket mit der Endung
-dbg
, zum Beispiel avahi-dbg
für avahi
, installieren.
Um einen Prozess postmortal zu analysieren, muss ich das System anweisen, ein
Corefile zu schreiben, das den Zustand des Prozesses beim
Programmabsturz enthält.
Dazu kann ich in der Bash mit dem Befehl ulimit -c $size
die
maximale Größe des Corefiles festlegen, die das Betriebssystem schreibt.
Diese muss ich hoch genug wählen, damit das Corefile groß genug für den
gewünschten Prozess werden kann.
Ich kann den GNU Debugger auf verschiedene Arten starten:
$core
) oder, wenn ich ein Programm nur beim Ablauf verfolgen will
(ohne $core
). Dabei ist $program
der Name der Programmdatei und
$core
der Name des Corefiles.Von den Optionen, die GDB beim Aufruf mitgegeben werden können, sind die
folgenden für die Fehlersuche relevant. Weitere bekomme ich aus der
Handbuchseite, der GDB-Texinfo-Datei oder mit gdb -help
.
-s
und -e
können auch zu -se
zusammengefasst werden, wenn die Symboltabellen noch in der
Binärprogrammdatei enthalten sind.Nachdem ich GDB gestartet habe, steuere ich den Ablauf der Sitzung mit Textbefehlen. Alle diese Befehle kann ich soweit abkürzen, wie sie noch eindeutig sind. Die wichtigsten Befehle für die Fehlersuche sind:
break main
auf, damit der Debugger
an dieser Funktion anhält und ich anschließend das Programm in Ruhe
analysieren kann. Vor einer Funktion kann ich, durch :
getrennt
die Datei angeben, falls der Debugger momentan eine andere geladen hat.$arglist
Kommandozeilenargumente mitgeben.print
lasse ich mir die Werte in den
Variablen und verschiedene andere Daten ausgeben. Dabei kann
$expr
ein komplexer C-Ausdruck sein.c
(continue) läuft das Programm weiter bis zum
nächsten Haltepunkt oder bis zum Programmende.GDB ist ein mächtiges Werkzeug für die Fehlersuche und sehr komplex in der Anwendung. Da die Bedienung nicht einfach ist und es auch einiger Vorkehrungen für den erfolgreichen Einsatz bedarf, setze ich es selten ein, quasi als Ultima Ratio. Trotzdem ist es sinnvoll, sich gelegentlich hinzusetzen und zum Test das eine oder andere Programm im Debugger zu beobachten und zu analysieren, damit es im Ernstfall einfacher von der Hand geht.
Installiere die Debug-Informationen (Paket mit Endung Verfolge anschließend den Ablauf des Programms mit |
Mit hdparm kann ich die Schnittstelle zur Festplatte des Rechners beeinflussen. Ich verwende das Programm zum Feintuning der Festplattenzugriffe, aber auch zum Verifizieren und Beheben von Plattenfehlern. Es arbeitet zusammen mit der Kernelschnittstelle des SATA-, PATA- und des SAS-Subsystems. Auch bei manchen USB-Festplatten kann ich Parameter mit hdparm verändern. Für einige der Optionen benötige ich eine aktuelle Kernelversion.
Allgemein sieht der Aufruf von hdparm wie folgt aus:
hdparm [$optionen] [$geraet ...]
Mit manchen Optionen kann ich Parameter sowohl abfragen als auch setzen (get/set). Bei diesen Optionen gilt, dass sie ohne zusätzliches Argument den Parameter abfragen und mit Argument den Wert entsprechend setzen.
Rufe ich hdparm ganz ohne Parameter auf, verhält es sich,
als hätte ich die Optionen -acdgkmur
angegeben.
Nachfolgend beschreibe ich die, aus meiner Sicht, wichtigsten Optionen. Weitere Informationen gibt es, wie immer, in den Handbuchseiten.
-A0
wird es ausgeschaltet, mit -A1
eingeschaltet.Es geht hierbei um den Transfer via PCI oder VLB zum Hostadapter. Das Kabel zur Festplatte hat immer 16 Bit.
Damit kann ich mir die Fragmentierung einer Datei auf der Platte ansehen, oder die Blöcke für Fehlertests bestimmen.
Wenn ich diese Option verwende, muss sie die einzige sein.
--write-sector
verwenden.--read-sector
, dass ich es
wirklich mit einem defekten Sektor zu tun habe.In /etc/hdparm.conf kann ich die Default-Konfiguration für hdparm hinterlegen. Diese Datei ist gut kommentiert, so dass ich mir weitere Erläuterungen dazu spare.
Ein Werkzeug, dass in meinem Werkzeugkasten für die lokale Fehlersuche nicht
fehlen darf, ist lsof
.
Das Programm zeigt Informationen zu Dateien, die
von gerade laufenden Prozessen geöffnet wurden.
Ich habe dieses Programm erfolgreich beim Untersuchen von Mount-Problemen eingesetzt. Auch beim Aufspüren und Untersuchen von Sicherheitsproblemen leistet es wertvolle Dienste.
Außer für Linux gibt es das Programm auch für andere UNIX-Derivate, bei denen einige Optionen eine andere Bedeutung haben. Aus diesem Grund und weil ich hier nicht alle Optionen erläutern werde, ist ein Blick in die Handbuchseite unumgänglich.
Offene Dateien, die lsof
auflistet, können
In einigen Aspekten überschneidet sich die Funktionialität von lsof
mit der
von netstat
, welches ich an anderer Stelle beschreibe.
Meist entscheide ich je nach vorliegendem Problem, zu welchem
Programm ich greife.
Ich kann von lsof
, statt einer einmaligen Ausgabe, automatisch in
bestimmten Abständen neue Schnappschüsse der angeforderten Informationen
erhalten und die Ausgabe so umformen, dass sie von einem Skript
überwacht werden kann.
Rufe ich lsof ohne Optionen und Argumente auf, bekomme ich eine Liste aller Dateien, die alle laufenden Prozesse im Moment geöffnet haben. Bin ich nur an wenigen Dateien interessiert, gebe ich diese als Argumente auf der Kommandozeile an. Bin ich nur an bestimmten Aspekten oder an Dateien interessiert, die ich nicht genau kenne, so spezifiziere ich das mit Optionen.
Selektiere ich mit einer Option eine definierte Menge von
Dateien, dann zeigt lsof
nur die Dateien, die dieser Selektion genügen.
Gebe ich mehrere Selektoren an, dann werden alle Dateien
angezeigt, die irgendeinem dieser Selektoren entsprechen.
Das heißt, die Menge der angezeigten Dateien entspricht der
ODER-Verknüpfung der einzelnen Selektoren.
Dazu gibt es folgende Ausnahme: wenn ich mit einer Option
bestimmte Dateien deselektiere (zum Beispiel durch vorangestelltes ^
bei Auswahllisten), dann zeigt lsof
diese Dateien auch nicht an, wenn sie
durch einen anderen Selektor ausgewählt werden.
Außerdem kann ich die Verknüpfung
der Selektionskriterien mit der Option -a
von ODER auf UND umstellen.
Gebe ich mehrmals die gleiche Option mit verschiedenen Selektoren an, so
werden diese vor der ODER- beziehungsweise UND-Verknüpfung zu einem Selektor
zusammengefasst.
Wenn ich zum Beispiel nur an allen Internetsockets interessiert bin, die von Prozessen mit UID xyz geöffnet sind, dann schreibe ich:
lsof -a -i -u xyz
Mit Option -c $name
selektiere ich Prozesse, deren Name mit $name
beginnt. Fängt $name
selbst mit ^
an, dann werden genau diese
Prozesse ignoriert. Beginnt und endet $name
mit einem Schrägstrich
(/
), dann interpretiert lsof
diesen als regulärer Ausdruck.
Mit der Option +d $s
bekomme ich alle geöffneten Dateien direkt im
Verzeichnis $s
. Demgegenüber liefert +D $s
auch die Dateien und
Verzeichnisse in den Unterverzeichnissen von $s
, die von Prozessen
geöffnet sind. Beide Optionen kann ich mit -x
kombinieren, damit lsof
symbolischen Links folgt und Mountpoints überquert, was es ansonsten
nicht machen würde.
Die Option -d $s
erwartet eine Liste von Dateideskriptoren (diese
stehen in der Ausgabe in Spalte FD), die ich einschließen, oder mit ^
ausschließen kann. Möchte ich das Arbeitsverzeichnis, aber nicht
Standardeingabe, -ausgabe und -fehlerausgabe von Prozessen wissen, dann
drücke ich das so aus:
lsof -d cwd,^0,^1,^2
Mit der Option -i [$m]
bekomme ich Internetsockets und zwar speziell
für TCP oder UDP angezeigt. Optional kann ich diese mit dem Muster $m
genauer spezifizieren.
Dazu gebe ich m
in der folgenden Form
[46][$protocol][@$hostname|$hostaddr][:$service|$port]
an. Hierbei steht
Demgegenüber kann ich mit -U
UNIX-Domain-Sockets auswählen.
Die Option -n
unterdrückt die Umwandlung von Netzadressen in Namen,
-P
die Umwandlung von Portnummern in Servicenamen und schließlich
-l
die Umwandlung von UID in Benutzernamen. Diese Optionen verwende
ich, wenn ich mehr Klarheit haben will, oder wenn diese Umwandlung
ihrerseits die Ausführung von lsof verzögert, weil DNS- oder NIS-Anfragen
für die Auflösung notwendig sind.
Mit der Option -u $s
lassen sich die Prozesse nach UID oder
Benutzernamen auswählen, während ich mit -p $s
die Prozesse direkt
nach PID auswählen kann.
Starte ich lsof
mit Option -r [$t]
, so liefert es die Informationen
wiederholt in dem mit $t
spezifizierten Zeitabstand (ohne Angabe 15
Sekunden).
Diese Option kann ich mit -F [$f]
kombinieren um die Ausgabe
für die einfachere Verarbeitung in einem Skript zu modifizieren.
Dabei gebe ich mit dem optionalen Argument $f
die Felder, die lsof
ausgibt.
Ltrace ist, ähnlich strace, ein Programm, mit dem ich einem Prozess bei der Arbeit zusehen kann. Im Gegensatz zu strace, welches nur die Kernel-Schnittstelle beobachtet, zeigt ltrace den Aufruf von Bibliotheksfunktionen.
Beim Start eines Programmes via ltrace läßt dieses den Prozess laufen, bis das Programm endet. Dabei fängt ltrace Aufrufe von Bibliotheksfunktionen durch den Prozess und Signale an den Prozess ab und zeigt sie auf STDERR an. Das macht strace auch, da ltrace aber Bibliotheksaufrufe abfängt, ist die Ausgabe viel feiner granuliert und umfangreicher.
Für eine komplette Liste der Optionen verweise ich auf die Handbuchseite. Die folgenden Optionen sind für die Fehlersuche mit ltrace interessant.
SYS_
voran.-S
sinnvoll, da sie die Ausgabe der
Bibliotheksaufrufe unterdrückt.
Mit beiden Optionen zusammen zeigt ltrace etwa das gleiche an wie strace,
der auffälligste Unterschied ist das vorangestellte SYS_
bei ltrace.!
.
So kann ich zum Beispiel mit ltrace -e malloc,free
nachschauen, ob
angeforderter Speicher auch wieder freigegeben wird.
Das interessiert mich insbesondere bei lange laufenden Prozessen.Netstat ist ein Werkzeug, das mir sowohl bei der lokalen, als auch bei der Fehlersuche im Netzwerk hilft. Auf den Aspekt Netzwerkfehlersuche gehe ich im dritten Teil des Buches näher ein. Hier konzentriere ich mich auf die Fehlersuche bei lokalen Problemen.
Dafür verwende ich meist die Optionen --protocol=unix
(alternativ: -A unix
) oder --unix
(-x
) um mir die
UNIX-Sockets ausgeben zu lassen.
Mit --program
(-p
) erhalte ich die PID und den Namen des
Prozesses, der den Socket benutzt.
Mit der PID kann ich dann zum Beispiel den Prozess mit strace
oder
ltrace
näher betrachten.
Für einige Informationen zu Prozessen anderer Benutzer benötige ich die
Privilegien von root oder die Capability CAP_DAC_READ_SEARCH.
Diese kann ich, wie in Kapitel 6 beschrieben,
vergeben.
Normalerweise zeigt netstat
nur aktive, das heißt verbundene Sockets an.
Mit der Option --listening
(-l
) kann ich dagegen die Ausgabe auf Sockets
einschränken, die auf eine Verbindung warten, oder mit --all
(-a
)
alle ausgeben.
Mehr Informationen bekomme ich, wenn ich zusätzlich die Optionen
--verbose
(-v
) oder --extend
(-e
) angebe.
Die Ausgabe von netstat kommt als Tabelle, deren Spalten die folgende Bedeutung haben:
Seit Jahren gibt es in den Prozessoren Performance-Counter, spezielle Zähler, die performance- und verhaltensrelevante Daten ermitteln, mit denen die Laufzeiteigenschaften von Programmen analysiert werden können.
Da die Mechanismen von CPU zu CPU unterschiedlich sind, gab es lange Zeit keine einheitliche Schnittstelle unter Linux, um diese Daten zu nutzen.
Mit perf
gibt es nun ein Werkzeug, das eine einheitliche
Schnittstelle für den Zugang zu diesen Daten bietet.
Dieses Werkzeug besteht aus zwei Teilen: dem Kernelcode für
den Zugriff auf die Daten und dem Programm zur Auswertung.
Da beide Teile eng gekoppelt sind, werden die Quellen des Programms perf
zusammen mit dem Kernelcode geführt.
Baue ich meinen Kernel selbst, brauche ich lediglich im Unterverzeichnis
tools/perf/ den Befehl make
aufrufen, um das für diesen Kernel passende
Programm zu erhalten.
In den verschiedenen Distributionen finde ich perf
in eigenen Paketen, die
von der Kernelversion abhängig sind, bei Debian oder Ubuntu beispielsweise in
linux-tools.
Das Grundprinzip funktioniert so, dass von der CPU verwaltete Zählerregister auf bestimmte Messereignisse programmiert werden können. Bei Erreichen eines Maximalwertes wird ein Interrupt ausgelöst, der zum Aufruf eines Handlers führt, welcher den aktuellen Wert des Befehlszählers in einem Ringpuffer speichert. Das Analyseprogramm wertet den Ringpuffer aus und stellt den Bezug vom Befehlszählerinhalt zu den CPU-Befehlen an dieser Stelle her.
Dadurch ergibt sich ein geringer Overhead beim Beobachten der Programme und diese können auch innerhalb von Funktionen beobachtet werden. Allerdings kann es vorkommen, dass der ermittelte Wert des Befehlszählers auf Grund von Verzögerungen bis zum Aufruf des Handlers nicht exakt ist. Diesen Umstand darf ich bei der Analyse nicht aus den Augen lassen.
Neben Hardwarezählern stellt perf
auch Softwarezähler aus dem Kernel
für die Auswertung zur Verfügung.
Damit kann ich global oder eingeschränkt auf eine Task oder eine
cgroup messen.
Ich kann das Programm perf
auf eine der folgenden Arten verwenden:
$ perf stat [$optionen] $programm
Bei diesem Aufruf startet perf
das Programm $programm
, sammelt
Performancedaten und gibt diese nach Beendigung des Programms aus.
# perf top [$optionen]
So gibt das Programm laufend aktualisierte Daten aus, ähnlich dem Programm
top
für Prozesse.
Dafür benötige ich erweiterte Rechte.
Ich kann das Programm als root aufrufen, oder über
/proc/sys/kernel/perf_event_paranoid die Einstellungen für perf
lockern.
$ perf record [$optionen] $programm
Dieser Aufruf ist ähnlich dem von perf stat
, allerdings werden die
Performancedaten nicht ausgegeben, sondern in der Datei perf.data
gespeichert.
$ perf report [$optionen]
Zeigt die in perf.data gespeicherten Daten an.
$ perf list [$optionen]
Damit zeigt perf
alle bekannten symbolischen Ereignistypen an.
Details finden sich, wie gewöhnlich, in den Handbuchseiten, die für die oben genannten fünf Aufrufe auch via
$ perf help $befehl
angesehen werden können.
Im Perf Wiki gibt es ein englischsprachiges
Tutorial, das den Einsatz
von perf
anhand von Beispielläufen erläutert.
Zwischen den Spezialwerkzeugen für die Fehlersuche und der Shell als Kommandozentrale benötige ich hin und wieder ein Werkzeug, mit dem sich auch knifflige Probleme angehen lassen, die so vorher noch nicht untersucht wurden. Etwas, das sich so schnell wie die Shell programmieren läßt, aber ausdrucksstärker ist und auch sehr komplexe Probleme angehen kann. Für mich ist das Perl. Für andere vielleicht Python oder eine der anderen Skriptsprachen.
Die Programmiersprache Perl umfasst Konzepte von einfachen Werkzeugen wie sed oder awk bis hin zu anspruchsvollen Programmiersprachen wie C oder Lisp. Es gibt umfangreiche Fachliteratur sowohl offline als auch online sowie Communities, an die man sich bei Problemen wenden kann.
Was Perl aber heraushebt gegenüber vielen anderen Skriptsprachen ist CPAN, das Comprehensive Perl Archive Network, ein umfangreiches Reservoir an Softwaremodulen für fast alle erdenklichen Zwecke. Dieses macht es möglich, die meisten Skripts auf wenige Zeilen zu beschränken. Die besten und meist verwendeten Module schaffen es mit der Zeit in die Standarddistribution und stehen dann nach der Installation von Perl gleich zur Verfügung. In vielen Fällen muß ich Perl auch gar nicht zusätzlich installieren, weil es bereits Bestandteil des Systems ist.
Besonders hilfreich ist das Perl Kochbuch von Tom Christiansen und Nathan Torkington [CT2000]. In diesem Buch sind Lösungen für viele Probleme in Rezeptform aufbereitet und vor allen Dingen erläutert. Die Codebeispiele aus dem Kochbuch sind online verfügbar, den meisten Wert ziehe ich jedoch aus den Erläuterungen im Buch.
Es kommt vor, dass ich die Zeilen in den Systemprotokollen miteinander verknüpfen muss, um eine ganz spezielle Auswertung zu bekommen. Finde ich kein geeignetes Programm, beginne ich mit folgendem Fragment, dass ich dann für die Lösung des konkreten Problems ausbaue:
read-syslog.pl
1
#!/usr/bin/perl
2
use
Time::
Local
;
3
my
$wanted
=
'CRON'
;
4
my
$syslogline
=
qr/^
5
(\S{3})\s{1,2} # $1 month name
6
(\d{1,2})\s # $2 day of month
7
(\d\d):(\d\d):(\d\d)\s # $3,$4,$5 hour, min, sec
8
(\S+)\s # $6 host
9
([^[:]+) # $7 process name
10
(?:\[(\d+)\])? # $8 process id
11
:\s
12
(.+) # $9 log line
13
$/
x
;
14
my
(
$sec
,
$min
,
$hour
,
$mday
,
$mon
,
15
$year
,
$wday
,
$yday
,
$dst
)
=
localtime
time
;
16
while
(
<>
)
{
17
if
(
/$syslogline/
)
{
18
my
$time
=
utctime
(
$1
,
$2
,
$3
,
$4
,
$5
);
19
if
(
$7
eq
$wanted
)
{
20
process_line
(
$time
,
$6
,
$7
,
$8
,
$9
);
21
}
22
}
23
}
24
sub
utctime
{
25
my
(
$mon
,
$dom
,
$hour
,
$min
,
$sec
)
=
@_
;
26
my
%mons
=
(
27
Jan
=>
0
,
Feb
=>
1
,
Mar
=>
2
,
Apr
=>
3
,
28
May
=>
4
,
Jun
=>
5
,
Jul
=>
6
,
Aug
=>
7
,
29
Sep
=>
8
,
Oct
=>
9
,
Nov
=>
10
,
Dec
=>
11
,
30
);
31
my
$mod
=
$mons
{
$mon
};
32
return
timegm
(
$sec
,
$min
,
$hour
,
$dom
,
$mod
,
$year
);
33
}
34
sub
process_line
{
35
# print join(" ",@_,"\n");
36
# ... your code here ...
37
}
In Zeile 3 trage ich den Prozessnamen ein, an dessen Logzeilen ich interessiert bin. Sind es mehrere Prozesse, muss ich auch Zeile 19 anpassen.
Die Zeilen 4-13 definieren einen regulären Ausdruck mit dessen Hilfe ich die Protokollzeilen in ihre Bestandteile aufspalte.
Die Zeitfelder fasse ich in der Funktion utctime()
zu einer Zahl
zusammen. Als Jahr nimmt das Skript das aktuelle an, dieses wird in den
Zeilen 14, 15 ermittelt.
In Funktion process_line()
kann ich die gesamte
Auswertelogik für die Protokollzeilen schreiben und dabei auf die bereits
separierten allgemeinen Felder und die eigentliche Nachricht zurückgreifen.
Wenn ich mehrere Protokollzeilen miteinander in Beziehung setzen will,
verwende ich eine globale Datenstruktur und gebe das Ergebnis entweder am
Ende, nach der while (<>)
Schleife oder zwischendurch in der Funktion
process_line()
aus.
Die Shell ist für mich ein wichtiges Hilfsmittel bei der Fehlersuche. Dies in zweierlei Hinsicht: zum einen starte ich in einer interaktiven Shell die Kommandos, mit denen ich den Fehler eingrenzen will, zum anderen verwende ich die Shell für simple, schnell zusammengeschriebene Programme, die mich bei der Fehlersuche unterstützen.
Bei der Arbeit auf der Kommandozeile bevorzuge ich eine Shell mit
History-Funktion und Kommandozeilenergänzung (command line completion).
Die History-Funktion benutze ich, um bereits ausgeführte
Befehle wieder hervorzuholen, gegebenenfalls geringfügig zu ändern und noch
einmal auszuführen. Die Kommandozeilenergänzung beschleunigt den Zusammenbau
von neuen Befehlen, indem die Shell, meist nach Eingabe von <TAB>
,
die Zeile komplettiert oder die nächsten Argumente vorschlägt, wenn mehrere
Vervollständigungen möglich sind.
Das halte ich für unverzichtbar, um den Gedankenflug nicht
abreißen zu lassen.
Ich bin an die Bash gewöhnt, aber andere Shells können das ebensogut.
Für Shell-Scripts bevorzuge ich als kleinsten gemeinsamen Nenner die POSIX
Shell (/bin/sh
).
Auf manchen Systemen ist das nur ein Link auf die Bash.
Das ist kein Problem, weil diese in der Lage ist, POSIX Shell-Skripts
auszuführen.
Da aber nicht jede POSIX-kompatible Shell in der Lage ist, Bash-Erweiterungen
zu verstehen, beschränke ich mich auf den kleinsten gemeinsamen Nenner,
um auf der sicheren Seite zu sein, wenn das Skript in einer anderen
Umgebung läuft.
Ich will hier keine komplette Einführung in die Programmierung mit der POSIX Shell geben, sondern verweise stattdessen auf die Handbuchseiten und die Beschreibung im Standard [POSIX.1-2008]. Stattdessen stelle ich ein Skript vor, das ich bereits mehrfach für die Fehlersuche verwendet habe
Bei einem schwierigen Problem greife ich oft auf
strace zurück,
um den Prozess bei der Arbeit zu beobachten.
Wenn dieser Prozess jedoch nicht von Kommandozeile,
sondern von einem anderen Prozess gestartet wird, verwende ich folgenden
Trick: Ich benenne das Programm um, indem ich an den Namen die Endung
.orig
anhänge. Unter dem ursprünglichen Programmnamen platziere ich
einen Link auf dieses Skript:
strace-invocator
1
#!/bin/sh
2
progname
=
$(
basename $0
)
3
origin
=
$0
.orig
4
die ()
{
5
ev
=
$1
6
shift
7
echo
$*
1>&
2
8
exit
$ev
9
}
10
[
-x $origin
]
||
die 2 "Can't execute $origin"
11
tmpdir
=
$(
mktemp -d /tmp/$progname
.XXXXX)
12
date > $tmpdir
/invocation
13
echo
"#----- args -----"
>> $tmpdir
/invocation
14
for
arg in "$@"
;
do
15
echo
$arg
>> $tmpdir
/invocation;
16
done
17
echo
"#----- id -----"
>> $tmpdir
/invocation
18
id >> $tmpdir
/invocation
19
echo
"#----- env -----"
>> $tmpdir
/invocation
20
env >> $tmpdir
/invocation
21
strace -f -o $tmpdir
/strace.out $origin
"$@"
In Zeile 2 bestimme ich den Namen des aufgerufenen Programms und in Zeile 3 den Namen des eigentlichen Programms.
In Zeile 4 bis 9 definiere ich eine Funktion, mit der ich das Skript mit Fehlermeldung und -code beenden kann.
In Zeile 10 teste ich, ob das Originalprogramm da und ausführbar ist und breche andernfalls ab. Dazu nutze ich die oben definierte Funktion.
In Zeile 11 lege ich ein temporäres Verzeichnis mit eindeutigem Namen an. Damit kann ich mich bei der Auswertung in aller Ruhe auf jeden einzelnen Aufruf des Programms konzentrieren.
In den Zeilen 12 bis 20 halte ich verschiedene Informationen zum Aufruf fest. Und zwar die aktuelle Zeit, die übergebenen Argumente, die Benutzer-Id, unter der der Prozess läuft und die Umgebungsvariablen.
In Zeile 21 schließlich rufe ich via strace das Originalprogramm mit allen Argumenten auf. Diesen Aufruf kann ich noch modifizieren, wenn ich an der Standardeingabe für das Programm interessiert bin:
tee $tmpdir
/stdin \
|
strace -f -o $tmpdir
/strace.out $origin
"$@"
Oder, wenn ich sowohl an der Standardeingabe als auch an der Standardausgabe interessiert bin:
tee $tmpdir
/stdin \
|
strace -f -o $tmpdir
/strace.out $origin
"$@"
\
|
tee $tmpdir
/stdout
Manchmal habe ich ein Problem in einem Shell-Skript selbst.
Dann hilft es, die Shell mit der Option -x
zu starten,
um zu sehen, wie sie das Skript abarbeitet.
Als Beispiel nehme ich ein Skript, das mir ausgibt, in welchem Verzeichnis ich mich gerade befinde und zu welchem Projekt dieses gehört:
1
#!/bin/sh
2
HERE
=
$PWD
3
echo
"($HERE)"
4
while
test
"/"
!=
"$HERE"
;
do
5
if
[
-f "$HERE/.project"
]
;
then
6
echo
"==="
$(
cat "$HERE/.project"
)
"==="
7
break
8
fi
9
HERE
=
$(
dirname $HERE
)
10
done
Die Ausgabe des Skripts sieht beispielsweise so aus:
$
where
(
/home/mathias/A/2012/12/07/t1)
===
GDB Tests
===
Mit Shell-Debugging dagegen so:
$
sh -x ~/bin/where
+ PROJECT
=
.project
+ HERE
=
/home/mathias/A/2012/12/07/t1
+ echo
(
/home/mathias/A/2012/12/07/t1)
(
/home/mathias/A/2012/12/07/t1)
+ test
/ !=
/home/mathias/A/2012/12/07/t1
+ [
-f /home/mathias/A/2012/12/07/t1/.project ]
+ dirname /home/mathias/A/2012/12/07/t1
+ HERE
=
/home/mathias/A/2012/12/07
+ test
/ !=
/home/mathias/A/2012/12/07
+ [
-f /home/mathias/A/2012/12/07/.project ]
+ cat /home/mathias/A/2012/12/07/.project
+ echo
===
GDB Tests
===
===
GDB Tests
===
+ break
Damit sehe ich genau, was beim Abarbeiten des Skripts passiert.
Alle Variablenzuweisungen mit den zugehörigen Werten sowie die Befehlsaufrufe
mit ihren Argumenten stehen auf je einer Zeile, die mit '+ '
eingeleitet wird.
Strace setze ich ein, wenn mir das Verhalten eines Programmes unklar ist. Wenn ich Probleme mit Zugriffsrechten vermute, aber keinen Anhaltspunkt in den Fehlermeldungen oder Systemprotokollen finde. Wenn ich mit einem Programm noch wenig Erfahrung habe, keine Hilfe im Internet finde, aber das Problem trotzdem so schnell wie möglich beheben will. In [Koenig2012] führt Harald König sehr gut in die Arbeit mit strace ein.
Strace hilft mir, wenn ich beobachten will, wie ein Programm mit seiner Umgebung interagiert. Es setzt an der Kernelschnittstelle an und protokolliert alle Systemaufrufe mit den Parametern und Ergebnissen. Diese stellt es im Protokoll ähnlich den Systemaufrufen in der Programmiersprache C dar, mit folgendem Unterschied: das Ergebnis steht hinter einem Gleichheitszeichen am Ende der Zeile, die Systemzeit steht vor dem Systemaufruf. Wenn ich mehrere Prozesse beobachte und in dieselbe Datei protokolliere, steht zusätzlich die Prozess-ID am Anfang der Zeile.
Um die Ausgabe von strace interpretieren zu können, ist es hilfreich, die Sektion 2 der Handbuchseiten installiert zu haben. Diese befinden sich bei Debian-basierten Systemen im Paket manpages-dev.
Da strace Systemaufrufe der beobachteten Prozesse protokolliert, verlangsamt sich deren Ablauf, was zu zusätzlichen Problemen bei der Fehlersuche führen kann. Dessen muss ich mir beim Einsatz von strace immer bewusst sein.
Ich setze strace bei der Fehlersuche meist auf eine der folgenden Weisen ein.
Bei Programmen, die ich von der Kommandozeile aus aufrufe,
starte ich das betreffende Programm wie gewohnt, allerdings setze ich an den
Anfang strace
mit einigen Optionen.
So wird aus
$
make xyz
dann
$
strace -f -o make.strace make xyz
Dabei bedeuten die Optionen
Bei Problemen mit bereits gestarteten Prozessen verwende ich die Option
-p $PID
um mich mit dem Prozess mit dieser PID zu verbinden.
Alle weiteren Optionen bleiben wie gehabt, auf die Angabe des Programmnamens
und der Programmparameter kann ich verzichten.
Prozesse, die bereits vorher von dem untersuchten Prozess gestartet wurden,
beobachtet strace
nicht, wohl aber neu gestartete Prozesse, wenn ich
die Option -f
angebe.
Schwieriger ist es, wenn ein Programm ein zweites aufruft, dieses ein drittes und so weiter. Wenn ich nur an einem der Programme interessiert bin und nicht genau weiß, von welchem Prozess beziehungsweise Programm dieses gestartet wird, helfe ich mir mit einem Trick. Ich benenne das Programm um und ersetze es durch ein Skript, welches alle mich interessierenden Werte protokolliert und schließlich das Originalprogramm via strace aufruft. Dieses Skript ist im Abschnitt zur Shell näher beschrieben. Natürlich darf ich am Ende nicht vergessen, das Skript wieder durch das Originalprogramm zu ersetzen.
Mache Dich mit strace -o ls.strace ls -l
Studiere die einzelnen Systemaufrufe mit Hilfe der Handbuchseiten und versuche zu verstehen, was beim Ablauf des Programmes passiert. |
Das Paket sysstat enthält Accounting-Programme, mit denen ich den Ressourcenbedarf des Systems erfassen kann.
Üblicherweise startet cron
aller 10 Minuten ein Skript,
das die Systemstatistiken sichert.
Diese Statistiken kann ich mit verschiedenen Auswertewerkzeugen auslesen.
Alternativ können diese Werkzeuge selbst periodisch die Performancedaten sammeln und als aktuelle Schnappschüsse ausgeben.
Zur Auswertung mit sysstat stehen die folgenden Werkzeuge zur Verfügung:
Die Auswertung der Statistikdaten mit sar ist in [Loukides1996] beschrieben. Da sich die Software seitdem weiterentwickelt hat, ist ein Blick in die Handbuchseiten unerlässlich.
Für die grafische Auswertung gibt es verschiedene Programme. Ein Programm, sargraph, wird als Beispiel mit sysstat verteilt. Dieses wertet die XML-Ausgabe von sadf aus und übergibt sie an gnuplot zur Darstellung.
Das Programm vmstat
verwende ich, um bei lokalen Performanceengpässen einen
Überblick über das Gesamtsystem zu bekommen. Es liefert mir statistische
Informationen über Prozesse, Speicher, I/O, Platten- und CPU-Aktivitäten.
Durch die kompakte Darstellung kann ich im Wiederholungsmodus ein Gefühl
für die Aktivität des Gesamtsystems bekommen.
Wenn ein Performanceengpass auftritt, sehe ich sofort, ob es ein allgemeines
Ressourcenproblem ist - zuwenig Speicher, zu schwache CPU, zu langsame
Platte - oder ob ich mich eher mit dem betreffenden Programm
beschäftigen muss.
Für die meisten Optionen sind keine speziellen Privilegien erforderlich.
Ich kann mit vmstat
als normaler Benutzer nachsehen,
wie es dem System geht.
Im Wiederholungsmodus zeigt das Programm in der ersten Zeile für alle Werte den Durchschnitt seit Systemstart an und in den folgenden Zeilen die Werte für die betreffende Abtastperiode. Diesen Modus verwende ich am häufigsten:
vmstat [-a] [-n] [$periode [$anzahl]]
Lasse ich die beiden Zahlen für $periode
und $anzahl
weg, dann
bekomme ich nur die Durchschnittswerte seit Systemstart angezeigt. Das
brauche ich eigentlich nur, wenn ich das System sehr gut kenne oder mehrere
ähnliche Systeme vergleichen will.
Gebe ich eine Zahl für $periode
an, dann verwendet vmstat
diese als Länge
der Periode in Sekunden mit der es kontinuierlich die Werte der letzten Periode
ausgibt. Auch hier steht in der ersten Zeile der Durchschnitt seit
Systemstart, so dass ich gleich ab der zweiten Zeile sehen kann, wie das
System im Moment beschäftigt ist.
Gebe ich eine Zahl für $anzahl
an, beendet sich vmstat
nach
soviel Perioden, wie angegeben.
Die Ausgabe von vmstat
in diesem Modus bedeutet folgendes:
r
steht die Anzahl der Prozesse, die laufen
können und unter b
die Anzahl der Prozesse in
uninteruptible sleep, das sind Prozesse, die im Kernelcode auf I/O
warten.swpd
die Menge des verwendeten
virtuellen Speichers, also des ausgelagerten Hauptspeichers.
Unter free
finde ich den unbenutzten Speicher, unter buff
Speicher, den der Kernel für Datei- und Socketpuffer verwendet.
Den unter cache
aufgeführte Speicher verwendet er für zwischengespeicherte
Daten von Blockdevices.
Habe ich die Option -a
angegeben, zeigt vmstat
statt buffer/cache
aktiven und inaktiven Speicher an.
bi
finde ich die Anzahl der von
Blockdevices gelesenen Blöcke (blocks in) und unter bo
die
geschriebenen (blocks out).in
die Anzahl der Interrupts pro
Sekunde und unter cs
die Anzahl der Kontextwechsel pro Sekunde.us
steht der Anteil, den die CPU mit Code in
Benutzerprogrammen verbringt. Unter sy
steht die Zeit für das
Abarbeiten von Systemaufrufen und unter id
die untätige (idle)
Zeit. Bis zum Kernel 2.5.41 zählte die Zeit, in der die CPU auf I/O
wartete hierzu, ab dann gab es dafür die Spalte wa
.Im Wiederholungsmodus kann ich mit Option -a
die Anzeige des Speichers
von buffer/cache auf inactive/active umschalten und mit -n
die
wiederholte Anzeige der Kopfzeilen abschalten. Letztere sind insbesondere
bei länger laufender Ausgabe praktisch, wenn ich nicht genau im Kopf habe,
welche Spalte was anzeigt. Daher verwende ich -n
so gut wie nie.
Mit der Option -m
, die Superuserprivilegien erfordert, zeigt vmstat
Informationen zum Slab Allocator an. Das ist ein Verfahren zur Verwaltung
des Arbeitsspeichers. Weitere Informationen dazu finden sich in
[Bonwick1994].
Mit Option -d
zeigt vmstat
einmalig Diskstatistiken für alle Disk
Devices an, das können auch RAM-Disks und Loop Devices sein.
Dabei bedeuten:
cur
die gerade laufende I/O und unter s
die
Sekunden, die das System mit I/O verbracht hat.Mit Option -D
zeigt vmstat
einmalig zusammengefasste Statistiken zu
allen Disks.
Und mit -p $partition
schließlich zeigt vmstat
einmalig Statistiken für
diese Partition. Hierbei bedeuten in der Ausgabe: