146 11 10MB
German Pages 507 [697] Year 2021
Steffen Wendzel, Johannes Plötner
Linux Der Grundkurs
Impressum Dieses E-Book ist ein Verlagsprodukt, an dem viele mitgewirkt haben, insbesondere: Lektorat Christoph Meister
Korrektorat Isolde Kommer, Großerlach
Covergestaltung Julia Schuster
Herstellung E-Book Norbert Englert
Satz E-Book Steffen Wendzel, Johannes Plötner
Bibliografische Information der Deutschen Nationalbibliothek:
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.dnb.de abrufbar. ISBN 978-3-8362-8545-2 1. Auflage 2022
© Rheinwerk Verlag GmbH, Bonn 2022
Liebe Leserin, lieber Leser, noch vor einigen Jahren war die Frage, welches Betriebssystem man einsetzt, eine Grundsatzentscheidung, über die oft mit großer Leidenschaft gestritten wurde. Besonders berühmt ist die Aussage des ehemaligen Microsoft-Chefs Steve Ballmer, der Linux 2001 als »Krebsgeschwür« bezeichnete. Zum Glück sind diese Zeiten aber lange vorbei. Inzwischen setzt auch Microsoft auf Linux-Dienste und hat die PowerShell und Visual Studio Code portiert. Die meisten Azure-Systeme nutzen Linux und mit dem Windows Subsystem for Linux hat der Kernel einen festen Platz in der Windows-Welt erobert. Wissen über das freie Betriebssystem ist also wertvoller denn je. Sei es wegen der weiten Verbreitung des Kernels, wegen den vielen cleveren Tricks, die die Shell draufhat, oder wegen den nützlichen Netzwerk-Tools auf der Konsole: In jedem Bereich der IT werden Sie von Linux-Know-how profitieren. Wenn Sie wissen, wie Systeme sicher über SSH administriert werden oder wenn Sie Konfigurationsdateien elegant mit wenigen Kommandos des Vim ändern können, haben Sie einen Vorsprung vor Kollegen, die nicht ohne Maus und Fenster arbeiten können. Dazu erklären Ihnen Steffen Wendzel und Johannes Plötner in diesem Grundkurs, wie Linux funktioniert. Aufbauend auf solider Betriebssystemtheorie zeigen sie Ihnen, was die Arbeit mit der Bash, Vim und anderen Werkzeugen ausmacht. Abschließend noch ein Wort in eigener Sache: Dieses Werk wurde mit großer Sorgfalt geschrieben, geprüft und produziert. Sollte dennoch etwas nicht wie erwartet funktionieren, freue ich mich,
wenn Sie sich mit mir in Verbindung setzen. Ihre Kritik und konstruktiven Anregungen sind jederzeit willkommen. Ihr Christoph Meister
Lektorat Rheinwerk Computing [email protected]
www.rheinwerk-verlag.de
Rheinwerk Verlag • Rheinwerkallee 4 • 53227 Bonn
Inhaltsverzeichnis Aus dem Lektorat Inhaltsverzeichnis
Vorwort
1 Einleitung 1.1 Warum Linux? 1.1.1 Man muss kein Informatiker sein ... 1.1.2 ... aber es hilft
1.2 Grundbegriffe: Kernel, Distribution, Derivat 1.2.1 Bekannte Distributionen und Derivate 1.2.2 Arten von Distributionen
1.3 Die Entstehungsgeschichte von Linux 1.3.1 Die Entstehung von Unix 1.3.2 BSD wird ins Leben gerufen 1.3.3 Die Geburtsstunde von Linux 1.3.4 Die Kernelversionen 1.3.5 Stallman und das GNU-Projekt 1.3.6 Geschichte der Distributionen
1.4 Zusammenfassung 1.5 Aufgaben
2 So funktioniert Linux 2.1 Grundlagen 2.1.1 Prozessor 2.1.2 Speicher 2.1.3 Fairness und Schutz 2.1.4 Programmierung 2.1.5 Benutzung
2.2 Aufgaben eines Betriebssystems 2.2.1 Abstraktion 2.2.2 Virtualisierung 2.2.3 Ressourcenverwaltung
2.3 Prozesse, Tasks und Threads 2.3.1 Definitionen 2.3.2 Lebenszyklen eines Prozesses 2.3.3 Implementierung
2.4 Speichermanagement 2.4.1 Paging 2.4.2 Hardware 2.4.3 Organisation des Adressraums
2.5 Eingabe und Ausgabe 2.5.1 Hardware und Treiber 2.5.2 Interaktion mit Geräten 2.5.3 Ein-/Ausgabe für Benutzerprogramme 2.5.4 Dateisysteme
2.6 Zusammenfassung 2.7 Aufgaben
3 Erste Schritte 3.1 Die Unix-Philosophie 3.1.1 Kleine, spezialisierte Programme 3.1.2 Wenn du nichts zu sagen hast: Halt die Klappe 3.1.3 Die Shell 3.1.4 Administration 3.1.5 Netzwerktransparenz
3.2 Der erste Kontakt mit dem System 3.2.1 Booten 3.2.2 Login 3.2.3 Arbeiten am System 3.2.4 Die Linux-Verzeichnisstruktur 3.2.5 Das Rechtesystem 3.2.6 Herunterfahren 3.2.7 Wie Laufwerke bezeichnet werden
3.3 Bewegen in der Shell 3.3.1 Der Prompt 3.3.2 Absolute und relative Pfade 3.3.3 pwd 3.3.4 cd
3.4 Arbeiten mit Dateien 3.4.1 ls 3.4.2 more, less und most 3.4.3 Und Dateitypen?
3.5 Der Systemstatus 3.5.1 uname 3.5.2 uptime 3.5.3 date
3.6 Hilfe 3.6.1 Manpages 3.6.2 GNU info 3.6.3 Programmdokumentation
3.7 Zusammenfassung 3.8 Aufgaben
4 Grundlagen der Shell 4.1 Einführung und Überblick 4.1.1 Welche Shells gibt es?sh 4.1.2 Welche Shell für dieses Buch? 4.1.3 Die Shell als Programm 4.1.4 Der Prompt 4.1.5 Shellintern vs. Programm 4.1.6 Kommandos aneinanderreihen 4.1.7 Mehrzeilige Kommandos
4.2 Konsolen 4.3 screen 4.4 Besseres Arbeiten mit Verzeichnissen 4.4.1 Pfade 4.4.2 Und das Ganze mit Pfaden ...
4.5 Die elementaren Programme 4.5.1 echo und Kommandosubstitution 4.5.2 sleep 4.5.3 Erstellen eines Alias 4.5.4 cat
4.6 Programme für das Dateisystem 4.6.1 mkdir – Erstellen eines Verzeichnisses 4.6.2 rmdir – Löschen von Verzeichnissen 4.6.3 cp – Kopieren von Dateien 4.6.4 mv – Verschieben einer Datei 4.6.5 rm – Löschen von Dateien 4.6.6 head und tail
4.7 Ein- und Ausgabeumlenkung 4.7.1 Fehlerausgabe und Verknüpfung von Ausgaben 4.7.2 Anhängen von Ausgaben 4.7.3 Gruppierung der Umlenkung
4.8 Pipes 4.8.1 Beispiel: sort und uniq verbinden 4.8.2 Beispiel: Zeichen vertauschen 4.8.3 Um- und Weiterleiten mit tee 4.8.4 Named Pipes (FIFOs)
4.9 xargs 4.10 Zusammenfassung 4.11 Aufgaben
5 Prozesse in der Shell 5.1 Sessions und Prozessgruppen 5.2 Vorder- und Hintergrundprozesse 5.2.1 Prozessgruppen mit mehreren Prozessen 5.2.2 Wechseln zwischen Vorder- und Hintergrund 5.2.3 Jobs – behalten Sie sie im Auge 5.2.4 Hintergrundprozesse und Fehlermeldungen
5.2.5 Wann ist es denn endlich vorbei?
5.3 Das kill-Kommando und Signale 5.3.1 Welche Signale gibt es? 5.3.2 Beispiel: Anhalten und Fortsetzen eines Prozesses
5.4 Prozessadministration 5.4.1 Prozesspriorität 5.4.2 pstree 5.4.3 Prozesslistung mit Details via ps 5.4.4 top 5.4.5 Timing für Prozesse
5.5 Zusammenfassung 5.6 Aufgaben
6 Reguläre Ausdrücke 6.1 Grundlagen und Aufbau regulärer Ausdrücke 6.2 grep 6.2.1 Aufrufparameter für grep 6.2.2 grep -E und egrep 6.2.3 Exkurs: PDF-Files mit grep durchsuchen
6.3 awk 6.3.1 awk starten 6.3.2 Arbeitsweise von awk 6.3.3 Reguläre Ausdrücke in awk anwenden 6.3.4 Einfache Strings 6.3.5 Der Punkt-Operator 6.3.6 Der Plus-Operator 6.3.7 Die Zeichenvorgabe
6.3.8 Negierte Zeichenvorgabe 6.3.9 Zeilenanfang und -ende 6.3.10 awk – etwas detaillierter 6.3.11 Zusätzliche Parameter beim Aufruf 6.3.12 awk und Variablen 6.3.13 Rechenoperationen 6.3.14 Bedingte Anweisungen in awk 6.3.15 Funktionen in awk 6.3.16 Builtin-Funktionen 6.3.17 Arrays und String-Operationen 6.3.18 Was noch fehlt
6.4 sed 6.4.1 Erste Schritte 6.4.2 sed-Befehle 6.4.3 Nach Zeilen filtern 6.4.4 Wiederholungen in regulären Ausdrücken
6.5 Zusammenfassung 6.6 Aufgaben
7 Werkzeuge für die Konsole 7.1 touch – Zeitstempel von Dateien setzen 7.2 cut – Dateiinhalte abschneiden 7.3 paste – Dateien zusammenfügen 7.4 tac – Dateiinhalt umdrehen 7.5 column – Ausgaben tabellenartig formatieren 7.6 colrm – Spalten entfernen
7.7 nl – Zeilennummern für Dateien 7.8 wc – Zählen von Zeichen, Zeilen und Wörtern 7.9 od – Dateien zur Zahlenbasis x ausgeben 7.10 split – Dateien aufspalten 7.11 script – Terminal-Sessions aufzeichnen 7.12 bc – der Rechner für die Konsole 7.13 Der Midnight Commander 7.13.1 Bedienung 7.13.2 Verschiedene Ansichten
7.14 Zusammenfassung 7.15 Aufgaben
8 Eigene Shellskripte entwickeln 8.1 Grundlagen der Shellskript-Programmierung 8.1.1 Was genau ist ein Shellskript? 8.1.2 Wie legen Sie los? 8.1.3 Das erste Shellskript 8.1.4 Kommentare
8.2 Variablen 8.2.1 Rechnen mit Variablen 8.2.2 Benutzereingaben für Variablen
8.3 Arrays 8.3.1 Neue Elemente hinzufügen 8.3.2 Array-Länge
8.4 Kommandosubstitution und Schreibweisen 8.5 Argumentübergabe 8.6 Funktionen 8.6.1 Eine simple Funktion 8.6.2 Funktionsparameter 8.6.3 Rückgabewerte
8.7 Bedingte Anweisungen 8.7.1 Vergleichen von Zahlen 8.7.2 Returncodes für Bedingungen nutzen 8.7.3 Case-Bedingungen
8.8 Schleifen 8.8.1 Die while-Schleife 8.8.2 Die for-Schleife 8.8.3 seq – Schleifen mit Aufzählungen 8.8.4 until 8.8.5 break – Schleifen abbrechen
8.9 Menüs bilden mit select 8.10 Temporäre Dateien 8.11 Syslog-Meldungen via Shell 8.12 Pausen in Shellskripte einbauen 8.13 Startskripte 8.14 Das Auge isst mit: der Schreibstil 8.15 Ein paar Tipps zum Schluss 8.16 Weitere Fähigkeiten der Shell
8.17 Zusammenfassung 8.18 Aufgaben
9 Der vi(m)-Editor 9.1 vi, vim, gvim und neovim 9.2 Erste Schritte 9.3 Kommando- und Eingabemodus 9.4 Dateien speichern 9.5 Arbeiten mit dem Eingabemodus 9.6 Navigation 9.7 Löschen von Textstellen 9.8 Textbereiche ersetzen 9.9 Kopieren von Textbereichen 9.10 Shiften 9.11 Die Suchfunktion 9.12 Konfiguration 9.13 Zusammenfassung 9.14 Aufgaben
10 Grundlegende Administration 10.1 Benutzerverwaltung
10.1.1 Das Verwalten der Benutzerkonten 10.1.2 Benutzer und Gruppen
10.2 Installation neuer Software 10.2.1 Welches Paketsystem nutzen? 10.2.2 Das DEB-Paketsystem 10.2.3 Das RPM-Paketsystem 10.2.4 Softwareinstallation ohne Pakete
10.3 Backups erstellen 10.3.1 Die Sinnfrage 10.3.2 Backup eines ganzen Datenträgers 10.3.3 Backup ausgewählter Daten
10.4 Logdateien und dmesg 10.4.1 /var/log/messages 10.4.2 /var/log/wtmp 10.4.3 /var/log/Xorg.log 10.4.4 syslogd 10.4.5 logrotate und DoS-Angriffe 10.4.6 tail und head
10.5 Weitere nützliche Programme 10.5.1 Speicherverwaltung 10.5.2 Festplatten analysieren 10.5.3 Wer ist eingeloggt? 10.5.4 Offene Dateideskriptoren mit lsof
10.6 Grundlegende Systemdienste 10.6.1 cron 10.6.2 at
10.7 Manpages 10.8 Dateien finden mit find
10.8.1 Festlegung eines Auswahlkriteriums 10.8.2 Festlegung einer Aktion
10.9 Zusammenfassung 10.10 Aufgaben
11 Netzwerke unter Linux 11.1 Etwas Theorie 11.1.1 TCP/IP 11.1.2 Ihr Heimnetzwerk
11.2 Konfiguration einer Netzwerkschnittstelle 11.2.1 Welche Netzwerkschnittstellen gibt es? 11.2.2 Konfiguration von Netzwerkkarten mit ip und ifconfig 11.2.3 Automatische Konfiguration: DHCP
11.3 Routing 11.3.1 Was ist Routing? 11.3.2 Der Befehl ip route
11.4 Netzwerke benutzerfreundlich – DNS 11.4.1 DNS 11.4.2 DNS und Linux 11.4.3 Windows und die Namensauflösung 11.4.4 Die Datei /etc/services
11.5 Firewalls unter Linux 11.5.1 ufw 11.5.2 firewalld
11.6 Secure Shell 11.6.1 Das SSH-Protokoll
11.6.2 Secure Shell nutzen 11.6.3 Der Secure-Shell-Server
11.7 Das World Wide Web 11.7.1 Das HTTP-Protokoll 11.7.2 Einrichten eines Apache-Webservers 11.7.3 Den Apache verwalten
11.8 Windows-Netzwerkfreigaben 11.8.1 Die smb.conf-Konfigurationsdatei 11.8.2 Benutzersetup mit smbpasswd 11.8.3 Das Share verbinden
11.9 Dateien tauschen mit klassischem FTP 11.9.1 Das FTP-Protokoll 11.9.2 ftp
11.10 Weitere nützliche Netzwerktools 11.10.1 ping 11.10.2 netstat 11.10.3 nmap 11.10.4 tcpdump
11.11 Zusammenfassung 11.12 Aufgaben
12 Softwareentwicklung 12.1 Interpreter und Compiler 12.1.1 C und C++ 12.1.2 Python 12.1.3 Java 12.1.4 Was es sonst noch gibt
12.2 Shared Libraries 12.2.1 Vorteile der Shared Libraries 12.2.2 Statisches Linken 12.2.3 Dateien
12.3 Debugging 12.3.1 Vorbereitung 12.3.2 Konsolenarbeit
12.4 Make 12.4.1 Makefile 12.4.2 Makros 12.4.3 Shellvariablen in Makefiles 12.4.4 Einzelne Targets übersetzen 12.4.5 Spezielle Targets 12.4.6 Tipps im Umgang mit Make
12.5 Die GNU-Autotools 12.6 Unix-Software veröffentlichen 12.7 Manpages erstellen 12.7.1 groff nutzen 12.7.2 Manpages installieren
12.8 Versionsmanagement mit Git 12.8.1 Eine kurze Einführung
12.9 Docker-Container erstellen 12.9.1 Virtualisierung, Container und andere Betriebssysteme 12.9.2 Docker-Images bauen 12.9.3 Docker-Container starten und verwalten 12.9.4 Mit Containern interagieren
12.10 Zusammenfassung
12.11 Aufgaben
13 Umgang mit dem Raspberry Pi 13.1 Die Hardware 13.1.1 Schnittstellen 13.1.2 Zubehör
13.2 Die Inbetriebnahme 13.2.1 Linux-Distributionen für den Raspberry Pi 13.2.2 SD-Karte mit einem Image bespielen
13.3 Der Raspberry Pi als Homeserver 13.3.1 Die initiale Konfiguration mit raspi-config 13.3.2 Die Grundlagen 13.3.3 Die weitere Konfiguration
13.4 Der Raspberry Pi als Mediacenter 13.4.1 Kodi konfigurieren 13.4.2 Freigaben einbinden – Filme, Serien und Musik 13.4.3 Add-ons konfigurieren
13.5 Zusammenfassung 13.6 Aufgaben
A Die Installation planen und durchführen A.1 Die Anforderungen an Ihre Hardware A.2 Hardwareunterstützung A.2.1 Hardwarekompatibilitätslisten der Hersteller A.2.2 Grafikkarten A.2.3 Andere Geräte
A.3 Festplatten und Partitionen A.3.1 Funktionsweise unter Linux A.3.2 Für Ambitionierte: die Partitionierung von Hand durchführen A.3.3 Das Tool cfdisk A.3.4 Vorinstallierte Systeme A.3.5 Windows und Linux A.3.6 Erstellen eines Backups
A.4 Die Installation durchführen A.4.1 Vorbereitung: Linux herunterladen A.4.2 Der klassische Weg: Installation auf echter Hardware A.4.3 Der einfache Weg: Installation in einer virtuellen Maschine
A.5 Eine typische Linux-Installation durchführen A.5.1 Partitionierung der Festplatte A.5.2 Zeitzone festlegen A.5.3 Anlegen eines Benutzers A.5.4 Systeminstallation A.5.5 Fertigstellung
A.6 Zusammenfassung
Stichwortverzeichnis Rechtliche Hinweise Über die Autoren
Vorwort Linux bietet Ihnen viele kleine Zahnräder, an denen Sie drehen können. Einzeln betrachtet erscheinen diese auf den ersten Blick unübersichtlich oder kompliziert. Wenn Sie aber einmal das Potenzial ihres Zusammenspiels erkannt haben, wird Ihnen klar, wie mächtig diese Werkzeuge doch sind. Was auch immer Sie mit Ihrem System vorhaben: Linux bietet Einsteigerinnen und Einsteigern wie Profis alle Möglichkeiten. Wir möchten Ihnen in diesem Buch das Grundwissen samt wichtigen Vorgehensweisen zur Problemlösung im Umgang mit Linux vermitteln. Dieser Grundkurs basiert auf den Artikeln, Vorlesungen, Workshops und Büchern, die wir bisher zum Thema verfasst haben. Seitdem die 8. Auflage unseres Einstieg in Linux erschienen ist, hat sich in der Linux-Welt wieder eine ganze Menge getan. In diesem neuen Format haben wir die Inhalte daher wieder komplett überarbeitet und aktualisiert. Insbesondere haben wir die Kapitelstruktur so geändert, dass wir zusätzlich in die Tiefe des Linux-Systems gehen und einige anspruchsvollere Themen einführen können. Eingefügt haben wir zudem Aufgabenstellungen am Ende jedes Kapitels. Selektierte Lösungen zu diesen Aufgaben finden Sie auf unserer Webseite unter https://linuxbuch.blogspot.de. Unter http://linuxbuch.blogspot.de sowie https://www.rheinwerkverlag.de/5368 finden Sie weitere Informationen, wie etwa Rezensionen und etwaige Korrekturen zu diesem Buch und zu unseren weiteren Büchern.
Unser Dank gilt in erster Linie den Lektorinnen und Lektoren sowie allen anderen Personen, die an Satz, Sprachkorrektur, Herstellung oder Überarbeitung der Grafiken in den letzten Jahren beteiligt waren: Christoph Meister, Anne Scheibe, Isolde Kommer und Norbert Englert. Nicht zuletzt wird dieses Buch auch durch Hinweise seiner Leserinnen und Leser immer weiter verbessert. Von diesen seien Stefanie Holtzhäuser und Karl-Martin Weimer ganz besonders gedankt, die einige Fehler aufdeckten, die sich über mehrere Auflagen hinweg im Buch befanden. Steffen Wendzel und Johannes Plötner Über die Autoren
Steffen Wendzel beschäftigt sich seit seinen Tagen als Schüler mit Linux, Netzwerken und IT-Sicherheit. Nach seinem Studium der Informatik an den Hochschulen Kempten und Augsburg promovierte er 2013 an der FernUniversität in Hagen über verdeckte Kanäle in Netzwerken. 2020 habilitierte er sich schließlich an selbiger Universität (Fach Informatik). Nach seiner Promotion leitete er für mehrere Jahre ein Forschungsteam im Bereich SmartHomes/Buildings am Fraunhofer-Institut für Kommunikation, Informationsverarbeitung und Ergonomie (FKIE) in Bonn. Seit 2016 ist er Professor für Netzwerke und IT-Sicherheit an der Hochschule Worms. Er lehrt dort in den Themenfeldern Betriebssysteme, Netzwerke und IT-Sicherheit und ist wissenschaftlicher Leiter des Zentrums für Technologie und Transfer (ZTT). Er veröffentlichte über 160 Werke, darunter sechs Bücher, und ist Gutachter, Organisator und Gasteditor für diverse akademische Tagungen und Fachzeitschriften sowie Mitglied verschiedener Initiativen, insbesondere zur Bekämpfung von Cyberkriminalität. Er nutzt
Linux seit Kernel 2.0.0. Seine Webseite: https://www.wendzel.de. Twitter: @cdp_xe. Johannes Plötner beschäftigt sich seit Kernel 2.0.34 mit Linux. In der Spätphase des Dotcom-Booms fand er seinen ersten Job als Programmierer für Perl und C/C++ über die Mailingliste »Securityfocus«. Später studierte er Informatik an der Universität Karlsruhe (TH). Seit dieser Zeit ist Linux im professionellen Produktionsbetrieb seine Welt: So administrierte er unter anderem größere Websites wie www.sueddeutsche.de, migrierte zahllose Serverdienste und komplette Kundensysteme über Plattform- und Rechenzentrumsgrenzen hinweg von A nach B und zuletzt in die Cloud, optimierte E-Mail-Cluster auf einen Durchsatz von mehreren hunderttausend E-Mails pro Stunde oder kümmerte sich um die internen IT-Systeme eines Internet-Service-Providers. Von kleineren Bugs bis hin zu kompletten Stromausfällen ganzer Rechenzentren hat er dabei schon einiges gesehen und behauptet beharrlich, in der Summe mehr repariert als kaputt gemacht zu haben. Nach über 10 Jahren als Führungskraft im IT-Bereich ist er mittlerweile wieder als IT-Berater im DevOps- und Cloud-Umfeld selbstständig, baut CloudInfrastruktur aus Code, automatisiert Deploymentprozesse und versucht mit allen Mitteln, die Systeme korrekt konfiguriert, selbstheilend und selbstskalierend zu halten.
1 Einleitung »Das Beste, was wir von der Geschichte haben,
ist der Enthusiasmus, den sie erregt.«
– Johann Wolfgang von Goethe Das fängt ja gut an. Da will man ein Buch schreiben und weiß nicht einmal, wie man das Thema grob umreißen soll. Dabei könnte alles so einfach sein – wir schreiben doch nur über ein Betriebssystem, das eigentlich keines ist. Aber wir schreiben eben auch über einen Begriff, der nicht mehr nur Technik, sondern mittlerweile so etwas wie eine Philosophie umschreibt. Neugierig? Zu Recht! Kurz gesagt steht der Begriff Linux heute für ein sehr stabiles, schnelles, freies, unixähnliches Betriebssystem – obwohl Linux streng genommen nur der Kern (»Kernel«) dieses Betriebssystems ist. Doch eins nach dem anderen. Zunächst einmal ist ein Betriebssystem die grundlegende Software auf einem Computer. In den Worten von Andrew Tanenbaum, einer Koryphäe der Betriebssystemforschung, klingt dies so: Ein Betriebssystem ist eine Software, die die Aufgabe hat, vorhandene Geräte zu verwalten und Benutzerprogrammen eine einfache Schnittstelle zur Hardware zur Verfügung zu stellen. Ein Betriebssystem ermöglicht es demnach, Hardware anzusteuern, Dateien zu lesen und zu speichern und Programme zu installieren, zu starten und zu verwenden. Ein Betriebssystem verwaltet den Speicher Ihres Computers inklusive sämtlicher Medien und ermöglicht Netzwerk- und Internetzugriff. Betriebssysteme haben dabei ganz unterschiedliche Einsatzgebiete (siehe Kasten).
Doch nun zurück zum Betriebssystem dieses Buches: Linux! Wie erwähnt, können Sie Linux auf einer riesigen Mainframe laufen lassen, aber eben auch auf einem Mikrocontroller, einem SmartHome-Gerät oder einem Smartphone. Einsatzgebiete von Betriebssystemen Im Standardwerk zu Betriebssystemen von Andrew Tanenbaum und Herbert Bos (Moderne Betriebssysteme, erschienen bei Pearson Studium) werden Einsatzgebiete von Betriebssystemen unterschieden. Linux kann die meisten dieser Einsatzgebiete völlig problemlos abdecken. Gehen wir diese Einsatzgebiete einmal durch: Mainframe-Betriebssysteme sind für Großrechner ausgelegt. Großrechner können besonders hohe Anforderungen an den Datendurchsatz und die -kapazität (bspw. mit Tausenden von Festplatten) sowie Zuverlässigkeit (Ausfallsicherheit) gewährleisten. Ihr Einsatzgebiet sind große (Web-)Server, Business-to-Business-Anwendungen, Systeme für Flugbuchungen und große Banken, die Tausende von Transaktionen gleichzeitig abwickeln müssen. Typischer Vertreter ist neben Linux auch z/OS. Server-Betriebssysteme müssen ähnliche Anforderungen wie Mainframe-Betriebssysteme erfüllen, allerdings in geringerer Ausprägung. Sie dienen primär der Erbringung von Netzwerkdiensten, etwa zur Bereitstellung eines Webservers (also eines Dienstes zur Auslieferung von Webinhalten). Typische Vertreter sind Linux, Unix, BSD und Windows Server. Betriebssysteme für PCs und Workstations kommen auf kleinen Heimcomputern und ihren leistungsfähigeren Geschwistern – den Workstations – zum Einsatz. Sie dienen im
privaten und Geschäftsumfeld der Bearbeitung sämtlicher digital unterstützter Aufgaben, von der Bearbeitung einfacher E-Mails bis hin zum computergestützten Entwurf von Bauteilen oder zur Durchführung wissenschaftlicher Simulationen. Typische Vertreter sind Windows, macOS, Linux, BSD und Unix. Echtzeit-Betriebssysteme werden hauptsächlich in der Fertigung (Steuerung von Produktionsanlagen samt Robotern) und sonstigen Bereichen der Automation eingesetzt, etwa in der Gebäudeautomation. Sie steuern zeitkritische Prozesse. Wenn beispielsweise sichergestellt werden muss, dass zehnmal pro Sekunde Temperaturwerte elektronisch gemessen werden, dann käme ein Echtzeitbetriebssystem dafür infrage. Typische Vertreter sind VxWorks, QNX und Linux (nur bestimmte echtzeitfähige Varianten). Betriebssysteme für eingebettete Systeme kommen auf Microcontrollern und Einplatinensystemen (z. B. Raspberry Pi), Smartphones oder Smart-Home-Steuerungen zum Einsatz. Sie müssen in ressourcenbeschränkten Umgebungen (wenig Speicher, geringe Rechenleistung, kurze Batterielaufzeit) kontextspezifische Dienste (etwa Überwachung und Steuerung eines Gebäudes) ermöglichen. Typische Vertreter sind verschiedene Linux-Distributionen, iOS, Android (ebenfalls Linuxbasiert). Betriebssysteme für Sensorknoten werden auf winzigen Sensoren untergebracht, die miteinander und mit einer Basisstation kommunizieren (per Funk). Sie sind batteriebetrieben und müssen besonders energiesparsam sein. Ihr Ziel ist der wartungsarme Langzeitbetrieb (oft im Außenbereich). Ein Beispiel sind Temperatursensoren, die mit einer Smart-Home-Zentrale
kommunizieren. Typische Vertreter sind TinyOS, RIOT und Contiki. Betriebssysteme für Chipkarten müssen unter extremer Ressourcenknappheit (geringe Rechenleistung und minimale Speicherkapazität) auf Chipkarten laufen. Eingesetzt werden diese Systeme etwa beim elektronischen Bezahlverkehr, weshalb sie trotz ihrer eingeschränkten Umgebung rechenintensive Verschlüsselungsverfahren durchführen müssen. Typische Vertreter sind bspw. Security Card Operating System (SECCOS) für die EC-Karte in Deutschland sowie STARCOS und CardOS.
1.1 Warum Linux? Vielleicht stellen Sie sich gerade diese Frage: Warum Linux? Sicher – wenn Sie mit Ihrem gewohnten Betriebssystem zufrieden und einfach »Nutzerin« oder »Nutzer« sind, ist die Motivation gering, hier überhaupt Zeit zu investieren und das Betriebssystem zu wechseln. Wer wäre also ein typischer Linux-Nutzer – und warum? 1.1.1 Man muss kein Informatiker sein ...
Linux hat in den dreißig Jahren erhebliche Fortschritte hinsichtlich Benutzerfreundlichkeit und Ergonomie gemacht, sodass man kein Informatikstudium absolviert haben muss, um das System bedienen zu können. Freie Software fristet mittlerweile kein Nischendasein mehr, sondern erobert allerorten Marktanteile. Im Web ist Linux ein Quasi-Standard für Internetserver aller Art. Wer ein leistungsfähiges und günstiges Hosting der eigenen Webseite, des eigenen Webshops oder anderer Dienste will, wird hier fündig. Viele kleine, schicke Netbooks werden bereits ab Werk
mit Linux geliefert, sehr performant und unschlagbar günstig durch die schlichte Abwesenheit jeglicher Lizenzkosten. Aber egal, ob Server- oder Anwendersystem: Die eigentliche Software setzt sich mittlerweile auch ganz unabhängig von Linux auf anderen Betriebssystemen durch. Auch so kommen viele Anwenderinnen und Anwender auf die Idee, sich näher mit Linux auseinanderzusetzen. Es ist nur ein kleiner Schritt von Firefox, Thunderbird und LibreOffice unter Windows zu einem komplett freien Betriebssystem wie Linux. Außerdem sind einst komplizierte Einstiegshürden heute leicht zu nehmen, es gibt Anleitungen und Hilfe allerorten, professionellen kommerziellen Support genauso wie zahlreiche Webseiten und Internetforen. Und natürlich gibt es auch dieses Buch. Um Linux effektiv nutzen zu können, muss man heute wirklich kein halber Informatiker mehr sein. 1.1.2 ... aber es hilft
Trotzdem ist Linux vor allem für Anwenderinnen und Anwender zu empfehlen, die einfach »mehr« wollen: Mehr Möglichkeiten, mehr Leistung oder schlicht mehr Freiheiten. Linux ist eine offene Plattform, bevorzugt genutzt von vielen Entwicklern, Systemadministratorinnen und sonstigen interessierten PowerUsern. Die Faszination der Technik macht sicherlich einen Teil der Beliebtheit aus. Aber ist das alles? Was haben wohl Google, Instagram, Facebook, YouTube und Co. gemeinsam? Richtig, ihre Server laufen unter Linux.[ 1 ] Nach der Lektüre dieses Buches haben Sie zumindest eine Ahnung davon, welche Möglichkeiten Linux besitzt, die Ihnen andere Betriebssysteme so nicht bieten können.
Im nächsten Abschnitt machen wir Sie jedoch erst einmal mit den wichtigsten und gängigsten Begriffen vertraut.
1.2 Grundbegriffe: Kernel, Distribution, Derivat Der Begriff Linux bezeichnet dabei eigentlich kein ganzes Betriebssystem, sondern nur die Kernkomponente, den sogenannten Kernel. Damit man mit Linux etwas anfangen kann, benötigt man zusätzlich zum Kernel noch System- und Anwendersoftware. Diese zusätzliche Software wird daher zusammen mit dem Kernel und einer mehr oder weniger ansprechenden Installationsroutine von sogenannten Distributoren zu Distributionen zusammengepackt. Zu den bekanntesten Distributionen zählen Arch Linux, Linux Mint, (open)SUSE, Fedora, Red Hat Enterprise Linux (RHEL), Slackware, Gentoo, Debian und Ubuntu. Ist eine Distribution von einer anderen abgeleitet, so spricht man von einem Derivat[ 2 ]. So ist beispielsweise das bekannte Ubuntu ein Debian-Derivat, wohingegen Fedora ein Red-Hat-Derivat ist. Derivate bzw. abgespaltete Projekte sind in der Open-Source-Welt recht häufig und eine wichtige Innovationsquelle. Doch dazu später mehr. 1.2.1 Bekannte Distributionen und Derivate
Im Folgenden werden wir einen kleinen Einblick in die aktuelle Welt der Distributionen und Derivate geben. Der Rest des Buches geht dann nur noch in wichtigen Fällen auf Besonderheiten einzelner Distributionen und Derivate ein, da wir Ihnen Wissen vermitteln möchten, mit dem Sie unter jedem System zum Ziel kommen.
In Abschnitt 1.3, »Die Entstehungsgeschichte von Linux«, erfahren Sie mehr über die ersten Derivate und Distributionen.
1.2.2 Arten von Distributionen
Es gibt Distributionen, die direkt von einer CD, DVD oder einem USB-Stick gebootet werden können und mit denen Sie ohne vorhergehende Installation auf einer Festplatte arbeiten können. Man nennt diese Distributionen Live-Systeme. Hierzu zählt beispielsweise Knoppix, das die grafische Oberfläche LXDE sowie viele Zusatzprogramme enthält. Neben Knoppix als »reinem« LiveSystem bieten auch viele gängige Distributionen wie bspw. Ubuntu Installationsmedien an, die sich auch als Live-System starten lassen. Dann wiederum gibt es Embedded-Distributionen. Dabei handelt es sich um stark minimierte Systeme, bei denen alle unnötigen Programme und Kernel-Features deaktiviert wurden, um Speicherplatz und Rechenbedarf einzusparen. Sinn und Zweck solcher Systeme ist es, eine Distribution auf sogenannten eingebetteten Systemen lauffähig zu machen, die teilweise nur über sehr wenig Hauptspeicher und Rechenleistung verfügen. Es gibt hierfür übrigens auch speziell minimierte C-Bibliotheken, die Sie beispielsweise auf kernel.org finden. Verwendung finden Embedded-Distributionen unter anderem im Router-Bereich. Man kann mit Distributionen wie OpenWRT oder FreeWRT auf diese Weise z. B. Linux-Firewalls auf handelsüblichen Routern installieren. Die wichtigsten Distributionen sind für den Allzweck-Einsatz auf Heimanwender-Desktops, professionellen Workstations und Servern ausgelegt (und dementsprechend in verschiedenen
Ausführungen zu haben). Distributionen wie openSUSE, Fedora, Ubuntu und Gentoo zählen zu diesem Bereich. Sie umfassen sowohl eine Vielzahl von Paketen für das Arbeiten mit verschiedensten Oberflächen-Systemen als auch Serversoftware, Entwicklerprogramme, Spiele und was man sonst noch alles gebrauchen kann. Darüber hinaus gibt es noch Security-Distributionen/Derivate, die speziell darauf ausgelegt sind, eine besonders sichere Umgebung für sensible Daten oder den Schutz von Netzwerken zu bieten. Hierzu zählen Distributionen wie Hardened Gentoo, die im Unterschied zu anderen Distributionen oft modifizierte Kernel und eine minimalistische Softwareauswahl zur Reduktion der Angriffsoberfläche anbieten. Solche Distributionen sind auch für den Einsatz als Firewall/VPN-System geeignet, doch es gibt auch spezielle Distributionen, die hierfür optimiert sind und beispielsweise keine gehärteten Kernel benutzen. Hierzu zählen das bereits erwähnte OpenWRT und seine Derivate. Im Security-Kontext gibt es auch Distributionen wie Kali Linux, die vor allem Anwendungen zur Identifikation von Sicherheitslücken, zur Durchführung von Penetrationstests und zur digitalen Forensik mitbringen. Es gibt noch viele weitere spezialisierte Linux-Distributionen. Beispielsweise werden spezielle Distributionen mit wissenschaftlichen Programmen für den Forschungsbereich erstellt. Schauen Sie sich bei Interesse doch einmal die Distribution Scientific Linux unter www.scientificlinux.org an. Unter distrowatch.com und www.distrorankings.com finden Sie Übersichten zu einer Vielzahl bekannter Distributionen und
Derivate.
Es gibt also offensichtlich viel Auswahl. Aber was ist die richtige Distribution für Sie? Für einen allerersten Eindruck eignet sich oft ein »Live-System« – werfen Sie also vielleicht einen Blick auf Ubuntu, Fedora oder Mint Linux. Sie sind herzlich zum Ausprobieren eingeladen!
1.3 Die Entstehungsgeschichte von Linux Linux übernahm diverse Konzepte und Eigenschaften des Betriebssystems Unix. Daher beschäftigen wir uns an dieser Stelle zunächst einmal mit der Entstehungsgeschichte von Unix. Wir beginnen dazu mit einem Rückblick auf die graue Frühzeit der Informatik. 1.3.1 Die Entstehung von Unix
In den 1960er Jahren wurden die ersten großen Softwaresysteme gebaut.[ 3 ] Zu dieser Zeit, nämlich im Jahr 1965, begannen BELL, General Electric und das MIT, an einem System namens MULTICS (Multiplexed Information and Computing System) zu arbeiten. Als allerdings feststand, dass dieses Vorhaben scheitern würde, stieg BELL aus. Die Raumfahrt
Als 1969 das Apollo-Raumfahrtprogramm der USA im Mittelpunkt der Aufmerksamkeit stand, begann Ken Thompson (BELL) mit der Entwicklung einer MULTICS-Variante für zwei Benutzer. Dieses System entwickelte er für den Computer PDP-7 des Herstellers DEC. Sein Ziel war es, raumfahrtbezogene Programme zu entwickeln, um Orbit-Berechnungen für Satelliten, Mondkalender und Ähnliches zu realisieren. Das Grundprinzip von MULTICS wurde dabei übernommen und so bekam das daraus resultierende Betriebssystem beispielsweise ein hierarchisches Dateisystem. Brian Kernighan nannte dieses System spöttisch UNICS (von uniplexed). Erst später benannte man es aufgrund der Begrenzung
für die Länge von Dateinamen auf der Entwicklungsplattform GECOS in UNIX bzw. Unix um.[ 4 ] Ursprünglich waren alle Unix-Programme in der Programmiersprache Assembler geschrieben. Ken Thompson entschied sich später, eine Unterstützung für die Sprache FORTRAN zu entwickeln (ein sogenannter FORTRAN-Compiler), da Unix seiner Meinung nach ohne eine solche wertlos wäre. FORTRAN ist (wie C) eine Programmiersprache der dritten Generation und erlaubt, verglichen mit Assembler, das Programmieren auf einer höheren Abstraktionsebene. Nach kurzer Zeit entschied er sich allerdings, eine neue Programmiersprache namens B zu entwickeln, die stark von der Sprache BCPL (Basic Combined Programming Language) beeinflusst wurde. Aus B wird C
Da das Team 1971 ein PDP11-System bekam, das byteadressiert arbeitete, entschloss sich Dennis Ritchie, aus der wortorientierten Sprache B eine byteorientierte Sprache mit dem schlichten Namen »C« zu entwickeln, indem er unter anderem Typen hinzufügte. Tatsächlich gab es zwischen B und C noch einen Zwischenschritt in Form der Sprache New B (NB).[ 5 ] 1973 wurde der Unix-Kernel komplett neu in C geschrieben. Dieses neue Unix (mittlerweile in der Version 4) wurde damit auf andere Systeme portierbar. Noch im selben Jahr wurde Unix zu einem Mehrbenutzer-Mehrprozess-Betriebssystem (MultiuserMultitasking) weiterentwickelt und der Öffentlichkeit vorgestellt. Auf diesem System konnten nun mehrere Benutzer gleichzeitig unterschiedliche Programme laufen lassen. Da C gleichzeitig eine sehr portable, aber auch systemnahe Sprache war, konnte Unix recht gut auf neuen Plattformen implementiert werden, um dann
auch dort performant zu laufen. Die Vorteile einer Hochsprache wurden hier deutlich: Man braucht nur einen Übersetzer auf einer neuen Hardwareplattform und schon kann der Code mit nur wenigen Änderungen übernommen werden. Ein Jahr später, also 1974, erschien ein gemeinsamer Artikel mit dem Titel The UNIX Time-Sharing System von Dennis Ritchie und Ken Thompson im Fachblatt Communications of the ACM, in dem sie auf die Entstehungsgeschichte und die Wurzeln von Unix eingingen, die eben nicht völlig neu waren, sondern auf bestehenden Systemen basierten. So stammen Grundkonzepte etwa aus dem Berkeley Timesharing System (sogenanntes Forking, d. h. das Konzept zur Erzeugung neuer Prozesse), dem bereits erwähnten MULTICS (Konzepte der Systemaufrufe und der Shell) und TENEX oder wurden zumindest durch diese beeinflusst: »Der Erfolg von Unix liegt nicht so sehr in neuen Innovationen als vielmehr darin, dass sorgfältig ausgewählte Ideen zur vollen Blüte gebracht wurden.« (eigene Übersetzung aus dem Englischen) Ken Thompson und Dennis Ritchie erhielten 1998 von Bill Clinton die National Medal of Technology der USA für die Entwicklung von Unix und C. 1983 erhielten Thompson und Ritchie den Turing Award – die bedeutendste Auszeichnung der Informatik und für Informatiker prinzipiell wertig wie der Nobelpreis. Weitere wichtige Bücher mit Beteiligung dieser Autoren sind beispielsweise The Unix Programming Environment von Rob Pike und Brian Kernighan sowie The Unix time-sharing system von Dennis Ritchie. Es ist zudem zu sagen, dass Ritchie und Kernighan mit ihrem Buch The C Programming Language eines der bedeutendsten Werke der Informatik verfasst haben. Die Entstehung der Unix-Derivate
1977 nahm man dann auch die erste Implementierung auf einem NichtPDP-System vor, nämlich auf ein Interdate 8/32. Dies regte weitere Unix-Portierungen durch Firmen wie HP und IBM an und die Unix-Entwicklung begann, sich auf viele Abkömmlinge, sogenannte Derivate, auszuweiten. Die Unix-Variante von AT&T wurde 1981 mit der von BELL zu einem einheitlichen »Unix-System III« kombiniert. 1983 kündigte BELL das »System V« an, das primär für den Einsatz auf VAX-Systemen an Universitäten entwickelt wurde. Im Jahr darauf annoncierte AT&T die zweite Version von System V. Die Anzahl der Unix-Installationen stieg bis dahin auf ca. 100.000 an. 1986 erschien System V, Release 3. Schließlich wurde 1989 System V Release 4 (SVR4) freigegeben, das noch heute als Unix-Standard gilt. 1.3.2 BSD wird ins Leben gerufen
Neben SVR4-Unix gab es noch eine Entwicklung von BSD-Unix, auf deren Darstellung wir hier natürlich keineswegs verzichten möchten. Schließlich haben wir der BSD-Implementierung der sogenannten TCP/IP-Protokolle (so bezeichnet man die Kommunikationsprotokolle für das Internet) mehr oder weniger das heutige Internet zu verdanken. Bereits 1974 verteilte AT&T Quellcodelizenzen an einige Forschungsinstitute. Auch das Computing Sciences Research Center (CSRC) der Bell Labs bekam solch eine Lizenz. In Berkeley entwickelte ein Kreis von Programmierern der dortigen Universität in den folgenden Jahren einen neuen Code und nahm Verbesserungen gegenüber AT&T-Unix vor, wonach 1977 »1BSD«, die erste Berkeley Software Distribution, von Bill Joy zusammengestellt
wurde. Im darauffolgenden Jahr wurde »2BSD« veröffentlicht, das über neue Software und Verbesserungen verfügte. 1979 beauftragte die Defense Advanced Research Projects Agency (DARPA) der amerikanischen Regierung die Computer Systems Research Group (CSRG) der University of California, Berkeley, die Unix-Referenzimplementierung der Protokolle für das ARPANET, den Vorläufer des Internets, zu entwickeln. Die CSRG veröffentlichte schließlich das erste allgemein verfügbare Unix namens 4.2BSD, das unter anderem eine Integration der Kommunikationsprotokolle für das Internet (der oben erwähnten TCP/IP-Protokolle) aufwies: Damit konnte bereits sehr ähnlich über ein Netzwerk kommuniziert werden, wie es heutige Rechner nach wie vor tun.[ 6 ] Außerdem wurde ein neues Dateisystem eingeführt, nämlich das Berkeley Fast Filesystem (FFS). Somit kann dieses BSD-Derivat als Urvater des Internets angesehen werden. Durch die Integration von TCP/IP und der Berkeley-SocketAPI (das ist eine Programmierschnittstelle für die Netzwerkkommunikation) wurden Standards geschaffen bzw. geschaffene Standards umgesetzt, die für das spätere Internet essenziell sein sollten. Wenn man bedenkt, dass selbst heute noch ebendiese Berkeley-Socket-API als Standard in allen netzwerkfähigen Betriebssystemen implementiert ist, wird erst das volle Ausmaß der Bedeutung dieser Entwicklungen deutlich. 1989 entschloss man sich dazu, den TCP/IP-Code in einer von AT&T unabhängigen Lizenz als Networking Release 1 (Net/1) zu vertreiben. Net/1 war die erste öffentlich verfügbare Version. Viele Hersteller benutzten den Net/1-Code, um TCP/IP in ihre Systeme zu integrieren. In 4.3BSD Reno wurden 1990 noch einmal einige Änderungen am Kernel und an den Socket-APIs vorgenommen, um OSI-Protokolle zu integrieren.
Im Juni 1991 wurde Net/2 herausgegeben, das komplett neu und unabhängig vom AT&T-Code entwickelt wurde. Die wichtigsten Neuerungen von Net/2 waren eine komplette Neuimplementierung der C-Bibliothek und vieler Systemprogramme sowie die Ersetzung des AT&T-Kernels bis auf sechs Dateien. Nach einiger Zeit hatte William Frederick Jolitz auch die letzten sechs Dateien neu entwickelt. Er stellte ein vollständiges, bootbares Betriebssystem zum freien FTP-Download zur Verfügung. Es trug den Namen 386/BSD und lief auf Intel-Plattformen. Die Berkeley Software Design, Inc. (BSDI) brachte 1991 mit BSD/OS eine kommerzielle Weiterentwicklung von 386/BSD auf den Markt. Diese Version konnte für den Preis von 999 US-Dollar erworben werden. BSD/OS konnte sich über mehr als zehn Jahre auf dem Markt halten, insbesondere als Betriebssystem für Server. 1992 entstand außerdem das freie NetBSD-Projekt, das es sich zum Ziel setzte, 386/BSD als nicht kommerzielles Projekt weiterzuentwickeln und es auf möglichst vielen Plattformen verfügbar zu machen. Nachdem die Unix System Laboratories, eine Tochtergesellschaft von AT&T, BSDI wegen einer Urheberrechtsverletzung verklagt hatten, mussten einige Veränderungen am Net/2-Code vorgenommen werden. Daher mussten 1994 alle freien BSDProjekte ihren Code auf den von 4.4BSD-Lite (auch als Net/3 bezeichnet) umstellen. Mit der Veröffentlichung von 4.4BSD-Lite2 im Jahr 1995 wurde die CSRG aufgelöst. Eingestellt wurde auch die Entwicklung von BSD/OS – allerdings erst Anfang des neuen Jahrtausends. Die vier großen zu diesem Zeitpunkt existierenden BSD-Derivate FreeBSD, OpenBSD sowie das bereits erwähnte NetBSD (und ihre jeweiligen Ableger) werden noch bis heute gepflegt und ständig weiterentwickelt.
1.3.3 Die Geburtsstunde von Linux
Wir schreiben das Jahr 1991 und Linus Torvalds kann die Linux 0.02 bereits in der Newsgroup comp.os.minix posten. Hier die Originalnachricht: From: [email protected] (Linus Benedict Torvalds)
Newsgroups: comp.os.minix
Subject: What would you like to see most in minix?
Date: 25 Aug 91 20:57:08 GMT
Hello everybody out there using minix -
I'm doing a (free) operating system (just a hobby, won't be big
and professional like gnu) for 386(486) AT clones. This has been
brewing since april, and is starting to get ready. I'd like any
feedback on things people like/dislike in minix, as my OS
resembles it somewhat (same physical layout of the file-system
(due to practical reasons) among other things).
I've currently ported bash(1.08) and gcc(1.40), and things seem
to work. This implies that I'll get something practical within
a few months, and I'd like to know what features most people
would want. Any suggestions are welcome, but I won't promise
I'll implement them :-)
Linus ([email protected])
PS. Yes - it's free of any minix code, and it has a multi-
threaded fs. It is NOT protable (uses 386 task switching etc),
and it probably never will support anything other than AT-
harddisks, as that's all I have :-(.
Listing 1.1 Linus Torvalds’ Posting an comp.os.minix
Zu diesem Zeitpunkt liefen bereits wichtige Programme wie der GNU C-Compiler (gcc, dient der Übersetzung von SoftwareQuellcode der Programmiersprache C in ein ausführbares Programm), die bash (dient der Eingabe von Befehlen) und compress (dient zum Komprimieren von Dateien) auf diesem System. Im Folgejahr veröffentlichte Torvalds Version 0.12 auf einem öffentlichen FTP-Server, wodurch die Anzahl derjenigen stieg, die an der Systementwicklung mitwirkten. Im selben Jahr wurde das
Diskussionsforum alt.os.linux gegründet. Es handelt sich dabei um eine sogenannte Newsgroup – so hießen die Foren im Usenet.[ 7 ] So wie das Internet mit BSD groß wurde, ist Linux also ein Kind des Internets. Im Jahr 1994 wurde Version 1.0 veröffentlicht. Der Kernel verfügte zu diesem Zeitpunkt schon über Netzwerkfähigkeit. Außerdem portierte das XFree86-Projekt seine grafische Oberfläche – das XWindow-System – auf Linux. Das wohl wichtigste Ereignis in diesem Jahr war jedoch, dass Torvalds auf die Idee kam, den Kernelcode unter der GNU General Public License zu veröffentlichen. Zwei Jahre später war Linux 2.0 zu haben. Erste Distributionen stellten ihre Systeme nun auf die neue Version um, darunter auch Slackware mit dem »96«-Release. 1998 erschien die Kernelversion 2.2. Von da an verfügte Linux auch über Multiprozessorsupport. Im Jahr 2001 erschien schließlich Version 2.4 und im Dezember 2003 Version 2.6. 2011 kam Version 3.0 heraus. Nach einer Meinungsumfrage auf der Plattform Google+ wurde die Version im Jahr 2015 schließlich von 3.19 nicht auf 3.20, sondern auf 4.0 erhöht. Linux 5.0 erschien dann schließlich 2019. Die zum Zeitpunkt des Schreibens aktuelle Version 5.14.6 erschien Mitte September 2021. Empfehlenswerte Bücher zur Geschichte der Betriebssysteme A. S. Tanenbaum, H. Bos: Moderne Betriebssysteme. 4. Auflage, Pearson Studium, 2016. B. Hansen (Hrsg.): Classic Operating Systems. From Batch Processing to Distributed Systems. Springer, 2001. (Dieser Titel ist für anspruchsvolle Leserinnen und Leser geeignet, die
selektierte englische Originalaufsätze einiger Koryphäen lesen möchten.)
1.3.4 Die Kernelversionen
Das Versionsschema der Kernelversionen ist seit Kernel 3.0 wie folgt: Alle paar Monate wird die erste Stelle nach dem Punkt (3.x) erhöht, kleine Änderungen (Fehlerbehebungen und Sicherheitsupdates) werden mit der zweiten Stelle hinter dem Punkt angegeben (3.x.y). Entwicklerversionen des Kernels gibt es nur in einem separaten Entwicklungszweig und der Entwicklungsprozess läuft dabei folgendermaßen ab: Es gibt ein Zeitfenster, innerhalb dessen neue Features in den Kernel eingebaut werden. Anschließend werden diese Features optimiert und auf ihre korrekte Funktionsweise hin überprüft. Steht fest, dass alle neuen Features ordentlich funktionieren, wird schließlich eine neue Kernelversion nach dem o. g. Schema herausgegeben. Sollten Sie mal jemanden treffen, der Ihnen von irgendwelchen komischen Versionen à la »Linux 20.0« erzählen will, haben Sie ein seltenes Exemplar der Spezies Mensch gefunden, die offensichtlich die falschen Bücher liest. Diese bringen nämlich die Versionen der Distributionen und des Kernels durcheinander. Aber keine Angst: Aktuelle Distributionen beinhalten natürlich immer die Stable-Version des Kernels. Einige Anbieter von Distributionen beschäftigen auch Kernelentwickler, die die Features des (eigenen) Kernels erweitern, um den Anwenderinnen und Anwendern beispielsweise zusätzliche Treiber zur Verfügung zu stellen.
Wie bereits erwähnt, gibt es Distributionen, die einen modifizierten Kernel beinhalten, und solche, die den unmodifizierten Kernel nutzen. Dieser unmodifizierte Kernel ohne zusätzliche Patches wird auch als Vanilla-Kernel bezeichnet. Auf kernel.org erfahren Sie zu jedem Zeitpunkt etwas über die aktuellen Versionen des Linux-Kernels. Das Linux-Maskottchen
Da Linus Torvalds ein Liebhaber von Pinguinen ist, wollte er einen als Logo für Linux haben. Larry Erwing entwarf mit dem Grafikprogramm GIMP einen Pinguin (siehe Abbildung 1.1). Er gefiel Torvalds und fertig war Tux, der übrigens für Torvalds Unix steht.
Abbildung 1.1 Tux
1.3.5 Stallman und das GNU-Projekt
Im Jahre 1992 wurde Linux unter die GNU General Public License (GPL) gestellt, die 1989 von Richard Stallman erarbeitet worden war.
Stallman gründete 1983 das GNU-Projekt, das freie Software und Kooperationen zwischen den Entwicklerinnen und Entwicklern befürwortet. Außerdem ist Stallman Entwickler von bekannten Programmen wie dem Emacs-Editor oder dem GNU-Debugger. Stallman ist noch heute einer der wichtigsten – wenn nicht der wichtigste – Verfechter des Open-Source-Gedankens. Stallman arbeitete in den 70er Jahren am Massachusetts Institute of Technology (MIT) in einem Labor für künstliche Intelligenz und kam dort zum ersten Mal mit Hackern in Kontakt. Die dortige Arbeitsatmosphäre gefiel ihm so gut, dass er ihre spätere Auflösung sehr bedauerte. Zudem wurde Software immer mehr in binärer Form und weniger durch Quelltexte vertrieben, was Stallman ändern wollte. Aus diesem Grund schuf er das GNU-Projekt, dessen Ziel die Entwicklung eines kompletten freien Betriebssystems war. [ 8 ] Den Kern dieses Betriebssystems bildet heutzutage meistens Linux. Umgekehrt sind die wichtigsten Komponenten der Userspace-Software von Linux seit Beginn GNU-Programme wie der gcc. Richard Stallman versuchte daher später, den Namen GNU/Linux durchzusetzen, was ihm aber nur bedingt gelang. Dass Linux selbst eigentlich nur den Kernel umfasst, wurde bereits angesprochen. Die für den Betrieb nötige Systemsoftware kommt in erster Linie vom bereits erwähnten GNU-Projekt (http://www.gnu.org). Diese Initiative gibt es seit 1984 und damit viel länger als Linux selbst. Das Ziel war von Anfang an, ein völlig freies Unix zu entwickeln, und mit Linux hatte das Projekt seinen ersten freien Kernel. Somit ist auch die Bezeichnung GNU/Linux für das Betriebssystem als Ganzes gebräuchlich. Was aber ist eigentlich freie Software? Wenn man ein Programm schreibt, so besitzt man an dessen Quelltext ein Urheberrecht wie ein Buchautor. Die resultierende Software kann verkauft werden,
indem man den Käuferinnen und Käufern durch eine Lizenz gewisse Nutzungsrechte einräumt. Alternativ kann man aber auch festlegen, dass das eigene Programm von anderen kostenlos benutzt werden kann. Gibt man sogar den eigenen Quellcode frei, so spricht man von offener Software. Im Linux- und BSD-Umfeld gibt es nun unterschiedliche Lizenzen, die mit teilweise besonderen Bestimmungen ihr jeweils ganz eigenes Verständnis von »Freiheit« verdeutlichen. Die GPL
Linux steht wie alle GNU-Projekte unter der GNU Public License, der GPL. Laut dieser Lizenz muss der Quellcode eines Programms frei zugänglich sein. Das bedeutet jedoch nicht, dass GPL-Software nicht verkauft werden darf. Mehr dazu finden Sie unter www.gnu.org/philosophy/selling.de.html. Selbst bei kommerziellen Distributionen zahlt man allerdings oft nicht für die Software selbst, sondern für die Zusammenstellung der Software, das Brennen der CDs/DVDs, die eventuell vorhandenen Handbücher und den (Installations-)Support. Die GPL stellt damit Programme unter das sogenannte Copyleft: Verändert man ein entsprechendes Softwareprojekt, so muss das veränderte Ergebnis wieder frei sein. Man darf zwar Geld für ein GPL-basiertes Produkt nehmen, muss aber den Sourcecode samt den eigenen Änderungen weiterhin frei zugänglich halten.
Somit bleibt jede einmal unter die GPL gestellte Software immer frei – es sei denn, alle jemals an einem Projekt beteiligten Entwicklerinnen und Entwickler stimmen einer Lizenzänderung zu.
Bei großen Softwareprojekten wie dem Linux-Kernel mit vielen Tausend Beteiligten ist das undenkbar. Die BSD-Lizenz
Im Unterschied zu der im Linux-Umfeld verbreiteten GPL verzichtet die von BSD-Systemen verwendete Lizenz auf ein Copyleft. Man darf zwar den ursprünglichen Copyright-Vermerk nicht entfernen, doch darf entsprechend lizenzierte Software durchaus Ausgangspunkt für proprietäre, kommerzielle Software sein. Die BSD-Lizenz ist also weniger streng als die GPL, aber aufgrund der möglichen freien Verteilbarkeit und Veränderbarkeit immer noch freie Software. Weitere freie Projekte
Natürlich gibt es freie Software nicht nur vom GNU-Projekt oder von dem BSD-Entwicklerteam. Jeder kann für eigene Softwareprojekte die GPL oder die BSD-Lizenz verwenden. Natürlich kann man – wie beispielsweise das Apache-Projekt – auch eigene Open-Source-Lizenzen mit besonderen Bestimmungen entwickeln. Jedoch haben bekannte Lizenzen den Vorteil, dass sie in der Community auch anerkannt sind und einen guten Ruf genießen oder – wie die GPL – sogar bereits von einem deutschen Gericht in ihrer Wirksamkeit bestätigt wurden. 1.3.6 Geschichte der Distributionen Bootdisk und Rootdisk
Ursprünglich war nur der Quellcode des Linux-Kernels verfügbar, der von erfahrenen Unix-Anwenderinnen und -Anwendern übersetzt und gebootet werden konnte. Mit einem blanken,
bootbaren Kernel konnte man aber nicht sonderlich viel anfangen, wenn man nicht wusste, wie die zugehörigen Benutzerprogramme, mit denen man dann etwa seine Mails lesen konnte, installiert werden. Aus diesem Grund stellte Linus Torvalds zunächst zwei Disketten-Images im Internet zur Verfügung, die besonders Anwendern alter Slackware-Versionen bekannt sein dürften: die Boot- und die Rootdisk. Von der Bootdisk war es möglich, den LinuxKernel beim Start des Rechners zu laden. War der Ladevorgang abgeschlossen, musste man die Rootdisk einlegen. Diese enthielt Basisanwendungen und machte das Linux-System für Anwenderinnen und Anwender ohne größere Vorkenntnisse zugänglich. SLS
Die erste halbwegs benutzbare Linux-Distribution nannte sich SLS (Softlanding Linux System) und wurde 1992 von Peter McDonald erstellt. Da SLS viele Fehler beinhaltete, entwickelten zwei Personen basierend auf SLS jeweils eine neue Distribution, die beide die ältesten heute noch aktiven Distributionsprojekte darstellen. Slackware und Debian
Der erste Entwickler war Patrick J. Volkerding, der im Juli 1993 Slackware 1.0.0 ver"offentlichte. Ian Murdock gab im August 1993 die erste Debian-Version frei. Auf Debian und Slackware basieren zahlreiche der heute aktiven Distributionen (etwa Arch, Zenwalk oder Ubuntu). Die letzte Version von Slackware (14.2) wurde im Juli 2016 veröffentlicht und ist damit zwar noch nicht eingestellt, aber doch eher etwas für Enthusiasten. Debian dagegen hat eine sehr aktive
Community und es werden kontinuierlich drei verschiedene Varianten gepflegt: stable, testing und unstable. Die stable-Release enthält nur stabile Pakete, die über einen längeren Zeitraum mit Updates versorgt werden. Oft nutzt man Pakete aus diesem Zweig für die Serverinstallation, da hier Sicherheit in der Regel vor Aktualität geht. Im testing-Zweig findet man alle Pakete, die in das zukünftige stable-Release eingehen sollen. Hier können die Pakete ausführlich getestet und für die Veröffentlichung vorbereitet werden. Der unstable-Zweig ist trotz seines Namens nicht zwangsläufig instabil. Stattdessen findet man hier immer die aktuellen Pakete, die so oder anders frühestens in das übernächste Debian-Release Einzug halten werden. Aufgrund der Aktualität können wir trotz manchmal auftretender Probleme diesen Zweig vor allem für Workstation-Installationen empfehlen. Red Hat und Fedora
Im November 1994 wurde die Distribution Red Hat begründet, die auf Slackware basierte, aber ein eigenes Paketformat (RPM) bekam. Auf ihr basieren die heutigen Distributionen Red Hat Enterprise Linux und Fedora. SuSE, OpenSuSE und SUSE
Ebenfalls 1994 wurde die Distribution SuSE Linux veröffentlicht. SuSE Linux war jahrelang die in Deutschland populärste LinuxDistribution, zusammengestellt durch die Software- und SystemEntwicklungsgesellschaft mbH aus Nürnberg. Mit ihr gab es (neben Red Hat Linux) eine einfach zu bedienende Distribution mit großer Paketauswahl. Für den deutschen Markt war zudem die ISDNUnterstützung sehr bedeutsam. Später wurde die Firma von Novell übernommen und der Name SuSE komplett groß geschrieben, also
»SUSE«. Heute gibt es die von der Community mitgepflegte Variante openSUSE sowie die Enterprise-Varianten SLES und SLED (SUSE Linux Enterprise Server/Desktop) für Unternehmen. Knoppix
Knoppix von Klaus Knopper war die erste wirklich bekannte Distribution, die sich direkt von CD starten und komplett benutzen ließ. Diese Distribution wird nach wie vor aktiv weiterentwickelt und setzt den LXDE-Desktop ein. Gentoo
Gentoo Linux basiert auf einem BSD-artigen Ports-System (man spricht bei Gentoo allerdings nicht von Ports, sondern von Ebuilds), also einem System, bei dem Software erst kompiliert werden muss. Die Hauptvorteile von Gentoo liegen in der großen Anpassbarkeit und der Performance der für den eigenen Prozessor optimierten Software. Gentoo richtet sich eher an fortgeschrittene User und bietet mittlerweile neben dem Linux-Kernel auch einen FreeBSDKernel an (dies gilt übrigens auch für einige andere Distributionen). Ubuntu
Eine der mittlerweile populärsten Linux-Distributionen ist das auf Debian basierende Ubuntu. Das Ubuntu-Projekt verfolgt das Ziel, eine möglichst einfach zu bedienende, an den Anwenderinnen und Anwendern orientierte Distribution zu schaffen. Die Versionsnummern von Ubuntu setzen sich übrigens aus dem Erscheinungsjahr und -monat zusammen. Die Version 20.04 erschien entsprechend im April 2020. Der Distributor gibt zudem
sogenannte LTS-Versionen (Long Time Support) heraus, die besonders lang unterstützt werden. Linux Mint
Linux Mint ist eine populäre, auf dem jeweils aktuellen LTS-Release von Ubuntu basierende Distribution. Sie ist in mehreren Varianten (»Editionen«) verfügbar, die sich hauptsächlich in der standardmäßig installierten Desktop-Umgebung unterscheiden. Neben dem normalen Linux Mint gibt es auch eine auf Debian basierende Variante, LMDE (Linux Mint Debian Edition). Arch Linux und Manjaro Linux
Arch Linux und das darauf basierende, etwas einsteigerfreundlicher ausgelegte Manjaro Linux benutzen dagegen ein Rolling-ReleaseModell ohne feste Versionszyklen. Manjaro bietet dabei im Gegensatz zu Arch Linux wieder diverse Editionen mit unterschiedlichen vorinstallierten Desktopumgebungen an. Manjaro ist aktuell eine der populärsten Linux-Distributionen.
1.4 Zusammenfassung In diesem Kapitel haben Sie grundlegende Begrifflichkeiten und die Geschichte rund um Linux gelernt. Sie wissen, dass ein Kernel allein noch kein Betriebssystem macht. Sie kennen die wichtigsten Distributionen und deren Geschichte.
1.5 Aufgaben Kernel.org
Besuchen Sie die Webseite http://kernel.org. Informieren Sie sich. Was ist die letzte stabile Version des Linux-Kernels? Ubuntu-Dokumentation
Finden Sie die offizielle Dokumentation zur freien LinuxDistribution Ubuntu. Stöbern Sie etwas.
2 So funktioniert Linux »Ach, der Mensch begnügt sich gern.
Nimmt die Schale für den Kern.«
- Albert Einstein (zugeschrieben) Wie bereits in der Einleitung erwähnt, bezeichnet der Begriff Linux eigentlich nur den Kern des Betriebssystems, kurz »Kernel« genannt. In diesem Kapitel wollen wir nun erklären, wie Linux unter der Oberfläche funktioniert. Dazu werden wir am Beispiel des LinuxKernels die Grundlagen einer typischen Betriebssystemarchitektur vorstellen und dabei die in diesem Kontext zu lösenden Probleme sowie zentrale Zusammenhänge erläutern. Zwar ist es wenig sinnvoll, im Rahmen dieses Buches jede einzelne Quelldatei des Linux-Kernels besprechen zu wollen, jedoch werden wir an geeigneten Stellen die eine oder andere konkrete Verbindung zum Quellcode herstellen. Schließlich kann man bei Linux auf die Kernel-Quellen zugreifen und sich selbst von der Korrektheit der folgenden Aussagen überzeugen. Zum Aufbau und zur Ausrichtung dieses Kapitels hat uns folgende Erfahrung angeregt: Selbst Leute, die viel auf ihr Fachwissen halten, disqualifizieren sich regelmäßig durch Aussagen wie: »Warum programmiert man nicht endlich mal ein Betriebssystem in Java, das ist doch so genial objektorientiert?« »Benutzerprogramme haben keinen direkten Zugriff auf die Hardware; alles läuft über den Kernel.«
»Benutzerprogramme können nicht auf den Kernel zugreifen, der ist geschützt.« Solche Sätze sind entweder Halbwahrheiten oder sogar ausgemachter Unsinn. Nach diesem Kapitel sollten Sie diese und andere gängige Aussagen und Internet-Mythen in den richtigen Zusammenhang bringen können. Außerdem legt dieses Kapitel eine Grundlage für das Verständnis von Linux und damit für den Rest des Buches.
2.1 Grundlagen Beginnen wir mit den wichtigsten Grundlagen, die Sie für das Verständnis des restlichen Kapitels benötigen werden. Viele dieser Informationen erscheinen Ihnen vielleicht selbstverständlich – und trotzdem empfehlen wir Ihnen, diesen Abschnitt sorgfältig zu lesen. Vielleicht wird doch noch der eine oder andere mutmaßlich bekannte Fakt etwas deutlicher oder lässt sich in den richtigen Kontext einordnen. Fangen wir also beim Computer selbst an. Wenn man so davor sitzt, sieht man in erster Linie Dinge, die mit seiner Funktion herzlich wenig zu tun haben: Tastatur, Maus und Bildschirm. Diese Geräte braucht der Mensch, um irgendwie mit dem Rechner in Kontakt zu treten – das allgegenwärtige »Brain-Interface« ist ja schließlich noch Science-Fiction. Was in einem Computer rechnet (und nichts anderes tut das Ding, selbst wenn wir Texte schreiben oder im Netz surfen)[ 9 ], ist der Prozessor. 2.1.1 Prozessor
In den meisten PCs steckt heutzutage ein mit Intel x86 kompatibler Prozessor, wobei mittlerweile die ARM-Architektur zunehmende Verbreitung findet. So ein auch CPU (Central Processing Unit) genannter Mikrochip hat im Wesentlichen drei Aufgaben: das Ausführen arithmetisch-logischer Operationen das Lesen und Schreiben von Daten im Arbeitsspeicher das Ausführen von Sprüngen im Programm Die letzte Aufgabe deutet schon an, dass ein Prozessor keine »gottgegebenen« Dinge tut. Vielmehr führt er ein in Maschinencode vorliegendes Programm aus. Wie dieser Maschinencode nun aussieht, bestimmt der Befehlssatz des Prozessors. Mit anderen Worten gibt es nicht den Maschinencode, sondern viele Maschinencodes – so ziemlich für jeden Prozessor einen eigenen. Ausnahmen bilden nur Fabrikate wie die Prozessoren von AMD, die im Wesentlichen Intels x86Befehlscode ausführen. Allerdings ist diese Einschränkung in Bezug auf die Kompatibilität nicht so erheblich, wie sie auf den ersten Blick scheint. Die meisten Hersteller moderner Prozessoren achten nämlich auf Abwärtskompatibilität, um mit den jeweiligen Vorgängermodellen noch kompatibel zu sein. In letzter Konsequenz führte genau dieser Fakt – also die Abwärtskompatibilität der Befehlssätze neuer Prozessoren – zum unglaublichen Erfolg der Intel-Prozessoren. Als klassisches Beispiel bietet sich hier der 16-Bit-Code des 80386Prozessors von Intel an, der auch von aktuellen Many-CoreProzessoren noch unterstützt wird, obwohl diese intern völlig anders aufgebaut sind und demzufolge auch anders arbeiten.
Die meisten Benutzerinnen und Benutzer stellen sich nun vor, dass ihre Programme in eine solche Maschinensprache übersetzt und vom Prozessor ausgeführt werden. Dies ist natürlich nur teilweise richtig: Das vom Prozessor ausgeführte Maschinencode-Programm ist nur eine Folge von Maschinenbefehlen. Damit man nun von mehreren »parallel« laufenden Programmen auf diese lose Folge von Befehlen abstrahieren kann, braucht man zum ersten Mal den Begriff des Betriebssystems – eine vertrauenswürdige Instanz, die die zu verarbeitenden Programme in kleine Häppchen aufteilt und diese dann nacheinander zur Ausführung bringt. Diese Multitasking genannte Vorgehensweise werden wir später noch ausführlich beleuchten; im Augenblick benötigen wir nur das Verständnis dieser für Endbenutzerinnen und -benutzer so wichtigen Aktionen. 2.1.2 Speicher
Bevor wir diesen Gedanken weiterdenken, soll kurz der Speicheraspekt betrachtet werden. Bisher haben wir nämlich nur gesagt, dass der Prozessor irgendwie rechnen und mit seiner Maschinensprache bezüglich dieser Berechnungen und der Flusskontrolle gesteuert werden kann. Fluss bezeichnet hier den Ablauf des Programms. Dieser kann durch bedingte Sprünge variiert und kontrolliert werden. Eine Frage ist aber noch offen: Woher kommen überhaupt die Ausgangswerte für die Berechnungen? Wir haben zwar bei der Beschreibung der Aufgaben eines Prozessors schon den ominösen Punkt »Das Lesen und Schreiben von Daten im Arbeitsspeicher« erwähnt, jedoch wollen wir diesen Fakt nun in den richtigen Zusammenhang setzen.
Die Register des Prozessors
Jeder Prozessor besitzt eine gewisse Anzahl von Registern, auf die im Maschinencode direkt zugegriffen werden kann. Diese Register sind hardwaremäßig auf dem Prozessorchip selbst integriert und können damit ohne Zeitverzug noch im selben Takt angesprochen werden. Der Platz auf dem Prozessor ist jedoch beschränkt und meistens werden einige Register auch für Spezialaufgaben gebraucht: Befehlsregister
Hierin ist die Adresse des nächsten auszuführenden Befehls gespeichert. Sprungbefehle können dieses Register verändern und so die Ausführung des Programms an einer anderen Stelle fortsetzen lassen. Nullregister
Die meisten Prozessorarchitekturen besitzen ein spezielles schreibgeschütztes Register, aus dem man nur die Null lesen kann. Dies ist sehr praktisch, da man so diese wichtige Konstante direkt nutzen kann und nicht erst aus dem Speicher zu laden braucht. Statusregister
Im Statusregister stehen bestimmte Bits für diverse Statusinformationen, beispielsweise dafür, ob das letzte Ergebnis null war oder ob bei der letzten Berechnung ein Über- oder Unterlauf stattgefunden hat. Jedes dieser Register ist entweder 32 Bit groß oder bei aktuellen 64Bit-Prozessoren 64 Bit groß. Der Speicherplatz in den Registern ist also sehr stark begrenzt und höchstens für kleinste Programme ausreichend.
Der Hauptspeicher
Der Großteil des benötigten Platzes wird daher im Hauptspeicher zur Verfügung gestellt. Auch hier gab es früher aufgrund der damals noch üblichen Adressbreite von 32 Bit eine Begrenzung, die dem Hauptspeicher eine maximale Größe von 4 Gigabyte auferlegte. Man greift ja auf die 1 Byte großen Speicherstellen über Adressen zu – und 232 Byte sind gerade 4 Gigabyte. Bei 64-Bit-Betriebssystemen k"onnen dagegen theoretisch bis zu 264 Byte Speicher (entspricht 16 Exa-Byte RAM) adressiert werden. Mit verschiedenen Maschinenbefehlen kann man nun auf diese Adressen zugreifen und die dort gespeicherten Bytes lesen oder schreiben. Ein interessanter Effekt bei der byteweisen Adressierung auf einem 32-Bit-basierten System sind die zustande kommenden Adressen: Beim Lesen ganzer Datenwörter[ 10 ] wird man nämlich nur Vielfache von 4 als Adressen nutzen. Schließlich ist ein Byte 8 Bit lang, und 4 mal 8 Bit sind gerade 32 Bit. Interessanterweise können der Prozessor und damit indirekt auch Programme nicht direkt auf die Festplatte zugreifen. Stattdessen wird der DMA-Controller (Direct Memory Access) so programmiert, dass die betreffenden Datenblöcke in vorher festgelegte Bereiche des Hauptspeichers kopiert werden. Während der DMA-Controller die Daten von der sehr langsamen Festplatte in den im Vergleich zum Prozessor auch nicht gerade schnellen Hauptspeicher kopiert, kann die CPU nun weiterrechnen. Da das eben noch ausgeführte Programm nun vielleicht vor dem Ende des Transfers nicht weiterlaufen kann, wird wahrscheinlich ein anderes Programm ausgeführt. Das Betriebssystem sollte also das nächste abzuarbeitende Programm heraussuchen und zur Ausführung bringen. Ist der Transfer dann abgeschlossen, kann der
Prozessor die Daten von der Platte ganz normal aus dem Hauptspeicher lesen. Im Übrigen wird so auch mit ausführbaren Programmen verfahren, die vor der Ausführung intern natürlich ebenfalls in den Hauptspeicher kopiert werden. Das Befehlsregister referenziert also den nächsten auszuführenden Befehl, indem es dessen Hauptspeicheradresse speichert. Caches
Auch der Hauptspeicher ist also langsamer als der Prozessor. Konkret bedeutet das, dass der Takt ein anderer ist. In jedem Takt kann ein Arbeitsschritt erledigt werden, der beim Prozessor im Übrigen nicht unbedingt mit einem abgearbeiteten Befehl gleichzusetzen ist, sondern viel eher mit einem Arbeitsschritt beim Ausführen eines Befehls. (Tatsächlich nutzen fast alle aktuellen Prozessoren intern Pipelines oder andere Formen der Parallelisierung, um in einem Takt mehr als einen Befehl ausführen und so im Idealfall alle Komponenten des Chips auslasten zu können.) Um nun die Zugriffszeit auf häufig benutzte Datensätze und Variablen aus dem Hauptspeicher zu verkürzen, hat man Pufferspeicher, sogenannte Caches, eingeführt. Diese Caches befinden sich entweder direkt auf dem Prozessor-Chip (L1-Cache) oder »direkt daneben«. Caches können je nach Bauart zwischen ein paar Kilobytes und wenigen Megabytes groß sein und werden bei meist vollem oder halbem Prozessortakt angesprochen. Die aus dem Hauptspeicher stammenden gepufferten Werte können so für den Prozessor transparent zwischengespeichert werden. Dieser nämlich greift weiterhin auf Adressen im Hauptspeicher zu – ob ein Cache dabei den Zugriff beschleunigt
oder nicht, ist für den Prozessor nicht ersichtlich und auch unerheblich. Zusammenfassung: Die Speicherhierarchie
Der Computer besitzt also eine Speicherhierarchie, die absteigend mehr Speicherplatz bei längeren Zugriffszeiten bietet: 1. Die Register des Prozessors
Die Register bieten einen direkten Zugriff bei vollem Prozessortakt. Neben speziellen Registern für festgelegte Aufgaben gibt es auch solche, die frei für Programmiererinnen und Programmierer benutzbar sind. 2. Der L1-Cache des Prozessors
Der Level-1-Cache sitzt direkt auf dem Prozessor und ist in der Regel 8 bis 256 Kilobyte groß. 3. Der L2-Cache
Je nach Modell kann der Level-2-Cache entweder auf dem Prozessor (on-die) oder direkt neben dem Prozessor auf einer anderen Platine untergebracht sein. Der L2-Cache ist normalerweise zwischen 512 und 2048 Kilobyte groß.
Abbildung 2.1 Die Speicherpyramziele
4. Der L3-Cache
Falls der L2-Cache auf dem Chip sitzt, kann durch einen zusätzlichen externen Level-3-Cache noch eine weitere Beschleunigung erreicht werden. 5. Der Hauptspeicher
Auf das RAM kann der Prozessor nur mit einer gewissen Zeitverzögerung zugreifen. Dafür kann dieser Speicher bei einer 32-Bit-Architektur bis zu 4 Gigabyte groß werden. 6. Die Festplatte oder anderer Hintergrundspeicher
Da Daten vom Hintergrundspeicher oft erst aufwendig gelesen werden müssen, bevor sie schließlich in den Hauptspeicher übertragen werden können, sind diese Speicher i.d.R. am langsamsten. Aber von einigen wenigen bis einigen Tausend Gigabyte sind hier die Speicherkapazitäten am größten. Zudem besitzen zum Beispiel Festplatten oft noch eigene Caches im jeweiligen Controller, um auch selbst den Zugriff durch Zwischenspeicherung oder vorausschauendes Lesen etwas beschleunigen zu können. 7. Fehlende Daten
Es kann natürlich auch vorkommen, dass der Prozessor beziehungsweise ein Programm auf Daten wartet, die erst noch eingegeben werden müssen. Ob dies über die Tastatur, die Maus oder einen Scanner passiert, soll hier nicht weiter interessieren. Ein L1-Cache bietet also die kürzesten Zugriffszeiten und den geringsten Platz. Weiter unten bietet in der Regel die Festplatte den meisten Platz an, ist aber im Vergleich zum Cache oder auch zum Hauptspeicher extrem langsam. 2.1.3 Fairness und Schutz
Führen wir also nun unseren ersten Gedanken bezüglich der »parallelen« Ausführung mehrerer Programme logisch weiter. Wenn der Ablauf unterschiedlicher Programme quasiparallel, also abwechselnd in jeweils sehr kurzen Zeitabschnitten erfolgen soll, muss eine gewisse Fairness gewährleistet werden. Rein intuitiv denkt man da eigentlich sofort an zwei benötigte Zusicherungen: 1. Gerechte Zeiteinteilung
Selbstverständlich darf jedes Programm nur einen kurzen Zeitabschnitt lang auf dem Prozessor rechnen. Mit anderen Worten: Es muss eine Möglichkeit für das Betriebssystem geben, ein laufendes Programm zu unterbrechen. Dies aber ist so nicht ohne Weiteres möglich: Schließlich läuft gerade das Programm und nicht das Betriebssystem. Es bleiben also zwei Möglichkeiten, um doch noch für das Scheduling, also das Umschalten zwischen zwei Benutzerprogrammen, zu sorgen: Entweder geben die Programme freiwillig wieder Rechenzeit ab oder der Prozessor wird nach einer gewissen Zeitspanne in seiner aktuellen Berechnung unterbrochen. Unterbrochen werden kann ein Prozessor dabei durch Interrupts. Über diese »Unterbrechungen« signalisieren zum Beispiel viele I/O-Geräte, dass sie angeforderte Daten nun bereitgestellt haben, oder ein Zeitgeber signalisiert den Ablauf einer bestimmten Zeitspanne. Wird solch ein Interrupt nun aktiv, unterbricht der Prozessor seine Ausführung und startet eine für diesen Interrupt spezielle Interrupt Service Routine. Diese Routine ist immer ein Teil des Betriebssystems und könnte nun zum Beispiel entscheiden, welches andere Programm als Nächstes laufen soll. Terminologische Feinheiten
Eigentlich kennt der Prozessor Interrupts und Exceptions. Die Literatur unterscheidet beide Begriffe gewöhnlich unter dem Aspekt, dass Interrupts asynchron auftreten und von anderen aktiven Elementen des Systems geschickt werden, während Exceptions schlichte Ausnahmen im Sinne eines aufgetretenen Fehlers sind und damit immer synchron auftreten. Für uns hier ist dieser feine Unterschied jedoch nicht relevant, daher möchten wir im Folgenden ausschließlich von Interrupts sprechen – auch wenn es sich bei manchen Ereignissen eigentlich um Exceptions handelt.
2. Speicherschutz
Die einzelnen Programme sollen sich natürlich nicht gegenseitig beeinflussen. Das heißt vor allem, dass die Speicherbereiche der einzelnen Programme voreinander geschützt werden. Man erreicht dies durch das im Folgenden noch näher erläuterte Prinzip des virtuellen Speichers: Dies bedeutet für die Programme, dass sie nicht direkt auf die physischen Adressen des RAMs zugreifen können. Die Programme merken davon aber nichts – sie haben in ihren Augen den gesamten Speicherbereich für sich allein. In einer speziellen Hardwareeinheit, der MMU (Memory Management Unit), wird dann die virtuelle Adresse bei einem Speicherzugriff in die physische übersetzt. Dieses Konzept hat auch den nützlichen Nebeneffekt, dass bei einer hohen Speicherauslastung – also wenn die gestarteten Programme zusammen mehr Speicher benötigen, als der PC RAM besitzt – einige Speicherbereiche auf die Festplatte ausgelagert werden können, ohne dass die betroffenen Programme davon etwas merken. Greifen diese dann auf die ausgelagerten Daten zu, wird der betroffene Speicherbereich
von der Festplatte wieder ins RAM kopiert und die MMU aktualisiert. Wird das vor dieser Aktion unterbrochene Programm des Benutzers fortgesetzt, kann es wieder ganz normal auf die Daten des angeforderten Speicherbereichs zugreifen. Außer dem Schutz des Speichers durch das Konzept des Virtual Memory gibt es noch die unter anderem vom x86-Standard unterstützten Berechtigungslevel (auch Ringe genannt). Diese vier Level oder Ringe schränken dabei den jeweils verfügbaren Befehlssatz für alle Programme ein, die im jeweiligen Ring beziehungsweise Berechtigungslevel laufen. Die gängigen Betriebssysteme wie Linux oder Windows nutzen dabei jeweils nur zwei der vier bei x86 verfügbaren Ringe: Im Ring 0 wird das Betriebssystem samt Treibern ausgeführt, während alle Benutzerprogramme im eingeschränktesten Ring 3 ablaufen. So schützt man das Betriebssystem vor den Anwenderprogrammen, während diese selbst durch virtuelle Adressräume voneinander getrennt sind. 2.1.4 Programmierung
So viel zu einer kurzen Einführung in den Prozessor und dessen Implikationen für unsere Systeme. Der nächste wichtige Punkt ist die Programmierung: Wie kann man einem Prozessor sagen, was er tun soll? Bisher haben wir nur über Maschinencode gesprochen, also über Befehle, die der Prozessor direkt versteht. Die binäre Codierung dieser Befehle wird dann mehr oder weniger direkt benutzt, um die Spannungswerte auf den entsprechenden Leitungen zu setzen. Assembler
Nun möchte aber niemand mit Kolonnen von Nullen und Einsen hantieren, nicht einmal in der Betriebssystemprogrammierung. Aus diesem Grund wurde bereits in den Anfangsjahren der Informatik die Assembler-Sprache entworfen, in deren reinster Form ein Maschinenbefehl durch eine für einen Menschen lesbare Abkürzung – ein Mnemonic – repräsentiert wird. Neuere Assembler, also Programme, die einen in einer AssemblerSprache geschriebenen Code in eine Maschinensprache übersetzen, bieten zusätzlich zu dieser 1:1-Übersetzung noch Makros als Zusammenfassung häufig benötigter Befehlskombinationen zur Vereinfachung an. Im Rahmen dieser 1:1-Zuordnung von Assembler zu Maschinencode ist natürlich auch die umgekehrte Richtung möglich, was man dann Disassemblieren nennt. Betrachten wir das folgende Beispielprogramm, das auf einem MIPS-2000- System[ 11 ] den Text »Hello World!« ausgeben würde: str: main:
.data .asciiz "Hello World!\n" .text li $v0, 4 la $a0, str syscall
# # # # # # # #
Datensegment
String ablegen
Codesegment
4 = Print_string
Adresse des
Strings übergeben
Systemfunktion
aufrufen
li $v0, 10 syscall
# 10 = Quit
# Programm beenden
Listing 2.1 »Hello World«-Beispielcode in MIPS-Assembler
Zunächst einmal legen wir nämlich die Zeichenfolge Hello World!, gefolgt von einem Zeichen für den Zeilenumbruch (\n), im Hauptspeicher ab und bezeichnen diese Stelle für den späteren Gebrauch im Programm kurz mit str. Im Hauptprogramm (gekennzeichnet durch das Label main) laden wir eine bestimmte
Nummer in ein Register des Prozessors und die Adresse der Zeichenkette in ein anderes Register. Anschließend lösen wir durch den syscall-Befehl einen Interrupt aus, bei dessen Bearbeitung das Betriebssystem die im Register $v0 angegebene Nummer auswertet. Diese Nummer gibt nun an, was das Betriebssystem weiter tun soll: In unserem Fall soll es den Text auf dem Bildschirm ausgeben. Dazu holt es sich noch die Adresse der Zeichenkette aus dem zweiten Register und erledigt seine Arbeit. Zurück im Programm wollen wir dieses jetzt beenden, wozu die Nummer 10, gefolgt vom bekannten Interrupt, genügt. Zugriff auf das Betriebssystem
In diesem Beispiel haben wir nun schon das große Mysterium gesehen: den Zugriff auf das Betriebssystem, den Kernel. Das Beispielprogramm tut nichts weiter, als diverse Register mit Werten zu füllen und ihm erlaubte Interrupts aufzurufen. Da Benutzerprogramme in einem eingeschränkten Berechtigungslevel laufen, können sie nicht wahllos alle Interrupts aufrufen. Das Betriebssystem erledigt in diesem Beispiel die ganze Arbeit: Der Text wird aus dem Speicher ausgelesen, auf dem Bildschirm ausgegeben und das Programm wird schließlich beendet. Diese Beendigung findet, wie leicht zu erkennen ist, nicht auf der Ebene des Prozessors statt (es gibt auch einen speziellen Befehl, um den Prozessor beim Herunterfahren des Systems richtig anzuhalten), sondern es wird nur eine Nachricht an das Betriebssystem gesendet. Das System wusste unser Programm irgendwie zu starten und es wird sich jetzt wohl auch um dessen Ende kümmern können. Aber betrachten wir zunächst die definierten Einstiegspunkte in den Kernel: die Syscalls. In den meisten Büchern über Linux finden
Sie bei der Erläuterung des Kernels ein Bild wie das folgende:
Abbildung 2.2 Ein nicht ganz korrektes Schema
Ein solches Bild soll verdeutlichen, dass Benutzerprogramme nicht direkt auf die Hardware zugreifen, sondern den Kernel für diese Aufgabe benutzen. Diese Darstellung ist aber nicht vollkommen korrekt und lässt ein falsches Bild entstehen. Im Assembler-Beispiel haben wir gesehen, dass ein Benutzerprogramm sehr wohl auf die Hardware zugreifen kann: Es kann zum Beispiel Daten aus dem Hauptspeicher in Register laden, alle möglichen arithmetischen und logischen Operationen ausführen sowie bestimmte Interrupts auslösen. Außerdem ist in der obigen Grafik der Zugriff auf den Kernel nicht visualisiert; man könnte also annehmen, dass dieser nach Belieben erfolgen kann. Jedoch ist das genaue Gegenteil der Fall.
Abbildung 2.3 So sollte es sein.
In Abbildung 2.3 wird schon eher deutlich, dass ein Benutzerprogramm nur über ausgewiesene Schnittstellen mit dem Kernel kommunizieren kann. Diese ausgewiesenen Systemaufrufe (engl. system calls, daher auch die Bezeichnung Syscalls) stellen einem Programm die Funktionalität des Betriebssystems zur Verfügung. So kann man über Syscalls zum Beispiel, wie Sie gesehen haben, einen Text auf den Bildschirm schreiben oder das aktuelle Programm beenden. Entsprechend kann man natürlich Eingaben der Tastatur lesen und neue Programme starten. Außerdem kann man auf Dateien zugreifen, externe Geräte ansteuern oder die Rechte des Benutzers bzw. der Benutzerin überprüfen. Linux kennt einige hundert Syscalls, die alle in der Datei unistd_32.h (bzw. unistd_64.h) Ihrer Kernel-Sources verzeichnet sind. Wenn Sie sich die Datei anschauen möchten, sollten Sie in Ihrer Distribution die Kernel-Header-Dateien installieren. Unter Ubuntu heißt das Paket bspw. linux-headers-VERSION, die Datei findet sich dann im Dateisystem unter /usr/src/linux-headers-VERSIONgeneric/arch/x86/include/generated/uapi/asm/unistd_32.h.
#define #define #define #define #define #define #define #define
__NR_exit __NR_fork __NR_read __NR_write __NR_open __NR_close __NR_waitpid __NR_creat
1
2
3
4
5
6
7
8
Listing 2.2 Auszug aus der unistd_32.h von Linux-Kernel 5.x
Dem exit-Call ist in diesem Fall die Nummer 1 und dem write-Call die Nummer 4 zugeordnet, also etwas andere Nummern als in unserem Beispiel für das MIPS-System. Auch muss unter Linux/x86 ein Syscall anders initialisiert werden als in unserem Beispiel.[ 12 ] Das Prinzip ist jedoch gleich: Wir bereiten die Datenstruktur für den Syscall vor und bitten das Betriebssystem anschließend per Interrupt, unseren Wunsch zu bearbeiten. Für ein Benutzerprogramm sind Syscalls die einzige Möglichkeit, direkt eine bestimmte Funktionalität des Kernels zu nutzen.
Natürlich lässt unsere Definition der Syscalls noch viele Fragen offen. Bisher wissen wir ja nur, dass wir die Daten irgendwie vorbereiten müssen, damit das Betriebssystem nach einem Interrupt diesen Systemaufruf verarbeiten kann. Was uns noch fehlt, ist die Verbindung zu den verschiedenen Hochsprachen, in denen ja fast alle Programme geschrieben werden. Hochsprachen
Im Folgenden müssen wir zunächst klären, was eine Hochsprache überhaupt ist. Als Hochsprache bezeichnet man eine abstrakte höhere Programmiersprache, die es erlaubt, Programme problemorientierter und unabhängig von der Prozessorarchitektur
zu schreiben. Bekannte und wichtige Hochsprachen sind zum Beispiel C/C++, Java oder auch PHP. Unser etwas kompliziert anmutendes MIPS-Beispiel sieht in C auch gleich viel einfacher aus: #include
main()
{
printf("Hello, World!\n");
}
Listing 2.3 »Hello, World« in C
In der ersten Zeile binden wir eine Datei ein, in der der einzige Befehl in unserem Programm definiert wird: printf(). Dieser Befehl gibt nun wie zu erwarten einen Text auf dem Bildschirm aus, in unserem Fall das bekannte »Hello, World!«. Auch wenn dieses Beispiel schon einfacher zu lesen ist als der Assembler-Code, zeigt es doch noch nicht alle Möglichkeiten und Verbesserungen, die eine Hochsprache bietet. Abgesehen davon, dass Hochsprachen leicht zu lesen und zu erlernen sind, bieten sie nämlich komplexe Daten- und Kontrollstrukturen, die es so in Assembler nicht gibt. Außerdem ist eine automatische Syntax- und Typüberprüfung möglich. Dumm ist nur, dass der Prozessor solch einen schön geschriebenen Text nicht versteht. Die Textdateien mit dem Quellcode, die man im Allgemeinen auch als Source bezeichnet, müssen erst in Assembler beziehungsweise gleich in Maschinensprache übersetzt werden.[ 13 ] Eine solche Übersetzung (auch Kompilierung genannt) wird von einem Compiler vorgenommen. Wird ein Programm jedoch nicht nur einmal übersetzt, sondern während der Analyse der Quelldatei gleich Schritt für Schritt ausgeführt, so spricht man von interpretierten Sprachen und nennt das interpretierende Programm einen Interpreter. Die meisten Sprachen sind entweder pure
Compiler- oder pure Interpreter-Sprachen (auch Skriptsprachen genannt). Eine interessante Ausnahme von dieser Regel ist Java. Diese Sprache wurde von Sun Microsystems entwickelt, um möglichst portabel und objektorientiert Anwendungen schreiben zu können. Ganz davon abgesehen, dass jede Sprache portabel ist, sofern ein entsprechender Compiler/Interpreter und alle benötigten Bibliotheken – das sind Sammlungen von häufig benutztem Code, beispielsweise Funktionen, die den Zugriff auf eine Datenbank abstrahieren – auf der Zielplattform vorhanden sind, wollte Sun dies mit dem folgenden Konzept erreichen: Ein fertig geschriebenes Java-Programm wird zuerst von einem Compiler in einen Bytecode übersetzt, der schließlich zur Laufzeit interpretiert wird. Dieser Bytecode ist eine Art maschinenunabhängige Maschinensprache. Mehr zur Programmierung unter Unix finden Sie in Kapitel 12. Für unser kleines C-Beispiel reicht dagegen der einmalige Aufruf des GNU-C-Compilers, des gcc, aus: $ gcc -o hello hello.c
$ ./hello
Hello, World!
$
Listing 2.4 Das Beispiel übersetzen und ausführen
In diesem Beispiel wird die Quelldatei hello.c mit unserem kleinen Beispielprogramm vom gcc in die ausführbare Datei hello übersetzt, die wir anschließend mit dem gewünschten Ergebnis ausführen. In diesem Beispiel haben Sie auch zum ersten Mal die Shell gesehen. Diese interaktive Kommandozeile wirkt auf viele Leute, die sich zum ersten Mal mit Unix auseinandersetzen, recht anachronistisch und überhaupt nicht komfortabel. Man möchte nur klicken müssen und am liebsten alles bunt haben. Sie werden jedoch spätestens
nach unserem Shell-Kapitel dieses wertvolle und höchst effiziente Werkzeug nicht mehr missen wollen. Die Datei hello ist zwar eine ausführbare Datei, enthält aber keinen reinen Maschinencode. Vielmehr wird unter Linux/BSD das ELFFormat für ausführbare Dateien genutzt. In diesem Format ist zum Beispiel noch angegeben, welche Bibliotheken benötigt oder welche Variablen im Speicher angelegt werden müssen. (Auch wenn Sie in Assembler programmieren, wird eine ausführbare Datei in einem solchen Format erzeugt – das Betriebssystem könnte sie sonst nicht starten.) Doch zurück zu unseren Syscalls, die wir in den letzten Abschnitten etwas aus den Augen verloren haben. Die Frage, die wir uns zu Beginn stellten, war ja, ob und wie wir die Syscalls in unseren Hochsprachen nutzen können. Unter C ist die Sache einfach: Die Standardbibliothek (libc) enthält entsprechende Funktionsdefinitionen. Nach außen hin kann man über die Datei unistd.h die von der Bibliothek exportierten Funktionssymbole einbinden und Syscalls auf diese Weise direkt nutzen. Intern werden die Syscalls wieder in Assembler geschrieben. Dies geschieht teils durch vollständig in Assembler geschriebene Quelldateien und teils auch durch Inline-Assembler. Die Programmiersprache C erlaubt es nämlich, zwischen den Anweisungen in der Hochsprache auch Assembler-Direktiven zu verwenden, die dann natürlich speziell gekennzeichnet werden. Würde man das Beispielprogramm nicht mit printf schreiben, einem Befehl direkt aus dem C-Standard, sondern direkt mit dem Linux-Syscall write, so sähe es wie folgt aus: #include
int main() {
write(0, "Hello, World!\n", 13);
}
Listing 2.5 Das C-Beispiel mit dem write-Syscall
Hier nutzen wir den Syscall direkt statt indirekt wie über printf. Der Aufruf sieht auch schon etwas komplizierter aus, da mehr Argumente benötigt werden. Doch diese steigern nur die Flexibilität des Syscalls, der auch zum Schreiben in Dateien oder zum Senden von Daten über eine Netzwerkverbindung genutzt werden kann – wohlgemerkt: Im Endeffekt sind dies alles Aufgaben für den Kernel. In welche Datei beziehungsweise auf welches Gerät geschrieben werden soll, gibt das erste Argument an. Dieser Deskriptor ist in unserem Fall die standardmäßig mit dem Wert »0« belegte normale Ausgabe: der Bildschirm. Danach folgen der zu schreibende Text sowie die letztendlich davon wirklich zu schreibende Anzahl Zeichen (eigentlich Bytes, aber ein Zeichen entspricht normalerweise einem Byte). 2.1.5 Benutzung
Nachdem wir bisher betrachtet haben, welche Implikationen sich aus der Hardware für das Betriebssystem ergeben, wollen wir im Folgenden die Eigenschaften des Systems aus Benutzersicht erläutern. Dazu betrachten wir zuerst ein beliebiges Betriebssystem beim Start. Der Bootvorgang
Wenn man den PC anschaltet, bootet nach einer kurzen Initialisierung des BIOS das Betriebssystem. Für die Benutzer äußert sich dieser Vorgang vor allem in einer kurzen Wartezeit, bis sie sich am System anmelden können. In dieser Zeit werden alle Dienste initialisiert, die das System erbringen soll.
Bei Arbeitsplatzrechnern gehört dazu in 90 % der Fälle eine grafische Oberfläche. Bei einer Vollinstallation eines Linux-Systems kann dazu auch schon einmal ein Webserver- oder Fileserver-Dienst gehören. Werden solche komplexen Dienste beim Booten gestartet, dauert ein Systemboot natürlich länger als bei puren DesktopSystemen – insofern lässt sich ein Windows 10 Home nicht mit einer Unix-Workstation vergleichen. Für das System selbst heißt das, dass alle für die Arbeit benötigten Datenstrukturen zu initialisieren sind. Am Ende des Bootvorgangs wird den Benutzern eine Schnittstelle angeboten, mit der sie arbeiten können. Im laufenden Betrieb
Im laufenden Betrieb möchten Benutzerinnen und Benutzer ihre Programme starten, auf ein Netzwerk zugreifen oder spezielle Hardware wie Webcams nutzen. Das Betriebssystem hat nun die Aufgabe, diese Betriebsmittel zu verwalten. Der Zwiespalt ist dabei, dass die Benutzer so etwas nicht interessiert – schließlich sollen die Programme ausgeführt und auch ihre restlichen Wünsche möglichst mit der vollen Leistung des Systems erfüllt werden. Würde der Kernel also zur Erfüllung dieser Aufgaben den halben Speicher oder 50 % der Rechenzeit benötigen, könnte er diesen indirekten Anforderungen nicht gerecht werden. Tatsächlich stellt es für jeden Betriebssystemprogrammierer die größte Herausforderung dar, den eigenen Ressourcenverbrauch möglichst gering zu halten und trotzdem alle Wünsche zu erfüllen. Ebenfalls zu diesem Themenkreis gehört die Korrektheit des Systems. Es soll seine Aufgabe nach Plan erfüllen – grundlose Abstürze, vollständige Systemausfälle beim Ausfall einzelner kleiner
Komponenten oder nicht vorhersagbares Verhalten sind nicht zu akzeptieren. Daher wollen wir die Korrektheit im Folgenden als gegeben annehmen, auch wenn sie in der Realität nicht unbedingt selbstverständlich ist. Das Herunterfahren
Das Herunterfahren dient zum Verlassen des Systems in einem korrekten Zustand, damit die Systemintegrität beim nächsten Start gewahrt bleibt. Vor allem beim Dateisystem zeigt sich die Wichtigkeit eines solchen Vorgehens: Puffer und Caches erhöhen die Performance beim Zugriff auf die Platte extrem, dreht man jedoch plötzlich den Strom ab, sind alle gepufferten und noch nicht auf die Platte zurückgeschriebenen Daten weg. Dabei wird das Dateisystem mit ziemlicher Sicherheit in einem inkonsistenten Zustand zurückgelassen, so dass es beim nächsten Zugriff sehr wahrscheinlich zu Problemen kommen wird. Aber auch den Applikationen muss eine gewisse Zeit zum Beenden eingeräumt werden. Vielleicht sind temporäre Daten zu sichern oder andere Arbeiten noch korrekt zu beenden. Das Betriebssystem muss also eine Möglichkeit haben, den Anwendungen zu sagen: Jetzt beende dich bitte selbst – oder ich tue es.
2.2 Aufgaben eines Betriebssystems Zusammengefasst verwaltet ein Betriebssystem die Betriebsmittel eines Computers – also Rechenzeit, Speicher oder I/O-Geräte – und ermöglicht dem Benutzer bzw. der Benutzerin das Ausführen von Programmen. Zwei weitere, jedoch eher indirekte Aufgaben, die wir bisher noch nicht besprochen haben, sind Abstraktion und Virtualisierung. 2.2.1 Abstraktion
Abstraktion haben wir von Anfang an als selbstverständlich betrachtet. Niemand möchte Maschinencode schreiben oder sich direkt mit dem Prozessor auseinandersetzen. Man möchte viel lieber auf ein hübsches Symbol auf dem Desktop klicken, um ein bestimmtes Programm zu starten. So weit, so gut. Ein weiteres gutes Beispiel sind die bereits vorgestellten Syscalls. Sie abstrahieren die komplexen Fähigkeiten des Betriebssystems in wenige, konkret gegebene Funktionsaufrufe. Eine ebenfalls sofort einsichtige Anwendung des Abstraktionsprinzips gibt es beim Dateisystem. Eine Festplatte wird schließlich über ihre geometrischen Eigenschaften adressiert, also über ihre Zylinder, Köpfe und Sektoren. Teilweise wird aber auch schon vom Controller davon abstrahiert, sodass das Betriebssystem es nur noch mit abstrakten Blocknummern zu tun hat. Für den Benutzer bzw. die Benutzerin werden diese unhandlichen Eigenschaften jedoch auf Dateien und Verzeichnisse abgebildet und diese werden sogar mit diversen Rechten und anderen Eigenschaften versehen. Die Syscalls dienen nun dazu, diese abstrahierte Funktionalität aus Programmen heraus nutzen zu können.
Unter Unix-Systemen wie zum Beispiel Linux und BSD werden verwaltete Geräte auf spezielle Dateien im /dev-Verzeichnis abgebildet. Das hat den Vorteil, dass man auch für die Benutzung von I/O-Geräten Rechte vergeben kann und sich die Handhabung nicht wesentlich von der des restlichen Systems unterscheidet. 2.2.2 Virtualisierung
Virtualisierung ist eine weitere wichtige Aufgabe des Betriebssystems. Der Ausdruck bezeichnet in der Informatik eine Ressourcenteilung möglichst ohne ungünstige Nebenwirkung für die Benutzerinnen und Benutzer. Für diese beziehungsweise für ihr Programm sieht es nämlich so aus, als stünde die Ressource ausschließlich ihm zur Verfügung. Virtuelle Prozessoren und Multitasking
Das erste Beispiel für Virtualisierung kennen wir bereits: die Virtualisierung des Prozessors. Das bereits erwähnte pr"aemptive Multitasking ermöglicht das quasiparallele Ausführen von mehreren Programmen. Dazu wird, wie wir bereits festgestellt haben, die verfügbare Prozessorzeit in viele i.d.R. gleich große Zeitscheiben (Timeslices) unterteilt, die vom Betriebssystem nun einzelnen Programmen zugewiesen werden. Im Folgenden kann dann jedes Programm rechnen, als hätte es den gesamten Prozessor für sich allein. Es merkt nichts von den Unterbrechungen und von anderen parallel laufenden Programmen.
Abbildung 2.4 Multitasking
Um dies umzusetzen, startet der Kernel einen Timer, um nach dem Ablauf der Zeitscheibe des Programmes wieder aufgeweckt zu werden. Ist der Timer angelaufen, wird über einen Interrupt der Kernel wieder aufgeweckt – vorher werden jedoch alle Prozessorregister gesichert, sodass das Programm später ohne Probleme wieder fortgesetzt werden kann. Virtueller Speicher
Wie schon der Name dieses ebenfalls bereits erwähnten Prinzips sagt, handelt es sich hier um eine Virtualisierung des Speichers. Jedes Programm hat den Eindruck, dass ihm der gesamte Hauptspeicher zur Verfügung steht. Alle Adressen können benutzt werden, ohne dass ein Programm von anderen, zur selben Zeit laufenden Programmen etwas merken würde. Die anderen Programme haben natürlich ebenfalls ihren eigenen virtuellen Speicher. Greift ein Programm auf eine (virtuelle) Adresse zu, wird diese von der MMU in die entsprechende reale Adresse im Hauptspeicher übersetzt. Das Setup, also die Verwaltung der MMU und des Hauptspeichers, übernimmt dabei wieder das Betriebssystem. Dieses wird auch durch einen Interrupt informiert, falls ein
Speicherbereich, auf den ein Programm gern zugreifen möchte, auf die Festplatte ausgelagert wurde. In der Behandlungsroutine des Interrupts kann das Betriebssystem diesen Speicherblock nun wieder in den Hauptspeicher kopieren und die fehlgeschlagene Anweisung des unterbrochenen Programms noch einmal wiederholen – diesmal ist die betreffende virtuelle Adresse im Hauptspeicher eingelagert und die Adressübersetzung der MMU schlägt nicht mehr fehl. Diese vom Betriebssystem zu erledigende Verwaltungsarbeit für den virtuellen Speicher zeigt auch die Relevanz eines Syscalls auf, der für ein Programm neuen Speicher anfordert. Der meist malloc() genannte Funktionsaufruf sorgt dafür, dass die Memory Management Unit entsprechend initialisiert wird. Dann kann dem Programm die virtuelle Adresse zurückgegeben werden, unter der der angeforderte Speicherbereich ansprechbar ist. Griffe ein Programm auf eine virtuelle Adresse zu, die noch nicht initialisiert ist, würde die MMU natürlich wieder das Betriebssystem über einen Interrupt informieren. Das Betriebssystem s"ahe nun nach, ob der betreffende Speicherbereich vielleicht ausgelagert wäre – aber das ist er nicht. Es liegt also ein klassischer Speicherzugriffsfehler vor, bei dem ein Programm normalerweise beendet wird. Falls so etwas auftritt, liegt meist ein etwas komplizierterer Programmierfehler vor. #include
int main() {
char* puffer; // Variable deklarieren
puffer = malloc(4096); // 4 KB = 4096 Bytes anfordern
// Nun kann der Speicherbereich benutzt werden
...
free(puffer); // Speicherbereich wieder freigeben
// Jeder Zugriff auf den Speicherbereich der
// Variablen 'puffer', der nach dem Freigeben
// erfolgt, führt zu einem Speicherzugriffsfehler.
return 0; // Programm beenden
}
Listing 2.6 Anfordern von Hauptspeicher mit malloc()
Das obige Beispiel in der Programmiersprache C zeigt sehr deutlich, wie sich der Programmierer bei dieser Sprache selbst um die Verwaltung des Speichers kümmern kann. Der Speicher wird nicht nur per malloc() angefordert, sondern muss auch mit einem Aufruf von free() wieder freigegeben werden. Unter Linux sind malloc() und free() keine Syscalls, sondern »nur« Funktionen der Standard-C-Library. Der zugehörige Syscall, über den der Kernel Speicher reserviert oder freigibt, heißt brk(). Laut Manpage sollte brk() aber nicht direkt verwendet werden, stattdessen sind malloc() und free() zu benutzen. Andere Programmiersprachen verstecken diese Syscalls teilweise vor dem Programmierer. Damit ist das Programmieren zwar einfacher, aber nicht unbedingt flexibler. In Java zum Beispiel sieht die Anforderung von neuem Speicher ganz anders aus, auch wenn intern natürlich dieselben Syscalls genutzt werden. Neue Objekte werden bei solchen Sprachen mit einem Aufruf von new angelegt. Natürlich reserviert dieser Aufruf auch den für das Objekt nötigen Speicherplatz, allerdings ohne dass sich der Programmierer näher damit auseinandersetzen muss, wie viel das ist. Einen free()-ähnlichen Aufruf sucht man dagegen vergeblich. Nicht mehr benutzter Speicher wird nämlich von der das JavaProgramm ausführenden virtuellen Maschine, also dem Programm, das den maschinenunabhängigen Bytecode interpretiert, durch eine Garbage Collection wieder freigegeben: Ein »Müllsammler« durchsucht den gesamten Speicherbereich der Applikation nach
nicht mehr genutzten Referenzen und löscht diese. Diese Garbage Collection war früher auf etwas langsameren Systemen die Ursache dafür, dass bei etwas umfangreicheren Java-Programmen das System in regelmäßigen Abständen stehen blieb. Mittlerweile sind die Systeme aber so leistungsfähig und die Algorithmen so ausgereift, dass dieser Performancenachteil nicht mehr ins Gewicht fällt. 2.2.3 Ressourcenverwaltung
Betrachtet man die letzten Beispiele, so leuchtet auch die Aufgabe der Ressourcenverwaltung ein. Der Begriff Ressource wird dabei im weitesten Sinne verstanden: Der Prozessor zählt nämlich genauso als Ressource wie die durch ihn realisierte Rechenzeit. Zu den verwalteten Ressourcen gehören also darüber hinaus: Prozessor (bzw. Rechenleistung und Timer) Speicher (bzw. RAM und Festplatte) Maus Bildschirm Drucker Netzwerkkarten Warum und inwiefern eine solche Verwaltung nötig ist, zeigt das klassische Druckerbeispiel: Stellen Sie sich vor, zwei Programme versuchten, parallel zu drucken. Hätten beide direkten Zugriff auf den Drucker, wären wahrscheinlich beide Ausgaben auf einem Ausdruck gemischt – das genaue Gegenteil von dem, was erreicht werden sollte. Stattdessen wird das Betriebssystem oder (wie im Falle von Linux und BSD) ein spezieller Dienst die Druckwünsche entgegennehmen und erst einmal auf der Festplatte
zwischenspeichern. Dieser Dienst könnte dann exklusiv auf den Drucker zugreifen und einen Auftrag nach dem anderen abarbeiten, ohne dass es zu Problemen und Konflikten kommt.
2.3 Prozesse, Tasks und Threads Nachdem wir nun die Grundlagen geklärt haben, wollen wir auf die interessanten Einzelheiten zu sprechen kommen. Wir werden die Begriffe Prozess, Thread und Task sowie deren »Lebenszyklen« im System analysieren und dabei auch einen kurzen Blick auf das Scheduling werfen. Ebenfalls interessant wird ein Abschnitt über die Implementierung dieser Strukturen im Kernel sein. Was in diesem Kapitel zum Kernel über Prozesse fehlt – die Administration und die Userspace-Sicht – können Sie in Kapitel nachlesen. Im aktuellen Kernel-Kontext interessieren uns eher das Wesen und die Funktion als die konkrete Bedienung, die eigentlich auch nichts mehr mit dem Kernel zu tun hat – sie findet ja im Userspace statt. (Wörtlich übersetzt heißt das Wort »Raum des Benutzers«, meint also den eingeschränkten Ring 3, in dem alle Programme ausgeführt werden.) Wenn Sie jetzt den Einspruch wagen, dass die zur Administration der Prozesse benutzten Programme nun auch wieder über Syscalls auf den Kernel zugreifen und so ihre Aktionen erst ausführen können, haben Sie verstanden, worum es geht. 2.3.1 Definitionen
Beginnen wir also mit den Definitionen. Anfangs haben wir bereits die aus der Erwartung der Benutzerinnen und Benutzer resultierende Notwendigkeit des Multitasking-Betriebs erläutert. Sie möchten nun einmal mehrere Programme gleichzeitig ausführen können, die Zeiten von MS-DOS und des Singletaskings sind schließlich vorbei. Die Programme selbst liegen dabei als Dateien irgendwo auf der Festplatte.
Prozess
Wird ein Programm nun ausgeführt, spricht man von einem Prozess. Dabei hält das Betriebssystem natürlich noch einen ganzen Kontext weiterer Daten vor: die ID des ausführenden Benutzers bzw. der Benutzerin, bereits verbrauchte Rechenzeit, alle geöffneten Dateien – alles, was für die Ausführung wichtig ist. Ein Prozess ist ein Programm in Ausführung.
Besonders hervorzuheben ist, dass jeder Prozess seinen eigenen virtuellen Adressraum besitzt. Wie dieser Speicherbereich genau organisiert ist, werden wir später noch im Detail klären. Im Moment ist es jedoch wichtig zu wissen, dass im virtuellen Speicher nicht nur die Variablen des Programms, sondern auch der Programmcode selbst sowie der Stack enthalten sind. Der Stack
Der Stack ist eine besondere Datenstruktur, über die man Funktionsaufrufe besonders gut abbilden kann. Sie bietet folgende Operationen: push
Mit dieser Operation kann man ein neues Element auf den Stack legen. Auf diese Weise erweitert man die Datenstruktur um ein Element, das gleichzeitig zum aktuellen Element wird. top
Mit dieser Operation kann man auf das oberste beziehungsweise aktuelle Element zugreifen und es auslesen. Wendet man sie an, so bekommt man das letzte per push auf den Stack geschobene Element geliefert.
pop
Mit dieser Operation kann man schließlich das oberste Element vom Stack löschen. Beim nächsten Aufruf von top würde man also das Element unter dem von pop gelöschten Element geliefert bekommen.
Abbildung 2.5 Das Prinzip eines Stacks
Die einzelnen Funktionen und ihre Auswirkungen auf den Stack sind in Abbildung 2.5 veranschaulicht -- push und pop bewirken jeweils eine Veränderung der Datenstruktur, während top auf der aktuellen Datenstruktur operiert und das oberste Element zurückgibt. Um zu veranschaulichen, wieso diese Datenstruktur so gut das Verhalten von Funktionsaufrufen abbilden kann, betrachten wir das folgende kleine C-Beispiel: #include
void funktion1()
{
printf("Hello World!\n");
return;
}
int main()
{
funktion1();
return 0;
}
Listing 2.7 Ein modifiziertes »HelloWorld!«-Programm
In diesem Beispiel wird die Funktion funktion1() aus dem Hauptprogramm heraus aufgerufen. Diese Funktion wiederum ruft die Funktion printf() mit einem Text als Argument auf. Der Stack für die Funktionsaufrufe verändert sich während der Ausführung wie folgt: Start des Programms
Beim Start des Programms ist der Stack zwar vom Betriebssystem initialisiert, aber im Prinzip noch leer. Das ist zwar nicht ganz richtig, aber für uns erst einmal uninteressant. Aufruf von funktion1()
An dieser Stelle wird der Stack benutzt, um sich zu merken, wo es nach der Ausführung von funktion1() weitergehen soll. Im Wesentlichen wird also das Befehlsregister auf den Stack geschrieben – und zwar mit der Adresse des nächsten Befehls nach dem Aufruf der Funktion: von return. Es werden in der Realität auch noch andere Werte auf den Stack geschrieben, aber diese sind hier für uns uninteressant. Schließlich wird der Befehlszeiger auf den Anfang von funktion1() gesetzt, damit die Funktion ablaufen kann. In der Funktion funktion1()
Hier wird sofort eine weitere Funktion aufgerufen: printf() aus der C-Bibliothek mit dem Argument »Hello World!«. Auch hier wird wieder die Adresse des nächsten Befehls auf den Stack geschrieben. Außerdem wird auch das Argument für die Funktion, also eben unser Text, auf dem Stack abgelegt.[ 14 ] Die Funktion printf()
Die Funktion kann jetzt das Argument vom Stack lesen und somit unseren Text schreiben. Danach wird der vor dem Aufruf von printf() auf den Stack gelegte Befehlszeiger ausgelesen und das
Befehlsregister mit diesem Wert gefüllt. So kann schließlich funktion1() ganz normal mit dem nächsten Befehl nach dem Aufruf von printf() weitermachen. Dies ist nun schon ein Rücksprungbefehl, der das Ende der Funktion anzeigt. Das Ende
Wir kehren nun ins Hauptprogramm zurück und verfahren dabei analog wie beim Rücksprung von printf() zu funktion1(). Wie man sieht, eignet sich der Stack also prächtig für Funktionsaufrufe: Schließlich wollen wir nach dem Ende einer Funktion in die aufrufende Funktion zurückkehren – anderen interessieren uns nicht. Mit dem Rücksprung nach main() ist unser Programm abgeschlossen und wird vom Betriebssystem beendet. Es liegen noch mehr Daten auf dem Stack als nur der Befehlszeiger oder die Funktionsargumente – mit diesen Daten werden wir uns später noch auseinandersetzen. Thread
Kommen wir also zur nächsten Definition. Im letzten Abschnitt haben wir einen Prozess als ein Programm in Ausführung definiert. Jedoch ist klar, dass die eigentliche Ausführung nur von den folgenden Daten abhängt: dem aktuellen Befehlsregister dem Stack dem Inhalt der Register des Prozessors Ein Prozess besteht nun vereinfacht gesagt aus dem eigenen Speicherbereich, dem Kontext (wie zum Beispiel den geöffneten
Dateien) und genau einem solchen Ausführungsfaden, einem sogenannten Thread. Ein Thread ist ein Ausführungsfaden, der aus einem aktuellen Befehlszeiger, einem eigenen Stack und dem Inhalt der Prozessorregister besteht.
Ein auszuführendes Programm kann nun theoretisch auch mehrere solcher Threads besitzen. Das bedeutet, dass diese Ausführungsfäden quasiparallel im selben Adressraum laufen. Diese Eigenschaft ermöglicht ein schnelles Umschalten zwischen verschiedenen Threads einer Applikation. Außerdem erleichtert die Möglichkeit zur parallelen Programmierung einer Applikation die Arbeit des Programmierers teilweise deutlich. Threads müssen dabei nicht notwendigerweise im Kernel implementiert sein: Es gibt nämlich auch sogenannte UserlevelThreads. Für das Betriebssystem verhält sich die Applikation wie ein normaler Prozess mit einem Ausführungsfaden. Im Programm selbst sorgt jetzt jedoch eine besondere Bibliothek dafür, dass eigens angelegte Stacks richtig verwaltet und auch die Threads regelmäßig umgeschaltet werden, damit die parallele Ausführung gewährleistet ist. Außerdem gibt es neben den dem Kernel bekannten KLTs (Kernellevel-Threads) und den eben vorgestellten PULTs (puren Userlevel-Threads) auch noch sogenannte Kernelmode-Threads. Diese Threads sind nun Threads des Kernels und laufen komplett im namensgebenden Kernelmode. Normalerweise wird der Kernel eigentlich nur bei zu bearbeitenden Syscalls und Interrupts aktiv. Es gibt aber auch Arbeiten, die unabhängig von diesen Zeitpunkten, vielleicht sogar periodisch ausgeführt werden müssen. Solche
typischen Aufgaben sind zum Beispiel das regelmäßige Aufräumen des Hauptspeichers oder das Auslagern von lange nicht mehr benutzten Speicherseiten auf die Festplatte, wenn der Hauptspeicher einmal besonders stark ausgelastet ist. Task
Ein Task ist eigentlich nichts anderes als ein Prozess mit mehreren Threads. Zur besseren Klarheit wird teilweise auch die folgende Unterscheidung getroffen: Ein Prozess hat einen Ausführungsfaden, ein Task hat mehrere. Somit ist ein Prozess ein Spezialfall eines Tasks. Aus diesem Grund gibt es auch keine Unterschiede bei der Realisierung beider Begriffe im System. Unter Unix spricht man trotzdem meistens von Prozessen, da hier das Konzept der Threads im Vergleich zur langen Geschichte des Betriebssystems noch relativ neu ist. So hat zum Beispiel Linux erst seit Mitte der 2.4er-Reihe eine akzeptable Thread-Unterstützung. Vorher war die Erstellung eines neuen Threads fast langsamer als die eines neuen Prozesses, was dem ganzen Konzept natürlich widerspricht. Aber mittlerweile ist die Thread-Unterstützung richtig gut, insofern ist dies schon lang kein Manko mehr. Identifikationsnummern
Damit das Betriebssystem die einzelnen Prozesse, Threads und Tasks unterscheiden kann, wird allen Prozessen beziehungsweise Tasks eine Prozess-ID (PID) zugewiesen. Diese PIDs sind auf jeden Fall eindeutig im System. Threads haben entsprechend eine Thread-ID (TID). Ob TIDs nun aber im System oder nur innerhalb eines Prozesses eindeutig sind, ist eine Frage der Thread-Bibliothek. Ist diese im Kernel implementiert,
ist es sehr wahrscheinlich, dass die IDs der Threads mit den IDs der Prozesse im System eindeutig sind. Schließlich gilt es für das Betriebssystem herauszufinden, welcher Prozess oder Thread als Nächstes laufen soll. Dazu ist natürlich ein eindeutiges Unterscheidungsmerkmal wichtig. Allerdings könnte auch das Tupel (Prozess-ID, Thread-ID) für eine solche eindeutige Identifizierung herangezogen werden, falls die TID nur für jeden Prozess eindeutig ist. Ist der Thread-Support nur im Userspace über eine Bibliothek implementiert, so ist eine eindeutige Identifizierung für den Kernel unnötig – er hat mit der Umschaltung der Threads nichts zu tun. So werden die TIDs nur innerhalb des betreffenden Tasks eindeutig sein. Mehr zu PIDs und Prozessverwaltung erfahren Sie in Kapitel 5. 2.3.2 Lebenszyklen eines Prozesses
Der nächste wichtige Punkt – die Lebenszyklen eines Prozesses – betrifft die Tasks; Threads spielen in diesem Kontext keine Rolle. Ein Prozess hat verschiedene Lebensstadien. Das wird schon deutlich, wenn man sich vor Augen führt, dass er erstellt, initialisiert, verarbeitet und beendet werden muss. Außerdem gibt es noch den Fall, dass ein Prozess blockiert ist – wenn er zum Beispiel auf eine (Tastatur-)Eingabe der Benutzerin wartet, diese sich aber Zeit lässt. Prozesserstellung
Zuerst wollen wir die Geburt eines neuen Prozesses betrachten. Dabei interessieren uns zunächst die Zeitpunkte, an denen theoretisch neue Prozesse erstellt werden könnten:
Systemstart Anfrage eines bereits laufenden Prozesses zur Erstellung eines neuen Prozesses Anfrage eines Users zur Erstellung eines neuen Prozesses Starten eines Hintergrundprozesses (Batch-Job) Sieht man sich diese Liste näher an, so fällt auf, dass die letzten drei Punkte eigentlich zusammengefasst werden können: Wenn man einen neuen Prozess als Kopie eines bereits bestehenden Prozesses erstellt, braucht man sich nur noch beim Systemstart um einen Urprozess zu kümmern. Von diesem werden schließlich alle anderen Prozesse kopiert, indem ein Prozess selbst sagt, dass er kopiert werden möchte. Da die Benutzerinnen und Benutzer das System ausschließlich über Programme bedienen, können die entsprechenden Prozesse auch selbst die Erstellung eines neuen Prozesses veranlassen – Punkt 3 wäre damit also auch abgedeckt. Bleiben noch die ominösen Hintergrundjobs: Werden diese durch einen entsprechenden Dienst auf dem System gestartet, so reicht auch für diese Arbeit das einfache Kopieren eines Prozesses aus. Eine solche Idee impliziert aber auch die strikte Trennung zwischen dem Starten eines Prozesses und dem Starten eines Programms. So gibt es denn auch zwei Syscalls: fork() kopiert einen Prozess, sodass das alte Programm in zwei Prozessen ausgeführt wird, und exec*() ersetzt in einem laufenden Prozess das alte Programm durch ein neues. Offensichtlich kann man die häufige Aufgabe des Startens eines neuen Programms in einem eigenen Prozess dadurch erreichen, dass erst ein Prozess kopiert und dann in der Kopie das neue Programm gestartet wird. Beim fork()-Syscall muss man also nach dem Aufruf unterscheiden können, ob man sich im alten oder im neuen Prozess befindet. Eine
einfache Möglichkeit dazu ist der Rückgabewert des Syscalls: Dem Kind wird 0 als Ergebnis des Aufrufs zurückgegeben, dem Elternprozess dagegen die ID des Kindes: #include
#include
int main()
{
if( fork() == 0 ) {
// Hier ist der Kindprozess
printf("Ich bin der Kindprozess!\n");
} else {
// Hier ist der Elternprozess
printf("Ich bin der Elternprozess!\n");
}
return 0;
}
Listing 2.8 Ein fork-Beispiel
Im obigen Beispiel wurde dabei die Prozess-ID (PID), die der fork()Aufruf dem erzeugenden Prozess übergibt, ignoriert. Wir haben diese Rückgabe nur auf die Null überprüft, die den Kindprozess anzeigt. Würde den erzeugenden Prozess anders als in diesem Beispiel doch die PID des Kindes interessieren, so würde man den Rückgabewert von fork() einer Variablen zuweisen, um diese schließlich entsprechend auszuwerten. #include
#include
#include
int main()
{
if( fork() == 0 ) {
execl( "/bin/ls", "ls", NULL );
}
return 0;
}
Listing 2.9 Das Starten eines neuen Programms in einem eigenen Prozess
In diesem Beispiel haben wir wie beschrieben ein neues Programm gestartet: Zuerst wird ein neuer Prozess erzeugt, in dem dann das
neue Programm ausgeführt wird. Beim Starten eines neuen Programms wird fast der gesamte Kontext des alten Prozesses ausgetauscht – am wichtigsten ist dabei der virtuelle Speicher. Das neue Programm erhält nämlich einen neuen Adressraum ohne die Daten des erzeugenden Prozesses. Dies erfordert auf den ersten Blick unnötig doppelte Arbeit: Beim fork() wird der Adressraum erst kopiert und eventuell sofort wieder verworfen und durch einen neuen, leeren Speicherbereich ersetzt. Dieses Dilemma umgeht man durch ein einfaches wie geniales Prinzip: Copy on Write. Beim fork() wird der Adressraum ja nur kopiert, die Adressräume von Eltern- und Kindprozess sind also direkt nach der Erstellung noch identisch. Daher werden die Adressräume auch nicht real kopiert, sondern die Speicherbereiche werden als »nur lesbar« markiert. Versucht nun einer der beiden Prozesse, auf eine Adresse zu schreiben, bemerkt das Betriebssystem den Fehler und kann den betreffenden Speicherbereich real duplizieren – und in der Folge natürlich für beide Prozesse schreibbar machen. Dies passiert für den schreibenden Prozess transparent, er bemerkt von dieser Optimierung idealerweise nichts. Die Prozesshierarchie
Wenn ein Prozess immer von einem anderen erzeugt wird, so ergibt sich eine Prozesshierarchie. Jeder Prozess – außer dem beim Systemstart erzeugten Initprozess mit der PID 1 – hat also einen Elternprozess. So kann man aus allen Prozessen zur Laufzeit eine Art Baum mit dem Initprozess als gemeinsamer Wurzel konstruieren.
Eine solche Hierarchie hat natürlich gewisse Vorteile bei der Prozessverwaltung: Die Programme können so nämlich über bestimmte Syscalls die Terminierung samt eventuellen Fehlern überwachen. Außerdem war in den Anfangsjahren von Unix das Konzept der Threads noch nicht bekannt und so musste Nebenläufigkeit innerhalb eines Programms über verschiedene Prozesse realisiert werden. Dies ist der Grund, warum es eine so strenge Trennung von Prozess und Programm gibt und warum sich Prozesse selbst kopieren können, was schließlich auch zur Hierarchie der Prozesse führt. Das Scheduling
Nach der Prozesserstellung soll ein Prozess nach dem MultitaskingPrinzip abgearbeitet werden. Dazu müssen alle im System vorhandenen Threads und Prozesse nacheinander jeweils eine gewisse Zeit den Prozessor nutzen können. Die Entscheidung, wer wann wie viel Rechenleistung bekommt, obliegt einem besonderen Teil des Betriebssystems, dem Scheduler. Als Grundlage für das Scheduling dienen dabei bestimmte Prozesszustände, diverse Eigenschaften und jede Menge Statistik. Betrachten wir jedoch zuerst die für das Scheduling besonders interessanten Zustände: RUNNING
Den Zustand RUNNING kann auf einem Einprozessorsystem nur je ein Prozess zu einer bestimmten Zeit haben: Dieser Zustand zeigt nämlich an, dass dieser Prozess jetzt gerade die CPU benutzt. READY
Andere lauffähige Prozesse haben den Zustand READY. Diese
Prozesse stehen also dem Scheduler für dessen Entscheidung, welcher Prozess als nächster laufen soll, zur Verfügung. BLOCKED
Im Gegensatz zum READY-Zustand kann ein Prozess auch aus den verschiedensten Gründen blockiert sein, wenn er etwa auf Eingaben von der Tastatur wartet, ohne bestimmte Datenpakete aus dem Internet nicht weiterrechnen oder aus sonstigen Gründen gerade nicht arbeiten kann. Wenn die Daten für einen solchen wartenden Prozess ankommen, wird das Betriebssystem üblicherweise über einen Interrupt informiert. Das Betriebssystem kann dann die Daten bereitstellen und durch Setzen des Prozessstatus auf READY diesen Prozess wieder freigeben. Natürlich gibt es noch weitere Zustände, von denen wir einige in späteren Kapiteln näher behandeln werden. Mit diesen Zuständen hat nämlich weniger der Scheduler als vielmehr der Benutzer bzw. die Benutzerin Kontakt, daher werden sie hier nicht weiter aufgeführt. Weitere wichtige Daten für den Scheduler sind eventuelle Prioritäten für spezielle Prozesse, die besonders viel Rechenzeit erhalten sollen. Auch muss natürlich festgehalten werden, wie viel Rechenzeit ein Prozess im Verhältnis zu anderen Prozessen schon bekommen hat. Wie sieht nun aber der Scheduler in Linux genau aus? In der 2.6erKernelreihe wurde das Scheduling bis zum 2.6.23er Kernel im sogenannten O(1)-Scheduler wie folgt realisiert: Der Kernel verwaltete zwei Listen mit allen lauffähigen Prozessen: eine Liste mit den Prozessen, die schon gelaufen sind, und die andere mit allen Prozessen, die noch nicht gelaufen sind. Hatte ein Prozess nun
seine Zeitscheibe beendet, wurde er in die Liste mit den abgelaufenen Prozessen eingehängt und aus der anderen Liste entfernt. War die Liste mit den noch nicht abgelaufenen Prozessen abgearbeitet und leer, so wurden die beiden Listen einfach getauscht. Eine Zeitscheibe, in der ein Prozess rechnet, dauert unter Linux übrigens maximal 1/1000 Sekunde. Sie kann natürlich auch vor Ablauf dieser Zeit abgebrochen werden, wenn das Programm zum Beispiel auf Daten wartet und dafür einen blockierenden Systemaufruf benutzt hat. Bei der Auswahl des nächsten abzuarbeitenden Prozesses wurden dabei interaktive vor rechenintensiven Prozessen bevorzugt. Interaktive Prozesse interagieren mit dem Benutzer und brauchen so meist eine geringe Rechenzeit. Dafür möchte der Benutzer bei diesen Programmen eine schnelle Reaktion haben, da eine Verzögerung zum Beispiel bei der grafischen Oberfläche X11 sehr negativ auffallen würde. Der O(1)Scheduler besaß nun ausgefeilte Algorithmen, um festzustellen, von welcher Art ein bestimmter Prozess ist und wo Grenzfälle am besten eingeordnet werden. Seit Kernel 2.6.24 verwendet Linux den »completely fair scheduler«, CFS. Dieser Scheduler basiert auf einem einfachen Prinzip: Beim Taskwechsel wird der Task gestartet, der schon am längsten auf die Ausführung bzw. Fortsetzung wartet. Dadurch wird eine auf Desktop- wie auf Serversystemen gleichermaßen »faire« Verteilung der Prozessorzeit erreicht. Bei vielen »gleichartigen« Tasks auf Serversystemen ist die Fairness offensichtlich. Auf Desktopsystemen mit vielen interaktiven Prozessen wird jedoch auch die Zeit berücksichtigt, die ein Prozess blockiert ist, weil er bspw. auf Eingaben der Benutzerin bzw. des Benutzers oder auf Rückmeldung der Festplatte wartet. Ist ein
solcher interaktiver Prozess dann lauffähig, wird er mit hoher Wahrscheinlichkeit auch zeitnah vom Scheduler gestartet, da er ja schon lang »gewartet« hat. Beim CFS entfällt somit die Notwendigkeit, über Heuristiken etc. festzustellen, ob es sich um einen interaktiven oder rechenintensiven Task handelt. Auch gibt es keine zwei Listen mehr, da alle Tasks in einer zentralen, nach »Wartezeit« geordneten Datenstruktur gehalten werden. Beenden von Prozessen
Irgendwann wird jeder Prozess einmal beendet. Dafür kann es natürlich verschiedenste Gründe geben, je nachdem, ob sich ein Prozess freiwillig beendet oder beendet wird. Folgende Varianten sind zu unterscheiden: normales Ende (freiwillig) Ende aufgrund eines Fehlers (freiwillig) Ende aufgrund eines Fehlers (unfreiwillig) Ende aufgrund eines Signals eines anderen Prozesses (unfreiwillig) Die meisten Prozesse beenden sich, weil sie ihre Arbeit getan haben. Ein Aufruf des find-Programms durchsucht zum Beispiel die gesamte Festplatte nach bestimmten Dateien. Ist die Festplatte durchsucht, beendet sich das Programm. Viele Programme einer grafischen Oberfläche geben dem Benutzer bzw. der Benutzerin die Möglichkeit, durch einen Klick auf »das Kreuz rechts oben« das Fenster zu schließen und die Applikation zu beenden – auch eine Art des freiwilligen Beendens. Diesem ging ein entsprechender Wunsch des Benutzers voraus.
Im Gegensatz dazu steht das freiwillige Beenden eines Programms aufgrund eines Fehlers. Möchte man zum Beispiel mit dem gcc eine Quelldatei übersetzen, hat sich aber dabei bei der Eingabe des Namens vertippt, wird Folgendes passieren: $ gcc -o test tset.c
gcc: tset.c: Datei oder Verzeichnis nicht gefunden
gcc: keine Eingabedateien
$
Listing 2.10 Freiwilliges Beenden des gcc
Damit sich Programme auf diese Weise freiwillig beenden können, brauchen sie einen Syscall. Unter Unix heißt dieser Aufruf exit() und man kann ihm auch noch eine Zahl als Rückgabewert übergeben. Über diesen Rückgabewert können Fehler und teilweise sogar die Fehlerursache angegeben werden. Ein Rückgabewert von »0« signalisiert dabei: »Alles okay, keine Fehler.« In der Shell kann man über die Variable $? auf den Rückgabewert des letzten Prozesses zugreifen. Im obigen Beispiel eines freiwilligen Endes aufgrund eines Fehlers erhält man folgendes Ergebnis: $ echo $?
1
$
Listing 2.11 Rückgabewert ungleich null: Fehler
Wie aber sieht nun ein vom Betriebssystem erzwungenes Ende aus? Dieses tritt vor allem auf, wenn ein vom Programm nicht abgefangener und nicht zu reparierender Fehler auftritt. Dies kann zum Beispiel eine versteckte Division durch null sein, wie sie beim folgenden kleinen C-Beispiel auftritt: #include
int main()
{
int a = 2;
int c;
// Die fehlerhafte Berechnung
c = 2 / (a - 2);
printf("Nach der Berechnung.\n");
return 0;
}
Listing 2.12 Ein Beispielcode mit Division durch null
Will man dieses Beispiel nun übersetzen, ausführen und sich schließlich den Rückgabewert ansehen, muss man wie folgt vorgehen: $ gcc -o test test.c
$ ./test
Gleitkomma-Ausnahme
$ echo $?
136
Listing 2.13 Den Fehler betrachten
Der Punkt, an dem der Text »Nach der Berechnung.« ausgegeben werden sollte, wird also nicht mehr erreicht. Das Programm wird vorher vom Betriebssystem abgebrochen, nachdem es von einer Unterbrechung aufgrund dieses Fehlers aufgerufen wurde. Das System stellt fest, dass das Programm einen Fehler gemacht und dafür keine Routine zur einer entsprechenden Behandlung vorgesehen hat – folglich wird der Prozess beendet. Einen solchen Fehler könnte ein Programm noch abfangen, aber für bestimmte Fehler ist auch dies nicht mehr möglich. Sie werden ein solches Beispiel im Abschnitt über Speichermanagement kennenlernen, wenn auf einen vorher nicht mit malloc() angeforderten Bereich des virtuellen Speichers zugegriffen und damit ein Speicherzugriffsfehler provoziert wird.
Jetzt müssen wir nur noch die letzte Art eines unfreiwilligen Prozessendes erklären: die Signale. Signale sind ein Mittel der Interprozesskommunikation (IPC). Die dort beschriebenen Mechanismen regeln die Interaktion und Kommunikation der Prozesse miteinander und sind so für die Funktionalität des Systems sehr bedeutend. Ein Mittel dieser IPC ist nun das Senden der Signale, von denen zwei in diesem Zusammenhang für das Betriebssystem besonders wichtig sind: SIGTERM
Dieses Signal fordert einen Prozess freundlich auf, sich zu beenden. Der Prozess hat dabei die Möglichkeit, dieses Signal abzufangen und noch offene temporäre Dateien zu schließen bzw. alles zu unternehmen, um ein sicheres und korrektes Ende zu gewährleisten. Jedoch ist für ihn klar, dass dieses Signal eine deutliche Aufforderung zum Beenden ist. SIGKILL
Reagiert ein Prozess nicht auf ein SIGTERM, so kann man ihm ein SIGKILL schicken. Dies ist nun keine freundliche Aufforderung mehr, denn der Prozess bemerkt ein solches Signal nicht einmal. Er wird vom Betriebssystem sofort beendet, ohne noch einmal gefragt zu werden. Ein unfreiwilliges Ende wäre also der Empfang eines SIGKILL-Signals. (Dieses Signal kann, wie gesagt, nicht abgefangen werden, aber der Prozess ist doch in gewissem Sinne der Empfänger dieses Signals.) Beim Herunterfahren des Systems wird entsprechend der Semantik beider Signale auch meist so verfahren: Zuerst wird allen Prozessen ein SIGTERM gesendet, dann einige Sekunden gewartet und schließlich allen ein SIGKILL geschickt. 2.3.3 Implementierung
Im Folgenden geben wir einen kurzen Überblick über die Implementierung von Tasks und Threads im Linux-Kernel. Wir haben schon vereinzelt viele Details erwähnt, wenn diese gerade in den Kontext passten. In diesem Abschnitt möchten wir nun einige weitere Einzelheiten vorstellen, auch wenn wir natürlich nicht alle behandeln können. Konzentrieren wir uns zunächst auf die Repräsentation eines Prozesses im System. Wir haben festgestellt, dass ein Prozess viele zu verwaltende Daten besitzt. Diese Daten werden nun direkt oder indirekt alle im Prozesskontrollblock gespeichert. Diese Struktur bildet also ein Programm für die Ausführung durch das Betriebssystem in einen Prozess ab. Alle diese Prozesse werden nun in einer großen Liste, der Prozesstabelle (engl. process table), eingetragen. Jedes Element dieser Tabelle ist also ein solcher Kontrollblock. Sucht man diesen Kontrollblock nun im Kernel-Source, so wird man in der Datei include/linux/sched.h fündig. Dort wird nämlich der Verbund task_struct definiert, der auch alle von uns erwarteten Eigenschaften besitzt: struct task_struct {
volatile long state;
/* -1 unrunnable,
0 runnable, >0 stopped */
struct thread_info *thread_info;
…
Listing 2.14 Beginn der task_struct im Kernel
In diesem ersten Ausschnitt kann man bereits erkennen, dass jeder Task (Prozess) einen Status sowie einen initialen Thread besitzt. Dieser erste Ausführungsfaden wird aus Konsistenzgründen benötigt, um beim Scheduling keine weitgreifenden Unterscheidungen zwischen Threads und Prozessen treffen zu müssen.
Auch zum Scheduling gibt es in dieser Struktur Informationen – benötigt werden insbesondere Prozessprioritäten und Daten über die bisherige CPU-Nutzung: …
int prio, static_prio;
struct list_head run_list;
prio_array_t *array;
unsigned long sleep_avg;
long interactive_credit;
unsigned long long timestamp, last_ran;
int activated;
unsigned long policy;
cpumask_t cpus_allowed;
unsigned int time_slice, first_time_slice;
…
Listing 2.15 Informationen über den Benutzer bzw. die Benutzerin
Natürlich hat auch jeder Task seinen eigenen Speicherbereich. In der Struktur mm_struct merkt sich der Kernel, welche virtuellen Adressen belegt sind und auf welche physischen, also real im Hauptspeicher vorhandenen Adressen diese abgebildet werden. Jedem Task ist eine solche Struktur zugeordnet: struct mm_struct *mm, *active_mm;
Listing 2.16 Informationen zum Memory Management
Eine solche Struktur definiert nun einen eigenen Adressraum. Nur innerhalb eines Tasks kann auf die im Hauptspeicher abgelegten Werte zugegriffen werden, da innerhalb eines anderen Tasks keine Übersetzung von einer beliebigen virtuellen Adresse auf die entsprechend fremden realen Speicherstellen existiert. Später werden in der Datenstruktur auch essenzielle Eigenschaften wie die Prozess-ID oder Informationen über die Prozesshierarchie gespeichert:
pid_t pid;
…
struct task_struct *parent; struct list_head children;
struct list_head sibling;
…
Listing 2.17 Prozesshierarchie
Diese Hierarchie ist also so implementiert, dass ein Prozess einen direkten Zeiger auf seinen Elternprozess besitzt und außerdem eine Liste seiner Kinder sowie eine Liste der Kinder des Elternprozesses. Diese Listen sind vom Typ list_head, der einen Zeiger prev und next zur Verfügung stellt. So kann bei entsprechender Initialisierung schrittweise über alle Kinder beziehungsweise Geschwister iteriert werden.
…
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
…
Listing 2.18 Informationen über den Benutzer bzw. die Benutzerin
Natürlich sind auch alle Benutzer- und Gruppen-IDs für die Rechteverwaltung im Task-Kontrollblock gespeichert. Anhand dieser Werte kann bei einem Zugriff auf eine Datei festgestellt werden, ob dieser berechtigt ist. Mehr über die Rechteverwaltung erfahren Sie in Abschnitt 3.2.5. Natürlich finden sich auch alle bereits angesprochenen Statusinformationen des Tasks in der Datenstruktur. Dazu gehören unter anderem seine geöffneten Dateien: …
/* file system info */
int link_count, total_link_count;
/* ipc stuff */
struct sysv_sem sysvsem;
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespace */
struct namespace *namespace;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
…
Listing 2.19 Offene Dateien und Co.
In diesem Ausschnitt konnten Sie auch einige Datenstrukturen für die Interprozesskommunikation erkennen, beispielsweise eine Struktur für Signalhandler, also die Adressen der Funktionen, die die abfangbaren Signale des Prozesses behandeln sollen. Ebenso haben auch Threads einen entsprechenden Kontrollblock, der natürlich viel kleiner als der eines kompletten Tasks ist. union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
struct thread_info {
struct task_struct *task; /* main task structure */
…
unsigned long status; /* thread flags */
__u32 cpu; /* current CPU */
…
mm_segment_t addr_limit;
/* thread address space: 0-0xBFFFFFFF for user-thread
0-0xFFFFFFFF for kernel-thread */
…
unsigned long previous_esp;
/* ESP of the previous stack in case of nested
(IRQ) stacks */
…
Listing 2.20 Thread-Strukturen
An den Strukturen in Listing 2.20 kann man nun sehr schön die reduzierten Eigenschaften eines Threads sehen: Stack, Status, die aktuelle CPU und ein Adresslimit, das die Abgrenzung zwischen Threads des Kernels und des Userspace leistet. Außerdem ist ein Thread natürlich einem bestimmten Task zugeordnet. Auch wenn Sie jetzt nur einen kurzen Blick auf die den Prozessen und Threads zugrunde liegenden Datenstrukturen werfen konnten, sollen diese Ausführungen hier genügen. Natürlich gibt es im Kernel noch sehr viel Code, der diese Datenstrukturen mit Werten und damit mit Leben füllt – die Basis haben Sie jedoch kurz kennengelernt.
2.4 Speichermanagement Kommen wir nun jedoch zum Speichermanagement. Wir haben dieses Thema schon kurz bei der Besprechung der Speicherhierarchie und der Aufgabe des Stacks angeschnitten und wollen uns nun näher mit den Aufgaben sowie den konkreten Lösungen in diesem Problemkreis beschäftigen. Zur Wiederholung erläutern wir zunächst noch einmal das Prinzip des virtuellen Speichers: Dieses stellt sicher, dass jeder Task beziehungsweise jeder Prozess seinen eigenen Adressraum besitzt. Um dies zu erreichen, können Programme nicht direkt auf den Hauptspeicher, sondern nur auf virtuelle Adressen zugreifen, die erst in entsprechende reale Adressen übersetzt werden müssen. Mit dem virtuellen Speicher ist es also möglich, dass ein Prozess A in der Speicheradresse 0x010010 den Wert 4 und Prozess B den Wert 10 speichert. Beide Prozesse merken nichts voneinander, da für Prozess A die virtuelle Adresse 0x010010 beispielsweise in die reale Adresse 0x111010 und für Prozess B in die reale Adresse 222010 übersetzt wird. Beide Prozesse wissen nichts voneinander und können sich auch nicht gegenseitig beeinflussen. Für jeden Prozess sieht es so aus, als ob er den gesamten Speicherbereich für sich allein hätte. 2.4.1 Paging
Ein sehr wichtiger Bestandteil des Speichermanagements ist das Paging. Hierzu wird der verfügbare Speicher zur besseren Verwaltung in Seiten unterteilt, die zum Beispiel bei Intels x86Architektur meist 4 KB groß sind. Bei der Übersetzung von
virtuellen in reale Adressen muss also nicht mehr jede Adresse einzeln verwaltet, sondern es muss lediglich festgestellt werden, zu welcher Seite eine bestimmte Adresse gehört und auf welche physische Seite diese virtuelle Seite übersetzt wird. Eine »Seite« definiert sich also über die Adressen der entsprechenden 4-KB-Blöcke. Der Adressraum wird mit anderen Worten passend aufgeteilt, anstatt die Seiten dort willkürlich aufzuteilen. Dieses Vorgehen hat außerdem den Vorteil, dass die externe Fragmentierung des Hauptspeichers vermieden wird. Natürlich kann eine Seite noch intern fragmentieren, es kann also Platz verschenkt werden, wenn einzelne Seiten nicht voll sind. Man kann sich das so vorstellen, dass bei Adressen der Form 0x010010 beispielsweise die letzten drei Stellen der Adresse zu einer Seite zusammengefasst werden. In diesem Fall würden alle Adressen von 0x010000 bis 0x010FFF auf der entsprechenden Seite 0x010 liegen. Das Betriebssystem muss sich dann nur noch »merken«, dass die virtuelle Seite 0x010 des Prozesses A beispielsweise auf die physische Seite 0x111 gemappt ist, um den korrekten physischen Speicherort für eine konkrete Adresse (wie beispielsweise 0x010010) zu finden. Swapping
Außerdem wird durch die Verwaltung von ganzen Seiten statt einzelner Adressen auch das bereits vorgestellte Swapping vereinfacht, bei dem ja bestimmte, länger nicht benötigte Speicherbereiche auf die Festplatte ausgelagert werden. Praktischerweise kann man sich beim Paging auf AuslagerungsAlgorithmen für ganze Seiten konzentrieren.
Bei diesen Auslagerungs-Algorithmen geht es nun darum, eine Seite zu finden, die möglichst nicht so bald wieder gebraucht wird. Durch das Auslagern solcher alter Speicherseiten wird bei einer starken Auslastung des Hauptspeichers wieder Platz frei. Für die Ermittlung der zu ersetzenden Seiten gibt es nun unter anderem folgende Verfahren: First in – first out
Die Speicherseite, die zuerst angefordert wurde, wird zuerst ausgelagert. Least recently used
Bei dieser Strategie wird die am längsten nicht genutzte Seite ausgelagert. Not recently used
Seiten, die in einem bestimmten Zeitintervall nicht benutzt und nicht modifiziert wurden, werden bei dieser Strategie bevorzugt ausgelagert. Gibt es keine solche Seite, wird auf die anderen Seiten zurückgegriffen. Not frequently used
Hier werden bevorzugt Seiten ausgelagert, auf die in letzter Zeit nicht häufig zugegriffen wurde. Heutzutage wird Speicher immer billiger. Normale Mittelklasse-PCs haben schon mehrere Gigabyte RAM und Speicheraufrüstungen sind nicht teuer. Aus diesem Grund verliert das Swapping immer mehr an Bedeutung, auch wenn gleichzeitig der Speicherhunger der Applikationen immer größer wird. Die Pagetable
Der virtuelle Speicher und somit auch das Paging werden vom Betriebssystem verwaltet. Der Kernel sorgt dafür, dass jeder Prozess
seinen eigenen Speicherbereich besitzt und auch entsprechend Speicher anfordern kann. Irgendwo muss es also eine Tabelle geben, die jeder virtuellen Seite eines Prozesses eine physische zuordnet. In dieser Tabelle müsste dann auch entsprechend vermerkt sein, wenn eine Seite ausgelagert wurde. Die sogenannte Pagetable erfüllt genau diesen Zweck. Der Logik folgend muss also für jeden Prozess beziehungsweise Task eine eigene Pagetable existieren, während die Threads eines Tasks ja alle auf demselben Adressraum operieren und somit keine eigene Pagetable brauchen. Die Seitentabelle liegt dabei im Hauptspeicher und enthält auch diverse Statusinformationen: Wurde auf die Seite in letzter Zeit zugegriffen? Wurde die Seite verändert? Interessant ist außerdem der Zusammenhang zwischen der Größe der Pagetable und der Seitengröße: Je größer eine Seite ist, desto höher ist die Möglichkeit zu interner Fragmentierung, aber umso kleiner wird die Pagetable. Schließlich lässt sich der Hauptspeicher bei einer höheren Seitengröße insgesamt in weniger Seiten zerlegen, was sich dann direkt auf die Anzahl der Elemente in der Seitentabelle niederschlägt. 2.4.2 Hardware
Das Umsetzen des Pagings kann natürlich nicht ohne entsprechenden Hardware-Support realisiert werden. Maschinenbefehle greifen nun einmal direkt auf den Hauptspeicher zu, ohne dass das Betriebssystem irgendeine Möglichkeit hätte, diese Übersetzung per Software zu bewerkstelligen.
Die MMU
Als Hardware-Element, das diese Übersetzung vornimmt, haben wir Ihnen bereits kurz die Memory Management Unit (MMU) vorgestellt. Nach der Einführung des Pagings können wir nun auch im Detail erklären, wie die Übersetzung der virtuellen in die physische Adresse von der Hardware vorgenommen wird: 1. Aus der virtuellen Adresse wird die zugehörige virtuelle Seite berechnet. 2. Die MMU schaut in der Pagetable nach, auf welche physische Seite diese virtuelle Seite abgebildet wird. 3. Findet die MMU keine entsprechende physische Seite, wird dem Betriebssystem ein Page Fault (Seitenfehler) geschickt. 4. Andernfalls wird aus dem Offset (also dem Abstand der abzufragenden virtuellen Adresse vom Seitenanfang) sowie dem Beginn der physischen Seite die physische Adresse berechnet. 5. Der Wert, der an dieser physischen Adresse gespeichert ist, wird jetzt vom Hauptspeicher zum Prozessor kopiert. Die MMU speichert also keine Kopie der Seitentabelle, sondern bei jedem Prozess- beziehungsweise Taskwechsel werden bestimmte Register neu gesetzt, die zum Beispiel die physische Adresse des Anfangs der Seitentabelle im Hauptspeicher enthalten. Das Verfahren bei einem Speicherzugriffsfehler (Page Fault) ist wie folgt: Der aktuell laufende und den Fehler verursachende Prozess wird durch einen sogenannten Page-Fault-Interrupt unterbrochen und das Betriebssystem mit der entsprechenden Behandlungsroutine gestartet, die nun dafür sorgt, dass die entsprechende Seite wieder eingelagert und die Seitentabelle
aktualisiert wird. Dann kann die fehlgeschlagene Instruktion des abgebrochenen Programms wiederholt werden, da die MMU jetzt eine entsprechende physische Seite finden kann. Page Fault: Interrupt oder Exception? Eigentlich handelt es sich bei einem Page Fault nicht um einen Interrupt, sondern vielmehr um eine Exception. Die Unterbrechung ist nämlich eine gewisse »Fehlermeldung« der MMU und tritt synchron auf, also immer direkt nach einem fehlerhaften Zugriff. Da das ausgeführte Programm jedoch wirklich unterbrochen wird, wollen wir bei der Bezeichnung Interrupt für dieses Ereignis bleiben, auch wenn dies formell nicht ganz korrekt sein mag.
Natürlich kann ein Programm auch durch fehlerhafte Programmierung einen Seitenfehler verursachen, wie etwa das folgende Beispielprogramm: #include
int main()
{
char* text = "Hello, World!\n";
// Hier wird fälschlicherweise eine neue Adresse zugewiesen
text = 13423;
printf(text);
return 0;
}
Listing 2.21 Ein Programm, das einen Absturz durch einen Seitenfehler verursacht
Die Variable text ist hier ein Zeiger auf den Text »Hello, World!«. Sie enthält also nicht den Text selbst, sondern nur die Adresse, wo dieser zu finden ist. (In C sind diese Zeiger oder auch Pointer genannten Variablen sehr mächtig, in anderen Programmiersprachen versteckt man diese Interna teilweise.) Diese
Adresse wird nun im Folgenden »versehentlich« verändert. Beim Versuch, die Zeichenkette auf dem Bildschirm auszugeben, wird nun die MMU zu der betreffenden virtuellen Seite keine physische finden. Das Betriebssystem wird nun wiederum per Page Fault benachrichtigt, kann aber mit dem Fehler nichts anfangen – die betreffende Seite wurde nie ausgelagert. Daher wird der verursachende Prozess mit einem Speicherzugriffsfehler beendet. Der TLB
Der Translation Lookaside Buffer (TLB) ist dafür da, den Zugriff auf häufig genutzte Seiten zu beschleunigen. Der TLB funktioniert dabei als eine Art Cache für die Adressübersetzungen: Da Programme in begrenzten Codeabschnitten meist nur einige wenige Variablen nutzen, werden dort nur jeweils wenige Seiten wieder und wieder genutzt. Damit für diese Adressen der zeitraubende Zugriff auf die Seitentabelle entfallen kann, speichert der TLB die zuletzt übersetzten virtuellen Seiten. Wird nun bei einem Zugriff festgestellt, dass die angeforderte virtuelle Adresse im TLB gepuffert wurde, kann man sich die komplizierte Übersetzung sparen. Sinnvollerweise ist der TLB dabei ein besonderer Teil der MMU, da hier der Hardware-Support für den virtuellen Speicher angesiedelt ist. Natürlich wird bei einem Task-Wechsel mit der MMU auch der TLB geändert: Es werden nämlich alle gepufferten Übersetzungen gelöscht. Für die Dauer einer Zeitscheibe hat dies eine gewisse Auswirkung, da eine Mindestlänge nötig ist, um die Vorteile des TLB und anderer Puffer und Caches wirklich ausnutzen zu können. 2.4.3 Organisation des Adressraums
Wir haben bereits viele Details der Speicherverwaltung behandelt, aber noch nicht die genaue Organisation des Adressraums unter die Lupe genommen. Im Folgenden wollen wir noch einmal zusammenfassen, was wir bisher über den Adressraum – also den für einen Prozess sichtbaren Hauptspeicher – alles wissen: Virtualisierung
Der Speicher und damit natürlich auch der Adressraum sind virtualisiert. Jeder Prozess/Task hat seinen eigenen virtuellen Adressraum, auf den er über virtuelle Speicheradressen zugreift. Code und Daten
Im Adressraum des Prozesses sind der auszuführende Programmcode sowie alle benutzten Daten gespeichert. Stack
Der Stack besteht aus besonderen Daten, denn hier werden die Funktionsaufrufe verwaltet. Jeder neue Aufruf wird dabei oben auf dem Stack abgelegt, sodass beim Funktionsende die entsprechenden Daten gleich gefunden werden. Das Betriebssystem
Wir haben erläutert, warum das Betriebssystem in jedem Adressraum einen bestimmten Bereich zugewiesen bekommt: Beim Auftreten von Interrupts kann vor dem Aufruf der Interrupt-Serviceroutine kein Wechsel des Adressraums erfolgen. Threads
Alle Threads eines Tasks arbeiten im selben Adressraum. Das hat den Effekt, dass alle Threads auf die gemeinsamen globalen Daten zugreifen können und dass es auch mehrere Stacks im Adressraum gibt – für jeden Thread einen. Bei einer 32-Bit-Architektur müssen also alle Daten mit 32 Adressbits adressiert werden können. Damit der Speicherbereich
des Betriebssystems immer an derselben Stelle residiert, ist ein uniformes Layout des Adressraums für jeden Prozess gegeben (siehe Abbildung 2.6).
Abbildung 2.6 Der Adressraum
Unter Linux liegt das Speichersegment des Betriebssystems im obersten Gigabyte, was also drei Gigabyte Platz für die jeweilige Applikation lässt. (Windows beansprucht zum Beispiel die obersten zwei Gigabyte des Adressraums.) Der Stack wächst dabei nach unten und der Heap (der Speicherbereich für dynamischen Speicher – mehr dazu auf den nächsten Seiten) nach oben. So wird gewährleistet, dass beide genug Platz zum Wachsen haben. Das Codesegment
Im untersten Teil des Adressraums ist dabei das Code- oder auch Textsegment eingelagert. Wenn das Befehlsregister des Prozessors nun immer auf den nächsten Befehl zeigt, so wird dessen Speicheradresse mit an Sicherheit grenzender Wahrscheinlichkeit in diesem Teil des Adressraums liegen. Die Ausnahmefälle wie Buffer-Overflows, bei denen Hacker zum Beispiel Daten mittels Veränderung des Befehlsregisters zur
Ausführung bringen, wollen wir hier nicht betrachten. Näher erläutern wollen wir in diesem Abschnitt noch den bereits angesprochenen Unterschied zwischen einer ausführbaren Datei und dem puren Maschinencode. Linux nutzt normalerweise das Executable and Linking Format – kurz ELF – für ausführbare Dateien. (Es werden auch noch das veraltete a.out-Format sowie eventuell auch Java-Dateien direkt vom Kernel unterstützt.) Die Besonderheiten dieses Formats liegen in der Möglichkeit des dynamischen Linkens und Ladens, was bei der Nutzung dynamischer Bibliotheken[ 15 ] von großer Bedeutung ist. Eine ELFDatei ist dabei wie folgt aufgebaut: 1. ELF-Header (mit Verweisen auf die anderen Teile der Datei) 2. Programmkopf-Tabelle 3. Sektionskopf-Tabelle 4. Sektionen 5. Segmente So ist zum Beispiel in der Sektionskopf-Tabelle verzeichnet, welche Sektionen wo im Speicher angelegt werden sollen, wie viel Platz diese benötigen und wo in der Datei die entsprechenden Daten gefunden werden können. Diese Daten können dann genutzt werden, um den Adressraum entsprechend zu initialisieren. Den genauen Aufbau einer solchen Datei kann man zum Beispiel mit dem Tool objdump auf der Kommandozeile studieren: $ objdump -h /bin/ls
/bin/ls: file format elf32-i386
Sections:
Idx Name Size VMA LMA …
9 .init 00000017 0804945c 0804945c
File off
Algn
0000145c
2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
…
11 .text
0000c880 CONTENTS, 12 .fini 0000001b CONTENTS, 13 .rodata 000039dc CONTENTS, …
15 .data 000000e8 CONTENTS, …
18 .ctors 00000008 CONTENTS, 19 .dtors 00000008 CONTENTS, …
22 .bss 000003b0 ALLOC
08049a50 08049a50 00001a50 ALLOC, LOAD, READONLY, CODE
080562d0 080562d0 0000e2d0 ALLOC, LOAD, READONLY, CODE
08056300 08056300 0000e300 ALLOC, LOAD, READONLY, DATA
2**4
0805a000 0805a000 ALLOC, LOAD, DATA
00012000
2**5
0805a25c 0805a25c ALLOC, LOAD, DATA
0805a264 0805a264 ALLOC, LOAD, DATA
0001225c
2**2
00012264
2**2
0805a400
00012400
2**5
0805a400
2**2
2**5
Listing 2.22 Ein Auszug des Sektionsheaders von /bin/ls
Wenn man sich den Aufbau eines vermeintlich einfachen Programms wie ls ansieht, bemerkt man die doch beachtliche Anzahl der vorhandenen Segmente. Außerdem sind offensichtlich nicht nur die Angaben über diverse Positionen im virtuellen Speicher oder in der Datei gesichert, sondern auch Informationen über Zugriffsrechte und andere Eigenschaften. Im Folgenden stellen wir kurz die wichtigsten Segmente der Datei noch einmal vor: .text
Im Textsegment finden sich die Maschinenbefehle, die später im untersten Teil des Adressraums abgelegt werden. Außerdem ist es wichtig zu erwähnen, dass für diese Seiten nur das Lesen (READONLY) erlaubt ist. Dies geschieht im Wesentlichen aus Sicherheitsgründen, da fehlerhafte Programme sonst durch einen entsprechend falsch gesetzten Pointer den Programmcode modifizieren könnten. Versucht aber ein Programm, nun auf die als nur lesbar markierten Speicherseiten schreibend zuzugreifen, wird das Programm in bekannter Weise mit einem Speicherzugriffsfehler abstürzen.
.data
In diesem Segment werden alle Daten und Variablen zusammengefasst, die bereits mit bestimmten Werten vorbelegt sind. Dieses Segment wird ebenfalls direkt in den Hauptspeicher eingelagert, ist jedoch mit Lese- und Schreibrechten ausgestattet. .rodata
In diesem Segment stehen im Prinzip dieselben Daten wie in .data, sie sind jedoch schreibgeschützt. .bss
In diesem Segment wird angegeben, wie viel Platz die uninitialisierten globalen Daten im Arbeitsspeicher benötigen werden. Uninitialisierte Daten haben keinen speziellen Wert, daher wird für sie auch kein Platz in der ausführbaren Datei und damit im Dateisystem belegt. Bei der Initialisierung des Adressraums werden die entsprechenden Felder dann mit Nullen gefüllt, daher reicht die Angabe des zu verbrauchenden Platzes in diesem Header vollkommen aus. .ctors und .dtors
Wer objektorientiert programmiert, kennt Konstruktoren und je nach Sprache auch Destruktoren für seine Klassen. Diese speziellen Funktionen haben in ELF- Dateien auch ein eigenes Segment, das gegebenenfalls in den Codebereich des Adressraums eingelagert wird. Die restlichen Segmente enthalten ähnliche Daten – teils Verweise auf real in der ausführbaren Datei vorliegende Daten und teils Metadaten. Betrachten wir aber nun die weiteren interessanten Teile des Adressraums. Der Heap
Den Heap haben wir bereits weitgehend erklärt: Er ist der Speicher für globale Daten. Fordert man per malloc() bzw. new neuen globalen Speicher an, werden die entsprechenden Bytes hier reserviert und später auch wieder freigegeben. Da wir die wichtigsten Grundlagen bereits erläutert haben, wollen wir nun noch einmal kurz den Zusammenhang zum Paging darstellen. Der Kernel kann einem Prozess nur ganze Seiten zuweisen, was mitunter zur bereits angesprochenen internen Fragmentierung führt. Schließlich ist es unwahrscheinlich, dass man exakt so viel Platz angefordert hat, dass eine ganze Seite komplett gefüllt ist – selbst wenn man nur ein Byte braucht, muss im Extremfall dafür eine ganze 4-KB-Seite angefordert werden. Der verschenkte Platz ist dabei Verschnitt und wird als interne Fragmentierung bezeichnet. Würde man nun weitere Daten anfordern, würden diese natürlich auf der neuen, bisher nur mit einem Byte belegten Seite abgelegt werden – so lange, bis diese Seite voll ist. Natürlich kann dieser reservierte Speicher auch im Prinzip beliebig wieder freigegeben werden, was wiederum Lücken in den dicht gepackten Speicher reißen und somit wieder zu interner Fragmentierung führen kann. Man braucht also eine Speicherverwaltung, die solche Lücken auch wieder füllt. Entsprechend können Seiten auch ausgelagert werden, wenn ein Programm die angeforderten Speicherbereiche beziehungsweise die damit verknüpften Variablen längere Zeit nicht nutzt. In der Theorie unterscheidet man auch zwischen dem Working Set und dem Resident Set. Working Set
Das Working Set beschreibt alle in einem bestimmten Programmabschnitt benötigten Daten. Egal, ob ein Programm gerade in einer Schleife einen Zähler erhöht und im Schleifenrumpf ein Array bearbeitet oder ob ein Grafikprogramm einen Filter auf ein bestimmtes Bild anwendet: Immer wird ein Satz bestimmter Daten benötigt.
Die Seiten dieser Daten sollten natürlich im Hauptspeicher eingelagert sein; wäre dies nicht der Fall, hätte man mit vielen Page Faults zu kämpfen. Wenn das System so überlastet ist, dass es eigentlich nur noch mit dem Aus- und Einlagern von Speicherseiten beschäftigt ist, nennt man das Thrashing. Resident Set Die Menge aller aktuell im Hauptspeicher befindlichen Daten eines Prozesses bezeichnet man als Resident Set.
Um Thrashing zu vermeiden, sollte das Resident Set also zumindest immer das Working Set umfassen können. Dass möglichst keine Seiten aus dem aktuellen Working Set eines Prozesses auf die Festplatte ausgelagert werden, ist wiederum die Aufgabe des Swappers. Natürlich könnte sich der vom Swapper verwendete Seitenalgorithmus auch richtig dämlich anstellen und immer die aktuellen Seiten des Working Sets zum Auslagern vorschlagen. Da ein Programm aber immer sein aktuelles Working Set im Hauptspeicher benötigt, um arbeiten zu können, würden daher viele Page Faults auftreten und das Betriebssystem zur Wiedereinlagerung der Seiten von der Festplatte in den Hauptspeicher veranlassen – von Performance könnte man also nicht mehr wirklich sprechen. Das Betriebssystem wäre vor allem
mit sich selbst beschäftigt – eine Situation, die wir eigentlich vermeiden wollten. Der Stack
Der Stack speichert nun nicht die globalen, sondern mit den Funktionsaufrufen die jeweils lokalen Daten. Auch haben wir bereits erörtert, dass deswegen jeder Thread seinen eigenen Stack braucht. Anders gesagt: Diese Datenstruktur kann auch mehrfach im Adressraum vorkommen. Im Normalfall eines Prozesses gibt es also erst einmal nur einen Stack, der an der oberen Grenze des für den Benutzer bzw. die Benutzerin ansprechbaren Adressraums liegt und nach unten – dem Heap entgegen – wächst. Interessant ist weiterhin, inwieweit die Hardware, sprich der Prozessor, den Stack kennt und mit ihm arbeitet. Der Stack selbst liegt im virtuellen Speicherbereich des Prozesses und kann über Adressen angesprochen werden. Bei der Datenstruktur selbst interessiert dabei nur, was gerade aktuell, sprich »oben«[ 16 ], ist. Was liegt also näher, als ein spezielles Register des Prozessors immer zur aktuellen Spitze des Stacks zeigen zu lassen? Dieser Stackpointer muss natürlich bei einem Kontextwechsel – also beim Umschalten zu einem anderen Task oder auch zu einem anderen Thread derselben Applikation – entsprechend gesetzt werden. Daher hat ein Prozess- beziehungsweise ThreadKontrollblock auch immer einen Eintrag für den Stackpointer. Des Weiteren gibt es auch noch einen im Prinzip eigentlich unnötigen Framepointer, also ein weiteres Prozessorregister, das das Ende des aktuellen Kontextes auf dem Stack anzeigt. Den Framepointer braucht man eigentlich nur zur Beschleunigung
diverser Adressberechnungen, wenn eine Funktion zum Beispiel auf ihre Argumente zugreifen will. Aber schauen wir uns den Stack bei einem Funktionsaufruf einmal etwas genauer an.
Abbildung 2.7 Der Stack vor und nach einem Funktionsaufruf
In Abbildung 2.7 finden wir alles wieder, was wir schon dem Stack zugeordnet haben: die Rücksprungadresse, lokale Variablen, die Parameter, mit denen die Funktion aufgerufen wurde, und außerdem noch Informationen zur Verwaltung der Datenstruktur selbst. Diese Verwaltungsinformation ist, wie unschwer aus der Grafik zu erkennen ist, der alte Framepointer. Schließlich ist der Stackframe nicht immer gleich groß, denn Parameterzahl und lokale Variablen variieren von Funktion zu Funktion. Aus dem alten Framepointer kann schließlich der Zustand des Stacks vor dem Funktionsaufruf wiederhergestellt werden. Allerdings wird der Stack nie wirklich physisch gelöscht und mit Nullen überschrieben, was gewisse Auswirkungen auf die Sicherheit haben kann. Bösartige Programme könnten bestimmte Bibliotheksfunktionen aufrufen und hinterher
den Inhalt der lokalen Variablen dieser Funktionen inspizieren – Daten, auf die sie eigentlich keinen Zugriff haben dürften. Einen interessanten Nebeneffekt hat der Stack auch bei bestimmten Thread-Implementierungen. Prinzipiell kann man Threads nämlich beim Scheduling anders behandeln als Prozesse oder Tasks, was bei puren Userlevel-Threads auch einleuchtet. Dort weiß der Kernel nichts von den Threads der Anwendung, da die Implementierung und die Umschaltung der Threads von einer besonderen Bibliothek im Userspace vorgenommen wird. Aber auch bei KernellevelThreads ist es angebracht, das Scheduling von dem der Prozesse und Tasks zu unterscheiden. Es ist nämlich nicht fair, wenn eine Anwendung mit zwei Threads doppelt so viel Rechenleistung bekommt wie ein vergleichbares Programm mit nur einem Thread. Da Anwendungsprogrammierer sowieso am besten wissen, wann ihre Threads laufen und nicht mehr laufen können, ist in vielen Thread-Bibliotheken ein sogenanntes kooperatives Scheduling implementiert. Im Gegensatz zum präemptiven Scheduling wird die Ausführung eines Threads dort nicht beim Ende einer Zeitscheibe unterbrochen. Im Gegenteil: Es gibt überhaupt keine Zeitscheiben. Ein Thread meldet sich einfach selbst, wenn er nicht mehr rechnen kann oder auf Ergebnisse eines anderen Threads warten muss. Dazu ruft er eine meist yield() genannte spezielle Funktion auf, die dann einen anderen Thread laufen lässt. Der Thread ruft also eine Funktion auf und schreibt dabei verschiedenste Daten auf seinen Stack – unter anderem den Befehlszeiger. Der Thread merkt sich also mit anderen Worten selbst, wo er hinterher weitermachen muss. Das spart unserer yield()-Funktion viel Arbeit. Sie muss jetzt nur noch den nächsten zu bearbeitenden Thread auswählen und den Stack- und Framepointer des Prozessors auf dessen Stack zeigen lassen. Danach
kann yield() einfach ein return ausführen und so zum Aufrufer zurückkehren. Der Rest geschieht quasi von allein, da nun der alte Framepointer zurückgesetzt und der Stack langsam abgebaut wird, wobei natürlich auch die Rücksprungadresse des neuen Threads ausgelesen wird. Der neue Thread macht also dort weiter, wo er beim letzten Mal aufgehört hat: nach einem Aufruf von yield(). Bei neuen Threads ist der Ablauf ebenso einfach: Hier muss die ThreadBibliothek beziehungsweise der Kernel einfach nur den Stack so initialisieren, dass die Rücksprungadresse auf den ersten Befehl des neu zu startenden Threads zeigt – genial einfach und einfach genial. Nur der Vollständigkeit halber sei an dieser Stelle noch erwähnt, dass kooperatives Scheduling von ganzen Applikationen – also von Prozessen und Tasks – so überhaupt nicht funktioniert: Jeder Programmierer würde nämlich sein Programm für das ultimativ wichtigste und die Offenbarung überhaupt halten; und warum sollte er dann freiwillig Rechenzeit freigeben? Außerdem könnte eine Endlosschleife in einer einzigen falsch programmierten Anwendung das ganze System zum Stillstand bringen. Fazit: Kooperatives Scheduling ist, obwohl für das Scheduling von Threads durchaus üblich und eine gute Lösung, für Prozesse und Tasks ungeeignet – außer bei Echtzeitbetriebssystemen mit einer begrenzten Anzahl bestimmter Anwendungen. Die in den letzten Abschnitten beschriebenen Komponenten der unteren 3 Gigabyte des Adressraums bilden den für das Benutzerprogramm theoretisch komplett nutzbaren Adressrahmen. Wenden wir uns nun dem letzten Gigabyte zu. Das Betriebssystem
Der Speicherbereich des Kernels befindet sich immer an der oberen Grenze des Adressraums und ist bei Linux ein Gigabyte groß. Der
Kernel muss sich immer an derselben Stelle befinden, schließlich war die Interrupt-Behandlung der Grund dafür, einen besonderen Speicherbereich des Betriebssystems im Adressraum einer jeden Anwendung einzurichten. Tritt ein Interrupt auf, so geht der Kernel in den Ring 0 und will sofort zur Interrupt-Serviceroutine springen. Läge diese nun bei jedem Prozess an einer anderen Adresse, würde das ganze Prinzip nicht funktionieren. Dieser Speicherbereich ist bekanntlich auch so geschützt, dass aus dem Usermode nicht auf die Kerndaten zugegriffen werden kann – ein Zugriff aus Ring 3 würde mit einem Speicherzugriffsfehler quittiert. Dabei ist nicht nur der Kernel-Code selbst schützenswert, sondern insbesondere auch dessen Daten. Der Kernel besitzt selbstverständlich eigene Datenstrukturen wie Prozess- oder Thread-Kontrollblöcke, ebenso wie einen eigenen Kernel-Stack. Der Kernel-Stack wird vom Betriebssystem für die eigenen Funktionsaufrufe genutzt. Der Stack des Anwenderprogramms ist dafür aus mehreren Gründen nicht geeignet: Welcher Stack?
Bei einem Task mit mehreren Threads gibt es mehr als nur einen Stack im Adressrahmen des Prozesses. Das Betriebssystem müsste sich also auf einen Stack festlegen, was dann zu Problemen führen könnte, wenn zwischen den Threads umgeschaltet und somit der Stackpointer verändert wird. Taskwechsel
Überhaupt ist das Problem des Taskwechsels nicht geklärt. Was passiert, wenn der Kernel zu einem neuen Task schalten will und daher den Adressraum umschaltet? Die Daten des alten Prozesses und damit der Stack wären nicht mehr adressierbar und damit einfach weg – und irgendwann vielleicht plötzlich wieder da.
Sicherheit
Die Benutzer hätten außerdem vollen Zugriff auf ihren Stack, da dieser in ihrem Adressrahmen liegt. Mit etwas Glück könnten sie vielleicht sensible Daten des Kernels auslesen, die als lokale Variablen einer Funktion auf dem Stack gespeichert waren und nicht explizit mit Nullen überschrieben wurden. Das Betriebssystem braucht somit unbedingt seinen eigenen Stack. Beim Eintritt in den Kernel wird dann also unter anderem der Stackpointer so umgebogen, dass jetzt wirklich der Kernel-Stack benutzt werden kann. Obwohl ... einen Stack? Wir haben bereits Kernelmode-Threads vorgestellt, die Arbeiten des Kernels nebenläufig erledigen. Schließlich gibt es keinen Grund, streng einen Syscall nach dem anderen zu bearbeiten – stattdessen könnte man für jede Aktivität einen eigenen Kernelmode-Thread starten, der diese dann ausführt. Aber einzelne Threads brauchen eigentlich auch wieder jeder einen eigenen Stack. Ein Wort noch zu den physischen Speicherseiten des Betriebssystems: Zwar ist in jedem Adressraum das oberste Gigabyte für den Kernel reserviert, aber die betreffenden virtuellen Seiten verweisen natürlich überall auf dieselben physischen Seiten, realisieren also eine Art Shared Memory.
2.5 Eingabe und Ausgabe Kommen wir nun zur Ein- und Ausgabe, einer der wichtigsten Funktionen eines Betriebssystems. Wir wollen diese dabei einmal prinzipiell und einmal konkret am Beispiel des Dateisystems behandeln. Abbildung 2.8 verdeutlicht bereits, wie wichtig das Ein-/AusgabeSubsys- tem des Kernels ist. Mittlerweile besteht fast die Hälfte des Kernel-Codes aus Quelldateien für verschiedenste Treiber. Die anderen Komponenten des Kernels sind architekturspezifischer Code (dieser ist Treibern zumindest in seiner Hardwareabhängigkeit ähnlich), Code des Soundsystems, Dokumentation, Dateisystemimplementierungen, Headerdateien, Netzwerkstacks sowie diverse kleinere Komponenten – etwa zur Interprozesskommunikation oder kryptografische Routinen. Im Folgenden wollen wir zunächst erklären, was Sie sich unter Treibern vorstellen können. 2.5.1 Hardware und Treiber
Damit ein Gerät angesprochen werden kann, muss man »seine Sprache sprechen«. Man muss genau definierte Daten in spezielle Hardwareregister oder auch Speicherstellen laden, um bestimmte Effekte zu erzielen. Daten werden hin und her übertragen und am Ende druckt ein Drucker eine Textdatei oder man liest Daten von einer CD.
Abbildung 2.8 Der Anteil des Treibercodes am Kernel 2.6.10
Zur Übersetzung einer Schnittstelle zwischen den Benutzerprogrammen, die beispielsweise einen CD-Brenner unterstützen, und den eventuell von Hersteller zu Hersteller unterschiedlichen Hardwareschnittstellen benötigt man Treiber. Für die Anwenderprogramme werden die Geräte unter Unix als Dateien visualisiert. Als solche können sie natürlich von Programmen geöffnet und benutzt werden: Man sendet und empfängt Daten über Syscalls. Wie die zu sendenden Steuerdaten genau auszusehen haben, ist von Gerät zu Gerät unterschiedlich. Auch das vom Treiber bereitgestellte Interface kann sich von Gerät zu Gerät unterscheiden. Bei USB-Sticks oder CD-ROM-Laufwerken wird es sicherlich so beschaffen sein, dass man die Geräte leicht in das Dateisystem integrieren und auf die entsprechenden Dateien und Verzeichnisse zugreifen kann. Bei einem Drucker jedoch möchte man dem Gerät die zu druckenden Daten schicken; das Interface wird also eine völlig andere Struktur haben. Auch kann ein Gerät durch verschiedene Treiber durchaus mehrere Schnittstellen
anbieten: Eine Festplatte kann man sowohl über eine Art Dateisystem-Interface als auch direkt über das Interface des IDETreibers ansprechen. Module
Die meisten Treiber sind unter Linux als Module realisiert. Solche Module werden zur Laufzeit in den Kernel eingebunden und stellen dort dann eine bestimmte Funktionalität zur Verfügung. Dazu sind aber einige Voraussetzungen zu erfüllen: Interface
Der Kernel muss ein Interface anbieten, über das Module erst einmal geladen werden können. Einmal geladen, müssen sie auch irgendwie in den Kernel integriert werden können. Sicherheit
Lädt man externe Komponenten in den Kernel, so bedeutet dies immer ein Sicherheitsrisiko, und zwar in doppelter Hinsicht: Einerseits könnten schlecht programmierte Treiber das ganze System zum Absturz bringen, andererseits Hacker durch spezielle Module versuchen, den Kernel zu manipulieren. Gerätemanagement
Ein Modul beziehungsweise ein Treiber muss beim Laden mitteilen können: Ich bin jetzt für dieses oder jenes Gerät verantwortlich. Vielleicht muss mancher Treiber auch erst erkennen, ob und wie viele von ihm unterstützte Geräte angeschlossen sind. Was aber wäre die Alternative zu Treibern in Modulform? Treiber müssen teilweise privilegierte Befehle zur Kommunikation mit den zu steuernden Geräten nutzen, daher müssen sie zumindest zum großen Teil im Kernel-Mode ablaufen. Und wenn man sie nicht zur
Laufzeit in den Kernel laden kann, müssten sie schon von Anfang an in den Kernel-Code integriert sein. Würde man jedoch alle verfügbaren Treiber »ab Werk« direkt in den Kernel kompilieren, wäre der Kernel sehr groß und damit langsam sowie speicherfressend. Daher sind die meisten Distributionen dazu übergegangen, ihre Kernel mit in Modulform kompilierten Treibern auszuliefern. Der Benutzer bzw. die Benutzerin kann dann alle benötigten Module laden – oder das System erledigt diese Aufgabe automatisch. Zeichenorientierte Treiber
Treiber müssen ins System eingebunden werden, mit anderen Worten: Man benötigt eine einigermaßen uniforme Schnittstelle. Aber kann man zum Beispiel eine USB-Webcam und eine Festplatte in ein einheitliches und trotzdem konsistentes Muster bringen? Nun ja, Unix hat es zumindest versucht. Es unterscheidet zwischen zeichenorientierten und blockorientierten Geräten und klassifiziert damit auch die Treiber entsprechend. Der Unterschied ist dabei relativ simpel und doch signifikant: Ein zeichenorientiertes Gerät sendet und empfängt Daten direkt von Benutzerprogrammen.
Der Name der zeichenorientierten Geräte leitet sich von der Eigenschaft bestimmter serieller Schnittstellen ab, nur jeweils ein Zeichen während einer Zeiteinheit übertragen zu können. Diese Zeichen konnten nun aber direkt – also ohne Pufferung – gesendet und empfangen werden. Eine weitere wichtige Eigenschaft ist die, dass auf Daten im Allgemeinen nicht wahlfrei zugegriffen werden
kann. Man muss eben mit den Zeichen vorliebnehmen, die gerade an der Schnittstelle anliegen. Blockorientierte Treiber
Bei blockorientierten Geräten werden im Unterschied dazu meist ganze Datenblöcke auf einmal übertragen. Der klassische Vertreter dieser Gattung ist die Festplatte, bei der auch nur eine blockweise Übertragung der Daten sinnvoll ist. Der Lesevorgang bestimmter Daten gliedert sich nämlich in diese Schritte: 1. Aus der Blocknummer – einer Art Adresse – wird die physische Position der Daten ermittelt. 2. Der Lesekopf der Platte bewegt sich zur entsprechenden Stelle. 3. Im Mittel muss nun eine halbe Umdrehung gewartet werden, bis die Daten am Kopf anliegen. 4. Der Lesekopf liest die Daten. Die meiste Zeit braucht nun aber die Positionierung des Lesekopfs, denn wenn die Daten einmal am Kopf anliegen, geht das Einlesen sehr schnell. Mit anderen Worten: Es ist für eine Festplatte praktisch, mit einem Zugriff gleich mehrere Daten – zum Beispiel 512 Bytes – zu lesen, da die zeitaufwendige Positionierung dann eben nur einmal statt 512-mal erfolgen muss. Blockorientierte Geräte haben die gemeinsame Eigenschaft, dass die übertragenen Daten gepuffert werden. Außerdem kann auf die gespeicherten Blöcke wahlfrei, also in beliebiger Reihenfolge, zugegriffen werden. Darüber hinaus können Datenblöcke mehrfach gelesen werden.
Bei einer Festplatte hat diese Tatsache nun gewisse Vorteile wie auch Nachteile: Während des Arbeitens bringen zum Beispiel Schreibund Lesepuffer eine hohe Performance. Wenn ein Benutzer bzw. eine Benutzerin die ersten Bytes einer Datei lesen möchte, kann man schließlich auch gleich ein Readahead machen und die darauf folgenden Daten schon einmal vorsichtshalber im Hauptspeicher puffern. Dort können sie dann ohne Zeitverzug abgerufen werden, wenn ein Programm – was ziemlich wahrscheinlich ist – in der Datei weiterlesen will. Will es das nicht, gibt man den Puffer nach einiger Zeit wieder frei. Beim Schreibpuffer sieht das Ganze ähnlich aus: Um performanter zu arbeiten, werden Schreibzugriffe in der Regel nicht sofort, sondern erst in Zeiten geringer Systemauslastung ausgeführt. Wenn ein System nun aber nicht ordnungsgemäß heruntergefahren wird, kann es zu Datenverlusten bei eigentlich schon getätigten Schreibzugriffen kommen. Wenn die Daten n"amlich in den Puffer, aber eben noch nicht auf die Platte geschrieben wurden, sind sie weg. Ein interessantes Beispiel für die Semantik dieser Treiber ist eine USB-Festplatte. Es handelt sich bei diesem Gerät schließlich um eine blockorientierte Festplatte, die über einen seriellen, zeichenorientierten Anschluss mit dem System verbunden ist. Sinnvollerweise wird die Funktionalität der Festplatte über einen blockorientierten Treiber angesprochen, der aber intern wiederum über den USB-Anschluss und damit über einen zeichenorientierten Treiber die einzelnen Daten an die Platte schickt bzw. von ihr liest. Der wahlfreie Zugriff auf die Datenblöcke der Festplatte wird also über die am seriellen USB-Anschluss übertragenen Daten erledigt. Der Blocktreiber nutzt eine bestimmte Sprache zur Ansteuerung des Geräts und der zeichenorientierte USB-Treiber überträgt dann die
»Worte« dieser Sprache und gegebenenfalls zu lesende oder zu schreibende Daten. 2.5.2 Interaktion mit Geräten
Da wir im letzten Abschnitt die unterschiedlichen Treiber allgemein beschrieben haben, wollen wir im Folgenden den Zugriff auf sie aus dem Userspace heraus betrachten und dabei ihren internen Aufbau analysieren. Gehen wir also wieder ein paar Schritte zurück und führen wir uns vor Augen, dass Geräte unter Linux allesamt als Dateien unterhalb des /dev-Verzeichnisses repräsentiert sind. Die Frage ist nun, wie man diese Geräte und Ressourcen nutzen kann und wie der Treiber diese Nutzung unterstützt. Den passenden Treiber finden
Das Programm udev (»userspace /dev«) verwaltet das /devDateisystem. Es überwacht Hotplug-Ereignisse des Rechners, also Ereignisse wie »ein Gerät wurde mit einem USB-Port verbunden/von ihm getrennt«. Udev identifiziert die verbundenen Geräte über Serien-, Hersteller- oder Produktnummern und legt auf Basis vordefinierter Regeln Gerätedateien im /dev-Dateisystem an. Auf das Gerät zugreifen
Geräte sind also Dateien, auf die man im Wesentlichen mit den üblichen Syscalls zur Dateibearbeitung zugreifen wird. Bei der Kommunikation mit Gerätedateien werden die C-Funktionen fopen(), fprintf() usw. in der Regel nicht verwendet. Zwar greifen diese Funktionen intern auch auf die Syscalls zurück, allerdings
wird standardmäßig die gepufferte Ein-/Ausgabe benutzt, was im Regelfall für die Kommunikation mit Geräten nicht ideal ist. Die typischen Syscalls für den Gerätezugriff sind dabei: open() – öffnet eine Datei (dies ist notwendig, um in sie zu
schreiben und aus ihr zu lesen)
write() – schreibt in eine geöffnete Datei read() – liest aus einer geöffneten Datei close() – schließt eine geöffnete Datei lseek() – ändert den Schreib-/Lesezeiger einer geöffneten Datei,
also die Stelle in einer Datei, an der ein Programm arbeitet
ioctl() – bietet ausführliche Funktionen zur Gerätesteuerung
Diese Syscalls müssen nun natürlich vom Treiber als Callbacks bereitgestellt werden. Callbacks sind Funktionen, die genau dann ausgef"uhrt werden, wenn ein entsprechender Event – in diesem Fall der Aufruf des entsprechenden Syscalls auf eine Gerätedatei – auftritt. Wenn eine Applikation also mittels open() eine Gerätedatei öffnet, stellt der Kernel den zugehörigen Treiber anhand der bereits besprochenen Major/Minor- beziehungsweise der Gerätenummer fest. Danach erstellt er im Prozesskontext eine Datenstruktur vom Typ struct file, in der sämtliche Optionen des Dateizugriffs wie die Einstellung für blockierende oder nicht blockierende Ein-/Ausgabe oder natürlich auch die Informationen zur geöffneten Datei gespeichert werden. Als Nächstes wird der in der file_operations-Struktur vermerkte Callback für den open()-Syscall ausgerufen, dem unter anderem eine Referenz dieser file-Struktur übergeben wird. Anhand dieser Referenz wird auch bei allen anderen Callbacks die Treiberinstanz
referenziert. Eine Treiberinstanz ist notwendig, da ein Treiber die Möglichkeit haben muss, sitzungsspezifische Daten zu speichern. Solche Daten könnten zum Beispiel einen Zeiger umfassen, der die aktuelle Position in einem Datenstrom anzeigt. Dieser Zeiger muss natürlich pro geöffneter Datei eindeutig sein, selbst wenn ein Prozess ein Gerät mehrfach geöffnet hat. 2.5.3 Ein-/Ausgabe für Benutzerprogramme
Für Benutzerprogramme spiegelt sich dieser Kontext im Deskriptor wider, der nach einem erfolgreichen open() als Rückgabewert an das aufrufende Programm übergeben wird: #include
#include
#include
#include
int main()
{
// Ein Deskriptor ist nur eine Identifikationsnummer
int fd;
char text[256];
// Die Datei "test.c" lesend öffnen und den zurück-
// gegebenen Deskriptor der Variable fd zuweisen
fd = open( "test.c", O_RDONLY );
// Aus der Datei unter Angabe des Deskriptors lesen
read( fd, text, 256 );
// "text" verarbeiten
// Danach die Datei schließen
close( fd );
return 0;
}
Listing 2.23 Einen Deskriptor benutzen
Ein wichtiger Syscall im Zusammenhang mit der Ein-/Ausgabe auf Gerätedateien ist ioctl() (I/O-Control). Über diesen Syscall werden
alle Funktionalitäten abgebildet, die sich nicht in das standardisierte Interface einbauen lassen. 2.5.4 Dateisysteme
Ein besonderer Fall der Ein-/Ausgabe ist das Dateisystem, das wir im Folgenden näher behandeln wollen. Eigentlich müssen wir zwischen »Dateisystem« und »Dateisystem« unterscheiden, da Unix mehrere Schichten für die Interaktion mit Dateien benutzt. Über dem physischen Dateisystem, also der Hardware, liegt ein virtuelles Dateisystem, das die Verarbeitung von Daten vereinfacht. Der VFS-Layer
Die oberste Schicht des Dateisystems ist der sogenannte VFS-Layer (engl. virtual filesystem). Das virtuelle Dateisystem ist eine Schnittstelle, die die grundlegenden Funktionen beim Umgang mit Dateien von den physischen Dateisystemen abstrahiert: open() und close()
Wie Sie schon beim Umgang mit Treibern und Geräten gesehen haben, ist die Möglichkeit zum Öffnen und Schließen von Dateien essenziell. Mit dieser Architektur setzt das VFS jedoch eine zustandsbasierte Funktionsweise des Dateisystems voraus. Beim Netzwerkdateisystem NFS z.B. ist dies aber nicht gegeben: Dort gibt es keine open()- oder close()-Aufrufe, stattdessen müssen bei jedem lesenden oder schreibenden Zugriff der Dateiname sowie die Position innerhalb der Datei angegeben werden. Damit ein NFS-Dateisystem von einem entfernten Server nun in das VFS integriert werden kann, muss der lokale Treiber sich den jeweiligen Zustand einer geöffneten Datei merken und bei jedem Zugriff in die Anfragen für den NFS-Server übersetzen.
read() und write()
Hat man eine Datei einmal geöffnet, kann man über einen Deskriptor Daten an der aktuellen Position in der Datei lesen oder schreiben. Nachdem das VFS bereits beim open() festgestellt hat, zu welchem physischen Dateisystem ein Zugriff gehört, wird jeder read()- oder write()-Aufruf wieder direkt zum Treiber für das entsprechende Dateisystem weitergeleitet. create() und unlink()
Das VFS abstrahiert natürlich zudem auch das Erstellen und Löschen von Dateien. Die Erstellung wird dabei allerdings über den open()-Syscall abgewickelt. readdir()
Genauso muss auch ein Verzeichnis gelesen werden können. Schließlich ist die Art und Weise, wie ein Dateisystem auf einem Medium abgelegt ist, ebenfalls treiberspezifisch. Die Benutzer beziehungsweise ihre Programme greifen nun über solche uniformen Schnittstellen des VFS auf die Funktionen und Daten des physischen Dateisystems zu. Der Treiber des Dateisystems muss also entsprechende Schnittstellen anbieten, damit er in das VFS integriert werden kann. Das Einbinden eines Dateisystems in das VFS nennt man Mounting. Eingebunden werden die Dateisysteme unterhalb von bestimmten Verzeichnissen, den sogenannten Mountpoints. Definiert wird das Ganze in einer Datei im Userspace, /etc/fstab: # Proc-Verzeichnis
proc /proc proc defaults
# Festplatten-Partitionen
UUID=c5d055a1-8f36-41c3-9261-0399a905a7d5
/ ext3 relatime,errors=remount-ro UUID=c2ce32e7-38e4-4616-962e-8b824293537c
/home ext3 relatime # Swap
0 0
0 1
0 2
/dev/sda7 none swap sw
# Wechseldatenträger
/dev/scd0 /mnt/dvd udf,iso9660 user,noauto,exec,utf8
0 0
0 0
Listing 2.24 Eine /etc/fstab-Datei
Interessant sind für uns im Moment dabei vor allem die ersten beiden Spalten dieser Tabelle: Dort werden das Ger"at sowie der Mountpoint angegeben, wo das darauf befindliche Dateisystem eingehängt werden wird. Besonders interessant ist an dieser Stelle das Root-Dateisystem /. Die /etc/fstab befindet sich, wie gesagt, irgendwo auf dem Dateisystem, auf das man nur zugreifen kann, wenn man zumindest das Root-Dateisystem schon gemountet hat. Man hat also das klassische Henne-Ei-Problem, das nur gelöst werden kann, wenn der Kernel den Ort des Root-Dateisystems als Option beim Booten übergeben bekommt. So kennen die Bootmanager (bspw. grub und der veraltete lilo) eine Option root, mit der man dem zu bootenden Kernel mitteilt, was sein Root-Dateisystem sein soll. Von diesem kann er dann die fstab lesen und alle weiteren Dateisysteme einbinden.
2.6 Zusammenfassung In diesem Kapitel haben Sie bereits das Wichtigste über Linux gelernt: was der Kernel ist und wie er sich in das System integriert. Dazu wurden wichtige Fakten zur Architektur des Prozessors in Bezug zu Benutzerprogrammen und Multitasking gesetzt und die Syscalls als Einsprungpunkte in den Kernel erläutert. Nach den Aufgaben eines Betriebssystems wurden schließlich Prozesse und Tasks definiert und von den »leichtgewichtigen« Threads als puren Ausführungsfäden unterschieden. Als weitere wichtige Aufgabe wurde das Speichermanagement in allen Einzelheiten beschrieben. Dabei wurden sowohl das Paging als Aspekt der Software sowie die Unterstützung durch die Hardware besprochen. Am Ende standen die Ein- und Ausgabe sowie das zugehörige Treibermodell von Linux. Im nächsten Kapitel werden wir uns Linux von der anderen Seite – dem Userspace – nähern und anhand der Unix-Philosophie die essenziellen Grundlagen von Linux näher erläutern.
2.7 Aufgaben Sprücheklopfer
Sie begegnen einem Kollegen, der Ihnen die folgenden Aussagen vom Anfang des Kapitels auftischt. Wie nehmen Sie ihn verbal auseinander? »Warum programmiert man nicht endlich mal ein OS in Java, das ist doch so genial objektorientiert?« »Benutzerprogramme haben keinen direkten Zugriff auf die Hardware; alles läuft über den Kernel.« »Benutzerprogramme können gar nicht auf den Kernel zugreifen, der ist geschützt.« Seiten, Seiten, Seiten!
Welche Aufgaben verfolgen Seitenersetzungs-Algorithmen und welche Rolle spielt dabei der Pagetable? Wer sind Sie und was gehört zu Ihnen?
Wie werden Prozesse voneinander unterschieden? Woher weiß der Kernel, welcher Prozess welche geöffneten Dateien besitzt und welchem Benutzer und welcher Gruppe ein Prozess angehört?
3 Erste Schritte »Was für eine Philosophie man wähle,
hängt davon ab,
was für ein Mensch man ist.«
– Johann Gottlieb Fichte In diesem Kapitel erläutern wir die Grundlagen aus Anwendersicht, vom ersten Kontakt mit dem System bis hin zum Bewegen in der Shell. Viele der Themen werden später noch ausführlich behandelt, der folgende kurze Einstieg schafft jedoch eine gute Basis für spätere Kapitel. Im letzten Kapitel haben wir uns ausführlich mit dem Kernel und den Aufgaben eines Betriebssystems wie Linux auseinandergesetzt. In diesem Kapitel wollen wir uns nun mit dem Userland und den Grundlagen aus Anwendersicht beschäftigen. Als Userland, auch Userspace, bezeichnet man die Systemumgebung aus Sicht eines Benutzers bzw. einer Benutzerin. Wurden im ersten Kapitel also vorrangig interessante Hintergründe vermittelt und ein grundlegendes Verständnis für das Betriebssystem als Ganzes geschaffen, so möchten wir uns im Folgenden so langsam der Praxis zuwenden. Dazu setzen wir eigentlich nur voraus, dass Sie ein Linux-System, egal welcher Distribution, zur Hand haben. Für das Erste können Sie sich beispielsweise eine Live-Distribution herunterladen und sie von einem bootfähigen Medium oder auch in einer virtuellen Maschine starten.
3.1 Die Unix-Philosophie Um die Grundlagen aus Anwendersicht zu verstehen, hilft in jedem Fall ein Verständnis für die »Philosophie« hinter diesem Betriebssystem. Außerdem muss man verstehen, dass Unix und Linux eng verwandt sind – wer einen Linux-Rechner administrieren kann, braucht nur eine sehr kurze Einarbeitungszeit, um andere Unix-Systeme wie Solaris oder BSD zu verstehen. Alle diese Systeme haben auch eine weitere Gemeinsamkeit: Für WindowsAnwenderinnen und -Anwender wirken sie zunächst »anders« und »ungewohnt«, vielleicht sogar »umständlich«. Aber die Entwicklerinnen und Entwickler haben sich etwas bei der Erstellung dieses gemeinsamen Unix-Konzepts gedacht und wir wollen Ihnen diese Gedanken nun näherbringen. Zuerst einmal wurde Unix von Programmierern für Programmierer[ 17 ] entwickelt. Auf einem System können mehrere Benutzer mehrere Programme gleichzeitig nutzen, zum Beispiel um Software zu entwickeln oder anderweitig zu arbeiten. Die Benutzer sowie die einzelnen Programme können Daten gemeinsam nutzen sowie diese auf eine kontrollierte Art und Weise austauschen. Das Design von Unix setzt dabei einigermaßen intelligente Benutzer voraus, die in der Regel wissen, was sie tun – und sollte dies mal nicht der Fall sein oder sind böswillige Angreifer am Werk, ist das System durch die Implementierung von unterschiedlichen Benutzerkonten mit einem konsistenten Rechtesystem gut vor Manipulationen geschützt. All diese Ziele unterscheiden sich nun gewaltig von denen eines Einbenutzerbetriebssystems, das auch Anfängern ermöglichen will, eine Textverarbeitung zu benutzen. Dort möchte man die Benutzer führen und ein Stück weit auch bevormunden, da das System meistens besser weiß, was gut für sie ist, als diese selbst.
3.1.1 Kleine, spezialisierte Programme
Programmiererinnen, Admins oder erfahrene Anwender erwarten von einem System, dass es die ihm gestellten Aufgaben effizient löst. Das System muss sich nicht um seine Benutzer kümmern, denn diese wissen in der Regel selbst, was sie wollen und wie sie es erreichen. Das System muss dazu flexibel und mächtig sein, was Unix auf dem folgenden Weg zu erreichen versucht: Anstatt große und funktionsbeladene Applikationen anzubieten, werden kleine, spezialisierte Programme bereitgestellt. Jedes Programm sollte idealerweise nur eine Aufgabe erfüllen, diese aber optimal. Durch vielfältige Kombinationen dieser »Spezialisten« können Anwenderinnen und Anwender nun flexibel die ihnen gestellten Aufgaben bewältigen. Außerdem unterstützen alle Programme eine konsistente und redundanzarme Bedienung: Warum sollte man remove schreiben, wenn rm aussagekräftig genug und immer noch eindeutig ist? Außerdem sollte sich der Befehl analog zu anderen Befehlen verhalten: Wenn ls -R * alle Inhalte in einem Verzeichnis rekursiv auflistet, sollte rm -R * genau diese Dateien rekursiv löschen – und nicht etwa eine Datei namens * und eine Datei, deren Name aus einem Minus, gefolgt von einem großen »R« besteht. Überhaupt wird die textuelle Eingabe über die Shell der Bedienung des Systems über eine grafische Oberfläche in vielen Fällen vorgezogen. Bei knapp hundert Anschlägen pro Minute tippt sich so ein rm-Befehl viel schneller als man je zur Maus greifen, den Dateimanager durch Doppelklick starten, die Dateien heraussuchen,
sie mit der Maus markieren, das Kontextmenü durch einen Rechtsklick öffnen, den Punkt Löschen auswählen und die ganze Aktion durch das Klicken auf Ja in der Dialogbox bestätigen kann. Auch kann man in der Shell Befehle einfach kombinieren und häufig benutzte oder etwas komplexere Befehlsketten in ganze Skripte schreiben. Diese Skripte können auch zentral abgelegt und allen Benutzerinnen und Benutzern eines Systems zur Verfügung gestellt werden. Alternativ kann man diese Skripte zu bestimmten Zeitpunkten – beispielsweise jeden Tag, jede Woche, jeden ersten Sonntag im Monat um 3 Uhr oder in genau zwei Stunden – ausführen lassen. 3.1.2 Wenn du nichts zu sagen hast: Halt die Klappe
Ein weiterer wichtiger Punkt der Unix-Philosophie ist das Verhalten sowie indirekt auch die Bedienung der Programme. Programme unter Unix/Linux verhalten sich nämlich so, wie erfahrene Benutzerinnen und Benutzer es erwarten würden: Sie geben im Erfolgsfall keine Meldung aus, sondern nur im Fehlerfall oder wenn es anderweitig Ergebnisse zu präsentieren gibt. Ein »alles okay« am Ende ist schlicht unnötig und redundant, da es immerhin den Regelfall darstellen sollte. Außerdem werden Programme zu einem großen Teil nur über Parameter mit Eingaben gefüttert: Man startet zum Beispiel einen Texteditor mit der zu bearbeitenden Datei als Argument, anstatt vom Editor nach dem Start nach der Datei gefragt zu werden. Für Neulinge ist dies zum Teil recht frustrierend, da man oft Befehle
tippt, die dann keine Ausgabe erzeugen – aber gerade dann ist ja alles in Ordnung. Diese Prinzipien sind jedoch vor allem in der Shell anzutreffen, also wenn Sie direkt auf der Kommandozeile arbeiten. Unter grafischen Oberflächen sind die eben genannten Prinzipien nur schwer zu realisieren und oft auch nicht sinnvoll. 3.1.3 Die Shell
Ja, die Shell – viel wird von Neulingen oder Quereinsteigern über diese »anachronistische« Eingabeaufforderung geschimpft. Erfahrene Unix-Anwenderinnen und -Anwender möchten ihre Shell jedoch nicht missen und empfinden in der Regel umgekehrt ein System, das sie zur Mausbenutzung zwingt, als Zumutung. Natürlich gibt es auch unter Unix und Linux eine komplette und funktionsreiche grafische Oberfläche. Viele professionelle UnixAnwenderinnen und -Anwender nutzen diese Oberfläche nur, um viele grafische Terminals und damit viele Shells gleichzeitig im Blick zu haben. Außerdem bietet zum Beispiel die populäre DesktopUmgebung Gnome als grafische Oberfläche eine nahezu komplette Bedienung über Tastenkürzel und Shortcuts an. So muss man nicht einmal die Hand von der Tastatur nehmen, wenn man zu einem anderen (virtuellen) Bildschirm oder einer anderen grafischen Shell wechseln will. Aber natürlich gibt es auch Anwendungen, die sich in der Shell nur schlecht realisieren lassen. Ein populäres Beispiel dafür ist das Surfen im Web – es gibt zwar Webbrowser für eine Textoberfläche, aber eine wirkliche Alternative zu den grafischen Varianten sind sie nicht.
3.1.4 Administration
Ein weiterer wichtiger Bestandteil der Unix-Philosophie ist die Administration des Systems. Von vielen wird genau dies als Nachteil gesehen: Man beklagt sich, dass man bei Linux »basteln« muss, um zu einer vernünftigen Lösung zu kommen. Man beklagt den fehlenden Hardware-Support für die allerneueste Grafikkarte und dieses oder jenes nicht unterstützte exotische Feature bei einem neuen PC. Diese Klagen beruhen auf einem Missverständnis. Ja, Unix (und damit auch Linux) lässt sich bis ins letzte Detail konfigurieren und personalisieren. Schließlich handelt es sich dabei um ein System für professionelle Anwenderinnen und Anwender und weniger für absolute Computerneulinge. Und Profis können und wollen sich – zumindest in einem gewissen Rahmen – mit ihrem System auseinandersetzen. Außerdem zieht das Argument der Bastelei inzwischen nicht mehr wirklich, da heute eine frische Installation einer gängigen Linux-Distribution weitaus weniger Nacharbeit per Hand erfordert als ein frisches Windows – schließlich ist alle wichtige Software schon installiert und sinnvoll konfiguriert. 3.1.5 Netzwerktransparenz
Ein weiterer wichtiger Ansatz der Unix-Philosophie ist die Netzwerktransparenz. Bei einem durchdachten und konsistenten Mehrbenutzer- und Mehrprogrammkonzept hat man natürlich auch mit mehreren Rechnern keine Probleme. Die Netzwerktransparenz spiegelt sich schon in einem in Unix allgegenwärtigen Modell wider: dem Modell von Dienstnehmer (engl. client) und Dienstgeber (engl. server).
Bei diesem Prinzip der Systemarchitektur nutzt ein Client – meist ein direkt vom Benutzer bzw. von der Benutzerin gesteuertes Programm – die Dienste eines Servers. Ob dieser Server nun ein entfernter Rechner mit spezieller Software oder ein lokal im Hintergrund ablaufendes Programm ist, kann transparent geändert werden. Einige interessante Beispiele für die Netzwerktransparenz von Unix/Linux wollen wir im Folgenden aufzählen: Die grafische Oberfläche
Die grafische Oberfläche unter Linux ist auch netzwerktransparent, sowohl das traditionelle »X-WindowSystem«, oft auch kurz X11 genannt, wie auch die etwas neuere Implementierung Wayland. Sowohl X11 wie auch Wayland sind eigentlich nur Protokolle, die die Kommunikation regeln zwischen dem X-Server bzw. dem Wayland Compositor, die die Darstellung auf dem Desktop-PC des Benutzers vornehmen, und den vom Benutzer gestarteten X-Clients bzw. Wayland-Clients, also Programmen, die eine grafische Oberfläche benötigen. Insofern kann eine grafische Anwendung auf einem anderen Rechner gestartet sein als dem, auf dem sie angezeigt wird – das jeweilige Protokoll trennt die Ausführung und die Darstellung von grafischen Anwendungen. Dies ist auch ein schönes Beispiel für ein wichtiges, aber leider auch oft vernachlässigtes Prinzip bei der Entwicklung sauberer Systeme – der Orthogonalität: Halte alle Dinge auseinander, die nicht in einem unmittelbaren Zusammenhang stehen. Konkret sind hier die beiden Aspekte der Ausführung und Darstellung eines grafischen Programms sauber durch die Trennung von grafischem (X-)Client und X-Server bzw. Wayland-Compositor modelliert. Allein die Berücksichtigung dieses Prinzips der Orthogonalität ermöglicht es in diesem Fall, dass unter Linux bzw. unixartigen Betriebssystemen Szenarien nativ abgebildet
werden können, die unter anderen Betriebssystemen wie Windows separate Infrastruktur und Software (»Terminal Server«) benötigen. Der Logging-Dienst
Unter Unix wird sehr viel protokolliert, wobei das Management der Protokoll- beziehungsweise Logdateien von einem bestimmten Dienst, dem syslogd, übernommen wird. Die Anwendungen können nun über bestimmte Aufrufe mit diesem Dienst kommunizieren, der dann die Meldungen in die Dateien schreibt. Mit wenigen Änderungen an der Systemkonfiguration ist es möglich, die Anwendungen nicht mehr den lokal laufenden syslogd nutzen zu lassen, sondern einen auf einem fremden Rechner installierten syslogd. Die Eigenschaft, mit steigenden Anforderungen mitwachsen zu können, nennt man übrigens Skalierbarkeit. NFS
Über das virtuelle Dateisystem (VFS) kann man unter Unix unabhängig vom darunterliegenden Dateisystem auf Dateien und Verzeichnisse immer auf die gleiche Art und Weise zugreifen. Die Benutzer merken nicht, ob sie gerade auf einer CD-ROM oder einer lokalen Festplatte arbeiten. Dieses Konzept lässt sich auch auf das Netzwerk ausdehnen, bei dem zum Beispiel der für Unix typische NFS-Dienst ganze Verzeichnisbäume von einem Rechner exportieren und anderen Systemen im Netzwerk zur Verfügung stellen kann. Die exportierten Verzeichnisse können von anderen Rechnern schließlich regulär gemountet und wie lokale Speichermedien benutzt werden – für den Benutzer bzw. die Benutzerin macht es keinen Unterschied, dass die Dateien nicht lokal, sondern auf einem anderen Rechner gespeichert sind.
Diese Liste könnte man fast endlos fortsetzen. Um das DienstgeberKonzept zu unterstützen, bietet Unix spezielle Prozesse an: die Hintergrundprozesse oder Daemons. Ein Hintergrundprozess ist ein Prozess, der ohne Interaktion mit den Benutzerinnen und Benutzern seine Arbeit im Hintergrund verrichtet. Somit benötigt ein solcher Prozess keinen Zugang zur Tastatur oder direkt zum Bildschirm.
So viel also erst einmal zur Unix-Philosophie, der sich Linux selbstverständlich anschließt. Vielleicht haben Ihnen diese Themen schon etwas Lust auf mehr gemacht; auf jeden Fall werden wir später im Buch alles näher erläutern. Im Folgenden kommen wir nun endlich zum eigentlichen Thema des Kapitels: zu einer kurzen Einführung in Linux.
3.2 Der erste Kontakt mit dem System In diesem Abschnitt beschäftigen wir uns mit dem ersten Kontakt mit einem Linux-System. Dieser »erste Kontakt« kann natürlich nicht jeden Aspekt der Kontaktaufnahme mit dem System umfassend behandeln, daher werden wir später noch ausführlich auf einzelne Punkte eingehen. 3.2.1 Booten
Beginnen wir mit dem Start des Systems. Egal, ob bei einer Installation auf Festplatte oder beim Laden einer Live-Distribution – das Geschehen ist eigentlich immer gleich. Im BIOS wird festgelegt, auf welchen Medien in welcher Reihenfolge nach einem zu bootenden Betriebssystem gesucht werden soll. In diese Bootreihenfolge können nahezu alle Speichermedien wie USB-Sticks, CD-ROMs/DVDs oder auch Festplatten einbezogen werden. Wird nun auf einem der im BIOS angegebenen Bootlaufwerke ein zu startendes Betriebssystem gefunden, wird dieses auch geladen. Auf allen bootfähigen Medien wird nämlich nach einem gültigen MBR (Master Boot Record) gesucht. Dies ist zum Beispiel bei einer Festplatte immer der erste Sektor (entspricht den ersten 512 Bytes) der Festplatte. Er enthält dabei folgende Informationen: Bootloader
Der Bootloader besteht aus Code zum Laden eines Betriebssystems oder aus weiteren Bootloader-Codes. Damit von einem Medium gebootet werden kann, muss dieser Code gültig sein und ein Betriebssystem laden können.
Partitionstabelle
Die Partitionstabelle gibt an, welche Partitionen wo auf dem Medium vorhanden sind. Die Partitionstabelle besteht aus vier Einträgen zu je 16 Byte und ist damit insgesamt 64 Byte groß. Sie liegt relativ nah am Ende des Bootsektors bei Byte 446. Magic Number
Die fehlenden 2 Byte[ 18 ] bis zum Ende des Sektors werden nun mit einem Wert gefüllt, anhand dessen das BIOS entscheiden kann, ob es sich um ein bootbares Medium handelt oder nicht: Ist der Wert 0x55aa, so kann der Code am Anfang des MBR geladen werden. Ändert man diesen Wert, wird das Medium nicht als bootfähig erkannt. Sehen wir uns den Bootvorgang weiter an: Hat man Linux auf seinem System installiert, so wird mit ziemlicher Sicherheit ein Bootloader wie GRUB geladen. Mit diesem kann man zwischen allen auf diesem Medium installierten Betriebssystemen das zu startende auswählen – normalerweise wird jedoch bei keiner Eingabe nach wenigen Sekunden automatisch ein vordefiniertes System gestartet. Als Nächstes wird der Kernel geladen, der das System schließlich allm"ahlich initialisiert und mit dem Initprozess (erkennbar an der PID 1) auch den ersten Prozess des Userlands explizit startet. Dieser Prozess, in den meisten Distributionen ist das heutzutage »systemd«, übernimmt dann das eigentliche Starten des Systems, indem dafür vorgesehene Konfigurationsdateien ausgelesen und entsprechende Dienste im Hintergrund gestartet werden. Dabei werden verschiedene Systemkonfigurationen, sogenannte Targets, unterschieden. Je nach Target werden unterschiedliche Dienste gestartet. So gibt es zum Beispiel bei den meisten Systemen ein Target mit grafischer Oberfläche und einen ohne. Auch gibt es bestimmte Targets zum Herunterfahren bzw. Neustarten des
Systems, bei dem zuerst alle Dienste sauber gestoppt werden, bevor das System schließlich angehalten bzw. neu gestartet wird. 3.2.2 Login
Auch die Login-Dienste der Textoberfläche – realisiert durch das Programm (m)getty – werden durch den Initprozess gestartet. Je nach Target kann aber auch ein grafischer Login-Dienst wie GDM oder KDM gestartet sein. In jedem Fall werden die Nutzerinnen und Nutzer aufgefordert, sich mit einem Usernamen und einem Passwort einzuloggen, damit sie am System arbeiten können. Das Login ist dabei wieder ein typisches Beispiel für die Netzwerktransparenz: Normalerweise wird bei der Anmeldung einer Benutzerin bzw. eines Benutzer überprüft, ob der Benutzername in der lokalen Datei /etc/passwd verzeichnet ist und mit dem Passwort in der Datei /etc/shadow übereinstimmt. Setzt man im Netzwerk jedoch Dienste wie LDAP ein, kann man ein UnixSystem überreden, die Benutzerinformationen von einem solchen zentralen Server zu laden – die Anwenderin bzw. der Anwender selbst merkt davon nichts. Verbindet man dieses Feature noch geschickt mit dem Einsatz von NFS, kann so auf jedem System der Firma die gleiche Arbeitsumgebung zur Verfügung gestellt werden. 3.2.3 Arbeiten am System
Nach dem Einloggen kann man am System arbeiten. Je nach Aufgabenspektrum oder Präferenz kann dies in verschiedenen Umgebungen erfolgen. Auch die verwendeten Programme werden stark variieren. Eines ist jedoch für alle Benutzerinnen und Benutzer gleich: Der Ort ihrer Arbeit und der Platz zum Speichern wichtiger
Daten ist jeweils das Heimatverzeichnis (engl. home directory, im Deutschen oft auch Home-Verzeichnis genannt). Unter Linux besitzt jeder Benutzer sein eigenes Verzeichnis in /home, wohingegen dieses Verzeichnis unter anderen UnixSystemen zum Beispiel auch unterhalb von /usr liegen kann. Im Normalfall hat der Benutzer nur in seinem eigenen Verzeichnis das Recht, Dateien anzulegen, zu ändern und zu löschen – von Spezialfällen wie dem Verzeichnis für temporäre Dateien /tmp einmal abgesehen. Dafür besitzt er dort dann auch Narren- und Gestaltungsfreiheit. Wie man die eigenen Daten organisiert, ist jedem Benutzer und jeder Benutzerin selbst überlassen. Alle Programme können Dateien im jeweiligen Verzeichnis des Benutzers ablegen. Im Regelfall sind diese jedoch »versteckt«, werden also bei einem normalen Betrachten des Verzeichnisses nicht angezeigt. Die Namen versteckter Dateien beginnen alle mit einem Punkt; solche Dateien können natürlich unter Angabe spezieller Optionen dennoch angezeigt werden. In diesem Sinne sind sie also nicht versteckt, sondern werden im Normalfall schlicht ausgeblendet, um den Benutzerinnen und Benutzern einen besseren Überblick über die von ihnen selbst angelegten und bearbeiteten Dateien zu geben. 3.2.4 Die Linux-Verzeichnisstruktur
Wie Sie bereits wissen, besitzt Linux ein virtuelles Dateisystem, das von physischen Speichermedien auf eine Verzeichnisstruktur abstrahiert. Doch auch diese Verzeichnisstruktur selbst ist interessant, da sich das zugrunde liegende Konzept von anderen, nicht unixartigen Betriebssystemen wie Windows unterscheidet.
Im Folgenden wollen wir die wichtigsten Verzeichnisse und ihre Bedeutung kurz erläutern. Dazu müssen vorher noch einige Begriffe geklärt werden, mit denen die Daten später klassifiziert werden: shareable
Dateien sind shareable, wenn sie auf einem Rechner im Netzwerk gespeichert und auf anderen Rechnern genutzt werden können, die also das Prinzip der Netzwerktransparenz unterstützen. Beispielsweise ist ein Homeverzeichnis eines Benutzers shareable, da es theoretisch auf einem zentralen Server im Netzwerk gespeichert sein und auf dem PC, auf dem sich der betreffende Benutzer gerade angemeldet hat, genutzt werden kann. (Dateien, die den Zugriff auf lokale Systemressourcen wie Bildschirm oder Tastatur regeln, sind eher nicht im Netzwerk teilbar und damit unshareable.) static
Dateien sind statisch (engl. static), wenn sie nicht ohne die Intervention des Administrators bzw. der Administratorin geändert werden können. Typische Beispiele für statische, also im Normalfall nicht schreibbare Daten sind Programm-Binaries, Dokumentationen oder Systembibliotheken. variable
Im Gegensatz zu static-Dateien stehen variable, also zur Laufzeit veränderbare Daten. Solche veränderbaren Dateien sind zum Beispiel Logfiles, temporäre Dateien oder Datenbanken. Diese Kategorien finden sich nun in der Verzeichnisstruktur unter Linux wieder. Beginnen wir daher mit den wichtigsten Verzeichnissen und ihrer Bedeutung: /bin
Dieses Verzeichnis beinhaltet essenzielle (Shell-)Programme. Diese sind statisch und durchaus shareable.
/boot
Das /boot-Verzeichnis beinhaltet alle wichtigen Dateien zum Hochfahren des Systems, wozu vor allem der Kernel gehört. Diese Dateien sind im Allgemeinen statisch und nicht shareable, da sie durch verschiedene Treiber und die Konfiguration sehr systemspezifisch sind und direkt beim Systemstart verfügbar sein müssen. /dev
In diesem Verzeichnis finden sich die Gerätedateien. /etc
Alle systemweiten Konfigurationsdateien eines Rechners sollten im /etc-Verzeichnis abgelegt sein. Da sich eine Konfiguration selten selbst ändert, sind auch diese Daten statisch und aufgrund des personalisierenden Charakters einer Konfiguration eher als unshareable einzustufen.[ 19 ] /home
Das Home-Verzeichnis eines Users unter /home/username haben wir Ihnen bereits vorgestellt. Hier werden die eingeloggten Benutzerinnen und Benutzer in der Regel arbeiten. Auch benutzerspezifische, also nicht systemweite Einstellungen finden sich in diesem Verzeichnis. /lib
In diesem Verzeichnis finden sich alle essenziellen Bibliotheken. In dem Verzeichnis /lib/modules/ finden sich somit auch die Module, die zur Laufzeit dynamisch in den Kernel geladen werden können. /mnt
In /mnt sollten Wechseldatenträger wie DVDs oder USB-Sticks gemountet werden.
/opt
Damit Softwarepakete auch von Drittanbietern ins System integriert werden können, gibt es das /opt-Verzeichnis. Dort können entsprechend dem Firmen- oder Softwarenamen Unterverzeichnisse angelegt werden, in denen dann die jeweilige Software installiert werden kann. /proc
Im /proc-Verzeichnis findet sich ein gemountetes virtuelles Dateisystem, in dem sich Informationen über alle Prozesse und über das System abrufen lassen. /root
Dies ist das Home-Verzeichnis von root. Da man als root nicht direkt am System arbeiten sollte, wird dieses Verzeichnis recht selten genutzt werden. /sbin
In diesem Verzeichnis finden sich essenzielle System-Binaries. /tmp
Dies ist das Verzeichnis für temporäre Daten. Im Regelfall werden während des Bootens alte, von der letzten Sitzung zurückgebliebene Dateien gelöscht. /usr
Die /usr-Hierarchie ist die größte und wichtigste Ansammlung statischer, sharebarer Daten. Die wichtigsten Unterverzeichnisse finden Sie hier: /usr/bin/
Benutzerprogramme (Binaries) /usr/lib/
Bibliotheken für Benutzerprogramme
/usr/local/
Extra-Hierarchie für selbstkompilierte Software, in sich wieder genauso gegliedert wie /usr /usr/sbin/
nicht essenzielle Systemprogramme /usr/share/
architekturunabhängige Daten[ 20 ] /usr/src/
Verzeichnis für Quellcode (optional) Aus den Charakteristika dieser Daten ergibt sich die Möglichkeit, das Verzeichnis /usr auf eine eigene Partition zu legen, es readonly zu mounten und es ggf. im Netzwerk freizugeben und auf anderen Systemen zu mounten. Beim Aktualisieren des Systems muss dann natürlich ein Remount mit möglichem Schreibzugriff erfolgen, da sonst zum Beispiel keine Binaries ersetzt werden können. /var
Das /var-Verzeichnis umfasst ähnlich wie /usr eine ganze Hierarchie von Unterverzeichnissen mit speziellen Aufgaben. Im Gegensatz zu diesem sind die Daten in /var jedoch variabel und im Allgemeinen nicht shareable. /var/cache/
anwendungsspezifische Cache-Daten /var/lib/
variable Statusinformationen /var/local/
variable Daten für /usr/local
/var/lock/
Lockdateien[ 21 ] /var/log/
Logdateien /var/opt/
variable Daten für /opt /var/run/
für laufende Prozesse relevante Daten[ 22 ] /var/spool/
Spooling-Daten wie beispielsweise noch zu druckende Dateien oder noch nicht abgeholte Mails /var/tmp/
temporäre Dateien, die nicht bei einem Reboot gelöscht werden sollten Auch bei /var kann sich die Speicherung der Daten auf einer eigenen Partition oder Platte anbieten, um bei knapper werdendem Plattenplatz immer noch etwas Platz auf der RootPartition freihalten und damit ein funktionierendes System gewährleisten zu können. Mit Ausnahme des Home-Verzeichnisses wird man mit diesen Verzeichnissen in aller Regel jedoch nur als Administrator bzw. Administratorin zu tun haben. Das liegt vor allem am Rechtesystem: Bis auf die temporären Verzeichnisse wie /tmp/ oder /var/tmp/ dürfen normale Benutzerinnen und Benutzer in der Regel nur in ihr Homeverzeichnis schreiben. 3.2.5 Das Rechtesystem
Diese restriktive Handhabung des Schreibzugriffs ist sinnvoll, da so kein normaler Benutzer das System manipulieren oder umkonfigurieren kann. Wir wollen im Zusammenhang mit dem Rechtesystem als Erstes natürlich die Rechte etwas näher betrachten, die einem Benutzer bzw. einer Benutzerin gewährt oder nicht gewährt werden können. Write (w)
Das Schreibrecht: Hat ein Benutzer dieses Recht (man spricht auch von einem Rechte-Flag oder Rechte-Bit) auf eine Datei, so kann er sie zum Schreiben öffnen oder sie auch löschen. In Verzeichnissen, auf das ein Benutzer ein Schreibrecht hat, können durch diesen Dateien oder weitere Unterverzeichnisse angelegt werden. Read (r)
Das Leserecht: Dieses Recht erlaubt es einem Benutzer bzw. einer Benutzerin, lesend auf entsprechende Dateien oder Verzeichnisse zuzugreifen. Execute (x)
Dateien mit diesem Rechte-Flag können ausgeführt werden. Entweder handelt es sich bei diesen Dateien um binäre Formate wie ELF oder um Skriptdateien bestimmter Sprachen wie Bash oder Perl. Bei Letzteren muss jedoch in der ersten Zeile des Skripts der Interpreter genannt werden, mit dem die Datei ausgeführt werden kann. Dieses Rechte-Flag wird in erster Linie verwendet, um zwischen Programmen und Daten zu differenzieren, und seltener, um festzulegen, wer ein bestimmtes Programm ausführen darf und wer nicht. Bei Verzeichnissen hat dieses Flag eine etwas andere Semantik: Dort wird nämlich durch das x-Flag der Zugriff auf ein Verzeichnis
gesteuert. Wem dieses Recht nicht gewährt wird, dem bleibt das Wechseln in den entsprechenden Ordner verwehrt. Werden Rechte auf Dateien beziehungsweise Verzeichnisse vergeben, so müssen sich von einer bestimmten Rechtemaske auf einer Datei die Berechtigungen für jeden möglichen Benutzer ableiten lassen. Jedem Benutzer und jeder Benutzerin ist im System daher eine Benutzer-ID (UID, User ID) und mindestens eine Gruppen-ID (GID, Group ID) zugeordnet. Eine Datei wird nun auch einem Benutzer – nämlich dem Eigentümer beziehungsweise dem Ersteller der Datei – sowie einer Gruppe zugeordnet. Für den Rechtekontext bedeutet dies, dass man eine Rechtemaske setzen kann, die aus je einem Bit für Read, Write und Execute für den Eigentümer bzw. die Eigentümerin, die Gruppe und schließlich noch für den Rest der Welt besteht. Möchte eine Benutzerin nun auf eine Datei zugreifen, so wird zuerst geprüft, ob sie die Eigentümerin dieser Datei ist. Ist dies der Fall, so wird die entsprechende Rechtemaske zur Prüfung der Gültigkeit des geforderten Zugriffs herangezogen. Ist die Benutzerin nicht die Eigentümerin, so wird geprüft, ob sie in der Gruppe der Datei ist, um eventuell die Rechtemaske dieser Gruppe heranzuziehen. Ist auch dies nicht der Fall, werden automatisch die Rechte für den Rest der Welt angewendet. $ ls -l .bashrc
-rw-r----- 1 admin users 1379 10. Jun 2019
.bashrc
Listing 3.1 Die eine beispielhafte Rechtemaske
Im oben genannten Beispiel kann man sehen, dass die Datei /etc/passwd dem Benutzer admin und der Gruppe users gehört. Greift der Eigentümer admin nun selbst auf die Datei zu, kommt die erste Rechtemaske rw- zum Einsatz: Er darf die Datei lesen und schreiben, aber nicht ausführen. Ist der zugreifende Benutzer nicht
admin, aber Mitglied der Gruppe users, kommt die zweite
Rechtemaske r-- zum Einsatz. Ein Benugtzer der Gruupe könnte die Datei nur Lesen, aber nicht Schreiben oder Ausführen. Ist der zugreifende Benutzer auch nicht in der Gruppe, kommt die dritte Rechtemaske --- zum Einsatz und der Zugriff würde komplett verweigert. root
Eine Ausnahme in diesem Rechtekontext bildet der Benutzer root (UID 0), der immer auf alle Dateien Zugriff hat. Er ist, vom Eigentümer einer Datei abgesehen, auch der Einzige, der die Rechte auf eine Datei ändern kann. Dieser Superuser ist in der Regel der Administrator bzw. die Administratorin des Systems und verfügt sozusagen über absolute Macht durch den unbeschränkten Zugriff auf alle Dateien. Rechte und Hardware
Rechte werden nur auf Dateien oder Verzeichnisse vergeben. Da jedoch zum Beispiel Geräte und bestimmte Ressourcen als Dateien im System repräsentiert sind und Unix an sich generell sehr dateiorientiert ist, ergibt sich so wieder ein konsistentes Gesamtbild. Auch sollte erwähnt werden, dass es problemlos möglich ist, mit mehreren Benutzerinnen und Benutzern zur selben Zeit an einem System zu arbeiten. Natürlich ist ein PC in der Regel mit nur einem Bildschirm und nur einer Grafikkarte ausgestattet, jedoch kann zum Beispiel über den SSH-Dienst das Remote-Arbeiten, also ausgehend von anderen Rechnern im Netzwerk, ermöglicht werden.
3.2.6 Herunterfahren
Ein weiterer wichtiger Schritt im ersten Kontakt mit dem System ist das Herunterfahren. Wie heutzutage bei fast allen komplexeren Systemen üblich, kann man ein System nicht einfach beliebig von seiner Stromquelle trennen. Damit man es in einem konsistenten Zustand halten kann, müssen alle Programme ihre temporären Daten speichern, alle verwendeten Ressourcen freigeben und das Dateisystem in einem konsistenten Zustand hinterlassen. Vor allem die Problematik des Dateisystems ist offensichtlich, wenn man sich an das letzte Kapitel und die Tatsache erinnert, dass viele Daten zwischengespeichert und gepuffert werden, um die Performance des Systems zu erhöhen. Werden diese gepufferten Daten nicht zurückgeschrieben oder wird die Festplatte vielleicht sogar inmitten eines Schreibvorgangs unterbrochen, tritt ein Datenverlust auf oder es kommt zu inkonsistenten (Meta-)Daten auf der Festplatte. Ein System herunterfahren und solche Probleme vermeiden können Sie mit dem Befehl shutdown: shutdown -h now
Mit diesem Befehl hält man das System an (engl. halt). Dazu wird das System in den shutdown-Target überführt, wobei alle gestarteten Dienste über ihre Stop-Skripte beendet werden. Schließlich werden alle verbleibenden Prozesse über ein SIGTERM aufgefordert, sich zu beenden, um sie dann nach kurzer Zeit mit einem SIGKILL auf die »harte Tour« durch den Kernel zu beenden. Die Prozesse werden gesondert beendet, damit alle offenen Dateien noch geschlossen werden können. Ignorierte man diese
im Prozesskontrollblock vermerkten Daten, würden eventuell bisher nur gepufferte Schreibzugriffe nicht ausgeführt und gingen somit verloren. shutdown -r now
Mit diesem Befehl wird das System neu gestartet (engl. reboot). Die dazu notwendigen Aktionen, vom Herunterfahren der beim Systemstart von Initprozess bzw. systemd aktivierten Dienste bis zum Senden der Signale an alle Prozesse, entsprechen dem Vorgehen beim Systemhalt. Natürlich muss man diese Befehle nicht auf der Shell eingeben, sondern kann auch von einer grafischen Oberfläche wie Gnome aus ein System herunterfahren. Allerdings hat man auf der Shell die Möglichkeit, anstelle von now einen genauen Zeitpunkt anzugeben, zu dem das System heruntergefahren oder neu gestartet wird. Auch kann man hier eine Nachricht eingeben, die vor der shutdown-Aktion an alle eingeloggten Nutzer gesendet wird. 3.2.7 Wie Laufwerke bezeichnet werden
Wenn Sie eine Windows-Anwenderin bzw. ein Windows-Anwender sind, dann kennen Sie Laufwerksbezeichnungen als Buchstaben (etwa C: oder D:). Unter Linux ist das Prinzip ähnlich, Laufwerke werden hier allerdings als Gerätedateien repräsentiert und heißen daher anders. Wie für Gerätedateien üblich sind Dateien, die Laufwerksgeräte repräsentieren, im Verzeichnis /dev zu finden. Laufwerke werden (im Falle von CD-/DVD-, (S)ATA- und SCSILaufwerken) mit sdX bezeichnet, wobei anstelle des X ein Kleinbuchstabe eingesetzt wird. /dev/sda ist etwa eine typische Festplattenbezeichnung, genauso wie /dev/nvme. Es kann sich bei
den jeweiligen Geräten aber auch um CD-Laufwerke und Ähnliches handeln. Auch einzelne Partitionen sind unter Linux als Dateien vorhanden. So ist die erste Partition der Festplatte /dev/sda als /dev/sda1 ansprechbar, die zweite Partition als /dev/sda2 und so fort. Die genannten Bezeichner für Festplatten sind für die Systemkonfiguration von großer Bedeutung. Sie werden etwa verwendet, um anzugeben, wo eine Platte im Dateisystem eingehängt werden soll. Es kann allerdings sein, dass eine Festplatte umkonfiguriert und dadurch ihr Bezeichner verändert wird, was wiederum die Systemkonfiguration empfindlich treffen kann. Aus diesem Grund sind viele Distributionen dazu übergegangen, sogenannte UUIDs (Universally Unique Identifier) zu verwenden. Dabei handelt es sich um eindeutige Bezeichner für Laufwerke, die auch nach einer Umkonfiguration erhalten bleiben können. Sollten Sie also einmal eine Festplatte umstecken, so müssen Sie die Systemkonfiguration nicht ändern. Eine UUID ist eine eindeutige und zudem recht lange Hex-Zahl. Über das Programm blkid können Sie sich die UUIDs Ihrer Partitionen anzeigen lassen. $ blkid
/dev/sda1: UUID="7b898fa6-391e-4b81-888c-48ef10d7a95f"
SEC_TYPE="ext2" TYPE="ext3"
/dev/sdb1: UUID="7b76eae9-1d58-43b2-856e-f4c6b7a914f9"
SEC_TYPE="ext2" TYPE="ext3"
/dev/sdb2: UUID="c646f84c-2c4c-446b-ac09-9d398099867e"
TYPE="swap"
/dev/sdb3: UUID="018ad305-97b0-40a6-b8c0-54734cf6e6b3"
SEC_TYPE="ext2" TYPE="ext3"
Listing 3.2 Das Programm blkid zeigt die UUIDs des Systems an.
Die erste Spalte enthält die Partitionsbezeichnung. Darauf folgen die eigentliche UUID und zwei Dateisystemtyp-Angaben. Die Angabe TYPE steht für den eigentlichen Dateisystemtyp (hier also
»ext3«). Kann ein Dateisystem auch als ein anderes Dateisystem gemountet werden (das Dateisystem ext3 kann auch als ext2Dateisystem eingehängt werden, ist also rückwärtskompatibel), so gibt SEC_TYPE (secondary filesystem type) diesen alternativen Typ an. Möchten Sie nur die UUID einer bestimmten Partition angezeigt bekommen, können Sie deren Dateinamen auch an blkid übergeben: $ blkid /dev/sdb3
/dev/sdb3: UUID="018ad305-97b0-40a6-b8c0-54734cf6e6b3"
SEC_TYPE="ext2" TYPE="ext3"
Listing 3.3 Die UUID von /dev/sdb3 anzeigen
Die UUIDs sind als Links im Dateisystem präsent, können also auch durch das ls-Programm angezeigt werden. $ ls -l /dev/disk/by-uuid
insgesamt 0
lrwxrwxrwx 1 root root 10 2010-09-12 10:12
018ad305-97b0-40a6-b8c0-54734cf6e6b3 -> lrwxrwxrwx 1 root root 10 2010-09-12 10:12
7b76eae9-1d58-43b2-856e-f4c6b7a914f9 -> lrwxrwxrwx 1 root root 10 2010-09-12 10:12
7b898fa6-391e-4b81-888c-48ef10d7a95f -> lrwxrwxrwx 1 root root 10 2010-09-12 10:12
c646f84c-2c4c-446b-ac09-9d398099867e ->
Listing 3.4 UUIDs mit ls anzeigen
../../sdb3
../../sdb1
../../sda1
../../sdb2
3.3 Bewegen in der Shell Wir haben die Shell bereits als wichtigen Bestandteil der UnixPhilosophie vorgestellt und sind auch in den Beispielen bisher auf Befehle eingegangen. Im Folgenden wollen wir, um den Anspruch dieses Kapitels zu erfüllen, kurz die Grundlagen des Arbeitens in der Shell vorstellen. In Ihrer grafischen Oberfläche können Sie eine Shell bspw. über Programme wie »iTerm« oder »Terminal« starten. 3.3.1 Der Prompt
Die Eingabeaufforderung der Shell besteht nicht nur aus einem blinkenden Cursor für die Eingabe, sondern auch noch aus dem Prompt. Dieses gibt meist den Kontext der Arbeit durch die Anzeige des Rechner- und Benutzernamens sowie des Arbeitsverzeichnisses wieder. Allerdings kann jeder Benutzer seinen Prompt auch personalisieren und sogar farbig gestalten. $
user@host$
user@host:/home/user$
#
/root#
Listing 3.5 Beispiel-Prompts
Dass Informationen wie der Rechner- und Benutzername angezeigt werden, hilft vor allem beim Arbeiten auf verschiedenen Rechnern im Netzwerk. Das Arbeitsverzeichnis hilft dabei, den Ausgangspunkt relativer Pfade zu bestimmen. 3.3.2 Absolute und relative Pfade
Unix-Systeme kennen keine Laufwerke und sprechen alle Speichermedien über den VFS-Layer und einen Verzeichnisbaum an. So ergeben sich zwei verschiedene Arten, wie man Dateien und Verzeichnisse referenzieren kann. Bei der Angabe eines absoluten Pfades wird der Dateiname immer von der Wurzel / des Dateisystems aus angegeben.
Dies kann jedoch zu recht langen Eingaben und redundanten Angaben führen, falls ein Benutzer bzw. eine Benutzerin hauptsächlich in einem bestimmten Verzeichnis arbeitet. Daher besitzt jeder Prozess – und damit natürlich auch jede Shell – mit dem aktuellen Arbeitsverzeichnis einen Kontext. Von diesem Verzeichnis aus kann man Verzeichnis- oder Dateinamen auch relativ angeben. Ein relativer Pfad beginnt nicht mit der Wurzel des Dateisystems, sondern wird relativ zum aktuellen Arbeitsverzeichnis des Prozesses interpretiert, indem das Arbeitsverzeichnis implizit vor den relativen Pfad gesetzt und das Ergebnis schließlich als absoluter Pfad gelesen wird.
Erst so wird es möglich, dass man zum Beispiel einen Texteditor mit text.txt als Argument aufrufen kann, anstatt sich über den Pfad /home/user/text.txt auf die Datei zu beziehen. 3.3.3 pwd
Sollte der Prompt einer Shell einmal weniger aussagekräftig sein, so kann man sich das Arbeitsverzeichnis auch mit dem pwd-Befehl
anzeigen lassen. Die Abkürzung steht für print working directory. $ pwd
/home/jploetner
Listing 3.6 Arbeitsverzeichnis mit pwd ausgeben
Ein neuer Prozess entsteht unter Unix stets als Kopie eines bereits bestehenden Prozesses. Als Kopie erbt er alle Eigenschaften wie eben auch das Arbeitsverzeichnis. 3.3.4 cd
Natürlich kann das Arbeitsverzeichnis der Shell auch durch einen bestimmten Befehl gewechselt werden. Der cd-Befehl ist die Abkürzung für change directory und erwartet eine Pfadangabe als Argument. Diese kann selbstverständlich wieder relativ oder absolut gemacht werden, wobei man zwei Spezialfälle relativer Pfade unterscheidet: ».«
Jedes Verzeichnis enthält eine Referenz auf sich selbst, die der Kürze halber mit einem einfachen Punkt bezeichnet wird. Diesen Punkt benötigt man vor allem, wenn man eine ausführbare Datei starten möchte, die sich vielleicht im Homeverzeichnis des Benutzers bzw. der Benutzerin befindet. Normalerweise sucht die Shell nur in bestimmten Ordnern – diese Ordner werden in einer speziellen Shell-Variable, dem PATH, gespeichert – nach ausführbaren Dateien, sodass man den Pfad zu einem an anderer Stelle gespeicherten Programm explizit angeben muss: $ ./schach
Listing 3.7 Programm aus dem aktuellen Verzeichnis starten
Dieser Pfad referenziert nun eine Datei schach im aktuellen Verzeichnis. Für den cd-Befehl braucht man die Selbstreferenz jedoch selten, da man schließlich das Verzeichnis wechseln möchte. »..«
Mit den zwei Punkten bezeichnet man das nächsthöhere Verzeichnis. Zusammen mit den direkt referenzierbaren Unterverzeichnissen ergibt sich so die komplette Navigation in der Shell: $ pwd
/home/jploetner
$ cd ..
$ pwd
/home
$ cd jploetner
$ pwd
/home/jploetner
Listing 3.8 Navigation in der Shell
Interessanterweise hat aus Konsistenzgründen auch das Wurzelverzeichnis / einen solchen Backlink. Dieser zeigt jedoch wieder auf das Wurzelverzeichnis selbst. Am Beispiel von cd kann man auch sehr gut sehen, dass Shellbefehle in der Regel im Erfolgsfall keine Meldung ausgeben. Das Kommando erledigt nur seine Aufgabe und wenn diese zur Zufriedenheit des Benutzers bzw. der Benutzerin ausgeführt werden konnte, muss es dies nicht extra kundtun. Etwas anderes gilt natürlich im Fehlerfall, also wenn man mit cd in ein nicht existierendes Verzeichnis wechseln will: $ cd swendzel
-bash: cd: swendzel: No such file or directory
$
Listing 3.9 Ein fehlgeschlagener cd-Aufruf
Was dieses -bash in der obigen Ausgabe bedeutet, erläutern wir in Kapitel 4, wo wir den Unterschied zwischen Programmen und ShellBuiltins erklären.
3.4 Arbeiten mit Dateien Unser nächster Schwerpunkt soll das Arbeiten mit Dateien sein. Zuerst wollen wir dabei betrachten, wie man sich Dateien in der Shell anzeigen lassen kann. 3.4.1 ls
Für die Auflistung von Dateien in der Shell ist der ls-Befehl zuständig. Ohne Argument zeigt ls den Inhalt des Arbeitsverzeichnisses an, allerdings kann man sich die Dateien jedes beliebigen Verzeichnisses durch dessen Angabe als Argument auflisten lassen: $ pwd
/usr/src/linux-2.6.10
$ ls
arch crypto fs ipc ...
CREDITS drivers init lib REPORTING-BUGS sound
$ ls /home
jploetner mploetner aploetner
MAINTAINERS
mm
Listing 3.10 Dateien auflisten mit ls
Im Normalfall – also wie hier im Listing ohne Angabe weiterer Optionen – zeigt ls nur Dateien und Verzeichnisse an. Mit einem Punkt beginnende und somit »versteckte« Elemente eines Verzeichnisses werden ausgeblendet. Möchte man sich diese Dateien dennoch alle anzeigen lassen, sollte man das -a-Flag benutzen: $ ls
test test.c
$ ls -a
test test.c .vimlog
Listing 3.11 Versteckte Dateien anzeigen
Natürlich kann ls auch viele mit einer Datei verknüpfte Metadaten wie Rechte oder Eigentümer und Gruppe anzeigen. Man will mit anderen Worten ein langes Listing, das man mit dem -l-Flag erhält: $ ls -l
-rwxr-xr-x 1 jploetner users 28 05-03-13 22:03 test
-rw-r--r-- 1 jploetner users 371 05-02-10 13:40 test.c
Listing 3.12 Lange und ausführliche Dateilistings
In diesem Beispiel können Sie das Rechtesystem auch in der Praxis sehen: Beide Dateien haben den Eigentümer jploetner und gehören zur Gruppe users. Ganz am Anfang sieht man auch drei Dreiertupel, die in der Reihenfolge »Eigentümer«, »Gruppe« und »Sonstige« jeweils über die Berechtigungen r (read), w (write) und x (execute) Auskunft geben. Wird der entsprechende Buchstabe in der Ausgabe von ls angezeigt, so wird das Recht gewährt. Andernfalls signalisiert ein Minus das Fehlen der entsprechenden Berechtigung. 3.4.2 more, less und most
Möchte man sich textuelle Dateien (etwa Shellskripte, ein README oder Dateien aus /etc) ansehen, so kann man sich zum Beispiel zweier Programme bedienen: more und less. Beide Tools sind sogenannte Pager und zeigen den Inhalt einer Datei als Text interpretiert an. Sie unterscheiden sich dabei nur in ihrer Bedienung, wobei less etwas benutzerfreundlicher ist als more. Bei more kann man nur mittels der (¢)-Taste jeweils eine Zeile tiefer scrollen, less dagegen erlaubt eine intuitivere und umfassendere Bedienung mittels Cursor- und den Bildlauftasten. Bei beiden Pagern kann man in der angezeigten Datei suchen, indem man den Slash (/), gefolgt vom Suchbegriff und (¢), eintippt. Über die Taste
(N) kann man schließlich zur nächsten Fundstelle des Suchbegriffs
springen.
Mit dem Programm most können Sie gegenüber less nochmals an Bedienkomfort gewinnen, denn most kann farbige Ausgaben verschiedener Eingabe-Typen (etwa Manpages) erstellen. Sowohl less als auch most können mehrere Dateien gleichzeitig geöffnet haben (das nächste Fenster erhält man durch :n, bei less kann das vorherige zudem mit :p erreicht werden). In most können auch Fenster aufgeteilt werden, sodass Sie mehrere geöffnete Dateien gleichzeitig betrachten können (dazu (Strg) + (X) und anschließend (2) drücken). Beenden können Sie alle drei Programme durch die Taste (Q). 3.4.3 Und Dateitypen?
Einige Verwirrung bei der Arbeit mit Dateien entsteht hinsichtlich der Verwendung von Dateiendungen. Endungen wie .jpg oder .txt sollten ja im Regelfall einen Rückschluss auf den Dateiinhalt erlauben, also im Beispiel auf eine Bild- beziehungsweise Textdatei hinweisen. Unter Linux und anderen Unix-Varianten ist der Punkt nun ein gültiger Bestandteil des Dateinamens. Mit Ausnahme eines Punkts als ersten Buchstaben im Dateinamen – der bekanntlich eine Datei »versteckt« – kann man den Punkt so oft man will oder eben auch gar nicht verwenden. Der Kernel kann nur Programme starten, keine Bild- oder Textdateien. Auf Dateien wird unabhängig vom Dateityp über ein einheitliches Interface mittels open(), read() und auch write() zugegriffen. Für das System sind alle Dateien nur eine
Folge von Bits und Bytes. Die Anwendung allein ist dafür zuständig, diese Daten zu interpretieren. Folglich sind Erweiterungen des Dateinamens wie .jpg und .txt nur für Sie als Benutzerin oder Benutzer relevant. Sie können auf den ersten Blick erkennen, um welche Art Datei es sich handelt. Wenn Sie nun aber unbedingt eine Musikdatei in einem Texteditor bearbeiten wollen, können Sie dies tun – dem System ist das egal. Eine Einschränkung gilt jedoch für grafische Oberflächen: Wenn Sie mit einem Klick auf eine Textdatei diese Datei in einen Editor laden und anschließend bearbeiten wollen, so muss eine gewisse Verknüpfung vom Dateityp zu der für diesen Typ bevorzugten Anwendung bestehen. Der Einfachheit halber bietet es sich dann natürlich an, diese Zuordnung aufgrund der Dateiendungen vorzunehmen.[ 23 ] file
Eine weitere Möglichkeit ist der Versuch, den Inhalt aufgrund bestimmter charakteristischer Muster zu erkennen. Für die Kommandozeile ist hier das file-Tool das Programm der Wahl: Wird es mit einer zu untersuchenden Datei aufgerufen, gibt es den aufgrund einer Inhaltsanalyse vermuteten Dateityp aus: $ file test.c
test.c: ASCII C program text
$ file test
test:ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for
GNU/Linux 2.2.0, dynamically linked (uses shared libs), not stripped
Listing 3.13 In Aktion: file
Je nach Dateityp kann die Analyse dabei relativ kurz oder auch etwas ausführlicher ausfallen.
3.5 Der Systemstatus Selbstverständlich können Sie in einem gewissen Rahmen auch den Systemstatus kontrollieren. Diesem Thema ist ein eigenes Kapitel zur Administration gewidmet, jedoch wollen wir vorab einige grundlegende und einfache Programme vorstellen. 3.5.1 uname
Mit dem uname-Befehl können Sie unter Linux zum Beispiel feststellen, welche Kernel-Version gebootet ist. Aber auch unter anderen Unix-Systemen kann man Näheres über die eingesetzte Betriebssystemversion oder die Rechnerarchitektur erfahren. Alle verfügbaren Informationen können Sie sich mit dem -a-Parameter anzeigen lassen: $ uname -a
Linux sw-ThinkPad-X13-Yoga-Gen-1 5.11.0-36-generic \
#40 20.04.1-Ubuntu SMP Sat Sep 18 02:14:19 UTC \
2021 x86_64 x86_64 x86_64 GNU/Linux
Listing 3.14 uname
Das Format der ausgegebenen Daten kann dabei von Linux-Version zu Linux-Version variieren, da nicht alle dieselbe Implementierung des Programms verwenden. 3.5.2 uptime
Ein weiterer interessanter Befehl ist uptime. Mit diesem Kommando kann man sich darüber informieren, wie lange ein System nun
schon ohne Neustart läuft – vor allem bei Servern kann dies interessant, aber oft auch wichtig sein. $ uptime
3:21:38 up 4:03, 1 user, load average: 2.09,0.85,0.59
Listing 3.15 uptime
Aus der Ausgabe lesen Sie zunächst die aktuelle Systemzeit, gefolgt von der Uptime des Rechners und einigen Daten zur Auslastung ab. 3.5.3 date
Mit dem Befehl date können Sie die Systemzeit sowie das Datum abfragen und auch setzen. Ohne Optionen zeigt das Tool die Uhrzeit samt Datum an: $ date
So Apr 11 19:09:22 CEST 2021
Listing 3.16 Die Zeit auslesen
Das Setzen der Zeit geschieht nun über den Parameter -s, gefolgt von der neuen Zeit. Damit die Benutzerinnen und Benutzer mit einem solchen Befehl keine Spielchen treiben und vielleicht zeitkritische Anwendungen durcheinanderbringen, ist das Setzen der Systemzeit nur dem Administrator root erlaubt: # date -s 20:36:40
So Apr 11 20:36:40 CEST 2021
Listing 3.17 Die Zeit setzen
Auch wenn es etwas ungewöhnlich ist, aber der Befehl date ist auch für die Uhrzeit zuständig. Es gibt zwar einen time-Befehl, doch hat dieser nichts mit der Uhrzeit, sondern vielmehr mit der
Zeitmessung zu tun und wird von uns im Kapitel zur Softwareentwicklung behandelt.
3.6 Hilfe Zu guter Letzt fehlt uns für eine komplette Betrachtung des Einstiegs noch die Möglichkeit, Hilfe zu erhalten. Schließlich sind die Optionen und Möglichkeiten vieler Programme so reichhaltig, dass man sie kaum komplett im Kopf behalten kann. Vor allem in nicht ganz alltäglichen Situationen wird man gerne einmal auf Befehlsreferenzen zurückgreifen. 3.6.1 Manpages
Eine solche Befehlsreferenz finden Sie zum einen natürlich am Ende dieses Buches, zum anderen aber auch in den Manpages. Diese sind das traditionelle Hilfesystem für Unix und somit (wie auch nicht anders zu erwarten) in erster Linie über die Shell erreichbar. Zu fast allen Befehlen und Programmen gibt es eine Handbuchseite (engl. manual page), die aus der Shell heraus mit dem man-Kommando betrachtet werden kann. Das Scrollen funktioniert dabei wie gewohnt und das Suchen erfolgt wie bei less oder auch beim vi über die (/)-Taste, gefolgt vom Suchausdruck und (¢), sowie Betätigen der Taste (N) zum Aufrufen der nächsten Fundstelle. $ man ls
Listing 3.18 Aufrufen der Manpage für ls
Manpages enthalten dabei üblicherweise eine kurze Beschreibung des Programms sowie eine komplette Referenz der verfügbaren Kommandozeilenoptionen. Nur selten findet sich ein ausführlicheres Beispiel in einer Manpage. Und so passt diese Hilfe wieder zur UnixPhilosophie: Erfahrene Nutzer wollen nur kurz die Syntax bestimmter Optionen nachschlagen und sich dafür nicht durch seitenlange Einführungen quälen müssen. Sections
Für manche Stichwörter gibt es mehr als nur ein Hilfethema und somit auch mehr als eine Manpage. Ein gutes Beispiel dafür ist das Programm man selbst: Es gibt zu diesem Thema einmal eine Hilfeseite zur Bedienung des manProgramms und eine Hilfeseite zur Erstellung von Manpages. Damit man Hilfeseiten zu unterschiedlichen Themenkomplexen unterscheiden kann, gibt es unterschiedliche Sections (Abschnitte), in die die Manpages eingeteilt werden: 1. ausführbare Programme oder Shell-Befehle 2. Systemaufrufe (Kernel-Funktionen) 3. Bibliotheksaufrufe (Funktionen in Systembibliotheken) 4. spezielle Dateien (gewöhnlich in /dev) 5. Dateiformate und Konventionen, z. B. /etc/passwd 6. Spiele 7. Makropakete und Konventionen, z. B. man(7), groff(7) 8. Systemadministrationsbefehle (in der Regel nur für root) 9. Kernel-Routinen (linux-spezifisch) Die Sektionen sind im System als einfache Verzeichnisse realisiert, in denen dann jeweils die Manpages der entsprechenden Sektionen abgelegt sind. Die Manpages selbst sind wiederum nur Dateien in bestimmter Formatierung. Möchte man explizit auf eine Seite innerhalb einer Sektion zugreifen, so gibt man beim Aufruf von man einfach die Sektionsnummer des eigentlichen Hilfethemas an: $ man 1 write
$ man 2 write
Listing 3.19 Unterschiedliche Man-Sektionen
In diesem Beispiel wird zuerst die Manpage für das ausführbare Programm write aus der Sektion 1 und danach die Manpage zum Syscall write() aus der Sektion 2 aufgerufen. Lässt man diese explizite Angabe der Sektionsnummer weg und tippt nur man write, so wird die Manpage aus der niedrigsten Sektion – also in unserem Fall die Manpage aus Sektion 1 zum Programm write –
angezeigt. Um die Verwirrung zu reduzieren, wird für den Bezug auf eine bestimmte Sektion diese in Klammern nach dem Befehl angegeben, also beispielsweise write(2). whatis
Das kleine Programm whatis hilft uns nun, alle Sektionen zu einem bestimmten Thema herauszufinden. Das Tool ist dabei lediglich ein Frontend für den Aufruf von man mit dem Parameter -f: $ whatis write
write (1) write (2) $ man -f write
write (1) write (2)
- send a message to another user
- write to a file descriptor
- send a message to another user
- write to a file descriptor
Listing 3.20 whatis
Angezeigt werden also der Titel der Manpage, die Sektionsnummer sowie eine kurze Beschreibung des Seiteninhalts. apropos
Eine etwas weiter gefasste Suche ermöglicht das Tool apropos, das wiederum nur Frontend für man mit der Option -k ist: $ apropos write
…
kwrite (1) llseek (2) login (3) …
- KDE text editor
- reposition read/write file offset
- write utmp and wtmp entries
Listing 3.21 apropos
Hier werden alle Manpages angezeigt, bei denen im Namen der Seite oder in der Kurzbeschreibung auf das Suchwort Bezug genommen wird. Beide Tools – whatis und apropos – ergänzen somit das Manpage-Hilfesystem von Unix. 3.6.2 GNU info
Ähnlich wie man funktioniert das Programm info der GNU-Community. Die Bedienung ist etwas anders, aber eigentlich auch recht intuitiv. Der Grund für ein eigenes Hilfesystem dieser Open-Source-Gruppe liegt in der Abkürzung GNU selbst: GNU is Not Unix. Mit info sollte ein eigenes Hilfesystem für ein komplett freies GNU-basiertes Betriebssystem geschaffen werden. Mittlerweile spricht man teilweise von GNU/Linux, um auszudrücken, dass Linux zwar den Systemkern, aber GNU die wichtigsten grundlegenden Systemtools zur Verfügung stellt. Aber natürlich gibt es alle GNU-Infoseiten auch als Manpages. 3.6.3 Programmdokumentation
Solche Manual- oder Infoseiten sind natürlich meist ein zentraler Bestandteil der Dokumentation von Softwarepaketen. Außerdem werden oft sogenannte README-Dateien nach /usr/doc oder /usr/share/doc installiert, die noch einmal im Detail Auskunft über spezielle Funktionen und Aspekte der Bedienung geben. Eine Auflistung aller verfügbaren Optionen und damit eine ähnliche Ausgabe wie in den Manpages kann man oft durch die Angabe der Parameter --help oder -h auf der Kommandozeile erhalten. Ansonsten hilft meistens auch die Entwicklerdokumentation weiter, die man bei Open-Source-Projekten oft auf GitHub (http://www.github.com) findet. Alternativ gibt es für jede Distribution auch noch zahlreiche Foren auf den entsprechenden Internetseiten. Bei Linux-Fragen sind oft diverse Mailinglisten und Newsgroups recht hilfreich. Ansonsten hilft natürlich auch immer die Suchmaschine Ihrer Wahl, gefüttert mit passenden Suchbegriffen.
3.7 Zusammenfassung In diesem Kapitel haben Sie anhand der Unix-Philosophie die Grundlagen von Linux und allen verwandten Unix-Systemen kennengelernt – und hoffentlich auch verstanden. Dabei sind wir vor allem auf die Sicht- und weniger auf die Funktionsweise des Systems eingegangen. Sicher sind Sie über einige interessante Anmerkungen zum Dateisystemlayout, zu den Rechten oder auch zu den Dateitypen gestolpert. Außerdem hatten Sie bereits Ihren ersten Kontakt mit der Shell, den wir in den nächsten Kapiteln[ 24 ] intensivieren werden. Im Anschluss an diese Shell-Grundlagen werden wir uns mit der Administration und Verwaltung von Linux befassen.
3.8 Aufgaben Philosophisches
Ein Bekannter belästigt Sie wieder einmal mit unqualifizierten Aussagen über Linux. Unter anderem erwähnt er die unnötige und anachronistische Komplexität des Systems. Alles sei einfach zu umständlich realisiert. Was antworten Sie ihm? Richtige Partitionierung
Sie müssen als Administrator bzw. Administratorin eines großen Rechenzentrums einen neuen Linux-Server aufsetzen. Der Server soll verschiedene (Daten-)Verzeichnisse für die Benutzerinnen und Benutzer freigeben. Wie gestalten Sie vom Prinzip her die Partitionierung?
4 Grundlagen der Shell »On a number of points we were influenced by Multics, which
suggested the particular form of the I/O system calls (…) and
both the name of the Shell and its general functions.«
(dt. »In vielfacher Hinsicht wurden wir von Multics inspiriert, das
verschiedene Parameter für I/O-Systemaufrufe (…) und den
Namen der Shell sowie deren generelle Funktionen beeinflusste.)
– Dennis M. Ritchie und Ken Thompson Eine Shell ist das Programm, in das Sie Ihre Befehle und Kommandos zum Aufruf von anderen Programmen eingeben. Möchten Sie beispielsweise das Programm X starten, so erteilen Sie der Shell den Befehl dazu. Die Shell interpretiert das Kommando (daher werden Shells auch oft als Kommandointerpreter bezeichnet) und leitet es bei Bedarf an den Kernel weiter (um ein Programm starten zu lassen).
4.1 Einführung und Überblick Wenn Sie mit einer grafischen Oberfläche wie KDE oder Gnome arbeiten, dann müssen Sie nur ein Terminal starten, um in die Shell zu gelangen. Nach einem erfolgreichen Login ohne grafische Oberfläche wird hingegen direkt die Login-Shell eines Benutzers bzw. einer Benutzerin gestartet. Von dieser Shell aus werden alle Programme der Arbeitssession gestartet.
Eine gestartete Shell sieht immer in etwa wie in Abbildung 4.1 aus, wobei das $-Zeichen (manchmal auch eine Ausgabe wie benutzer@rechner$ oder beim Superuser das #-Zeichen) zur Eingabe von Befehlen auffordert. Beim Verlassen der Login-Shell loggt man sich aus dem System aus beziehungsweise beendet die grafische Terminalsitzung. Geben Sie dazu exit ein oder drücken Sie (Strg) + (D). Zur Weiterarbeit ist eine erneute Anmeldung über das login-Programm oder ein erneuter Start des grafischen Terminals notwendig.
Abbildung 4.1 Eine Shell in einem Terminal
4.1.1 Welche Shells gibt es?
Ohne an dieser Stelle zu weit ins Detail zu gehen, wollen wir erst einmal etwas zu den populärsten Shells sagen. Lassen Sie uns mit der sogenannten Bourne-Shell (sh) beginnen. Diese wurde Ende der 1970er-Jahre geschrieben. Aufgrund zu geringer Fähigkeiten dieser Shell wurde später die C-Shell (csh) entwickelt. Hierbei wurde die Syntax an die Programmiersprache C angelehnt und die Arbeit mit der Shell etwas komfortabler gestaltet. Später wurde die Korn-Shell (ksh) entwickelt. Sie baut auf den Funktionalitäten sowie der Syntax der Bourne-Shell auf. Die Features der C-Shell wurden übernommen und mit ihr stand eine recht beliebte, wenn zunächst auch kostenpflichtige Shell zur Verfügung. Auf heutigen Linux-Systemen ist daher nicht die
originale kostenpflichtige Korn-Shell, sondern die freie Version pdksh (Public-Domain-Korn-Shell) installiert. Später wurden die freie Bourne-again-Shell (bash) sowie die ebenfalls freie TC-Shell (tcsh) entwickelt, die von vielen heutigen Unix-Nutzern verwendet werden. Die tcsh baut, wie der Name schon sagt, auf der csh auf, während die bash wiederum auf der sh und der ksh basiert. Die meisten populären Distributionen setzen die bash als Standardshell ein. Eine äußerst beliebte Shell mit der Syntax der Bourne-Shell und Fähigkeiten der Korn-Shell und der TC-Shell ist die Z-Shell (zsh). Diese Shell wurde erstmals 1990 veröffentlicht, ist modular aufgebaut und verfügt über einige Raffinessen. Die meisten der oben genannten Shells befinden sich nach wie vor in aktiver Entwicklung. Ihr altes Ersterscheinungsdatum bedeutet daher nicht, dass es sich um veraltete Software handelt. 4.1.2 Welche Shell für dieses Buch?
In diesem Buch werden wir uns mit der bash befassen, da sie verbreiteter als die C-Shell-Reihe ist und die meisten Shellskripte ihre Syntax und Features verwenden. Auch die Skripte der Distributionen verwenden die bash. Zudem ist das meiste, was Sie über die bash lernen werden, auch 1:1 für die ebenfalls sehr populäre zsh und die beiden Klassiker sh und ksh gültig. 4.1.3 Die Shell als Programm
Zunächst ist die Shell ein Programm wie jedes andere. Über den Aufruf des Programmnamens wird sie also schlicht und einfach gestartet. Normalerweise übernimmt das Programm getty bzw.
systemd diesen Startvorgang nach dem Login. Jedoch kommt es sehr
oft vor, dass ein Benutzer bzw. eine Benutzerin eine Shell in der Shell starten möchte. Beim Verlassen einer auf diese Weise gestarteten Shell befindet man sich anschließend wieder in der vorherigen Shell. Beendet man jedoch die Login-Shell, loggt man sich aus dem System aus. user$ ksh $ csh % exit $ exit user$ exit logout
myhost login:
// // // // //
Start der Korn-Shell
Start der C-Shell
zurück zur Korn-Shell
zurück zur Login-Shell
Beenden der Shellsession
// Ihr Rechner wartet auf erneute Anmeldung
Listing 4.1 Shellstart
4.1.4 Der Prompt
Wie Sie bereits wissen, verfügt eine Shell über einen Standardprompt. Es gibt zudem noch einige Nebenprompts. Zur Erinnerung: Ein Prompt ist im Prinzip eine Zeichenkette, die Sie auffordert, einen Befehl einzugeben. Je nach Shell – und besonders nach der persönlichen Konfiguration – sehen diese Prompts anders aus. Bisher benutzten wir im Buch meist den Prompt user$, jedoch wäre auch durchaus eine andere Kombination denkbar. Die bash bietet an dieser Stelle eine Menge Möglichkeiten zur Gestaltung des Prompts. Nach dem Start der bash sieht man in der Regel Folgendes: Benutzer@Hostname:~$, wobei ~ das aktuelle Arbeitsverzeichnis ist. Der Prompt wird über eine Variable mit dem Namen PS1 gestaltet. Eine Variable speichert einen veränderlichen Wert, in diesem Fall das Aussehen Ihres Prompts. Sie trägt den Namen PS1 und ist über diesen ansprechbar. Setzt man diese Variable auf einen Wert X, so
ändert sich der Prompt in X. Im Normalfall exportiert man diese Variable, jedoch reicht uns an dieser Stelle zunächst einmal das ganz normale, nicht dauerhafte Verändern des Prompts aus. Im folgenden Beispiel geben wir über das echo-Programm den Inhalt der PS1-Variablen aus und setzen ihn anschließend neu. Das Ergebnis zeigt, dass der neue Prompt übernommen wird. Mit dem unset-Kommando kann der Prompt gelöscht werden. Am Ende wird wieder unser Standardprompt gesetzt. user$ PS1="prompt > "
prompt > PS1="Steffen% "
Steffen% unset PS1
PS1="Linux$ "
Linux$
Listing 4.2 Setzen des Prompts
Dieser Prompt bietet uns jedoch noch keine praktischen Features wie die Anzeige des Arbeitsverzeichnisses. Für Aufgaben dieser Art werden sogenannte Escapesequenzen (siehe Tabelle 4.1) in den Prompt eingebettet. Es existieren noch weitere Escapesequenzen, die beispielsweise zur Festsetzung der farblichen Hervorhebung dienen. Sie werden im Rahmen dieses Buches jedoch nicht behandelt, denn sie funktionieren nicht auf allen Terminals. Einige Distributionen und eine große Anzahl der Benutzerinnen und Benutzer verwenden die PS1-Variante »Benutzer@Host Verzeichnis$«, die ein an dieser Stelle sehr gut passendes Beispiel zur Nutzung der Escapesequenzen darstellt. Sequenz Wirkung \a
Ausgabe eines Tons im PC-Lautsprecher
Sequenz Wirkung \d
zeigt das Datum an
\e
Escapezeichen
\h
der Hostname (z. B. »rechner«)
\H
FQDN-Hostname (z. B. »rechner.netzwerk.edu«)
\j
Anzahl der Hintergrundprozesse
\l
Name des Terminals
\n
neue Zeile (Prompts können über mehrere Zeilen verteilt werden.)
\r
Carriage-Return
\s
der Name der Shell
\t
Zeit im 24-Stunden-Format
\T
Zeit im 12-Stunden-Format
\@
Zeit im AM/PM-Format
\u
der Name des Benutzers bzw. der Benutzerin
\v
die Version der bash
\V
wie \v, jedoch mit Patch-Level
\w
gibt das Arbeitsverzeichnis an (mit Pfad)
\W
gibt das Arbeitsverzeichnis an (ohne Pfad)
\#
Anzahl der bereits aufgerufenen Kommandos während der Shellsession des Terminals
Sequenz Wirkung \$
Ist man als normaler Benutzer eingeloggt, erscheint ein Dollarzeichen ($), root bekommt eine Raute (#) zu sehen.
\\
ein Backslash
Tabelle 4.1 Escapesequenzen user$ PS1="\u@\h \w\$ "
swendzel@steffenmobile /usr$ ls
…
Listing 4.3 So setzen Sie den bash-Prompt mit Escapesequenzen
4.1.5 Shellintern vs. Programm
In der Shell werden, wie Sie bereits wissen, Programme – wie beispielsweise ein Programm zum Versenden von E-Mails – gestartet. Hierbei gibt es zwei unterschiedliche Gruppen von Befehlen. Die eine Gruppe besteht aus den tatsächlich auf der Festplatte abgelegten Programmen. Die andere Gruppe ist shellintern: Sie wurde sozusagen in die Shell »reinprogrammiert« und wird daher auch als Builtin bezeichnet. type
Nun können wir Ihnen leider nicht die allgemeine Frage danach beantworten, welche Kommandos intern bzw. extern sind. Dies liegt ganz einfach daran, dass jede Shell verschiedene Kommandos implementiert hat. Die bash verfügt zum Beispiel über ein internes kill-Kommando, die Bourne-Shell nicht (kill sendet Nachrichten an laufende Programme, etwa zum Beenden). Möchten Sie Einzelheiten erfahren, nutzen Sie das Programm type:
$ type kill
kill ist eine von der Shell mitgelieferte Funktion.
Listing 4.4 Builtin oder Programm? – Beispiel 1 $ type ls
ls ist ein Link auf /bin/ls
Listing 4.5 Builtin oder Programm? – Beispiel 2
alias
In der Shell kann ein sogenannter Alias angelegt werden. Das ist ein Befehl, der einen anderen ersetzt. type gibt Ihnen auch darüber Auskunft, ob ein Kommando ein solcher Alias ist. $ type ls
ls is an alias for /bin/ls -aF
Listing 4.6 Alias-Überprüfung
Wenn Sie in der bash also einfach nur kill eingeben, wird die shellinterne Variante benutzt. Bei /bin/kill wird hingegen das Programm aus dem entsprechenden Verzeichnis verwendet. Für Sie ergeben sich aber außer einer leicht erweiterten Funktionalität und einem latenten Geschwindigkeitsvorteil bei der bash-Version keine Unterschiede. 4.1.6 Kommandos aneinanderreihen
Verschiedene Kommandos in der Shell können aneinandergereiht werden. Der Grund für eine Aneinanderreihung ist die Platzersparnis auf dem Bildschirm und natürlich auch die zeitweise Abarbeitung der Kommandos. Stellen Sie sich einmal Folgendes vor: Zehn Kommandos sollen nacheinander ausgeführt werden, wobei jedes einzelne Kommando
voraussichtlich einige Minuten Ausführungszeit beanspruchen wird. Nun könnten Sie die ganze Zeit vor dem Rechner sitzen bleiben und die Kommandos einzeln eingeben. Doch wäre es nicht besser, wenn Sie sich nicht darum kümmern müssten und die Zeit zum Kaffeekochen nutzen könnten? Der Trennungsoperator
Szenario Nummer eins: Die Kommandos sollen der Reihe nach ablaufen, egal, was mit den vorherigen Kommandos geschieht. Für diesen Fall verwenden Sie den Trennungsoperator – das Semikolon (;). Das folgende Beispiel soll die Wirkung und Anwendung des Trennungsoperators simulieren. Dabei wird unter anderem ein Verzeichnis ausgegeben, das nicht existiert. ls Verzeichnis listet den Inhalt des Verzeichnisses auf, uname nennt Ihnen den Namen des Betriebssystems und find sucht eine Datei – all diese Befehle behandeln wir noch im Detail. user$ ls VerZeiChniS; uname; find / -name Datei
/bin/ls: VerZeiChniS: No such file or directory
Linux
/usr/local/share/Datei
/home/user/Datei
Listing 4.7 Der Trennungsoperator – ein Beispiel
Wie Sie sehen, werden unabhängig vom Resultat des lsKommandos alle anderen Kommandos ausgeführt. Die erste Ausgabezeile stammt von ls, die zweite von uname, das uns sagt, dass wir Linux verwenden, und die letzten beiden Zeilen sind Suchergebnisse des Dateisuchprogramms find. Weiter nur bei Erfolg
Szenario Nummer zwei: Die Kommandos sollen der Reihe nach ausgeführt werden, vorausgesetzt, das jeweils vorangegangene war erfolgreich. Für den Fall, dass ein Programm nicht korrekt abläuft, wird das nächste Programm also nicht gestartet und die Abarbeitung gestoppt. Um dies zu realisieren, schreiben Sie anstelle des Trennungsoperators ein doppeltes kaufmännisches UndZeichen (&&). user$ ls VerZeiChniS && uname && find / -name Datei
/bin/ls: VerZeiChniS: No such file or directory
Listing 4.8 Erfolgsbedingte Kommandoreihe
Es stellt sich nun die Frage, woher die Shell weiß, ob ein Kommando korrekt ausgeführt wurde. Die Antwort ist recht simpel: Programme liefern einen sogenannten Rückgabewert an die Shell zurück. In der Programmiersprache C würde dies beispielsweise durch ein return Wert; in der main-Funktion erreicht werden. Auch Funktionen innerhalb eines Shellskripts können solch eine Funktionalität bieten. Weiter nur bei Fehlschlag
Natürlich gibt es zum && auch eine Alternative, nämlich ||, die genau das Gegenteil bewirkt: Der hintere Befehl wird nur ausgeführt, falls der erste fehlschlägt, wobei der Befehl echo schlicht Text ausgibt: user$ ls VerZeiChnis || echo "HILFE!"
/bin/ls: VerZeiChniS: No such file or directory
HILFE!
Listing 4.9 Abfangen des Misserfolgs
4.1.7 Mehrzeilige Kommandos
Oft erstrecken sich Kommandoeingaben über mehrere Zeilen. Ist dies der Fall, können Sie den Backslash-Operator (\) verwenden, um die aktuelle Eingabezeile in der nächsten Zeile fortzusetzen: user$ find /usr/local/share/WindowMaker/ \
-name Image.jpg
/usr/local/share/WindowMaker/Backgrounds/Image.jpg
Listing 4.10 Kommandos über mehrere Zeilen
Hier tragen wir find also über zwei Zeilen hinweg auf, nach der Datei Image.jpg im Verzeichnis /usr/local/share/WindowMaker zu suchen.
4.2 Konsolen Unter Linux (und ähnlichen Systemen, etwa BSD) steht Ihnen nicht nur eine einzige Konsole zur Verfügung, womit das parallele Arbeiten (durch mehrfaches Einloggen) komfortabel wird. Sofern nicht die grafische Oberfläche läuft, landen Sie nach dem Systemstart normalerweise auf der ersten Konsole. Je nach Konfiguration gibt es meist zwischen fünf und acht, wobei nicht alle immer als eigentliche Shellkonsole verwendet werden. Auf Konsole eins, fünf oder sieben läuft in der Regel die grafische Oberfläche, und auf einer weiteren Konsole könnten Systemmeldungen angezeigt werden. Der Wechsel zwischen den Konsolen erfolgt über die Tastenkombination (Strg) + (Alt) + Funktionstaste, also etwa (Strg) + (Alt) + (F2) für die zweite Konsole.
4.3 screen Ein weiteres wichtiges Werkzeug für die Shell ist screen. Wird es gestartet, so wird der ganze Bildschirm des Terminals für das Programm verwendet. Ohne Parameter startet screen einfach nur eine Shell. Was aber ist das Besondere an diesem Programm? Im Folgenden werden die Begriffe Fenster und Terminal der Einfachheit halber synonym verwendet.
Ganz einfach: screen ermöglicht es Ihnen, parallel auf mehreren virtuellen Terminals zu arbeiten, obwohl Sie in Wirklichkeit nur eines verwenden. Nehmen wir einmal an, Sie loggen sich über ein Programm wie SSH oder Telnet auf einem entfernten Rechner ein. Dort möchten Sie ein Programm schreiben. Um dies möglichst komfortabel zu erledigen, benötigen Sie zumindest ein Terminal, in dem ein Texteditor läuft, mit dem man den Quellcode editieren kann, und eines, mit dem man das Programm kompilieren, ausführen und debuggen kann. Mit screen ist genau das möglich, obwohl Sie sich nur ein einziges Mal einloggen müssen. Das Programm wird durch einen Aufruf von screen (für eine Shell) oder screen [programm] gestartet. Screen legt dafür ein erstes virtuelles Terminal an. Anschließend können Sie in der gestarteten Shell beziehungsweise mit dem gestarteten Programm wie gewohnt arbeiten. Nehmen wir das obige Beispiel nun zur Hand und erleichtern wir uns die Arbeit an einem Programm. Dazu könnte man beispielsweise einen Editor (etwa vi) starten.[ 25 ]
Nun erstellen Sie durch die Tastenkombination (Strg) + (A) und anschließendes Drücken der Taste (C) (für create) ein neues virtuelles Terminal. Sodann wechselt screen auch gleich die Ansicht auf das neu erstellte Terminal, das mit einer Shell auf Eingaben wartet. Sie könnten darin nun den Compiler oder Ähnliches anwerfen. Um zwischen den existierenden virtuellen Terminals zu wechseln, nutzen Sie die Tastenkombination (Strg) + (A) und drücken anschließend eine Taste zwischen (0) und (9). Damit steht die Möglichkeit zur Verfügung, zwischen insgesamt zehn virtuellen Terminals zu wechseln (sofern Sie tatsächlich so viele erzeugen möchten).
Abbildung 4.2 screen mit Fensterliste
Eine weitere Möglichkeit ist der Weg über die Fensterliste (Window List). In die Fensterliste gelangen Sie über die Tastenkombination (Strg) + (A) und anschließendes Drücken der AnführungszeichenTaste. Mit den Cursortasten wechseln Sie dort zwischen den einzelnen Fenstern (man sieht deren Nummer und Name). Einzelnen Terminals kann man über die übliche Tastenkombination sowie anschließendes Drücken der Taste (A) auch Namen verpassen. Diese erscheinen dann in der Fensterliste. Nach (Strg) + (A) und anschließend (W) erscheint am unteren Fensterrand
übrigens eine Namensliste der Terminals. Drückt man dann beispielsweise die (1), so landet man auf dem ersten Terminal. Ein Fenster kann durch die Tastenkombination (Strg) + (A) und anschließendes Drücken von (K) (kill) beendet werden. Sie können die Fenster auch schließen, indem Sie die Shell und/oder das gestartete Programm (in dieser Shell) verlassen. (Was aber natürlich davon abhängt, ob man das Programm direkt durch screen oder erst in einer Shell innerhalb eines virtuellen Terminals gestartet hat.) Hat man das letzte Fenster zerstört, wird eine Meldung wie »screen is terminating« auf dem Terminal angezeigt und man befindet sich wieder in der Ausgangsshell.
4.4 Besseres Arbeiten mit Verzeichnissen Im Folgenden werden wir auf das Arbeiten mit Verzeichnissen eingehen. Erinnern Sie sich also noch einmal an die Konzepte des VFS (Virtual File System) und daran, was Sie in diesem Zusammenhang vielleicht beachten müssen. 4.4.1 Pfade
Zuerst müssen wir natürlich diesen Begriff klären: Ein Pfad gibt einen Weg durch den hierarchischen Verzeichnisbaum hin zu einem bestimmten Ziel an. Eine vollständige Verzeichnisangabe wie /home/user beschreibt also auch einen Pfad, der angibt, wie man zu eben diesem Verzeichnis gelangt. Pfadnamen
Auch den Unterschied zwischen absoluten und relativen Pfaden kennen Sie bereits aus vorherigen Kapiteln. An dieser Stelle möchten wir allerdings einige zusätzliche Hinweise zu diesen Pfadarten geben. Es gibt folgende Möglichkeiten zur Verzeichnisangabe: .
Der Punkt (.) bezeichnet das aktuelle Arbeitsverzeichnis. Ein Wechsel in dieses Verzeichnis mit cd . führt also dazu, dass das Verzeichnis gar nicht gewechselt wird. ..
Zwei Punkte geben das nächsthöhere Verzeichnis an. Würden Sie
sich also im Verzeichnis /usr/local/bin befinden, hätte ein cd .. den Wechsel in /usr/local zur Folge. Ein weiterer Aufruf von cd .. würde in das Verzeichnis /usr wechseln und ein weiterer in das Verzeichnis /. ~-
Eine Tilde mit Minuszeichen zeigt auf das Verzeichnis, in dem Sie sich vor dem letzten cd-Aufruf befanden. ~
Der Tilde-Operator (~) bezeichnet das Heimatverzeichnis des Benutzers. Ein Spezialfall ist ~Name, wobei Name der Account eines lokalen Benutzers sein muss. $HOME
Die globale Variable $HOME wird beim Login eines Benutzers gesetzt und zeigt auf dessen Heimatverzeichnis. kein Parameter
Ein parameterloser cd-Aufruf wechselt in das Heimatverzeichnis des Benutzers. Eine Kombination des Pfadnamens mithilfe dieser Abkürzungen ist natürlich auch problemlos möglich. Der folgende cd-Aufruf wechselt in das Heimatverzeichnis des Benutzers (~), dort in das Unterverzeichnis Verzeichnis/, verlässt durch ../ dieses Verzeichnis wieder (somit landet man wieder im Heimatverzeichnis) und anschließend in das Unterverzeichnis buch/. $ pwd
/usr/local
$ cd /Verzeichnis/../buch/
$ pwd
/home/swendzel/buch
Listing 4.11 Beispiel für einen Verzeichniswechsel
4.4.2 Und das Ganze mit Pfaden ...
Natürlich kann man diese ganzen »speziellen« Angaben für Verzeichnisse jeweils mit weiteren, komplettierenden Pfaden kombinieren. So wirkt ein .. immer relativ, ein $HOME hingegen absolut. Schauen wir uns zum besseren Verständnis ein kleines Beispiel an: // Wir befinden uns in …
$ pwd
/usr/local
// ins Arbeitsverzeichnis wechseln (kein Effekt):
$ cd . ; pwd
/usr/local
// nächsthöheres Verzeichnis:
$ cd .. ; pwd
/usr
// absolute Pfadangabe:
$ cd /usr/local/bin; pwd
/usr/local/bin
// … und eine simple relative Pfadangabe
$ pwd
/usr/local
$ cd share/slrn ; pwd
/usr/local/share/slrn
//… komplizierter:
$ pwd
/usr/local/bin
$ cd ../../lib/modules; pwd
/usr/lib/modules
//… und völlig sinnlos:
$ cd /usr/lib/../local/bin; pwd
/usr/local/bin
Listing 4.12 Verzeichniswechsel auf Unix-Art
4.5 Die elementaren Programme Nun besprechen wir sowohl die wichtigen internen Shellkommandos als auch die wichtigen Programme zur täglichen Arbeit mit der Shell. Fast alle dieser Kommandos »spielen« mit normalem Text herum. Denken Sie jetzt jedoch bitte nicht, dass diese Kommandos nicht zeitgemäß wären: Linux ohne Shell(programme) wäre wie Windows ohne grafische Oberfläche, macOS ohne Maus oder ein Smartphone ohne Gestensteuerung. 4.5.1 echo und Kommandosubstitution
Beginnen wir mit dem echo-Kommando. Der Sinn und Zweck von echo ist es, Text auf dem Bildschirm auszugeben. Der auszugebende Text wird dabei einfach als Parameter angegeben: user$ echo "Das echo-Kommando ist nicht immer shellintern."
Das echo-Kommando ist nicht immer shellintern.
Listing 4.13 Das echo-Kommando
In Skripten wird echo oft zum Ausgeben der Werte von Variablen benutzt oder um die Ausgaben eines Programms in Text einzubetten. Da wir Variablen erst weiter hinten im Kapitel behandeln, sie an dieser Stelle jedoch kurz gebrauchen werden, sei Folgendes gesagt: Eine Variable speichert einen Wert. Doch nun zurück zur Ausgabe von Variablen. Es gibt drei verschiedene Möglichkeiten zur Ausgabe von Text mittels echo. Die erste benutzt normale Anführungszeichen. Bei dieser Variante kann der Wert einer Variablen ausgegeben werden. Die zweite Variante benutzt Backshifts und bewirkt die Ausführung eines
Befehls und damit die Integration der Ausgabe dieses Befehls in den eigentlichen Text. Die Ausführung eines Befehls auf diese Weise wird als Kommandosubstitution bezeichnet. Variante Numero 3 wird in Hochkommata gepackt und erlaubt keine Wertausgaben oder Kommandosubstitutionen. Variablenaufrufe und Kommandos werden also direkt ausgegeben. Das folgende Beispiel soll diese Schreibweisen zum besseren Verständnis demonstrieren. Später werden Sie lernen, dass Variablen über die Syntax $VARIABLEN_NAME angesprochen werden. // Normale Anführungszeichen geben neben dem eigentlichen
// Text auch den Wert von Variablen preis:
user$ echo "Der Wert von NUMMER ist $NUMMER"
Der Wert von NUMMER ist 13
// Backticks nutzt man zur Kommandosubstitution:
user$ echo "Heute ist `date` !"
Heute ist Sat Oct 16 17:41:09 CEST 2021 !
// Mit Hochkommata wird Ihnen gar nichts gegönnt:
user$ echo 'Heute ist `date` und der Wert von X ist $X'
Heute ist `date` und der Wert von X ist $X
Listing 4.14 Entwirrung der Schreibweisen
4.5.2 sleep
Das sleep-Kommando wartet einen gewissen Zeitraum, bevor es sich beendet. Dies ergibt in der Shellprogrammierung hin und wieder Sinn. Der Aufruf sleep 10 »schläft« für zehn Sekunden. Wenn wir zweimal die aktuelle Uhrzeit mit dem date-Befehl ausgeben lassen und dazwischen schlafen, zeigt sich der Effekt: user$ date; sleep 10; date
So 23. Mai 18:52:40 CEST 2021
So 23. Mai 18:52:50 CEST 2021
$
Listing 4.15 Anwendungsbeispiel für sleep
4.5.3 Erstellen eines Alias
Unter Linux ist es möglich, einen sogenannten Alias zu erstellen. Ein solcher Alias wird verwendet, um eine Kurzform für ein in der Regel etwas längeres Kommando zu schaffen. Beispielsweise könnten Sie einen Alias namens ll erstellen, der stellvertretend für ls -laF steht. Um diese Funktionalität der Linux-Shells zu verwenden, greift man auf das Kommando alias zurück. Im Normalfall listet es nur die aktuell eingerichteten Kommando-Aliase auf, doch es kann auch zur Erstellung eines neuen benutzt werden. user$ alias
alias ll="ls -laF"
alias ls="/bin/ls -aF"
user$ ls ~/projects/netlib
./ eigrp.h imap.h ../ hello.h ip4.h arp.h http.h ip6.h bgp.h icmp.h net_error.h dhcp.h icmp6.h net_wrapper.h dns.h icmprd.h netlib.h egp.h igrp.h ospf.h
pop3.h rip.h signal.h smtp.h snmp.h tcpscn.h telnet.h
test*
test.c
testcode/
udpd*
udpd.c
udpscn.h
x11.h
Listing 4.16 Das alias-Kommando
Ein neuer Alias wird mit alias name="Kommando" erstellt, wobei das Kommando nur in Anführungszeichen geschrieben werden muss, wenn es nicht druckbare Zeichen enthält oder Escapesequenzen angewandt werden müssen. Ein Alias wird via unalias entfernt. $ alias p=pwd
$ p
/home/swendzel/projects/netlib
$ unalias p
$ p
bash: p: command not found
Listing 4.17 Einen eigenen Alias einrichten und löschen
4.5.4 cat cat ist eines der wichtigsten Programme jedes Linux-Rechners und
gibt die ihm angegebenen Dateien auf dem Monitor aus. cat wird vor allem verwendet, um die Ausgabe einer Datei in eine (andere) Datei umzulenken oder um den Inhalt der Datei an ein anderes Programm weiterzugeben. Wir werden darauf in naher Zukunft noch genauer zu sprechen kommen. user$ cat /etc/passwd
root:x:0:0::/root:/bin/bash
bin:x:1:1:bin:/bin:
daemon:x:2:2:daemon:/sbin:
…
…
…
nobody:x:99:99:nobody:/:
swendzel:x:1000:100:Steffen W.,,,:/home/swendzel:/bin/bash
// Es ist möglich, mehrere Dateien ausgeben zu lassen
user$ cat DateiA
Inhalt von DateiA
user$ cat DateiB
Inhalt von DateiB
user$ cat DateiA DateiB
Inhalt von DateiA
Inhalt von DateiB
Listing 4.18 Das cat-Programm
4.6 Programme für das Dateisystem Wie erstellt man eigentlich ein Verzeichnis und löscht es wieder? Wie kopiert oder verschiebt man Dateien? 4.6.1 mkdir – Erstellen eines Verzeichnisses
Ein Verzeichnis wird ganz einfach mithilfe des Programms mkdir erstellt. Der Verzeichnisname wird dabei als Parameter übergeben, doch Achtung: Linux unterscheidet bei Dateinamen zwischen Großund Kleinbuchstaben, »Verzeichnis« ist demnach nicht das Gleiche wie »VERZEICHNIS«! Schauen wir uns ein Beispiel an. $ ls -F
hallo test.txt
$ mkdir Verzeichnis
$ ls -F
hallo test.txt Verzeichnis/
Listing 4.19 mkdir
Der Parameter -F bewirkt bei ls, dass Verzeichnisse anders dargestellt werden als normale Dateien, nämlich mit einem / hinter ihrem Namen. 4.6.2 rmdir – Löschen von Verzeichnissen
Das Löschen eines Verzeichnisses ist genauso simpel wie dessen Erstellung, nur dass hierfür nicht das Kommando mkdir (make directory), sondern rmdir (remove directory) verwendet wird. Die Syntax entspricht der von mkdir. Der Nachteil dieses Programms ist jedoch der, dass keine rekursive Löschung eines Verzeichnisses
möglich ist. Das heißt, nur Verzeichnisse, die keine Dateien enthalten, können gelöscht werden. $ ls -F VerzeichnisA VerzeichnisB
VerzeichnisA:
VerzeichnisB:
Datei.txt
$ rmdir VerzeichnisA
$ rmdir VerzeichnisB
rmdir: konnte 'VerzeichnisB' nicht entfernen:
Das Verzeichnis ist nicht leer
Listing 4.20 rmdir in Aktion
Beherbergt ein Verzeichnis jedoch nur eine einzige Hierarchie von Unterverzeichnissen ohne sonstige Dateien, so können Sie mit dem Parameter -p das Verzeichnis inklusive seiner Unterverzeichnisse löschen. Würde also das Verzeichnis B ein Unterverzeichnis SA und dieses wiederum ein Subverzeichnis SB beinhalten, so wäre das Ergebnis der folgenden beiden Aufrufe äquivalent: user$ rmdir -p VerzeichnisB/SA/SB
user$ rmdir VerzeichnisB/SA/SB; \
rmdir VerzeichnisB/SA; rmdir VerzeichnisB
Listing 4.21 Verzeichnis und Subverzeichnis löschen
4.6.3 cp – Kopieren von Dateien
Das Programm cp legt eine Kopie von einer bereits im Dateisystem vorhandenen Datei an. Dabei kann es sich natürlich auch um ein Verzeichnis handeln. Die Syntax ist einfach und in der Form cp [Option] DateiA [DateiB] Ziel
gehalten, wobei mehrere Dateien in das Ziel kopiert werden können. Ein rekursives Kopieren von Verzeichnissen ist über den -rParameter möglich.
user$ user$ user$ user$
cp prog kopie_prog
mkdir Verzeichnis
cp prog kopie_prog Verzeichnis/
cp -r Verzeichnis kopie_Verzeichnis
Listing 4.22 Dateien und Verzeichnisse kopieren
4.6.4 mv – Verschieben einer Datei
Mit dem mv-Kommando werden Dateien »verschoben«, wobei das nicht ganz korrekt formuliert ist. Eigentlich wird die Datei nur einer anderen Verzeichnisdatei zugeordnet: Der physikalische Dateninhalt der Datei bleibt im Normalfall dort, wo er ist. Dies ist nicht nur der Unterschied zwischen cp und mv, sondern auch der Grund dafür, dass ein mv-Aufruf viel schneller erledigt ist als ein Kopiervorgang. Die einzige Ausnahme ist der Verschiebungsvorgang über verschiedene Dateisysteme. mv funktioniert rekursiv, d. h., Verzeichnisse samt ihrem Inhalt
können ohne weitere Parameter komplett verschoben werden. Parameter Nummer 1 gibt die Quelle, Parameter Nummer 2 das Ziel an. Aber es gibt noch eine Nutzungsmöglichkeit für mv: Sie können damit einen neuen Namen für eine bestehende Datei vergeben. user$ mv kopie_Verzeichnis /home/user/neu
user$ mv Datei Neuer_Name_der_Datei
Listing 4.23 Das mv-Kommando
4.6.5 rm – Löschen von Dateien
Das den Windows-Benutzerinnen und -Benutzern als del (delete) bekannte Kommando zum Löschen von Dateien heißt unter Linux rm (remove) und kann eine ganze Menge toller Sachen.
Grundsätzlich rufen Sie rm mit dem zu löschenden Dateinamen auf: rm [Optionen] Dateiname. rm kann zunächst einmal neben regulären Dateien auch
Verzeichnisse löschen. Hierzu übergeben Sie den Parameter -d. Des Weiteren besteht die Möglichkeit, jeden einzelnen Löschvorgang über den -i-Parameter zu bestätigen. Ein rekursives Löschen ist über den bereits recht bekannten -r-Parameter möglich. user$ rm -ri Verzeichnis
rm: descend into directory 'Verzeichnis'? y
rm: remove 'Verzeichnis/filea'? y
rm: remove 'Verzeichnis/fileb'? y
rm: remove 'Verzeichnis'? y
Listing 4.24 Löschen mit Nachfrage
Einige Befehle wie auch rm bieten einen Parameter »--« zur Terminierung der Optionsliste. Dadurch können Sie Dateien mit einem Namen wie »-k« löschen: rm --k. 4.6.6 head und tail
Zwei weitere wichtige Programme sind head und tail. Ersteres zeigt den Kopf einer Datei, besser gesagt, die ersten Zeilen, Letzteres das Ende einer Datei auf dem Bildschirm an. Die Anzahl der auszugebenden Zeilen wird via -n angegeben, wobei n kein Parametertyp selbst, sondern die Anzahl ist. // Die letzten fünf Einträge der syslog-Datei liefern uns aktuelle
// Meldungen:
user$ tail -5 /var/log/syslog
Oct 25 16:28:50 laptop kernel: device lo left promiscuous mode
Oct 25 16:29:25 laptop kernel: device lo entered promiscuous mode
Oct 25 16:50:16 laptop – MARK –
Oct 25 17:10:18 laptop – MARK –
Oct 25 17:25:32 laptop kernel: device lo left promiscuous mode
# Die ersten zwei Einträge liefern uns alte Daten vom September:
user$ head -2 /var/log/syslog
Sep 19 15:41:31 laptop syslogd 1.4.1: restart.
Sep 19 15:41:32 laptop kernel: klogd 1.4.1, log source = /proc/kmsg
started.
Listing 4.25 Die letzten und ersten Logeinträge
tail kann der -f-Parameter übergeben werden. Dieser listet
zunächst die letzten Zeilen der angegebenen Datei auf, wartet aber auf neue. Würde also tail -f /var/log/syslog aufgerufen und nach fünf Minuten eine neue Logmeldung eingetragen, so würde diese automatisch ausgegeben. tail wartet jeweils eine Sekunde, bis die nächste Prüfung auf neue Zeilen in der Zieldatei gestartet wird. Für schnelle Aktualisierungen der Ausgabe ist es also weniger geeignet.
4.7 Ein- und Ausgabeumlenkung Ein sehr bedeutendes und oft verwendetes Feature der Shells ist die Ein- und Ausgabeumlenkung. Doch was hat es damit eigentlich auf sich? Konsolenprogramme geben Text im Terminal aus, um Anwenderinnen und Anwendern Informationen zu übermitteln. Diese Form der Ausgabe wird über die oben schon kurz angesprochene Standardausgabe (genannt: STDOUT) geschickt. Man kann diese Standardausgabe jedoch auch umleiten, beispielsweise in ein anderes Programm (das geht mithilfe von Pipes, die weiter unten besprochen werden) oder in eine Datei. Diese Umleitung der Ausgabe wird, wie Sie wohl bereits erahnen, als Ausgabeumlenkung bezeichnet und mit dem Größer-als-Operator (>) realisiert. Der Operator wird hinter das auszuführende Programm geschrieben und anschließend wird der Name der Datei angegeben, in die die Ausgabe umgelenkt werden soll. user$ ls -l
total 3800
-rw-r--r-- 1 swendzel wheel 379 Nov 1 1:34 Makefile
-rw-r--r-- 1 swendzel wheel 1051 Nov 2 0:56 anhang.aux
-rw-r--r-- 1 swendzel wheel 1979 Nov 1 0:53 anhang.tex
-rwx------ 1 swendzel wheel 283 Nov 1 1:13 backup
…
…
user$ ls > output
user$ head -4 output
total 3808
drwxr-xr-x 3 swendzel wheel 1536 Nov 30 7:48 ./
drwxr-xr-x 21 swendzel wheel 512 Nov 22 3:00 ../
-rw-r--r-- 1 swendzel wheel 379 Nov 15 1:34 Makefile
Listing 4.26 Ausgabeumlenkung in eine Datei
Das gleiche Prinzip verfolgt auch die Eingabeumlenkung, jedoch mit dem Unterschied, dass hier natürlich das gegenteilige Verfahren genutzt wird: Der Inhalt einer Datei wird als Eingabe für ein Programm verwendet. Das heißt, die Eingabe wird nicht mehr manuell getätigt, sondern kann in einer Datei dauerhaft gespeichert und immer wieder als Steuerung für ein Programm verwendet werden. Ein gutes Beispiel für solch eine Anwendung ist das mail-Programm. Der Inhalt einer Datei kann so durch Eingabeumlenkung ganz schnell und einfach an den Mann gebracht werden. user$ mail -s Testmail swendzel@workstation3 < Nachricht.txt
Listing 4.27 Eingabeumlenkung
4.7.1 Fehlerausgabe und Verknüpfung von Ausgaben
Neben der Schreibweise > Ausgabe ist auch die Schreibweise Nummer > Ausgabe möglich, wobei Nummer die Nummer des Ausgabekanals angibt. Der Eingabekanal (STDIN) hat die Nummer 0, die Standardausgabe (STDOUT) die Nummer 1 und die Standardfehlerausgabe (STDERR) die Nummer 2. Nur durch Angabe der Nummer kann demzufolge auch die Standardfehlerausgabe umgelenkt werden, da die Nummer der Shell mitteilt, was genau umgeleitet werden soll. /dev/null ist dabei eine Art Datengrab. Alle Daten, die hineingeschrieben werden, werden schlicht verworfen. user$ ls /root 1>/dev/null 2> Fehler
user$ cat Fehler
ls: root: Permission denied
Listing 4.28 Fehler- und Standardausgabe umlenken
Die Umlenkung der Ausgabe kann auch verknüpft werden. Dies wird realisiert, indem man einem anderen Kanal das Ziel eines
vorher umgeleiteten Kanals zuweist. Das folgende Listing zeigt die zugehörige Schreibweise: user$ ls /* > /dev/null 2>&1
user$
Listing 4.29 Sowohl Standard- als auch Fehlerausgabe ins Nirwana schicken
Die Ausgabeumlenkung kann übrigens zur gleichen Zeit wie die Eingabeumlenkung realisiert werden: programm < a.txt > b.txt 4.7.2 Anhängen von Ausgaben
Zu den obigen Möglichkeiten kommt hinzu, dass eine Ausgabe an eine bereits vorhandene Datei oder eine Eingabe an bereits vorhandene Eingaben angehängt werden kann. Dazu verwenden Sie den Umleitungsoperator einfach doppelt: user$ echo "Das ist Zeile 1" > Output
user$ echo "Das ist noch eine Zeile" » Output
user$ cat Output
Das ist Zeile 1
Das ist noch eine Zeile
Listing 4.30 Umleitung hinzufügen
4.7.3 Gruppierung der Umlenkung
Es ist möglich, eine Umlenkung mehrerer Programme zu gruppieren. Das heißt, alle in dieser Gruppe enthaltenen Kommandos sind von der Umlenkung der Gruppe betroffen. Eine Gruppierung wird auf zwei Arten vorgenommen: mit normalen und mit geschweiften Klammern. Verwenden Sie normale Klammern, wird für die Befehlsgruppe zusätzlich eine Subshell gestartet. Die enthaltenen Anweisungen nehmen also weniger Einfluss auf die aktuelle Shell.
user$ { ls user$ tail zzz
6:46PM up user$ ( rm
-l; uptime } > Output
-2 Output
3:46, 4 users,load averages:0.24,0.19,0.18
`find / -name '*.core'` ) 2> /dev/null &
Listing 4.31 Gruppierung
4.8 Pipes Pipes (|) sind mehr oder weniger mit dem Feature der Ausgabeumlenkung verwandt. Sie wurden von Ken Thompson erfunden und stellten zur Zeit der frühen UNIX-Entwicklung eine revolutionäre Vereinfachung der Programminteraktion dar. Der Unterschied zur klassischen Ausgabeumlenkung besteht darin, dass Pipes die Ausgabe eines Kommandos nicht in eine Datei, sondern an ein weiteres Kommando leiten. Dieses zweite (und gegebenenfalls auch dritte, vierte, fünfte ...) Kommando wird die Ausgabe des vorgeschaltenen Programms als Eingabe betrachten. Setzen wir das oben bereits aufgeführte Mailbeispiel doch einmal via Pipe um: # Der alte Aufruf sah wie folgt aus:
user$ mail -s Testmail [email protected] < Nachricht.txt
# Hier dasselbe mit Pipes:
user$ cat Nachricht.txt | mail -s Testmail [email protected]
Listing 4.32 Mailen via Pipe
4.8.1 Beispiel: sort und uniq verbinden
Sie haben bereits sort und uniq kennengelernt. Wenn wir die besprochene Beispieldatei sortieren und anschließend Redundanzen entfernen möchten, ohne eine Zwischendatei mit Ausgabeumleitung anzulegen (und ohne sort -u zu verwenden), dann geht das wie folgt: user$ sort Beispieldatei | uniq
000 IP
001 ICMP
002 IGMP
003 GGP
006 TCP
012 017 022 089 255
PUP
UDP
IDP
OSPF
RAW
Listing 4.33 Sortieren und Redundanzen entfernen
Prinzipiell würde auch cat Beispieldatei | sort | uniq zum gleichen Ergebnis kommen. 4.8.2 Beispiel: Zeichen vertauschen
Wir haben manchmal ein Dilemma mit heruntergeladenen Dateien, die wir kategorisieren wollen. Die eigentlichen Dateien des Typs befinden sich in Kleinbuchstaben auf der Platte, die heruntergeladenen haben jedoch oftmals Großbuchstaben im Namen. Nun, dies ist eines von den Szenarien, in denen tr Abhilfe schaffen kann. tr konvertiert ein Zeichen x in y, z. B. einen Großbuchstaben in einen Kleinbuchstaben, ein Leerzeichen in einen Unterstrich oder auch eine runde Klammerung in eine eckige. Dabei werden die zu konvertierenden Zeichen in der Form »[alt] [neu]« übergeben, zu löschende Zeichen werden durch den Parameter -d bzw. --delete, zu komplementierende durch -c bzw. -complement gekennzeichnet. user$ cat Datei
Da-tei-in-halt
user$ cat Datei | tr -d \-
Dateiinhalt
user$ cat Datei | tr a \?
D?-tei-in-h?lt
Listing 4.34 Das Kommando tr
4.8.3 Um- und Weiterleiten mit tee
Doch was ist, wenn Sie die Ausgabe einer Pipe nicht nur weiterleiten, sondern gleichzeitig umlenken möchten? Dafür gibt es das Programm tee. Sie übergeben tee den Namen der Datei, in die die Ausgabe umgeleitet werden soll. (Die Ausgabeumlenkung funktioniert nur mit der Standard-, nicht mit der Fehlerausgabe.) Zugleich wird die Ausgabe jedoch auf dem Bildschirm (also STDOUT) ausgegeben und kann entweder in eine weitere Datei oder eine Pipe geleitet werden. user$ wc -l kap??.tex | sort | tee output.tex | ./statistik.sh
Listing 4.35 Anwendung von tee
4.8.4 Named Pipes (FIFOs)
Named Pipes (sogenannte FIFOs) erweitern die Fähigkeiten einer Pipe. Eine FIFO kann als Datei auf dem Dateisystem erzeugt werden (was mit dem Befehl mkfifo bewerkstelligt wird) und kann Daten eines Prozesses einlesen. Das Schöne daran ist, dass mehrere Prozesse diese FIFO verwenden können. Das FIFO-Prinzip FIFOs arbeiten nach dem First-In-First-Out-Prinzip (daher der Name). Das bedeutet: Die Daten, die zuerst in einer FIFO abgelegt werden, werden auch zuerst wieder vom lesenden Prozess gelesen. user$ mkfifo fifo
# In die FIFO schreiben:
$ echo Gleich sind Shellskripte an der Reihe. > fifo
# Aus der FIFO lesen:
$ cat fifo
Gleich sind Shellskripte an der Reihe.
Listing 4.36 Erstellung und Verwendung einer FIFO
4.9 xargs Zum Abschluss des einleitenden Shellkapitels möchten wir noch ein Tool namens xargs vorstellen. Es leitet die Ausgabe des ersten Programms nicht als Eingabe (wie in einer Pipe), sondern als Parameter für ein zweites Programm weiter. Soll beispielsweise die Ausgabe von ls als Parameter für grep (ein Tool, das den Dateiinhalt nach einem vorgegebenen Muster durchsucht) herhalten, würde man dies folgendermaßen realisieren: $ ls *.tex | xargs grep gpKapitel
anhg_komref.tex:\gpKapitel{Kommandoreferenz}
...
kap01_kernel.tex:\gpKapitel{Der Kernel}
kap05_sysadmin.tex:\gpKapitel{Systemadministration}
kapxx_software.tex:\gpKapitel{Softwareentwicklung}
Listing 4.37 ls und xargs mit grep
4.10 Zusammenfassung In diesem Kapitel haben Sie die wichtigsten Grundlagen sowie einige fortgeschrittene Fähigkeiten der Shell kennengelernt. Die Shell erlaubt das Ausführen und Administrieren von Prozessen, die Arbeit mit Dateien und sogar Interprozesskommunikation. Im vertiefenden Kapitel 8, »Die Shell richtig nutzen«, werden Sie lernen, wie Sie in der Shell eigene Miniprogramme schreiben, um Aufgaben zu automatisieren und Systemskripte anzupassen.
4.11 Aufgaben Fancy Bash Prompt
Spieglein, Spieglein an der Wand, wer hat den schönsten BashPrompt im Land? Natürlich Sie! Konfigurieren Sie Ihren BashPrompt so, dass möglichst viele Informationen – verteilt auf drei Zeilen – ausgegeben werden. Konsolen-Wechsel
Loggen Sie sich in Ihre grafische Oberfläche ein. Wechseln Sie anschließend auf Konsole Nummer 3. Danach wechseln Sie wieder zurück auf die grafische Oberfläche, indem Sie die restlichen Konsolennummern ausprobieren.
5 Prozesse in der Shell »Unix is basically a simple operating system,
but you have to be a genius to understand the simplicity.«
(Dt. »Unix ist im Wesentlichen ein einfaches Betriebssystem,
es braucht allerdings ein Genie, um diese Einfachheit zu verstehen.«)
– Dennis Ritchie Wenn von Prozessen die Rede ist, meint man in der Ausführung befindliche Programme. Prozesse wurden bereits in Kapitel 2 eingeführt. Nun wollen wir uns damit befassen, wie Prozesse in der Shell gesteuert werden können.
5.1 Sessions und Prozessgruppen Jeder Prozess ist Mitglied einer Prozessgruppe. Wie der Name schon sagt, befinden sich ein oder mehrere Prozesse in solch einer Gruppe. Eine Prozessgruppe ist wiederum einer Prozess-Session untergeordnet (siehe Abbildung 5.1). Betrachten wir diese Gebilde nun einmal im Einzelnen. Prozessgruppen werden – wie auch Prozesse – durch eine eindeutige Identifikationsnummer unterschieden, die sogenannte Prozessgruppen-ID (PGID). Prozessgruppen haben wiederum einen Prozessgruppenführer. Dieser ist einer Session zugeordnet. Eine sogenannte Session kann aus einer oder mehreren Prozessgruppen bestehen. Wie auch bei Prozessgruppen haben
Sessions einen Sessionführer. Dies kann beispielsweise eine Shell oder auch ein Daemonprozess sein. Ein Sessionführer richtet die Verbindung zum Kontrollterminal ein und wird daher als Kontrollprozess bezeichnet.
Abbildung 5.1 Eine Session mit zwei Prozessgruppen
Ein Beispiel
Zum besseren Verständnis hier ein Beispiel: Eine Benutzerin meldet sich am System an, ihre Shell startet. Die Shell wird Sessionführer und nimmt die Eingaben der Benutzerin entgegen. Würde die Benutzerin nun beispielsweise die in Kapitel 4, »Grundlagen der Shell«, beschriebene Pipe (|) nutzen und die Ausgabe eines Programms als Eingabe für ein anderes verwenden, würden diese beiden Programme (ls und grep, siehe Abschnitt 6.2) eine Prozessgruppe bilden:
$ ls | grep ".txt"
hallo.txt
$
Listing 5.1 Filter: Beispiel für eine einfache Prozessgruppe
Das nächste Beispiel würde die Ausgabe von ls nach dem Muster ».txt« filtern und alle Textdateien aus dem aktuellen Verzeichnis anzeigen. $ ls *.txt
hallo.txt
$
Listing 5.2 Eine Prozessgruppe mit nur einem Mitglied
Mit der Eingabe aus dem zweiten Listing würde ein Benutzer dasselbe Ergebnis erzielen, jedoch mit nur einem Prozess. Das spart dem Kernel Aufwand. Und da das Filtern bei der Verzeichnisausgabe recht oft nötig ist, wurde das Feature kurzerhand in ls implementiert. Würden nun beide Prozessgruppen parallel ablaufen, indem wir eine Prozessgruppe in den Hintergrund packen (siehe folgender Abschnitt), so würden beide Prozessgruppen derselben Session angehören.
5.2 Vorder- und Hintergrundprozesse Normalerweise werden in der Shell eines Benutzers Programme gestartet, die bestimmte Eingaben des Benutzers erwarten, dann etwas berechnen und schließlich das bzw. die Ergebnisse ausrechnen und ausgeben. Dieses EVA-Prinzip (Eingabe – Verarbeitung – Ausgabe) kann natürlich auch abgewandelt sein, sodass wie bei ls gleich die »Berechnung« der Verzeichniseinträge startet und darauf dann die Ausgabe folgt. Jedenfalls werden solche interaktiven, für den Benutzer bzw. die Benutzerin steuerbaren Programme als Vordergrundprozesse bezeichnet. Jedoch gibt es eine weitere Möglichkeit für den Ablauf eines Programms innerhalb einer Session: den Hintergrund. Ein Hintergrundprozess läuft zwar, bekommt aber keine Eingaben seitens des Benutzers. Der Benutzer kann, während ein Hintergrundprozess läuft, die Shell weiter zur Arbeit nutzen und neue Prozesse starten ... Zumindest gibt es in der Regel keine störenden Ausgaben des Hintergrundprozesses, doch selbst diese könnte man unterdrücken. Hierzu später mehr. Stellen Sie sich einmal Folgendes vor: Sie benötigen eine Datei, starten also eine Suche. Das dazugehörige Kommando find durchsucht das Dateisystem – das eine große Anzahl an Dateien beinhaltet – über einen längeren Zeitraum nach dieser Datei. Sie selbst sind genervt und möchten eigentlich mit der Arbeit fortfahren. Genau hier setzt das Prinzip des Hintergrundprozesses an: Sie starten den Befehl im Hintergrund oder befördern ihn nachträglich dorthin – und können weiterarbeiten. Um einen Prozess im Hintergrund zu starten, hängen Sie ein kaufmännisches »Und« (&) an das Kommando an:
$ Prozess &
[1] 14215
…
[1] + done /usr/local/bin/Prozess
Listing 5.3 Schreibweise zum Starten eines Prozesses im Hintergrund
Das Listing zeigt einen Prozess, der im Hintergrund gestartet wird. Nach dem Start wird die Nummer des Hintergrundprozesses – die sogenannte Job-ID – in eckigen Klammern (in diesem Fall Nummer 1), gefolgt von der Prozess-ID (hier 14215), ausgegeben. Nach einiger Zeit ist der Prozess mit der Abarbeitung seiner Aufgaben fertig. Dem Benutzer bzw. der Benutzerin wird dies durch die done-Zeile mitgeteilt. Bei der Arbeit mit Prozessen steht also oft die Frage im Vordergrund, ob ein Prozess bzw. ein Programm Ihre direkte Aufmerksamkeit erfordert oder ob es still im Hintergrund seine Arbeit verrichten kann. Eine spezielle Art von Prozessen sind die sogenannten Daemonprozesse. Sie arbeiten im Hintergrund und werden vorwiegend für Aufgaben genutzt, die keiner direkten Kontrolle bedürfen. Das sind Serverdienste wie beispielsweise Webserver oder Mailserver. Daemonprozesse und Shell-Hintergrundprozesse Dummerweise werden Daemonprozesse oftmals mit den Hintergrundprozessen der Shell verwechselt. Wie wir oben jedoch erläutert haben, sind Daemonprozesse eigene Sessionführer und unabhängig von einer Shell. Solche Daemonprozesse werden normalerweise während des Bootens gestartet und erst beim Shutdown des Systems beendet,
indem der Kernel ein TERMINATE- bzw. KILL-Signal an den Prozess sendet.
Wie wir bereits erwähnt haben, haben Hintergrundprozesse ebenfalls eine Nummer zur Identifikation. Daraus lässt sich schließen, dass es möglich ist, mehrere Prozesse parallel im Hintergrund ablaufen zu lassen. $ sleep 10 &
[1] 10203
$ sleep 10 &
[2] 10204
$ sleep 10 &
[3] 10205
$ sleep 1
[3] + Done [2] - Done [1] Done
sleep 10
sleep 10
sleep 10
Listing 5.4 Parallele Hintergrundprozesse
Es ist wichtig zu wissen, dass ein Hintergrundprozess automatisch laufen muss. So ist es beispielsweise nicht möglich, Tastatureingaben an diesen Prozess zu senden. Des Weiteren werden die Ausgaben des Hintergrundprozesses einfach zwischen die Ausgaben anderer Shellprogramme gemischt, was Ihnen einiges durcheinanderbringen könnte. Um dieses Problem zu lösen, sollten Sie die Ausgabeumlenkung (siehe Abschnitt 4.7) benutzen, die wir zu diesem Zweck in Abschnitt 5.2.4 anwenden werden. 5.2.1 Prozessgruppen mit mehreren Prozessen
Durch Klammerung kann eine Prozessgruppe, die aus mehreren Prozessen besteht, in den Hintergrund gepackt werden. $ (sleep 1; echo "Hallo") &
[12] 9790
$ Hallo
Listing 5.5 Nach einer Sekunde »Hallo« sagen: eine ganze Prozessgruppe im Hintergrund
5.2.2 Wechseln zwischen Vorder- und Hintergrund
In einigen Shells, wie der bash oder der ksh, ist es möglich, zwischen Vorder- und Hintergrundprozessen zu wechseln. Damit aus einem Vordergrundprozess ein Hintergrundprozess wird, muss er erst einmal angehalten (gestoppt) werden. Das wird mittels der Tastenkombination (Strg) + (Z) realisiert. Testen können Sie das folgendermaßen: $ sleep 10
^Z[1] + Stopped
sleep 10
Listing 5.6 Stoppen eines Vordergrundprozesses
Sollte die Tastenkombination (Strg) + (Z) bei Ihnen nicht funktionieren, ist Ihr Terminal wahrscheinlich auf eine andere Tastenkombination eingestellt. Prüfen Sie die Terminalkonfiguration mit dem Kommando stty nach. Die Kombination für susp (suspend) ist zum Anhalten eines Prozesses vorgesehen. Die Zeichen vor den Großbuchstaben stehen hierbei für die Tastenkombination (Strg) + Buchstabe. $ stty -a
…
eol2 = ; start = ^Q; stop = ^S; susp = ^Z…
…
Listing 5.7 Was tun, wenn’s nicht funktioniert?
Um den obigen Vordergrundprozess nun in den Hintergrund zu befördern, muss das bg-Kommando (background) aufgerufen
werden. Der Aufruf ist wirklich simpel und erfolgt mittels bg %: $ sleep 10
^Z[1] + Stopped $ bg %1
[1] sleep 10
[1] + Done
sleep 10
sleep 10
Listing 5.8 Einen Prozess in den Hintergrund befördern
Hin und wieder kommt es jedoch vor, dass man einen Prozess wieder (zurück) in den Vordergrund bringen möchte. Dazu wird das fg-Kommando verwendet: $ sleep 120
^Z[1] + Stopped $ bg %1
[1] sleep 120
$ kill -STOP %1
[1] + Stopped (signal) $ fg %1
sleep 120
$
sleep 120
sleep 120
Listing 5.9 Einen Prozess in den Vordergrund holen
Wenn Sie die Prozessnummer eines Hintergrundprozesses mit einem Programm wie kill, fg oder bg verwenden, müssen Sie das Prozentzeichen (%) vor die ID setzen: $ bg %1
$ fg %1
$ kill -STOP %1 && kill -CONT %1
Listing 5.10 Beispielaufrufe mit Modulo
Alternativ können Sie auch mit der Prozess-ID arbeiten, aber die ist meist eine größere Zahl und die Gefahr, dass man sich vertippt, ist daher recht hoch. Außerdem sind Informatiker faul. 5.2.3 Jobs – behalten Sie sie im Auge
Oftmals hat man mehrere Prozesse oder gar Prozessgruppen parallel im Hintergrund laufen. Wird die Shell jedoch beendet, werden alle Hintergrundprozesse »mit in den Tod gerissen«. Um dies zu vermeiden, geben die meisten Shells, etwa die Z-Shell, bei dem ersten Versuch, die Shell zu beenden, eine Warnung aus, sofern noch Hintergrundprozesse ablaufen. Im nächsten Beispiel starten wir in der laufenden Shell eine weitere, um nach der Beendigung der neu gestarteten die Ausgabe der letzten Shell zu sehen. bash$ zsh
…
zsh$ sleep 10000&
[1] 782
zsh$ exit
zsh: you have running jobs.
zsh$ exit
zsh: warning: 1 jobs SIGHUPed
bash$
Listing 5.11 Warnmeldungen der Shells beim Beenden
Nachdem wir uns nicht um den noch laufenden Hintergrundprozess (auch »Job« genannt) gekümmert haben, wird dieser über ein sogenanntes HUP-Signal (Hang-up) beendet. Jobs
Um sich eine Übersicht über die momentan laufenden Hintergrundprozesse zu verschaffen, wird das Kommando jobs verwendet. Dieses Kommando listet alle Hintergrundprozesse der aktuellen Shell auf – also nicht alle Hintergrundprozesse eines Benutzers, denn Benutzer und Benutzerinnen können gleichzeitig mehrere Shells verwenden.
$ jobs
[1]+ Running $ jobs -p
214
$ jobs -l
[1]+ 214 Running
sleep 10000 &
sleep 10000 &
Listing 5.12 Das Kommando jobs
Wird der Parameter -p verwendet, werden die Prozess-IDs der Hintergrundprozesse ausgegeben, und bei -l werden diese zur Default-Ausgabe hinzugefügt. 5.2.4 Hintergrundprozesse und Fehlermeldungen
Es kommt sehr häufig vor, dass Hintergrundprozesse störende Fehlermeldungen auf die Konsole schreiben. Dies kann durch eine nicht gefundene Datei, eine Zugriffsverletzung oder Ähnliches hervorgerufen werden. In diesem Fall sollten Sie sich mit der Ausgabeumlenkung der Fehlerausgabe behelfen – natürlich bevor Sie den Prozess starten. Standarddeskriptoren: Prozesse verfügen über verschiedene Deskriptoren. Die drei Standarddeskriptoren sind die Standardeingabe (0) zum Eingeben von Zeichen, die Standardausgabe (1) zum Ausgeben der normalen Programmausgabe und eine Fehlerausgabe (2) zur Ausgabe der Fehlermeldungen. Diese Deskriptoren werden über ihre Nummern (0–2) angesprochen und können über Pipes und Ausgabeumlenkungen von der Konsole »verbannt« werden. Schauen wir uns einmal folgendes Beispiel an: Die Benutzerin startet ein Programm im Hintergrund, das versucht, die Datei /etc/blub zu löschen. Dabei wird jedoch eine Fehlermeldung direkt auf die Konsole geschrieben, da diese Datei gar nicht vorhanden ist:
$ rm /etc/blub &
[1] 132
$ rm: cannot remove '/etc/blub': No such file or directory
[1]+ Exit 1 rm /etc/blub
Listing 5.13 Fehlermeldungen bei Hintergrundprozessen
Stellen Sie sich ein Programm vor, das kontinuierlich einmal pro Sekunde eine ähnlich lästige Meldung ausgibt. Über die Umlenkung des zweiten Deskriptors (also der Fehlerausgabe) kann dies nun vermieden werden. Leiten wir die Fehlerausgabe einmal in die Datei Logdatei um: $ rm /etc/blub 2>Logdatei &
[1] 133
$
[1]+ Exit 1 rm /etc/blub
$ cat Logdatei
rm: cannot remove '/etc/blub': No such file or directory
Listing 5.14 Fehlermeldungen umlenken
Auf diese Weise haben wir gleich zwei Fliegen mit einer Klappe geschlagen: Die lästigen Ausgaben sind weg und wir haben den Fehler archiviert. Jetzt könnten wir sogar den gestressten Support mit der exakten Fehlermeldung nerven! 5.2.5 Wann ist es denn endlich vorbei?
Keine Sorge, dies ist der letzte Abschnitt zum Thema Hintergrundprozesse. Die Überschrift gilt jedoch einer anderen Angelegenheit: dem Warten auf die Beendigung eines Hintergrundprozesses. Hierfür wird ganz einfach das Kommando wait verwendet. Als Parameter wird der gewünschte Hintergrundprozess, besser gesagt dessen Nummer, angegeben. Wie Sie bereits wissen, muss für Hintergrundprozesse der Modulo-Operator verwendet werden:
$ sleep 10&
[1] 237
$ jobs
[1]+ Running $ wait %1
[1]+ Done $
Listing 5.15 wait
sleep 10 &
sleep 10
5.3 Das kill-Kommando und Signale Zur Steuerung von Prozessen werden unter Unix sogenannte Signale verwendet. Ein Signal können Sie sich als Steueranweisung in Form einer Zahl vorstellen. Solch ein Signal wird entweder durch einen Prozess im Userspace oder direkt vom Kernel an einen Prozess gesendet. (Letztendlich sendet immer der Kernel und kein Prozess solch ein Signal. Ein Kommando wie kill fordert den Kernel lediglich auf, ein Signal zu senden.) Softinterrupts Signale werden oft auch als Softinterrupts bezeichnet.
Es gibt verschiedene Signale. So kann zum Beispiel ein Prozess zur Terminierung über das Signal 9 (SIGKILL) gezwungen werden. Terminator! Es gibt allerdings auch Signale, die vom Prozess abgefangen werden können. Ob ein Prozess auf ein solches Signal reagiert und welche Aktionen genau er dann durchführt, liegt ganz allein im Ermessen des Programmierers. Dem Benutzer bzw. der Benutzerin steht mit dem Kommando kill die Möglichkeit zur Verfügung, Signale zu versenden. Hierbei werden der Signaltyp und die Prozess-ID des Zielprozesses bzw. dessen Jobnummer angegeben. $ kill 499
$ kill -9 500
$ kill -SIGKILL 501
Listing 5.16 Beispielaufruf des kill-Kommandos
SIGTERM als Standardeinstellung
Wird kill ohne einen Signalparameter und lediglich mit einer Prozess-ID aufgerufen, so wird das Signal SIGTERM an den Prozess gesendet, das ihn zur Beendigung auffordert, aber nicht zwingend seine Beendigung erwirkt.
5.3.1 Welche Signale gibt es?
Wie wir bereits erwähnt haben, gibt es zwei Gruppen von Signalen: Eine Gruppe kann vom Prozess ignoriert werden, die andere nicht. Es gibt zwei Signale, die den Prozess zwingen, sich dem Signal zu beugen (besser gesagt: die den Kernel veranlassen, den Prozess zu beenden oder zu stoppen): Signal 9, SIGKILL oder KILL
Dieses Signal beendet einen Prozess zwingend durch den Kernel. Signal 19, SIGSTOP oder STOP
Dieses Signal unterbricht die Verarbeitung eines Prozesses, bis er fortgesetzt wird. Die anderen Signale (es gibt ca. 30 Signale) werden teilweise so gut wie niemals vom Anwender benötigt. Daher stellen wir an dieser Stelle nur die für den Anwender und den Administrator bzw. die Administratorin wichtigsten und nützlichen Signale vor: Signal 1, SIGHUP oder HUP
Der Prozess soll sich selbst beenden und neu starten. Dieses Signal wird oftmals benutzt, um Daemonprozesse neu zu starten, damit diese ihre Konfigurationsdaten neu einlesen. Signal 15, SIGTERM oder TERM
Dieses Signal soll den Prozess dazu bewegen, sich freiwillig zu beenden. Wenn der Computer heruntergefahren wird, sendet der Kernel allen Prozessen solch ein Signal. Daraufhin haben die
Prozesse einige Sekunden Zeit, sich zu beenden und beispielsweise Konfigurationsdaten zu speichern, bevor letztendlich das SIGKILL-Signal an alle Prozesse gesendet wird. Signal 18, SIGCONT oder CONT
Durch dieses Signal wird die Fortsetzung eines angehaltenen Prozesses eingeleitet. Einige Shells (wie z. B. die bash) enthalten ihre eigenen Implementierungen des kill-Kommandos. Diese Implementierungen bieten vereinzelt weitere Signaltypen. Die bash zum Beispiel unterstützt über 60 verschiedene Signale. Hierbei sollte zwischen den shellinternen Kommandos und Programmen unterschieden werden. Eine Liste der von Ihrem kill unterstützten Signale bekommen Sie durch einen Aufruf von kill -l. Das Linux-kill-Kommando kennt darüber hinaus den -L-Parameter für eine tabellarische Ausgabe. 5.3.2 Beispiel: Anhalten und Fortsetzen eines Prozesses
Zur Verdeutlichung folgt nun ein Anwendungsbeispiel. Unser Ziel ist es, einen Prozess mit hohem Rechenaufwand zu starten. Er soll jedoch angehalten werden, um einige andere Prozesse ohne Verzögerung starten zu können. Nachdem die Nutzung der anderen Prozesse abgeschlossen ist, soll der Prozess fortgesetzt werden. Unser »Rechenleistungskiller« wird in diesem Fall das findKommando sein, mit dem das gesamte Dateisystem nach Vorkommen des Dateinamens »group« durchsucht wird. In der Zwischenzeit soll allerdings ein anderes Programm ausgeführt werden. In Abschnitt 5.4.1 lernen Sie übrigens eine weitere Methode zur Lösung dieses Problems kennen, indem wir die Priorität eines Prozesses verringern.
$ find / -name group >erg.txt 2>/dev/null &
[1] 628
$ kill -STOP 628
[1]+ Stopped find / -name group >erg.txt 2>/dev/null
$ date
Mo 21. Jan 15:58:16 CET 2019
$ date
Mo 21. Jan 15:58:18 CET 2019
$ jobs
[1]+ Stopped find / -name group >erg.txt 2>/dev/null
$ kill -CONT %1
$ wait %1
[1]+ Exit 1 find / -name group >erg.txt 2>/dev/null
$ cat erg.txt
/etc/X11/xkb/symbols/group
/etc/group
Listing 5.17 Anwendung des kill-Kommandos mit STOP und CONT
Wie Sie sehen, ist bei Hintergrundprozessen sowohl die Angabe der Prozess-ID als auch die der Hintergrundprozessnummer möglich. Statt des Aufrufs kill -STOP 628 hätte genauso das Kommando kill -STOP %1 »nach Rom« geführt. Natürlich könnte anstelle des -STOPParameters auch -SIGSTOP oder -19 stehen. killall
Das Kommando killall funktioniert ähnlich wie kill, beendet Prozesse jedoch über ihren Namen. // Ein Benutzer startet auf einem Terminal 'find' …
$ find / -name lib
// … währenddessen in einem anderen Terminal …
# killall find
// … der Benutzer erfährt schließlich von der Beendigung:
Terminated
Listing 5.18 Ein Beispiel für killall
5.4 Prozessadministration Im letzten Abschnitt dieses Kapitels beschäftigen wir uns mit der Administration von Prozessen. Ein sehr wichtiges administratives Programm (nämlich kill) kennen Sie bereits. Die folgenden Programme dienen primär dazu, sich einen Überblick über die laufenden Prozesse zu verschaffen. 5.4.1 Prozesspriorität
Bei der Berechnung der Prozesspriorität spielt vor allem der sogenannte Nice-Wert eines Prozesses eine wichtige Rolle. Je nachdem, wie hoch (oder niedrig) dieser Nice-Wert ist, desto niedriger bzw. höher ist die Wahrscheinlichkeit, dass ein Prozess mit mehr CPU-Zeit beglückt wird. Nice-Werte können zwischen --20 und 19 variieren, wobei --20 für die höchste zu vergebende Priorität steht. Das Sonderrecht des Superusers Nur der Administrator darf die Prozesspriorität über den Nullwert steigern. Den normalen Benutzern ist es nur erlaubt, die Prozesspriorität herabzusetzen, was beispielsweise bei längeren Dateisuchen sinnvoll sein kann.
Das Kommando nice
Die Nice-Werte werden mit dem nice-Kommando gesetzt. Das Wort nice kommt aus dem Englischen und bedeutet so viel wie »nett«,
und nett ist man ja, wenn man freiwillig auf Rechenzeit verzichtet. Dem Kommando wird die Priorität über den Parameter -n mitgeteilt und das eigentliche Kommando wird nachstehend mit allen Aufrufargumenten beschrieben: $ nice -n 19 find / -name libcurses.a >Ergebnis
Listing 5.19 nice in Aktion
Bereits laufende Prozesse
Die Priorität bereits laufender Prozesse wird mit dem reniceKommando verändert. Dabei kann über den Parameter -p die Priorität über die Prozess-ID, via -u-Parameter über den Benutzernamen oder via -g über die Gruppe geändert werden. Im folgenden Listing wird die Nice-Priorität des Prozesses mit der PID 491 um den Wert 10 verringert. Das Gleiche gilt für alle Prozesse des Benutzers nobody. $ renice +10 -p 491 -u nobody
Listing 5.20 renice für Benutzer und PIDs
Natürlich können sich Benutzer nicht gegenseitig die Rechenzeit stehlen, indem sie die Prioritäten erhöhen. Normalerweise kann jeder Benutzer bzw. jede Benutzerin nur eigene Prozesse herunterpriorisieren. Das Heraufpriorisieren und der Zugriff auf »fremde«, d. h. unter einer anderen User-ID laufende, Prozesse ist dem Administrator root vorbehalten. Auch sollte man beachten, dass man durch Herauf- bzw. Herabsetzen von Prioritäten in Summe keine Rechenzeit gewinnt, sondern nur die Zeit für einzelne Prozesse anders verteilt. 5.4.2 pstree
Das Kommando pstree (process tree) gibt einen Prozessbaum aus. Dies ist eine sehr sinnvolle Funktion, um sich einen Überblick über das Verhalten einiger Programme und deren Kindprozesse zu verschaffen. Darüber hinaus eignet es sich hervorragend, um LinuxProzesse und ihre Hierarchie kennenzulernen. Was ist ein hierarchischer Prozessbaum? Die Hierarchie der Prozesse kennen Sie bereits. pstree visualisiert im Prinzip diese virtuelle Ordnung in einem ASCII-Baum – jeder Zweig des Baums stammt von einem Elternprozess ab. $ pstree
systemd-+-ModemManager-+-gdbus
| `-gmain
|-NetworkManager-+-dhclient
| |-dnsmasq
| |-gdbus
| `-gmain
…
|-2*[dbus-daemon]
…
Listing 5.21 pstree ohne Argumente (gekürzt)
Ein interessantes Feature, das Sie im nächsten Listing sehen, ist die Gruppierung der Kindprozesse zu ihrem Elternprozess. Dies wird in der Form Parent---Anzahl*[Child] verdeutlicht, wobei Child der Name des Kindprozesses ist und Anzahl die Anzahl der parallel laufenden Kindprozesse angibt. Der systemd-Prozess hat zweimal dbus-daemon gestartet Threads (also leichtgewichtige Prozesse, das sind sozusagen Miniprozesse innerhalb eines Prozesses) werden mit geschweiften Klammern angezeigt. Eine ausführlichere Ausgabe kann mit dem Parameter -a erreicht werden. Dies bewirkt, dass die beim Programmstart eines jeden Prozesses übergebenen Parameter mit angezeigt werden: $ pstree -a
systemd splash
|-ModemManager
| |-gdbus
| `-gmain
|-NetworkManager --no-daemon
| |-dhclient -d -q -sf /usr/lib/NetworkManager/nm-dhcp-helper
-pf /var/run/dhclient-enp0s3.pid -lf...
| |-dnsmasq --no-resolv --keep-in-foreground --no-hosts
--bind-interfaces --pid-file=/var/run/NetworkManager/
dnsmasq.pid--lis
…
Listing 5.22 pstree mit Detailausgabe (gekürzt)
Sofern Sie ein Terminal mit Fettschriftunterstützung verwenden, können Sie den Parameter -h (highlight) nutzen. Dieser zeigt den pstree-Prozess inklusive aller seiner Elternprozesse in Fettschrift an. Das ist eine gute Möglichkeit, die Hierarchie der Prozesse nochmals zu verinnerlichen. Weitere wichtige Parameter sind -p für eine Ausgabe der Prozess-IDs aller Prozesse und -u für die Angabe des effektiven Benutzers eines Prozesses (sofern dieser geändert wurde). Im folgenden Beispiel läuft der Prozess VBoxClient etwa mit der Prozess-ID 2122 und der Benutzer wurde in wendzel geändert. $ pstree -apu
systemd,1 splash
|-ModemManager,894
| |-gdbus,948
| `-gmain,946
|-NetworkManager,889 --no-daemon
| |-dhclient,7003 -d -q -sf /usr/lib/NetworkManager/...
…
|-VBoxClient,2122,wendzel --draganddrop
| `-VBoxClient,2123 --draganddrop
| |-dndHGCM,2127
| `-dndX11,2129
…
Listing 5.23 pstree-Parameterkombination
5.4.3 Prozesslistung mit Details via ps
Kommen wir nun zu einem der wichtigsten Programme von Linux – dem ps-Kommando. ps gibt Ihnen eine Übersicht über Ihre eigenen oder auch alle laufenden Prozesse des Systems. Dabei werden diverse Prozessattribute auf Wunsch mit ausgegeben. Die Besonderheit an der Linux-Version von ps ist, dass es sowohl die Features der SVR4- als auch die der BSD-Version von ps unterstützt. Hinzu kommen einige GNU-Features. Oftmals führen daher mehrere Parameter zum gleichen Resultat. Bei einem parameterlosen Aufruf des Programms erscheint eine Liste aller Prozesse, die in Ihrer aktuellen Shell laufen: $ ps
PID TTY 8286 pts/7 8440 pts/7
TIME CMD
00:00:00 bash
00:00:00 ps
Listing 5.24 ps
Wie Sie sehen, erfolgt die Ausgabe in Form einer Tabelle. Die Spalte PID enthält die Prozess-ID, die TTY-Spalte gibt das Terminal an, auf dem der Prozess läuft. Die Abkürzung »TTY« geht auf die historischen Druckterminals, genannt Teletype, aus den viktorianischen Telegrafennetzen zurück.[ 26 ] TIME gibt die bereits für den Prozess aufgebrachte CPU-Zeit an. Die letzte Spalte, CMD, repräsentiert das eigentliche Kommando, also den Befehl, so wie er irgendwann einmal eingegeben wurde. Befassen wir uns nun mit den Parametern. Der Parameter -A gibt alle momentan laufenden Prozesse aus. Der Parameter -e liefert das gleiche Ergebnis, -a zaubert lediglich eine Ausgabe aller Prozesse des Terminals hervor. Sofern Sie sehr detaillierte Informationen zur Prozessliste benötigen, verwenden Sie den Parameter -f. Mit -l wird das »long
format« benutzt. Das heißt, User-IDs werden in Benutzernamen aufgelöst und es werden die Aufrufparameter der Prozesse angezeigt. $ F 0 0
ps -lf
S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
S sw 8286 2678 0 80 0 - 6114 wait 15:58 pts/7 00:00:00 bash
R sw 8449 8286 0 80 0 - 9748 16:09 pts/7 00:00:00 ps -lf
Listing 5.25 ps mit -f und -l
Sehr interessant sind im Übrigen einige BSD-Parameter wie u und f. Der Parameter u bewirkt die benutzerspezifische Ausgabe, gibt also Ihre eigenen Prozesse aus. f gibt den Prozessstatus in der Spalte S an und erstellt außerdem – ähnlich wie pstree, jedoch nicht so hübsch – einen Prozessbaum. Der Status wird in Form eines Großbuchstabens repräsentiert. S steht beispielsweise für Sleep, R steht für Running. Für Individualisten gibt es noch den -o-Parameter. Er liefert eine selbst konfigurierbare Ausgabe. Dabei kann die Ausgabe in der Form »SpalteA SpalteB SpalteC« festgelegt werden. Erlaubte Schlüsselwörter zeigt Tabelle 5.1. Schlüsselwort Beschreibung pcpu
CPU-Nutzung
group
Gruppenzugehörigkeit
ppid
Elternprozess-ID
user
Eigentümer
args
Parameter beim Programmaufruf
comm
Name des Prozesses
Schlüsselwort Beschreibung nice
Nice-Priorität
pid
Prozess-ID
pgid
Prozessgruppen-ID
time
verbrauchte Rechenzeit
tty
benutztes Terminal
Tabelle 5.1 Schlüsselwörter für den ps-Parameter -o
Probieren Sie es einmal aus. Unser Ziel ist es, alle Prozesse auszugeben, die momentan in der Prozessliste aufzufinden sind. Dabei sollen jedoch nur der Benutzername, die Prozess-ID, das Kommando selbst und das Terminal des Prozesses ausgegeben werden. $ ps -eo "user pid comm"
USER PID COMMAND
…
wendzel 1891 systemd
wendzel 1892 (sd-pam)
wendzel 1897 pulseaudio
wendzel 1899 tracker-miner-f
wendzel 1903 dbus-daemon
wendzel 1905 gnome-keyring-d
wendzel 1925 gvfsd
wendzel 1930 gvfsd-fuse
wendzel 1936 gvfs-udisks2-vo
wendzel 1944 gvfs-mtp-volume
wendzel 1951 gvfs-afc-volume
wendzel 1958 kwalletd5
wendzel 1959 startplasma-x11
wendzel 1960 gvfs-gphoto2-vo
wendzel 1968 gvfs-goa-volume
wendzel 1972 goa-daemon
wendzel 1998 goa-identity-se
wendzel 2026 tracker-store
wendzel 2030 ssh-agent
wendzel 2053 start_kdeinit
wendzel 2054 kdeinit5
wendzel 2065 kded5
wendzel wendzel wendzel wendzel …
2073 2087 2092 2094
kaccess
kglobalaccel5
dconf-service
kactivitymanage
Listing 5.26 Individueller ps-Aufruf
5.4.4 top
Ein weiteres, sehr beliebtes und einfach zu handhabendes Tool zur Überwachung der Prozesse ist top. Der Unterschied zum psKommando besteht darin, dass top die Prozessliste periodisch auf dem Bildschirm ausgibt und diese Ausgabe nach einigen vorgegebenen Kriterien – beispielsweise nach der CPU-Nutzung oder dem Speicherverbrauch – sortieren kann. Nachdem man top gestartet hat, zeigt sich ein Header mit anschließender Prozesstabelle (siehe Abbildung 5.2). Der mehrzeilige Header enthält die Laufzeit des Systems (genannt uptime) und die aktuelle Uhrzeit, die Anzahl der momentan angemeldeten Benutzer (x users) und die durchschnittliche Anzahl der Prozesse in den letzten 1, 5 und 15 Minuten, die auf ihre Abarbeitung durch die CPU warten (load average).
Abbildung 5.2 top
Zeile 2 des Headers gibt die Anzahl der Prozesse und deren Status an, das heißt, wie viele dieser Prozesse gerade »schlafen«, von der CPU verarbeitet werden oder Zombie-Prozesse sind und gestoppt wurden. Die dritte Zeile gibt Aufschluss über die Verteilung der aktuellen Rechenzeit an den Userspace (user), den Kernelspace (system) und an nice-Zeit. Die Angabe idle gibt an, zu wie viel Prozent der Zeit der Prozessor nicht ausgelastet ist. Die Zeilen 4 und 5 informieren über den physikalischen Hauptspeicher und den Swap-Speicher. Der Wert vor av gibt den maximal verfügbaren Speicher an, used den davon momentan benutzten Teil und free den unbenutzten Anteil. Die Ausgabe in Form einer Prozesstabelle an sich ist ähnlich wie bei ps aufgebaut und besteht im Prinzip aus den gleichen Spalten. Rufen Sie top mit dem Parameter -i auf, um nur die derzeit laufenden Prozesse anzeigen zu lassen.
Ordnung im Chaos
Die Sortierfunktion von top wird über die Steuertasten aufgerufen. Eine Liste dieser Tasten erhalten Sie, indem Sie (H) (Help) drücken. Eine Liste der wichtigsten Sortiertasten finden Sie auch in Tabelle 5.2. Taste Funktionalität
(N)
absteigende Sortierung nach Prozess-ID
(P)
absteigende Sortierung nach CPU-Nutzung
(M)
absteigende Sortierung nach Speichernutzung
(T)
absteigende Sortierung nach bisheriger Beanspruchung von CPU-Zeit
Tabelle 5.2 Funktionstasten für die Sortierung der Prozessliste
5.4.5 Timing für Prozesse
Das Kommando time wird in Verbindung mit einem Programm aufgerufen. Nach Beendigung dieses Prozesses werden dessen gesamte Laufzeit und die verbrauchte CPU-Zeit im Kernelspace (sys) und im Userspace (user) ausgegeben. time ist zwar kein richtiger Benchmark, aber wenn Sie wissen wollen, wie schnell Ihr Rechner eine Aufgabe bewältigen kann, erhalten Sie so die passenden Informationen. $ time find /usr/bin -name gedit
/usr/bin/gedit
real 0m0.015s
user 0m0.008s
sys 0m0.004s
Listing 5.27 time
5.5 Zusammenfassung In diesem Kapitel haben wir Prozesse sowie ihre Administration besprochen. Das bezieht das Auflisten der aktuell laufenden Prozesse mit Programmen wie top und ps und das Beenden von Prozessen mit von kill an den Kernel übertragenen Signalen sowie das Arbeiten mit Hintergrundprozessen ein.
5.6 Aufgaben Anzahl der Prozesse nach dem Start
Überprüfen Sie mithilfe von ps die Anzahl der Prozesse, die direkt nach der Anmeldung auf Ihrem Rechner laufen. Starten Sie nun den Browser, besuchen Sie eine Webseite und starten Sie ein paar Programme. Wie hat sich die Anzahl der Prozesse verändert? Überprüfen Sie zudem mit pstree, welche Prozesse welche anderen Prozesse erzeugt haben. Eine Session, zwei Prozessgruppen
Erstellen Sie in Ihrer Login-Shell zwei Prozessgruppen mit jeweils zwei Prozessen.
6 Reguläre Ausdrücke »Everybody stand back. I know regular expressions.«
(Dt. »Aus dem Weg. Ich kenne reguläre Ausdrücke.«)
– Randall Munroe, https://xkcd.com/208/ In diesem Kapitel werden wir uns nun mit einem sehr bedeutsamen Thema, nämlich den regulären Ausdrücken, befassen. Diese können über die Tools awk und sed hervorragend verwendet werden. Sie erleichtern den Alltag der Linuxnutzung enorm und sind ein wichtiger Bestandteil von Shell-Skripts.
6.1 Grundlagen und Aufbau regulärer Ausdrücke Bisher haben Sie schon eine ganze Menge gelernt. Kommen wir nun zum nächsten großen Abschnitt zum Thema Shell: den regulären Ausdrücken (Regular Expressions) und damit zu den Programmen awk und sed. Reguläre Ausdrücke wurden bereits vor Jahrzehnten mit 4BSD bzw. dem ed-Editor eingeführt und werden heute noch im Wesentlichen so verwendet, wie sie einst definiert wurden. Wir werden vorwiegend grundlegende Formen der regulären Ausdrücke behandeln und weniger oft benutzte auslassen, um im Rahmen des Buches zu bleiben und Sie nicht unnötig zu »quälen«. Zum Ende dieses Abschnitts werden wir uns noch mit dem Tool grep beschäftigen. Doch nun zurück zur Einleitung. Reguläre Ausdrücke können als Platzhalter verwendet werden. So bieten sie die Möglichkeit, Kommandozeilenaufrufe und
Programme in sehr kurzer Weise zu gestalten, wenn es um die Verarbeitung von Textstreams geht. Dies wiederum geht auf Kosten der Lesbarkeit, doch lassen reguläre Ausdrücke sich mit etwas Übung bis zu einem gewissen Grad problemlos entziffern. Um Ihnen ein etwas genaueres Bild von dieser Thematik zu liefern, möchten wir ein einfaches Beispiel nutzen. Lassen Sie sich davon aber nicht täuschen: Reguläre Ausdrücke sind unglaublich mächtig und werden auch in vielen Programmiersprachen genutzt. Wer sie beherrscht, kann Kunststücke vollführen, für die sonst hunderte oder tausende Mausklicks nötig wären. Für den Einstieg befinden wir uns in einem Verzeichnis X, dessen Inhalt aus drei Dateien besteht: Baum, baum und Haus. Da reguläre Ausdrücke auch bei Programmen wie ls angewandt werden können, werden wir sie nun anhand obiger Dateien erläutern. Zuerst sollen alle Dateien aufgelistet werden, die mit einem beliebigen Zeichen beginnen und auf »aum« enden. Hierzu verwenden wir den regulären Ausdruck ?, der für genau ein beliebiges Zeichen (jedoch kein leeres, also fehlendes Zeichen) steht: user$ ls ?aum
Baum baum
Listing 6.1 Der Zeichen-Operator
Als Nächstes sollen alle Dateien, die mit einem kleinen oder großen »B« beginnen (und mit »aum« enden) aufgelistet werden, anschließend alle Dateien, die mit einem »m« enden, und schließlich alle Dateien, die ein »a« beinhalten. * steht dabei für beliebig viele beliebige Zeichen und das leere Zeichen. user$ ls [bB]aum
Baum baum
user$ ls *m
Baum baum
user$ ls *a*
Baum baum Haus
Listing 6.2 Einer für alles
Sie werden diese Operatoren im Verlauf des Abschnitts noch näher kennenlernen. Hier folgt zunächst erst einmal (der Übersicht halber) die Auflistung in einer Tabelle. Zeichen Beschreibung .
beliebiges Zeichen
*
beliebige Anzahl von beliebigen Zeichen (d. h., auch gar kein Zeichen – also ein leerer String – entspricht diesem Metazeichen)
+
beliebige Anzahl des Zeichens (mindestens einmal)
?
einzelnes oder kein Vorkommen eines Zeichens
[..]
Alle in der Klammerung eingeschlossenen Zeichen können an dieser Stelle vorkommen, beispielsweise [09].
[^..]
Eine Negierung. Kommt ein Zeichen nicht vor, ist die Bedingung erfüllt.
^
Zeilenanfang
$
Zeilenende
x
Das Zeichen kommt x-mal vor.
x,
Das Zeichen kommt x-mal oder mehrmals vor.
x,y
Das Zeichen kommt x- bis y-mal vor.
Tabelle 6.1 Reguläre Ausdrücke
6.2 grep grep ist eines der meistgenutzten Programme auf der Shell. Es
wurde ursprünglich von Ken Thompson entwickelt, der es zunächst s (für das englische Wort search) nannte. Es wird dazu verwendet, bestimmte Zeilen aus ASCII-Streams herauszufiltern, womit gemeint ist, dass sie entweder unterdrückt oder nur angezeigt werden. Das Programm kann die bereits erwähnten regulären Ausdrücke (etwa . für ein beliebiges Zeichen) direkt verwenden. Das folgende Beispiel zeigt, wie Sie mit grep alle Dateien finden, die mit »kap« beginnen, dann zwei beliebige Zeichen enthalten und auf ».tex« enden. Dabei wird die Ausgabe aus der Pipe des lsKommandos herausgefiltert. Wir verwenden das Backslash (\) vor dem Punkt, damit dieser nicht als regulärer Ausdruck, sondern schlicht als regulärer Punkt interpretiert wird. user$ ls | grep 'kap..\.tex'
kap01.tex
kap02.tex
kap03.tex
kap04.tex
…
Listing 6.3 Verwendung von grep
Analog könnten wir auch alle Nachrichten aus der Datei /var/log/syslog herausfiltern, die am 22. November zwischen 16:00:00 und 16:59:59 geloggt wurden: user$ grep 'Nov 22 16:..:.. ' /var/log/syslog
Nov 22 16:30:13 eygo /bsd: fupids: user 1000: …
Nov 22 16:34:17 eygo /bsd: fupids: new programm …
…
Listing 6.4 Zeitbereiche filtern
Die in obiger Tabelle erwähnten Zahlenbereiche können ebenfalls mit regulären Ausdrücken gesucht werden. Dies funktioniert übrigens auch bei Programmen wie ls. Im folgenden Beispiel filtern wir alle Dateien, die mit »kap« beginnen, dann eine »0« oder eine »1«, anschließend eine Zahl zwischen »0« und »9« im Namen tragen und auf ».tex« enden. Im ersten Beispiel filtern wir direkt bei ls, im zweiten Beispiel bei grep. wc -l zählt uns zum Vergleich schlicht die Anzahl der ausgegebenen Zeilen. user$ ls kap[01][0-9].tex | wc -l
15
user$ ls | grep 'kap[01][0-9]\.tex'
15
Listing 6.5 Zahlenbereiche filtern
6.2.1 Aufrufparameter für grep
Der Parameteraufruf für grep bietet eine Menge nützlicher Befehle. Die wichtigsten und meistgenutzten sind: -v
Löscht alle Zeilen, die mit dem regulären Ausdruck übereinstimmen: grep -v 'regex'. -c / --count
Dieser Parameter zeigt die gefilterten Zeilen nicht an, sondern zählt sie. Wenn er zusammen mit -v verwendet wird, werden die Zeilen gezählt, die dem Ausdruck nicht entsprechen. user$ ls | grep -c '^kap..\.tex'
15
Listing 6.6 Wie viele Dateien sind es denn?
-n / --line-number
Dieser Parameter gibt die Zeilennummer für die gefundenen
Zeilen aus. user$ grep -n Shellskript kap09.tex
37:\gpAbschnitt{Grundlagen der Shellskript-Programmierung}
41:Programmierung von Shellskripten ist in der Linux-Welt ein
44:Zunächst werden wir uns mit den grundlegenden Dingen der
...
Listing 6.7 Zeilenzahl
6.2.2 grep -E und egrep
Sehr hilfreich ist die Fähigkeit, mehrere Ausdrücke in einem Befehl zu filtern. Dabei verwendet man ein logisches ODER in Form eines Pipe-Zeichens zwischen den Ausdrücken sowie entweder grep mit der Option -E oder das Programm egrep. $ egrep -v 'n$|k$' Standorte
Augsburg
Bernburg
Halle
Krumbach
$ grep -vE 'n$|k$' Standorte
Augsburg
Bernburg
Halle
Krumbach
Listing 6.8 egrep
Ein Blick in die Manpage verrät uns das Geheimnis: egrep ist mit einem Aufruf von grep -E gleichzusetzen. Zudem findet man im Dateisystem, zumindest unter Slackware-Linux, den symbolischen Link /bin/egrep auf /bin/grep. Dies bedeutet, dass das Programm grep intern abfragt, ob der Programmname egrep oder nur grep lautet, und sein Verhalten der Option -E im Falle von egrep automatisch anpasst. 6.2.3 Exkurs: PDF-Files mit grep durchsuchen
Es ist möglich, mithilfe der poppler-utils (auch poppler-tools genannt), den Inhalt von PDF-Dateien in Textform auszugeben. Diese Textform kann dann wiederum mit Programmen wie grep durchsucht werden. Die poppler-utils stellen dazu das Programm pdftotext bereit. Übergeben wird dem Programm dabei zunächst ein Dateiname und als zweiter Parameter die Ausgabedatei oder ein »-«, um zu signalisieren, dass der Inhalt der Datei auf die Standardausgabe geschrieben werden soll. $ pdftotext CovertChannels.pdf - | grep portknocker
keywords : covert, channels, ... portknocker
It seems obvious that t... portknocker or ...
...
Listing 6.9 Eine PDF-Datei durchsuchen (Ausgabe gekürzt)
Die poppler-utils beinhalten übrigens auch einige weitere Programme wie etwa pdftohtml, mit dem der Inhalt von PDFDateien in HTML umgewandelt werden kann. Mit pdftops lassen sich die Dateien hingegen ins PostScript-Format konvertieren. $ pdftohtml essay.pdf essay.html
Page-1
Page-2
...
Listing 6.10 Eine PDF-Datei in HTML konvertieren
6.3 awk Nach dieser kurzen Einführung in grep wollen wir uns im Detail mit regulären Ausdrücken auseinandersetzen. Dazu verwenden wir awk. awk ist eine Skriptsprache zur Verarbeitung von ASCII-Text. Sie
wurde nach ihren Entwicklern Aho, Kernighan und Weinberger benannt und entwickelte sich im Laufe der Jahre zu einem populären Werkzeug der Anwender und Administratoren. Der Grund dafür ist unter anderem der, dass awk sehr einfach zu erlernen ist, da es nur recht wenige Befehle gibt. Darüber hinaus ist die Syntax an die Sprache C angelehnt und daher schon vielen Entwicklerinnen und Entwicklern vertraut. awk kann über Skripte oder direkt über die Kommandozeile benutzt
werden, wobei jeweils das Programm awk bzw. gawk für diese Zwecke verwendet wird. awk ist das eigentliche, auf jedem unixartigen System vorhandene Grundprogramm, besser gesagt, der Interpreter. gawk ist die GNU-Version und auf vielen Linux-Systemen verfügbar. 6.3.1 awk starten awk (wie auch die Implementierungen nawk und gawk) wird ganz
einfach über die Kommandozeile gestartet. Die Befehle zur Verarbeitung des Textes werden entweder in Hochkommata ((ª) + (#)) oder in einer Datei abgelegt und als zweiter bzw. dritter Parameter bei Optionen übergeben. Danach folgt optional eine Datei, in der die zu verarbeitenden Daten enthalten sind, die mittels Pipe übergeben werden können.
user$ user$ user$ user$
awk cat awk cat
'{print $1}' DateiA DateiB … DateiN
Datei | awk '{print $1}'
-f skript.awk Datei
Datei | awk -f skript.awk
Listing 6.11 So starten Sie awk.
6.3.2 Arbeitsweise von awk
Das Programm besteht aus den folgenden Teilen: dem BEGIN-Teil, dem Hauptteil und dem END-Teil. Nach dem Aufruf wird zunächst einmal der eventuell vorhandene BEGIN-Teil des Programmcodes abgearbeitet, der zur Initialisierung verwendet wird. Anschließend wird jede einzelne Zeile des zu verarbeitenden Textes separat verarbeitet, was im Hauptteil geschieht. Der Hauptteil enthält demnach den Code für alle Anweisungen, die mit den Zeilen durchgeführt werden sollen, und wird für jede Zeile komplett neu ausgeführt. Bei großen Dateien ist es daher recht sinnvoll, auf zu rechenaufwendige Anweisungen zu verzichten und einen effizienten Code zu schreiben. Nachdem die Eingabedatei komplett verarbeitet wurde, wird (sofern implementiert) der Code im END-Teil des Skripts ausgeführt. Hier sehen Sie ein Beispiel für den Aufbau eines awk-Skripts: BEGIN
{
print "Monatsabrechnung"
}
{
print $1 "+" $2 "=" $1+$2
}
END
{
print "Geschafft."
}
Listing 6.12 Aufbau eines Skripts mit awk
Kommandotrennung In awk werden einzelne Kommandos, die in derselben Zeile stehen, durch ein Semikolon getrennt: print $1; variable=1.
6.3.3 Reguläre Ausdrücke in awk anwenden
Eine nützliche Fähigkeit von awk besteht darin, als Filter für Muster zu dienen. Damit weist es ähnliche Funktionalitäten wie grep auf, das Sie in Abschnitt 6.2 kennengelernt haben. Und was verwendet man dazu? Richtig! Reguläre Ausdrücke. user$ cat file
Steffen, Friedrichshafen
Tobias, Ettenbeuren
Johannes, Karlsruhe
user$ awk '/u/' file
Tobias, Ettenbeuren
Johannes, Karlsruhe
Listing 6.13 Aufruf von awk mit einem Muster
6.3.4 Einfache Strings
Einfache Strings werden durch die Angabe des Strings selbst gefiltert. Die gesuchten Strings werden in Hochkommata und zwischen zwei Slashes geschrieben: user$ awk '/on/' Zweigstellen
Bonn
London
user$ awk '/S/' Zweigstellen
Salzburg
Stockholm
Listing 6.14 Filtern auf Zeichensalat
6.3.5 Der Punkt-Operator
Der Punkt-Operator steht, wie Sie bereits wissen, für ein beliebiges Zeichen an einem einzigen Platz. Man kann ihn mitten in Strings einbauen, um zum Beispiel sowohl große als auch kleine Buchstaben zu erwischen. Eine ebenfalls praktische Anwendung ist die Namensfindung – es könnte vorkommen, dass der Name einer Person entweder mit »c« oder mit »k« geschrieben wird. user$ awk '/K.ln/' Zweigstellen
Köln
Listing 6.15 Der Punkt-Operator
6.3.6 Der Plus-Operator
Der Plus-Operator + bewirkt hier keine Addition im eigentlichen Sinne; er ist lediglich der langweilige Operator, dessen Bedingung nur erfüllt ist, sofern mindestens einmal das vor ihm geschriebene Zeichen auftritt. user$ awk '/n+/' Zweigstellen
Bonn
München
London
Bern
Köln
Listing 6.16 Mindestens einmal muss das n vorkommen.
6.3.7 Die Zeichenvorgabe
Es ist möglich, eine bestimmte Zeichenvorgabe zu setzen. Eines dieser von Ihnen vorgegebenen Zeichen muss dann an der entsprechenden Stelle im String vorkommen. Die ausgewählten Zeichen werden dabei in eckige Klammern eingebettet: [abc]. Außerdem können einige Abkürzungen wie a-z oder 0-9 verwendet werden. Einzelne Zeichengruppen werden durch Kommata getrennt.
user$ awk '/M?nch[a-z,0-9][nNzkvps]/' Zweigstellen
München
Listing 6.17 Diese Zeichen dürfen alle vorkommen.
6.3.8 Negierte Zeichenvorgabe
Das Gegenteil von der obigen Zeichenvorgabe ist die negierte Zeichenvorgabe. Die dabei angegebene Menge von Zeichen darf an der entsprechenden Stelle nicht vorkommen, damit die Bedingung erfüllt ist. user$ awk '/M?nch[^e][^bn]/' Zweigstellen
user$ awk '/M?nch[^X][^bY]/' Zweigstellen
München
Listing 6.18 Negierte Zeichenvorgabe
Diese Negierung kann nur auf Mengen und nicht direkt auf Einzelzeichen, d.h. ohne eckige Klammerung, angewandt werden. Später werden wir sehen, dass man auf diese Art und Weise den Anfang einer Zeile beschreibt. 6.3.9 Zeilenanfang und -ende
Oftmals sortiert man Datensätze nach dem Anfang bzw. dem Ende einer Zeile – ein gutes Beispiel hierfür wäre die Aussortierung der Logdatei-Einträge des Tages x. Die regulären Ausdrücke stellen uns hierfür zwei Zeichen zur Verfügung: ^ und $, wobei der Zeilenanfang durch ^ angegeben wird und als sogenanntes XOR (Exclusive OR) bezeichnet wird, das in der Digitaltechnik Verwendung findet. Das Dollarzeichen sollte jedem bekannt sein und wird in awk (ungeachtet der Bedeutung in regulären Ausdrücken) für die Kennzeichnung eines Variablenzugriffs verwendet. (Dies muss nicht zwangsläufig so sein, macht aber einen guten Programmierstil aus.)
user$ awk '/^B/' Zweigstellen
Bonn
Bern
user$ awk '/n$/' Zweigstellen
Bonn
München
London
Bern
Köln
Listing 6.19 Filtern nach Zeilenanfang und -ende
6.3.10 awk – etwas detaillierter
Nein, wir möchten an dieser Stelle keine 200 Seiten awk-Know-how vermitteln, sondern uns auf wenige Seiten beschränken, um Ihnen in recht kurzer Form die Grundzüge dieser Sprache zu erklären, was wir in den vorangegangenen Abschnitten bereits ansatzweise getan haben. awk bietet die Möglichkeit, auf einige wichtige Variablen
zuzugreifen, die für die korrekte Ausführung des Programmcodes wichtig erscheinen. Sehen wir uns diese Variablen doch einmal an: $1, $2, …, $N
Diese Variablen geben die Werte der Spalten des Eingabestreams an. $1 ist dabei die erste Spalte, $2 die zweite usw. $0
In dieser Variablen ist die komplette Zeile abgelegt, die sich in der aktuellen Verarbeitung befindet. Damit könnten Sie sich auch ein eigenes cat-Programm programmieren: print $0. ARGC und ARGV
Wie bei jedem Betriebssystem, das über ein CLI (Command Line Interface) verfügt, gibt es Programme (und Skripte), denen Parameter (etwa das zu erstellende Verzeichnis) übergeben werden. In ARGV, einem sogenannten Array (wir werden uns noch
genauer mit Arrays auseinandersetzen), werden diese Argumente gespeichert. ARGC gibt lediglich deren Anzahl an. CONVFMT
Diese Variable repräsentiert das Konvertierungsformat für Zahlen in Strings und ist für uns in dieser Einführung nicht weiter von Bedeutung. Die Variable OFMT gibt übrigens das Ausgabeformat von Zahlen an. ENVIRON
Dieses Array speichert die Umgebungsvariablen, in denen der Aufruf des awk-Codes erfolgt, samt ihren Werten. ERRNO
Tritt ein Fehler auf, speichert diese Variable seinen Wert. Fragt man diesen Wert nun über eine entsprechende Funktion ab, so wird eine – zumindest für den Entwickler bzw. die Entwicklerin – verständliche Fehlermeldung ausgegeben. FIELDWIDTHS
Normalerweise gibt man in awk das Zeichen, das die einzelnen Spalten der zu verarbeitenden Daten voneinander trennt, direkt an. Möchten Sie lieber fixe Spaltengrößen verwenden, so können Sie sie in FIELDWIDTHS angeben. FILENAME
Normalerweise gibt man den Namen der Eingabedatei (also der Datei, in der die zu verarbeitenden Daten enthalten sind) direkt an oder cat-ted sie über eine Pipe an awk. Wenn Sie das nicht tun, wird die Standardeingabe der Shell als Datenquelle verwendet. Ist FILENAME gesetzt, wird jedoch der ihr zugewiesene Dateiname als Datenquelle verwendet. FNR
Die Zeilen der Eingabedatei sind durchnummeriert und FNR
enthält die aktuell verarbeitete Zeilennummer. Dies ist beispielsweise dann hilfreich, wenn es nötig ist, die ersten N Zeilen einer Quelldatei zu verarbeiten, oder wenn man eine Durchnummerierung seiner Datensätze erreichen möchte. FS
FS steht für Field Separator und gibt das Zeichen an, das zur Trennung der Spalten in der Quelldatei verwendet wird. FS wird direkt beim Aufruf von awk übergeben. In der Datei /etc/passwd werden die einzelnen Spalten durch Doppelpunkte getrennt. Um diese Datei spaltenweise auszulesen, setzen wir den Field Separator auf das Doppelpunkt-Zeichen, was mit dem Parameter -F erledigt wird: user$ awk -F: /etc/passwd
Benutzer/UID: Benutzer/UID: Benutzer/UID: Benutzer/UID: Benutzer/UID: Benutzer/UID: …
'{print "Benutzer/UID: "$1"/"$3}' \
root/0
bin/1
daemon/2
adm/3
lp/4
sync/5
Listing 6.20 Field Separator
Die print-Funktion wird, wie zu sehen ist, für die Ausgabe von Text und Variablenwerten verwendet, doch dazu später mehr. NF
Diese Variable gibt die Anzahl der Felder in der Quelldatei an; NF steht für Number of Fields. NR
Die Anzahl der Datensätze in der Quelldatei wird in NR abgelegt. (Diese Ausdrucksweise ist eigentlich grundlegend falsch, da Variablen eigentlich keine Werte »enthalten«. Die Variablen stehen nur für einen Speicherbereich, in dem die Werte abgelegt
werden, bzw. verweisen im Fall eines Daten-Pointers auf diesen Bereich.) NR steht für Number of Records, also Anzahl der Datensätze. OFS
Dies ist das Gegenstück zu FS, der Output Field Separator, also das Trennungszeichen der ausgegebenen Daten. ORS
Dies ist das Separierungszeichen für einzelne Datensätze bei der Ausgabe. ORS steht für Output Record Separator. RS
Natürlich gibt es auch zur obigen Output-Version ein Gegenstück: den Separator für Datensätze in der Eingabe. RT
Der Record Terminator legt das Zeichen fest, das das Ende der Datensätze angibt. 6.3.11 Zusätzliche Parameter beim Aufruf
Zu diesem Zeitpunkt kennen Sie nur die grundlegende Form eines Aufrufs von awk und den Parameter zur Trennung der einzelnen Spalten (den oben erwähnten Field Separator). Allerdings sind noch einige weitere wichtige Parameter vorhanden. Mit -f geben Sie den Namen eines Skripts an, das den Programmcode enthält. Vor dem Start können Sie übrigens auch Variablen erzeugen und diese mit neuen Werten beglücken, was mit awk -v Variable=Wert geschieht. --copyright gibt die Kurzform der Lizenz aus, mit der gawk
ausgeliefert wurde. Wer zusätzlich die verwendete awk-Version sehen möchte, sollte --version nutzen. Der Kompatibilitätsmodus
mit dem »Ur-awk« wird mit --compat und --traditional aktiviert und eine Anwendungshilfe wird mit --usage und --help ausgegeben. 6.3.12 awk und Variablen
Auch in awk gibt es Variablen. Und das Schöne daran ist, dass ihre Handhabung einfach ist. Werte werden direkt über den Zuweisungsoperator (das Gleichheitszeichen) an die Variable übergeben. Inkrement- und Dekrement-Operatoren der Sprache C sind auch hier nutzbar, sodass der Wert via variable++ um 1 erhöht und mit variable-- um denselben Wert gesenkt werden kann. Schreiben wir einmal ein kleines Testprogramm, das die Zeilen der Eingabedatei ähnlich wie das wc-Programm zählt. Dazu nehmen wir eine Variable Linecount, die die Zeilen zählt, setzen sie am Anfang (also im BEGIN-Bereich) auf den Wert 0 und zählen sie bei jedem Durchlauf des Hauptprogramms eine Zeile weiter: BEGIN { # Im Initialisierungsteil weisen wir der Variablen den Wert 0 zu.
Linecount=0; # Verwendung: Zeilen zählen.
}
{ # Die Hauptschleife wird für jede Zeile erneut durchlaufen.
Linecount++;
}
END {
print "Wert: " Linecount;
}
Listing 6.21 Der Zeilenzähler in awk
Ein Vergleich mit unserem Skript und dem wc-Kommando zeigt uns, dass alles funktioniert: Die Ergebnisse sind äquivalent. user$ awk -f script.awk file
Wert: 367
user$ wc -l file
367 file
Listing 6.22 Der Test
Kommentare werden in awk mit einer Raute (#) eingeleitet und gelten jeweils für die aktuelle Zeile ab dem Punkt, an dem sie gesetzt wurden. 6.3.13 Rechenoperationen
Selbstverständlich kann man auch Berechnungen in awk durchführen, und zwar bedeutend komfortabler als in der bloßen Shellskript-Programmierung. Es gibt verschiedene Standardoperatoren, wie den Additions- (+) und den Subtraktionsoperator (-) und die Operatoren für Multiplikation (*) und Division (/). Aber auch Kombinationen mit dem Zuweisungsoperator sind möglich. So können Sie – wie in der Programmiersprache C – aus Variable = Variable + 2; ein kurzes Variable += 2; oder aus Variable = Variable / 5; ein Variable /= 5; machen. BEGIN {
Var = 1000;
Var = 999; print Var;
Var = Var * 2; print Var;
Var += 10; print Var;
Var *= 2; print Var;
}
Listing 6.23 Rechenbeispiel für awk user$ awk -f script.awk
999
1998
2008
4016
^D
Listing 6.24 Anwendung des Rechenbeispiels
Neben diesen Rechenoperationen können einige weitere (z. B. die Potenzierung) durchgeführt werden: var = 3 ^ 3 weist var den Wert
»drei hoch drei«, also 27, zu. Modulo-Operationen sind über den gleichnamigen Operator (%) ebenfalls möglich: var = 5 % 4. Prä- und Postinkrementierung Die Inkrementierung und Dekrementierung von Variablen kennen Sie bereits; was wir Ihnen jedoch noch verschwiegen haben, ist der Unterschied zwischen der sogenannten Prä- und PostInkrementierung bzw. -dekrementierung.
Eine Präverarbeitung hat die Syntax ++variable bzw. --variable, eine Postverarbeitung variable++ bzw. variable--. Der Unterschied zwischen diesen beiden Varianten ist, dass bei der Präversion eine Verarbeitung in einer Anweisung noch vor der eigentlichen Anweisung durchgeführt wird. Bei der Postvariante geschieht dies erst, nachdem solch eine Anweisung beendet wurde. Aber am besten lernt man ja bekanntlich an Beispielen. user$ cat test.awk
BEGIN {
test = 1;
print test++;
print test;
print ++test;
}
user$ awk -f test.awk
1
2
3
Listing 6.25 Post- und Präinkrementierung und -dekrementierung
6.3.14 Bedingte Anweisungen in awk
Es stehen natürlich auch in awk einige Möglichkeiten zur Verfügung, bedingte Anweisungen mittels relationaler Ausdrücke zu
formulieren. Im Rahmen dieser kleinen awk-Einführung behandeln wir die if-Anweisung und die for-Schleife. if
Mithilfe dieser Anweisungen werden die Werte von Variablen getestet. Dafür werden sogenannte Bedingungen erstellt, die entweder erfüllt werden oder eben nicht. Wird eine Bedingung also (nicht) erfüllt, wird entweder die nachstehende Anweisung oder eine ganze Gruppe von Anweisungen ausgeführt, wobei in diesem Fall ein Bereich für die Anweisungen innerhalb von geschweiften Klammern geschaffen werden muss. Zum besseren Verständnis folgt nun ein Beispiel: Der Wert der Variablen var wird mit der if-Anweisung auf verschiedene Bedingungen hin geprüft. Ist die erste Bedingung nicht erfüllt, wird mit else if eine weitere Verzweigung dieser Anweisung eröffnet. Durch das else wird also nur eine Prüfung vorgenommen, wenn die vorherige Bedingung nicht erfüllt ist. Wenn auch die zweite Bedingung nicht erfüllt ist, var also den Wert 100 hat, tritt die letzte else-Anweisung in Kraft. Eine elseAnweisung ohne zusätzliches if wird immer dann ausgeführt, wenn alle vorherigen Bedingungen unerfüllt sind. BEGIN {
var=100; # Wert anpassbar für Ihre Tests
if(var > 100)
print "var ist > 100"
else if(var < 100)
print "var ist < 100"
else {
print "var ist 100"
print "Hier haben wir eine Gruppierung von zwei Anweisungen"
}
if(var =1), wenn die binären Werte von a und b, verknüpft mit einem logischen Und, mindestens 1 ergeben.
a || b
Ein logisches Oder. Beachten Sie, dass es in awk kein einfaches Oder-Zeichen (|) für diese Zwecke gibt. Es würde als Pipe-Zeichen behandelt werden.
!a
Diese Bedingung ist erfüllt, wenn a nicht erfüllt ist.
muss ein Array und a ein Element dieses Arrays sein.
Bedingung Beschreibung Wenn die Bedingung x erfüllt ist, wird a als Bedingungswert bezeichnet, andernfalls b.
x?a:b
Tabelle 6.2 Grundlegende und logische Bedingungen
Geschweifte Klammern – nicht immer nötig! Wie Ihnen vielleicht schon aufgefallen ist, folgen auf if nicht immer geschweifte Klammern. Dies liegt daran, dass die Klammern entfallen können, wenn nur eine Anweisung durch if ausgeführt werden soll. Klammern sind allerdings nie verkehrt und im Zweifelsfall richtig! Dies gilt analog auch für for und while, die wir als Nächstes betrachten.
for und while
Kommen wir nun zu zwei Schleifentypen, die uns in ähnlicher Form bereits von der Shellprogrammierung her bekannt sind und daher nicht nochmals in ihrer Funktionalität, dafür aber in ihrer Syntax erklärt werden. In der for-Schleife gibt es in der Regel drei Parameter (eine Ausnahme stellt die in-Bedingung für Arrays dar). Das erste Argument legt den Wert einer Variablen fest, wird also zur Initialisierung verwendet. Der mittlere Teil enthält die Bedingung in der gleichen Form wie die if-Anweisung. Der letzte Parameter gibt die Anweisung an, die bei jedem Schleifendurchlauf ausgeführt werden soll. Die einzelnen »Teile« werden dabei durch ein Semikolon voneinander getrennt: Syntax:
for( Init; Bedingung; Anweisung ) {
Anweisung1;
Anweisung2;
…;
AnweisungN;
}
Beispiel:
for(var=1; var cd $1
> if [ "$?" = "1" ]; then return 1; fi
> ls
> if [ "$?" = "1" ]; then return 1; fi
> return 0
>
$ dols /
...
$ echo $?
0
$ dols /root
cd: /root: Permission denied
$ echo $?
1
Listing 8.32 dols()
8.7 Bedingte Anweisungen Bedingte Anweisungen sind ein elementarer Grundstein der Programmierung. Mit diesen Bedingungen können Werte abgefragt werden und es kann entsprechend darauf reagiert werden. Ein einfaches Beispiel dafür ist folgendes: Die Benutzerinnen und Benutzer sollen in einem Programm angeben, ob sie einen Ausdruck ihres Dokuments haben möchten. Das Programm muss nun die Benutzereingaben prüfen und sie zum Beispiel in einer Variablen speichern. Enthält die Variable den Wert »Ja«, erfolgt der Ausdruck, andernfalls wird ganz einfach davon abgesehen. Zur Formulierung von bedingten Anweisungen verwendet man in der Regel die if-Anweisung (es gibt weitere Möglichkeiten, die wir weiter unten behandeln möchten). Sie hat folgende Syntax: if [ BedingungA ] && [ BedingungB ]
then
Anweisungen
elif [ BedingungA ]
then
Anweisungen
else
Anweisungen
fi # nicht mit 'if' verwechseln!
Listing 8.33 Die if-Anweisung
Die if-Anweisung in Zeile 1 legt die Grundbedingung fest. Ist diese erfüllt, werden die Anweisungen ausgeführt, die hinter dem thenSchlüsselwort stehen. Ist die Bedingung jedoch nicht erfüllt, wird geprüft, ob die elif-Bedingung – sofern vorhanden – erfüllt ist, und deren Anweisungen werden ausgeführt. Ist auch diese nicht erfüllt, wird zur nächsten elif-Anweisung gesprungen, bis es keine weitere
mehr gibt. Existiert zudem eine else-Anweisung, wird diese nur ausgeführt, falls alle anderen Bedingungen nicht zutrafen. Bedingungen können auch verknüpft werden: Ein && beispielsweise bedeutet, dass sowohl Bedingung 1 als auch Bedingung 2 erfüllt sein müssen, damit die Anweisungen ausgeführt werden. Des Weiteren gibt es die Negierung (!) – die Bedingung ist also erfüllt, wenn ihr Inhalt nicht erfüllt wurde. Außerdem können Sie das »Oder« (||) verwenden, bei dem nur eine der verknüpften Bedingungen erfüllt sein muss. Es gibt eine ganze Menge Möglichkeiten, Bedingungen zu formulieren, die um einiges über String-Vergleiche hinausgehen. So können Sie prüfen, ob eine Datei existiert oder eine bestimmte Datei ein Verzeichnis ist. Da dieser Abschnitt lediglich zur Einführung in die Shellskript-Programmierung dient, werden wir Sie nicht auch noch damit quälen. Das folgende Skript prüft, ob ein Wert und, wenn ja, welcher Wert als erster Parameter übergeben wurde, und führt eine bedingte Anweisung aus. Der Parameter -z in einer Bedingung prüft, ob das angegebene Element leer ist. Beachten Sie bitte, dass Variablen, deren Inhalt in Form eines Strings verglichen werden soll, in Anführungszeichen geschrieben werden müssen. #!/bin/sh
if [ -z $1 ]
then
echo "Erforderlicher Parameter fehlt!"
elif [ "$1" = "backup" ]
then
echo "Erstelle Backup."
…
elif [ "$1" = "restore" ]
then
echo "Spiele Sicherungsdaten wieder ein."
…
else
echo "Unbekannter Parameter."
fi
Listing 8.34 Bedingte Anweisungen
8.7.1 Vergleichen von Zahlen
Oftmals hat man es mit Zahlen zu tun. Diese werden allerdings auf spezielle Arten verglichen. Tabelle 8.1 listet die Vergleichsmöglichkeiten auf. Vergleich
Beschreibung
$a -eq $b
Die verglichenen Werte sind gleich (equal).
$a -ne $b
Die Werte sind ungleich (not equal).
$a -lt $b
$a
ist kleiner als $b.
$a -le $b
$a
ist kleiner als/gleich $b.
$a -gt $b
$a
ist größer als $b.
$a -ge $b
$a
ist größer als/gleich $b.
Tabelle 8.1 Bedingungen für Zahlen
Würden wir das obige Beispiel anhand von Zahlenvergleichen realisieren, wäre zum Beispiel folgende Bedingung denkbar: elif [ $1 -eq 1 ]
then
echo "Erstelle Backup."
fi
Listing 8.35 Ein typischer Zahlenvergleich
8.7.2 Returncodes für Bedingungen nutzen
Mithilfe von Returncodes ist es möglich, den Erfolg eines ausgeführten Programms zu überprüfen. Führt ein Shellskript beispielsweise ein Backup vom Verzeichnis /export/home/nobody durch und existiert das Verzeichnis nicht, sollte das Programm einen entsprechenden Fehlercode zurückgeben. In der Regel steht eine »0« für eine erfolgreiche Programmausführung, eine »1« für eine fehlerhafte. Fehlercodes werden immer in der Shellvariablen $? hinterlegt. user$ ls Datei
Datei
user$ echo $?
0
user$ ls DateiABCD
ls: DateiABCD: No such file or directory
user$ echo $?
1
user$ ls /root 2>/dev/null 1>output
user$ if [ $? -eq 1 ]; then
then> echo "Programmausführung fehlerhaft, breche Skript ab."
then> fi
Programmausführung fehlerhaft, breche Skript ab.
Listing 8.36 Prüfen des Rückgabewertes
8.7.3 Case-Bedingungen
Beschäftigen wir uns noch mit einer weiteren Möglichkeit, bedingte Anweisungen zu programmieren, nämlich mit der case-Anweisung. Der Nachteil dieser Anweisung ist, dass sie nur einen Wert verarbeitet. Das ist aber auch gleichzeitig ihr Vorteil gegenüber der Anweisung if. Möchten Sie nämlich dort eine Variable auf mehrere Werte überprüfen, so müssen Sie jeweils elsif-Bedingungen formulieren. case bietet dafür eine kürzere und bessere Schreibweise. Die Syntax von case ist folgendermaßen aufgebaut:
case "$VARIABLE" in
WertA)
Anweisungen für A
;;
WertB)
Anweisungen für B
;;
WertC|WertD|WertE)
Gemeinsame Anweisungen für die Werte C, D und E
;;
*)
Anweisungen
;;
esac
Listing 8.37 case-Syntax
Die Werte WertA und WertB werden für die Variable $VARIABLE überprüft. Beinhaltet die $VARIABLE einen dieser Werte, werden die entsprechenden Anweisungen ausgeführt. Die beiden Semikola beenden den Anweisungsbereich. Der letzte Werte-Test (*) ist sozusagen das else der if-Anweisung, das immer dann greift, wenn obige Werte nicht mit dem Variablenwert übereinstimmten. Die case-Anweisung wird in der Regel für die Erstellung von Runlevel-Skripten (besonders unter Solaris) verwendet. Man übergibt dem Skript dabei ein Argument $1, das Werte wie start oder stop enthält, um einen Dienst automatisch beim Wechseln in ein bestimmtes Target (oder durch manuellen Start des Skripts) zu starten bzw. zu beenden. Skripte, die Kommandos im Hintergrund ausführen, können Sie auf diese Art und Weise wundervoll stoppen und starten. Das folgende Skript wendet dieses Verfahren an, um den syslog-Daemon zu starten bzw. anzuhalten. #!/bin/bash
case "$1" in
start)
echo "starte syslogd"
/usr/sbin/syslogd
;;
hup|restart)
echo "rekonfiguriere syslogd"
pkill -HUP syslogd
;;
stop)
echo "stoppe syslogd"
pkill syslogd
;;
*)
# Fehler abfangen
echo "Kommando unbekannt!"
;;
esac
Listing 8.38 Start/Stopp-Skript
8.8 Schleifen Sie verfügen nun über das notwendige Grundwissen, um Schleifen zu verstehen. Einer Schleife wird eine Bedingung übergeben. Ist diese erfüllt, werden ihre Anweisungen so lange ausgeführt, bis diese Bedingung nicht mehr erfüllt ist. Eine Bedingung ist sowohl in einer Schleife als auch in einer caseVerzweigung oder if-Anweisung immer wahr, wenn sie den Wert »1« ergibt. Probieren Sie einmal folgende Anweisung aus: user$ if [ 1 ]
then
echo wahr
fi
wahr // diese Zeile ist bereits die Ausgabe der if-Anweisung
user$
Listing 8.39 Eine immer wahre if-Anweisung
Bei einer Schleife würde der Befehl echo wahr so lange ausgeführt, bis die Bedingung der Schleife nicht mehr erfüllt wäre. 8.8.1 Die while-Schleife
Die Syntax der while-Schleife ist im folgenden Listing abgebildet. Die Anweisungen werden mit dem do-Schlüsselwort eingeleitet und mit done beendet. while [ Bedingung ]
do
Anweisung A
Anweisung B
…
done
Listing 8.40 Syntax der while-Schleife
Doch in der Praxis lernt man bekanntlich am besten. Im Folgenden wird eine Endlosschleife erstellt, die alle 30 Minuten die verfügbare Kapazität der Datenträger überprüft. Sinkt die verbliebene freie Kapazität auf unter 5 % (der df-Befehl gibt in diesem Fall einen Wert von über 95 % für die genutzte Kapazität an), wird eine Meldung ausgegeben. Das Prozentzeichen wird mit dem sed-Programm herausgefiltert. awk und sed haben wir in Kapitel 6 besprochen. #!/bin/bash
while [ 1 ]
do
df -h | grep -v '[Uu]se' | sed s/\%// | \
awk '{
if($5>95)
print $1 " is almost full. ("$5"%)"
}'
sleep 1800
done
Listing 8.41 Endlosschleife
8.8.2 Die for-Schleife
Die for-Schleife bietet gegenüber der while-Schleife einen Vorteil: Sie kann eine Liste von Werten durcharbeiten. Das heißt, die Schleife wird für jeden angegebenen Wert einmal durchlaufen. Dabei wird einer Variablen für jeden Schleifendurchlauf ein weiterer zuvor angegebener Wert zugewiesen, mit dem Sie innerhalb des Anweisungsblocks arbeiten können. Dies ist beispielsweise sehr nützlich, wenn eine Liste von Dateien verarbeitet werden soll. for VAR in Werte
do
AnweisungA
AnweisungB
…
done
Listing 8.42 Syntax der for-Schleife
Stellen Sie sich einmal vor, dass alle Benutzerverzeichnisse archiviert werden sollen. Die Benutzerverzeichnisse der Benutzer, die mit dem Buchstaben »A« beginnen, sind im Verzeichnis /home/a abgelegt, die derjenigen, die mit »B« beginnen, sind in /home/b/ untergebracht usw. Als Archivmedien stehen der Veranschaulichung halber – Achtung, eher praxisfern – USB-Sticks mit einer Kapazität von 2 GByte zur Verfügung und die BenutzerQuotas beschränken sich pro Buchstabenverzeichnis auf genau diese Kapazität. Mit der for-Schleife kann nun ein simples Skript entwickelt werden, das jeweils ein Verzeichnis auf einem Stick sichert und Sie danach auffordert, den nächsten Stick einzustecken. #!/bin/bash
USBSTICK=/dev/sde1
TARGET=/mnt/stick
for DIR in /home/*; do
# Ist es ein Verzeichnis?
if [ -d $DIR ]; then
mount -t vfat $USBSTICK $TARGET
if [ $? -eq 0 ]
then
cp -r $DIR $TARGET
umount $TARGET
echo "Bitte nächstes Medium einlegen."
else
echo "Mountvorgang schlug fehl!"
exit
fi
fi
done
Listing 8.43 Ein einfaches Backup
Im Optimalfall sollte noch die verfügbare Kapazität auf dem Medium überprüft und mit dem tar-Kommando eine Komprimierung erzielt werden. Doch hätten wir dies gezeigt, wäre
wohl das eigentliche Ziel, nämlich die Demonstration der forSchleife, untergegangen. 8.8.3 seq – Schleifen mit Aufzählungen
Manchmal kommt es vor, dass man eine Schleife "uber eine Ziffernfolge durchlaufen möchte oder aus irgendeinem anderen Grund eine Ziffernfolge benötigt. Dafür wurde das Programm seq geschrieben, das hier nur kurz angesprochen werden soll. Eine Parameterliste für seq finden Sie in der Kommandoreferenz. Soll seq beispielsweise alle Zahlen von 1 bis 10 auflisten, dann ist dies ganz einfach. Übergeben Sie den Start- und Endwert: $ seq 1 10
1
2
3
4
5
6
7
8
9
10
Listing 8.44 seq in Aktion
Möchte man seq nun in eine Schleife packen, so nutzt man am besten die Kommandosubstitution. Will man etwa einen bestimmten Satz Dateien löschen, dann wäre Folgendes eine Möglichkeit: $ for i in `seq 1 10`; do
rm "file$i"
done
Listing 8.45 seq in einer for-Schleife
8.8.4 until
Zu den bereits bekannten Schleifen kommt nun noch die untilSchleife hinzu. Diese neue Schleife kann als Negation der whileSchleife verstanden werden. Der Anweisungsblock der untilSchleife wird so lange ausgeführt, wie die Bedingung nicht erfüllt ist. Man kann praktisch die until-Schleife mit einer while !-Schleife gleichsetzen: while [ ! 1 ]; do echo Test; done
until [ 1 ]; do echo Test; done
# oder noch kürzer:
until :; do echo Test; done
Listing 8.46 until und while !
8.8.5 break – Schleifen abbrechen
Um eine Schleife mitten im Durchlauf an einer beliebigen Stelle zu verlassen, muss man dort nur das break-Schlüsselwort setzen. Dies funktioniert sowohl mit while- als auch mit for- und untilSchleifen. Da die select-Anweisung wie eine Schleife fungiert, kann man sie durch (Strg) + (D) abbrechen, oder man integriert eine Abbruchfunktion, in der man break verwendet. Letzteres ließe sich folgendermaßen umsetzen: #!/bin/bash
echo "Was haben Sie für ein Haustier?"
select HAUSTIER in Hund Katze Beenden
do
if [ "$HAUSTIER" = "Beenden" ]; then break; fi
echo "Sie haben also ein/eine(n) $HAUSTIER"
echo "Kann Ihr Haustier auch in Common-Lisp programmieren?"
done
Listing 8.47 select-Beispiel
8.9 Menüs bilden mit select In der Regel wird eine Benutzereingabe mit mehreren Möglichkeiten folgendermaßen getätigt: Auf dem Bildschirm werden die Möglichkeiten ausgegeben und der Benutzer bzw. die Benutzerin gibt die gewünschte Auswahl ein. Dabei wird beispielsweise ein String »backup« eingegeben, um das Dateisystem zu archivieren. Doch was passiert, wenn »Backup«, »Bäckup« oder »Bkup« eingegeben wird? Das Skript wird eine Fehlermeldung liefern und sich beenden bzw. eine unvorhergesehene Aktion durchführen und eventuell Schaden am System anrichten. Mithilfe der select-Anweisung können Menüs besser aufgebaut werden. Den Auswahlmöglichkeiten sind dabei Zahlen zugeordnet, die die Benutzerinnen und Benutzer auswählen können. Dabei kann von der Anwenderseite her wesentlich weniger schieflaufen. Zudem wird bei unbekannter Eingabe einfach eine erneute Eingabeaufforderung ausgegeben, sodass kein zufälliger oder falscher Code ausgeführt wird. Die Syntax ist der for-Schleife ähnlich. In der Eingabevariablen wird der Wert der gewählten Auswahl hinterlegt. select AUSWAHL in MöglichkeitA MöglichkeitB …
do
Anweisung(en)
done
Listing 8.48 select-Syntax
Natürlich gibt es wie immer ein Beispiel, das Ihnen die eigentliche Funktionsweise etwas offener darlegen sollte, als es die bloße Syntax könnte. Das folgende Skript bietet die Auswahl einer Logdatei im Verzeichnis /var/log an. Die ausgewählte Datei wird mit
dem tail-Programm überwacht. Das Skript wird durch die Tastenkombination (Strg) + (C) bzw. mithilfe der exit-Funktion beendet. #!/bin/bash
cd /var/log
select AUSWAHL in authlog daemon syslog maillog \
ftpd named.log exit
do
if [ "$AUSWAHL" == "exit" ]; then
exit
fi
tail -f $AUSWAHL
done
Listing 8.49 Logwatcher mit select und tail
Bei der Eingabeaufforderung für die Menüauswahl wird der Prompt $PS3 angezeigt. Dieser ist, wie bereits weiter oben beschrieben wurde, eine Variable. Wird diese Variable mit einem anderen Wert versehen, können Sie Ihre eigene Aufforderungsmeldung definieren: export PS3='input>'.
8.10 Temporäre Dateien Manchmal benötigt man für ein Skript eine oder mehrere temporäre Datei(en). Zur Erzeugung einer solchen Datei gibt es verschiedene Verfahren. Zunächst einmal geht es um den Ort, an dem eine solche Datei erstellt werden soll. Es könnte das aktuelle Arbeitsverzeichnis, das Verzeichnis /tmp oder auch jedes andere Verzeichnis sein. Es empfiehlt sich jedoch, für temporäre Dateien auch das spezielle Verzeichnis für temporäre Dateien (also /tmp) zu verwenden. Es ist nicht sonderlich schlau, eine Datei wie /tmp/a und /tmp/b als temporäre Datei zu verwenden. Zum einen sind diese Dateien vom Namen her recht einfallslos, was zur Folge haben kann, dass auch andere Programme oder Skripte denselben Dateinamen verwenden. [ 27 ] Zum anderen ergibt sich ein Sicherheitsproblem: Ein Angreifer könnte eine Race Condition erschaffen und ausnutzen. Sagen wir, dass Ihr Shellskript auf die folgende Weise in die Datei /tmp/a schreibt: echo $1 > /tmp/a
Listing 8.50 Sehr verwundbarer Code
Besitzt der Angreifer auch nur einen Hauch von Hacking-Know-how, so sieht er sofort, dass dieser Code, wenn er vom Superuser ausgeführt wird, praktisch alle Türen auf einem Linux-System öffnet. Denn erzeugt der Angreifer nun einfach vorher einen Link von /tmp/a auf /etc/passwd, so würde der Superuser beim Aufruf des Skripts (vielleicht ohne es zu merken) die Passwortdatei überschreiben.
Wäre der Angreifer zudem in der Lage, den übergebenen Parameter $1 zu manipulieren, könnte er den neuen Inhalt der Passwortdatei selbst gestalten. Nun könnte man meinen, dass man als einfachsten Weg, hier Abhilfe zu schaffen, an den Dateinamen die Prozess-ID des Shellskripts anhängen und damit den Skriptcode folgendermaßen modifizieren könne: echo $1 > /tmp/a.$$
Listing 8.51 Immer noch verwundbarer Code
Dies ist schon etwas besser, da Prozess-IDs in Linux in aller Regel nicht mehr inkrementell, sondern randomisiert vergeben werden. Eine wesentlich bessere Lösung stellt das Programm mktemp dar, das beim Aufruf eine Datei im Verzeichnis /tmp erstellt und ihren Namen ausgibt. Diese Datei hat eine recht lange und vor allen Dingen zufällige Endung, die sich nur sehr schwer voraussagen lässt. Das obige Skript ließe sich also folgendermaßen wesentlich sicherer implementieren: echo $1 > `mktemp`
Listing 8.52 Bereits relativ sicherer Code
Das Problem besteht nun darin, dass Sie keine Ahnung haben, wie die entsprechende Datei heißt. Daher muss der Dateiname gemerkt (und am besten auch gleich nach der Verwendung wieder überschrieben) werden. TMPFILE=`mktemp`
echo $1 > $TMPFILE
...
...
# Den Dateinamen in $TMPFILE überschreiben
rm -f $TMPFILE
TMPFILE="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Listing 8.53 Ein noch besser abgesicherter Code
Jetzt müsste es dem Angreifer schon gelingen, den Dateinamen vorherzusagen, oder er müsste das Programm mktemp hacken und durch eine eigene Version ersetzen. Dies wiederum ließe sich mit Intrusion-Detection-Systemen für Dateisysteme herausfinden. Selbstverständlich ist der Inhalt der temporären Datei noch nicht unwiderruflich gelöscht, aber auch dies soll nur erwähnt werden, weil eine Lösung des Problems das eigentliche Thema (Shellskriptprogrammierung) übersteigt.
8.11 Syslog-Meldungen via Shell Manche Shellskripte müssen in der Lage sein, den systemweiten Syslog-Dienst zu benutzen. Syslog protokolliert Geschehnisse in den Dateien im Verzeichnis /var/log, etwa in /var/log/messages. Um eine Nachricht an diesen Dienst zu schicken, kann in der Shell das Programm logger verwendet werden. $ logger 'Hallo, Welt!'
$ tail -1 /var/log/messages
Jun 8 16:29:25 koschka swendzel: Hallo, Welt!
Listing 8.54 Eine Logmeldung via logger schicken
8.12 Pausen in Shellskripte einbauen Pausen können sekundengenau in Shellskripte eingebaut werden, indem sleep aufgerufen wird. Dem Programm übergeben Sie dazu die Anzahl der Sekunden, die es zur Ausführung benötigen – also den Ablauf eines Skripts verzögern – soll. $ sleep 10
Listing 8.55 Anwendung von sleep: 10 Sekunden warten
Die meisten sleep-Implementierungen unterstützten die Angabe von Minuten-, Stunden- und Tageswerten über die Wert-Endungen m (minute), h (hour), d (day). Im Normalfall handelt es sich um Sekundenangaben (s), doch ist das Anhängen von s (wie im obigen Beispiel gezeigt) optional. # sleep 20m ; halt -p
Listing 8.56 In 20 Minuten (20m) den Rechner herunterfahren
8.13 Startskripte Da wir uns in diesem Buch primär an das Beispiel der bash halten möchten, beschreiben wir an dieser Stelle auch das Startskriptsystem dieser Shell. In den Startskripten werden globale und benutzerspezifische Initialisierungen vorgenommen. So werden Funktionen definiert, wird der eine oder andere Alias eingerichtet oder werden Variablen wie der Programmsuchpfad $PATH gesetzt. Ist die bash als Login-Shell eingerichtet, so werden zunächst die Dateien /etc/profile und – sofern vorhanden – .bash_profile im Heimatverzeichnis des Benutzers bzw. der Benutzerin ausgeführt. Anschließend werden die ebenfalls im Heimatverzeichnis liegenden Dateien .bash_login und .profile ausgeführt. Die Datei /etc/profile enthält globale Einstellungen. Dies ist praktisch, da bei jedem Login eines Benutzers eine vom Administrator vorgegebene Einstellung übernommen werden kann. So könnte zum Beispiel ein bestimmtes Kommando oder ein für alle Benutzerinnen und Benutzer verwendbarer Alias eingerichtet werden. Die anderen Dateien können vom Benutzer selbst eingerichtet und dazu verwendet werden, persönliche Einstellungen vorzunehmen. Nachdem man sich aus der bash ausgeloggt hat, wird die Datei .bash_logout ausgeführt. In diese Datei kann man nützliche Funktionen, etwa zum Löschen temporärer Daten, einbauen. Wird eine interaktive Nicht-Login-Shell gestartet, liest die bash die Datei .bashrc im Heimatverzeichnis des Benutzers bzw. der
Benutzerin ein und führt sie aus. Hier ist eine minimale .bashrc: # Setzen von einigen Variablen
export NULL=/dev/null
export MAINLOG=/var/log/syslog
export TERM=xterm-color
export LC_ALL=de_DE
export EDITOR="vi"
…
# Alias-Definitionen
alias ls="/bin/ls -aF"
alias ll="ls -alhoF"
alias cl="cd ..;ls"
alias cll="cd ..;ll"
…
# Willkommenstext fuer jede neue Shell
echo
echo "Welcome on `hostname`, $USER!"
echo
printf "%79s\n" "`uname -a`"
printf "%79s\n" "`uptime`"
…
# Der Willkommensspruch
/usr/games/fortune
Listing 8.57 Beispiel für eine .bashrc-Datei
8.14 Das Auge isst mit: der Schreibstil Im Laufe der Zeit werden Ihre Shellskripte etwas größer und komplexer ausfallen. Aus viel Code schlau zu werden, ist nicht schwer, aus unübersichtlichem Code schlau zu werden, umso mehr. Daher empfiehlt es sich, wie in jeder Programmiersprache auch in den Shellskripten bestimmte Formen einzuhalten. Dies wird Ihnen selbst und natürlich all denen, an die Sie Ihren Code weitergeben möchten, das Lesen erleichtern. Dazu gehört zum Beispiel, dass nicht alle Kommandos aneinandergereiht, sondern in Zeilen separiert werden. Doch was noch viel wichtiger ist: Die Anweisungsblöcke bedingter Anweisungen und Schleifen sollten jeweils übereinanderstehen. Betrachten Sie einmal folgendes Beispiel: for NAME in /Haustiere/*
do mail -s "$NAME" < text;
if [ "$NAME" == "Felix" ]
then
echo "Felix gefunden."; fi
done
Listing 8.58 Unübersichtlicher Code
Die Erfahrung lehrt, dass ein Großteil der Anwenderinnen und Anwender tatsächlich solche Skriptcodes schreibt. Eine bessere Lösung wäre doch, die Kommandos zu separieren und die Schleifen hierarchisch anzuordnen: for NAME in /Haustiere/*
do
# Dieser Bereich ist der for-Schleife
# untergeordnet.
mail -s "$NAME" < text;
if [ "$NAME" == "Felix" ]
then
# Der if-Anweisung untergeordnet.
echo "Felix gefunden."
fi
done
Listing 8.59 Übersichtlicher Code
Kommentare gehören zum guten Programmierstil und ermöglichen Ihnen und allen anderen Menschen, die Ihren Code lesen möchten, einen leichteren Zugang zu diesem.
8.15 Ein paar Tipps zum Schluss Bevor wir Ihnen im letzten Abschnitt noch ein paar weitere Fähigkeiten der Shell vorstellen werden, geben wir Ihnen noch ein paar Spezialbefehle mit auf den Weg. Das zuletzt ausgeführte Kommando kann in der bash durch die Eingabe von zwei Ausrufezeichen erneut gestartet werden: user$ ls
…
user$ !!
…
user$ echo !!
echo ls
ls
Listing 8.60 Programme aus der History
Der zuletzt angegebene Parameter für ein Kommando wird durch !$ angesprochen: ls buch.ps; ls !$. Die aktuelle Prozess-ID liefert die bash über die Variable $$.
8.16 Weitere Fähigkeiten der Shell Von Aufgaben in der Administration einmal abgesehen, ist es relativ problemlos möglich, Skripte für Webserver in der Shell zu realisieren. Shellskripte können im Prinzip für fast alle Aufgaben genutzt werden. Die Nachteile sind: Sie können keine Systemfunktionen (Syscalls) direkt ausführen und die Shell benötigt einen Interpreter. Wenn es um Millisekunden geht, ist die Shell alles andere als geeignet, da ihre Performance mäßig ist. Viele Aufgaben können nur durch externe Programme gelöst werden, die eventuell Ihren Anforderungen nicht perfekt entsprechen. Wenn Sie sich nicht mit dem Thema der sicheren Webprogrammierung befasst haben, könnte es sein, dass Sie Sicherheitslücken in Ihre Skripte einbauen. Sofern Sie etwas umfangreichere Applikationen entwickeln möchten, sollten Sie auf eine Sprache wie C, C++, Java oder Python zurückgreifen.
8.17 Zusammenfassung In diesem Kapitel haben wir die Shellskript-Programmierung besprochen. Sie sind nun in der Lage, komplexe Aktionen über die Shell durchzuführen und Startskripte Ihres Linux-Systems zu verstehen und anzupassen. Sie haben gelernt, welche Elementaren Funktionalitäten dabei zum Einsatz kommen können – insbesondere die Parameterübergabe an Skripte, Kommandosubstitution, Schleifen, bedingte Anweisungen und Funktionen.
8.18 Aufgaben Abschließend wollen wir das bisher vermittelte Wissen zur Shell kombinieren. Wer möchte, kann sich den folgenden Aufgaben stellen, die recht hohe Anforderungen an Neulinge in der Shellskriptprogrammierung stellen, jedoch definitiv lösbar sind. Die größten Programme
Zum »Warmwerden« zunächst eine etwas leichtere Aufgabe: Es sollen die zehn größten ausführbaren Programme und Skripte in den Verzeichnissen der PATH-Variable in einer sortierten Top-TenListe ausgegeben werden. Achten Sie darauf, dass Sie keine Verzeichnisse mit in die Suche einbeziehen! Rausschmiss!
Die zweite Aufgabe besteht darin, eine Funktion zu schreiben, die Sie in die Startdatei Ihrer Shell integrieren können. Diese Funktion soll einen angemeldeten Benutzer aus dem System schmeißen. Dazu müssen alle Prozesse des Benutzers beendet werden.
9 Der vi(m)-Editor »I love and use VIM heavily too.«
(Dt. »Ich liebe und nutze ständig den VIM.«)
– Larry Wall Unter Linux und anderen unixartigen Systemen schreibt man nicht nur Shellskripte im Editor, man wickelt quasi die gesamte Systemkonfiguration über Editoren ab, da die Systemkonfiguration durch Textdateien realisiert wird. Anders als unter grafischen Systemen wie Windows erfolgt die Konfiguration hier nicht mit der Maus, sondern oft über Konfigurationsdateien. Möchten Sie ein System richtig konfigurieren, benötigen Sie also folglich einen Editor sowie das Know-how, um ihn zu bedienen. Außerdem müssen Sie den Aufbau jeder einzelnen Konfigurationsdatei und ihre Syntax kennen. Das hört sich schlimmer an, als es ist: Die Softwareentwicklerinnen und -entwickler achten natürlich darauf, eine möglichst einheitliche Syntax für die Konfigurationsdateien zu implementieren. Neben dem historisch sicherlich wertvollen Editor ed, mit dem wir Sie nicht quälen werden, gibt es noch einige vernünftige und daher äußerst beliebte Editoren. Der populärste Konsoleneditor dürfte wohl nach wie vor der vi-Editor sein, um den es auch in diesem Kapitel gehen soll. Ebenfalls populär sind nano und emacs. Wenn Sie unter der grafischen Oberfläche arbeiten, dann empfehlen wir Ihnen kate (unter KDE), gedit (unter Gnome), pluma (unter MATE) und die grafische Variante des vi, nämlich gvim, siehe Abschnitt 9.1 (unter allen Oberflächen). Falls Sie eine vollständige
Entwicklungsumgebung suchen, sind Atom oder Visual Studio Code empfehlenswert, denen aber die minimalistische Eleganz des vim fehlt.
9.1 vi, vim, gvim und neovim Neben dem Standard-vi, dessen Geschichte bereits 1976 began, existiert noch eine sich ständig in der Weiterentwicklung befindliche Version mit dem Namen vim (vi-improved), die als ein Klon angesehen werden kann. Im Gegensatz zum Standard-vi läuft der vim auf noch mehr Systemen und Plattformen. Zudem kommt eine auf der GTK-Library basierende grafische Oberfläche hinzu. Des Weiteren ist farbiges Syntax-Highlighting für diverse Programmiersprachen implementiert. In vielen modernen Distributionen startet der Befehl vi direkt vim, da der vim vollständig vi-kompatibel ist und alle seiner Features unterstützt. (Wir verwenden daher in diesem Buch die beiden Kommandos synonym.) Mehr über ihn erfahren Sie unter https://www.vim.org.
Abbildung 9.1 gvim: vi unter X11 – so entsteht unser Buch.
Die grafische Variante gvim (Abbildung 9.1) bietet zudem diverse Farbeinstellungen (sogenannte Themes) an. Sehr praktisch ist
außerdem die Möglichkeit, mehrere Dateien parallel in separaten Fenstern zu laden. Dabei wird der Hauptbereich des vim in Subfenster aufgeteilt. Programme können direkt aus dem Editor heraus kompiliert werden und einige zusätzliche Features wie etwa ein Hexeditor sind ebenfalls integriert. Ganz neu ist Fork neovim, der die Wartung und Weiterentwicklung des Editors einfach machen soll. Dabei wird jedoch eine vollständige Kompatibilität mit vim versprochen.
9.2 Erste Schritte Auch wenn der vi-Editor nicht alle der heute üblichen Features abdecken kann, so ist er doch, hat man ihn einmal verstanden, recht einfach zu bedienen. Und vor allen Dingen ist er klein, schnell und überall verfügbar. Und: Mit ihm können Sie Linux auch über die Konsole konfigurieren, etwa einen Linux-Server, auf den Sie sich über das Netzwerk verbinden. vi bzw. vim wird über die Kommandozeile aufgerufen. Dabei wird
optional ein zu editierender oder neu zu erstellender Dateiname übergeben, z. B.: vim /proc/cpuinfo.
Abbildung 9.2 Der vim-Editor mit geladener /proc/cpuinfo
Nachdem der Editor geladen worden ist, bekommt man ein fast leeres Terminalfenster zu sehen. (Der vi läuft nicht auf allen Terminals ganz perfekt. Ein vt100- oder vt220-Terminal sollte jedoch problemlos seinen Dienst tun.) Die Tilde-Zeilen stehen für leere Zeilen. Die unterste Zeile wird als Statuszeile genutzt und gibt Ihnen den Dateinamen sowie die Zeilenzahl und die Dateigröße in Zeichen respektive Bytes an:
# Statuszeile bei einer neuen Datei:
/tmp/file: new file: line 1
# Statuszeile bei einer bereits gesicherten Datei:
/tmp/file: 1 lines, 5 characters.
Listing 9.1 Statuszeile
9.3 Kommando- und Eingabemodus Um mit dem vi arbeiten zu können, müssen Sie seine beiden Modi kennen: den Eingabe- und den Kommandomodus. Im Eingabemodus wird Text eingefügt, ersetzt oder gelöscht. Im Kommandomodus lässt sich der Editor konfigurieren. Zudem werden die wichtigen Operationen wie das Abspeichern einer Datei oder das Suchen von Textstellen in diesem Modus abgewickelt. Wie kommt man denn überhaupt ... wieder heraus? Ganz einfach: Zunächst wechseln Sie mit der (Esc)-Taste in den Kommandomodus. Anschließend können Sie in der Statuszeile (die nun zur Kommandozeile geworden ist) den Befehl zum Beenden eingeben. Da Steuerbefehle mit einem Doppelpunkt beginnen und zum Beenden der Quit-Befehl (q) verwendet wird, muss nun folglich :q eingegeben werden. (Sie werden vielleicht lachen, aber dies ist eine der am häufigsten gestellten Fragen auf der Plattform Stackoverflow.)
9.4 Dateien speichern Dateien werden im Kommandomodus gespeichert. Das dafür notwendige Kommando »write« (w) schreibt die Datei unter dem geöffneten Namen auf das Speichermedium. Allerdings kann auch ein Dateiname als Parameter übergeben werden: :w test.txt. Möchten Sie speichern und gleichzeitig den Editor beenden, so können Sie die Befehle kombinieren: :wq [Dateiname]. Soll ohne Rücksicht auf den Verlust von veränderten Daten (also ohne sie zu speichern) ein »Quit« durchgeführt werden oder sollen Veränderungen an einer schreibgeschützten Datei vorgenommen werden, übergeben Sie dem Kommando ein Ausrufezeichen: :q! bzw. :w! oder :wq!. Natürlich müssen Sie dazu das Recht besitzen, Änderungen an den Dateiattributen vornehmen zu dürfen.
9.5 Arbeiten mit dem Eingabemodus Nach dem Start des vi-Editors befinden Sie sich im Kommandomodus. Möchten Sie Text schreiben, müssen Sie zunächst in den Eingabemodus wechseln. Je nachdem, in welchen Eingabemodus Sie wechseln möchten, müssen Sie dafür eine bestimmte Funktionstaste drücken. Kommando Wirkung i
(insert)
Text am Anfang der Zeile einfügen
I a
(append)
O
Text hinter dem aktuellen Zeichen einfügen Text am Ende der Zeile einfügen
A o
Text vor dem aktuellen Zeichen einfügen
(open)
Text in einer neuen Zeile unterhalb der aktuellen Zeile einfügen Text in einer neuen Zeile oberhalb der aktuellen Zeile einfügen
Tabelle 9.1 Eingabemodi
Am besten probieren Sie alle obigen Kommandos einmal mit einer Beispieldatei aus. Schreiben Sie sich doch eine Mail im vi und senden Sie sich diese dann via Eingabeumlenkung mit dem mailProgramm (dieses muss manchmal erst noch installiert werden). Kommando Wirkung $
zum Ende der aktuellen Zeile
Kommando Wirkung ^
und 0
zum Anfang der aktuellen Zeile
w
ein Wort vorwärts
b
ein Wort rückwärts
(Strg)+F
eine Seite vorwärts blättern (geht manchmal auch mit der (Bildë)-Taste)
(Strg)+B
eine Seite rückwärts blättern (geht manchmal auch mit der (Bildì)-Taste)
G
zur letzten Zeile der Datei bewegen
f[n]
zum nächsten Vorkommen des Zeichens n in der aktuellen Zeile bewegen (Achtung: case-sensitive)
:[n] [n]G
und
zur Zeile n bewegen; eignet sich gut, um Kompilierfehlern im Quellcode nachzugehen
Tabelle 9.2 Navigationskommandos
9.6 Navigation Um sich im Text zu bewegen, greift man in der Regel auf die Cursortasten zurück. Mit diesen allein werden leider oftmals längere Scroll-Abenteuer unvermeidlich sein. Aus diesem Grund haben sich die Entwicklerinnen und Entwickler des Editors einige Features überlegt, mit denen Sie sich effizient und schnell im Text bewegen können. An älteren Unix-Terminals standen oftmals keine Cursortasten zur Verfügung. Daher kann man auch mit den Tasten (H) (links), (J) (runter), (K) (rauf) und (L) (rechts) navigieren. Dazu müssen Sie allerdings in den Kommandomodus wechseln.
9.7 Löschen von Textstellen Sofern Sie an Editoren mit grafischem Interface wie etwa aus der Windows-Welt gewöhnt sind, ist Ihnen bekannt, dass mit der (í)Taste ein geschriebenes Zeichen wieder gelöscht werden kann. Im vi-Editor steht diese Funktionalität nicht zur Verfügung. Ein Löschvorgang muss – wie immer – über den Kommandomodus mit entsprechenden Anweisungen bewerkstelligt werden. Tabelle 9.3 fasst die vi-Kommandos zum Löschen von Textstellen zusammen. Kommando Wirkung löscht das aktuelle Wort
dw d$
und D
löscht vom aktuellen Zeichen bis zum Ende der Zeile
d0
und d^
löscht vom aktuellen Zeichen bis zum Zeilenanfang
df[c]
löscht vom aktuellen Zeichen bis zum Vorkommen von c in der aktuellen Zeile
dG
löscht alles von der aktuellen bis zur einschließlich letzten Zeile des Zwischenspeichers (Puffer)
d1G
löscht alles von der aktuellen bis zur einschließlich ersten Zeile des Zwischenspeichers
dd
löscht die aktuelle Zeile
Tabelle 9.3 Löschkommandos
Sollten Sie einmal aus Versehen eine Aktion durchgeführt haben (etwa unbeabsichtigtes Überschreiben oder Löschen), können Sie diesen Vorgang mit dem Kommando u (Undo) rückgängig machen. Doch beachten Sie, dass diese Möglichkeit nur für den letzten Vorgang besteht. Sobald Sie ein neues Wort geschrieben haben, ist Ihnen diese Möglichkeit also verbaut. Da hilft nur das vorher erstellte Backup bzw. das Beenden, ohne zu speichern (:q!).
9.8 Textbereiche ersetzen Oftmals möchten Sie ein Wort nicht einfach nur löschen, sondern überschreiben, einzelne Zeichen eines Wortes ersetzen oder Ähnliches. Dies wird mit den c-Kommandos bewerkstelligt. Kommando Wirkung überschreibt ein ganzes Wort
cw c$
und D
überschreibt vom aktuellen Punkt bis zum Ende der Zeile
c0
und c^
überschreibt vom aktuellen Punkt bis zum Anfang der Zeile
cf[c]
überschreibt bis zum nächsten Zeichen c der aktuellen Zeile
Tabelle 9.4 Ersetzen-Kommandos
Nehmen wir einmal an, der Inhalt einer Datei text.txt beinhaltet den String »Das ist der Inhalt von >text.txt >) und (< Verzeichnis.tar
$ ls *.tar
Verzeichnis.tar
Listing 10.16 Ein Archiv mit tar erstellen
tar schreibt die binären Daten standardmäßig einfach auf die
Standardausgabe, also in unserem Fall auf den Bildschirm. Das ist nützlich für Pipes, siehe Kapitel 4, »Grundlagen der Shell«. Weil wir sie aber nicht auf dem Bildschirm, sondern lieber in einer Datei haben wollen, müssen wir die Ausgabe mit dem >-Operator in eine Datei umlenken. Möchten wir das Ganze auch noch packen, dann müssen wir zur Option -c für »create« auch noch ein -z packen, um das Resultat noch zu gzippen. Das erspart uns den Aufruf eines Extraprogramms und so ist es also nicht ganz richtig, dass wir am Anfang sagten, dass Archivierer und Packer streng voneinander getrennt sind. Das Resultat ist allerdings: Es handelt sich um ein gepacktes tar-Archiv. $ tar -cz Verzeichnis > Verzeichnis.tar.gz
$ ls *.gz
Verzeichnis.tar.gz
Listing 10.17 Ein komprimiertes Archiv mit tar erstellen
Jetzt haben wir alle Dateien in Verzeichnis gepackt. Wir drücken die Tatsache, dass wir den Inhalt von Verzeichnis erst gepackt und dann komprimiert haben, durch die Endung .tar.gz aus, die oft auch als .tgz abgekürzt wird. Möchten wir so ein Archiv wieder entpacken, nutzen wir statt -c für »create« einfach die Option -x für »extract«. Handelt es sich um ein gzip tar-Archiv, packen wir wie beim Erstellen einfach noch das -z dazu. $ tar -xz Verzeichnis.tar.gz
Listing 10.18 Ein Archiv mit tar entpacken
Was man mit tar noch so alles anstellen kann, erfahren Sie wie immer auf der Manpage, die Sie mit $ man tar lesen. Dort finden Sie unter anderem Informationen darüber, wie bestehende Archive modifiziert werden können oder wie Sie sich deren Inhalt ausgeben lassen können. Komprimieren mit gzip, bzip2 und compress
Wie wir bereits erwähnt haben, gibt es unterschiedliche Werkzeuge, die alle unterschiedliche Komprimierungsverfahren einsetzen und daher mehr oder weniger effektiv sind. Je mehr eine Datei komprimiert wird, umso länger muss in der Regel diese Komprimierung berechnet werden. Im Folgenden Listing stellen wir einfach die entsprechenden Komprimierungsprogramme gegenüber. Dazu haben wir unser Buchverzeichnis gepackt, das im Original zum aktuellen Zeitpunkt stolze 13 MByte groß ist. Wie Sie sehen, sind die Ergebnisse für verschiedene Kompressoren doch sehr unterschiedlich. Das GNUgzip-Programm ist in Verbindung mit tar äquivalent zum historischen PKZip aus der DOS/Windows-Welt und liefert das größte Ergebnis. Mit Abstand die kleinste Datei hat bzip2 erzeugt, allerdings hat das Programm dafür auch am längsten gebraucht. Das ist ein Grund dafür, dass man in der Unix-Welt oft auf den Kompromiss gzip zurückgreift. $ ls -lh Buch*
-rw-r--r-- 1 jploetne -rw-r--r-- 1 jploetne -rw-r--r-- 1 jploetne -rw-r--r-- 1 jploetne …
users users users users
2.2M 3.7M 3.8M 13M
Oct9 Oct9 Oct9 Oct9
Buch.tar.bz2
Buch.tar.gz
Buch.zip
Buch.tar
Listing 10.19 Vergleich der Komprimierungsprogramme
Sie sehen auch gut, dass tar die Daten standardmäßig nicht packt – das .tar-Archiv ist nämlich genauso groß wie das Verzeichnis selbst. Nun möchten wir die Programme einzeln kurz vorstellen und ihre Bedienung erläutern. compress
Das Programm compress hat mittlerweile nur noch historische Bedeutung und wird in der Praxis kaum noch eingesetzt. Die Programme zum Packen bzw. Entpacken heißen compress und uncompress und die entsprechenden Dateien werden dabei einfach auf der Kommandozeile übergeben. Weil compress kaum noch genutzt wird, ist auf vielen Systemen oft nur uncompress vorhanden. So haben Sie zwar die Möglichkeit, noch vorhandene alte Archive zu entpacken, jedoch nicht die Möglichkeit, neue zu erstellen. bzip2
Das effektivste unter den Komprimierungsprogrammen wird durch die Kommandos bzip2 und bunzip2 gesteuert, an die sich der Dateiname der zu komprimierenden Datei anschließt. Optional kann man beim Packen beispielsweise noch das Verhältnis von Geschwindigkeit und Effektivität durch die Parameter -1 bis -9 steuern, wobei -1 die schnellste und -9 die effektivste Art zu packen bezeichnet. Haben Sie eine gepackte Textdatei, so können Sie sie auch lesen, ohne sie gleich entpacken zu müssen. Für diesen Fall gibt es nämlich das praktische Programm bzcat, das den Inhalt der Datei einfach ausgibt: $ bzcat Readme.bz2
Das ist
ein Test
.
$
Listing 10.20 Gepackte Dateien lesen: bzcat
gzip
Die einfache Steuerung durch zwei Programme zum Packen und Entpacken gibt es natürlich auch – gzip und gunzip. Allerdings gibt es für das beliebte und populärste Packprogramm weitaus mehr Tools als nur ein zcat wie bei bzip2. Es sind nämlich unter anderem diff, less und grep jeweils durch ein vorangestelltes z auch für so gepackte Dateien verfügbar.
10.4 Logdateien und dmesg Nachdem wir unsere Benutzer und die Software verwalten sowie wichtige Daten sichern können, wollen wir jetzt das System erst einmal so am Laufen halten. Linux erleichtert diese Arbeit dadurch, dass für alle wichtigen Ereignisse Logdateien erstellt werden. Eine Logdatei ist eine Datei, in der wichtige Vorgänge im System verzeichnet werden. Viele Softwarepakete haben eigene Logdateien. Sie werden in diesem Abschnitt die wichtigsten kennenlernen. Diese Dateien sind im Allgemeinen unter /var/log oder dessen Unterverzeichnissen gespeichert. 10.4.1 /var/log/messages
Die wichtigste Logdatei eines Systems enthält so ziemlich alles, was der Kernel oder einige Systemprozesse mitzuteilen haben. So schreibt der Kernel eigene Meldungen in diese Datei, aber auch Anwendungen ohne eigene Logdateien haben die Möglichkeit, Nachrichten hineinzuschreiben. Sehen wir uns einfach mal einen Auszug an: …
Oct 12 11:44:44 localhost kernel: eth0: no IPv6 \
routers present
…
Oct 12 14:59:00 localhost /USR/SBIN/CRON[2011]: \
CMD ( rm -f /var/spool/cron/lastrun/cron.hourly)
Okt 12 15:29:09 localhost su: FAILED SU (to root)
jploetner on /dev/pts/4
Okt 12 15:29:16 localhost su: (to root) jploetner \
on /dev/pts/4
Okt 12 15:29:16 localhost su: pam_Unix2: session \
started for user root, service su
Okt 12 15:31:42 localhost su: pam_Unix2: session
finished for user root, service su
\
Listing 10.21 Auszug aus /var/log/messages
Hier wird auch schon der generelle Aufbau deutlich, den Logdateien oft haben: Das Datum und die genaue Uhrzeit werden vor der eigentlichen Nachricht angegeben. Im Fall der /var/log/messages folgt nach der Zeit der Name des Rechners, der die Meldung verursachte, dann der Name des aufrufenden Programms, gefolgt von der eigentlichen Meldung. In unserem Fall beinhaltet die Datei ausschließlich Meldungen eines einzigen Computers, der im Logfile als localhost identifiziert wird. (Der Name localhost bezeichnet immer den aktuellen, also eigenen Rechner. In einem Netzwerk können aber unterschiedliche Hostnamen vergeben werden, siehe Kapitel 11, »Netzwerke unter Linux«.) In unserem Beispiel haben wir Meldungen von den Programmen cron, su und dem Kernel sowie Einträge des LoggingSystems selbst, die wir aber getrost ignorieren können. An der Ausgabe können wir zum Beispiel erkennen, dass der Benutzer jploetner einmal vergeblich versucht hat, sich per suKommando die root-Identität zu verschaffen, um als Systemadministrator Aufgaben zu übernehmen. Ein paar Sekunden später hat er es aber dann doch geschafft und ist für ein paar Minuten in einer Session als root aktiv. Aus den Meldungen des Kernels wurde eine beliebige herausgegriffen, in diesem Fall eine Meldung vom Bootvorgang, die beim Initialisieren der Netzwerkschnittstellen aufgetreten ist. Es werden also schwerwiegende Fehler, Warnungen sowie schlichte Informationen in dieser Logdatei angezeigt.
Eine typische Information ist der Eintrag des cron-Programms. Es handelt sich dabei um einen Dienst, der regelmäßig Programme beispielsweise zu Administrationszwecken ausführt. Eine solche Logdatei ist ein komfortabler Weg, um an Rückmeldungen über die gestarteten Programme zu kommen – in diesem Fall wird einfach nur eine Statusdatei mit dem Kommando rm gelöscht, um die Festplatte nicht mit unnützen Daten zu verstopfen. 10.4.2 /var/log/wtmp
Die wtmp-Datei enthält Informationen über die letzten LoginVersuche bzw. die letzten Logins für jeden einzelnen Nutzer. Leider ist die Datei in keinem lesbaren Textformat gespeichert. Daher sollten Sie lieber das Programm lastlog nutzen, um Informationen aus dieser Datei auszulesen und anzuzeigen: $ lastlog
Username Port From root tty2 bin …
jploetner :0 console swendzel pts/5 jupiter.wg …
Latest
Don Sep 18 16:35:01 +0200 2003
**Never logged in**
Son Okt 12 11:46:30 +0200 2003
Son Okt 12 20:05:22 +0200 2003
Listing 10.22 Die letzten Logins mit lastlog
Beim einfachen Aufruf von lastlog werden alle Benutzer mit den entsprechenden Daten ausgegeben. Zu diesen Daten gehört natürlich auf der einen Seite der Zeitpunkt, auf der anderen Seite beispielsweise aber auch der Ort, von dem aus sich der entsprechende Benutzer bzw. die Benutzerin eingeloggt hat. Das kann eine lokale Konsole (beispielsweise tty2) oder auch ein fremder Rechner (jupiter.wg) sein, der eine virtuelle Konsole (pts/5) zum Einloggen nutzt.
10.4.3 /var/log/Xorg.log
Die Logdatei der grafischen Oberfläche ist ein typisches Beispiel für eine Logdatei einer Anwendung, die ja vom System getrennt ist. An dieser Stelle möchten wir nur erwähnen, dass es diese Datei gibt, da man nach unerklärlichen Abstürzen des XServers vielleicht hier einen Hinweis auf die Ursache findet. 10.4.4 syslogd
Der syslogd hat nur bedingt etwas mit Logdateien zu tun – er ist ein sogenannter Daemonprozess und verwaltet die Logdateien. Wenn dieser Dienst läuft, haben auch andere Rechner im Netzwerk die Möglichkeit, auf diesem Rechner ihre Logdateien anzulegen. Aus diesem Grund ist in der /var/log/messages ein Feld für den Rechnernamen reserviert – so können Sie die einzelnen Systeme auseinanderhalten. Sinnvoll ist der Einsatz eines Logging-Servers oft nur in größeren Netzwerken, wo beispielsweise Hacker daran gehindert werden sollen, auf gehackten Systemen durch das Löschen von Logdateien ihre Spuren zu verwischen. 10.4.5 logrotate und DoS-Angriffe
Logdateien werden mit der Zeit immer größer und übergroße Logdateien können ein System potenziell außer Gefecht setzen, indem sie irgendwann die ganze Festplatte füllen und das System somit keine neuen Daten mehr ablegen kann. Man nennt einen derartigen Angriff einen DoS-Angriff (Denial of Service), weil der Rechner vom Angreifer so überlastet wird, dass er schließlich den Dienst verweigert.[ 29 ] Zur Lösung des Problems überfüllter Platten gibt es das Programm logrotate. Wird eine Logdatei zu groß oder ist ein bestimmter
Zeitabschnitt vorüber, wird die Logdatei gepackt bzw. gelöscht und eine neue, leere Logdatei erstellt. logrotate ist kein Daemonprozess und das hat auch seine Gründe.
Es ist nicht notwendig, dass logrotate die gesamte Zeit im Hintergrund läuft und Speicher sowie Rechenzeit frisst – logrotate läuft als cron-Job, wird also vom cron-Daemon in einem regelmäßigen Intervall gestartet. In vielen Distributionen ist logrotate schon als cron-Job sinnvoll vorkonfiguriert und wenn Sie in Ihrem Logverzeichnis /var/log ein paar durchnummerierte und gepackte Dateien finden, ist logrotate bereits am Werk. $ ls /var/log/messages*
/var/log/messages
/var/log/messages-20030510.gz
/var/log/messages-20030629.gz
Listing 10.23 logrotate im Einsatz
10.4.6 tail und head
Wenn Sie einmal einen Blick in /var/log werfen, werden Sie feststellen, dass dort viel mehr Dateien liegen als nur die paar, die wir Ihnen hier vorgestellt haben. Zudem sind Logdateien trotz logrotate mitunter sehr groß und ein Anschauen mit cat macht da nicht wirklich Spaß. Als Lösung bieten sich wie immer ein paar mysteriöse Shellprogramme an: head und tail. Beide Programme haben eine ähnliche Syntax und zeigen jeweils Teile einer Datei, doch mit dem Unterschied, dass head die ersten und tail entsprechend die letzten Zeilen einer Datei anzeigt. $ head Makefile
all : .
latex buch
makeindex -s gp97.ist buch
latex buch
makeindex -s gp97.ist buch
latex buch
@echo "-------------done-------------"
dvips -P gp -j0 -o buch.ps buch
@echo "----------realy done----------"
…
$ tail Makefile
view:
evince buch.ps
backup :
cp -r *.tex ../working_backup/
sync
…
Listing 10.24 head und tail
In diesem Beispiel haben wir uns einfach das Makefile für unser Buch angeschaut. Ein Makefile ist eine Art Skript, das normalerweise eher für die Programmierung genutzt wird. Wir haben jedoch ein Skript geschrieben, das unser Buch automatisch übersetzt, sichert oder in einem Extra-Betrachter anzeigt. Aber eigentlich ist das nebensächlich, denn es geht schlicht um den Fakt, dass head wie versprochen den Anfang und tail das Ende der Datei ausgibt. Per Default sind das jeweils 10 Zeilen, es kann aber mit der Option -n eine andere Zeilenzahl angegeben werden. Da in Logdateien die neuesten Einträge sinnvollerweise immer unten stehen, ist sicherlich tail für Sie das Programm der Wahl. Mit der Option -f können Sie auch aktuelle Änderungen mitverfolgen. Das Programm bleibt aktiv und wenn neue Einträge in die Datei geschrieben werden, werden diese auch gleichzeitig auf dem Bildschirm ausgegeben.
10.5 Weitere nützliche Programme In ein Kapitel zur grundlegenden Administration gehört noch eine Reihe kleiner, aber doch wichtiger Programme, die wir nun im Einzelnen vorstellen wollen. 10.5.1 Speicherverwaltung
Sie werden es von anderen Systemen vielleicht weniger gewohnt sein, Kontrolle über Ihren Hauptspeicher zu besitzen, um eventuelle Spitzen in der Auslastung abfangen zu können. Die Kontrolle über den Hauptspeicher fängt schon beim Sticky-Bit an und erstreckt sich bis zur Laufzeitkontrolle über Swap-Bereiche auf der Festplatte. Sie merken schon: An dieser Stelle fängt es so langsam an, sich auszuzahlen, wenn man das Buch bisher intensiv gelesen und nicht einfach Abschnitte überblättert hat. free
Bevor man etwas aktiv unternimmt, steht zuallererst einmal die Kontrolle an. Einen Überblick über die Auslastung bietet beispielsweise das free-Programm: $ free
total used free shared buffers cached
Mem: 256412 250636 5776 0 43204 90168
-/+buffers/cache:117264 139148
Swap: 546200 6152 540048
Listing 10.25 Speicherauslastung mit free prüfen
Sofern Sie sich nicht näher für das Speichermanagement von Linux interessieren, sind für Sie im Allgemeinen nur die Spalten used bzw.
free von Interesse.
Standardmäßig werden die Größen bei free in Kilobyte angegeben. Über den Daumen gepeilt kann man die Angaben also durch 1.000 teilen bzw. gleich den -m-Parameter verwenden, um die entsprechenden Angaben in Megabyte zu erhalten. Hier im Beispiel sind also von 256 MByte RAM 250 belegt und noch knapp 6 MByte frei. Der Swap-Speicher ist für diese Auslastung also eindeutig überdimensioniert: Von über 540 MByte Platz sind gerade einmal 6 MByte belegt. swapon/swapoff
In so einem Fall könnten Sie die Swap-Partition auch einfach deaktivieren. Sie werden sich jetzt sicher fragen, was mit den 6 MByte passiert, die vorher ausgelagert waren. Um es einfach zu sagen: Sie werden wieder in den Hauptspeicher kopiert und dafür werden der Puffer und der Festplattencache, den Linux aus Performancegründen anlegt, etwas verkleinert. # swapoff /dev/sda4
# free
total used free shared buffers cached
Mem: 256412 252848 3564 0 40648 93704
-/+buffers/cache:118496 137916
Swap: 0 0 0
# swapon /dev/sda4
# free
total used free shared buffers cached
Mem: 256412 252744 3668 0 40292 93564
-/+buffers/cache:118888 137524
Swap: 546200 0 546200
Listing 10.26 Swap-Partition ausschalten
Nach dem Deaktivieren der Swap-Partition zeigt free auch keinen verfügbaren Swap mehr an. Nachdem die Swap-Partition jedoch wieder mit swapon aktiviert wurde, ist erneut Auslagerungsspeicher
vorhanden. Bis dieser dann so langsam wieder genutzt wird, kann allerdings noch einige Zeit vergehen. 10.5.2 Festplatten analysieren
Als Nächstes wollen wir nun nicht den Haupt-, sondern den Plattenspeicher betrachten. Auch bei Festplatten gibt es viel zu überwachen. Zunächst werden Sie lernen, wie generelle Einstellungen von eingehängten Festplatten abgefragt werden können und anschließend betrachten wir Speicherplatzverwendung und -bedarf. hdparm
Mit dem Programm hdparm können Sie Festplatteneinstellungen ansehen und ggf. auch ändern Um sich einen Überblick über die aktuelle Konfiguration zu verschaffen, rufen Sie hdparm mit der zu überprüfenden Festplatte als Argument auf: # hdparm /dev/sda
/dev/sda:
multcount = 16 (on)
IO_support = 0 (default)
readonly = 0 (off)
readahead = 256 (on)
geometry = 30400/255/63, sectors = 488390625, start = 0
Listing 10.27 Festplatteneigenschaften anzeigen mit hdparm
An dieser Stelle möchten wir nicht weiter auf die Operationen zum Ändern dieser Werte eingehen. Die Werte sollten seitens Ihrer Distribution bereits vernünftig gesetzt sein, alternativ legen wir Ihnen das Studium der Manpage ans Herz. du vs. df
Das Tunen der Festplatte ist eine Sache, ihre Auslastung allerdings eine andere. Einen Überblick über die Auslastung der Partitionen bzw. die Größe einzelner Verzeichnisse geben die Programme df und du. $ df -h
Dateisystem Größe Benutzt Verf. Verw% Eingehängt auf
udev 1,9G 0 1,9G 0% /dev
tmpfs 395M 808K 394M 1% /run
/dev/sda1 229G 21G 197G 10% /
...
$ du -h latex_grundk
327M latex_grundk/images
510M latex_grundk
Listing 10.28 df und du
Wie Sie sehen, zeigt df die Statistik für alle Platten und Partitionen an, du dagegen die Größe bestimmter Dateien bzw. Verzeichnisse. In beiden Fällen wurde die Option -h benutzt, die die Ausgaben von Kilobytes in die entsprechend passende Größenordnung ändert. Das macht das Ganze lesbarer. -h steht daher auch für human readable. 10.5.3 Wer ist eingeloggt?
Die Programme who und w dienen beide dazu, die im Moment eingeloggten Benutzer anzuzeigen. In der Standardeinstellung ohne Parameter erledigen beide Programme also die gleiche Aufgabe: $ w
10:16am up 21 min, 5 users, load average: 0.12, 0.14, 0.17
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
jploetne :0 console 9:56am ? 0.00s ? -
jploetne pts/0 9:56am 19:28 0.00s ? -
jploetne pts/2 9:57am 4.00s 0.11s 0.10s /usr/bin/mc
jploetne pts/1 9:57am 19:14 0.04s 0.04s /bin/bash
jploetne pts/3 9:57am 1.00s 0.02s 0.00s w
$ who
jploetner :0 Oct 16 09:56 (console)
jploetner pts/0 Oct 16 09:56
jploetner pts/2 Oct 16 09:57
jploetner pts/1 jploetner pts/3
Oct 16 09:57
Oct 16 09:57
Listing 10.29 who und w
Dass es beide Programme gibt und sie nicht zu einem zusammengefasst wurden, ist eine Folge der doch etwas verworrenen Unix-Geschichte. Außerdem sind beide Programme unterschiedlich aussagekräftig. So können Sie bei w sogar noch sehen, welche Programme der Benutzer bzw. die Benutzerin auf den einzelnen Konsolen jeweils gestartet hat. 10.5.4 Offene Dateideskriptoren mit lsof
Stellen Sie sich einmal folgende Situation vor: Sie möchten ein Dateisystem unmounten, aber leider wird noch irgendwo eine Datei benutzt – umount schlägt also fehl. Schließlich verbringen Sie dann Ihre kostbare Zeit damit, diese Datei zu suchen. Natürlich muss das nicht sein. Schauen Sie sich doch einfach mit dem lsof-Kommando alle offenen Deskriptoren des Kernels an – und damit alle geöffneten Dateien und Verzeichnisse aller Prozesse. …
gv
2291 jploetner 4r REG 3,3 15254978 8160
/home/jploetner/Documents/Buch/Linux/buch.ps
gs 2292 jploetner cwd DIR 3,3 1784 8191
/home/jploetner/Documents/Buch/Linux
gs 2292 jploetner rtd DIR 3,1 4096 2
/
gs 2292 jploetner txt REG 3,1 2414232 1757093
/usr/bin/gs-gnu
gs 2292 jploetner mem REG 3,1 90144 180580
/lib/ld-2.3.2.so
gs 2292 jploetner mem REG 3,1 7796 1953644
/usr/X11R6/lib/X11/locale/common/xlcDef.so.2
…
Listing 10.30 lsof
Hier im Beispiel sehen Sie unter anderem, dass der Prozess gv (PID 2291) auf die Datei /home/jploetner/Documents/Buch/Linux/buch.ps lesend (4r) zugreift. Des Weiteren sehen Sie den ausführenden Benutzer (jploetner) und andere Angaben. Ein eigener Eintrag ist natürlich dem entsprechenden Verzeichnis der Datei gewidmet, schließlich wird es ja auch »genutzt«. Um auf unser Problem zurückzukommen: Sollten Sie also offene Dateien suchen, greppen Sie doch einfach die Ausgabe von lsof nach dem Mountpoint: $ lsof | grep /mnt/usbdrive
bash 541 jploetner cwd DIR /mnt/usbdrive
3,1
4096
2380335
Listing 10.31 Finden offener Dateien auf /mnt/usbdrive
Hier sind wir also noch mit einer Shell im entsprechenden Verzeichnis und können daher nicht unmounten. Ein cd / auf der entsprechenden Konsole schafft aber sofort Abhilfe. Das Problem wäre damit gelöst.
10.6 Grundlegende Systemdienste In diesem Kapitel haben Sie bereits den syslogd als Serverdienst kennengelernt, der grundlegende Aufgaben zur Systemverwaltung übernimmt. Einige weitere wichtige Dienste werden wir Ihnen jetzt vorstellen. 10.6.1 cron
Im Zusammenhang mit logrotate haben wir in diesem Kapitel auch schon von cron gesprochen. cron ist ein Dienst, der regelmäßig Programme laufen lässt. Aber erklären wir das mit einem Beispiel: $ ls /etc/cron.*
/etc/cron.d:
. ..
/etc/cron.daily:
. clean_catman .. do_mandb
/etc/cron.hourly:
. ..
/etc/cron.monthly:
. ..
/etc/cron.weekly:
. ..
logrotate
tetex
Listing 10.32 Die Konfiguration von cron
Sie sehen in /etc viele Verzeichnisse, die mit cron zu tun haben. Diese sind bis auf /etc/cron.d bereits alle entsprechend ihrer Funktion bezeichnet: Je nach Verzeichnis können Sie Skripte stündlich, täglich, wöchentlich oder monatlich ausführen lassen. Die Dateien in den Verzeichnissen sind dabei nur einfache Shellskripte, wie das nächste Listing zeigt:
$ cat /etc/cron.daily/logrotate
#!/bin/sh
/usr/sbin/logrotate /etc/logrotate.conf
Listing 10.33 logrotate und cron
In diesem Skript wird einfach nur der logrotate-Befehl mit den benötigten Parametern aufgerufen. Auch erlaubt das crontabKommando viel präzisere Möglichkeiten, den regelmäßigen Ablauf von Programmen zu steuern. Dieses Kommando im Zusammenhang mit der entsprechenden Konfigurationsdatei /etc/crontab zu beschreiben, würde den Rahmen des Buches sprengen. Bei Bedarf helfen hier wie immer die Manpage sowie zahlreiche Onlinedokumentationen weiter. Fürs Erste reichen diese Verzeichnisse auch völlig aus, um einfache Abläufe zu regeln. Nehmen Sie sich einfach ein Beispiel und probieren Sie es aus! 10.6.2 at
Im Gegensatz zu cron, der Jobs regelmäßig ausführt, führt at Jobs nur einmal zu einem bestimmten Zeitpunkt aus. $ date
So 23. Mai 18:43:29 CEST 2021
$ at 18:44
warning: commands will be executed using /bin/sh
at> echo "Hallo" > /dev/pts/7
at> (Nutzer drückt Strg+D)
job 3 at Sun May 23 18:44:00 2021
$ Hallo (Ausgabe erscheint mit Zeilenumbruch)
date
So 23. Mai 18:44:06 CEST 2021
Listing 10.34 at im Einsatz
Das Beispiel zeigt recht eindrucksvoll, wie einfach at zu bedienen ist. Am besten rufen Sie nämlich at mit der gewünschten
Ausführungszeit als Argument auf. Danach startet eine Art Shell, in der Sie die Kommandos eingeben können. Die Eingabe wird durch ein EOF-Zeichen quittiert, das Sie in der bash durch Drücken einer Tastenkombination ((Strg) + (D)) erzeugen. Damit es besonders anschaulich wird, haben wir uns in diesem Beispiel einfach etwas auf die aktuelle Konsole schreiben lassen. Und siehe da, zwei Minuten später tauchte plötzlich ein ominöses »Hallo« auf, ohne dass wir etwas eingegeben hätten -- at hat also seine Arbeit erledigt.
10.7 Manpages Manpages (kurz für Manual Pages) beinhalten eigentlich alle Informationen zu allen Programmen und Library-Funktionen des Basissystems. Manpages sind zwar sehr kurz gehalten, dafür aber äußerst übersichtlich und sparen Ihnen oftmals viel Zeit, wenn es darum geht, herauszufinden, wie Sie ein Programm Y dazu bewegen können, X zu tun. Anschauen können Sie sich diese Manual Pages mit dem Programm man. Das Kommando oder die Funktion, zu dem bzw. der Sie eine Hilfe benötigen, wird dabei einfach als Parameter übergeben: man ls.
Abbildung 10.2 Eine Manpage
Manpages können über den Parameter -k (keyword) gesucht werden. Die Ausgabe der Suchergebnisse erfolgt spaltenweise. Die erste Spalte gibt das Kommando bzw. die Funktion oder das Programm aus, Spalte 3 enthält seine Beschreibung und die mittlere Spalte enthält die Sektion. $ man -k uptime
uptime(1)- Tell how long the system has been running.
Listing 10.35 Manpages suchen
Manpage-Sektionen wurden erstellt, um eine einfachere Suche nach den Manpages zu gestalten und gleichnamige Themen mit unterschiedlichen Bedeutungen voneinander zu trennen. Das Kommando printf etwa ist zwar einerseits für die Shell (Sektion 1), andererseits aber auch für die C-Library (3) zu finden. Sektion Beschreibung 1
Diese Sektion enthält die Benutzerkommandos.
2
In Sektion 2 sind Syscalls untergebracht. Das sind Beschreibungen von Funktionen, die mithilfe der Sprache C auf den Kernel zugreifen.
3
Diese Sektion beschreibt die Subroutinen. Dabei handelt es sich um Funktionen, die keine Syscalls sind, wie printf().
4
Diese Sektion enthält Beschreibungen zu den Gerätedateien und Treibern.
5
Die fünfte Sektion beinhaltet die Beschreibungen zu den Dateiformaten.
6
Diese Sektion beschreibt die einzelnen installierten Spiele.
7
Alles, was keiner anderen Sektion zugeordnet werden konnte, findet in dieser Sektion ein Zuhause.
8
In dieser Sektion dreht sich alles um das Thema Systemadministration – sie beinhaltet Beschreibungen der entsprechenden Programme.
Sektion Beschreibung 9
Für Kernelhacker immer wieder interessant: die Kernelsektion.
Tabelle 10.2 Manpage-Sektionen
Wollen Sie nun eine Manpage aus Sektion 1 aufrufen, geben Sie die Nummer entweder vor dem Suchbegriff oder explizit über den -sParameter an: man 1 ls.
Damit alle installierten Manpages gefunden werden, muss die Shell wissen, wo sich die Manpages überhaupt befinden. Für diese Aufgabe wird die MANPATH-Variable gesetzt. Sie enthält alle Verzeichnisse, in denen nach Manpages gesucht werden soll. Alternative Verzeichnisse für Manpages Im Normalfall befinden sich die Manpages im Verzeichnis /usr/man oder im Verzeichnis /usr/local/man.
10.8 Dateien finden mit find Die Suche nach Dateien ist eine grundlegende Aufgabe für jeden Systemadministrator und jeden Benutzer bzw. jede Benutzerin. Unter Linux steht Ihnen für diese Aufgabe das Programm find – zumindest die GNU-Version von find – zur Verfügung. find verfügt, wie Sie gleich sehen werden, über weitaus mehr Funktionalitäten, als man vielleicht vermuten würde. find durchsucht den ihm übergebenen Pfad rekursiv, d. h., es
durchsucht auch die Unterverzeichnisse des Pfades. Ohne weitere Parameter werden alle Dateien gesucht und ausgegeben. Möchten Sie – wie es wohl fast immer der Fall sein wird – lieber bestimmte Kriterien für die Suche festlegen, müssen Sie diese zusätzlich übergeben. Soll dann wiederum noch eine Aktion mit den gefundenen Dateien, wie etwa das Löschen der Dateien, durchgeführt werden, so geben Sie sie ebenfalls noch an. Der Aufbau für einen Aufruf von find ist:
find [Pfad] [Kriterium] [Aktion]
Das folgende Beispiel illustriert diesen Aufbau. Dabei wird die Datei buch.ps im Heimatverzeichnis samt Unterverzeichnissen des Benutzers user gesucht. $ find /home/user/ -name buch.ps
/home/user/cd/LINUX_BUCH/buch.ps
Listing 10.36 find-Aufruf
10.8.1 Festlegung eines Auswahlkriteriums
Es steht eine Reihe von Parametern zur Verfügung, die find beim Aufruf übergeben werden können, um bestimmte Kriterien für die
Auswahl der Dateisuche festzulegen. Unter anderem können Sie nach dem Namen, der Erstellungszeit, der Größe, den Zugriffsrechten oder der Erstellungs- und Modifikationszeit einer Datei suchen: Parameter
Kriterium
-amin/-atime [n]
Sucht nach Dateien, auf die in den letzten n Minuten/Tagen ein Zugriff erfolgte.
-cmin/ctime [n]
Sucht nach Dateien, die in den letzten n Minuten/Tagen neu erstellt wurden.
-empty
Sucht nach leeren, regulären Dateien und Verzeichnissen.
-fstype [Typ]
Sucht im Dateisystem Typ.
-gid [n]
Sucht nach Dateien mit Gruppen-ID n.
-group [Name]
Sucht nach Dateien der Gruppe Name.
-inum [inode]
Sucht nach Dateien mit der InodeNummer inode (dienen der Identifikation von Dateien).
-links [n]
Sucht nach Dateien, auf die n Hardlinks verweisen.
-mmin/mtime [n]
Sucht nach Dateien, deren Inhalt innerhalb der letzten n Minuten/Tage modifiziert wurde.
-name [Name]
Sucht nach Dateien mit dem Namen Name.
Parameter
Kriterium
-nouser/group
Sucht nach Dateien, deren Benutzer-/Gruppen-ID keinem Eintrag in der Passwortdatei zugeordnet werden kann.
-perm [Recht]
Sucht nach Dateien mit dem Zugriffsrecht Recht.
-size [n]
Sucht nach Dateien mit der Größe n, wobei die Einheit festgelegt werden kann. Dabei kann nach der Blockgröße (b), Bytegröße (c), KBytegröße (k) und der Wortgröße (w – ein Wort ist 2 Byte groß) gesucht werden: »2k« entspricht also der Angabe von 2 KByte.
-type [Typ]
Sucht nach Dateien des Typs Typ. Als Typ können Sie b (Blockdatei), c (Character-Datei), d (Verzeichnis), p (FIFO), f (reguläre Datei), l (Softlink) und s (Socket) angeben.
-uid [UID]
Sucht nach Dateien, die dem Benutzer mit der ID UID gehören. Übrigens kann mittels -user [Name] auch direkt über den Benutzernamen gesucht werden.
Tabelle 10.3 Suchkriterien
Suchen wir einmal nach einer Datei, die mit dem Zugriffsrecht 644 (oktal) versehen ist, dem Benutzer nobody gehört und deren Name mit .tex endet. Wie Sie sehen, stellt es für find kein Problem dar,
mehrere Kriterien gleichzeitig zu beachten. Der Stern-Operator ist ein sogenannter regulärer Ausdruck (siehe Kapitel zu selbigem Thema). $ find . -perm 644 -user nobody -name '*.tex'
./anhang.tex
./buch.tex
./glossar.tex
./kap01.tex
./kap02.tex
./kap03.tex
./kap04.tex
./kap05.tex
./kap06.tex
./kap07.tex
./kap08.tex
…
Listing 10.37 Beispielsuche
Merkmale für Suchkriterien können auch mit dem Additions- bzw. Subtraktionszeichen spezifiziert werden. -size +2048k sucht beispielsweise nach Dateien, deren Größe mindestens 2 MByte ist: $ find /usr/local/bin -ctime -3 -perm +2755
-links +2 -name '[euolimn]*'
\
Listing 10.38 Wenn es mal nicht so genau sein muss ...
Logische Operationen
Die Suchkriterien können auch logischen Operationen unterzogen werden. So kann eine logische Verneinung (!-Operator) beispielsweise dazu führen, dass alle Dateien gesucht werden, die nicht dem Suchkriterium entsprechen. Darüber hinaus können ein logisches Und sowie ein logisches Oder in den Kriterien vorkommen, womit alle Suchbedingungen (logisches Und) bzw. nur eine (logisches Oder) erfüllt sein müssen, damit eine Datei dem Suchkriterium entspricht.
$ find . ! -name '*.tex'
.
./buch.toc
./buch.pic.xml
./buch.log
./buch.idx
./upquote.sty
./find_old_image_files.sh
./buch.ps
./buch.ind
./buch.ivz
./buch.dvi
./buch.ivz.xml
…
Listing 10.39 Alle Dateien suchen, die nicht auf .tex enden
Sollen mehrere Bedingungen erfüllt sein (logisches Und), werden diese einfach nebeneinandergeschrieben oder durch -a (wahlweise auch -and) getrennt. Das obige logische Nicht kann auch mittels -not angegeben werden und ein logisches Oder formulieren Sie mit -o und -or. Einzelne Kriteriengruppen werden durch Klammern voneinander getrennt. 10.8.2 Festlegung einer Aktion find bietet die Möglichkeit, mit den gefundenen Dateien bestimmte
Aktionen durchzuführen, die hinter den Suchkriterien festgelegt werden (siehe Tabelle 10.4).
Das Kommando -exec wird mit einigen zusätzlichen Parametern aufgerufen: Der aktuelle Dateiname wird mit geschweiften Klammern signalisiert, und die Kommandos müssen mit Semikola abgeschlossen werden. $ find . -name '*' -exec \
echo "Ist {} nicht ein toller Dateiname???" \;
Ist . nicht ein toller Dateiname???
Ist ./images nicht ein toller Dateiname???
…
Listing 10.40 exec
Parameter
Aktion
-exec [cmd]
Führt das Kommando cmd mit den gefundenen Dateien aus. Dabei steht der Platzhalter {} für gefundene Dateinamen.
-ls
Listet die gefundenen Dateien mit ls lisa auf.
-print
Gibt jeden Fund in einer separaten Zeile aus. Diese Einstellung ist der Standard. -print0 hingegen gibt den Dateinamen mit anschließendem \0 Zeichen aus.
-fls/fprint [Datei]
Diese Kommandos erzeugen eine äquivalente Ausgabe wie -ls bzw. print. Der Unterschied ist, dass die Ausgabe in die Datei Datei geschrieben wird.
Tabelle 10.4 Mögliche Aktionen
10.9 Zusammenfassung In Ihrem Linux-System können Sie dynamisch Benutzer hinzufügen und entfernen (über adduser und deluser). Des Weiteren können Sie über das jeweilige Paketsystem neue Software installieren und bestehende entfernen sowie updaten. In diesem Kapitel haben Sie auch gelernt, Backups zu erstellen und Dateien zu suchen.
10.10 Aufgaben Legen Sie einen neuen Benutzer an
Legen Sie den Benutzer linus an und vergeben Sie ein Passwort. Ändern Sie anschließend das Passwort des Benutzers und löschen Sie den Benutzer wieder. Installieren Sie zwei Programme
Installieren Sie auf Ihrem Linux-System mithilfe des jeweiligen Paketmanagers die Spiele nethack und supertux. Probieren Sie diese beiden äußerst unterschiedlichen Spiele aus und deinstallieren Sie anschließend dasjenige, das Ihnen weniger gut gefiel.
11 Netzwerke unter Linux »All this stuff was done via FTP
but the web has put a really nice
user interface on it.«
(Dt. »Das wurde alles über FTP erledigt,
doch das Web bot eine tolle Oberfläche dafür.«)
– Jon Postel Wir gehen davon aus, dass Sie selber in der Lage sind, auf einer grafischen Oberfläche wie GNOME oder KDE auf ein Netzwerksymbol zu klicken, ein WiFi-Netzwerk auszuwählen und das entsprechende Zugangspasswort einzugeben. Deshalb werden wir in diesem Kapitel lieber unter die Haube von Linux-Netzwerken schauen und Ihnen Konsolentools zeigen, mit denen Sie auch Systeme ohne Oberfläche konfigurieren können.
11.1 Etwas Theorie Zuallererst kommt wie so oft die Theorie vor der Praxis und damit vor dem Spaß. Und leider ist gerade beim Thema Netzwerk die Theorie sehr wichtig, da man sonst wesentliche Fachbegriffe und Zusammenhänge einfach nicht kennt und nicht weiterkommt, wenn ein Problem auftaucht. Der Sinn eines Netzwerks ist klar: Man will zwei oder mehr Rechner miteinander verbinden, um Daten auszutauschen oder um es einzelnen Rechnern zu ermöglichen, Dienste in Anspruch zu nehmen. Um so eine Verbindung bereitzustellen, braucht man
natürlich zuerst eine physikalische Verbindung wie ein Kabel oder Funkhardware. In diesem Buch soll aber nicht thematisiert werden, wie Sie Ihr Netz anständig verkabeln, wir möchten uns lieber auf die Konfiguration Ihres Heimnetzwerks unter Linux konzentrieren. 11.1.1 TCP/IP
Damit sich zwei Systeme in einem Netzwerk unterhalten können, müssen sie – umgangssprachlich ausgedrückt – dieselbe Sprache sprechen. Und die Sprache der Netzwerke ist heutzutage TCP/IP. Das Transmission Control Protocol/Internet Protocol ist eigentlich eine Protokollfamilie, die aus vielen einzelnen Protokollen besteht. Wir würden hier gern auf jedes einzelne Protokoll im Detail eingehen und mit dem entsprechenden technischen Hintergrund vor allem den kreativen Umgang mit der Technik näher beschreiben, jedoch schreiben wir ein Buch über Linux und nicht einen technischen Fortsetzungsroman oder die »unendliche Geschichte«. Daher werden wir uns auf das Nötigste beschränken. Damit man also einen Rechner in einem Netz finden kann, braucht man eine Art Adresse. Eine solche Adresse nennt man bei TCP/IP eine IP-Adresse. Diese IP-Adresse (meist nur kurz »IP« genannt) ist klassisch eine Kombination aus vier durch einen Punkt getrennten Nummern von 0 bis 255 (das entspricht jeweils einem Byte), also etwa 1.2.3.4 oder 192.168.21.131. Man nennt eine solche »klassische« IP-Adresse auch eine IPv4-Adresse. Allerdings gehen schon länger die IP-Adressen aus. Wir haben knapp 2564 mögliche IP-Adressen, also über den Daumen gepeilt etwa 4,3 Milliarden Stück. Und sie reichen trotzdem nicht: Bei etwa 9 Milliarden Menschen auf der Erde, von denen zumindest viele einen Internetzugang mit mehreren Geräten (Computer, Smartphones,
smarte Geräte, Fahrzeuge etc.) haben, und mit unzähligen Servern und IT-Geräten in Unternehmen und staatlichen Organisationen benötigen wir schlicht deutlich mehr Adressen. Adressen haben eine ganz besondere Eigenschaft, die sie erst zu Adressen, so wie wir sie verstehen, macht: ihre Eindeutigkeit. Und so muss auch im Internet, dem größten TCP/IP-Netzwerk der Welt, diese Eindeutigkeit gegeben sein. Nun ist es aber so, dass in den Anfängen des Internets mit IP-Adressen nur so um sich geworfen wurde. Firmen und Universitäten bekamen sie gleich blockweise und irgendwann im großen Dotcom-Boom um die Jahrtausendwende wurden sie halt knapp. So steht uns – allerdings von der Öffentlichkeit eher unbeachtet – nach dem Jahr-2000-Problem nun der nächste größere technische Salat bevor, nur weil uns wieder einmal ein paar Nummern fehlen. Aber keine Angst, die besten Ingenieure und Informatiker der Welt haben schon einen Nachfolger für IP Version 4 entwickelt – IP Version 6, kurz IPv6. Dort sehen die Nummern leider weniger übersichtlich aus, da die Adressen viermal so zahlreich sind. Aber dafür reichen die IP-Adressen hoffentlich für die nächsten 100 Jahre, denn es handelt sich um 340 Sextillionen (also 2128 oder 3,4 × 1038) Adressen. Und mit ein bisschen Glück läuft die ganze Umstellung auch noch reibungslos ab. Immerhin soll nun auch jeder Kühlschrank, jedes Handy und jeder Toaster eine eigene Adresse erhalten können. Aber vom weltweiten Netz zurück zu unserem Heimnetzwerk – das Internet braucht uns jetzt erst einmal nicht zu kümmern, denn dort bekommen Sie für die Zeit Ihrer Einwahl von Ihrem Provider sozusagen eine offizielle IP vermietet und automatisch zugeteilt.
Damit es aber nicht zu Konflikten zwischen internen, sprich privaten IP-Adressen und den offiziellen des Internets kommt, wurden einige nicht offizielle, private Bereiche definiert, die jeder für sein eigenes, abgeschlossenes LAN zu Hause oder in der Firma benutzen kann: Adressbereich
Menge
10.0.0.0–10.255.255.255
ca. 16 Mio. Adressen
172.16.0.0–172.31.255.255
ca. 1 Mio. Adressen
192.168.0.0–192.168.255.255 ca. 65.500 Adressen Tabelle 11.1 Nicht offizielle IP-Bereiche
Dazu kommt der Adressbereich 127.0.0.1–127.255.255.255, der nur für die lokale Kommunikation im Rechner benutzt wird. So bezeichnet die IP 127.0.0.1 den localhost und demnach den eigenen Rechner. Somit kann man auch etwas mit TCP/IP spielen, ohne gleich ein echtes Netzwerk zu haben. Nun wird ein IP-Netzwerk in der Regel keine hunderttausend Rechner umfassen, oftmals sind es sogar weniger als zehn. Daher gibt es sogenannte Netzmasken, auch Subnetzmasken genannt. Sie legen einen binären Wert in der Länge einer IP-Adresse fest. In dieser Maske identifizieren alle Bits der IP, die in der Maske auf 1 gesetzt sind, den Netzwerkteil. Also ist eine Netzmaske sinnvollerweise so aufgebaut, dass bis zu einem gewissen Punkt nur Einsen und ab dann nur Nullen gesetzt sind. Da binäre Zahlen aber sehr lang sind, schreibt man die Netzmaske in der Regel dezimal. 11.1.2 Ihr Heimnetzwerk
Nehmen wir an, in Ihrem Heimnetzwerk sollen 254 Rechner untergebracht werden können (maximal), dann könnten Sie das Netzwerk 192.168.0.0/24 verwenden, was sich auch als 192.168.0.0/255.255.255.0 schreiben ließe. Diese Konfiguration wird von den meisten Internet-Routern, die man zu Hause stehen hat, verwendet. Es gibt leichte Abweichungen, beispielsweise Netzwerke wie 192.168.1.0/24 oder 192.168.2.0/24, aber die Berechnung und Netzwerkgröße sind immer dieselben. Oftmals muss dazu gar nichts mehr konfiguriert werden. Hier dennoch die Erklärung: Damit ersichtlich wird, wie viele Hosts im Netzwerk untergebracht werden können, schreiben wir zunächst die Netzwerkadresse und die Netzmaske untereinander. Netzwerkadresse: Netzmaske:
192.168.000.000
255.255.255.000
Listing 11.1 Netzmaske anwenden
Da der Wert 255 dem Binärwert 11111111 entspricht, sind folglich nur die letzten 8 Bits der 32 Bit umfassenden Netzmaske auf 0 gesetzt. Somit umfasst das Netzwerk den Adressbereich 192.168.0.0 bis 192.168.0.255 (weil sich nur die letzten 8 Bits ändern können), wobei die erste Adresse (also 192.168.0.0) die Netzwerkadresse selbst und die letzte Adresse (also 192.168.0.255) die Broadcastadresse darstellt. Eine Nachricht, die an die Broadcastadresse geschickt wird, empfangen schlicht alle Rechner des Netzwerkes. Alle übrigen IPAdressen sind solche, die für die Rechner im Netzwerk zur Verfügung stehen: 192.168.0.1 bis 192.168.0.254. Es können also maximal 254 Rechner in diesem Netzwerk adressiert werden. Das alles wirkt jetzt sicher recht verwirrend auf Sie. Aber sicherlich werden Sie einsehen, dass ohne exakt geregelte Protokolle keine Kommunikation zustande kommen kann. Und zudem werden Sie mit Netzwerken sicherlich nicht so viel zu tun bekommen, es sei
denn, Sie erlegen es sich mit Ihrem Heimnetzwerk selbst auf. Dort kann man sich sehr gut mit den nicht offiziellen Standardadressen behelfen, die bei vielen Konfigurationstools schon voreingestellt sind. Und wenn die Hersteller aus guter Absicht nicht alles verkomplizieren, indem sie blumige Umschreibungen für die oben genannten technischen Fakten liefern, können Sie nach der Lektüre dieses Kapitels alle Einstiegshürden meistern. Zu beachten bleibt, dass Rechner von sich aus nur mit Rechnern aus demselben Netzwerk kommunizieren können – da sie sinnvollerweise mehr oder weniger direkt miteinander verkabelt sind. Wie Sie den Rechnern aber sagen können, wo sie andere Netzwerke und damit die entsprechenden Rechner finden, erklären wir in Abschnitt 11.3, »Routing«. Und ins Internet
Wie kommt ein Rechner aber nun »ins Internet«? Damit Ihre Rechner nach erfolgreicher Konfiguration des Heimnetzwerks auch ins Internet können, brauchen Sie natürlich eine Art Tor. Dieses Tor hätte dann die Aufgabe, sich mit dem Internet zu verbinden und die Kommunikation aller weiteren Rechner im Heimnetzwerk ins Internet zu tunneln.
Abbildung 11.1 Schema eines Heimnetzwerks
Diese Aufgabe übernimmt meistens ein sogenanntes Gateway bzw. ein Router – üblicherweise ist dies der Plastikrouter, den Sie von Ihrem Internetanbieter per Post bekommen haben. Diese Geräte übersetzen dann auch die privaten IP-Adressen in offizielle, die Sie von Ihrem Internet-Service-Provider bei der Einwahl erhalten. Mehr zu diesem Thema erfahren Sie in Abschnitt 11.3. In Abbildung 11.1 sehen Sie ein kleines Heimnetzwerk mit privaten IP-Adressen. Ein Gateway stellt die Verbindung ins Internet her und hat neben der privaten noch eine vom Provider zugewiesene offizielle IP. Gute Neuigkeiten! Im Normalfall ist Ihr Linux-System so schlau, sich während der Installation automatisch mit dem Internet zu verbinden, weil es sich entweder über eine Kabelverbindung zum Router automatisch konfiguriert oder dies über eine WLAN-Verbindung geschieht, bei der Sie letztlich nur das WLAN-Passwort eingeben müssen. Für den Fall, dass dies nicht so ist, beschreiben wir in diesem Kapitel, wie Sie Ihre Internet-/Netzwerkverbindung von Hand konfigurieren.
11.2 Konfiguration einer Netzwerkschnittstelle Jede Netzwerkschnittstelle, sei sie physikalisch im Rechner als Netzwerkkarte vorhanden oder nur als logische Repräsentation einer VPN-Verbindung, wird unter Linux durch ein entsprechendes Device, also als Netzwerkschnittstelle, repräsentiert. Um Netzwerkschnittstellen – für die wir uns ja im aktuellen Kapitel besonders interessieren – zu konfigurieren, gibt es mehrere Programme. Klassisch ist dies ifconfig, moderner ist ip. 11.2.1 Welche Netzwerkschnittstellen gibt es?
Eine Liste der vorhandenen Netzwerkschnittstellen liefert ein Aufruf von ip a bzw. ifconfig. Netzwerkschnittstellen haben dabei Bezeichnungen wie lo, eth oder enp sowie – meistens – eine Nummerierung. $ ip a
1: lo: mtu 65536 qdisc noqueue state
UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp0s3: mtu 1500 qdisc
pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:5b:06:a4 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 79459sec preferred_lft 79459sec
inet6 fe80::1322:96e7:63d0:b00b/64 scope link
valid_lft forever preferred_lft forever
3: enp0s8: mtu 1500 qdisc
pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:f8:65:05 brd ff:ff:ff:ff:ff:ff
inet 172.16.0.1/16 brd 172.16.255.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::7ebb:b755:814d:c88d/64 scope link
valid_lft forever preferred_lft forever
$ ifconfig
enp0s3 Link encap:Ethernet Hardware Adresse 08:00:27:5b:06:a4
inet Adresse:10.0.2.15 Bcast:10.0.2.255
Maske:255.255.255.0
inet6-Adresse: fe80::1322:96e7:63d0:b00b/64
Gültigkeitsbereich:Verbindung
UP BROADCAST RUNNING MULTICAST MTU:1500 Metrik:1
RX-Pakete:24714 Fehler:0 Verloren:0 Überläufe:0 Fenster:0
TX-Pakete:13012 Fehler:0 Verloren:0 Überläufe:0 Träger:0
Kollisionen:0 Sendewarteschlangenlänge:1000
RX-Bytes:19404819 (19.4 MB) TX-Bytes:4755563 (4.7 MB)
enp0s8 Link encap:Ethernet Hardware Adresse 08:00:27:f8:65:05
inet Adresse:172.16.0.1 Bcast:172.16.255.255
Maske:255.255.0.0
inet6-Adresse: fe80::7ebb:b755:814d:c88d/64
Gültigkeitsbereich:Verbindung
UP BROADCAST RUNNING MULTICAST MTU:1500 Metrik:1
RX-Pakete:0 Fehler:0 Verloren:0 Überläufe:0 Fenster:0
TX-Pakete:94 Fehler:0 Verloren:0 Überläufe:0 Träger:0
Kollisionen:0 Sendewarteschlangenlänge:1000
RX-Bytes:0 (0.0 B) TX-Bytes:12490 (12.4 KB)
lo Link encap:Lokale Schleife
inet Adresse:127.0.0.1 Maske:255.0.0.0
inet6-Adresse: ::1/128 Gültigkeitsbereich:Maschine
UP LOOPBACK RUNNING MTU:65536 Metrik:1
RX-Pakete:1591 Fehler:0 Verloren:0 Überläufe:0 Fenster:0
TX-Pakete:1591 Fehler:0 Verloren:0 Überläufe:0 Träger:0
Kollisionen:0 Sendewarteschlangenlänge:1
RX-Bytes:337159 (337.1 KB) TX-Bytes:337159 (337.1 KB)
Listing 11.2 Netzwerkschnittstellen anzeigen
Die Schnittstelle lo ist dabei die für den lokalen Rechner, weshalb sie auch die Netzwerkadresse 127.0.0.1 (IPv4) bzw. ::1 (IPv6) aufweist. Die anderen beiden Schnittstellen (enp0s3 und enp0s8) sind Ethernet-Netzwerkkarten, die die IP-Adressen 10.0.2.15 bzw. 172.16.0.1 besitzen. Auch werden die Subnetzmasken und die Broadcastadressen angezeigt (/24 brd 10.0.2.255 bedeutet, dass die ersten 24 Bits der Subnetzmaske auf 1 gesetzt sind und die Broadcastadresse 10.0.2.255 ist). Die restlichen Parameter sind zunächst nicht so wichtig.
Ersichtlich sind außerdem die MAC-Adressen (also die Hardwareadressen, die für die Kommunikation auf einer unteren Ebene verwendet werden), zum Beispiel 08:00:27:f8:65:05. Wichtig ist zudem die Information, dass die Schnittstelle aktiviert (UP) ist, dass die Karte Multicast (MULTICAST) und Broadcast (BROADCAST) unterstützt und dass hier die MTU (Maximal Transmission Unit), also die maximale Größe eines Pakets, 1.500 Byte beträgt. ifconfig liefert in den letzten paar Zeilen noch Statistiken, die
ausführlich Auskunft über empfangene und gesendete Pakete geben. Die Ausgabe schließt mit der Angabe des Interrupts und der Basisadresse der Karte. Diese Informationen sind wichtig für die Hardwarekonfiguration Ihres Computers. Das ist aber nicht das Thema dieses Buches, also ignorieren Sie ruhig alles, was Sie nicht verstehen. 11.2.2 Konfiguration von Netzwerkkarten mit ip und ifconfig
Sollte Ihre Netzwerkkarte noch nicht (wie etwa im obigen Beispiel zu sehen) automatisch konfiguriert sein, können Sie von Hand nachhelfen, um dies zu ändern. Normalerweise benutzt man ip bzw. ifconfig, wenn man eine Netzwerkkarte für das eigene LAN fit machen will. Für einen solchen Einsatz reicht für gewöhnlich ein einziger Aufruf des Programms: # ip addr add 10.0.2.15/24 dev enp0s3
Listing 11.3 Konfigurieren einer Netzwerkschnittstelle mit ip # ifconfig enp0s3 10.0.2.15 netmask 255.255.255.0
Listing 11.4 Konfigurieren einer Netzwerkschnittstelle mit ifconfig
Wir weisen der Schnittstelle enp0s3 (das ist die lokale Netzwerkkarte) die IP-Adresse 10.0.2.15 zu. Die Netmask (Netzmaske) wird mit
255.255.255.0 angegeben, d. h., dass unser Netz über keine weiteren Subnetze verfügt. Wir können eine Netzwerkschnittstelle auch wieder aus dem Betrieb nehmen, indem wir ip mit dem Parameter down aufrufen. Die Inbetriebnahme erfolgt mit dem Parameter up. # ip link set dev enp0s3 down
# ip link set dev enp0s3 up
Listing 11.5 Netzwerkschnittstelle enp0s3 (de)aktivieren
Wichtig ist außerdem, dass wir einer Schnittstelle mitteilen, über welchen Router standardmäßig in andere Netzwerke gesendet werden soll (siehe Abschnitt 11.3). Zu beachten bleibt jetzt lediglich, dass ip mögliche Änderungen bei einem Neustart wieder vergessen hat. MAC-Adressen
Wie bereits erwähnt, besitzen Netzwerkschnittstellen auch die oben erwähnte Hardware- oder MAC-Adresse. Bei der Auflistung der Schnittstellen zeigt ip diese nebst anderen Informationen für uns an. Dabei liefert a (oder address) sämtliche Adressen zu allen Schnittstellen. # ip a
...
2: wlan0: mtu 1500 qdisc
mq state UP group default qlen 1000
link/ether 5c:51:4f:d0:3f:7c brd ff:ff:ff:ff:ff:ff
inet 192.168.2.106/24 brd 192.168.2.255 scope global wlan0
valid_lft forever preferred_lft forever
inet6 fe80::5e51:4fff:fed0:3f7c/64 scope link
valid_lft forever preferred_lft forever
Listing 11.6 MAC-Adresse erfragen
In obigem Fall wäre die Hardwareadresse unserer WiFi-Schnittstelle (wlan0) die 5c:51:4f:d0:3f:7c. Das heißt, dass diese MAC-Adresse einer Schnittstelle mit der oben ebenfalls angezeigten IP-Adresse 192.168.2.106 zugeordnet ist. Möchten wir nun wissen, wie die MACAdressen der Rechner heißen, mit denen unser Rechner wiederum im lokalen Netzwerk kommuniziert (und welche IP-Adressen diese nutzen), so können wir ip neighbour aufrufen: # ip neighbour
192.168.2.1 dev wlan0 lladdr 4c:09:d4:a5:00:34 STALE
Listing 11.7 Dem eigenen Rechner bekannte Hardwareadressen ausgeben lassen
Unser Rechner kommuniziert also mit einem Rechner, der die IP 192.168.2.1 verwendet. Diesem Rechner gehört wiederum die Hardwareadresse 4c:09:d4:a5:00:34. Verwendet wird für die Kommunikation mit diesem Rechner erneut die WiFi-Schnittstelle. Konfiguration wieder löschen
Um die Konfiguration einer Schnittstelle wieder auf null zurückzusetzen, verwenden Sie den Parameter flush zusammen mit der Netzwerkschnittstelle. Anschließend taucht die IP-Adresse nicht mehr auf, wie hier am Beispiel der Ethernetschnittstelle eth0 zu sehen ist: # ip addr flush eth0
# ip a
...
2: eth0: mtu 1500 ...
link/ether 08:00:27:2f:90:22 brd ff:ff:ff:ff:ff:ff
Listing 11.8 IP-Adresskonfiguration löschen
Nach dem Neustart ist alles weg?
Schade ist nur, dass Linux diese Einstellungen beim nächsten Reboot wieder vergessen hat. Es gibt verschiedene Möglichkeiten, dem entgegenzuwirken, beispielsweise indem Sie den ip-Aufruf in ein Startskript unter /etc einbauen, ein mit der Distribution mitgeliefertes Tool verwenden oder andere spezifische Konfigurationsmöglichkeiten nutzen. Letztendlich gibt es aber auch bei Distributionen keine Zauberei und alles landet wieder bei ip und irgendwelchen Skripten – nur vor Ihren Augen versteckt. Unter openSUSE findet die Konfiguration im Wesentlichen im Verzeichnis /etc/sysconfig/network (samt Unterverzeichnissen) statt. Bei Ubuntu wird /etc/network dafür verwendet. Unter anderen Distributionen heißen die Verzeichnisse und Dateien ähnlich und die Konfiguration gestaltet sich meist einfacher als beim Aufruf der Tools von Hand. Unter Ubuntu können Sie zum Beispiel die Datei /etc/network/interfaces editieren. Eine statische Konfiguration sähe wie folgt aus: auto eth1
iface eth1 inet static
address 172.16.0.1
netmask 255.255.255.0
broadcast 172.16.0.255
Listing 11.9 Ein Beispiel für /etc/network/interfaces
Beim Booten wird hier automatisch die Schnittstelle eth1 aufgesetzt, und zwar mit einer statischen (also nicht dynamisch zugewiesenen) IP-Adresse. Wir legen die weiteren Parameter wie IPAdresse, Netzmaske und Broadcastadresse ebenfalls fest. Detailkonfiguration
Nun macht lesen aber nur halb so viel Spaß wie selbst ausprobieren! Gesetzt den Fall, Sie wissen, was Sie tun, oder Sie wissen zumindest, wie Sie später den Grundzustand wiederherstellen (beispielsweise durch einen Neustart), bietet Ihnen ip alle Möglichkeiten zur Manipulation. Wenn Sie möchten, können Sie zum Beispiel Ihrer Netzwerkkarte mehr als eine IP-Adresse gleichzeitig geben. Linux bietet Ihnen nämlich die Möglichkeit, mehrere virtuelle Netzwerkkarten zu konfigurieren. Hängen Sie dazu beispielsweise an Ihre Schnittstelle einen Doppelpunkt und die Nummer der Konfiguration an: # ip addr add 192.168.1.1/24 dev enp0s8:1
Listing 11.10 Eine Netzwerkkarte mit zwei IPv4-Adressen
Das führt dazu, dass bei Aufruf von ip a show plötzlich eine neue IP-Adresse hinzugekommen ist: # ip a show enp0s8
3: enp0s8: mtu 1500 qdisc
pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:f8:65:05 brd ff:ff:ff:ff:ff:ff
inet 172.16.0.1/16 brd 172.16.255.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet 192.168.1.1/24 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::7ebb:b755:814d:c88d/64 scope link
valid_lft forever preferred_lft forever
Listing 11.11 Beispielausgabe von ip
Jetzt besitzt Ihre Schnittstelle zwei IP-Adressen in separaten Netzwerken. Beachten Sie aber bitte, dass die Karte immer noch nur über ein Kabel mit der Außenwelt verbunden ist. Jetzt fragen Sie sicherlich, wofür man solche Spielereien überhaupt braucht. Gute Frage! Aber in Unternehmen gibt es manchmal Situationen, die es notwendig machen, die IT-Infrastruktur umzustellen. Zwei Server werden dann zum Beispiel zu einem vereinigt, weil die Hardware
gerade so billig ist und die Leistung ausreicht, um beide Dienste zu betreiben. Jetzt haben Sie aber das Problem, dass auf allen Clients im Unternehmen noch die alten IP-Adressen der beiden ursprünglichen Server eingestellt sind. Sie könnten sich natürlich an die Arbeit machen und Tausende PCs umkonfigurieren oder Sie nutzen bei Ihrem neuen Server einfach die Möglichkeit, ihm zwei IPAdressen statt einer zu geben. Selbstverständlich lässt sich auch die MTU festlegen: # ip link set dev enp0s8 mtu 500
Listing 11.12 Die Maximum Transfer Unit herabsetzen
Dieser Befehl setzt die maximale Paketgröße (MTU) für die Netzwerkschnittstelle enp0s8 von 1.500 Byte auf 500 Byte herab. Alle Pakete, die diese Schnittstelle passieren, werden jetzt in kleinere Pakete aufgeteilt. Überprüfen lässt sich die Änderung natürlich auch: # ip link list
...
3: enp0s8: mtu 500 qdisc
pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 08:00:27:f8:65:05 brd ff:ff:ff:ff:ff:ff
Listing 11.13 Überprüfen der neuen MTU
11.2.3 Automatische Konfiguration: DHCP
Eine andere Möglichkeit, eine Netzwerkkarte ganz einfach zu konfigurieren, ist DHCP, eine neuere und erweiterte Version von BOOTP. Wenn Sie DHCP nutzen, brauchen Sie im Allgemeinen gar nichts von Hand einzustellen – alles geht nämlich automatisch. Der Nachteil ist natürlich, dass Sie einen DHCP-Server im Netzwerk brauchen. Diesen Server, der Ihrem Rechner die notwendigen Informationen zur Konfiguration gibt, müssen Sie nicht kennen.
Vereinfacht gesagt schicken Sie nur einen großen Hilferuf ins Netzwerk – also über das Kabel, das an Ihrer Netzwerkkarte angeschlossen ist. Der DHCP- Server fühlt sich dann angesprochen und schickt Ihnen die Daten zu. So wird Ihnen beispielsweise automatisch eine freie IP-Adresse zugewiesen und es wird Ihnen gesagt, wo lang es ins Internet geht. Die meisten Distributionen lassen Ihnen bei der Installation die Wahl zwischen dem automatischen Beziehen der IP-Adresse mittels DHCP oder der bereits vorgestellten statischen Methode. Da Sie meistens nur in größeren fremden Netzwerken, wie es z. B. Firmennetzwerke sind, mit DHCP zu tun haben werden, wollen wir nicht im Detail auf DHCP eingehen. dhcp-client
Das Programm dhcp-client ermöglicht die automatische Konfiguration einer Netzwerkschnittstelle mit DHCP: # dhcp-client -i eth1
Listing 11.14 eth1 mittels dhcp-client konfigurieren lassen
Statt dhcp-client verwenden manche Distributionen das Tool dhclient, das ähnlich funktioniert.
11.3 Routing Mit der Zuweisung der entsprechenden IP-Adressen kann, aber muss unser Netzwerk nicht notwendigerweise richtig konfiguriert sein. Ein wichtiger Aspekt der Netzwerkkonfiguration ist das Routing und das eröffnet aufgrund der vielfältigen Möglichkeiten, die Linux bietet, eine hübsche Spielwiese – nicht nur für alle technikbegeisterten Freaks. 11.3.1 Was ist Routing?
Sie wissen bisher, dass Rechner in einem Netzwerk IP-Adressen haben. Was Sie aber nicht wissen, ist, wie die Kommunikation von Netzwerk zu Netzwerk funktioniert – woher sollen denn die Rechner wissen, wohin sie ihre Pakete schicken sollen, wenn die Adresse nicht im eigenen Netz liegt? Die Antwort ist einfach: Sie wissen es nicht. Man muss es ihnen sagen, und zwar durch das Routing. Prinzipiell kann man Routing statisch oder dynamisch organisieren. Dynamisches Routing basiert auf diversen Routing-Protokollen, die auch im TCP/IP-Protokollpaket enthalten sind. Allerdings sind diese Protokolle eher für große Netzwerke gedacht, in denen beispielsweise ein Ausfall von einzelnen Routern automatisch überbrückt werden muss. Aus diesem Grund möchten wir uns nur mit statischem Routing befassen, bei dem jedem Rechner durch die Konfiguration gesagt wird, wie er in andere Netze kommt. Um zu wissen, wohin welches Paket geschickt werden muss, hat jeder Rechner eine sogenannte Routing-Tabelle. Eine RoutingTabelle enthält als wichtigste Spalten das jeweilige Ziel, das ein einzelner Rechner oder ein ganzes Netz sein kann, das
Netzwerkinterface, über das die Pakete gesendet werden müssen, und eventuell einen sogenannten Router bzw. ein Gateway, der bzw. das sich dann weiter um die Pakete kümmert. So ein Gateway ist immer dann im Einsatz, wenn zwei Netze miteinander verbunden werden sollen. Dabei ist dieses Gateway nur ein Rechner, der in beiden Netzen vorhanden ist. Will ein Rechner aus Netz A zu einem Rechner X in einem anderen Subnetz Kontakt aufnehmen, schickt er seine Pakete also zu einem Gateway. Dieses Gateway ist in beiden Netzen präsent und leitet die Pakete einfach nur weiter. Natürlich müssen Sie nicht für jedes einzelne Netzwerk einen Eintrag in der Routing-Tabelle hinzufügen. Sie können auch einfach ein sogenanntes Standard-Gateway angeben, zu dem alle Pakete geschickt werden, für die sonst keine Regel existiert. Das ist vor allem dann sinnvoll, wenn Sie für Ihr kleines LAN einen gemeinsamen Internetzugang konfigurieren möchten – dann hat das Standard-Gateway eine Verbindung zum Netz und leitet für Ihr gesamtes internes Netz die Pakete weiter. Natürlich könnten Sie auch einen Hardwarerouter für diese Aufgabe kaufen. Aber warum sollten Sie unnötig Geld bezahlen? 11.3.2 Der Befehl ip route
Der Standardbefehl für das Arbeiten mit der Routing-Tabelle ist mal wieder ip. Wir übergeben zunächst den Parameter route: # ip route
default via 10.0.2.2 dev eth0 proto dhcp metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
Listing 11.15 Die Routing-Tabelle
Hier sehen Sie, dass wir das Netz 10.0.2.0 direkt (kein Gateway) über das Interface eth0 erreichen. Für alles andere schicken wir die Pakete auch über eth0 zu 10.0.2.2 und vertrauen darauf, dass dieser Rechner die Pakete ordnungsgemäß weiterleiten wird. Anders formuliert ist 10.0.2.2 unser Standard-Gateway. Dabei ist zu beachten, dass das Netz, in dem das Standard-Gateway ist, auch erreicht werden kann! Es nützt nichts, wenn Sie ein schönes Gateway installiert und konfiguriert haben und die anderen Rechner zwar wissen, dass es dieses Gateway gibt, es allerdings nicht erreichen können! Daher ist in diesem Beispiel der erste Eintrag wichtig. Jetzt schauen wir uns aber einmal an, wie wir eine Routing-Tabelle überhaupt aufbauen. Dazu nehmen wir an, dass wir das Netzwerk 172.20.0.0/16 über die Netzwerkkarte, die mit eth0 bezeichnet wird, erreichbar machen möchten. Dazu legen wir auch fest, dass das Netzwerk direkt über diese Schnittstelle erreichbar ist: # ip addr add 172.20.0.1/16 dev eth0
# ip route add 172.20.0.0/16 dev eth0
Listing 11.16 Füllen einer Routing-Tabelle
Wir überprüfen unsere Änderungen – es sollte eine neue Route im Routing sichtbar sein. # ip route
default via 10.0.2.2 dev eth0 proto dhcp metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
172.20.0.0/16 dev eth0 scope link
172.20.0.0/16 dev eth0 proto kernel scope link src 172.20.0.1
Listing 11.17 Überprüfen der aktualisierten Routing-Tabelle
Es hat funktioniert: Unser Rechner weiß nun, dass wir das Netzwerk 172.20.0.0/16 direkt über die Schnittstelle eth0 erreichen können. Den Standard-Gateway festlegen
Möchten wir unserem Rechner nun sagen, welcher Rechner eines Netzwerks der Standard-Gateway ist, so können wir auch dies mit ip erledigen: # ip route add default gw 172.20.0.10 dev eth0
Listing 11.18 Den Standard-Gateway festlegen
Routen löschen
Bleibt noch zu klären, wie man eine Route wieder aus der Tabelle löschen kann. Das ist ganz einfach: # ip route del 172.20.0.0/16
Listing 11.19 Löschen einer Route
11.4 Netzwerke benutzerfreundlich – DNS Nun haben wir Sie lange genug gequält und können endlich mit den guten Nachrichten herausrücken. Es ist natürlich absolut unpraktikabel, sich IP-Adressen als Adressen für Rechner zu merken, und außerdem hatten Sie sicherlich bisher eher mit Rechnernamen der Form www.ichbineineSeite.com zu tun. 11.4.1 DNS
Für eine solche Übersetzung von leicht eingängigen und leicht zu merkenden Namen in computerverständliche IP-Adressen ist der DNS-Dienst zuständig. DNS steht für Domain Name Service und muss natürlich für das Internet zentral verwaltet werden. Natürlich können Sie DNS auch in Ihrem kleinen Heimnetzwerk einsetzen und Ihren Computern Namen wie Jupiter geben. Ebenso haben Netze einen Namen und alles zusammen wird durch Punkte getrennt. Anders als bei IP-Adressen steht der Rechnername dabei vor dem Netzwerkbezeichner. Der Rechner www.ichbineineSeite.com ist demnach der Rechner www im Netz ichbineineSeite.com. Dem Netzwerknamen ist dabei noch eine sogenannte TLD, Top-Level Domain, zugeordnet, die definiert, wer für die Verwaltung des DNS in dieser Zone des Internets verantwortlich ist. Die bekannten Domains .com, .net und .org sind für internationale, kommerzielle bzw. gemeinnützige Seiten gedacht. Deutsche Seiten gehören demnach in die von der DENIC verwaltete .de-Hierarchie. Über alles hält die mächtige ICANN (Internet Corporation for Assigned Names and Numbers) ihre Hand, ein leider ziemlich amerikanisch dominiertes Gremium, das in allen Fragen das letzte Wort hat. Die
ICANN entscheidet zum Beispiel, welche Firma die Hauptregistrierung für die lukrativen .com-Domains bekommt. Aber wie funktioniert DNS nun wirklich? Wir haben schon angedeutet, dass in diesem System ziemlich zentralistische Strukturen herrschen. In der Tat werden die Informationen von sogenannten Rootservern an alle weiteren untergeordneten DNSServer verteilt. Die Oberhoheit über diese Server hat natürlich auch die ICANN und wer aus ihnen entfernt wird, ist nicht mehr im Internet zu finden. Wenn Sie nun www.ichbineineSeite.com laden möchten, müssen Sie also erst einmal einen DNS fragen, welche IP-Adresse eigentlich zu diesem Namen gehört. Im Normalfall wird Ihnen ein solcher Server von Ihrem Provider automatisch nach der Einwahl ins Internet zugeteilt. Haben Sie die IP bekommen, so können Sie ganz normal, wie Sie es gelernt haben, über TCP/IP mit diesem Rechner in Verbindung treten. Namenskonventionen Es gibt gewisse Konventionen, wie man Server für bestimmte Dienste nennen sollte. So heißen Webserver beispielsweise www, FTP-Server schlicht ftp, Mailserver zum Versenden mail bzw. smtp und Mailserver zum Abholen von Mails pop bzw. imap. Hält man sich an diese Konventionen, fällt es leicht, sich in einem solchen Netzwerk zurechtzufinden.
In einem kleinen Netzwerk kann man dann auch die Möglichkeit nutzen, einem Rechner mehrere Namen zu geben. Und in einem größeren Netzwerk ist es bei einer solchen Namensgebung egal, ob ein Server irgendwann einmal umzieht – eine kurze Änderung im DNS, und die Clients finden ihren Server wieder. Zudem nutzen
viele Firmen diese Möglichkeit für sogenanntes Loadbalancing, bei dem die Last auf mehrere Server gleichmäßig verteilt wird. So kann es sein, dass man, wenn man www.mustersite.xyz eingibt, auf www43.mustersite.xyz landet. 11.4.2 DNS und Linux
Sollte Ihnen kein DNS-Server von Ihrem Provider zugeteilt worden sein, müssen Sie ihn ein einziges Mal vor Ihrer ersten Verbindung ins Netz festlegen. Dazu passen Sie einfach die Datei /etc/resolv.conf an, die diese Information speichert. Listen mit schnellen DNSServern gibt es im Internet oder Sie fragen einfach Ihren Provider nach seinen eigenen. $ cat /etc/resolv.conf
domain wg
nameserver 212.185.248.212
Listing 11.20 Der Nameserver in der /etc/resolv.conf
Es ist natürlich einleuchtend, dass man einen DNS-Server mit seiner IP-Adresse angeben sollte – ansonsten hat man wieder ein schönes Beispiel für das Henne-Ei-Problem. Wenn Sie nun für Ihr kleines Heimnetzwerk auch Namen verwenden wollen, können Sie natürlich einen DNS-Server aufsetzen. Im Normalfall wäre das allerdings ein absoluter Overkill und viel zu kompliziert. Viel einfacher ist es doch, Linux zu sagen, welche Namen man gern für Netzwerke und Rechner vergeben möchte. $ cat /etc/networks
# This file describes a number of netname-to-address
# mappings for the TCP/IP subsystem. It is mostly
# used at boot time, when no name servers are running
loopback 127.0.0.0
beispiel 192.168.1.0
# End.
Listing 11.21 Netzwerke selbst definieren: /etc/networks
Die entsprechende Datei für Netzwerke ist also die /etc/networks. In sie schreiben Sie nur den gewünschten Namen sowie das gewünschte Netz. Damit sind Sie fertig. $ cat /etc/hosts
#
# Syntax:
#
# IP-Address Fully-Qualified-Hostname Short-Hostname
#
127.0.0.1 localhost
# special IPv6 addresses
::1 localhost ipv6-localhost ipv6-loopback
fe00::0 ipv6-localnet
ff00::0 ipv6-mcastprefix
ff02::1 ipv6-allnodes
ff02::2 ipv6-allrouters
ff02::3 ipv6-allhosts
172.20.2.1 johannes.wg johannes
Listing 11.22 Und das Ganze mit Rechnern: /etc/hosts
Die Konfigurationsdatei für Hosts, die /etc/hosts, sieht dagegen auf den ersten Blick komplizierter aus. Das ist aber nur der Fall, da teilweise schon IPv6-Adressen angegeben wurden. Ansonsten haben wir nur die Rechner localhost und johannes definiert. Die Syntax ist auch hier wieder selbsterklärend: Zuerst kommt die IPAdresse und dann folgen die gewünschten Namen. Jetzt könnten wir statt 172.20.2.1 auch johannes oder johannes.wg schreiben, wenn wir den entsprechenden Rechner meinen. Dieses schöne System ist allerdings kein Dienst und damit nur lokal verfügbar. Das heißt, dass Sie auf allen Rechnern im System eine solche Datenbasis pflegen müssten.
Nun gibt es aber ein Problem – die Reihenfolge. Was hat nun Priorität: der DNS des Providers, die lokalen hosts- und networksDateien oder gar ein eventuell vorhandenes NIS-System? Für diese Probleme gibt es eine Lösung: die Datei /etc/nsswitch.conf. $ cat /etc/nsswitch.conf
passwd: compat
group: compat
hosts: files dns
networks: files dns
services: files
protocols: files
rpc: files
ethers: files
netmasks: files
netgroup: files
publickey: files
bootparams: files
automount: files nis
aliases: files
Listing 11.23 Bringt Ordnung ins Chaos: /etc/nsswitch.conf
Uns interessiert eigentlich der Eintrag für hosts und networks: Es soll zuerst lokal gesucht werden, dann erst im DNS des Providers. Das ist im Normalfall eigentlich immer sinnvoll und daher auch eine Voreinstellung. Die anderen Optionen in der Datei tragen einfach dem Fakt Rechnung, dass man noch viel mehr solcher Daten per Netzwerkdienst verteilen kann als nur diese beiden Dateien. 11.4.3 Windows und die Namensauflösung
Nun gibt es auch unter Windows die Möglichkeit, Rechnern im Netzwerk Namen zu geben. Die Windows-Namensgebung funktioniert noch einmal völlig anders, nämlich über das sogenannte NETBIOS-System. Als Laie kann man sich ganz einfach
vorstellen, dass Microsoft in diesem Fall das Rad einfach noch einmal erfunden hat,[ 30 ] mit dem Unterschied, dass das Rad diesmal nur mit anderen Rädern des Herstellers optimal funktioniert und sich leider nur in eine Richtung drehen kann – aber nur bei gutem Wetter. NETBIOS setzt zwar auf TCP/IP auf, macht sonst aber sein eigenes Ding. Sollten Sie wirklich ernsthaft in Erwägung ziehen, diese Namensgebung zu nutzen, müssen Sie das sogenannte SambaPaket installieren. Damit haben Sie die Möglichkeit, auf freigegebene Windows-Laufwerke zuzugreifen und eben die Namensgebung zu nutzen. In diesem Fall können Sie in die /etc/nsswitch.conf auch noch winbind als Quelle für entsprechende Namensinformationen eintragen. Mehr Hinweise zu Samba finden Sie in Abschnitt 11.8, »Windows-Netzwerkfreigaben«. 11.4.4 Die Datei /etc/services
Sie kennen bereits IP-Adressen und Hostnames. Was fehlt uns noch, damit beispielsweise ein Webbrowser sich mit einem Webserver verbinden kann? Nun, ein Webbrowser ist ein TCP-Client (also ein TCP-Dienstnutzer) und der Webserver ein TCP-Server (also ein Anbieter eines TCP-Dienstes). Damit ein TCP- bzw. UDP-Client sich mit einem TCP- bzw. UDP-Server verbinden kann, muss er dessen entsprechenden Port kennen. Aus diesem Grund sind bestimmte Dienste einzelnen Ports zugeordnet. Ein Client wird also für eine Webseite zuerst Port 80 für HTTP (bzw. 443 für HTTPS) probieren. In der /etc/services sind dafür entsprechende Dienste und ihre Ports definiert. Diese Datei wird unter anderem von vielen Programmen genutzt, die kryptische Ausgaben, die Netzwerke betreffen, etwas freundlicher gestalten wollen.
$ cat /etc/services
…
tcpmux 1/tcp echo 7/tcp
echo 7/udp
…
ssh 22/tcp …
smtp 25/tcp mail smtp 25/udp mail …
finger 79/tcp finger 79/udp http 80/tcp http 80/udp www 80/tcp www 80/udp www-http 80/tcp www-http 80/udp …
# TCP port service multiplexer
# SSH Remote Login Protocol
# Simple Mail Transfer
# Simple Mail Transfer
# # # # # # # #
Finger
Finger
World Wide World Wide World Wide World Wide World Wide World Wide
Web Web Web Web Web Web
HTTP
HTTP
HTTP
HTTP
HTTP
HTTP
Listing 11.24 Standarddienste und ihre Ports in der /etc/services
11.5 Firewalls unter Linux Eine Firewall ist ein Programm, das eingehenden und ausgehenden Datenverkehr eines Rechners filtert, um bestimmte Angriffe abzuwehren. Solche Angriffe können zum Beispiel durch Schadsoftware oder menschliche Angreifer hervorgerufen werden. Unter Linux gibt es mehrere Möglichkeiten zur Konfiguration einer Firewall. Neben dem Profi-Tool nftables und dem in die Jahre gekommenen iptables gibt es zwei nutzerfreundliche Programme, die den Aufbau einfacher Firewalls ermöglichen: ufw (i .d. R. unter Ubuntu im Einsatz) und firewalld (i. d. R. RedHat und OpenSUSE). Im Folgenden werden wir beide Tools prägnant beschreiben. Sie können letztlich unterschiedliche Tools auf demselben Rechner verwenden, wovon wir allerdings abraten, um inkonsistente Konfigurationen und schwierige Fehlersuchen zu vermeiden. 11.5.1 ufw ufw lässt sich sehr einfach bedienen. Um die Firewall zu aktivieren,
verwenden wir den Befehl ufw enable. Zur Deaktivierung können Sie jederzeit ufw disable aufrufen (jeweils mit Root-Rechten bzw. sudo). Um zu überprüfen, ob die Firewall aktiviert ist, können Sie ufw status aufrufen. $ sudo ufw status
Status: Inaktiv
$ sudo ufw enable
Die Firewall ist beim System-Start aktiv und aktiviert
$ sudo ufw status
Status: Aktiv
Listing 11.25 ufw-Status prüfen und Firewall aktivieren
Erste Regeln definieren
Zunächst werden Sie in der Regel eine Default Policy (also eine Standardregel) festlegen wollen, also entscheiden, ob eingehender bzw. ausgehender Datenverkehr im Standardfall geblockt werden soll. Üblich ist, sämtlichen ausgehenden Datenverkehr zu erlauben und sämtlichen eingehenden Datenverkehr zunächst zu blockieren (allow erlaubt eine Kommunikation, deny verbietet diese): $ sudo ufw default allow outgoing
$ sudo ufw default deny incoming
Listing 11.26 Eine Default Policy einstellen
Nun kann der Rechner zwar »nach draußen« kommunizieren, aber keine Verbindungen von außen annehmen. Wenn wir aber beispielsweise möchten, dass unser lokal laufender Webserver und ein lokal laufender SSH-Server Verbindungen annehmen können, müssen wir die entsprechenden Ports freigeben. Wie in Abschnitt 11.4.4 erläutert, können wir herausfinden, dass ein Webserver (HTTP(S)) und SSH die Ports 80 (HTTP), 443 (HTTPS) und 22 (SSH) verwenden. Entsprechend müssen wir diese Ports für eingehende Verbindungen freigeben. $ sudo ufw allow 22
$ sudo ufw allow 80
$ sudo ufw allow 443
Listing 11.27 Verbindungen auf den lokalen SSH- und HTTP(S)-Service erlauben
Wie Sie weitere Regeln formulieren
Möchten Sie ganze Portbereiche freigeben, so können Sie dies ebenfalls tun, indem Sie diese Bereiche per Doppelpunkt trennen (10000:12000 steht für die Ports 10.000–20.000). Protokolle können mit udp bzw. tcp festgelegt werden. Möchten Sie etwa DNS-
Verbindungen über Port 53 (UDP) annehmen, würden Sie ufw allow 53/udp schreiben. IP-Adressen können ebenfalls verwendet werden, auch in Verbindung mit Ports. Um beispielsweise eingehende Verbindungen von 192.168.2.1 nach 192.168.3.123 Port 80 zuzulassen, kann folgender Befehl verwendet werden: ufw allow from 192.168.2.1 to 192.168.3.123 port 80/tcp. Subnetzbereiche können mit Netzmasken angegeben werden, also bspw. 192.168.2.0/24. Weiterhin können Sie mit Netzwerkschnittstellen arbeiten. Wenn zum Beispiel nur über die Schnittstelle eth0 (nicht aber über eth1) eingehende Verbindungen auf Port 1234 erlaubt sein sollen, so können Sie folgenden Befehl nutzen: ufw allow in on eth1 to any port 1234. Regeln auflisten und löschen
Eine Auflistung der aktuellen Regeln erhalten Sie mit dem statusBefehl. Der Zusatz verbose gibt ein paar zusätzliche Informationen (etwa zum Level der Protokollierung) aus. $ sudo ufw status verbose
Status: Aktiv
Protokollierung: on (low)
Voreinstellung: deny (eingehend), allow (abgehend), disabled (gesendet)
Neue Profile: skip
Zu Aktion Von
– --------
22 ALLOW IN Anywhere
53/udp ALLOW IN Anywhere
192.168.3.123 80 ALLOW IN 192.168.2.1
22 (v6) ALLOW IN Anywhere (v6)
53/udp (v6) ALLOW IN Anywhere (v6)
Listing 11.28 ufw-Statusabfrage
Um Regeln wieder zu löschen, können wir diese nummeriert auflisten und anschließend die gewünschte Regelnummer mit ufw delete löschen: $ sudo ufw status numbered
Status: Aktiv
Zu Aktion Von
– --------
[ 1] 22 ALLOW IN Anywhere
[ 2] 53/udp ALLOW IN Anywhere
[ 3] 192.168.3.123 80 ALLOW IN 192.168.2.1
[ 4] 22 (v6) ALLOW IN Anywhere (v6)
[ 5] 53/udp (v6) ALLOW IN Anywhere (v6)
$ sudo ufw delete 3
Wird gelöscht:
allow from 192.168.2.1 to 192.168.3.123 port 80
Fortfahren (j|n)? y
Regel gelöscht
$ sudo ufw status numbered
Status: Aktiv
Zu Aktion Von
– --------
[ 1] 22 ALLOW IN Anywhere
[ 2] 53/udp ALLOW IN Anywhere
[ 3] 22 (v6) ALLOW IN Anywhere (v6)
[ 4] 53/udp (v6) ALLOW IN Anywhere (v6)
Listing 11.29 Regeln löschen
Sollten Sie sämtliche Regeln löschen wollen, geht dies ebenfalls: ufw reset. 11.5.2 firewalld
Die Verwendung von firewalld ist im Vergleich zu ufw etwas weniger intuitiv. Ob firewalld bereits läuft, können Sie mit systemctl status firewalld überprüfen. Ggf. muss firewalld zunächst aktiviert werden (systemctl start firewalld). Mit
systemctl können Sie firewalld auch jederzeit deaktivieren
(systemctl disable firewalld). Konfiguration
firewalld verwendet sogenannte Zonen (engl. Zones), in denen
Regelsätze gruppiert werden. Zonen werden oft mit Vertrauensstufen verbunden, sodass beispielsweise Pads oder Laptops, die einerseits im Unternehmensnetzwerk und andererseits in einem öffentlichen Café verwendet werden, je nach Umgebung unterschiedlich gesichert werden können. Einige Standardzonen können hierbei zum Einsatz kommen: drop ist die Zone, bei der der Netzwerkumgebung am wenigsten vertraut wird – alle eingehenden Verbindungen werden abgelehnt; public ist ähnlich wie drop, erlaubt aber einzelne eingehende Verbindungen auf Wunsch; work dient der typischen und zugleich recht vertrauenswürdigen Arbeitsumgebung, bei der viel Kommunikation erlaubt ist; home und trusted erlauben dann schließlich (fast) sämtliche Kommunikation in dem Sinne, dass allen anderen Hosts im Netzwerk vertraut wird. Bei der Konfiguration von firewalld empfehlen wir Ihnen die Nutzung der grafischen Oberfläche firewall-config. Über die Konsole geht es allerdings auch mit dem Programm firewall-cmd. Die aktuell verwendete Zone erhalten Sie via firewall-cmd --getdefault- zone. Eine Liste aller Zonen erhalten Sie wiederum mit firewall-cmd --list- all-zones. Um nun beispielsweise eingehende Verbindungen für unseren HTTP- und SSH-Service zu erlauben, müssen wir diese Dienste hinzufügen: $ sudo firewall-cmd --add-service=ssh
$ sudo firewall-cmd --add-service=http
Listing 11.30 eingehende SSH- und HTTP-Verbindungen
Da wir im obigen Beispiel keine Zone angegeben haben, gilt unsere Änderung für die Standardzone. Eine solche Erlaubnis können wir allerdings auch auf eine andere Zone fixieren, z. B. public: $ sudo firewall-cmd --zone=public --add-service=ssh
$ sudo firewall-cmd --zone=public --add-service=http
Listing 11.31 Eingehende SSH- und HTTP-Verbindungen für die public-Zone
Anstelle von Services können Sie selbstverständlich auch spezifische Ports freigeben: firewall-cmd --zone=public --addport=443/tcp (für den HTTPS-Port). Analog zu ufw können auch bei firewalld Portbereiche angegeben werden – allerdings nicht durch einen Doppelpunkt, sondern einen Bindestrich getrennt, z. B.: -add-port=10000-12000/tcp. Regeländerungen sind bei firewalld allerdings nicht permanent, sondern gehen beim nächsten Start wieder verloren. Aus diesem Grund möchten Sie vermutlich permanente Regeln festlegen, wofür der Parameter --permanent hinzugefügt werden sollte. Um zu sichten, welche Services innerhalb einer Zone erlaubt sind, können Sie jederzeit --list-services aufrufen, was ebenfalls zonenspezifisch funktioniert: firewall-cmd --zone=public --listservices.
11.6 Secure Shell Als ersten wichtigen Dienst wollen wir uns die sogenannte Secure Shell vornehmen, auf die wir schon mehrmals verwiesen haben. Im Prinzip geht es bei diesem Dienst darum, zwischen zwei Rechnern eine verschlüsselte Verbindung aufzubauen, über die dann Daten wie beispielsweise eine ganze Login-Session übertragen werden können. 11.6.1 Das SSH-Protokoll
Wir wollen jetzt nicht alle Einzelheiten des Protokolls besprechen und können auch nicht alle kryptografischen Verfahren darlegen. Und doch müssen einige Punkte erwähnt werden, die für den Umgang mit SSH wichtig sind. Zuallererst unterscheidet man zwischen SSH-Protokoll Version 1 und SSH- Protokoll Version 2. Der wichtigste Unterschied ist auch hier wieder die Sicherheit. Die Version 2 des Protokolls ist weniger anfällig für Attacken und unterstützt mehr Verschlüsselungsverfahren als die Vorgängerversion. Das wirklich Tolle an SSH ist aber nicht, dass es uns verschlüsselte Login-Sessions ermöglicht – SSH kann viel mehr. Im Prinzip können wir jedes Protokoll über SSH tunneln und damit die Übertragung verschlüsseln. Dafür muss zwar auf der Gegenseite der SSH-Daemon laufen, aber dann steht unserer verschlüsselten Kommunikation nichts mehr im Wege. Exkurs Kryptologie
Wenn wir über Verschlüsselung sprechen, kommen wir natürlich nicht um die entsprechende Wissenschaft herum – die Kryptologie. Die Kryptologie hat dabei zwei große Teilbereiche, die ständig miteinander im Wettstreit liegen. Da wäre zum einen die Kryptografie, die sich um sichere Verschlüsselung bemüht, und zum anderen die Kryptoanalyse, die sich mit dem Brechen von Verschlüsselungen beschäftigt. Beide Wissenschaften haben ihre Berechtigung, denn wo sensible Daten geschützt werden sollen, gibt es auch diverse Begehrlichkeiten. Verabschieden Sie sich am besten gleich von der Vorstellung der absoluten Geheimhaltung Ihrer Daten. Es ist nur eine Frage der Zeit und des Aufwands – letztlich lässt sich jede Verschlüsselung brechen.[ 31 ] Zudem ist jede Kette nur so stark wie ihr schwächstes Glied und wenn Sie Daten im Klartext auf der Festplatte Ihres Laptops speichern, haben Sie eigentlich schon verloren. Bei der Verschlüsselung unterscheidet man prinzipiell zwei Typen: die symmetrische und die asymmetrische. Der Unterschied ist einfach: Während man bei der symmetrischen Verschlüsselung zwei identische Schlüssel zum Ver- und Entschlüsseln braucht, sind es bei asymmetrischen Verfahren zwei unterschiedliche Schlüssel – ein öffentlicher sowie ein privater. Nun ist es zwar schön, wenn man unterschiedliche Schlüssel hat, allerdings sind asymmetrische Verfahren um einiges langsamer. Aus diesem Grund nutzt man teilweise asymmetrische Verfahren, um sicher einen Schlüssel für die schnellen symmetrischen Verschlüsselungen zu vereinbaren. Asymmetrische Verfahren eignen sich außerdem für digitale Signaturen, die ohne diese Art der Verschlüsselung nicht existieren würden. Der Trick ist nämlich, dass man einen Text mit dem
privaten Schlüssel signiert. Dann kann jeder mit Ihrem öffentlichen Schlüssel nachprüfen, ob der Text wirklich von Ihnen kommt – da ja nur Sie den privaten Schlüssel kennen und niemand ohne Schlüssel die Signatur fälschen kann. Auch ein verändertes Dokument würde auffallen, da die Signatur nicht mehr stimmen würde. Damit man in so einem Fall nicht den ganzen Text signieren muss, werden sogenannte Hashverfahren, etwa SHA-2, benutzt. Diese reduzieren einen Text oder auch andere Daten auf ein paar Bytes, die aber sehr stark vom Text abhängen. Ändert man beispielsweise nur einen Buchstaben, sieht der Hashwert des gesamten Textes gleich ganz anders aus. $ sha256sum buch.pdf
7df285aa60e28cb2a47d13549c6279d04e328a740bfeb5fe8bf8e34052842564
buch.pdf
Listing 11.32 SHA-2 mit 256 Bit unter Linux: sha256sum
Mit dem sha256sum-Programm können Sie unter Linux solche Prüfsummen für Dateien oder andere Daten berechnen lassen. Im obigen Fall wird der SHA-2-Algorithmus mit einer 256 Bit langen Prüfsumme verwendet. Linux unterstützt mit weiteren Tools auch andere Prüfsummenlängen für SHA-2. Außerdem sind in der Regel alternative Algorithmen wie SHA-1 (sha1sum) oder MD5 (md5sum) durch Tools nutzbar, jedoch veraltet und unsicher. Eine Prüfsumme sollte sich bei jeglicher Änderung in den Daten möglichst vollständig ändern. Ändern Sie daher nur ein einzelnes Zeichen, sieht der Hashwert völlig anders aus:[ 32 ] $ echo "." » buch.pdf
$ sha256sum buch.pdf
0cd37a82dfc25ed3e029547f61a568eb12304818fca3aaad7bc913587c13b195
buch.pdf
Listing 11.33 Modifizierung der SHA-2-Prüfsumme
11.6.2 Secure Shell nutzen
Um SSH nutzen zu können, benötigen Sie in erster Linie einen Client und einen Server. Unter Unix-Systemen gibt es das Programm ssh, das uns eine Verbindung zu entfernten Rechnern ermöglicht. Wie funktioniert das nun? Wir verbinden uns mit einem Rechner rechner als Benutzer user, indem wir ssh folgendermaßen aufrufen: $ ssh user@rechner
user@rechner's password:
No mail.
Last login: Sat Jan 12 09:40:52 2019 from a.b.c.d
user@rechner: $
Listing 11.34 ssh benutzen
Man muss nur den Benutzer auf dem entfernten Rechner angeben und sein Passwort wissen und bekommt eine Shell auf einem entfernten Rechner. Secure Copy
SSH ist auch zum sicheren Kopieren von Dateien zwischen unterschiedlichen Rechnern geeignet. Dazu verwenden Sie das scpProgramm von der Syntax her genau so wie das normale cpKommando. Der einzige Unterschied besteht in der Angabe der auf entfernten Rechnern liegenden Dateien: user@rechner:/home/user/test.txt
Listing 11.35 Entfernte Dateien ansprechen
Dieser Ausdruck bezeichnet die Datei /home/user/test.txt des Rechners rechner. Der Benutzername user wird dabei verwendet, um sich auf dem System anzumelden. Falls nötig, wird vor dem
Kopiervorgang nach einem Passwort für den Benutzer bzw. die Benutzerin gefragt. $ scp test.txt [email protected]:~
[email protected]'s password:
test.txt 100% |**************************| 103 00:00
Listing 11.36 Ein Beispiel
In diesem Beispiel wurde die Datei test.txt aus dem aktuellen Verzeichnis auf den Rechner 172.20.2.1 und dort in das Homeverzeichnis des Benutzers jploetner kopiert. Sie erinnern sich? Die Tilde (~) steht auch als Alias für das Homeverzeichnis. Automatisches Einloggen
SSH kann aber noch mehr, nämlich zum Beispiel Logins ohne Passwort auf der Basis asymmetrischer Verschlüsselungsverfahren durchführen. Zuerst müssen wir dazu mit dem Programm sshkeygen ein entsprechendes Schlüsselpaar auf dem Client erstellen. $ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key ( /.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /.ssh/id_rsa.
Your public key has been saved in /.ssh/id_rsa.pub.
The key fingerprint is:
9b:e7:1e:1b:11:3a:f3:c5:a0:e7:18:a3:68:55:60:88 root@gateway
Listing 11.37 Schlüsselerstellung mit ssh-keygen
Hier haben wir zwei RSA-Schlüssel ohne Passphrase hergestellt. Das RSA-Verfahren ist ein bekanntes asymmetrisches Verschlüsselungsverfahren, weswegen wir auch zwei Schlüssel erhalten – einen privaten und einen öffentlichen.
Um SSH nun ohne Passwort nutzen zu können, müssen wir den öffentlichen Schlüssel noch zum Benutzeraccount auf dem Server bringen: $ scp .ssh/id_rsa.pub [email protected]: /.ssh/
[email protected]'s password:
id_rsa.pub 100% |*************************| 222 00:00
$ ssh [email protected]
[email protected]'s password:
No mail.
Last login: Sun Dec 14 11:12:37 2003
$ cd /.ssh
$ cat id_rsa.pub » authorized_keys
$ exit
logout
Connection to 172.20.2.1 closed.
Listing 11.38 Schlüssel aktivieren
Dazu kopieren wir per scp den öffentlichen Schlüssel auf den Server und hängen die Datei dann mit einem cat lokal an die /.ssh/authorized_keys an. gateway:~# ssh [email protected]
No mail.
Last login: Sun Dec 14 18:25:13 2003 from gateway
jploetner@athlon2000: $
Listing 11.39 Der Test – Login ohne Passwort
Der abschließende Test zeigt nun, dass das Einloggen per SSH ohne Passwort funktioniert. Vielleicht machen Sie sich auch Gedanken über die Sicherheit, aber die ist nur gefährdet, wenn schon jemand Zugriff auf Ihren Benutzeraccount am Client hat, denn dann kann sich jeder ohne Passwort gleich zum nächsten Server verbinden. In der Praxis sollte man sich dieser Gefahr bewusst sein und für kritische Anwendungsfälle auf jeden Fall einen Schlüssel mit Passphrase erstellen.
Secure-Shell-Tunnel
Als Ausblick erläutern wir noch kurz den SSH-Tunnel. Bei einem solchen Tunnel wollen Sie eine unverschlüsselte Verbindung verschlüsseln, und zwar indem Sie die Daten über einen SSH-Kanal leiten. Dazu wird, anstatt eine direkte Verbindung aufzubauen, das ssh-Programm mit den entsprechenden Optionen gestartet. SSH öffnet dann lokal bei Ihnen einen Port und leitet diesen auf den anderen Rechner weiter. Dort verbindet es das andere Ende des Tunnels mit dem gewünschten Port auf dem Server (siehe Abbildung 11.2). Nun können Sie die Verbindung einfach nutzen, indem Sie Ihr Programm (beispielsweise einen Chatclient) anweisen, sich nicht mit dem Server, sondern mit dem entsprechenden Port auf Ihrem eigenen Rechner zu verbinden. Den Rest übernimmt SSH.
Abbildung 11.2 Schema für einen SSH-Tunnel
Einen Tunnel richten Sie wie folgt ein: $ ssh -f -N -C -L 8888:rechner:6667 -l user rechner
Listing 11.40 Einen Tunnel aufbauen
Dieser Aufruf bewirkt Folgendes:
-f
Nach dem erfolgreichen Verbindungsaufbau forkt (siehe Kapitel 8, »Programme und Prozesse«) sich SSH in den Hintergrund, sodass die Shell nicht weiter blockiert wird. -N
SSH führt nach einer erfolgreichen Verbindung auf der Gegenseite kein Kommando aus – wir wollen ja nur die Portweiterleitung. -C
Die Verbindung wird komprimiert, damit der Datentransfer beschleunigt wird. -L 8888:rechner:6667
Dies öffnet uns den lokalen Port 8888, der mit dem Port 6667 auf rechner verbunden ist. Die Strecke zwischen den beiden Systemen wird mit SSH getunnelt. -l user
Wir loggen uns auf der Gegenstelle mit dieser Benutzerkennung ein. rechner
Wir verbinden uns, um den Tunnel aufzubauen, zum SSH-Port dieses Systems. Jetzt müssen wir, um die verschlüsselte Verbindung zu nutzen, unserem Clientprogramm nur noch sagen, dass wir statt mit rechner:6667 mit localhost:8888 sprechen wollen. Ein netstat --tcp sollte uns dann eine Verbindung zu localhost Port 8888 und eine Verbindung zu rechner auf den SSH-Port anzeigen. Das Ganze funktioniert natürlich nur, wenn wir uns auf dem entsprechenden Server mit SSH einloggen können. Zudem müssen Sie, um Ports unterhalb von 1024 adressieren zu können, root-
Rechte besitzen. Also wählen Sie als Benutzer lieber einen höheren Port für die lokale Verbindung. 11.6.3 Der Secure-Shell-Server
Manche Leute meinen, dass ein Linux-Rechner einen SSH-Server dringender braucht als die Tastatur oder einen Bildschirm. Und da haben die Leute nicht so ganz unrecht. Eigentlich sollte auf jedem mit einem Netzwerk verbundenen Rechner dieser Dienst laufen, da er wirklich nützlich ist und als sicher angesehen wird. Konfiguriert wird der Server hauptsächlich über die sshd_config, die sich meistens im Verzeichnis /etc/ssh befindet. Die Datei ist zum großen Teil mit sinnvollen Voreinstellungen belegt, sodass wir hier nur auf die wichtigsten Optionen eingehen werden: X11Forwarding yes|no
Mit dieser Einstellung aktivieren bzw. deaktivieren Sie die Weiterleitung von X11-Verbindungen. Wird dieses Feature vom Client sowie vom Server unterstützt, können Sie die Netzwerkfähigkeit des X-Protokolls erleben. Dann ist es möglich, auf dem Server eine X-Anwendung zu starten, das Fenster aber auf Ihrem Client zu sehen. PermitRootLogin yes|without-password|no
Erlaubt bzw. verbietet ein Login als root über SSH. Bei withoutpassword ist kein Root-Login mit Passwort erlaubt. *Authentication yes|no
SSH unterstützt ja viele Authentifizierungsmethoden und mit den verschiedenen Optionen können Sie sie jeweils aus- bzw. einschalten.
Beispielsweise sollten Sie RSAAuthentication yes einsetzen, um die oben vorgestellte Methode mit ssh-keygen nutzen zu können. Um parallel das normale Verfahren zu deaktivieren, sollten Sie PasswordAuthentication auf no setzen.
11.7 Das World Wide Web Die wichtigste Dienstleistung des Internets ist das World Wide Web (WWW). Natürlich können Sie auch und gerade unter Linux eigene Webserver aufsetzen und nutzen. Im Folgenden beschreiben wir die Verwendung des Webservers Apache, der meistgenutzten Webserversoftware des Internets. 11.7.1 Das HTTP-Protokoll
Zuvor allerdings noch etwas Theorie. Viele Menschen setzen das World Wide Web mit dem Internet gleich – dem ist aber nicht so. Das Internet ist einfach ein großes Netzwerk sehr vieler, über die ganze Welt verteilter Computer. Wenn man aber vom World Wide Web spricht, dann meint man meist die Gesamtheit aller HTTPDienste (Hypertext Transfer Protocol) des Internets – also alles, was man sich mit einem Browser anschauen kann. Dass in einem Netzwerk aber noch viel, viel mehr Dienste angeboten werden können, wissen Sie spätestens seit diesem Kapitel. Um eine Webseite oder gar einen ganzen Internetauftritt zur Verfügung zu stellen, brauchen Sie logischerweise auch einen Serverdienst – in unserem Fall eben den Apache. Der Webbrowser als Client baut wie gewohnt eine Verbindung zum Server auf und bekommt von diesem die Seite. Interessant sind einige Besonderheiten des HTTP-Protokolls. Es gibt nämlich in einem gewissen Sinne keine Verbindungen: Wenn ein Client sich verbindet, wird die TCP-Verbindung im Allgemeinen nach dem Senden der Seite wieder unterbrochen. Damit hat der Webserver keine Möglichkeit, festzustellen, ob sich ein Benutzer
eingeloggt hat oder neu auf der Seite ist. Man benötigt daher einige Tricks, um solche Sessions zu ermöglichen. Cookies und Co.
Ein Weg ist einfach: Wenn sich ein Benutzer einloggt, wird jedes Mal eine eindeutige ID mitgeschickt. Damit kann eine dynamische Seite feststellen, zu welcher Session ein Request gehört, und dementsprechend die Seite erzeugen. Was bedeutet dynamisch? Standardmäßig sind Seiten nur statisch, da HTML eine Seitenbeschreibungssprache ist und keine dynamischen Inhalte ermöglicht. Dynamische Sprachen wie PHP oder ASP.NET erzeugen HTML-Seiten. Sie ermöglichen es, Inhalte dynamisch zu gestalten und trotzdem für den Client kompatibel zu bleiben. Eine andere Möglichkeit, Sessions zu verfolgen, sind Cookies – kleine Speicher auf dem Rechner des Nutzers oder der Nutzerin, die mit Informationen befüllt werden können, um beim nächsten Besuch wieder nutzerfreundlicher mit dem Server agieren zu können. Zu Recht haben Cookies bei Anwenderinnen und Anwendern einen schlechten Ruf und sind oft deaktiviert. Cookies werden nämlich auch genutzt, um Ihr Surfverhalten zu analysieren, sodass dann personalisierte Werbung für Sie auf Webseiten platziert werden kann. Proxyserver
Eine weitere Besonderheit sind sogenannte Proxyserver. Ein Proxy ist dabei eine Art Zwischenspeicher zwischen Client und Server. Der Client verbindet sich in einem solchen Fall mit dem Proxy statt mit dem Server und der Proxy holt dann die Seite für den Client.
Ein Proxy hat den Vorteil, dass er eventuell Seiten zwischenspeichern und damit den Zugriff für den Client beschleunigen kann. Vor allem in Firmennetzwerken oder Rechenzentren hat man damit einen deutlichen Gewinn. Ein Proxy wird im lokalen Netz immer schneller sein als ein Server aus dem Internet.
Abbildung 11.3 Schema Proxyserver
HTTP im Überblick
Aber schauen wir uns einmal im Detail an, was bei einem HTTPRequest übertragen wird. Wir nutzen dazu das Programm telnet, das ja nicht nur Client eines Login-Dienstes ist, sondern sich auch mit anderen Ports verbinden kann. Bei einer solchen Anwendung wird die Eingabe des Benutzers unverändert gesendet beziehungsweise die Ausgabe des Servers auf dem Bildschirm dargestellt – ideal also zum Testen von Protokollen. $ telnet www.gmx.de 80
Trying 213.165.65.100...
Connected to www.gmx.de.
Escape character is '^]'.
GET / HTTP/1.0 (Return)
(Return)
HTTP/1.1 200 OK
Date: Sat, 22 Nov 2003 15:20:36 GMT
Server: Apache
Cache-Control: no-cache
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Pragma: no-cache
Connection: close
Content-Type: text/html; charset=iso-8859-1