Author: Gräfe M.
Tags: programmierung linux open source netzwerke c betriebssystem softwareentwicklung c-programmierung systemprogrammierung linux-entwicklung compiler shell terminal hardwarezugriff codebeispiele linux-tools multithreading fehlerbehebung hanser verlag
ISBN: 978-3-446-42176-9
Year: 2010
martin GRÄFE
C UND
LINUX
DIE MÖGLICHKEITEN DES
BETRIEBSSYSTEMS MIT EIGENEN
PROGRAMMEN NUTZEN
4. Auflage
Gräfe
C und Linux
C
v
Bleiben Sie einfach auf dem Laufenden:
www.hanser.de/newsletter
Sofort anmelden und Monat für Monat
die neuesten Infos und Updates erhalten.
Martin Gräfe
C und Linux
Die Möglichkeiten des Betriebssystems
mit eigenen Programmen nutzen
4., vollständig überarbeitete und erweiterte Auflage
Dr.-Ing. Martin Gräfe, geboren 1968 in Hagen, studierte Elektrotechnik an der Universität Dortmund.
Dort war er nach Abschluss des Studiums als wissenschaftlicher Mitarbeiter tätig und promovierte
1998 auf dem Gebiet der Mikroelektronik. Bereits während des Studiums befasste sich Martin Gräfe
mit C-Programmierung unter Unix und seit 1995 schließlich auch mit Linux. Im Rahmen seiner
Promotion und seiner Tätigkeit als Ingenieur entwickelte er verschiedene Programme zur Simulation
elektronischer Schaltungen und Übertragungssysteme.
Alle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestem
Wissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mit keiner
Verpflichtung oder Garantie irgendeiner Art verbunden. Autoren und Verlag übernehmen infolgedessen keine juristische Verantwortung und werden keine daraus folgende oder sonstige Haftung
übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen – oder Teilen davon –
entsteht, auch nicht für die Verletzung von Patentrechten und anderen Rechten Dritter, die daraus
resultieren könnten. Autoren und Verlag übernehmen deshalb keine Gewähr dafür, dass die beschriebenen Verfahren frei von Schutzrechten Dritter sind.
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Buch
berechtigt deshalb auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen
im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Bibliografische Information der Deutschen Nationalbibliothek:
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Dieses Werk ist urheberrechtlich geschützt.
Alle Rechte, auch die der Übersetzung, des Nachdruckes und der Vervielfältigung des Buches, oder
Teilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung des Verlages in
irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren) – auch nicht für Zwecke der Unterrichtsgestaltung – reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden.
© 2010 Carl Hanser Verlag München Wien (www.hanser.de)
Lektorat: Margarete Metzger
Herstellung: Irene Weilhart
Copy editing: Manfred Sommer, München
Umschlagdesign: Marc Müller-Bremer, www.rebranding.de, München
Umschlagrealisation: Stephan Rönigk
Datenbelichtung, Druck und Bindung: Kösel, Krugzell
Ausstattung patentrechtlich geschützt. Kösel FD 351, Patent-Nr. 0748702
Printed in Germany
ISBN 978-3-446-42176-9
Inhaltsverzeichnis
1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1
1.2
1
Warum gerade C“? . . . . . . . . . . . . . . . . . . . . . . . . . . . .
”
Bevor es losgeht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
2
1.2.1
1.2.2
Paketverwaltung unter SuSE-Linux . . . . . . . . . . . . . .
Paketinstallation bei Ubuntu . . . . . . . . . . . . . . . . . .
2
4
Die Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.1 Der Editor – die Qual der Wahl . . . . . . . . . . . . . . . . .
6
6
1.3.2
Der GNU C-Compiler gcc . . . . . . . . . . . . . . . . . . . .
8
1.3.3
1.3.4
Ablaufsteuerung mit GNU make . . . . . . . . . . . . . . . .
Für die Fehlersuche: Die Debugger . . . . . . . . . . . . . . .
8
10
1.3.5 Integrierte Entwicklungsumgebungen . . . . . . . . . . . . .
Der Umgang mit Compiler, Debugger und make“ anhand von Bei”
spielen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.1 Primzahlen berechnen . . . . . . . . . . . . . . . . . . . . . .
11
14
14
1.4.2
1.4.3
Fehlersuche mit dem gcc . . . . . . . . . . . . . . . . . . . . .
Fehlersuche mit dem GNU Debugger . . . . . . . . . . . . .
16
17
1.4.4
1.4.5
Funktionsbibliotheken verwenden . . . . . . . . . . . . . . .
Quelltexte aufteilen . . . . . . . . . . . . . . . . . . . . . . . .
19
21
Weiterführende Informationen . . . . . . . . . . . . . . . . . . . . .
1.5.1 Die Unix-Online-Hilfen man“, xman“ und tkman“ . . . .
”
”
”
1.5.2 Ein Blick hinter die Kulissen: Die Include-Dateien . . . . . .
25
26
2 Arbeiten mit einer Entwicklungsumgebung . . . . . . . . . . . . . . . .
2.1 Anjuta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
31
1.3
1.4
1.5
2.1.1
2.1.2
Ein neues Projekt anlegen . . . . . . . . . . . . . . . . . . . .
Eingabe der Quelltexte . . . . . . . . . . . . . . . . . . . . . .
28
31
33
VI
Inhaltsverzeichnis
2.1.3 Kompilieren und Starten des Beispiels . . . . . . . . . . . . .
KDevelop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
36
Eclipse + C Development Tooling (CDT) . . . . . . . . . . . . . . . .
2.3.1 Plug-ins einbinden . . . . . . . . . . . . . . . . . . . . . . . .
39
40
2.3.2
Ein neues Projekt anlegen . . . . . . . . . . . . . . . . . . . .
40
3 Kommandozeilenprogramme . . . . . . . . . . . . . . . . . . . . . . . . .
43
2.2
2.3
3.1
Parameter und Rückgabewert der Funktion main() . . . . . . . . .
3.1.1 Die Bedeutung des Rückgabewertes von main() . . . . . .
43
44
3.1.2
3.1.3
Die Variablen argc und argv . . . . . . . . . . . . . . . . . .
Auswerten der Kommandozeilenparameter . . . . . . . . .
44
45
3.1.4 Achtung: Platzhalter! . . . . . . . . . . . . . . . . . . . . . . .
Konventionen für Kommandozeilenprogramme . . . . . . . . . . .
47
48
3.2.1
3.2.2
Ein Muss: Die Hilfe-Option . . . . . . . . . . . . . . . . . . .
Fehlermeldungen . . . . . . . . . . . . . . . . . . . . . . . . .
48
50
3.2.3
Eigene Manpages erstellen . . . . . . . . . . . . . . . . . . . .
51
Programme mehrsprachig auslegen . . . . . . . . . . . . . . . . . .
Ausgabesteuerung im Terminal-Fenster . . . . . . . . . . . . . . . .
53
60
3.4.1
3.4.2
ANSI-Steuersequenzen . . . . . . . . . . . . . . . . . . . . .
Die ncurses“-Bibliothek . . . . . . . . . . . . . . . . . . . . .
”
60
61
4 Dateien und Verzeichnisse . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1 Die Arbeit mit Dateien . . . . . . . . . . . . . . . . . . . . . . . . . .
67
67
3.2
3.3
3.4
4.1.1
4.1.2
Gepufferte Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . .
stdin, stdout und stderr . . . . . . . . . . . . . . . . . . .
67
68
4.1.3
4.1.4
Dateien öffnen und schließen . . . . . . . . . . . . . . . . . .
Lesen aus und Schreiben in Dateien . . . . . . . . . . . . . .
69
70
4.2
4.1.5 Ein Beispiel: Zeilen nummerieren . . . . . . . . . . . . . . .
Eigenschaften von Dateien oder Verzeichnissen auswerten . . . . .
74
75
4.3
Verzeichnisse einlesen . . . . . . . . . . . . . . . . . . . . . . . . . .
77
5 Interprozesskommunikation . . . . . . . . . . . . . . . . . . . . . . . . .
5.1 Prozessverwaltung unter Linux . . . . . . . . . . . . . . . . . . . . .
79
79
5.2
Neue Prozesse starten . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.1 Shell-Programme aufrufen mit system() . . . . . . . . . . .
80
80
5.2.2
81
Die Funktionen der exec-Familie . . . . . . . . . . . . . . .
Inhaltsverzeichnis
5.2.3
5.2.4
5.3
5.4
5.5
VII
Einen Kind-Prozess erzeugen mit fork() . . . . . . . . . . .
Warteschleifen . . . . . . . . . . . . . . . . . . . . . . . . . .
82
85
Signale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3.1 Die Weckfunktion alarm() . . . . . . . . . . . . . . . . . . .
86
87
5.3.2
5.3.3
Einen Signal-Handler einrichten . . . . . . . . . . . . . . . .
Auf die Beendigung eines Kind-Prozesses warten . . . . . .
88
89
5.3.4 Signale setzen mit kill() . . . . . . . . . . . . . . . . . . . .
Datenaustausch zwischen Prozessen . . . . . . . . . . . . . . . . . .
90
91
5.4.1
5.4.2
91
95
Pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
FIFOs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.4.3 Shared Memory . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Alternative Verfahren zur Erzeugung von Prozessen . . . . . . . . . 100
5.5.1
5.5.2
popen() und pclose() . . . . . . . . . . . . . . . . . . . . . 100
5.5.3
POSIX-Threads . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Die fork()-Alternative clone() . . . . . . . . . . . . . . . 101
6 Devices – das Tor zur Hardware . . . . . . . . . . . . . . . . . . . . . . . 107
6.1 Das Device-Konzept von Linux . . . . . . . . . . . . . . . . . . . . . 107
6.1.1
6.1.2
6.2
6.1.3 Devices steuern mit ioctl() . . . . . . . . . . . . . . . . . . 110
Das CD-ROM-Laufwerk . . . . . . . . . . . . . . . . . . . . . . . . . 111
6.2.1
6.2.2
6.3
6.5
OSS, ALSA und ESOUND . . . . . . . . . . . . . . . . . . . . 122
Der Mixer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
6.3.3 Audiodaten aufnehmen und wiedergeben . . . . . . . . . . 126
Video for Linux“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
”
6.4.1 Eigenschaften des Devices . . . . . . . . . . . . . . . . . . . . 130
6.4.2 Bilder aufzeichnen . . . . . . . . . . . . . . . . . . . . . . . . 133
Die serielle Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . . 142
6.5.1
6.5.2
6.6
Die CD auswerfen“ . . . . . . . . . . . . . . . . . . . . . . . 111
”
Fähigkeiten des Laufwerks auslesen . . . . . . . . . . . . . . 112
6.2.3 Audio-CDs abspielen . . . . . . . . . . . . . . . . . . . . . . 114
Ansteuerung einer Soundkarte . . . . . . . . . . . . . . . . . . . . . 121
6.3.1
6.3.2
6.4
Devices öffnen und schließen . . . . . . . . . . . . . . . . . . 108
Ungepuffertes Lesen und Schreiben . . . . . . . . . . . . . . 109
Terminal-Parameter einstellen . . . . . . . . . . . . . . . . . 143
Ein kleines Terminalprogramm . . . . . . . . . . . . . . . . . 145
Druckerausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
VIII
6.7
Inhaltsverzeichnis
Der Universal Serial Bus (USB) . . . . . . . . . . . . . . . . . . . . . . 154
6.7.1 Ansteuerung von USB-Geräten anhand eines Beispiels . . . 156
7 Netzwerkprogrammierung . . . . . . . . . . . . . . . . . . . . . . . . . . 163
7.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
7.2
7.1.1
7.1.2
Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Vorbereitung . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
7.1.3
7.1.4
Das Client-Server-Prinzip . . . . . . . . . . . . . . . . . . . . 169
Sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
Der TCP/IP-Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
7.2.1 Aufbau einer Verbindung . . . . . . . . . . . . . . . . . . . . 171
7.2.2
7.2.3
7.3
Server-Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
7.3.1 Die Funktionsweise eines Servers . . . . . . . . . . . . . . . 178
7.3.2
7.4
7.5
Ein Universal“-Client . . . . . . . . . . . . . . . . . . . . . . 173
”
Rechnernamen in IP-Adressen umwandeln . . . . . . . . . . 176
Ein interaktiver TCP/IP-Server . . . . . . . . . . . . . . . . . 180
7.3.3 Ein kleiner Webserver . . . . . . . . . . . . . . . . . . . . . . 184
Das User Datagram Protocol (UDP) . . . . . . . . . . . . . . . . . . . . 191
7.4.1
7.4.2
UDP-Nachrichten senden . . . . . . . . . . . . . . . . . . . . 191
Der UDP-Server . . . . . . . . . . . . . . . . . . . . . . . . . . 194
7.4.3
7.4.4
Pakete an alle Teilnehmer senden: Broadcast . . . . . . . . . 197
Multicast-Sockets . . . . . . . . . . . . . . . . . . . . . . . . . 199
7.4.5 UPnP – Universal Plug And Play . . . . . . . . . . . . . . . . . 200
Noch ein Wort zur Sicherheit . . . . . . . . . . . . . . . . . . . . . . 204
8 Grafische Benutzeroberflächen . . . . . . . . . . . . . . . . . . . . . . . . 205
8.1 Die grafische Oberfläche X11 . . . . . . . . . . . . . . . . . . . . . . 205
8.2
Das Toolkit GTK+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
8.2.1 GTK 1.2 versus GTK 2.0 . . . . . . . . . . . . . . . . . . . . . 206
8.2.2
GTK-Programme übersetzen . . . . . . . . . . . . . . . . . . 207
8.2.3
8.2.4
Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 208
Das Callback-Prinzip . . . . . . . . . . . . . . . . . . . . . . . 210
8.2.5
8.2.6
Schaltflächen (Buttons) . . . . . . . . . . . . . . . . . . . . . . 213
Hinweistexte (Tipps) . . . . . . . . . . . . . . . . . . . . . . . 216
8.2.7
8.2.8
Widgets anordnen . . . . . . . . . . . . . . . . . . . . . . . . 216
Text-Labels . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Inhaltsverzeichnis
IX
8.2.9 Dialogfenster . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
8.2.10 Auswahlfelder . . . . . . . . . . . . . . . . . . . . . . . . . . 224
8.2.11 Eingabefelder für Text und Zahlen . . . . . . . . . . . . . . . 228
8.2.12 Menüs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
8.2.13 Pixmap-Grafiken darstellen . . . . . . . . . . . . . . . . . . . 238
8.2.14 Zeichenflächen . . . . . . . . . . . . . . . . . . . . . . . . . . 244
8.2.15 Zeichenfläche mit Rollbalken . . . . . . . . . . . . . . . . . . 250
8.2.16 Dateiauswahlfenster . . . . . . . . . . . . . . . . . . . . . . . 252
8.2.17 Umlaute und Sonderzeichen . . . . . . . . . . . . . . . . . . 255
8.2.18 Wie geht es weiter? . . . . . . . . . . . . . . . . . . . . . . . . 255
8.3
Grafik ohne X11 mit der SVGALIB . . . . . . . . . . . . . . . . . . . 256
8.3.1 Besonderheiten beim Arbeiten mit der libvga . . . . . . . . . 256
8.3.2
8.3.3
Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 257
Mit Perspektive: 3D-Funktionen zeichnen . . . . . . . . . . . 260
8.3.4
Ein kleines Malprogramm . . . . . . . . . . . . . . . . . . . . 262
8.3.5
8.3.6
Erweiterte Funktionen mit der libvgagl . . . . . . . . . . . . 266
Weitere Informationsquellen . . . . . . . . . . . . . . . . . . 268
9 Hardware-Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . 271
9.1 Hardware-nahe Programme schreiben . . . . . . . . . . . . . . . . . 271
9.1.1
9.1.2
9.2
9.1.3 Zugriff auf die I/O-Ports . . . . . . . . . . . . . . . . . . . . 273
Ansteuerung des Parallelports . . . . . . . . . . . . . . . . . . . . . 274
9.2.1
9.2.2
9.3
Eigene Programme mit root-Rechten ausstatten . . . . . . . . 272
Zugriff auf I/O-Ports freischalten . . . . . . . . . . . . . . . 272
Beschreibung des Parallelports . . . . . . . . . . . . . . . . . 274
Die Adresse des Parallelports suchen . . . . . . . . . . . . . 275
9.2.3 Ein Beispiel: LED-Lauflicht“ . . . . . . . . . . . . . . . . . . 276
”
Modem-Steuerleitungen abfragen . . . . . . . . . . . . . . . . . . . . 279
10 Beispielprojekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
10.1 WebCam: Video-Übertragung per HTTP . . . . . . . . . . . . . . . . 283
10.1.1 Wie die Bilder laufen lernen . . . . . . . . . . . . . . . . . . . 284
10.1.2 Strukturierung der Quelltexte . . . . . . . . . . . . . . . . . . 284
10.1.3 Die HTTP-Authentifizierung . . . . . . . . . . . . . . . . . . 298
10.2 Telefonbuch mit automatischer Anwahl . . . . . . . . . . . . . . . . 300
10.2.1 Ziel des Projektes . . . . . . . . . . . . . . . . . . . . . . . . . 300
X
Inhaltsverzeichnis
10.2.2 Strukturierung des Projektes . . . . . . . . . . . . . . . . . . 301
10.2.3 Das Hauptprogramm . . . . . . . . . . . . . . . . . . . . . . 301
10.2.4 Funktionen zur Ansteuerung des Modems . . . . . . . . . . 304
10.2.5 Die Benutzerschnittstelle . . . . . . . . . . . . . . . . . . . . 307
10.2.6 To Do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
Anhang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
A1 – Daten zum Buch im Internet . . . . . . . . . . . . . . . . . . . . . . . 315
A2 – Das X11-Toolkit XView . . . . . . . . . . . . . . . . . . . . . . . . . . 315
A3 – Aufbau einer WAV-Audiodatei . . . . . . . . . . . . . . . . . . . . . 316
A4 – Aufbau einer AU-Audiodatei . . . . . . . . . . . . . . . . . . . . . . 317
A5 – Linux-Programmierung unter Windows: Cygwin
. . . . . . . . . . 317
Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Vorwort
Seit der 1. Auflage dieses Buches sind nun fast acht Jahre vergangen. In dieser
Zeit hat sich Linux auf verschiedenen Gebieten weiterentwickelt: Für den Anwender ist die Unterstützung von USB- und anderen Plug&Play-Geräten hinzugekommen, für den Programmierer hat eine Standardisierung der unterschiedlichen Linux-Distributionen stattgefunden; es wurde die so genannte Linux Standard Base (kurz LSB) entwickelt.
Beiden Entwicklungen wird in dieser 4. Auflage Rechnung getragen. Sämtliche
Quelltexte wurden an die LSB angepasst und das Kapitel über die Ansteuerung
von Geräten wurde um das Thema USB erweitert. Darüber hinaus sind in dem
Kapitel Netzwerkprogrammierung“ Beispiele zu UDP, Broadcast und Multicast
”
ergänzt worden.
Dieses Buch wendet sich sowohl an den Programmier-Einsteiger, der Grundkenntnisse in der Programmiersprache C“ besitzt, als auch an den fortgeschrit”
tenen Programmierer, der die vielfältigen Möglichkeiten des Betriebssystems in
eigenen Programmen nutzen möchte. Dabei geht es vor allem um die Linuxspezifischen Themen, wie z. B. die Ansteuerung von Devices. Durch zahlreiche
einfache Beispielprogramme soll der Einstieg in diese Themen erleichtert werden.
Als Programmiersprache kommt ausschließlich ANSI-C zum Einsatz.
Haiger, im Frühjahr 2010
Martin Gräfe
Kapitel 1
Einführung
1.1 Warum gerade C“?
”
Unter Linux ist mittlerweile eine Vielzahl von Programmiersprachen verfügbar,
angefangen von Pascal und Fortran über Skript- oder Interpretersprachen wie
TCL und Perl bis hin zu objektorientierten Compilersprachen wie C++ und Java.
Jede dieser Programmiersprachen hat ihre Vor- und Nachteile, C“ kommt jedoch
”
eine besondere Bedeutung zu, da fast das gesamte Betriebssystem in ANSI-C geschrieben ist.1 Mit den Kernel-Quellen stehen dadurch sämtliche für die systemnahe Programmierung erforderlichen Include-Dateien unter C zur Verfügung.
Aus diesem Grund lassen sich die Möglichkeiten des Betriebssystems (und der
Hardware) mit C so vollständig wie mit keiner anderen Programmiersprache nutzen.2
Da C eine kompakte, relativ maschinennahe“ Programmiersprache ist, sind C”
Programme effizient und schonen die System-Ressourcen. Für das Ausführen der
Programme ist weder ein Interpreter noch eine Laufzeitumgebung wie bei Java
erforderlich. Trotzdem sind die Quelltexte portabel – viele der Beispielprogramme
in diesem Buch laufen auch unter kommerziellen Unix-Varianten (z. B. Solaris von
Sun oder HPUX von Hewlett Packard) und mit Cygwin3 sogar unter WindowsTM .
Mit den Kernel-Quellen und vielen Open Source-Programmen hat der Programmierer Zugriff auf eine nahezu unbegrenzte Menge an Quelltexten, aus denen er
sich den einen oder anderen Programmierkniff abschauen kann – und darf . Denn
in der Welt der offenen Quelltexte muss zum Glück das Rad nicht immer wieder
neu erfunden werden.
1
Mit Ausnahme einiger Hardware-naher oder zeitkritischer Programmteile, die in Assembler programmiert wurden.
2
Außer vielleicht in Assembler, was aber keine echte Alternative zu Hochsprachen darstellt.
3
siehe Anhang A5 ab Seite 317
2
1 Einführung
1.2 Bevor es losgeht . . .
. . . müssen die notwendigen Tools und Dateien installiert sein. In der Anfangszeit von Linux, als man nach der Installation erst einmal den Kernel nach eigenen Wünschen neu kompilierte, waren die Werkzeuge für die C-Programmierung
fester Bestandteil der Linux-Distributionen und wurden in der Regel automatisch
mit installiert. Inzwischen ist das Kompilieren des Kernels dank der Modularisierung nicht mehr notwendig, und so werden bei vielen Distributionen die Pakete
zur Software-Entwicklung nicht mehr automatisch installiert. Bei einigen auf CD
erhältlichen Distributionen sind diese Pakete nicht einmal mehr enthalten, sondern müssen aus dem Internet nachgeladen werden.
Um die Beispiele in diesem Buch übersetzen zu können, benötigen Sie das Paket
mit dem C-Compiler gcc“ sowie das Programm make“. Außerdem werden für
”
”
das Einbinden von Funktionsbibliotheken in eigene Programme die zugehörigen
Include-Dateien benötigt, die häufig in separaten Paketen enthalten sind. Beispiel:
Die Funktionsbibliothek libncurses“ wird bei den meisten Distributionen als Vor”
einstellung installiert. Wenn Sie diese Bibliothek in einem eigenen Programm benutzen wollen, benötigen Sie zusätzlich das zugehörige Entwicklungspaket“. Je
”
nach Distribution heißt dieses Paket beispielsweise
ncurses-dev-Version“
”
oder
ncurses-devel-Version“.
”
Im Folgenden soll exemplarisch für die Distributionen SuSE“ und Ubuntu“ ge”
”
zeigt werden, wie Pakete nachinstalliert werden können.
1.2.1 Paketverwaltung unter SuSE-Linux
In der Linux-Distribution von SuSE1 ist das Tool YaST“ (Abkürzung für Yet
”
”
Another Setup Tool“) enthalten, das für alle wichtigen Systemeinstellungen
zuständig ist. Dieses kann entweder aus dem Menü des Windowmanagers oder
als Benutzer root“ aus einer Shell mit dem Kommando /sbin/yast2“ aufgeru”
”
fen werden. Durch einen Klick auf das Icon Software installieren oder löschen“ in
”
der Rubrik Software“ wird die Paketverwaltung geöffnet (siehe Abbildung 1.1).
”
Findet man das gesuchte Paket nicht unter den angezeigten Paketgruppen, kann
man den Filter“ (oben links) auf Suche“ umstellen und ein entsprechendes
”
”
Stichwort eingeben.
Die Pakete der Distribution von SuSE liegen im RPM-Format (Abkürzung für
RedHat Packet Manager“) vor. Daher können einzelne Pakete auch mit dem Be”
fehl rpm -i Paket-Datei“ installiert werden.
”
1
SuSE steht für Software- und Systementwicklung. So heißt das kleine Unternehmen, das diese Distribution zusammenstellt und inzwischen von der Firma Novell aufgekauft wurde.
1.2 Bevor es losgeht . . .
Abbildung 1.1: Die Paketverwaltung unter SuSE mit YaST
3
4
1 Einführung
1.2.2 Paketinstallation bei Ubuntu
Ubuntu-Linux ist von der Linux-Distribution Debian abgeleitet und verwendet
die gleichen Pakete. Anders als bei SuSE sind die Pakete daher im Debian-eigenen
DEB-Format. Für die Auswahl und Installation der Pakete bringt der GNOMEDesktop unter Ubuntu ein spezielles Tool mit, das über das GNOME-Menü aufgerufen werden kann (siehe Abbildung 1.2).
Abbildung 1.2: Öffnen der GNOME-Paketverwaltung bei Ubuntu-Linux
In der Startansicht bietet das Werkzeug nur das Installieren oder Entfernen ganzer Anwendungen an, ohne die einzelnen Pakete im Detail aufzulisten. Über
den Menüpunkt Datei/Erweitert“ lässt sich die Darstellung erweitern, sodass
”
die einzelnen Pakete aufgelistet werden können (siehe Abbildung 1.3). Ähnlich
wie bei YaST unter SuSE-Linux ist auch hier die Möglichkeit gegeben, nach bestimmten Paketen anhand von Stichworten zu suchen (Schaltfläche Suche“ unten
”
links).
Wenn Sie Pakete von Hand“ installieren wollen, so gibt es dafür bei Ubuntu zwei
”
Kommandozeilenprogramme: apt-get“ und dbkg“:
”
”
sudo dpkg -i Paket-Datei
sudo apt-get install Paket-Datei
1.2 Bevor es losgeht . . .
Abbildung 1.3: Erweiterte Ansicht für die GNOME-Paketverwaltung
5
6
1 Einführung
1.3 Die Werkzeuge
In diesem Abschnitt stellen wir in kurzer Form die für das Programmieren erforderlichen Werkzeuge vor. In Abschnitt 1.4 wird der Umgang mit diesen Werkzeugen dann anhand von Beispielen erläutert. Den Schwerpunkt bilden dabei die für
Unix und Linux üblichen Kommandozeilen-Tools. Den Umgang mit einer integrierten Entwicklungsumgebung beschreibt Kapitel 2.
1.3.1 Der Editor – die Qual der Wahl
Um ein Programm zu schreiben, benötigt man natürlich zunächst einen Editor,
mit dem man den Quelltext eingibt. Bei Verwendung einer integrierten Entwicklungsumgebung (siehe Abschnitt 1.3.5) ist bereits ein solcher Editor in diese Umgebung eingebaut, doch viele Programmierer verwenden stattdessen ihren Lieb”
lingseditor“ – wovon es unter Linux eine ganze Menge gibt. Dabei kann man zwei
Kategorien unterscheiden: Editoren, die auf der Konsole bzw. in einem Terminalfenster (wie XTerm) laufen, und Editoren, die über eine eigene grafische Benutzeroberfläche verfügen. Letztere sind in der Regel komfortabler, weil sie über
Syntax-Highlighting verfügen, benötigen aber auch weit mehr Ressourcen.
Editoren für die Textkonsole:
vim (steht für VI improved“)
”
emacs
pico
joe
jedit
...
Editoren mit grafischer Oberfläche:
kate (Bestandteil von KDE)
gedit (Bestandteil von GNOME)
nedit
xemacs
...
Der Editor kate“ bietet außerdem die Möglichkeit, C-Funktionen einzuklap”
”
pen“, um den Quelltext übersichtlicher darzustellen (siehe Abbildung 1.4).
Nicht alle hier erwähnten Editoren sind in jeder Linux-Distribution enthalten.
Ggf. müssen die zugehörigen Pakete erst aus dem Internet geladen und gemäß
Abschnitt 1.2 installiert werden.
1.3 Die Werkzeuge
Abbildung 1.4: Zwei Editoren für Programmierer: nedit“ und kate“
”
”
7
8
1 Einführung
1.3.2 Der GNU C-Compiler gcc
Kernstück der Software-Entwicklung ist der C-Compiler selbst, also der gcc bzw.
g++ (für C++-Programme). Der gcc ist ein so genannter Cross-Compiler, mit dem
man im Grunde auch Programme für andere Betriebssysteme oder HardwarePlattformen (also andere Prozessoren) entwickeln kann. Der einfachste Aufruf des
Compilers lautet:
gcc Quelltext
So aufgerufen, wird der Quelltext kompiliert, assembliert und gelinkt, sodass
ein ausführbares Programm entsteht. Dieses Programm wird voreingestellt unter
dem Dateinamen a.out“ abgespeichert. In der Regel wird man diese Voreinstel”
lung nicht verwenden, sondern einen anderen, zweckmäßigeren Namen wählen
1
wollen. Dies geschieht mit Hilfe der Option -o“:
”
gcc Quelltext -o Ausgabedatei
Beispiel:
gcc hello.c -o hello
Bei diesem Aufruf führt der gcc zwei Schritte durch: das eigentliche Kompilieren
(Übersetzen) und das Linken zu einem ausführbaren Programm. Letzterer sorgt
z.B. dafür, dass Funktionsaufrufe wie printf() mit den entsprechenden Funktionen aus der dynamischen Bibliothek libc“ verknüpft werden. Wird ein Pro”
gramm auf mehrere Quelltexte aufgeteilt, so müssen die einzelnen Programmteile zunächst nur übersetzt werden, ohne den Linker aufzurufen. Dazu wird beim
Übersetzen die Option -c“ angegeben. Der Compiler erzeugt in diesem Fall nur
”
eine Objektdatei, die automatisch die Endung .o“ erhält. Ein Beispiel hierzu fin”
det sich in Abschnitt 1.4.5.
Der gcc besitzt eine Vielzahl weiterer Optionen, von denen wir in diesem Buch
nur einen kleinen Teil benötigen. Eine vollständige Beschreibung erhält man mit
man gcc“. Die Reihenfolge der Parameter und Optionen ist beim gcc – bis auf
”
wenige Ausnahmen – beliebig.
1.3.3 Ablaufsteuerung mit GNU make
Für das Übersetzen kleinerer Programme benötigt man in der Regel nur den CCompiler wie im vorherigen Abschnitt beschrieben. Bei umfangreicheren Projekten sollten Sie den Quelltext in mehrere Teile zerlegen. Dadurch werden die Dateien nicht nur übersichtlicher, es ist dann auch möglich, bei Änderungen nur diejenigen Dateien neu zu übersetzen, die geändert wurden. Genau hier setzt das
1
Sie sollten für erste Versuche nicht den Namen test“ wählen, da ein gleichnamiges Programm schon
”
Bestandteil der Shell ist!
1.3 Die Werkzeuge
9
Programm make“ an. Es prüft, ob sich die Quellen eines Programmteils geändert
”
haben, und übersetzt diesen Teil dann neu.
Das Tool make“ benötigt dazu eine Datei, in der die Abhängigkeiten der Quell”
und Zieldateien und die Anweisungen (Compiler-Aufrufe) eingetragen sind. Ein
Eintrag in dieser Datei, dem so genannten Makefile, sieht wie folgt aus:
Zieldatei:
Quelldatei1 Quelldatei2 . . .
Anweisung1
Anweisung2
...
Beispiel:
hello:
hello.c
gcc hello.c -o hello
Alle Anweisungszeilen müssen mit einem oder mehreren Tabulatoren ( echte“
”
Tabs, keine Leerzeichen!) eingerückt sein, während die Zieldatei immer am Zeilenanfang stehen muss. Eine solche Make-Datei kann beliebig viele Zieldateien
mit den zugehörigen Quelldateien und Anweisungen enthalten. Zur Veranschaulichung sei auf das Beispiel in Abschnitt 1.4.5 verwiesen.
Wird die Make-Datei Makefile“ oder makefile“ genannt, so kann make“ ohne
”
”
”
Parameter aufgerufen werden. Andernfalls lautet der Aufruf:
make -f Make-Datei
Sind in dem Makefile mehrere Zieldateien angegeben, kann durch die Eingabe
von
make Zieldatei
gezielt eine dieser Dateien erzeugt werden, wobei make auch hier automatisch die
Abhängigkeiten prüft und ggf. weitere, für die angegebene Zieldatei erforderliche
Dateien neu erzeugt. Ohne Angabe der Zieldatei wird immer die erste Datei im
Makefile erzeugt.
An dieser Stelle sei noch darauf hingewiesen, dass es sich bei dem Ziel nicht unbedingt um eine Datei handeln muss. So findet sich in Makefiles häufig ein Eintrag
der folgenden Form:
clean:
rm -f *.o
Mit dem Aufruf make clean“ werden dann Objekt-Dateien, die man nicht mehr
”
benötigt, gelöscht. Man beachte, dass hier keine Quelldateien angegeben sind, was
dazu führt, dass die Anweisung immer ausgeführt wird. Für eine ausführliche
Anleitung siehe auch man make“.
”
10
1 Einführung
1.3.4 Für die Fehlersuche: Die Debugger
Nur selten läuft ein Programm auf Anhieb einwandfrei. Schnell schleichen sich
Fehler ein, im Programmiererjargon Bugs“ (Wanzen1 ) genannt. Zur Lokalisie”
rung und Beseitigung der Bugs greift man zu einem Debugger“ (Entwanzer).
”
Unter Linux hat der Programmierer in die Wahl zwischen dem textbasierten GNU
Debugger gdb“ – dem Urvater“ der Debugger unter Linux – und verschiede”
”
nen grafischen Front-Ends. Ursprünglich gab es eine relativ rudimentäre grafische Oberfläche für den GNU Debugger namens xxgdb“. Dieses Projekt wur”
de aber vor geraumer Zeit durch ein von Grund auf neu gestaltetes Tool ersetzt,
den DDD“ (Abkürzung für Data Display Debugger“, siehe Abbildung 1.5). Der
”
”
DDD ist kein eigenständiger Debugger sondern eine grafische Benutzeroberfläche
für den GNU Debugger gdb. Das Tool wurde übrigens in Deutschland entwickelt!
Abbildung 1.5: Ein elektronischer Kammerjäger: der DDD“
”
Um einen Fehler in einem Programm mit Hilfe des Debuggers zu finden, muss
das Programm Zusatzinformationen enthalten, mit deren Hilfe der Debugger das
1
Dieser Ausdruck stammt noch aus der Zeit der Relais-Computer. Hier hatte sich einmal eine Wanze
zwischen die Relaiskontakte verirrt und dadurch Rechenfehler verursacht.
1.3 Die Werkzeuge
11
ausführbare Programm mit dem zugehörigen Quelltext in Verbindung bringen
kann. Diese Zusatzinformationen fügt der gcc mit Hilfe der Option -g ein:
gcc -g hello.c -o hello
Anschließend kann der Debugger aufgerufen werden, z. B.:
ddd hello
Hier können Sie nun so genannte Breakpoints setzen, das Programm schrittweise ausführen und den Inhalt von Variablen anzeigen. Kommt es zur Laufzeit des
Programms zu einem Fehler, der die Ausführung sofort abbricht – beispielsweise
eine Speicher-Zugriffsverletzung (Segmentation fault) oder eine Division durch
null –, so zeigt der Debugger die entsprechende Zeile im Quelltext an, die zu diesem Fehler geführt hat.
Übrigens: nach erfolgreicher Fehlerbeseitigung lassen sich die für die Ausführung
des Programms nicht notwendigen Debug-Zusatzinformationen mit
strip hello
wieder entfernen, ohne das Programm neu zu übersetzen. Dadurch reduziert sich
die Größe des Programms offtmals erheblich.
1.3.5 Integrierte Entwicklungsumgebungen
Als Alternative zur direkten“ Verwendung der bisher vorgestellten Werkzeuge
”
gibt es die Möglichkeit, mit einer integrierten Entwicklungsumgebung1 zu arbeiten. Dabei handelt es sich um ein Programm, das neben einem Quelltext-Editor
auch eine grafische Schnittstelle für Compiler, Debugger usw. bietet. Insbesondere Umsteiger aus der Windows-Welt“ finden mit Hilfe solcher Programme häufig
”
leichter den Einstieg in die Linux-Programmierung. Man sollte jedoch beachten,
dass Entwicklungsumgebungen keine Compiler, sondern eben nur Umgebungen
sind und zum Übersetzen und Linken des Quelltextes wieder auf den C-Compiler
gcc zurückgreifen.
Der Vorteil solcher Programme ist, dass das Wechseln zwischen den Werkzeugen
Editor, Compiler, Debugger usw. entfällt. Tritt beispielsweise beim Übersetzen des
Programms ein Fehler auf, wird automatisch die entsprechende Zeile im Quelltext
markiert.
1
auch als IDE“ für Integrated Development Environment bezeichnet
”
12
1 Einführung
Abbildung 1.6: KDevelop – Entwicklungsumgebung des KDE
Abbildung 1.7: Das Entwicklungs-Framework Eclipse
1.3 Die Werkzeuge
Abbildung 1.8: Die GNOME-Entwicklungsumgebung Anjuta
Abbildung 1.9: Entwicklungsumgebung à la Turbo-Pascal: xwpe
13
14
1 Einführung
Unter Linux sind verschiedene Entwicklungsumgebungen frei verfügbar; einige
dieser Linux-IDEs sind:
KDevelop (Entwicklungsumgebung des KDE), siehe Abbildung 1.6
Eclipse + C Development Tooling“ (kurz CDT), siehe Abbildung 1.7
”
Anjuta (Entwicklungswerkzeug des GNOME-Projektes), siehe Abbildung 1.8
xwpe (X-Window Programming Environment)
Alle vier Programme sind mausgesteuert, menügeführt und mit einer mehr oder
weniger umfangreichen Online-Hilfe ausgestattet, die über den entsprechenden
Menüpunkt aufgerufen werden kann.
Die aktuellen Versionen von KDevelop, Eclipse (inkl. CDT) und Anjuta sind recht
umfangreich und benötigen mehr als 50 MByte. KDevelop bringt es mit den zugehörigen Dokumentationspaketen sogar auf mehrere 100 MByte – ein Grund, bei
nicht so leistungsstarken Rechnern doch auf die schlanken“ Kommandozeilen”
Programme zurückzugreifen.
Eine Ausnahme bildet das nicht so bekannte Tool xwpe“ (Abbildung 1.9), das
”
deutlich weniger Ressourcen benötigt, aber auch nicht so komfortabel ist wie beispielsweise KDevelop.
Die Entwicklungsumgebung Eclipse fällt etwas aus der Reihe: Das in Java
programmierte Tool bildet eine Art Framework“. Für die Entwicklung von
”
Java-Programmen wird zusätzlich das Paket JDT“ (Java Development Tooling)
”
benötigt, für C-Programme entsprechend das Paket CDT“.
”
In Kapitel 2 beschreiben wir die Arbeit mit einer integrierten Entwicklungsumgebung anhand der Programme Anjuta und KDevelop eingehender.
1.4 Der Umgang mit Compiler, Debugger und
make“ anhand von Beispielen
”
Häufig sagt ein Beispiel mehr als tausend Worte; auch bei der Beschreibung von
Programmierwerkzeugen ist das nicht anders. Aus diesem Grund wird in den
folgenden Abschnitten die Handhabung der zuvor beschriebenen Werkzeuge anhand einfacher Beispiele demonstriert.
1.4.1 Primzahlen berechnen
Das folgende kleine C-Programm berechnet die Primzahlen von 1 bis 100, Zahlen also, die sich nur durch 1 und sich selbst teilen lassen.1 Es soll im Folgenden
dazu dienen, das Übersetzen und die Fehlersuche mit den bereits vorgestellten
Programmierwerkzeugen zu verdeutlichen.
1
Mathematisch exakt betrachtet, sind Primzahlen diejenigen Zahlen, die genau zwei Teiler besitzen.
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen
”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
15
/*
primzahl.c
*/
# include <stdio.h>
int ist_primzahl(int zahl)
{
int teiler=2;
while (teiler*teiler <= zahl)
{
if (zahl % teiler == 0)
return(0);
/* ’zahl’ ist keine Primzahl */
teiler++;
}
return(1);
/* ’zahl’ ist eine Primzahl */
}
int main()
{
int zahl;
for (zahl=1; zahl<=100; zahl++)
if (ist_primzahl(zahl))
printf("%d\n", zahl);
return(0);
}
Nach Eingabe dieses Quelltextes in einen Editor und Abspeichern als primzahl.c“
”
lässt sich das Programm mit
gcc primzahl.c -o primzahl
übersetzen. Danach existiert eine ausführbare Datei mit dem Namen primzahl“.
”
Der gcc stellt die Zugriffsrechte direkt so ein, dass der Besitzer der Datei (also
Sie) diese ausführen darf. Mit ls -F“ wird dies durch ein *“ hinter dem Da”
”
teinamen gekennzeichnet. Jetzt können Sie das Programm direkt starten. Dies geschieht im Normalfall einfach durch Eingabe des Programmnamens.
Sollte das aktuelle Verzeichnis .“ nicht in dem Suchpfad für ausführbare
”
Dateien eingetragen sein – dies ist bei den meisten Linux-Distributionen der Fall
–, muss dem Programmnamen ein ./“ vorangestellt werden. Alternativ kann
”
mit dem Kommando export PATH=$PATH:.“ das aktuelle Verzeichnis in dem
”
Suchpfad eingetragen werden.
16
1 Einführung
Jetzt sollte in Ihrem Terminal-Fenster eine Liste der Primzahlen von 1 bis 97 erscheinen.
1.4.2 Fehlersuche mit dem gcc
Nicht immer läuft das Ganze so reibungslos. Schnell hat sich ein Bug eingeschlichen, und das Programm funktioniert nicht richtig. Doch bevor Sie jetzt einen
Debugger bemühen, sollte zunächst noch einmal der C-Compiler zu Rate gezogen werden. Um dies zu demonstrieren, ändern Sie die Zeile 24 des Programms
wie folgt ab
24
for (zahl=1; zahl=100; zahl++)
↑
und kompilieren es erneut. Wenn Sie es jetzt starten, passiert nichts. Das Programm rechnet und rechnet, ohne jemals eine Ausgabe zu erzeugen. Mit der Tastenkombination Strg“ (bzw. Ctrl“) und c“ können Sie das Programm abbre”
”
”
chen.
Was ist geschehen? Durch Entfernen des <“ ist aus dem Vergleich zahl<=100“
”
”
eine Zuweisung geworden, d.h. zahl wird bei jedem Schleifendurchlauf auf 100
gesetzt. Anders als z. B. bei BASIC oder Pascal liefert in C eine Zuweisung ebenso
wie ein Vergleich einen Wert zurück, nämlich den zugewiesenen Wert (hier 100).
Die for-Anweisung prüft nun bei jedem Schleifendurchlauf, ob der Rückgabewert falsch“ ist (was der Zahl 0 entspricht), um in diesem Fall die Schleife zu
”
beenden. Da jetzt der Rückgabewert immer 100 ist – und damit ungleich 0 –, wird
die for-Schleife nie beendet, und zahl bleibt immer 100.
Wie lässt sich nun dieser in C“ relativ häufige Fehler mit Hilfe des C-Compilers
”
gcc finden? Dazu geben Sie bitte folgende Zeile ein:
gcc -Wall primzahl.c -o primzahl
Sie erhalten dann eine Warnung des Compilers bezüglich der fehlerhaften Zeile
24:
primzahl.c:24: warning: suggest parentheses around
assignment used as truth value
Der Compiler weist also darauf hin, dass hier eine in Klammern gesetzte Zuweisung als Wahrheitswert ( wahr“ oder falsch“) verwendet wird. Doch was bedeu”
”
tet die Option -Wall“? Hiermit kann die Art der Warnmeldungen des Compilers
”
eingestellt werden. Die Syntax dieser Option lautet -WWarnungen“, wobei War”
nungen z.B. implicit“ (Verwendung einer nicht deklarierten Funktion) oder unu”
”
sed“ (Deklaration von Variablen, die nicht benutzt werden) sein kann – oder eben
auch all“ für alle Warnungen.
”
Viele Tippfehler“ in einem Quelltext lassen sich auf diese Weise relativ schnell
”
finden, ohne das kompilierte Programm überhaupt zu starten.
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen
”
17
1.4.3 Fehlersuche mit dem GNU Debugger
In diesem Abschnitt wird die Bedienung des GNU Debuggers an dem PrimzahlBeispiel von oben demonstriert. Dabei kommt zunächst die KommandozeilenVersion gdb“ zum Einsatz. Anschließend wird gezeigt, wie mit dem grafischen
”
Front-End kdbg“ gearbeitet wird.
”
Um den Einsatz eines Debuggers zu demonstrieren, soll in das PrimzahlProgramm ein anderer Fehler eingebaut werden. Dazu korrigieren Sie zunächst
die Zeile 24 und bringen so den Quelltext wieder in Übereinstimmung mit dem in
Abschnitt 1.4.1 abgedruckten Listing. Dann ändern Sie bitte Zeile 9 wie folgt ab:
9
int teiler=0;
↑
Danach übersetzen Sie das Programm, diesmal jedoch mit der Option -g“, um
”
die Verwendung eines Debuggers zu ermöglichen (vgl. auch Abschnitt 1.3.4):
gcc -Wall -g primzahl.c -o primzahl
Wenn Sie das Programm jetzt starten, erhalten Sie die Meldung
Gleitkomma-Ausnahme
Zur Fehleranalyse rufen Sie bitte den gdb auf und geben das Primzahl-Programm
als Parameter an. Der Debugger lädt dann automatisch das Programm, führt es
aber nicht sofort aus. Um das Programm zu starten, muss das gdb-Kommando
run“ eingegeben werden (die Benutzereingaben sind schräg dargestellt):
”
> gdb primzahl
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
(gdb) run
Starting program: /home/martin/TeX/Linux-Buch/primzahl
Program received signal SIGFPE, Arithmetic exception.
0x8048326 in ist primzahl (zahl=1) at primzahl.c:13
13
(gdb)
if (zahl % teiler == 0)
Der Debugger stoppt die Ausführung des Programms automatisch beim Auftreten des Fehlers (hier Arithmetic exception“ = Gleitkomma-Ausnahme) und zeigt
”
uns die zugehörige Zeile im Quelltext mitsamt Zeilennummer an. Um der Ursache für den Fehler jetzt auf den Grund zu gehen, kann man sich unter anderem
Variablen-Inhalte anzeigen lassen, und zwar durch das Kommando print“:
”
18
1 Einführung
(gdb) print zahl
$1 = 1
(gdb) print teiler
$2 = 0
Hier sieht man, dass bei der Operation zahl % teiler“ eine Division durch
”
Null erfolgt, was zum Abbruch des Programms führt. Also muss der Startwert
für teiler mit einer Zahl > 0 initialisiert werden. Nach erfolgreicher Fehlersuche
können Sie den Debugger mit quit“ beenden.
”
Breakpoints
Nicht immer führt ein Fehler im Programm zu einem Abbruch, der wie oben
gezeigt analysiert werden kann. Häufig läuft das Programm, liefert jedoch nicht
das gewünschte Ergebnis. Um dies bei unserem Beispielprogramm zu erreichen,
ändern Sie bitte die Zeile 9 wie folgt:
9
int teiler;
↑
Wenn Sie es jetzt kompilieren (bitte wieder mit der Option -g) und ausführen, so
erhalten Sie eine Liste aller Zahlen von 1 bis 100. Die Funktion ist primzahl()
funktioniert nicht mehr. Um in einem solchen Fall dem Fehler auf die Spur zu
kommen, muss das Programm während der Ausführung zwischendurch angehalten werden, um z.B. Variablen-Inhalte zu überprüfen. Genau dies ist mit Breakpoints möglich. Über das gdb-Kommando break“ lässt sich das Programm z.B.
”
bei jedem Aufruf der Funktion ist primzahl() unterbrechen:
> gdb primzahl
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
(gdb) break ist primzahl
Breakpoint 1 at 0x8048306: file primzahl.c, line 11.
(gdb) run
Starting program: /home/martin/TeX/Linux-Buch/primzahl
Breakpoint 1, ist primzahl (zahl=1) at primzahl.c:11
11
while (teiler*teiler <= zahl)
(gdb) print teiler
$1 = 1073972219
Die Variable teiler hat hier offensichtlich einen unsinnigen Wert – sie wurde vor
Verwendung in Zeile 11 nicht initialisiert.
Neben der Verwendung von Breakpoints ist auch ein schrittweises Ausführen des
Programms mit Hilfe von next“ und nexti“ häufig sehr hilfreich. Außerdem
”
”
bietet der Debugger eine Vielzahl weiterer Möglichkeiten, die sich am besten am
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen
”
19
realen Problem“ erforschen lassen. Hilfestellung erfahren Sie mit dem Komman”
do help“.
”
Verwendung des grafischen Front-Ends kdbg
Das Arbeiten mit dem DDD und dem kdbg hat gegenüber dem gdb den Vorteil, dass die aktuell ausgeführte Programmzeile sowie die Breakpoints direkt im
zugehörigen Quelltext markiert werden. Die Fehlersuche wird dadurch deutlich
übersichtlicher.
Ebenso wie beim gdb kann auch beim Aufruf der grafischen Front-Ends DDD und
kdbg das zu analysierende Programm als Parameter angegeben werden:
> kdbg primzahl
Der kdbg lädt nicht nur das Programm, sondern öffnet auch direkt den Quelltext und markiert die Zeile, in der die Ausführung des Programms beginnen wird (also die Funktion main()). Gestartet wird das Programm über das
Menü Ausführen“ oder die ensprechende Schaltfläche unter der Menüleiste, sie”
he Abbildung 1.10.
Abbildung 1.10: Die Ausführung des Programms im kdbg“ starten.
”
Tritt während der Ausführung des Programms ein Fehler auf – beispielsweise die
Gleitkomma-Ausnahme“ wie in dem Beispiel oben –, so wird die verursachende
”
Zeile direkt im Quelltext markiert (siehe Abbildung 1.11). Zur Anzeige der aktuellen Werte der Variablen kann der Menüpunkt View/Lokale Variablen“ gewählt
”
werden. Breakpoints können Sie mit dem Menüpunkt Haltepunkt“ setzen.
”
1.4.4 Funktionsbibliotheken verwenden
Dem C-Programmierer stehen für viele verschiedene Aufgabenstellungen umfangreiche Funktionsbibliotheken (engl. Libraries) zur Verfügung; beispielswei-
20
1 Einführung
Abbildung 1.11: Der kdbg markiert die Zeile, die zum Abbruch des Programms führt,
direkt im Quelltext.
se bei der Grafik-Programmierung wird hiervon intensiv Gebrauch gemacht.
Die Verwendung solcher Bibliotheken soll in diesem Abschnitt anhand der
Mathematik-Bibliothek libm“ demonstriert werden, die all jene Funktionen
”
enthält, die über die Grundrechenarten hinausgehen.
Auch hierzu wird wieder unser Primzahl-Programm als Beispiel dienen. Bitte
bringen Sie es zunächst wieder in die ursprüngliche Form gemäß Abschnitt 1.4.1.
In Zeile 11 wird überprüft, ob die Variable teiler schon größer als die Quadratwurzel aus zahl ist, denn Nicht-Primzahlen besitzen mindestens einen Teiler, der
kleiner als ihre Quadratwurzel ist. In der bisherigen Version des Programms wurde diese Abbruchbedingung der while()-Schleife mit Hilfe einer Multiplikation
umschrieben. Bitte ändern Sie diese Zeile nun wie folgt ab:
11
while (teiler <= sqrt(zahl))
und greifen damit auf die Quadratwurzel-Funktion sqrt() aus der mathematischen Bibliothek zu. Damit der C-Compiler weiß, von welchem Typ der Parameter
und der Rückgabewert dieser Funktion sind (hier double), muss die entsprechende Include-Datei eingebunden werden:
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen
”
6
21
# include <math.h>
Für das Übersetzen des Programms muss dem C-Compiler die Bibliothek angegeben werden, auf die das Programm zugreift – in diesem Fall also die MathematikBibliothek libm. Das geschieht mit Hilfe der Option -lBibliothek“, wobei das jeder
”
Bibliothek vorangestellte lib“ weggelassen wird:
”
gcc primzahl.c -lm -o primzahl
Jetzt können Sie das Programm wie zuvor starten – mit dem gleichen Resultat.
Es können beliebig viele Bibliotheken gleichzeitig verwendet werden, indem die
Option -l“ mehrfach angegeben wird. Sollte sich eine Bibliothek nicht in dem
”
dafür standardmäßig vorgesehenen Verzeichnis befinden (/usr/lib), kann mit
der Option -L“ ein zusätzlicher Suchpfad angegeben werden. Auch diese Option
”
lässt sich mehrfach verwenden. Beispiel:
gcc -L/usr/X11R6/lib -L/usr/openwin/lib my_program.c -lX11
-lxview -lolgx -lm -o my_program
Die Bibliothek libc, die Funktionen wie printf() enthält, wird übrigens automatisch
eingebunden und muss nicht explizit angegeben werden.
Übrigens: Eigene Funktionsbibliotheken kann man mit dem Shell-Befehl ar“ er”
stellen. Details dazu erhalten Sie mit man ar“.
”
1.4.5 Quelltexte aufteilen
Während sich das in dem Abschnitt 1.4.1 vorgestellte Beispielprogramm zur Primzahlberechnung noch geeignet an einem Stück“ programmieren ließ, werden
”
C-Programme größeren Umfangs recht schnell unübersichtlich, was wiederum
leicht zu vermeidbaren Fehlern führt. Daher sollten bei der Erstellung eines Programms direkt zu Beginn Funktionseinheiten festgelegt werden, denen sich die
einzelnen Programmteile bzw. Funktionen zuordnen lassen. Abbildung 1.12 zeigt,
wie diese Strukturierung für ein Programm aussehen könnte, welches sowohl eine
grafische Oberfläche ( GUI“ für Graphical User Interface) als auch mathematische
”
Berechnungsroutinen enthält.
Der Quelltext für das Programm my prog“ wird hier in die Dateien main.c“,
”
”
gui.c“ und mathfunc.c“ aufgeteilt. Die Include-Dateien gui.h“ und math”
”
”
”
func.h“ enthalten die Deklarationen der in gui.c“ bzw. mathfunc.c“ definierten
”
”
Funktionen, soweit sie in den anderen Programmteilen verwendet werden.
Um diese Vorgehensweise zu veranschaulichen, sei noch einmal das Programm
zur Primzahlberechnung herangezogen. Obwohl eine Aufteilung dieses kurzen Quelltextes keinen Gewinn an Übersichtlichkeit bringt, lässt sich daran das
Prinzip gut demonstrieren. Dazu zerlegen wir den Quelltext in einen Hauptteil
primz haupt.c“:
”
22
1 Einführung
Hauptprogramm
grafische Oberfläche
math. Funktionen
main.c
gui.h
mathfunc.h
gui.c
mathfunc.c
?
?
main.o
?
gui.o
q
?
mathfunc.o
)
ausführbares
Programm
my prog
Abbildung 1.12: Beispiel für die strukturierte Aufteilung eines Quelltextes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
primz_haupt.c
*/
# include <stdio.h>
# include "primz_math.h"
int main()
{
int zahl;
for (zahl=1; zahl<=100; zahl++)
if (ist_primzahl(zahl))
printf("%d\n", zahl);
return(0);
}
– in einen mathematischen Teil primz math.c“:
”
1
/*
2
primz_math.c
3
*/
4
# include <math.h>
5
6
7
int ist_primzahl(int zahl)
8
{
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen
”
9
10
11
12
13
14
15
16
17
18
23
int teiler=2;
while (teiler <= sqrt(zahl))
{
if (zahl % teiler == 0)
return(0);
/* ’zahl’ ist keine Primzahl */
teiler++;
}
return(1);
/* ’zahl’ ist eine Primzahl */
}
– und in die zugehörige Include-Datei primz math.h“:
”
1
/*
2
primz_math.h
/
3
*
4
5
int ist_primzahl(int zahl);
Der Programmteil primz math.c“ stellt die Funktion ist primzahl()“ zur
”
”
Verfügung, deren Parameter und Rückgabewert in primz math.h“ deklariert
”
sind. Durch die include-Anweisung in Zeile 6 der Datei primz haupt.c“ wird
”
diese Deklaration im Hauptteil verfügbar gemacht.
Doch wie erzeugt man nun aus diesen Quelldateien das ausführbare Programm?
Dazu müssen zunächst die einzelnen Programmteile in Objektdateien übersetzt
werden:
gcc -c primz_haupt.c
gcc -c primz_math.c
Diese Compiler-Aufrufe erzeugen die beiden Dateien primz haupt.o“ und
”
primz math.o“, aus denen wiederum mit der Anweisung
”
gcc primz haupt.o primz math.o -lm -o primzahl
das ausführbare Programm primzahl“ generiert wird. Man beachte, dass erst bei
”
dem letzten Schritt das Einbeziehen der Mathematik-Bibliothek (Option -lm“)
”
erfolgt.
Für das vollständige Übersetzen des Programms benötigen wir statt eines gccAufrufs nun drei Aufrufe. Genau hier kommt das Programm make“ ins Spiel, das
”
diesen Ablauf intelligent automatisiert – intelligent deshalb, weil nur die wirklich
erforderlichen Schritte durchgeführt werden. Die Make-Datei für das PrimzahlBeispiel könnte wie folgt aussehen:
1
2
#
# Makefile
24
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 Einführung
#
primzahl:
primz_haupt.o primz_math.o
gcc primz_haupt.o primz_math.o -lm \
-o primzahl
primz_haupt.o:
primz_haupt.c primz_math.h
gcc -c primz_haupt.c
primz_math.o:
primz_haupt.c
gcc -c primz_math.c
clean:
rm -f primz_haupt.o primz_math.o
Wird diese Make-Datei als Makefile“ oder makefile“ abgespeichert, so kann das
”
”
Programm primzahl“ einfach durch den Aufruf
”
make
erzeugt werden. Da es sich bei dem Programm make“ um ein gängiges Werk”
zeug handelt, könnte man den Quelltext für das Primzahl-Programm in dieser
Form (z.B. als tar-Archiv verschnürt) an andere Linux- oder Unix-Anwender weitergeben und ziemlich sicher sein, dass sie in der Lage wären, das Programm zu
kompilieren.
Das obige Beispiel für ein Makefile zeigt die rudimentäre Grundfunktion von
make“. Das Werkzeug bietet aber noch viele Möglichkeiten, die das Erstellen
”
von Makefiles für wirklich große Projekte – wie beispielsweise den Linux-Kernel
– vereinfachen. Einen kleinen Einblick soll Ihnen das folgende Makefile geben,
das ebenfalls das Primzahl-Programm kompiliert:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#
# Makefile2
#
TARGET
OBJECTS
HEADERS
LIBS
=
=
=
=
primzahl
primz_haupt.o primz_math.o
primz_math.h
-lm
$(TARGET): $(OBJECTS)
gcc $ˆ $(LIBS) -o $@
primz_%.o: primz_%.c $(HEADERS)
gcc -c $< -o $@
1.5 Weiterführende Informationen
15
16
17
25
clean:
rm -f $(OBJECTS)
In den Zeilen 5 bis 8 werden Konstanten definiert – ähnlich den #defineAnweisungen in C. Der Vorteil ist, dass die projektspezifischen Dateinamen nur
noch hier vorkommen; im restlichen Makefile tauchen nur mehr die Namen der
Konstanten auf!
In den Zeilen 10 und 11 wird festgelegt, wie das eigentliche Ziel (hier das Programm primzahl“) zu generieren ist. Dabei werden die zuvor definierten Kon”
stanten mit $(Name)“ eingesetzt. In Zeile 11 steht anstelle der Objektdateien hin”
ter dem gcc der Ausdruck $ˆ“. Dies bewirkt, dass alle in der Zeile 10 angegebe”
nen Quelldateien an dieser Stelle eingesetzt werden. Zusätzlich steht am Ende der
Zeile 11 der Ausdruck $@“. Dieser wird automatisch durch das zu dieser Regel
”
gehörende Ziel ersetzt – in diesem Fall also $(TARGET)“ bzw. primzal“.
”
”
Besonders interessant sind die Zeilen 13 und 14. Hier wird nämlich keine bestimmte Zieldatei beschrieben, sondern alle Dateien, die mit primz “ anfangen
”
und auf .o“ enden! Damit gilt diese Regel für die Objektdateien primz haupt.o“
”
”
und primz math.o“. Bei der Angabe der Quelldateien hinter dem Doppelpunkt
”
steht wiederum der Ausdruck primz_%“. Das Prozentzeichen wird an dieser
”
Stelle durch den gleichen Text ersetzt wie in der Zieldatei vor dem Doppelpunkt.
Damit wird den verschiedenen Zieldateien automatisch die passende Quelldatei zugeordnet. In Zeile 14 steht als Quelldatei für den C-Compiler der Ausdruck
$<“. Das Programm make ersetzt diesen Ausdruck durch die erste der in Zeile 13
”
angegebenen Quelldateien, also primz_%.c“.
”
Auf diese Weise lassen sich Makefiles für Programme mit vielen Objektdateien
auf wenige Zeilen reduzieren!
1.5 Weiterführende Informationen
Im Folgenden werden zahlreiche Werkzeuge und Funktionsbibliotheken angesprochen, deren vollständige Beschreibung den Rahmen des Buches bei weitem
sprengen würde. Daher konzentrieren wir uns darauf, für die vielen verschiedenen Bereiche (Device-Programmierung, Prozesskommunikation, Netzwerk- und
Grafikprogrammierung usw.) jeweils einen Einstieg zu bieten. Wenn Sie die verschiedenen Gebiete mit eigenen Programmen weiter vertiefen, werden Sie immer
wieder auf Fragen stoßen, deren Antwort Sie nicht in unserem Buch finden. Aus
diesem Grund soll in den folgenden Abschnitten gezeigt werden, wie man sich
anhand der zu Linux gehörenden Hilfe- und Include-Dateien an die Möglichkeiten des Systems weiter herantasten“ kann.
”
26
1 Einführung
1.5.1 Die Unix-Online-Hilfen man“, xman“ und tkman“
”
”
”
Eine der wichtigsten Hilfen zur Beschaffung weiterer Informationen ist das Programm man“ (Kurzform für Manual). Neben der Dokumentation fast aller Linux”
Programme stellt man“ auch eine ausführliche Beschreibung der meisten C”
Funktionen zur Verfügung. Die Beschreibungstexte werden auch als Man-Pages“
”
bezeichnet. Leider beinhalten einige Linux-Distributionen nur einen Teil der ManPages, oder es wird voreingestellt nur eine kleine Auswahl installiert.1 Bei SuSELinux müssen beispielsweise die beiden Pakete man“ und man-pages“ aus”
”
gewählt werden, um Man-Pages lesen zu können (siehe auch Abschnitt 1.2.1),
wobei die Man-Pages zu X11 in einem separaten Paket namens xorg-x11-man“
”
enthalten sind. Damit sie sich korrekt darstellen lassen, werden außerdem die
Tools groff“ und less“ benötigt, was aber durch die Paketabhängigkeiten au”
”
tomatisch berücksichtigt werden sollte.
Zur Anzeige der Man-Page einer C-Funktion oder eines Linux-Programms kann
man“ einfach mit dem Programm- oder Funktionsnamen als Parameter aufgeru”
fen werden. So liefert beispielsweise
man gcc
eine Beschreibung des C-Compilers mit seinen Parametern. In gleicher Weise
erhält man mit
man fprintf
eine Erläuterung der entsprechenden C-Funktion (aus der libc) und damit eng
verwandter Funktionen. Bitte beachten Sie auch den Hinweis SIEHE AUCH“
”
(bzw. SEE ALSO“) jeweils am Ende der Hilfe-Seiten.
”
Häufig kommt es jedoch vor, dass Sie nicht genau wissen, wie die Funktion oder
das Programm heißt, das Sie einsetzen möchten. Beispiel: Sie wollen einen Grafikbereich kopieren und suchen eine entsprechende Funktion aus der GrafikFunktionsbibliothek. In diesem Fall können mit der Option -k“ alle Hilfeseiten
”
nach einem Keyword (Schlüsselwort) durchsucht werden:1
> man -k copy area
XCopyArea (3x)
XCopyPlane (3x)
- copy areas
- copy areas
Die erste Angabe in jeder Zeile gibt den genauen Namen der Funktion (oder des
Programms) an. Danach folgt in Klammern die Sektion, in der diese Funktion beschrieben ist. Als Letztes folgt eine stichwortartige Beschreibung der Funktion.
1
Glüchlicherweise gibt es eine vollständige Online-Sammlung der Man-Pages im Internet, siehe [6].
1
Vorausgesetzt, dass eine entsprechende Datenbank mit mandb“ angelegt wurde. Alternativ zu man
”
”
-k“ kann auch der Befehl whatis“ verwendet werden, der wiederum eine mit makewhatis“ er”
”
stellte Datenbank benötigt.
1.5 Weiterführende Informationen
27
Mit man XCopyArea“ könnte in diesem Beispiel dann die konkrete Beschreibung
”
dieser Funktion angefordert werden.
Manchmal existieren zu einem Funktions-/Programmnamen mehrere Beschreibungen in unterschiedlichen Sektionen, beispielsweise dann, wenn es eine CFunktion und ein Shell-Programm gleichen Namens gibt:
> man -k sleep
sleep (1) - delay for a specified amount of time
sleep (3) - Sleep for the specified number of seconds
Sektion 1 enthält hier die Beschreibung des Shell-Kommandos, Sektion 3 die der
gleichnamigen C-Funktion. In diesem Fall kann über die Angabe der Sektion gezielt eine der beiden Seiten abgerufen werden:
man 3 sleep
Abbildung 1.13: xman – eine grafische Benutzerschnittstelle für man“
”
In der Tabelle 1.1 sind die Sektionen der Online-Hilfe aufgelistet. Um die Suche nach der richtigen Beschreibung zu vereinfachen, kann auch eine grafische
Benutzerschnittstelle wie xman“ oder tkman“ verwendet werden (siehe Abbil”
”
dung 1.13). xman“ listet – nach Sektionen geordnet – alle verfügbaren Hilfe-Texte
”
auf. Durch Anklicken der entsprechenden Funktion können Sie die gewünschte
Beschreibung abrufen.
28
1 Einführung
Tabelle 1.1: Beschreibung der Sektionen der Online-Hilfe man“
”
Sektion
1
2
3
4
5
6
7
8
Beschreibung
Shell-Kommandos
System-Aufrufe
Funktionen
Devices
Dateiformate
Spiele
Verschiedenes
System-Administration
1.5.2 Ein Blick hinter die Kulissen: Die Include-Dateien
Nicht immer bietet die Online-Hilfe man“ zu den C-Funktionen auch die genaue
”
Beschreibung der relevanten Strukturen. So liefern oder benötigen fast alle Funktionen der gepufferten Ein-/Ausgabe (z.B. fopen(), fclose(), fprintf(), . . . ) einen
Zeiger auf eine Struktur vom Typ FILE“. Schaut man einmal in die für diese
”
Klasse von Funktionen relevante Include-Datei /usr/include/stdio.h“, so findet
”
sich dort die Typendefinition:
typedef struct _IO_FILE FILE;
FILE“ ist also identisch mit der Struktur struct IO FILE“. Diese Struktur wie”
”
derum ist in der Include-Datei /usr/include/libio.h“ definiert:
”
struct _IO_FILE {
int _flags;
/* High-order word is _IO_MAGIC; rest is
flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf
protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields
directly. */
char* _IO_read_ptr;
/* Current read pointer */
char* _IO_read_end;
/* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base;
/* Start of reserve area. */
char* _IO_buf_end;
/* End of reserve area. */
/* The following fields are used to support backing up and
undo. */
char *_IO_save_base; /* Pointer to start of non-current get
area. */
1.5 Weiterführende Informationen
29
char *_IO_backup_base;
/* Pointer to 1st valid char. of backup
area */
char *_IO_save_end;
/* Pointer to end of non-current get
area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _blksize;
_IO_off_t _old_offset; /* This used to be _offset but it’s too
small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
Wie man sieht, ist hier nicht mit Kommentaren gespart worden. Auf diese Weise
kann man sich eine ganze Reihe nützlicher Informationen beschaffen.
Hilfreich ist hier auch die Verwendung des Programms grep“, mit dem Sie alle
”
Include-Dateien nach bestimmten Zeichenketten durchsuchen können, z.B.:
> grep ’typedef .* FILE;’ /usr/include/*.h
/usr/include/stdio.h:typedef struct IO FILE FILE;
Ist nicht bekannt, ob die entsprechende Include-Datei in einem Unterverzeichnis
(z.B. sys“ oder linux“) liegt, so kann grep auch mit dem Befehl find“ kombiniert
”
”
”
werden:
grep ’typedef .* FILE;’ ‘find /usr/include -follow
-name "*.h"‘
Damit lässt sich relativ schnell die gesuchte Definition aufspüren.
Kapitel 2
Arbeiten mit einer
Entwicklungsumgebung
Nachdem in Kapitel 1 der Umgang mit den Programmierwerkzeugen auf der
”
Kommandozeile“ beschrieben wurde, soll in diesem Kapitel das Arbeiten mit einer integrierten Entwicklungsumgebung gezeigt werden. Dafür wurden die Programme Anjuta“ und KDevelop“ ausgewählt. Abschließend reißen wir noch
”
”
kurz den Einstieg in das Tool Eclipse“ an.
”
2.1 Anjuta
Die Entwicklungsumgebung Anjuta“ ist an die Oberfläche GNOME ange”
lehnt und unterstützt daher auch im Besonderen die Erstellung von GNOMEApplikationen. Shell-Programme ohne grafische Oberfläche lassen sich selbstverständlich ebenfalls mit Anjuta erstellen. Anjuta ist viel zu umfangreich, um
in diesem Buch vollständig beschrieben werden zu können. In den folgenden Abschnitten soll stattdessen Schritt für Schritt die Erstellung eines einfachen Beispielprogramms unter dieser Entwicklungsumgebung gezeigt werden.
2.1.1 Ein neues Projekt anlegen
Nach dem Starten der Entwicklungsumgebung mit dem gleichnamigen Befehl
> anjuta
öffnet sich das Hauptfenster, das eine ganze Reihe von Menüs und Schaltflächen,
aber noch kein Editor-Fenster für die Eingabe von Quelltexten enthält. Um ein
neues Programm zu erstellen, sollte man zunächst ein neues Projekt anlegen. Dies
32
2 Arbeiten mit einer Entwicklungsumgebung
geschieht mit dem Menüpunkt Datei/neues Projekt“. Damit wird der Anwen”
”
dungsdruide“ gestartet, der in einem Dialog die Art des neu zu erstellenden Projekts und dessen Eigenschaften abfragt und daraus das Grundgerüst des Projekts
erstellt.
Abbildung 2.1: Die verschiedenen Projekttypen bei Anjuta
Als Projekttyp wählen Sie bitte Generic/Terminal Projcet“ aus (siehe Abbil”
dung 2.1) und geben als Projektname primzahl“ ein. Das anschließend erschei”
nende Eingabefeld für eine Projektbeschreibung können Sie leer lassen. Unter
Projekt Optionen“ sollten Sie die Unterstützung von Gettext deaktivieren. Da”
nach werden Sie um die Bestätigung Ihrer Eingaben gebeten:
2.1 Anjuta
33
Bestätigen Sie die folgenden Informationen:
Projektname:
Projekttyp:
Zieltyp:
Quelltyp:
Version:
Autor:
Sprache:
Gettext Support:
primzahl
GENERIC
EXECUTABLE
primzahl
0.1
C
Nein
Mit der Schaltfläche Abschließen“ bestätigen Sie die Einstellungen und gelangen
”
wieder zum Hauptfenster von Anjuta, das jetzt ein Fenster mit der Projektstruktur
und eines mit Compiler-Ausgaben enthält (siehe Abbildung 2.2).
Abbildung 2.2: Das neu angelegte Projekt primzahl“
”
2.1.2 Eingabe der Quelltexte
Durch einen Doppelklick auf main.c“ in dem Fenster mit der Projektübersicht
”
wird ein Editor-Fenster geöffnet, in dem diese Datei bearbeitet werden kann. Ersetzen Sie hier bitte den von Anjuta standardmäßig vorbereiteten Hello-WorldQuelltext durch Folgendes:
34
2 Arbeiten mit einer Entwicklungsumgebung
/*
main.c
*/
# include <stdio.h>
# include "primzahl.h"
int main()
{
int zahl;
for (zahl=1; zahl<=100; zahl++)
if (ist_primzahl(zahl))
printf("%d\n", zahl);
return(0);
}
Über den Menüpunkt Datei/Speichern“ können Sie die Änderungen speichern.
”
Als Nächstes soll der Quelltext der Funktion ist primzahl()“ in einer zweiten Da”
tei erstellt werden. Dazu wählen Sie den Menüpunkt Datei/Neu“ und geben in
”
das neue Editor-Fenster folgenden Quelltext ein:
/*
primzahl.c
*/
# include <math.h>
int ist_primzahl(int zahl)
{
int teiler=2;
while (teiler <= sqrt(zahl))
{
if (zahl % teiler == 0)
return(0); /* ’zahl’ ist keine Primzahl */
teiler++;
}
return(1);
/* ’zahl’ ist eine Primzahl */
}
Speichern Sie diese Datei dann mit Datei/Speichern unter...“ im Verzeichnis
”
Projects/primzahl/src“ als primzahl.c“ ab.
”
”
In der gleichen Weise erstellen Sie die zugehörige Include-Datei primzahl.h“:
”
2.1 Anjuta
35
/*
primzahl.h
*/
int ist_primzahl(int zahl);
Jetzt können die neu angelegten Dateien dem Projekt hinzugefügt werden. Dazu wählen Sie den Menüpunkt Projekt/Add File/Source-Datei“ für die Datei
”
primzahl.c“ und Projekt/Add File/Include-Datei“ für die Datei primzahl.h“.
”
”
”
Jetzt sollte die Struktur des Projektes so aussehen, wie in Abbildung 2.3 dargestellt.
Abbildung 2.3: Die fertige Struktur des Projektes primzahl“
”
2.1.3 Kompilieren und Starten des Beispiels
Um jetzt aus den Quelltexten des Beispielprojektes primzahl“ ein ausführba”
res Programm zu erzeugen, wählen Sie bitte den Menüpunkt Generieren/Ge”
nerieren“. Danach erscheint im Ausgabefenster am unteren Rand eine Reihe von
Compiler-Meldungen, die – wenn alles richtig gemacht wurde – mit folgendem
Text enden:
Übersetzungsvorgang beendet..........erfolgreich
Benötigte Zeit: 2 Sek.
Zum Starten des Beispielprogramms können Sie jetzt das in Anjuta eingebaute
Terminal-Fenster verwenden. Klicken Sie dazu bitte auf den Reiter Terminal“ am
”
unteren Rand des Ausgabe-Fensters und geben in dem Fenster
36
2 Arbeiten mit einer Entwicklungsumgebung
> primzahl
ein. Es erscheinen die Primzahlen von 1 bis 97.
2.2 KDevelop
Die Desktopumgebung KDE bringt ihr eigenes Entwicklungswerkzeug mit: KDevelop. Das Tool kann über das Desktop-Menü gestartet werden (bei SuSE-Linux
Entwicklung/Entwicklungsumgebung/KDevelop C/C++“) oder aus einer Shell
”
über das Kommando kdevelop“.
”
Um ein neues Projekt in KDevelop anzulegen, wählen Sie den Menüpunkt Pro”
ject/New Project...“. Danach öffnet sich ein Fenster zur Auswahl des Projekttyps
(siehe Abbildung 2.4). Wählen Sie hier bitte den Typ C/Simple Hello world pro”
gram“ aus und tragen als Application name“ den Projektnamen primzahlen“
”
”
ein.
Abbildung 2.4: Die Auswahl des Projekttyps bei KDevelop
Nach der Auswahl des Projekttyps fragt KDevelop nach General Options“ –
”
hier können Sie einfach auf next“ klicken – und nach dem Version control sy”
”
stem“, das Sie auf none“ stellen sollten. Danach zeigt KDevelop die Templates
”
2.2 KDevelop
37
für Header- und Quelltextdateien an. Auch hier können Sie einfach auf next“
”
bzw. finish“ klicken. Danach befinden Sie sich wieder in dem Hauptfenster
”
von KDevelop mit geöffnetem Editorbereich. Dort ist bereits ein Hello world“”
Quelltext eingetragen. Ersetzen Sie diesen bitte durch den Primzahlen-Quelltext
von Seite 34 oben. Danach speichern Sie die Datei bitte über File/Save“.
”
Als Nächstes benötigen wir noch den Quelltext mit der Funktion ist primzahl().
Dazu wählen Sie bitte den Menüpunkt File/new“ und geben als Dateinamen
”
primz math.c“ ein (Abbildung 2.5).
”
Abbildung 2.5: Eine weitere Quelltextdatei in KDevelop anlegen
Nach dem Bestätigen dieses Dialogs mit OK“ öffnet sich ein Fenster mit dem
”
Automake Manager“. Bestätigen Sie auch diesen Dialog mit OK“.
”
”
Um alle zum Projekt gehörenden Dateien aufzulisten, klicken Sie die Schaltfläche
File List“ am linken Rand des Hauptfensters an. Danach können Sie die neue Da”
tei primz math.c“ durch einen Klick auf den Dateinamen in dem Editorbereich
”
öffnen. Geben Sie hier bitte den entsprechenden Quelltext von Seite 34 unten ein.
Legen Sie bitte in gleicher Weise die Header-Datei primz math.h“ an und geben
”
dort Folgendes ein:
/*
primz_math.h
*/
int ist_primzahl(int zahl);
Damit sind nun alle Quelltexte für unser kleines Projekt eingegeben, und das
KDevelop-Hauptfenster müsste sich wie in Abbildung 2.6 darstellen.
Für das Übersetzen des Primzahlprogramms wird die Bibliothek libm“ benötigt,
”
es muss also die Compiler-Option -lm“ angegeben werden. Dies geschieht
”
bei KDevelop über den Menüpunkt Project/Project Options“. Das Fenster mit
”
den Projektoptionen zeigt am linken Rand eine Auswahl von Optionsgruppen
(vgl. Abbildung 2.7).
38
2 Arbeiten mit einer Entwicklungsumgebung
Abbildung 2.6: Das Hauptfenster von KDevelop nach Eingabe aller Dateien
Abbildung 2.7: Über die Project Options“ können die benötigten Libraries angegeben
”
werden (hier -lm“).
”
2.3 Eclipse + C Development Tooling (CDT)
39
Wenn Sie hier die Configure Options“ auswählen, erhalten Sie unter anderem ein
”
Eingabefeld für die Linker flags (LDFLAGS)“. Dort muss für das Primzahlpro”
gramm die Option -lm“ eingetragen werden.
”
Um die Quelltexte in ein ausführbares Programm zu übersetzen, wählen Sie nun
den Menüpunkt Build/Build Project“. KDevelop fragt an dieser Stelle nach,
”
ob mit automake“ ein Makefile automatisch erstellt werden soll (siehe Abbil”
dung 2.8).
Abbildung 2.8: KDevelop bietet den Einsatz von automake“ an.
”
Nach erfolgreichem Übersetzen der Quelltexte kann das Primzahlprogramm nun
über den Menüpunkt Build/Execute Program“ oder über das Zahnrad-Icon über
”
dem Editorbereich gestartet werden. Die Programmausgaben – in diesem Fall also
die Primzahlen bis 97 – erscheinen in einem separaten Terminal-Fenster, das von
KDevelop geöffnet wird.
2.3 Eclipse + C Development Tooling (CDT)
Im Folgenden wird der Einstieg in die Arbeit mit der Eclipse-Umgebung beschrieben, für weiterführende Details sei auf die umfangreiche Dokumentation und die
Tutorials im Internet verwiesen (z. B. [3] und [5]).
Das Programmpaket Eclipse ist in Java geschrieben und wurde ursprünglich für
die Entwicklung von Java-Programmen konzipiert. Es ist jedoch so angelegt, dass
die programmiersprachenabhängigen Teile als Plug-ins“ geladen werden. So gibt
”
es unter anderem das C Development Tooling (CDT) als Plug-in für die Programmierung in C. Um mit Eclipse C-Programme entwickeln zu können, benötigen Sie
zwingend dieses Paket. Sollte Ihre Distribution das CDT nicht beinhalten, können
Sie das Plug-in von der Eclipse-Hompage laden. In diesem Fall beachten Sie bitte
die Hinweise in Abschnitt 2.3.1.
Wird Eclipse zum ersten Mal gestartet, öffnet sich das in Abbildung 2.9 dargestellte Auswahlfenster. Für die nächsten Schritte wählen Sie bitte das Icon Work”
bench“ aus. So gelangen Sie in das Hauptfenster von Eclipse.
40
2 Arbeiten mit einer Entwicklungsumgebung
Abbildung 2.9: Das Auswahlfenster beim ersten Start von Eclipse
2.3.1 Plug-ins einbinden
Um zusätzliche Plug-ins wie das CDT in Eclipse einzubinden, kann aus dem
Menü der Eclipse-Workbench der Eintrag Help/Software Updates/Manage
”
Configuration“ gewählt werden. In dem Konfigurationsfenster muss anschließend der Button zur Anzeige der nicht aktivierten Funktionen ( Show Disabled
”
Features“) angeklickt werden:
Im linken Teil des Fensters erscheint dann die Liste mit Plug-ins und im rechten
ein Beschreibungstext mit Verknüpfungen für verschiedene Aktionen. Hier kann
für ein links selektiertes Plug-in die Aktion Aktivieren“ bzw. Enable“ gewählt
”
”
werden.
2.3.2 Ein neues Projekt anlegen
Ähnlich den beiden zuvor vorgestellten Entwicklungsumgebungen bietet auch
Eclipse einen Wizard“ für das Erstellen eines neuen Projekts an. Wählt man den
”
Menüpunkt File/New/Project...“ aus, öffnet sich das Auswahlfenster für die Art
”
2.3 Eclipse + C Development Tooling (CDT)
41
des Projekts. Nach erfolgreicher Installation des CDT kann hier Standard Make
”
C Project“ gewählt werden. Danach müssen Sie in einem weiteren Fenster den
Projektnamen eingeben.
Ist das neue Projekt angelegt, können Quelltexte neu erstellt ( File/New“) oder
”
bereits als Textdatei vorliegende Quelltexte importiert werden ( File/Import“).
”
Haben Sie den Quelltext eingegeben oder importiert, können Sie über den
Menüpunkt Project/Build Project“ das Programm kompilieren und über den
”
Menüpunkt Run“ ausführen.
”
Kapitel 3
Kommandozeilenprogramme
In der Linux-Welt gibt es eine Vielzahl leistungsfähiger Programme, die ohne
grafische Benutzerschnittstelle auskommen. Die Steuerung dieser Programme erfolgt über die Kommandozeilenparameter und die Standardein- und -ausgabe
(vgl. Abschnitt 4.1.2). Der Vorteil liegt zum einen in dem gegenüber einer grafischen Steuerung deutlich geringeren Programmieraufwand – häufig bei gleichem
Nutzen. Zum anderen eröffnet sich dadurch die Möglichkeit, verschiedene Programme über Pipes oder Shell-Skripte sehr einfach miteinander zu verknüpfen.
Das macht die Linux-Shell zu einem mächtigen Werkzeug.
Voraussetzung ist jedoch, dass sich die Programme – wie unter Linux/Unix
üblich – mit Hilfe geeigneter Parameter und Optionen entsprechend steuern lassen. In diesem Kapitel wird die Auswertung der Kommandozeilenparameter und
die Bedeutung des Rückgabewertes beschrieben, ebenso wie typische Optionen
und (Fehler-)Meldungen von Linux-Programmen. Diese Konventionen sollten im
Übrigen auch für X11-Programme gelten (vgl. Kapitel 8), da auch diese Programme häufig aus der Shell (d.h. aus einem Terminal-Fenster heraus) gestartet werden.
Weitere Themen des Kapitels sind die automatische Anpassung an die eingestellte
Landessprache und die Möglichkeiten für erweiterte Ausgabesteuerung.
3.1 Parameter und Rückgabewert der Funktion
main()
Werfen wir zunächst einen Blick auf die typische Deklaration der Funktion
main(), die gewissermaßen das Kernstück eines jeden C-Programms darstellt:
int main(int argc, char *argv[])
44
3 Kommandozeilenprogramme
Es handelt sich bei main() also um eine Funktion mit einen Rückgabewert vom
Typ Integer. Aber welche Bedeutung hat dieser Rückgabewert, wo doch das Programm nach der Rückkehr aus main() beendet ist?
3.1.1 Die Bedeutung des Rückgabewertes von main()
Unter Linux kann kein Prozess von allein“ entstehen, sondern jeder Prozess hat
”
einen Eltern“-Prozess, der den Kind“-Prozess startet. Wenn Sie in einer Shell
”
”
(also z.B. in einem Terminal-Fenster) einen Befehl wie ls“ eingeben, startet der
”
Shell-Prozess einen neuen Kind-Prozess, in dem das Programm ls“ abläuft.
”
Nach Beendigung des Programms ls“ wird der Shell-Prozess darüber infor”
miert, dass das Programm beendet wurde und mit welchem Resultat. Und genau
dieses Resultat entspricht dem Rückgabewert der Funktion main(), wie sie im
Programm ls“ realisiert ist.
”
Bei erfolgreicher Beendigung des Programms sollte immer der Wert 0 zurückgegeben werden. Ist ein Fehler aufgetreten, wurden z.B. falsche Parameter
angegeben, so sollte das Programm dies durch einen Rückgabewert 6= 0
signalisieren, damit auch der Eltern-Prozess entsprechend reagieren kann.
In einem Shell-Skript können Sie mit $?“ den Rückgabewert des zuletzt aus”
geführten Programms abfragen (die Benutzereingaben sind schräg dargestellt):
> ls xyz; echo "Resultat von ls: $?"
ls: xyz: Datei oder Verzeichnis nicht gefunden
Resultat von ls: 1
3.1.2 Die Variablen argc und argv
Zunächst sei einmal erwähnt, dass die Übergabeparameter der Funktion main()
nicht zwingend argc“ und argv“ heißen müssen – die Wahl der Variablenna”
”
men ist für main() ebenso wie für jede andere Funktion beliebig. Wenn es jedoch
nicht Ihr oberstes Bestreben ist, den Quelltext unleserlich zu gestalten, sollten Sie
auf eine Änderung dieser etablierten Bezeichnungen unbedingt verzichten.
Die Variable1 argc gibt die Anzahl der Kommandozeilenparameter einschließlich
des Programmnamens selbst an. Somit hat argc mindestens den Wert 1. Die Variable argv ist ein Zeiger auf ein Feld (engl. Array) mit der in argc angegebenen
Anzahl von Elementen (siehe Abbildung 3.1). Das erste Element dieses Feldes (also argv[0]) ist wiederum ein Zeiger auf eine Zeichenkette, nämlich den Namen
(und ggf. den Pfad) des Programms selbst. Das Element argv[1] zeigt auf die
1
argc ist tatsächlich eine Variable und keine Konstante. Es kann unter bestimmten Umständen
zweckmäßig sein, ihren Wert zu verändern.
3.1 Parameter und Rückgabewert der Funktion main()
45
Zeichenkette mit dem ersten Kommandozeilenparameter. Das letzte Element von
argv[] enthält eine Null.
argv
- argv[0]
argv[1]
argv[2]
..
.
argv[ N ]
0L
- Programmname
- 1. Parameter
- 2. Parameter
- letzter Parameter
Abbildung 3.1: Übergabe der Kommandozeilenparameter mittels argv
Übrigens: Das Feld argv[] darf vom Programm auch verändert werden. So
können z.B. Elemente entfernt und die nachfolgenden Einträge aufgerückt“ wer”
den. Verschiedene Funktionsbibliotheken, z. B. die libgtk, machen davon Gebrauch.
3.1.3 Auswerten der Kommandozeilenparameter
Am Anfang jedes Programms sollten zunächst die Kommandozeilenparameter
ausgewertet werden. Man unterscheidet hier zwischen Argumenten und Optionen.
Bei den Argumenten handelt es sich meistens um Dateinamen, während Optionen
als eine Art Schalter“ zu betrachten sind, über die sich das Verhalten des Pro”
gramms steuern lässt. Unter Unix hat es sich etabliert, dass Optionen jeweils aus
einem Zeichen bestehen und durch ein vorangestelltes -“ gekennzeichnet sind.
”
Beispiel:
rm -f primz_haupt.o primz_math.o
Bei diesem Beispiel sind primz haupt“ und primz math“ die Argumente –
”
”
nämlich die zu löschenden Dateien –, während die Option -f“ das Programm rm
”
veranlasst, falls eine der angegebenen Dateien nicht existiert, keine Fehlermeldung
auszugeben. Es sei noch erwähnt, dass Optionen ihrerseits auch wieder Argumente besitzen können – ein Beispiel hierfür ist die Option -o“ des C-Compilers gcc
”
(siehe Seite 8).
In der Linux-Welt setzten sich zunehmend Langtextoptionen durch, etwa in der
Form --help“. Diese sind zwar leichter lesbar, bedeuten für den Anwender aber
”
entsprechend mehr Tipparbeit. Daher wird im Folgenden nur die kurze Form betrachtet.
Will man sich bei der Auswertung der Parameter an die üblichen Konventionen
halten, also u.a. eine beliebige Reihenfolge der Optionen zulassen, so kann das
manuelle“ Auswerten der Kommandozeile schon mit relativ viel Aufwand ver”
bunden sein, insbesondere dann, wenn auch Optionen vorgesehen sind, die selbst
46
3 Kommandozeilenprogramme
wieder Argumente erfordern. Die C-Funktionsbibliothek libc bietet hier glücklicherweise eine Funktion, die dem Programmierer eine ganze Menge Arbeit abnimmt:
int getopt(int argc, char *argv[], char *optstring);
Neben den bekannten Parametern argc und argv wird dieser Funktion die Zeichenkette optstring übergeben, die alle zulässigen Optionen enthält. Ein :“ hin”
ter einem Optionszeichen in optstring bedeutet, dass diese Option ein Argument erfordert (z.B. "o:" für die Option -o Dateiname“). Zwei Doppelpunkte
”
zeigen an, dass das Argument für diese Option optional, also nicht notwendig ist.
Die Funktion getopt() sortiert zunächst das Feld argc[] so, dass die Optionen
nach vorn und die Argumente nach hinten geschoben werden. Als Rückgabewert
liefert die Funktion entweder die erste in der Kommandozeile angegebene Option
oder –1, falls keine Option angegeben wurde. Bei jedem weiteren Aufruf liefert
getopt() die jeweils nächste angegebene Option in der Kommandozeile. Wurden alle Optionen eingelesen, liefert getopt() –1. Danach gibt die globale Variable optind den Index des ersten Kommandozeilenargumentes im Feld argv an.
Die Funktion getopt() führt automatisch eine Fehlerbehandlung durch, d.h. bei
ungültigen Optionen oder fehlenden Optionsargumenten werden entsprechende
Fehlermeldungen ausgegeben.
Ein kleines Beispiel soll das Einlesen der Kommandozeilenparameter und damit
die Arbeitsweise von getopt() erläutern:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
read_args.c
*/
# include <stdio.h>
# include <unistd.h>
int main(int argc, char *argv[])
{
int option, i;
char *out_filename=NULL;
while ((option = getopt(argc, argv, "ho:")) >= 0)
switch (option)
{
case ’h’ : printf("Usage: %s [-o output-file] "
"[input-file ...]\n", argv[0]);
return(0);
case ’o’ : out_filename = optarg;
break;
case ’?’ : return(1);
/* unbekannte Option */
3.1 Parameter und Rückgabewert der Funktion main()
22
23
24
25
26
27
28
29
30
31
47
}
for (i=optind; i<argc; i++)
printf("’%s’\n", argv[i]);
if (out_filename)
printf("output is ’%s’\n", out_filename);
return(0);
}
Der dritte Parameter beim Aufruf von getopt() in Zeile 13, also die Optionsliste "ho:", gibt zwei zulässige Optionen vor: -h“ und -o“, wobei Letztere ein
”
”
Argument erfordert. Erkennt getopt() die Option -o“, so zeigt die globale Va”
riable optarg“ auf das Argument dieser Option (siehe Zeile 19). Stößt getopt()
”
auf eine unzulässige, also in der Optionsliste nicht angegebene Option, liefert die
Funktion das Zeichen ?“, was bei diesem Beispielprogramm zu einem Abbruch
”
mit Fehlercode 1 führt (Zeile 21).
Nach Abarbeitung aller Optionen mit Hilfe der while-Schleife (Zeile 13 bis 22)
erfolgt die Auswertung der Argumente (Zeile 24 und 25), die von getopt() ans
Ende des Feldes argv[] gestellt wurden.
3.1.4 Achtung: Platzhalter!
Häufig macht man bei der Angabe von Dateinamen Gebrauch von so genannten
Platzhaltern, insbesondere wenn ein Befehl auf mehrere Dateien mit bestimmten
Übereinstimmungen im Dateinamen angewendet werden soll. Beispiel:
rm *.o
wird alle Dateien löschen, die auf .o“ enden. Doch was geschieht, wenn sol”
che Platzhalter angewendet werden? Während es bei manchen Betriebssystemen
Aufgabe der Programme selbst ist, diese Platzhalter aufzulösen“, also durch die
”
entsprechenden Dateinamen zu ersetzen, übernimmt unter Linux die Shell diese
Aufgabe (z.B. die bash). Das bedeutet für das obige Beispiel, dass die Shell vor
der Ausführung des Programms rm“ die Kommandozeile ersetzt, z.B. durch:
”
rm primz_haupt.o primz_math.o
Dies erspart dem Programmierer unter Linux den Programmieraufwand für das
Durchsuchen des Verzeichnisses nach übereinstimmenden Dateinamen. Auf der
anderen Seite sollte der Programmierer aber auch berücksichtigen, dass ein Kommandozeilenparameter unter Umständen durch mehrere Dateinamen ersetzt wird.
In diesem Zusammenhang ist folgende Syntax für die Kommandozeilenparameter eines Programms möglichst zu vermeiden:
48
3 Kommandozeilenprogramme
mein programm Eingabedatei [Ausgabedatei]
Das Programm kann also wahlweise mit einem oder mit zwei Argumenten aufgerufen werden. Bei Angabe des zweiten Arguments wird dieses als Ausgabedatei interpretiert. Anderenfalls werden die Ausgaben z.B. an den StandardAusgabekanal geschickt. Verwendet der Anwender jetzt dieses Programm in Verbindung mit einem Platzhalter für die Eingabedatei (um sich Tipparbeit zu sparen):
mein_programm primz*.c
so wird die Shell dies möglicherweise ersetzen durch:
mein_programm primz_haupt.c primz_math.c
und damit würde unbeabsichtigt die zweite Datei als Ausgabedatei geöffnet und
überschrieben!
Besser ist es daher, für eine optionale Ausgabedatei eine entsprechende Option
mit Argument vorzusehen, wie in dem Beispiel aus Abschnitt 3.1.3. Auch sollte die Reihenfolge der Argumente nach Möglichkeit keine Rolle spielen, da die
Shell beim Ersetzen eines Platzhalters durch mehrere Dateinamen keine bestimmte
Reihenfolge einhält.1
3.2 Konventionen für Kommandozeilenprogramme
Um dem Linux-Anwender die Kommandozeilen-Bedienung von Programmen zu
erleichtern, sollten sich alle Programme an gewisse Linux-typische Konventionen
halten. Das sind natürlich keine Vorschriften“ – es obliegt letztendlich der Ver”
antwortung des Programmierers, inwieweit er sich daran hält. Es handelt sich
dabei mehr um nützliche Tipps für das Erstellen gut bedienbarer Programme, die
eine Vielzahl von Anwendern erreichen sollen.
3.2.1 Ein Muss: Die Hilfe-Option
Häufig stößt man bei freier Software (public domain) auf kommandozeilenbasierte
Programme, die laut Beschreibung (z.B. im Internet) genau die gesuchte Lösung
für ein bestimmtes Problem darstellen. Doch wie war noch gleich die Syntax für
die Argumente und Optionen? Häufig bleibt da nur die Suche nach der zugehörigen README“-Datei.
”
Gerade wenn man das Programm schon einmal angewendet hat, sich aber z.B.
nicht mehr ganz sicher über eine Option ist, ist das Nachforschen in Hilfetexten
1
Die bash sortiert die Namen alphabetisch. Das muss aber nicht der vom Anwender beabsichtigten
Reihenfolge entsprechen.
3.2 Konventionen für Kommandozeilenprogramme
49
etwas umständlich. Daher sollte jedes Programm über eine Kurzhilfe verfügen,
die sozusagen in das Programm eingebaut ist. Aus dieser Hilfe sollten der Zweck
des Programms sowie die Syntax des Kommandozeilenaufrufs hervorgehen. Ferner sollte man diese Hilfe entweder durch die Option -h“ oder die Langversion
”
--help“ aufrufen können. Ein schönes Beispiel ist das Programm dd“, das eine
”
”
ganze Reihe von Optionen besitzt, die nicht immer leicht zu merken sind. Mit dd
”
--help“ erhält man jedoch die folgende ausführliche Beschreibung:
Usage: dd [OPTION]...
Copy a file, converting and formatting according to the options.
bs=BYTES
cbs=BYTES
conv=KEYWORDS
count=BLOCKS
ibs=BYTES
if=FILE
obs=BYTES
of=FILE
seek=BLOCKS
skip=BLOCKS
--help
--version
force ibs=BYTES and obs=BYTES
convert BYTES bytes at a time
convert the file as per the comma separated
keyword list
copy only BLOCKS input blocks
read BYTES bytes at a time
read from FILE instead of stdin
write BYTES bytes at a time
write to FILE instead of stdout
skip BLOCKS obs-sized blocks at start of output
skip BLOCKS ibs-sized blocks at start of input
display this help and exit
output version information and exit
BYTES may be followed by the following multiplicative suffixes:
xM M, c 1, w 2, b 512, kD 1000, k 1024, MD 1,000,000, M 1,048,576,
GD 1,000,000,000, G 1,073,741,824, and so on for T, P, E, Z, Y.
Each KEYWORD may be:
ascii
ebcdic
ibm
block
unblock
lcase
notrunc
ucase
swab
noerror
sync
from EBCDIC to ASCII
from ASCII to EBCDIC
from ASCII to alternated EBCDIC
pad newline-terminated records with spaces to cbs-size
replace trailing spaces in cbs-size records with newli
change upper case to lower case
do not truncate the output file
change lower case to upper case
swap every pair of input bytes
continue after read errors
pad every input block with NULs to ibs-size
Report bugs to <bug-fileutils@gnu.org>.
Hier erhält der Anwender die Information, was das Programm leistet, welche Argumente und Optionen es kennt und an wen man mögliche Bugs berichten kann.
50
3 Kommandozeilenprogramme
3.2.2 Fehlermeldungen
Genauso wichtig wie eine Kurzhilfe sind sinnvolle und aufschlussreiche Fehlermeldungen, die den Anwender schnell die Ursache des (Bedien-)Fehlers finden
lassen.
Ein Beispiel für eine unzureichende Fehlermeldung ist die Ausgabe:
Bad arguments.
Welche Kommandozeilenargumente hat das Programm nicht verstanden? Erscheint diese Meldung bei Abarbeitung eines Shell-Skriptes, ist nicht einmal klar,
welches Programm mit fehlerhaften Argumenten aufgerufen worden ist! Dies erschwert die Fehlersuche und -beseitigung ungemein.
Wie eine Fehlermeldung sinnvollerweise aussehen sollte, sei hier einmal am Beispiel des Programms cp“ gezeigt:
”
> cp primzahl.c
cp: Fehlende Zieldatei
Versuchen Sie ≫cp --help≪ für weitere Informationen.
Die Fehlermeldung gibt hier nicht nur unmissverständlich den Grund des Fehlers
an, sondern auch den Namen des Programms selbst. Zusätzlich wird sogar auf
die eingebaute Hilfe-Option verwiesen.
Fehlermeldungen sollten immer an den Fehlerausgabekanal (stderr, siehe auch
Kapitel 4) geleitet werden, damit auch beim Umleiten der Standardausgabe
in eine Datei mittels >“ der Anwender die Fehlermeldung sofort zu sehen“
”
”
bekommt.
Für gewöhnlich werden in C-Programmen Fehlermeldungen mit Anweisungen
der Form
fprintf(stderr, "%s: Too many arguments.\n", argv[0]);
ausgegeben. Optional kann der Programmname (argv[0]) mit Hilfe der Funktion basename(argv[0])“ um Pfadangaben bereinigt werden, sodass z.B. statt
”
/home/martin/bin/my prog: Too many arguments.“ hier nur my prog:
”
”
Too many arguments.“ ausgegeben wird. Diese Funktion ist in der IncludeDatei string.h deklariert.
Bei Fehlermeldungen im Zusammenhang mit dem Öffnen, Lesen oder Schreiben
von Dateien ist es außerdem sinnvoll, die Funktion perror() aufzurufen, um
den genauen Grund des Dateizugrifffehlers anzugeben:
3.2 Konventionen für Kommandozeilenprogramme
51
void perror(const char *s);
Diese Funktion gibt die als Parameter angegebene Zeichenkette in den StandardFehlerkanal aus, fügt einen Doppelpunkt an und gibt dahinter die Beschreibung
der Fehlerursache – z.B. Datei oder Verzeichnis nicht gefunden“ – aus.
”
Wird der Funktion perror() eine leere Zeichenkette übergeben, so erfolgt nur
die Ausgabe der Fehlerursache, ein Doppelpunkt wird in diesem Fall nicht vorangestellt.
Soll die Fehlerursache nicht (nur) ausgegeben, sondern z.B. bei Programmen mit
grafischer Bedienoberfläche in einem Fenster dargestellt werden, so kann der
Fehlertext auch über die Funktion strerror() ermittelt werden. Als Parameter
müssen Sie hier die globale Variable errno angeben:
# include <errno.h>
# include <string.h>
char *error_text = strerror(errno);
Wie bereits in Abschnitt 3.1.1 beschrieben, sollte im Falle eines Fehlers, der zum
Abbruch des Programms führt, ein entsprechender Rückgabewert 6= 0 gesetzt
werden. Tritt der Fehler nicht direkt in main(), sondern innerhalb einer Unterfunktion auf, so kann dies mit der Funktion
exit(Rückgabewert);
geschehen.
3.2.3 Eigene Manpages erstellen
Zu einem guten Programm gehört auch eine man“-Hilfeseite oder auch Man”
”
page“. Solche Dateien finden sich in der Regel in dem Verzeichnis /usr/man“
”
oder /usr/local/man“. In der Umgebungsvariablen MANPATH sind die Ver”
zeichnisse eingetragen, die das Programm man“ durchsucht. Zu dem vollständi”
gen Pfad gehört das Unterverzeichnis, dessen Name sich aus man“ + Sekti”
onsnummer zusammensetzt. So findet sich z. B. der Hilfetext zum Shell-Befehl
sleep“ in Sektion 1 unter:
”
/usr/man/man1/sleep.1
Am Ende des Dateinamens ist noch einmal die entsprechende Sektionsnummer
angehängt. Häufig sind die Dateien mit gzip komprimiert, was an der Endung
.gz“ zu erkennen ist.
”
Manpages sind reine Textdateien und können mit jedem Editor erstellt werden.
Neben dem eigentlichen Text enthalten die Dateien Steuersequenzen, die u.a. die
52
3 Kommandozeilenprogramme
Formatierung beeinflussen. Sie müssen am Zeilenanfang stehen und beginnen mit
einem .“. Des Weiteren gibt es einige Steuersequenzen, die mit \“ beginnen und
”
”
z. B. Texthervorhebungen bewirken. Diese Sequenzen müssen nicht zwingend am
Zeilenanfang stehen. Die wichtigsten fasst Tabelle 3.1 zusammen.
Tabelle 3.1: Beschreibung einiger Steuersequenzen für man-Hilfeseiten
Sequenz
Beschreibung
.\"
.TH
.SH
.fi
.nf
Kommentarzeile
Titel, Kopf- und Fußzeile festlegen
Überschrift für den nächsten Abschnitt
ab hier: Blocksatz
ab hier: keine Formatierung
\fR
\fB
\fI
\-
normaler Text
Fettschrift
kursiv (oder unterstrichen)
Gedankenstrich
Der Sequenz für Titel, Kopf- und Fußzeile können bis zu fünf Angaben folgen:
.TH "Name" "Sektion" "Datum" "Author / Version" "Thema"
Beispiel:
.TH SLEEP 3 "Apr. 1993" GNU "Linux Programmer’s Manual"
Die Steuerungssequenz für eine Überschrift benötigt als einzige Angabe den
Überschrift-Text. Als Beispiel soll hier eine kurze Manpage für das PrimzahlProgramm aus Kapitel 1 dienen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.\" primzahl.1
.TH primzahl 1 "Feb. 2002" "M. Gräfe" "C und Linux"
.SH NAME
primzahl \- Primzahlen von 1 bis 100 berechnen.
.SH SYNTAX
primzahl
.SH BESCHREIBUNG
.fi
Das Programm \fBprimzahl\fR berechnet alle Primzahlen
zwischen 1 und 100. Es dient als Beispielprogramm für
das erste Kapitel des Buches "\fIC und Linux\fR".
.nf
.SH AUTOR
Martin Gräfe (AuM.Graefe@t-online.de)
Als primzahl.1“ abgespeichert lässt sich diese Hilfeseite mit
”
man -l primzahl.1
3.3 Programme mehrsprachig auslegen
53
formatieren und anzeigen (Abbildung 3.2). Die Option -l“ ermöglicht es, direkt
”
den Dateinamen der Manpage anzugeben, die dargestellt werden soll. Ist die
Manpage fertiggestellt, kann sie mit gzip komprimiert und in das entsprechende
Verzeichnis (z.B.: /usr/local/man/man1) kopiert werden. Danach lässt sie sich
jederzeit mit
man primzahl
anzeigen.
Abbildung 3.2: Ergebnis der selbst geschriebenen Manpage
3.3 Programme mehrsprachig auslegen
Die Zeiten, in denen alle Computer-Programme und auch die Betriebssysteme ausschließlich in Englisch waren, sind längst vorbei. Auch die aktuellen
Linux-Distributionen präsentieren sich hierzulande in deutschem Gewand“,
”
d. h. Menüs und Dialog-Boxen sind zum Großteil deutschsprachig.
Die Unterstützung länderabhängiger Formate wird bei Linux unter dem Begriff
locale zusammengefasst. Das gleichnamige Shell-Programm liefert Informationen
zu den aktuellen Einstellungen:
54
3 Kommandozeilenprogramme
> locale
LANG=de DE
LC CTYPE=de DE
LC NUMERIC=de DE
LC TIME=de DE
LC COLLATE=de DE
LC MONETARY=de DE
LC MESSAGES=de DE
LC ALL=
Mit Hilfe der Umgebungsvariablen LANG“ kann allgemein die Auswahl der Lan”
dessprache erfolgen, z.B.:
> export LANG=en US
Das Kürzel en“ steht hier für englisch“, die Erweiterung US“ kennzeichnet,
”
”
”
dass es sich um nordamerikanisches Englisch handelt. Diese Angabe kann noch um
die Spezifikation des ISO-Zeichensatzes ergänzt werden, z.B.:
> export LANG=de DE.ISO-8859-1
Die von Linux unterstützten Ländereinstellungen findet man als Verzeichnisse
unter dem Pfad /usr/share/locale“.
”
Die Auswahl der Sprach-/Ländereinstellung erfolgt in C-Programmen mit Hilfe
der Funktion setlocale():
char *setlocale(int category, char *locale);
wobei category eine der Kategorien LC COLLATE, LC CTYPE, LC MESSAGES,
LC MONETARY, LC NUMERIC, LC TIME oder LC ALL für alle Kategorien sein muss.
Der Parameter locale muss entweder eine gültige Länderkennzeichnung wie
de DE“ enthalten oder eine leere Zeichenkette ("") sein, wenn die Länderkenn”
zeichnung von der Umgebungsvariablen LANG übernommen werden soll.
Das folgende kleine Beispiel soll die Wirkung des setlocale()-Aufrufs verdeutlichen:
1
2
3
4
5
6
7
8
/*
sprache.c
*/
# include <stdio.h>
# include <locale.h>
int main()
3.3 Programme mehrsprachig auslegen
9
10
11
12
13
14
15
16
55
{
setlocale(LC_NUMERIC, "en_US");
printf("PI=%.3f\n", 3.141593);
setlocale(LC_NUMERIC, "de_DE");
printf("PI=%.3f\n", 3.141593);
return(0);
}
Nach dem Übersetzen mit gcc sprache.c -o sprache“ kann das Programm
”
aufgerufen werden:
> sprache
PI=3.142
PI=3,142
Offensichtlich führt die gleiche printf()-Anweisung in Zeile 13 des Quelltextes
zu einem anderen Ergebnis als in Zeile 11. Das Dezimal-Trennzeichen wird in diesem Beispiel von .“ auf ,“ umgeschaltet. Erreicht wird das durch Ändern der
”
”
Locale Category LC NUMERIC“, die – wie der Name schon sagt – das Zahlenformat
”
bei Ein- und Ausgaben steuert.
Auf manchen Systemen wird die Funktionsbibliothek libintl“ nicht automatisch
”
eingebunden, so dass das Einbinden der Funktionen zur Internationalisierung
von Programmen zu einer Fehlermeldung führt. In diesem Fall muss beim
Übersetzen des Quelltextes die Bibliothek explizit angegeben werden, also z. B.
gcc sprache.c -lintl -o sprache“.
”
Anstatt die Ländereinstellung wie in dem Beispiel explizit vorzugeben, sollten
Programme diejenige Einstellung verwenden, die der Benutzer mit Hilfe der Umgebungsvariable LANG voreingestellt hat, indem als Parameter locale eine leere Zeichenkette angegeben wird:
setlocale(LC_ALL, "");
Die Funktion setlocale() liefert als Rückgabewert einen Zeiger auf die Zeichenkette mit der entsprechenden Landeskennung.
Alle von der libc generierten Meldungen, z.B. mit Hilfe der Funktion perror(),
werden automatisch in der durch die Umgebungsvariable LANG vorgegebenen
Sprache ausgegeben. Doch wie kann man das Gleiche für Meldungen erreichen,
die mittels printf() oder fprintf() ausgegeben werden? Dies ist Aufgabe der
C-Funktionen gettext() und dgettext():
56
3 Kommandozeilenprogramme
char *gettext(char *msgid);
char *dgettext(char *textdomain, char *msgid);
Beide Funktionen dienen dazu, eine Meldung, hier als msgid bezeichnet, in die
Landessprache gemäß LC MESSAGES zu übersetzen. Natürlich handelt es sich
hierbei nicht um echte Übersetzungsfunktionen, vielmehr muss der Programmierer zuvor die übersetzten Texte in einer separaten Datei abgelegt haben. Die Texte
werden nach der Textdomain gegliedert, in der Regel handelt es sich dabei um den
Programmnamen selbst. Doch schauen wir uns zunächst das folgende kleine Programm an:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
sprache2.c
*/
# include <stdio.h>
# include <locale.h>
# include <libintl.h>
int main()
{
setlocale(LC_MESSAGES, "");
printf("%s\n", dgettext("grep", "out of memory"));
return(0);
}
Nach dem Übersetzen mit gcc sprache2.c -o sprache2“ kann das Pro”
gramm mit verschiedenen Einstellungen für die Umgebungsvariable LANG gestartet werden:1
> export LANG=de DE
> sprache2
Speicher ist alle.
> export LANG=en US
> sprache2
out of memory
> export LANG=fr FR
> sprache2
Mémoire épuisée.
Das Programm greift mit der Anweisung
1
Sollte dieses Beispiel auf Ihrem System immer nur die Meldung out of memory“ liefern, so stellen
”
Sie bitte sicher, dass das Programm grep“ installiert ist.
”
3.3 Programme mehrsprachig auslegen
57
dgettext("grep", "out of memory")
auf die Textdomain grep“ zu und damit auf die Übersetzungsdateien des
”
Programms grep“. Diese Dateien werden nach der Meldung (msgid) out
”
”
of memory“ durchsucht, und wenn diese Meldung gefunden wird, liefert
dgettext() das Pendant in der eingestellten Sprache. Findet dgettext() die
Meldung in der entsprechenden Übersetzungdatei nicht, gibt die Funktion einen
Zeiger auf die Meldung selbst (also auf msgid) zurück. Daher sollte als msgid immer der entsprechende Text in Englisch verwendet werden, sodass bei einer nicht
unterstützten Sprache englische Meldungen erscheinen.
Bei den Übersetzungsdateien, den so genannten Message-Object-Dateien, handelt
es sich um spezielle Binärdateien, die für gewöhnlich unter
/usr/share/locale/Länderkennung/LC MESSAGES/Textdomain.mo
stehen, also z.B.:
/usr/share/locale/de/LC MESSAGES/grep.mo
Wird als Länderkennung de DE“ angegeben, sucht das System zunächst im ent”
sprechenden Verzeichnis nach der betreffenden Message-Object-Datei. Bleibt die
Suche ohne Erfolg, wird automatisch die Länderkennung auf de“ reduziert und
”
erneut versucht, den entsprechenden Pfad zu öffnen. Auf diese Weise lässt sich
z.B. für schweizerisches Deutsch de CH“ ein anderer Text einstellen, während für
”
andere Dialekte (z.B. de AT“ für Österreich), für die keine speziellen Meldungen
”
vorgesehen sind, auf die Datei unter der übergeordneten“ Länderkennung de“
”
”
zugegriffen wird.
Bitte verstehen Sie das Beispielprogramm nur als Test! Sie sollten mit eigenen Programmen nicht die Message-Object-Dateien anderer Programme nutzen.
Message-Object-Dateien erstellen
Für das Erstellen eigener Message-Object-Dateien sind die Pakete gettext“ und
”
gettext-devel“ erforderlich. Stellen Sie sicher, dass diese Pakete installiert sind.
”
Als Beispiel für die Erzeugung und Verwendung eigener Message-Object-Dateien
soll das kleine Programm sprache3“ dienen, das abhängig von der eingestellten
”
Landessprache die Meldung Hello world!“ bzw. Hallo Welt!“ ausgibt:
”
”
58
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3 Kommandozeilenprogramme
/*
sprache3.c
*/
# include <stdio.h>
# include <locale.h>
# include <libintl.h>
int main()
{
bindtextdomain("sprache3", ".");
textdomain("sprache3");
setlocale(LC_MESSAGES, "");
printf("%s\n", gettext("Hello world!"));
printf("%s\n", gettext("This is a test."));
return(0);
}
Neben den bereits zuvor erläuterten Aufrufen der Funktion setlocale() und
gettext() finden sich hier die Funktionen bindtextdomain() (Zeile 11) und
textdomain() (Zeile 12). Mit der bindtextdomain()-Anweisung wird hier
festgelegt, dass die Message-Object-Dateien zur Textdomain sprache3 nicht unter /usr/share/locale“, sondern im aktuellen Verzeichnis ( .“) zu finden
”
”
sind. Die Anweisung textdomain("sprache3"); definiert sprache3 als aktuelle Textdomain für alle folgenden gettext()-Aufrufe.
Mit Hilfe des Programms xgettext“ können aus dem Quelltext alle Aufrufe von
”
gettext() und dgettext() in eine Portable-Message-Datei extrahiert werden:
xgettext -o sprache3.po sprache3.c
Das Programm xgettext erzeugt daraufhin die (editierbare) Portable-MessageDatei sprache.po“ mit folgendem Inhalt:
”
1
# SOME DESCRIPTIVE TITLE.
2
# Copyright (C) YEAR THE PACKAGE’S COPYRIGHT HOLDER
3
# This file is distributed under the same license as the PA
4
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
#
6
#, fuzzy
7
msgid ""
8
msgstr ""
9
"Project-Id-Version: PACKAGE VERSION\n"
10 "Report-Msgid-Bugs-To: \n"
11 "POT-Creation-Date: 2010-01-31 14:58+0100\n"
3.3 Programme mehrsprachig auslegen
12
13
14
15
16
17
18
19
20
21
22
23
24
25
59
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: Quelltexte/Kapitel3/sprache3.c:14
msgid "Hello world!"
msgstr ""
#: Quelltexte/Kapitel3/sprache3.c:15
msgid "This is a test."
msgstr ""
In den ersten Zeilen dieser Datei sind einige Kommentare vorbereitet, unter anderem zum Autor und Erstellungsjahr. Hier können Sie bei echten“ Software”
Projekten Ihre Daten eintragen. Ab Zeile 9 folgen automatisch generierte Informationen zur Datei. Wichtig sind hier die Zeilen 16 und 17, die die Codierung der
Datei angeben. In Zeile 16 sollten Sie als Zeichensatz ISO-8859-1“ eintragen, also
”
16 "Content-Type: text/plain; charset=ISO-8859-1\n"
Was jetzt noch fehlt, ist die eigentliche Übersetzung der Textmeldungen. Dazu
müssen Sie hinter dem Schlüsselwort msgstr in den Zeilen 21 und 25 jeweils
die Übersetzung der Texte in den vorangegangenen Zeilen eintragen, für Deutsch
z. B.:
19
20
21
22
23
24
25
#: Quelltexte/Kapitel3/sprache3.c:14
msgid "Hello world!"
msgstr "Hallo Welt!"
#: Quelltexte/Kapitel3/sprache3.c:15
msgid "This is a test."
msgstr "Dies ist ein Test."
Aus der Portable-Message-Datei kann nun mit Hilfe des Programms msgfmt“
”
die Message-Object-Datei sprache.mo“ generiert und im vorgesehenen Ver”
zeichnis abgelegt werden:
>
>
>
>
msgfmt -o sprache3.mo sprache3.po
mkdir de
mkdir de/LC MESSAGES
mv sprache3.mo de/LC MESSAGES
Danach spricht“ das Programm sprache3“ deutsch (und englisch):
”
”
60
3 Kommandozeilenprogramme
> export LANG=de DE
> sprache3
Hallo Welt!
Dies ist ein Test.
> export LANG=en US
> sprache3
Hello world!
This is a test.
Wird ein Programm korrekt installiert, sollten die zugehörigen MO-Dateien in die
entsprechenden Verzeichnisse des Linux-Systems kopiert werden, also z. B.
/usr/share/locale/Länderkennung/LC MESSAGES/
Dann kann der bindtextdomain()-Aufruf entfallen ( sprache3.c“, Zeile 11).
”
3.4 Ausgabesteuerung im Terminal-Fenster
In den bisherigen Beispielen wurden Textausgaben im Terminal-Fenster durch
einfache printf()-Anweisungen erreicht. Manchmal ist es jedoch von Vorteil,
bestimmte Ausgaben hervorzuheben, z.B. durch Fettschrift oder durch Verwendung einer anderen Schriftfarbe. Auch das Positionieren von Ausgaben an bestimmten Stellen innerhalb des Terminal-Fensters kann hilfreich sein. In den folgenden Abschnitten werden zwei Möglichkeiten aufgezeigt, eine solche Ausgabesteuerung innerhalb eines Terminal-Fensters, d.h. ohne Verwendung einer grafischen Benutzerschnittstelle, zu erreichen.
3.4.1 ANSI-Steuersequenzen
Eine sehr einfache Möglichkeit, Textausgaben hervorzuheben und zu positionieren, ist die Verwendung von Steuersequenzen nach dem ANSI-Standard. Es handelt
sich dabei insofern um einen Standard, als auch Drucker sowie andere Betriebssysteme – beispielsweise DOS bei Verwendung des Treibers ANSI.SYS – die Sequenzen (zumindest teilweise) verstehen“.
”
Diese Steuersequenzen werden fast ausschließlich mit einem ESC-Zeichen
(ASCII-Code 27dez bzw. 33okt ) eingeleitet, weshalb sie auch als Escape-Sequenzen
bezeichnet werden. Als Drucker nur bedingt grafikfähig waren, stellten diese Sequenzen die einzige Möglichkeit dar, Textbereiche zu unterstreichen, in Fett oder
Kursiv zu drucken oder die Schriftart zu wechseln.
Tabelle 3.2 zeigt eine Auswahl möglicher ESC-Sequenzen, die jedoch nicht alle
von sämtlichen Terminalprogrammen unterstützt werden. Diese Steuersequenzen
lassen sich direkt mit einer printf()-Anweisung ausgeben, z. B.:
printf("\033[31mDieser Text ist rot.\033[0m\n");
3.4 Ausgabesteuerung im Terminal-Fenster
61
Tabelle 3.2: ANSI-Steuersequenzen für die Terminal-/Druckerausgabe
ESC-Sequenz
Beschreibung
\033[m
\033[0m
\033[1m
\033[4m
\033[30m
\033[31m
\033[32m
\033[33m
\033[34m
\033[35m
\033[36m
\033[40m
\033[41m
\033[42m
\033[43m
\033[44m
\033[45m
\033[46m
\033[SpalteG
\033[ZeileH
\007
\011
\014
normaler Text
normaler Text
Fettschrift
unterstreichen
Schriftfarbe Schwarz
Schriftfarbe Rot
Schriftfarbe Grün
Schriftfarbe Gelb
Schriftfarbe Blau
Schriftfarbe Violett
Schriftfarbe Türkis
Hintergrundfarbe Schwarz
Hintergrundfarbe Rot
Hintergrundfarbe Grün
Hintergrundfarbe Gelb
Hintergrundfarbe Blau
Hintergrundfarbe Violett
Hintergrundfarbe Türkis
Cursor horizontal positionieren
Cursor vertikal positionieren
Signalton
Tabulator (horizontal)
Seitenvorschub (Terminal-Fenster löschen)
3.4.2 Die ncurses“-Bibliothek
”
Vielleicht haben Sie sich schon einmal gefragt, wie ein Editor-Programm in der Art
des vi“ (siehe Seite 6) arbeitet, das ohne die grafische Oberfläche X11 läuft. Die
”
im vorherigen Abschnitt beschriebenen ESC-Sequenzen reichen für eine derartige
Bildschirmsteuerung bei Weitem nicht aus. Für diesen Zweck gibt es unter Unix
die curses -Bibliothek, die unter Linux in der weiterentwickelten und frei kopierbaren Version n curses vorliegt. Neben der Cursor-Positionierung und der Texthervorhebung bietet diese Funktionsbibliothek auch relativ komplexe Funktionen
wie das Verschieben (Scrollen) des Fensterinhaltes um n Zeilen, das Einfügen oder
Löschen von Zeichen innerhalb einer Zeile mit Verschieben der Zeichen rechts
vom Cursor und das Abspeichern des gesamten Fensterinhaltes in eine Datei.
Die vollständige Behandlung der ncurses-Bibliothek würde den Rahmen dieses
Buches sprengen; ich möchte hier lediglich einen Einstieg in die Programmierung
solcher Anwendungen geben. Die vollständige Liste aller Funktionen der ncursesBibliothek erhalten Sie mit man ncurses“ und man -k curses“.
”
”
62
3 Kommandozeilenprogramme
Als Einstieg sei hier das folgende Programm betrachtet, das das freie Bewegen des
Cursors über das Terminal-Fenster sowie die Eingabe von Zeichen an der gerade
aktuellen Cursor-Position erlaubt – wie dies bei einem Editor-Programm der Fall
ist. Gleichzeitig wird oben links immer die aktuelle Cursor-Position angezeigt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
curses_bsp.c
*/
# include <stdio.h>
# include <curses.h>
void print_pos(WINDOW *win)
{
int x, y;
getyx(win, y, x);
attron(A_REVERSE);
mvprintw(0, 0, "(%2d,
attroff(A_REVERSE);
move(y, x);
return;
}
int main()
{
int
c, x, y;
WINDOW *win;
/* Cursor-Position darst. */
/* aktuelle Pos. abfragen */
/* inverse Darstellung ein */
%2d)", x, y);
/* inverse Darstellung aus */
/* Cursor wieder an alte Pos. */
/* Hauptprogramm */
if ((win = initscr()) == NULL)
return(1);
cbreak();
noecho();
keypad(win, TRUE);
/* Sondertasten auswerten */
move(1, 0);
print_pos(win);
while ((c = getch()) != KEY_END) /* Ende = Abbruch */
{
switch(c)
{
case KEY_UP:
getyx(win, y, x); move(y-1, x);
break;
case KEY_DOWN: getyx(win, y, x); move(y+1, x);
3.4 Ausgabesteuerung im Terminal-Fenster
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
63
break;
getyx(win, y, x); move(y, x-1);
break;
case KEY_RIGHT: getyx(win, y, x); move(y, x+1);
break;
case KEY_DC:
delch();
break;
case KEY_BACKSPACE: getyx(win, y, x); move(y, x-1);
delch();
break;
case KEY_IC:
insch(’ ’);
break;
case KEY_HOME: getyx(win, y, x); move(y, 0);
break;
case KEY_F(1): clear(); move(1, 0);
break;
default:
if (c < KEY_MIN) addch(c);
}
print_pos(win);
case KEY_LEFT:
}
endwin();
return(0);
}
Um dieses Programm zu übersetzen, muss die ncurses-Bibliothek mit der Option
-l“ explizit eingebunden werden:
”
gcc curses bsp.c -lncurses -o curses bsp
Wenn Sie das Programm starten, wird der Inhalt des Terminal-Fensters (oder der
Linux-Konsole) gelöscht, und oben links erscheint die aktuelle Cursor-Position
( 0, 1)“ invers dargestellt. Sie können den Cursor jetzt mit Hilfe der Cursor”
Tasten beliebig über das Fenster bewegen und an der aktuellen Position Text eingeben. Auch das Löschen von Zeichen mit Entf oder des gesamten Inhalts mit F1
ist möglich. Durch Drücken der Taste Ende wird das Programm beendet.
Schauen wir uns zunächst das Hauptprogramm“ (Zeile 20 bis 64) an. Bevor ir”
gendwelche Ein- oder Ausgaben über Funktionen der ncurses-Bibliothek erfolgen können, muss das Ausgabefenster initialisiert werden. Dies geschieht mit der
Funktion initscr() in Zeile 25, die bei erfolgreicher Ausführung einen Zeiger auf die WINDOW-Struktur der Bibliothek zurückliefert. Mit den Funktionen
cbreak() (Zeile 27) und noecho() (Zeile 28) wird die Betriebsart“ eingestellt; in
”
diesem Fall soll jeder Tastendruck sofort an das Programm weitergeleitet werden
und keine automatische Ausgabe des eingegebenen Zeichens erfolgen. Als letzte
Konfigurationseinstellung wird in Zeile 29 die Auswertung von Sondertasten mit
64
3 Kommandozeilenprogramme
der Funktion keypad() aktiviert. Mit der move()-Anweisung in Zeile 31 wird
der Cursor in die Position y = 1, x = 0 gebracht.
Als Nächstes erfolgt ein Aufruf der Funktion print pos(), die in den Zeilen 8
bis 18 definiert wird und die aktuelle Cursor-Position oben links in dem Fenster
ausgibt. In der darauf folgenden while()-Schleife (Zeile 34 bis 60) werden mittels getch() so lange Zeichen von der Tastatur eingelesen, bis die Ende-Taste gedrückt wird. Dabei wird mit der switch()-Funktion in Zeile 36 bis 58 geprüft, ob
es sich bei der Eingabe um eine der vier Cursortasten oder eine der Sondertasten
Entfernen, Löschen (Backspace), Einfügen, Pos1 oder Funktionstaste 1 handelt. Eine vollständige Liste der von der ncurses-Bibliothek unterstützten Sondertasten
erhält man mit
grep KEY /usr/include/curses.h
In Zeile 57 werden alle Zeichen, deren Wert kleiner als der erste Sondertastencode
(KEY MIN) ist, an der aktuellen Position ausgegeben.
Die Funktion getyx() liefert die aktuelle Cursor-Position in die Variablen x
und y zurück. Es handelt sich bei dieser Funktion um ein Makro, daher kann der
Inhalt der Variablen x und y verändert werden, ohne dass ein Zeiger auf die
Variablen übergeben wird; es wird also kein vorangestelltes &“ benötigt.
”
Vor Ende des Hauptprogramms muss noch das ncurses-Fenster wieder ge”
schlossen“ werden. Dies geschieht mit der Funktion endwin() in Zeile 62 des
Quelltextes.
Die Funktion print pos(), die in den Quelltextzeilen 8 bis 18 definiert ist, sichert zunächst mit getyx() die aktuelle Cursor-Position in die Variablen x und
y (Zeile 12). Diese Position wird dann mit Hilfe von mvprintw() an der festen
Position (0,0) – also links oben – ausgegeben (Zeile 14). Danach wird der Cursor mit move() wieder an die ursprüngliche Stelle verschoben (Zeile 16). Mit den
Funktionen attron() attroff() (Zeile 13 und 15) können Textattribute ein- und
ausgeschaltet werden. Tabelle 3.3 zeigt einige der möglichen Attribute.
Tabelle 3.3: Einige Textattribute der ncurses-Bibliothek
Attribut
Beschreibung
A
A
A
A
A
A
normaler Text
unterstreichen
inverser Text (Weiß auf Schwarz)
blinkender Text
reduzierter Kontrast
Fettschrift
STANDOUT
UNDERLINE
REVERSE
BLINK
DIM
BOLD
3.4 Ausgabesteuerung im Terminal-Fenster
65
Bei Verwendung der ncurses-Bibliothek sollten alle Ein- und Ausgaben über
die ncurses-eigenen Funktionen erfolgen, also z.B. über printw() und nicht
etwa über printf().
Farbige Textdarstellung mit der ncurses-Bibliothek
Natürlich kann mit der ncurses-Bibliothek auch farbiger Text ausgegeben werden
– sofern das Terminal oder die Console Farben unterstützen. Das folgende kleine
Programm gibt Text mit unterschiedlicher Vorder- und Hintergrundfarbe aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
curses_bsp2.c - Farbdarstellung mit ncurses
*/
# include <stdio.h>
# include <curses.h>
int main()
{
int
x, y;
WINDOW *win;
if ((win = initscr()) == NULL)
return(1);
start_color(); /* Farbausgabe initialisieren */
cbreak();
noecho();
init_pair(1, COLOR_BLACK, COLOR_WHITE);
init_pair(2, COLOR_YELLOW, COLOR_BLUE);
init_pair(3, COLOR_RED, COLOR_CYAN);
color_set(1, NULL);
/* Hintergrund weiß */
for (y=0; y<LINES; y++)
for (x=0; x<COLS; x++)
mvaddch(y, x, ’ ’);
box(win, 0, 0);
/* Fenster einrahmen */
mvprintw(2, 2, "Farbkombination 1");
color_set(2, NULL);
mvprintw(3, 2, "Farbkombination 2");
color_set(3, NULL);
mvprintw(4, 2, "Farbkombination 3");
66
35
36
37
38
39
40
3 Kommandozeilenprogramme
getch();
/* auf eine Taste warten */
endwin();
return(0);
}
Bevor Farben für den Text und den Hintergrund eingestellt werden können, muss
zunächst die Verwendung von Farben initialisiert werden. Das geschieht in Zeile
15 mit der Funktion start color(). Es wird empfohlen, diese Funktion unmittelbar nach initscr() aufzurufen.
Als Nächstes sollten die Farbenpaare (Vorder- und Hintergrundfarbe) definiert
werden, die man verwenden möchte (hier in den Zeilen 19 bis 21). Ein solches
Farbenpaar lässt sich dann mit der Funktion color set() aktivieren (Zeile 23).
In den Zeilen 24 bis 26 wird der gesamte Fensterinhalt auf die Hintergrundfarbe
der Farbkombination 2 – hier also Weiß – einstellen. Man beachte, dass die globalen Variablen LINES und COLS von der ncurses-Bibliothek mit der Größe des
Fensters initialisiert werden. Mit der Funktion box() in Zeile 28 wird das Fenster
dann noch eingerahmt“. Anschließend werden Textzeilen in den unterschiedli”
chen Farbkombinationen ausgegeben. Das Ergebnis ist in Abbildung 3.3 zu sehen.
Abbildung 3.3: Farbausgabe mit der ncurses-Bibliothek
Kapitel 4
Dateien und Verzeichnisse
In diesem Kapitel werden die für den Zugriff auf Dateien und Verzeichnisse relevanten C-Funktionen betrachtet. Ein Großteil der hier behandelten Funktionen
entspricht dem ANSI-C-Standard und ist daher auf andere Betriebssysteme übertragbar. Es werden aber auch Linux- bzw. Unix-spezifische Funktionen angesprochen, die über diesen Standard hinausgehen.
4.1 Die Arbeit mit Dateien
Die folgenden Abschnitte befassen sich zunächst mit den Grundlagen von Dateizugriffen. Es werden die relevanten C-Funktionen vorgestellt und deren Verwendung anhand von Beispielen demonstriert.
4.1.1 Gepufferte Ein-/Ausgabe
In C-Programmen werden in der Regel Funktionen zum so genannten gepufferten
Dateizugriff verwendet. Dabei wird für jede geöffnete Datei ein Puffer von 8192
Bytes1 im Hauptspeicher angelegt. Beim Schreiben in die Datei werden zunächst
die Zeichen nur in dem Puffer abgelegt, bis dieser voll ist. Erst dann wird der
Inhalt des Puffers tatsächlich auf das physikalische Medium (z.B. die Festplatte)
geschrieben.
Da eine Datei zum Lesen und zum Schreiben geöffnet werden kann, gibt es immer
zwei Puffer: einen Lese- und einen Schreibpuffer. Beim Schließen einer geöffneten
Datei werden automatisch die noch im Puffer befindlichen Zeichen auf das Medium geschrieben und dann der Speicher für den Puffer freigegeben.
1
Die Größe des Puffers ist als Konstante BUFSIZ definiert.
68
4 Dateien und Verzeichnisse
Vorteil der gepufferten Ein-/Ausgabe ist die deutlich gesteigerte Performance der
Dateizugriffe, da nicht für jedes Zeichen ein Zugriff auf das physikalische Medium erfolgen muss. Wie in dem Abschnitt 5.2.3 noch gezeigt wird, ist es sinnvoll,
sich als Programmierer der Tatsache bewusst zu sein, dass eine solche Pufferung
erfolgt.
Datei-Deskriptoren
Zu jeder geöffneten Datei – ob gepuffert oder nicht – gehört ein so genannter
Datei-Deskriptor (engl.: file descriptor). Es handelt sich dabei um eine Integer-Zahl
≥ 0. Einige Datei-Funktionen, z.B. für die ungepufferte Ein-/Ausgabe, benötigen oder liefern einen solchen Datei-Deskriptor. Beispiele: open(), close(),
fstat(), ioctl() usw.
Dateizeiger vom Typ Stream
Neben den Datei-Deskriptoren gibt es eine weitere Möglichkeit, eine geöffnete Datei mit einer Variablen zu assoziieren: durch einen Zeiger auf eine Struktur vom
Typ FILE, die häufig als Stream – also als Datenstrom“ – bezeichnet wird. Ein
”
solcher Zeiger ist jedoch nur für gepufferte Dateien verfügbar. Funktionen, die
einen Stream liefern oder als Parameter benötigen, sind unter anderem: fopen(),
fclose(), fread(), fwrite(), fprintf() und fscanf(). Wie bereits oben
erwähnt, ist zu jedem Stream auch ein Datei-Deskriptor verfügbar. Diesen liefert
die Funktion fileno().
4.1.2 stdin, stdout und stderr
In C sind drei Streams automatisch verfügbar, ohne dass diese explizit geöffnet
werden:
Stream
Datei-Deskriptor
Funktion
stdin
stdout
stderr
STDIN FILENO
STDOUT FILENO
STDERR FILENO
Standard-Eingabekanal
Standard-Ausgabekanal
Ausgabekanal für Fehlermeldungen
Daten, die ein Programm an stdout oder stderr schickt, werden in dem zugehörigen Terminal-Fenster ausgegeben. Eingaben über die Tastatur sind über
stdin verfügbar. Genau genommen sind diese drei Streams mit dem Device verbunden, das sich hinter dem Terminal verbirgt, also z.B. /dev/tty1“ für die
”
erste Linux-Konsole oder /dev/pts/1“ für ein X-Terminalfenster.
”
In der Shell hat der Anwender jedoch die Möglichkeit, jede dieser drei StandardKanäle mit Hilfe der Umlenksymbole <“ und >“ mit einer Datei zu verbinden,
”
”
z.B.:
primzahl > primzahlen.txt
4.1 Die Arbeit mit Dateien
69
Bei diesem Aufruf öffnet die Shell (also z.B. das Programm /bin/bash) die Datei primzahlen.txt“ zum Schreiben. Der Datei-Zeiger stdout wird auf die”
sen Stream eingestellt, sodass alle an stdout geschickten Ausgaben in der Datei primzahlen.txt“ gespeichert werden. Nach Beendigung des Programms
”
wird diese Datei von der Shell wieder geschlossen. An stderr geschickte Daten
– in der Regel Fehlermeldungen – werden bei diesem Beispiel nach wie vor im
Terminal-Fenster angezeigt und nicht in der Datei gespeichert!
Sollen die Ausgaben eines Programms sowohl in einer Datei gespeichert als auch
gleichzeitig in dem Terminal ausgegeben werden, kann dies mit Hilfe des Programms tee“ (sprich tie“) erfolgen:
”
”
primzahl | tee primzahlen.txt
Die Funktionen printf() und scanf() verwenden stdout bzw. stdin als Ausbzw. Eingabekanal.
4.1.3 Dateien öffnen und schließen
Neben den Standard-Ein-/Ausgabekanälen muss in der Regel auch auf weitere
Dateien zugegriffen werden. Dazu müssen sie zunächst geöffnet und sollten auch
wieder geschlossen werden, sobald kein weiterer Zugriff mehr erforderlich ist.
Wir werden in diesem Kapitel nur das Öffnen von Dateien als Stream verwenden, also für gepufferte Ein-/Ausgabe. Das ungepufferte Öffnen von Dateien und
die Verwendung von Datei-Deskriptoren behandeln wir in den Kapiteln 5 und 6
ausführlich.
Mit der Funktion fopen() wird die in path angegebene Datei geöffnet:
FILE *fopen(char *path, char *mode);
Als Rückgabewert liefert fopen() entweder einen Zeiger auf den Stream – also
auf eine Struktur vom Typ FILE – oder NULL1 für den Fall, dass sich die Datei
nicht öffnen ließ. Der Parameter mode ist ein Zeiger auf eine Zeichenkette, die
beschreibt, wie die Datei zu öffnen ist. Mögliche Werte für mode sind in Tabelle 4.1
aufgeführt.
Für das ordnungsgemäße Öffnen einer Datei muss überprüft werden, ob der
Rückgabewert NULL ist – die Datei sich also nicht öffnen ließ – und ggf. eine Fehlermeldung ausgegeben wird. Ein typischer Quelltextausschnitt sieht dann wie
folgt aus:
# include <stdio.h>
# include <string.h>
1
Die Konstante NULL entspricht dem Ausdruck ((void *)0)“.
”
70
4 Dateien und Verzeichnisse
Tabelle 4.1: Bedeutung des mode-Parameters der Funktion fopen()
Mode
Bedeutung
r
Datei zum Lesen öffnen.
r+
Datei zum Lesen und Schreiben öffnen.
w
Datei zum Schreiben öffnen. Falls die Datei bereits
existiert, wird ihr Inhalt gelöscht.
w+
Datei zum Lesen und Schreiben öffnen. Falls die Datei
noch nicht existiert, wird sie neu angelegt.
a
Datei zum Schreiben öffnen. Der Dateizeiger steht am
Ende der Datei. Der alte Inhalt geht nicht verloren.
a+
Datei zum Lesen und Schreiben öffnen. Der Dateizeiger
steht am Ende der Datei. Der alte Inhalt geht nicht
verloren.
char *filename;
FILE *in_stream;
if ((in_stream = fopen(filename, "r")) == NULL)
{
fprintf(stderr, "%s: Can’t open file ’%s’ for "
"input: ", basename(argv[0]), filename);
perror("");
exit(1);
}
Wurde eine Datei erfolgreich geöffnet, kann entsprechend der mode-Angabe auf
diese Datei zugegriffen werden. Wird kein weiterer Zugriff benötigt, sollte die
Datei mit
fclose(Stream);
wieder geschlossen werden. Zwar werden bei Beendigung eines C-Programms
alle noch offenen Dateien automatisch wieder geschlossen, trotzdem sollte man
Dateien, auf die nicht mehr zugegriffen wird, unmittelbar mit fclose() schließen, um Ressourcen zu schonen.
4.1.4 Lesen aus und Schreiben in Dateien
Die C-Standard-Funktionsbibliothek libc bietet eine ganze Reihe von Funktionen
für den Schreib-/Lesezugriff auf Dateien an. Die wichtigsten dieser Funktionen
werden in diesem Abschnitt angesprochen.
4.1 Die Arbeit mit Dateien
71
Eine Datei wird in C als sequenzieller Datenstrom betrachtet, bestehend aus einer
Folge von Bytes. Anders als bei einer Zeichenkette ist bei Dateien ohne weiteres
kein wahlfreier Zugriff (engl.: random access) auf die einzelnen Zeichen möglich.1
Stattdessen wird bei Schreib- und Lesezugriffen der Dateizeiger um die Zahl der
gelesenen oder geschriebenen Zeichen verschoben.
Zeichenweise zugreifen: fgetc() und fputc()
Die einfachste Art des Dateizugriffs erfolgt mit den Funktionen fgetc() und
fputc(), die jeweils das nächste Zeichen aus der Datei lesen bzw. in die Datei
schreiben:
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
Die Funktion fgetc() liefert als Rückgabewert das ausgelesene Zeichen, jedoch
nicht als Typ char (1 Byte), sondern als int (4 Bytes). Dadurch kann die Funktion
das Erreichen des Dateiendes bzw. das Auftreten eines Fehlers durch den speziellen Rückgabewert EOF ( End Of File“ = –1) signalisieren. Die Funktion fputc()
”
schreibt das Zeichen c, umgewandelt zum 1-Byte-Wert ohne Vorzeichen, in die angegebene Datei. Tritt ein Fehler auf (z.B. der Datenträger ist voll), wird auch hier
die Konstante EOF zurückgegeben. Bei fehlerfreier Ausführung liefert die Funktion das geschriebene Zeichen zurück.
Das folgende Beispiel soll die Verwendung der Funktionen fgetc() und
fputc() demonstrieren. Mit Hilfe einer while()-Schleife wird hier die Datei
in stream nach out stream kopiert:
int c;
FILE *in_stream, *out_stream;
while ((c = fgetc(in_stream)) != EOF)
fputc(c, out_stream);
Natürlich müssen die Dateien zuvor – wie in Abschnitt 4.1.3 beschrieben –
geöffnet werden. Das Erreichen des Dateiendes kann auch mit der Funktion
feof(Stream) abgefragt werden, ein Zugriffsfehler lässt sich mit ferror(Stream)
feststellen.
Die Funktion ungetc() erlaubt es, ein mit fgetc() gelesenes Zeichen wieder
in den Puffer zurückzuschreiben, sodass es für den nächsten Lesezugriff wieder
bereitsteht:
int ungetc(int c, FILE *stream);
1
Die Funktion fseek() ermöglicht hier scheinbar“ einen wahlfreien Zugriff, sie ist aber nicht auf
”
jede Art von Dateien anwendbar.
72
4 Dateien und Verzeichnisse
Folgendes Beispielprogramm ersetzt unter Verwendung der Funktion ungetc()
in Textdateien das unter DOS/Windows übliche Zeilenende \r\n“ durch ein
”
\n“, wie es bei Unix/Linux verwendet wird:
”
int c, c2;
while ((c = fgetc(stdin)) != EOF)
{
if (c == ’\r’)
{
if ((c2 = fgetc(stdin)) == ’\n’)
c = c2;
else
ungetc(c2, stdin);
}
fputc(c, stdout);
}
Einzelne \r“, auf die kein Zeilenvorschub ( \n“) folgt, werden nicht verändert.
”
”
Durch die Verwendung der Funktion ungetc() wird hier der Programmieraufwand deutlich verringert, da Sonderfälle wie \r\r\n“ oder ein \r“ am Datei”
”
ende nicht gesondert berücksichtigt werden müssen. Durch ungetc() wird das
auf \r“ folgende Zeichen – sofern es kein Zeilenvorschub ist – einfach für den
”
nächsten Durchlauf der while()-Schleife wieder zur Verfügung gestellt.
Datensätze lesen/schreiben: fread() und fwrite()
Ist die Länge eines Datensatzes in einer Datei bekannt, so kann der Dateizugriff
mit den Funktionen fread() und fwrite() vereinfacht werden:
size_t fread(void *ptr, size_t size, size_t nmemb,
FILE *stream);
size_t fwrite(void *ptr, size_t size, size_t nmemb,
FILE *stream);
Der Typ size t wird im Allgemeinen für Längen- und Offset-Angaben verwendet und entspricht unsigned long int. Die Parameter der Funktionen fread()
und fwrite() haben die folgende Bedeutung:
ptr
size
nmemb
stream
Zeiger auf den Ziel- bzw. Quell-Speicherbereich
Größe eines Datensatzes (in Bytes)
Anzahl der Datensätze
Datei, aus der gelesen bzw. in die geschrieben wird
Als Rückgabewert liefern beide Funktionen die Anzahl der gelesenen bzw. geschriebenen Datensätze (nicht der Bytes!).
4.1 Die Arbeit mit Dateien
73
Dateizeiger verschieben mit fseek()
Wie bereits zuvor beschrieben, wird eine Datei als sequenzieller Datenstrom behandelt, bei dem die Zeichen genau in der Reihenfolge gelesen werden, in der sie
zuvor in die Datei geschrieben wurden. Es gibt jedoch einige Funktionen, die das
Verstellen des Dateizeigers – und damit das Vor- und Zurückspringen innerhalb
der Datei – erlauben. Die wichtigste dieser Funktionen ist fseek():
int fseek(FILE *stream, long offset, int whence);
Die Funktion fseek() verschiebt den Dateizeiger um offset Zeichen, wobei der
Parameter whence angibt, von wo aus der Dateizeiger verschoben werden soll:
SEEK SET
SEEK CUR
SEEK END
Position relativ zum Dateianfang
Verschiebung relativ zur aktuellen Position
Position relativ zum Dateiende
Der Rückgabewert von fseek() ist 0 bei fehlerfreier Ausführung und –1 im Fehlerfall.
Es gibt Streams, auf die die Funktion fseek() nicht angewendet werden kann!
Das ist z.B. bei den Standardein- und -ausgabekanälen oder bei Pipes der Fall.
Weitere Funktionen
Es gibt eine ganze Reihe weiterer Funktionen für den Dateizugriff, die wir hier
nicht alle behandeln können. Auch an dieser Stelle sei noch einmal auf die man“”
Seiten hingewiesen. Einige der häufig verwendeten Funktionen soll die folgende
Übersicht aufzeigen:
fflush() Leert den Dateipuffer. Alle Zeichen im Schreibpuffer werden in die Datei
(das physikalische Medium) geschrieben. Nur anzuwenden auf Dateien, die
zum Schreiben geöffnet sind.
fgets() und fputs() Zeilenweise lesen/schreiben. fgets() liest aus der Datei bis
zum Zeilenende ( \n“) oder Dateiende. fputs() schreibt eine Zeichenkette in
”
die Datei.
fprintf() und fscanf() Formatiertes Lesen aus bzw. Schreiben in eine Datei. Die
Funktionen sind analog zu printf() und scanf() mit dem Unterschied,
dass statt der Standardein- und -ausgabe beliebige Dateien angegeben werden
können.
mmap() und munmap() Mit diesen Funktionen kann eine Datei oder der Teil einer Datei in den Hauptspeicher kopiert werden.
74
4 Dateien und Verzeichnisse
4.1.5 Ein Beispiel: Zeilen nummerieren
Der Umgang mit Dateien soll im Folgenden anhand eines kleinen Beispielprogramms veranschaulicht werden. Das Programm number“ liest die als Argu”
mente angegebenen Textdateien ein und fügt am Anfang jeder Zeile eine Zeilennummer ein. Die Ausgabe der so erweiterten Dateien erfolgt entweder über den
Standard-Ausgabekanal oder, falls die Option -o“ angegeben wird, in eine Aus”
gabedatei. Das Programm verwendet dazu die Funktionen fopen(), fclose(),
fgets() und fprintf().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
number.c - Add line numbers to text file(s).
*/
# include <stdio.h>
# include <unistd.h>
int main(int argc, char *argv[])
{
int option, i, line;
char *output_file=NULL;
static char buffer[256];
FILE *in_stream, *out_stream;
while ((option = getopt(argc, argv, "ho:")) > 0)
if (option == ’h’)
{
printf("Usage: number [-o output-file] "
"input-file ...\n");
return(0);
}
else if (option == ’o’)
output_file = optarg;
else
return(1);
if (optind == argc)
{
fprintf(stderr, "number: Missing file name. "
"Type ’number -h’ for help.\n");
return(1);
}
if (output_file == NULL)
out_stream = stdout;
4.2 Eigenschaften von Dateien oder Verzeichnissen auswerten
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
75
else if ((out_stream = fopen(output_file, "w")) == NULL)
{
perror("number: Can’t open output file");
return(1);
}
line = 1;
for (i=optind; i<argc; i++)
{
if ((in_stream = fopen(argv[i], "r")) == NULL)
{
fprintf(stderr, "%s: Can’t open file ’%s’ "
"for input: ", argv[0], argv[i]);
perror("");
return(1);
}
while (fgets(buffer, 256, in_stream) != NULL)
fprintf(out_stream, "%3d %s", line++, buffer);
fclose(in_stream);
}
if (output_file != NULL)
fclose(out_stream);
return(0);
}
4.2 Eigenschaften von Dateien oder Verzeichnissen
auswerten
Oftmals ist es erforderlich, Informationen über eine Datei oder ein Verzeichnis einzuholen, z.B. die Länge oder die Zugriffsrechte. Dazu dient die Funktion stat():
int stat(char *file_name, struct stat *status);
Diese Funktion
untersucht“ die Datei (oder das Verzeichnis) mit dem in
”
file name angegebenen Namen (und Pfad) und speichert die Informationen in
der Struktur vom Typ stat, auf die der Parameter status zeigt. Diese Struktur
enthält folgende Einträge:
76
4 Dateien und Verzeichnisse
st
st
st
st
st
st
st
st
st
st
st
st
st
dev
ino
mode
nlink
uid
gid
rdev
size
blksize
blocks
atime
mtime
ctime
Device
zugehöriger Inode (Block mit Dateikopf)
Zugriffsrechte
Anzahl der Hard Links
Benutzer-ID
Gruppen-ID
Typ des Devices (falls es ein Device ist)
Länge in Bytes
Blockgröße
Anzahl der belegten Blöcke
Uhrzeit u. Datum des letzten Zugriffs
Uhrzeit u. Datum des letzten Schreibzugriffs
Uhrzeit u. Datum der letzten Änderung der
Zugriffsrechte
Mit Hilfe der folgenden Makros lässt sich anhand des Elements st mode der Dateityp feststellen:
Makro
S
S
S
S
S
S
S
ISLNK(st mode)
ISREG(st mode)
ISDIR(st mode)
ISCHR(st mode)
ISBLK(st mode)
ISFIFO(st mode)
ISSOCK(st mode)
ergibt wahr“, falls die Datei. . .
”
ein symbolischer Link ist
eine reguläre“ Datei ist
”
ein Verzeichnis ist
ein character device ist
ein block device ist
ein FIFO ist
ein socket ist
Das folgende Beispielprogramm status“ zeigt die Anwendung der Funktion
”
stat(). Es liefert für den als Argument angegebenen Pfadnamen die Informationen über Namen, Länge, Typ, Zugriffsrechte, Besitzer der Datei (User ID) und
Zeitpunkt der letzten Änderung.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
status.c - get status of file or directory.
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<sys/stat.h>
<time.h>
<string.h>
int main(int argc, char *argv[])
{
struct stat status;
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
4.3 Verzeichnisse einlesen
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
77
{
printf("Usage: status filename\n");
return(1);
}
if (stat(argv[1], &status) != 0)
{
perror("status: Can’t get file status");
return(1);
}
printf("file/dir name:\t’%s’\n", argv[1]);
printf("file/dir size:\t%ld bytes\n",
status.st_size);
if (S_ISDIR(status.st_mode))
printf("type:\t\tdirectory\n");
else if (S_ISREG(status.st_mode))
printf("type:\t\tregular file\n");
printf("protection:\t%o\n", status.st_mode & 0x1ff);
printf("owner:\t\t%d\n", status.st_uid);
printf("last modified:\t%s",
ctime(&(status.st_mtime)));
return(0);
}
4.3 Verzeichnisse einlesen
Sollen alle Einträge eines Verzeichnisses aufgelistet werden – ähnlich wie beim
Programm ls“ –, ist dies mit Hilfe der Funktionen
”
DIR *opendir(char *name);
int closedir(DIR *dir);
struct dirent *readdir(DIR *dir);
möglich. Ähnlich, wie Dateien vor dem Lesen geöffnet werden müssen, ist auch
hier zunächst das Öffnen des Verzeichnisses mittels opendir() erforderlich.
Rückgabewert dieser Funktion ist ein Zeiger auf eine Struktur vom Typ DIR (analog zur Funktion fopen(), die den Zeiger auf eine Struktur vom Typ FILE liefert).
Mit Hilfe der Funktion readdir() können dann nacheinander alle Einträge des
Verzeichnisses gelesen werden, wobei readdir() einen Zeiger auf eine Struktur
vom Typ dirent liefert. Diese enthält unter anderem das Element d name, also
eine Zeichenkette mit dem Namen des Verzeichnis-Eintrags. Ist das Ende des Verzeichnisses erreicht, gibt readdir() einen Nullzeiger zurück.
78
4 Dateien und Verzeichnisse
Das folgende Programm get dir“ verhält sich wie ls -U1“, es zeigt alle Einträge
”
”
des als Argument angegebenen Verzeichnisses unsortiert an:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
get_dir.c
*/
#
#
#
#
include
include
include
include
<stdio.h>
<dirent.h>
<sys/types.h>
<string.h>
int main(int argc, char *argv[])
{
DIR *directory;
struct dirent *dir_entry;
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
{
printf("Usage: get_dir path\n");
return(1);
}
if ((directory = opendir(argv[1])) == NULL)
{
perror("get_dir: Can’t open directory");
return(1);
}
while ((dir_entry = readdir(directory)) != NULL)
printf("%s\n", dir_entry->d_name);
closedir(directory);
return(0);
}
Kapitel 5
Interprozesskommunikation
Linux ist ein Multi-Tasking-Betriebssystem, d. h. mehrere Prozesse werden (scheinbar) gleichzeitig abgearbeitet. Für den Anwender bedeutet dies, dass er mehrere
Anwendungen gleichzeitig nutzen kann – z. B. eine Textverarbeitung, während
ein MP3-Decoder Musik abspielt – sogar während andere Anwender weitere Programme auf dem gleichen System laufen lassen.
Dem Programmierer eröffnet sich durch die Multi-Tasking-Fähigkeit die Möglichkeit, verschiedene Aufgaben eines Programms auf mehrere Prozesse aufzuteilen.
Um diese Möglichkeit sinnvoll nutzen zu können, muss er sich auch mit der Kommunikation der Prozesse, also dem Datenaustausch zwischen verschiedenen Prozessen, auseinandersetzen.
5.1 Prozessverwaltung unter Linux
Neue Prozesse (auch als Tasks oder Threads bezeichnet) können unter Linux nicht
aus dem Nichts“ entstehen, sondern werden von einem Eltern-Prozess (engl.: pa”
rent process) gestartet – mit Ausnahme des INIT-Prozesses, der beim Hochfahren
des Systems gestartet wird und selbst keinen Eltern-Prozess besitzt.
Eltern- und Kind-Prozess (engl.: child process) bleiben miteinander verknüpft. So
liefert die C-Funktion getppid() die Prozess-ID des Eltern-Prozesses, und das
Beenden eines Kind-Prozesses wird dem zugehörigen Eltern-Prozess durch ein
Signal angezeigt.
Jeder Prozess erhält eine eindeutige Identifikationsnummer: die Prozess-ID. Jede
ID wird nur einmal vergeben. Selbst wenn der zugehörige Prozess beendet wurde,
wird dessen ID nicht neu belegt. Die Reihenfolge der IDs entspricht der Reihenfolge, in der die Prozesse erzeugt wurden: je jünger“ ein Prozess, desto größer seine
”
ID. Eine Liste aller existierenden Prozesse erhält man mit ps -A“. An erster Stel”
80
5 Interprozesskommunikation
le steht hier der Prozess init“, der immer die Prozess-ID 1 besitzt. Ganz unten
”
in der Liste steht in der Regel der Prozess, in dem das Programm ps“ selbst läuft.
”
Man erkennt daran, dass jedes aus einer Shell heraus gestartete Programm einen
eigenen Prozess erhält und nicht etwa in dem Shell-Prozess läuft.1
5.2 Neue Prozesse starten
Bevor man sich mit Interprozesskommunikation befasst, muss man wissen, wie
ein neuer Kind-Prozess erzeugt wird. In diesem Abschnitt werden zwei Methoden
zur Erzeugung eines neuen Prozesses vorgestellt. Außerdem wird angerissen, was
beim Erzeugen eines Prozesses geschieht und was man als Programmierer dabei
beachten muss.
5.2.1 Shell-Programme aufrufen mit system()
Manchmal kann es sinnvoll sein, innerhalb eines eigenen Programms ein anderes Programm zu starten, um dessen Funktionalität für das eigene Programm zu
nutzen. Beispiel: Sie berechnen mit Ihrem Programm eine Reihe von Zahlen, die
Sie auch gern grafisch darstellen würden. Sie können dann natürlich die grafische
Ausgabe selbst programmieren, was im Allgemeinen einen nicht unerheblichen
Aufwand bedeutet, oder rufen von Ihrem Programm aus ein bereits existierendes
Programm zur Visualisierung von Daten (z.B. gnuplot) auf. Dies ist mit Hilfe der
Funktion
int system(char *command);
möglich. Die Funktion system() übernimmt hier gleich mehrere Aufgaben: sie
startet eine neue Shell /bin/sh (in einem eigenen Prozess) und lässt diese Shell
das in command angegebene Kommando ausführen. Ferner wartet system() auf
die Beendigung des Kind-Prozesses, sodass Ihr Programm erst danach fortgesetzt
wird.
Als Rückgabewert liefert system() entweder 127 für den Fall, dass die Shell nicht
gestartet werden kann, oder den Rückgabewert des ausgeführten Kommandos.
Wurde das Kommando fehlerfrei ausgeführt, sollte system() den Wert 0 zurückgeben.
Das folgende Beispielprogramm zeigt die Verwendung der Funktion system().
Es stellt mit Hilfe des Programms gnuplot für fünf Sekunden eine Sinus-Kurve
dar (dazu müssen Sie natürlich das Paket gnuplot installiert haben):
1
Es sei denn, man stellt ein exec“ voran.
”
5.2 Neue Prozesse starten
1
2
3
4
5
6
7
8
9
10
11
12
81
/*
sinus.c
*/
# include <stdio.h>
# include <stdlib.h>
int main()
{
system("echo ’plot sin(x); pause 5’ | gnuplot");
return(0);
}
5.2.2 Die Funktionen der exec-Familie
Eigentlich gehört dieser Abschnitt gar nicht in das Kapitel Interprozesskommunikation, da die Funktionen der exec-Familie keine neuen Prozesse erzeugen.
Sie werden jedoch häufig in Verbindung mit fork() (s. u.) als Ersatz für die zuvor
beschriebene Funktion system() verwendet (vgl. hierzu auch man 3 system“).
”
Es gibt insgesamt sechs Funktionen der exec-Familie, die sich vor allem durch
Abweichungen in der Art der Parameter unterscheiden:
int
int
int
int
int
int
execl(char *path, char *arg, ...);
execlp(char *file, char *arg, ...);
execle(char *path, char *arg , ..., char *envp[]);
execv(char *path, char *argv[]);
execvp(char *file, char *argv[]);
execve(char *path, char *argv[], char *envp[]);
Alle diese Funktionen führen – ähnlich wie die Funktion system() – ein Programm aus, dessen Name in dem Parameter file bzw. path angegeben ist.
Im Unterschied zu system() wird keine neue Shell gestartet, sondern das auszuführende Programm läuft in dem gleichen Prozess, der die exec-Funktion
aufgerufen hat. Bei Beendigung des Programms wird auch dieser Prozess beendet, d.h. der Prozess kehrt nach erfolgreicher Ausführung einer exec()-Funktion
nicht mehr in das ursprüngliche Programm zurück. Lediglich bei Auftreten eines
Fehlers, weil z.B. das angegebene Programm nicht existiert, kehren die exec()Funktionen mit dem Rückgabewert –1 zurück.
Man erkennt, dass der Funktionsname jeweils aus exec“ plus eine Endung aus
”
ein oder zwei Buchstaben gebildet wird. Ein l“ in der Endung bedeutet da”
bei, dass die Argumente (also die Kommandozeilenparameter) der aufzurufenden
Funktion als Liste übergeben werden, d.h. für jedes Argument eine Zeichenkette.
Das erste Argument muss dabei den Programmnamen selbst enthalten! Ein v“
”
dagegen bedeutet, dass die Argumente als Vektor übergeben werden, und zwar
82
5 Interprozesskommunikation
in der Form, wie sie auch die Funktion main() erhält (vgl. Abschnitt 3.1.2). Auch
hier entspricht das erste Argument, also argv[0], dem Namen des aufzurufenden Programms.
Ein p“ in der Endung des Funktionsnamens weist darauf hin, dass diese Funkti”
on alle in der Umgebunsvariablen PATH eingetragenen Pfade nach dem angegebenen Programm durchsucht, während die Funktionen ohne ein p“ in der Endung
”
den vollständigen Pfad des auszuführenden Programmes benötigen (daher wurde der Parameter bei diesen Funktionen als path und nicht als file bezeichnet).
Das e“ in der Endung der Funktionen execle() und execve() bedeutet, dass
”
diese Funktionen zusätzlich zu den Argumenten für das aufzurufende Programm
einen Vektor mit Umgebungsvariablen benötigen, wie sie an das Programm weitergegeben werden sollen. Die anderen Funktionen behalten die aktuellen Umgebungsvariablen bei.
Die beiden folgenden Programmausschnitte sollen die Verwendung der execFunktionen demonstrieren:
Beispiel 1:
# include <stdio.h>
# include <unistd.h>
char *argv[4], *env[2];
argv[0] = "/bin/ls";
argv[1] = "--color";
argv[2] = "/home/martin";
argv[3] = NULL;
env[0] = "LS_COLORS=fi=00:di=01";
env[1] = NULL;
execve("/bin/ls", argv, env);
perror("execve() failed");
Beispiel 2:
execlp("ls", "ls", "-F", "/home/martin", NULL);
5.2.3 Einen Kind-Prozess erzeugen mit fork()
Die zentrale Funktion, mit der unter Linux/Unix ein Kind-Prozess erzeugt wird,
ist fork(). Die Funktion hat keinen Parameter; sie liefert als Rückgabewert die
Prozess-ID des neu erzeugten Kind-Prozesses. In dem Kind-Prozess selbst liefert
die Funktion eine 0 als Rückgabewert; konnte kein Kind-Prozess erzeugt werden,
ist der Rückgabewert –1:
5.2 Neue Prozesse starten
83
# include <stdio.h>
# include <unistd.h>
int child_pid;
if ((child_pid = fork()) == 0)
printf("Dies ist der Kind-Prozess.\n");
else if (child_pid > 0)
printf("Kind-Prozess %d wurde erzeugt.\n",
child_pid);
else
perror("fork() failed");
Doch was genau passiert beim Aufruf der Funktion fork()? Linux legt eine neue
Task-Struktur an, die eine Kopie der Struktur des Eltern-Prozesses darstellt. Dieser Prozess erhält eine neue Prozess-ID, benutzt jedoch zunächst den gleichen
Speicher wie der Eltern-Prozess. Erst wenn einer der beiden Prozesse in den Speicher schreibt, also z.B. eine Variable verändert, wird der entsprechende Speicherbereich dupliziert und für beide Prozesse getrennt weitergeführt. Diese Methode bezeichnet man als copy-on-write. Dadurch wird erreicht, dass beide Prozesse scheinbar getrennten Speicher benutzen, ohne bereits bei der Erzeugung des
Kind-Prozesses den gesamten Speicher des Eltern-Prozesses kopieren zu müssen.
Nach Ausführung der Funktion fork() existieren also zwei Prozesse, die sich
zunächst allein durch die Prozess-ID unterscheiden – und durch den Rückgabewert von fork(). Bei jedem Schreib-Zugriff wird jedoch der Unterschied beider
Prozesse größer. Das folgende Beispielprogramm new child“ soll diesen Vor”
gang veranschaulichen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
new_child.c - einen Kind-Prozess erzeugen
*/
# include <stdio.h>
# include <unistd.h>
int main()
{
int child_pid, test;
test = 1;
printf("\t\ttest=%d, &test=%p\n", test, &test);
if ((child_pid = fork()) == 0)
{
sleep(1);
84
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
5 Interprozesskommunikation
printf("Kind-Prozess:\ttest=%d, &test=%p\n",
test, &test);
test = 2;
printf("Kind-Prozess:\ttest=%d, &test=%p\n",
test, &test);
}
else
{
printf("Eltern-Prozess:\ttest=%d, &test=%p\n",
test, &test);
sleep(2);
printf("Eltern-Prozess:\ttest=%d, &test=%p\n",
test, &test);
}
return(0);
}
Wenn wir dieses Programm übersetzen und ausführen, erhalten wir die Ausgabe:
Eltern-Prozess:
Kind-Prozess:
Kind-Prozess:
Eltern-Prozess:
test=1,
test=1,
test=1,
test=2,
test=1,
&test=0xbffff8c0
&test=0xbffff8c0
&test=0xbffff8c0
&test=0xbffff8c0
&test=0xbffff8c0
Man sieht, dass der Inhalt der Variablen test durch die Anweisung in Zeile 19
des Quelltextes innerhalb des Kind-Prozesses von ursprünglich 1 auf 2 verändert
wird; im Eltern-Prozess bleibt jedoch auch danach der alte Wert erhalten, obwohl
die Adresse der Variablen (also die Speicherstelle) im Eltern- und Kind-Prozess
die gleiche zu sein scheint! Man muss sich hier ins Gedächtnis rufen, dass Linux
ein Betriebssystem mit einer virtuellen Speicherverwaltung ( virtual memory“) ist.
”
Es handelt sich bei den Adressen also nicht um physikalische Speicherstellen, sondern um Offsets innerhalb einer Seite ( Page“) des virtuellen Speichers. So bleibt
”
die Adresse für beide Prozesse konstant; durch den Schreibzugriff wird jedoch die
entsprechende Seite des virtuellen Speichers kopiert und die Änderung nur in der
Kopie durchgeführt.
Anhand dieses Beispielprogramms lässt sich ein weiterer interessanter Effekt demonstrieren. Leiten Sie dazu die Ausgabe des Programms in eine Datei um, und
geben Sie diese dann aus (die Benutzereingaben sind schräg dargestellt):
> new child > ausgabe
> cat ausgabe
test=1, &test=0xbffff8c0
Kind-Prozess:
test=1, &test=0xbffff8c0
5.2 Neue Prozesse starten
Kind-Prozess:
test=2,
test=1,
Eltern-Prozess: test=1,
Eltern-Prozess: test=1,
85
&test=0xbffff8c0
&test=0xbffff8c0
&test=0xbffff8c0
&test=0xbffff8c0
Die Reihenfolge der Ausgaben hat sich verändert. Außerdem erfolgt die Ausgabe
vor Erzeugung des Kind-Prozesses (Quelltext Zeile 13) doppelt! Was ist passiert?
Sobald der Standard-Ausgabekanal durch das Umlenk-Symbol >“ in eine Datei
”
geleitet wird, erfolgt eine Pufferung der Daten (vgl. Abschnitt 4.1.1); ohne Umleiten der Ausgabe ist das nicht der Fall. Durch die Pufferung werden die Zeichen
nicht sofort in die angegebene Datei geschrieben, sondern zunächst im Puffer zwischengespeichert – so auch die Ausgabe der printf()-Anweisung in Zeile 13.
Nach Erzeugung des Kind-Prozesses benutzen beide Prozesse den gleichen Puffer weiter. Schreibt einer der beiden Prozesse weitere Zeichen in den Puffer, wird
dieser zuvor dupliziert, d.h. sein gesamter Inhalt kopiert. Dadurch liegt die Ausgabe der printf()-Anweisung jetzt in zwei getrennten Puffern, die beide mit der
Datei ausgabe“ verknüpft sind.
”
Sowohl Eltern- als auch Kind-Prozess schreiben jetzt weitere Zeilen in ihren jeweiligen Puffer, ohne dass wirklich Zeichen in die angegebene Datei geschrieben werden. Erst wenn einer der beiden Prozesse beendet wird, dieser also beim
return(0) in Zeile 33 angelangt ist, wird sein Pufferinhalt in die Datei geschrieben. Dadurch enthält die Ausgabedatei zunächst alle Ausgaben des KindProzesses (der aufgrund der kürzeren Wartezeit bei der Funktion sleep() in Zeile 17 immer vor dem Eltern-Prozess beendet wird), gefolgt von allen Ausgaben
des Eltern-Prozesses – beide jeweils angeführt von der Ausgabe der printf()Anweisung in Zeile 13.
Will man das Duplizieren des Pufferinhaltes bei Erzeugung eines neuen KindProzesses verhindern, kann man mit Hilfe der Funktion fflush() (siehe Abschnitt 4.1.4) den Pufferinhalt unmittelbar vor Erzeugen des Kind-Prozesses in
die Datei schreiben und den Puffer selbst leeren.
5.2.4 Warteschleifen
Beim Programmieren unter Betriebssystemen, die nicht Multitasking-fähig sind,
ist es üblich, mit Hilfe von Warteschleifen auf bestimmte Ereignisse – etwa das
Drücken einer Taste – zu warten. Unter Linux sollte man jedoch nach Möglichkeit
auf solche Warteschleifen verzichten und stattdessen den Ablauf des Programms
mit Hilfe von Signalen (siehe nächsten Abschnitt) steuern. Jede Warteschleife kostet Rechenzeit, die sonst für andere Prozesse zur Verfügung stehen würde.
Soll ein Programm nur eine bestimmte Zeit lang warten, ohne dies von einem Ereignis abhängig zu machen, können dazu die Funktionen sleep() und usleep()
verwendet werden. Diesen Funktionen wird als Parameter die zu wartende Zeit
in Sekunden bzw. Mikrosekunden (das u“ in usleep() steht für µ und damit
”
86
5 Interprozesskommunikation
für 10 −6 ) übergeben. Beachten Sie jedoch, dass das Eintreffen eines nicht geblockten Signals (siehe Abschnitt 5.3) diese Funktionen ungeachtet der verbleibenden
Wartezeit sofort beendet.
Lässt sich einmal eine Warteschleife nicht oder nur mit viel Aufwand umgehen,
sollte die Schleife einen Aufruf der Funktion sched yield() enthalten. Diese
Funktion unterbricht die Ausführung des Programms und stellt den Prozess an
das Ende der Liste der auszuführenden Prozesse. Dadurch wird der Rechenzeitbedarf der Warteschleife deutlich reduziert. Als Beispiel sei das folgende Programm
betrachtet, das wartet, bis eine CD-ROM oder Audio-CD in das CD-Laufwerk eingelegt wird:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
cdwait.c - Auf das Einlegen einer CD-ROM warten.
*/
# include <unistd.h>
# include <fcntl.h>
# include <sched.h>
int main()
{
int fd;
while ((fd = open("/dev/cdrom", O_RDONLY)) == -1)
sched_yield();
close(fd);
return(0);
}
5.3 Signale
Ein wichtiges Hilfsmittel für die Ablaufsteuerung von Prozessen und für die
Kommunikation zwischen Prozessen sind die Signale. Linux kennt 31 verschiedene Signale, die in der Include-Datei
/usr/include/bits/signum.h
aufgelistet sind. Hier eine Übersicht über die wichtigsten Signale:
1
Zum Ausführen des Programms müssen Sie über Lesezugriff (r) auf das Device /dev/cdrom“
”
verfügen.
5.3 Signale
87
Signalname
SIGINT
SIGKILL
SIGUSR1
SIGUSR2
SIGALRM
SIGTERM
SIGCHLD
Signal-Nr.
2
9
10
12
14
15
17
Bedeutung
Unterbrechungsanforderung (Ctrl-C)
Prozess beenden (nicht blockierbar)
benutzerdefiniert, zur freien Verwendung
benutzerdefiniert, zur freien Verwendung
für die Weckfunktion
Prozess beenden (blockierbar)
Kind-Prozess wurde beendet
Ein Prozess kann einem anderen Prozess ein Signal schicken“, um ihn z.B. auf
”
ein Ereignis aufmerksam zu machen.
5.3.1 Die Weckfunktion alarm()
Wenn ein Prozess auf ein Ereignis wartet, ist es häufig erforderlich, eine maximale Wartezeit zu realisieren, für den Fall, dass das Ereignis nicht eintrifft. Um ein
solches Timeout-Verhalten zu realisieren, kann die Funktion alarm() verwendet
werden:
unsigned int alarm(unsigned int seconds);
Die Funktion startet einen Zeitgeber, der nach Ablauf der mit seconds angegebenen Zeit (in Sekunden) dem Prozess das Signal SIGALRM schickt. Als Rückgabewert liefert alarm() die Restzeit, die bei dem vorangegangenen alarm()-Aufruf
noch verblieben ist, bzw. 0, wenn zuvor kein Aufruf der Funktion alarm() erfolgte.
Als Voreinstellung führt das Signal SIGALRM zum Abbruch des Programms (Prozesses) mit Ausgabe der Meldung Der Wecker klingelt“ (bzw. Alarm clock“)
”
”
und einem Rückgabewert ungleich 0.
Ergänzt man das Beispiel-Programm aus Abschnitt 5.2.4 um die Funktion
alarm(), kann damit ein Timeout für das Einlegen einer CD realisiert werden:
1
2
3
4
5
6
7
8
9
10
11
12
/*
cdwait2.c - Auf das Einlegen einer CD-ROM warten.
*/
# include <unistd.h>
# include <fcntl.h>
# include <sched.h>
int main()
{
int fd;
88
13
14
15
16
17
18
19
20
5 Interprozesskommunikation
alarm(10);
/* Timeout: 10 Sekunden */
while ((fd = open("/dev/cdrom", O_RDONLY)) == -1)
sched_yield();
close(fd);
return(0);
}
5.3.2 Einen Signal-Handler einrichten
In dem vorangegangenen Beispiel wird das Programm durch das Signal SIGALRM
beendet. Mit Hilfe der Funktion signal() lässt sich die Reaktion eines Prozesses
auf ein bestimmtes Signal modifizieren:1
signal(int signum, void *handler());
Diese Funktion richtet für das Signal mit der Nummer signum einen so genannten
Signal-Handler ein. Der Parameter handler() kann dabei entweder eine der Konstanten SIG IGN und SIG DFL oder der Name einer benutzerdefinierten Funktion sein. SIG IGN (IGN steht für ignore) bewirkt, dass das Signal blockiert wird,
also keine Auswirkung zeigt, während SIG DFL (DFL steht für default) den für
das entsprechende Signal voreingestellten Signal-Handler einrichtet. Die Funktion signal() liefert als Rückgabewert den zuvor mit diesem Signal verknüpften
Signal-Handler bzw. die Konstante SIG ERR, falls der Signal-Handler nicht eingerichtet werden konnte.
Bei Verwendung einer benutzerdefinierten Funktion als Signal-Handler muss diese vom Typ void sein und einen Parameter vom Typ int haben. Mit diesem Parameter wird dem Signal-Handler die Signalnummer übergeben. Damit besteht die
Möglichkeit, den gleichen Signal-Handler für verschiedene Signale zu benutzen
und dann anhand des Parameters festzustellen, welches Signal tatsächlich gesetzt
wurde.
Im folgenden Programmbeispiel wurde der Quelltext aus dem vorangegangenen Abschnitt um das Einrichten eines Signal-Handlers für das Signal SIGALRM
ergänzt:
1
2
3
4
5
6
1
/*
cdwait3.c - Auf das Einlegen einer CD-ROM warten.
*/
# include <stdio.h>
# include <unistd.h>
Die Deklaration der Funktion signal() ist hier etwas vereinfacht dargestellt, die exakte Deklaration
erhält man mit man 2 signal“.
”
5.3 Signale
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#
#
#
#
include
include
include
include
89
<stdlib.h>
<fcntl.h>
<sched.h>
<signal.h>
void my_handler(int signum)
{
fprintf(stderr, "cdwait3: timeout\n");
exit(1);
}
int main()
{
int fd;
signal(SIGALRM, my_handler);
alarm(10);
/* Signal-Handler
einrichten */
/* Timeout: 10 Sek. */
while ((fd = open("/dev/cdrom", O_RDONLY)) == -1)
sched_yield();
close(fd);
return(0);
}
Wird dieses Programm gestartet und innerhalb der darauf folgenden 10 Sekunden
keine CD eingelegt, so gibt das Programm die Meldung cdwait3: timeout“
”
aus und bricht mit dem Rückgabewert 1 ab.
5.3.3 Auf die Beendigung eines Kind-Prozesses warten
Häufig ist es erforderlich, dass ein Eltern-Prozess auf die Beendigung eines
Kind-Prozesses wartet, z.B. wenn in dem Kind-Prozess mit execlp() (vgl. Abschnitt 5.2.2) ein Programm ausgeführt wird und der Eltern-Prozess erst danach
fortfahren soll. Zu diesem Zweck stehen die Funktionen wait() und waitpid()
zur Verfügung:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
Die Funktion wait() wartet auf die Beendigung eines Kind-Prozesses und liefert dessen Prozess-ID als Rückgabewert bzw. –1 bei einem Fehler. Die Funktion
waitpid() wartet auf die Beendigung des Kind-Prozesses mit der in pid angegebenen Prozess-ID. Beide Funktionen speichern Statusinformationen zu dem
90
5 Interprozesskommunikation
beendeten Prozess in die Variable status, sofern hier nicht die Konstante NULL
angegeben wurde. Als options kann entweder eine 0 oder eine Kombination aus
den Konstanten WNOHANG und WUNTRACED angegeben werden. WNOHANG bewirkt,
dass lediglich geprüft wird, ob der angegebene Prozess beendet wurde, ohne jedoch auf dessen Beendigung zu warten.
Der Rückgabewert des beendeten Kind-Prozesses kann aus der Statusinformation
mit Hilfe des Makros WEXITSTATUS() ermittelt werden.
Das folgende Programm, das in einem Kind-Prozess den Shell-Befehl ls“ auf”
ruft, soll die Handhabung von wait() und WEXITSTATUS() verdeutlichen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
my_ls.c - "ls" in einem Kind-Prozess ausfuehren
*/
#
#
#
#
include
include
include
include
<stdio.h>
<unistd.h>
<sys/types.h>
<sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
int status;
if ((pid = fork()) == 0)
{
/* Kind-Prozess */
execlp("ls", "ls", "-F", argv[1], NULL);
perror("my_ls: execlp() failed");
return(1);
}
wait(&status);
/* Eltern-Prozess */
printf("Child process exited with return code %d.\n",
WEXITSTATUS(status));
return(0);
}
5.3.4 Signale setzen mit kill()
Ein Kind-Prozess kann dem Eltern-Prozess nicht nur seine Beendigung signali”
sieren“. Man kann Signale auch zur Kommunikation zwischen Eltern- und KindProzess verwenden, etwa um ein bestimmtes Ereignis zu melden. Grundsätzlich
lassen sich aber nur Signale an einen Prozess des gleichen Benutzers schicken, es
sei denn, das Programm läuft mit root-Privilegien.
Das Setzen von Signalen erfolgt mit der Funktion kill():
5.4 Datenaustausch zwischen Prozessen
91
int kill(pid_t pid, int signum);
wobei pid die Prozess-ID des Prozesses angibt, bei dem das Signal signum gesetzt
werden soll. Kann es gesetzt werden, gibt kill() eine 0 zurück, anderenfalls eine
–1 (z. B. wenn der angegebene Prozess nicht existiert).
Durch Verwendung des Signals SIGKILL oder SIGTERM ist es mit Hilfe dieser
Funktion auch möglich, einen anderen Prozess vorzeitig zu beenden.
5.4 Datenaustausch zwischen Prozessen
Wie wir bereits in Abschnitt 5.2.3 gesehen haben, können Eltern- und KindProzess nicht ohne weiteres über Variablen Informationen austauschen – sobald
einer der Prozesse in den zunächst gemeinsamen Speicher schreibt, wird dieser
dupliziert und ist fortan für beide Prozesse getrennt. Daher bedarf es anderer Verfahren, um einen Datenaustausch zwischen Prozessen zu realisieren, der über das
Setzen von Signalen hinausgeht.
Linux bietet hierfür drei verschiedene Möglichkeiten an: Pipes, FIFOs und Shared
Memory. Diese werden in den folgenden Abschnitten vorgestellt und erläutert.
5.4.1 Pipes
Eine Pipe ist eine Art virtuelle Datei“, dargestellt über zwei Datei-Deskriptoren
”
(siehe Abschnitt 4.1.1) – einer zum Lesen und der andere zum Schreiben. Erzeugt
wird eine Pipe mit Hilfe der gleichnamigen Funktion:
int pipe(int filedes[2]);
Als Parameter wird ein Feld aus zwei Datei-Deskriptoren (Integer-Variablen)
benötigt, in das die Funktion pipe() die erzeugten Deskriptoren schreibt. pipe()
liefert den Rückgabewert 0 bei Erfolg oder –1 bei einem Fehler.
Zur Verdeutlichung der Anwendung von pipe() sei das folgende Programm betrachtet, bei dem der Eltern-Prozess eine Zeichenkette an den Kind-Prozess sen”
det“:
1
2
3
4
5
6
7
8
9
/*
pipe.c - Interprozesskommunikation mit einer Pipe
*/
# include <stdio.h>
# include <unistd.h>
# include <sys/wait.h>
int main()
92
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
5 Interprozesskommunikation
{
int fd[2], l;
char buffer[80];
if (pipe(fd) != 0)
{
perror("pipe: pipe() failed");
return(1);
}
if (fork() == 0)
{
close(fd[1]);
/* Kind-Prozess */
if ((l = read(fd[0], buffer, 79)) == -1)
perror("pipe: read() failed");
else
{
buffer[l] = ’\0’;
printf("Received string: ’%s’\n", buffer);
}
close(fd[0]);
return(0);
}
close(fd[0]);
sleep(1);
write(fd[1], "Test!", 5);
wait(NULL);
/* Eltern-Prozess */
close(fd[1]);
return(0);
}
In Zeile 14 wird zunächst eine Pipe erzeugt; die zugehörigen Datei-Deskriptoren
werden in das Feld fd[] geschrieben. Danach wird in Zeile 20 mit fork() der
Kind-Prozess gestartet. In dem Kind-Prozess wird der zweite Datei-Deskriptor
der Pipe geschlossen (Zeile 22), weil dieser nur vom Eltern-Prozess benötigt
wird. Das Schließen von fd[1] im Kind-Prozess hat wegen des copy-on-writeMechanismus (vgl. Abschnitt 5.2.3) keinen Einfluss auf den gleichen Deskriptor
im Eltern-Prozess. Analog wird im Eltern-Prozess der dort nicht benötigte Deskriptor fd[0] geschlossen (Zeile 34).
In Zeile 23 wird jetzt innerhalb des Kind-Prozesses aus der Pipe gelesen. Die Funktion read() verhält sich ähnlich wie fread(), benötigt anstelle eines Streams jedoch einen Datei-Deskriptor (eine genaue Beschreibung der Funktionen read()
5.4 Datenaustausch zwischen Prozessen
93
und write() folgt in Kapitel 6). Nach Ausgabe der Zeichenkette mit printf()
in Zeile 28 wird der zweite Datei-Deskriptor der Pipe geschlossen (Zeile 30) und
der Kind-Prozess beendet.
Der Eltern-Prozess wartet in diesem Beispiel zunächst eine Sekunde lang (Zeile 35) und schreibt danach eine Zeichenkette in die Pipe (Zeile 36). Anschließend
wartet der Eltern-Prozess auf die Beendigung des Kind-Prozesses (Zeile 37), bevor
der zweite Datei-Deskriptor geschlossen und das Programm beendet wird (Zeile
39 und 40).
Für eine bidirektionale Kommunikation zwischen zwei Prozessen müssen zwei
Pipes geöffnet werden.
Die Verwendung von Pipes als Standardein- und -ausgabe
Wenn mit Hilfe einer Funktion der exec-Familie (siehe Abschnitt 5.2.2) in einem
Kind-Prozess ein externes“ Programm aufgerufen wird, sollen häufig die Ein”
und Ausgaben dieses Programms über den Eltern-Prozess laufen. Dazu gibt es
die Möglichkeit, die Standardein- und -ausgabe eines Prozesses durch je eine Pipe
zu ersetzen. Das folgende Programm ruft in einem Kind-Prozess das Programm
sort“ auf und schickt eine Zeichenkette, bestehend aus mehreren Zeilen, an die
”
Standardeingabe von sort. Die Standardausgabe von sort wird wiederum vom
Eltern-Prozess gelesen und ausgegeben:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
pipe2.c - Pipes als Standardein- und -ausgabe
*/
# include <stdio.h>
# include <unistd.h>
# include <sys/wait.h>
int main()
{
int fd1[2], fd2[2], l;
char buffer[80];
if ((pipe(fd1) != 0) || (pipe(fd2) != 0))
{
perror("pipe2: pipe() failed");
return(1);
}
if (fork() == 0)
{
94
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
5 Interprozesskommunikation
close(fd1[1]);
/* Kind-Prozess */
close(fd2[0]);
if ((dup2(fd1[0], STDIN_FILENO) == -1)
|| (dup2(fd2[1], STDOUT_FILENO) == -1))
{
perror("pipe2: dup2() failed");
return(1);
}
close(fd1[0]);
close(fd2[1]);
execlp("sort", "sort", NULL);
perror("pipe2: execlp() failed");
return(1);
}
close(fd1[0]);
close(fd2[1]);
/* Eltern-Prozess */
write(fd1[1], "These\nlines\nshall\nbe\nsorted\n", 28);
close(fd1[1]);
wait(NULL);
if ((l = read(fd2[0], buffer, 79)) == -1)
perror("pipe2: read() failed");
else
write(STDOUT_FILENO, buffer, l);
close(fd2[0]);
return(0);
}
Besonders interessant sind die Zeilen 24 und 25. Hier werden mit Hilfe der
Funktion dup2() ( duplicate to“) die Datei-Deskriptoren der Pipes auf die der
”
Standardein- bzw. -ausgabe des Kind-Prozesses kopiert. Danach werden fd1[0]
und fd2[1] im Kind-Prozess nicht mehr benötigt, da diese jetzt als Standardeinund -ausgabe zur Verfügung stehen. Abbildung 5.1 veranschaulicht die Funktion
der vier Datei-Deskriptoren bei diesem Beispiel-Programm.
Durch Schließen des Deskriptors fd1[1] in Zeile 41 wird dem Kind-Prozess –
und damit dem Programm sort – das Ende der Eingaben signalisiert. Dadurch
beginnt sort mit der Sortierung und Ausgabe in die Pipe fd2[]. Nach Ausgabe
der letzten Zeile wird das Programm sort und der Kind-Prozess beendet, sodass
der Eltern-Prozess nach der wait()-Anweisung in Zeile 42 fortfährt.
5.4 Datenaustausch zwischen Prozessen
fd1[1]
95
fd1[0]
- stdin
ElternProzess
KindProzess
fd2[0]
fd2[1]
stdout
Abbildung 5.1: Verwendung der Pipes in dem Programm pipe2“
”
Hinweis: Ohne die close()-Anweisung in Zeile 41 würde sort auf weitere Eingaben warten und den Kind-Prozess (und somit auch den Eltern-Prozess) blockieren!
Wird der Standardausgabekanal eines Prozesses durch eine Pipe ersetzt, erfolgt
automatisch eine Pufferung der auszugebenden Zeichen. Dadurch werden
Ausgaben nicht sofort in die Pipe, sondern zunächst nur in den Dateipuffer
geschrieben und sind somit für den Eltern-Prozess noch nicht verfügbar. Erst
wenn der Puffer (8192 Bytes) voll ist oder der Kind-Prozess beendet wird,
werden die Ausgaben tatsächlich in die Pipe geschrieben.
5.4.2 FIFOs
Als Alternative zu Pipes können zur Kommunikation zwischen zwei (oder mehr)
Prozessen so genannte FIFOs ( first-in-first-out“) verwendet werden. Dabei han”
delt es sich im Prinzip ebenfalls um Pipes, die jedoch einen Eintrag im Dateisystem haben und daher mit den üblichen Funktionen für Dateizugriffe angesprochen werden können. Eine FIFO-Datei wird mit der Funktion mkfifo() (oder
dem gleichnamigen Shell-Befehl) erzeugt:
int mkfifo(char *pathname, mode_t mode);
Die Zeichenkette pathname gibt an, welchen Pfad- und Dateinamen die FIFODatei erhalten soll. Der Parameter mode bestimmt die Zugriffsrechte der FIFODatei, wobei nur solche Rechte gesetzt werden können, die auch für den aktuellen
Prozess gesetzt sind (siehe auch man umask“). Das folgende Beispiel-Programm
”
zeigt die Erzeugung und Handhabung einer FIFO-Datei:
1
2
3
4
5
6
7
/*
fifo.c - Datenaustausch mit Hilfe einer FIFO-Datei
*/
# include <stdio.h>
# include <unistd.h>
# include <sys/stat.h>
96
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
5 Interprozesskommunikation
int main()
{
char buffer[80];
FILE *stream;
if (mkfifo("my_fifo", 0600) != 0)
{
perror("fifo: mkfifo() failed");
return(1);
}
if (fork() == 0)
{
/* Kind-Prozess */
if ((stream = fopen("my_fifo", "w")) == NULL)
perror("fifo: Can’t open FIFO for writing");
else
{
fprintf(stream, "Can you hear me?\n");
fclose(stream);
}
return(0);
}
/* Eltern-Prozess */
if ((stream = fopen("my_fifo", "r")) == NULL)
perror("fifo: Can’t open FIFO for reading");
else
{
fgets(buffer, 80, stream);
printf("Child process sent: %s", buffer);
fclose(stream);
}
remove("my_fifo");
return(0);
}
Vielleicht fragen Sie sich an dieser Stelle, worin der Unterschied zwischen einer FIFO-Datei und einer regulären Datei besteht und welchen Vorteil FIFOs für
die Prozesskommunikation bringen. Ein wesenlicher Unterschied besteht darin,
dass eine FIFO-Datei kein Ende“ hat. Liest man ein Zeichen aus einer leeren,
”
regulären Datei (z.B. mit fgetc()), erhält man den Dateiende-Marker EOF. Bei
einer FIFO-Datei wird der Prozess so lange angehalten, bis ein Zeichen in die
FIFO-Datei geschrieben wurde. Auch beim Schreiben in die FIFO-Datei wird der
Prozess blockiert, bis ein anderer Prozess die FIFO-Datei zum Lesen öffnet. Da-
5.4 Datenaustausch zwischen Prozessen
97
durch lassen sich zwei Prozesse mit Hilfe von FIFO-Dateien synchronisieren, was
mit regulären Dateien nur schwer möglich ist.
5.4.3 Shared Memory
Pipes und FIFOs bieten die Möglichkeit, einen sequenziellen Datenstrom zwischen
zwei Prozessen auszutauschen. Wird dagegen der wahlfreie Zugriff “ (random
”
access) eines Prozesses auf Datenstrukturen eines anderen Prozesses benötigt, so
bietet sich die Verwendung von Shared Memory an. Dabei handelt es sich um Speicher, der von mehreren Prozessen parallel genutzt werden kann. Hier eine Liste
der relevanten Funktionen:
Name
Verwendung
shmget()
shmat()
shmdt()
shmctl()
Ein Shared-Memory-Segment anfordern
Ein Shared-Memory-Segment an einen Prozess anbinden
Ein Shared-Memory-Segment von einem Prozess lösen
Steuerung von Shared-Memory-Segmenten
Als erster Schritt muss die Reservierung eines Shared-Memory-Segments mit der
Funktion shmget() erfolgen:
int shmget(key_t key, int size, int shmflg);
Als key“ sollte hier das Schlüsselwort IPC PRIVATE angegeben werden, um
”
shmget() zu veranlassen, neuen Speicher zu reservieren. Der Parameter size
gibt die Größe des zu reservierenden Segments an, wobei shmget() immer
auf die nächsten 4 kByte (PAGE SIZE) aufrundet. Da wir in das Segment schreiben und aus dem Segment lesen wollen, muss als shmflg der Ausdruck
SHM_R | SHM_W“ angegeben werden. Die Funktion shmget() liefert dann ent”
weder eine ID-Nummer für das reservierte Shared-Memory-Segment oder eine
–1, falls das Segment nicht reserviert werden konnte. Achtung: Tatsächlich erfolgt
bei dem Aufruf von shmget() nur das Anlegen der erforderlichen Datenstrukturen, der Speicher selbst wird erst reserviert, wenn das Segment mit shmat() an
einen Prozess angebunden wird!
Im zweiten Schritt sollte das neu eingerichtete Shared-Memory-Segment, identifiziert über seine ID-Nummer, mit Hilfe der Funktion shmat() an mindestens
einen Prozess angebunden werden:
void *shmat(int shmid, void *shmaddr, int shmflg);
Der Parameter shmid stellt die von shmget() gelieferte ID-Nummer dar. Über
shmaddr kann hier eine feste Adresse vorgegeben werden. In der Regel sollte
dieser Parameter jedoch 0 sein, wodurch shmat() veranlasst wird, den nächsten
freien Speicherblock zu reservieren. Als shmflg kann man entweder eine 0 für
98
5 Interprozesskommunikation
Schreib- und Lesezugriff oder die Konstante SHM_RDONLY angeben, falls diesem
Prozess nur das Lesen aus dem Shared-Memory-Segment gestattet werden soll.
Als Rückgabewert erhält man die Adresse des reservierten Speichers oder –1 im
Fehlerfall.
Ein typischer Programmausschnitt zur Anforderung von Shared-Memory sieht
somit wie folgt aus:
int shmem_id;
void *shmem_addr;
if ((shmem_id = shmget(IPC_PRIVATE, PAGE_SIZE,
SHM_R | SHM_W)) == -1)
{
perror("shmget() failed");
exit(1);
}
if ((shmem_addr = shmat(shmem_id, 0, 0)) == (void *)-1)
{
perror("shmat() failed");
shmctl(shmem_id, IPC_RMID, 0);
exit(1);
}
Danach steht dem Prozess ab der Adresse shmem addr ein Speicherblock von
der Größe PAGE SIZE (4 kByte) zur Verfügung, den auch andere Prozesse nutzen
können. Wird nun mit fork() ein Kind-Prozess erzeugt, erbt dieser automatisch
auch das angebundene Shared-Memory-Segment, sodass der Kind-Prozess ohne
weitere Maßnahmen in diesen Speicher schreiben kann!
Vielleicht haben Sie in dem oben aufgeführten Programmausschnitt den Funktionsaufruf shmctl() bemerkt. Diese Funktion erlaubt es, mit dem SharedMemory-Segment verschiedene Aktionen auszuführen – eine davon ist IPC RMID,
mit der das Segment entfernt wird. Damit wird verhindert, dass die dem Segment
zugeordneten Datenstrukturen als Speicherleichen“ zurückbleiben.
”
Nach erfolgreichem shmget()-Aufruf sollte am Ende des Programms das auf
diese Weise angelegte Segment mit shmctl() und IPC RMID unbedingt wieder
entfernt werden.
Hinweis: Durch IPC RMID wird das Segment als zu löschen“ markiert. Das
”
tatsächliche Löschen des Shared-Memory-Segments und die Freigabe des Speichers erfolgen erst, wenn alle Prozesse, die das Segment anbanden, dieses wieder
gelöst haben!
5.4 Datenaustausch zwischen Prozessen
99
Das Lösen eines Shared-Memory-Segments geschieht automatisch bei Beenden
des Prozesses oder vorher durch Aufruf der Funktion:
int shmdt(void *shmaddr);
Als Parameter wird die Adresse des Speicherblocks benötigt, wie sie von der
Funktion shmat() geliefert wurde.
Das folgende Beispiel-Programm richtet ein Shared-Memory-Segment ein und beschreibt dieses vom Kind-Prozess aus. Der Eltern-Prozess liest nach Beendigung
des Kind-Prozesses den Inhalt dieses Segments und gibt ihn aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
sharedmem.c
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<sys/shm.h>
<sys/ipc.h>
<sys/wait.h>
int main()
{
int shmem_id;
char *buffer;
if ((shmem_id = shmget(IPC_PRIVATE, 80, SHM_R | SHM_W))
== -1)
{
perror("sharedmem: shmget() failed");
return(1);
}
if ((buffer = shmat(shmem_id, 0, 0)) == (char *)-1)
{
perror("sharedmem: shmat() failed");
shmctl(shmem_id, IPC_RMID, 0);
return(1);
}
buffer[0] = ’\0’;
/* Puffer initialisieren */
if (fork() == 0)
{
/* Kind-Prozess */
sprintf(buffer, "Message from child process");
shmdt(buffer);
100
35
36
37
38
39
40
41
42
43
44
45
5 Interprozesskommunikation
return(0);
}
wait(NULL);
/* Eltern-Prozess */
printf("Child process wrote into buffer: ’%s’\n",
buffer);
shmdt(buffer);
shmctl(shmem_id, IPC_RMID, 0);
return(0);
}
Die shmdt()-Aufrufe in den Zeilen 33 und 41 sind im Grunde obsolet. Wie bereits ausgeführt, werden alle an einen Prozess angebundenen Shared-MemorySegmente beim Beenden des Prozesses automatisch gelöst.
5.5 Alternative Verfahren zur Erzeugung von
Prozessen
Wie die Beispiele zur Interprozesskommunikation gezeigt haben, erfordert der
Datenaustauch zwischen Eltern-Prozess und dem mit fork() erzeugten KindProzess einen gewissen Aufwand. Unter Linux gibt es jedoch neben den bisher
beschriebenen Funktionen fork() und system() weitere Möglichkeiten, einen
neuen Prozess zu erzeugen. Diese alternativen Verfahren vereinfachen dabei die
Kommunikation zwischen Eltern- und Kind-Prozess.
5.5.1 popen() und pclose()
Ganz ähnlich wie die in Abschnitt 5.2.1 vorgestellte Funktion system() führt
auch die Funktion popen() das als Zeichenkette übergebene Kommando in einer Shell aus:
FILE *popen(const char *command, const char *type);
Gleichzeitig öffnet diese Funktion aber auch eine Pipe und gibt diese als Zeiger auf einen Stream (Achtung: gepufferte Ein-/Ausgabe!) zurück. Mit dem Parameter type lässt sich einstellen, ob der Ausgabe- oder der Eingabekanal der
Shell in die Pipe umgelenkt wird. Dementsprechend kann aus dem Stream gelesen (type= r“) oder in den Stream geschrieben (type= w“) werden. Anders als
”
”
bei der Funktion system() wartet popen() nicht auf die Beendigung des KindProzesses, sondern kehrt unmittelbar zurück, sodass Eltern- und Kind-Prozess
parallel laufen.
Die Funktion pclose() wartet auf die Beendigung des Kind-Prozesses und liefert
dessen Rückgabewert (Exit-Status):
5.5 Alternative Verfahren zur Erzeugung von Prozessen
101
int pclose(FILE *stream);
Das folgende Beispielprogramm zeigt die Verwendung der Funktionen popen()
und pclose(). Das Programm gibt alle im aktuellen Verzeichnis enthaltenen Unterverzeichnisse aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
popen.c - Unterverzeichnisse anzeigen
*/
# include <stdio.h>
# include <string.h>
int main()
{
int status;
FILE *stream;
char buffer[40];
if ((stream = popen("ls -F", "r")) == NULL)
{
perror("popen: popen() failed");
return(1);
}
while (fgets(buffer, 40, stream) != NULL)
if (buffer[strlen(buffer)-2] == ’/’)
printf("%s", buffer);
status = pclose(stream);
printf("(ls returned %d.)\n", status);
return(0);
}
5.5.2 Die fork()-Alternative clone()
Unter Linux – und nur dort – gibt es alternativ zu fork() die Funktion clone().
Diese Funktion arbeitet ähnlich wie fork(), erlaubt jedoch, dass sich Eltern- und
Kind-Prozess den Speicher teilen. Dadurch können beide Prozesse beispielsweise
über globale Variablen Informationen austauschen. Der Aufruf von clone() ist
leider etwas komplizierter als der von fork():
int clone(int *function, void *child_stack, int flags,
void *arg);
102
5 Interprozesskommunikation
Als ersten Parameter erwartet clone() einen Zeiger auf die im parallelen Prozess auszuführende Funktion. Diese Funktion muss vom Typ int sein und kann
maximal einen Parameter besitzen. Der zweite Parameter von clone() gibt die
Adresse des Stacks für den Kind-Prozess an; weil der Stack bei PCs von oben nach
unten gefüllt wird, muss dieser Parameter auf das obere Ende eines hinreichend
großen Speicherblocks zeigen. Der mit flags bezeichnete Parameter erlaubt es,
Optionen wie CLONE VM (Eltern- und Kind-Prozess teilen sich den virtuellen Speicher) anzugeben. Außerdem enthält flags die Signale, die bei Beendigung des
Kind-Prozesses gesendet werden sollen. Als letzter Parameter kann ein beliebiger Zeiger angegeben werden, der an die im Kind-Prozess auszuführende Funktion übergeben wird. Als Rückgabewert liefert clone() die Prozess-ID des KindProzesses bzw. –1 im Fehlerfall.
Um die Anwendung der Funktion clone() zu verdeutlichen, sei das folgende
Programm betrachtet, das einen neuen Prozess erzeugt, der eine globale Variable
verändert und so eine Information an den Eltern-Prozess überträgt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
clone.c - Kind-Prozess mit clone() erzeugen
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<string.h>
<sched.h>
<signal.h>
<sys/wait.h>
static char buffer[80] = "Can you hear me?";
static char stack[10000];
int child_function(void *text)
{
printf("child is running.\n");
strcat(buffer, text);
return(0);
}
int main()
{
int status;
printf("buffer=’%s’\n", buffer);
printf("running child...\n");
5.5 Alternative Verfahren zur Erzeugung von Prozessen
29
30
31
32
33
34
35
36
37
38
39
40
41
103
if (clone(&child_function, &(stack[10000]),
CLONE_VM | SIGCHLD, " Yes!") == -1)
{
perror("clone: clone() failed");
return(1);
}
wait(&status);
printf("child returned %d.\n", status);
printf("buffer=’%s’\n", buffer);
return(0);
}
Wenn Sie diesen Quelltext übersetzen und das Programm anschließend starten,
sollte die folgende Ausgabe erscheinen:
> clone
buffer=’Can you hear me?’
running child...
child is running.
child returned 0.
buffer=’Can you hear me? Yes!’
In Zeile 11 des Programms wird die globale Variable buffer definiert und mit
einer Zeichenkette initialisiert. In Zeile 13 ist der Speicher für den Stack des KindProzesses mit 10.000 Bytes angelegt. Die im Kind-Prozess ausgeführte Funktion
ist in den Zeilen 15 bis 20 definiert. Sie erweitert den Inhalt der Variablen buffer
um die als Parameter übergebene Zeichenkette text. Im Hauptprogramm wird
der Inhalt von buffer einmal vor dem Starten des Kind-Prozesses und einmal
nach dessen Beendigung ausgegeben.
Wie bereits erwähnt, ist clone() eine Linux-spezifische Funktion. Wenn Sie ein
Programm portabel, d. h. auf andere Unix-Systeme übertragbar halten wollen,
sollten Sie auf die Verwendung von clone() verzichten.
5.5.3 POSIX-Threads
Eine weitere Möglichkeit, Multi-Thread-Programme zu erstellen, besteht in der
Verwendung der Funktionsbibliothek libpthread“. Diese Bibliothek stellt nicht
”
nur Funktionen für die Erzeugung von Prozessen, sondern auch für eine komfortable Interprozesskommunikation bereit.
Die Abkürzung pthread“ steht für POSIX Threads“; aber was ist POSIX“ ei”
”
”
gentlich? Es handelt sich dabei ebenfalls um eine Abkürzung, und zwar für Port”
104
5 Interprozesskommunikation
able Operating System Interface“ 1. Dieser IEEE-Standard ist der Versuch eines
Konsortiums von Herstellern, ein einheitliches Unix zu entwickeln. Dabei beschreibt POSIX nicht nur C-Funktionen für Dateizugriff, Prozesse und Ein-/Ausgabe, sondern auch Shell-Befehle und die Arbeitsweise der Shell selbst.
Es würde den Rahmen dieses Buches sprengen, den POSIX-Standard für die Interprozesskommunikation, so wie er in der libpthread implementiert ist, vollständig
zu beschreiben. Hier soll lediglich der Einstieg in die Programmierung mit POSIXThreads gegeben werden. Ein (englisches) Tutorial zu den Funktionen der libpthread finden Sie unter [7].
Neue Prozesse werden mit der Funktion pthread create() erzeugt:
int pthread_create(pthread_t *thread,
pthread_attr_t *attr,
void *function, void *arg);
Als ersten Parameter erwartet diese Funktion den Zeiger auf eine Variable vom
Typ pthread t. In dieser Struktur werden Informationen über den neu erzeugten Prozess abgelegt. Der zweite Parameter zeigt auf eine Variable vom Typ
pthread attr t, in der die gewünschten Eigenschaften des neu zu erzeugenden
Prozesses eingetragen werden können. Wird statt eines Zeigers der Wert NULL
übergeben, verwendet pthread create() die Standard-Eigenschaften für neue
Threads.
Ähnlich wie bei clone() wird auch bei pthread create() ein Zeiger auf die
vom Kind-Prozess auszuführende Funktion übergeben – hier als dritter Parameter
– sowie ein Zeiger auf eine Variable, die dieser Funktion als Parameter übergeben
werden soll.
Die im Kind-Prozess auszuführende Funktion sollte mit pthread exit() beendet werden:
void pthread_exit(void *return_val);
Als Parameter wird bei pthread exit() ein Zeiger auf den Rückgabewert übergeben. Dieser Rückgabewert könnte beispielsweise ein Fehler-Code oder ExitStatus des Kind-Prozesses sein. Alternativ zu pthread exit() kann man den
Kind-Prozess auch mit return() beenden. In diesem Fall muss ebenfalls ein Zeiger auf den Rückgabewert als Parameter von return() verwendet werden.
Die dritte Funktion aus der libpthread, die hier betrachtet werden soll, ersetzt
quasi die Funktion waitpid():
int pthread_join(pthread_t thread, void **return_val);
1
Das X“ in der Abkürzung POSIX sollte wahrscheinlich die Nähe zu Unix verdeutlichen.
”
5.5 Alternative Verfahren zur Erzeugung von Prozessen
105
Diese Funktion wartet, bis der zum Parameter thread gehörende Prozess beendet wurde, und trägt dann in *return val den Zeiger auf den Rückgabewert des
Prozesses ein. Das folgende Programm verdeutlicht die Verwendung dieser drei
Funktionen aus der libpthread. Es handelt sich dabei um das clone()-Beispiel
aus dem vorigen Abschnitt, das für die Verwendung von POSIX-Threads umgeschrieben wurde (und damit nicht nur unter Linux, sondern auch auf anderen
POSIX-kompatiblen Systemen laufen sollte):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
pthread.c - Kind-Prozess mit der libpthread
*/
# include <stdio.h>
# include <string.h>
# include <pthread.h>
static char buffer[80] = "Can you hear me?";
void *child_function(void *text)
{
static int status;
printf("child is running.\n");
strcat(buffer, text);
status = 0;
pthread_exit(&status);
}
int main()
{
int *status_ptr;
pthread_t child_thread;
printf("buffer=’%s’\n", buffer);
printf("running child...\n");
if (pthread_create(&child_thread, NULL,
&child_function, " Yes!"))
{
fprintf(stderr,
"pthread: pthread_create() failed.\n");
return(1);
}
if (pthread_join(child_thread, (void *)&status_ptr))
106
38
39
40
41
42
43
44
45
5 Interprozesskommunikation
fprintf(stderr, "pthread: pthread_join() failed.\n");
else
printf("child returned %d.\n", *status_ptr);
printf("buffer=’%s’\n", buffer);
return(0);
}
Beim Kompilieren dieses Programms muss die libpthread mit eingebunden werden:
gcc pthread.c -lpthread -o pthread
Wenn Sie das Programm starten, sollte die gleiche Ausgabe wie bei dem clone()Beispiel erscheinen:
> pthread
buffer=’Can you hear me?’
running child...
child is running.
child returned 0.
buffer=’Can you hear me? Yes!’
Kapitel 6
Devices – das Tor zur Hardware
Eine der großen Stärken von Linux (und anderen Unix-Varianten) sind die Devices. Bei welchem anderen Betriebssystem können Sie mit einer Kommandozeile
eine MIDI1 -Sequenz aufzeichnen2 oder etwa den Master-Boot-Record (MBR) einer Festplatte auslesen?3 Devices bilden die Schnittstelle zwischen der Hardware (z. B. dem CD-ROM-Laufwerk) und dem Applikationsprogramm (z. B. einem
Brennprogramm wie cdrecord“).
”
Dieses Kapitel soll einen Einblick in den Umgang mit Devices aus Sicht des
Programmierers vermitteln. Es werden exemplarisch das CD-ROM-Laufwerk,
Soundkarte, Video-Device (z. B. eine WebCam) und die serielle Schnittstelle
RS 232 angesprochen. Auch die direkte Ansteuerung von USB-Geräten mit Hilfe der libusb wird gezeigt.
6.1 Das Device-Konzept von Linux
Ein Großteil der Hardware-Komponenten wie Festplatten, CD-ROM-Laufwerk,
Soundkarte, serielle und parallele Schnittstellen sind unter Linux als Devices –
im Deutschen häufig als Geräte“ bezeichnet – verfügbar. Diese besitzen einen
”
Eintrag im Dateisystem, für gewöhnlich im Verzeichnis /dev“. Die erste IDE”
Festplatte ist beispielsweise mit /dev/hda1“ verknüpft. Die mit den Devices
”
verknüpften Einträge im Dateisystem – auch als Inodes bezeichnet – sind vom
Typ character special file“ oder block special file“ (siehe auch Abschnitt 4.2). Bei
”
”
ls -l“ wird dieser Typ entsprechend durch ein c“ oder b“ am Zeilenanfang
”
”
”
gekennzeichnet.
1
Music Instrument Digital Interface – Schnittstelle für Keyboards und andere elektronische Musikinstrumente
2
cat /dev/sequencer > midi-file“ (Abbruch der Aufzeichnung mit Ctrl-C)
”
Für (E)IDE-Festplatten: dd if=/dev/hda of=mbr bs=512 count=1“
”
3
108
6 Devices – das Tor zur Hardware
Über zwei Gerätenummern, der major“ und der minor device number“, sind diese
”
”
Inodes mit dem entsprechenden Treiber verknüpft; die Nummern können ebenfalls mit ls -l“ angezeigt werden:
”
brw-rw----
1 root
disk
⌊ block special file
3,
1 Nov
|
⌊ minor device number
8
1999 /dev/hda1
⌊ major device number
Für Gerätetreiber, die als Kernel-Modul ausgeführt sind, ist die Verknüpfung zwischen der major device number und dem Modul in einer der Dateien
/etc/modules.conf“ oder /etc/modprobe.conf“ registriert. Beispiel:
”
”
alias block-major-2
floppy
Hier werden alle block special files, die eine major device number von 2 haben, mit
dem Kernel-Modul floppy.o“ verknüpft.
”
Aufgrund der wachsenden Zahl von Hot-Plug-and-Play“-Geräten1 wie USB”
Festplatten und USB-Kameras musste dieses starre“ Device-Konzept von Linux
”
in den letzten Jahren um ein dynamisches Gerätemanagement erweitert werden. Dies übernimmt udev, genauer gesagt der Dämon udevd. Siehe hierzu auch
man udev“. Doch für die in diesem Kapitel vorgestellten Methoden und Beispiel”
programme spielt es keine Rolle, ob das jeweilige Gerät fest im System verankert
ist (wie ein internes CD-ROM-Laufwerk) oder dynamisch eingebunden wird.
6.1.1 Devices öffnen und schließen
Der Vorteil, der sich durch die Integration der Devices in das Dateisystem ergibt, liegt auf der Hand: Man kann mit gängigen Dateioperationen auf HardwareKomponenten zugreifen. So ist es z. B. möglich, in einer Shell mit dem Kommando
cat /dev/audio > test.au“ ohne ein spezielles Programm eine Audio-Datei
”
aufzuzeichnen – vorausgesetzt, man hat eine Soundkarte installiert und unter
Linux korrekt eingerichtet. In gleicher Weise lässt sich diese Datei wieder abspielen: cat test.au > /dev/audio“. Manche Gerätetreiber stellen ein zusätzli”
ches Device zur Verfügung, das Informationen über die Hardware und den Treiber liefert, Beispiel: cat /dev/sndstat“.
”
Für den Programmierer bedeutet dies, dass er mit Hilfe der üblichen Funktionen
für den Dateizugriff auch die Devices ansprechen kann. Dies gilt natürlich auch
für das Öffnen und Schließen eines Devices, das mit den Befehlen fopen() bzw.
fclose() für gepufferte Ein-/Ausgabe möglich ist (vgl. Kapitel 4). Häufiger verwendet man für den Zugriff auf Devices jedoch die entsprechenden Befehle für
ungepufferte Ein-/Ausgaben:
int open(char *pathname, int flags);
int close(int fd);
1
Geräte, die bei laufendem Betrieb angeschlossen werden können.
6.1 Das Device-Konzept von Linux
109
Die Funktion open() benötigt als ersten Parameter den zu öffnenden Datei- bzw.
Device-Namen. Als zweiter Parameter kann eine der Konstanten O RDONLY (nur
lesen), O WRONLY (nur schreiben), O RDWR (lesen und schreiben) angegeben werden. Als Rückgabewert liefert open() entweder den neuen Dateideskriptor, also
eine positive, ganze Zahl, oder –1, falls die angegebene Datei nicht geöffnet werden konnte. Die Funktion close() schließt die Datei, deren Deskriptor als Parameter angegeben ist.
Während sich Dateizugriffe durch die Verwendung von Schreib- und Lesepuffern
deutlich beschleunigen lassen, bringt diese Pufferung bei Devices im Allgemeinen keine Vorteile. Ein Lesepuffer kann bei Devices sogar eher hinderlich sein –
insbesondere bei Geräten wie Soundkarte oder WebCam, bei denen die Daten nur
mit einer bestimmten Datenrate eintreffen“, würde das Füllen des Puffers mit ei”
ner Wartezeit verbunden sein. Solche Devices werden in der Regel durch character
special files repräsentiert – im Gegensatz zu den block special files, wie etwa für das
Device einer Festplatte.
6.1.2 Ungepuffertes Lesen und Schreiben
Im Gegensatz zur gepufferten Ein-/Ausgabe, für die es zahlreiche Schreib/Lesefunktionen unterschiedlicher Komplexität gibt, stehen für die ungepufferte
Ein- und Ausgabe nur je zwei Funktionen zur Verfügung:
ssize_t read(int fd, void *buffer, size_t count);
ssize_t pread(int fd, void *buffer, size_t count,
off_t offset);
ssize_t write(int fd, void *buffer, size_t count);
ssize_t pwrite(int fd, void *buffer, size_t count,
off_t offset);
Die Funktionen read() und pread() lesen aus der Datei mit dem Deskriptor fd
die Anzahl count Bytes und schreiben sie in den Speicher an die mit buffer angegebene Stelle. Dabei liest pread() nicht ab der aktuellen Position in der Datei,
sondern ab der durch offset angegebenen Position. Als Rückgabewert liefern
beide Funktionen die Anzahl der tatsächlich gelesenen Bytes bzw. –1 im Fehlerfall.
Analog werden die Funktionen write() und pwrite() verwendet, um count
Bytes aus dem Speicher ab Adresse buffer in die Datei mit dem Deskriptor fd
zu schreiben. Zuvor setzt pwrite() die aktuelle Position in der Datei auf offset.
Beide Funktionen geben die Anzahl der tatsächlich geschriebenen Bytes zurück
oder –1, falls ein Fehler aufgetreten ist.
110
6 Devices – das Tor zur Hardware
Insbesondere beim Lesen aus einem character special file mit read() oder
pread() ist die Anzahl der tatsächlich gelesenen Zeichen häufig geringer als
die mit count angegebene Zahl. Dies deutet nicht auf einen Fehler hin, sondern
liegt lediglich daran, dass im Moment noch keine weiteren Zeichen verfügbar
sind.
Im Zusammenhang mit dem Lesen aus einem Device ist auch die Funktion
select() interessant:
int select(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
Mit dieser Funktion kann bis zu einer vorgegebenen, maximalen Zeit gewartet
werden, bis mindestens ein Dateideskriptor bereit zum Lesen oder zum Schreiben
ist. Ein Beispiel für die Verwendung dieser Funktion finden Sie in Abschnitt 6.5.
6.1.3 Devices steuern mit ioctl()
Wenn auch die Einbindung der Devices in das Dateisystem den Lese- und Schreibzugriff auf die verschiedenen Hardware-Komponenten deutlich vereinfacht, kann
über die Schreib-/Lesefunktionen dennoch nur ein kleiner Teil der Möglichkeiten
der Devices genutzt werden. So kann man z. B. zwar eine Audiodatei von der
Soundkarte aufzeichnen, aber zum Einstellen der Sampling-Rate ist ein erweiterter Zugriff auf das Device erforderlich. Dies ist Aufgabe der Funktion ioctl()
(die Abkürzung steht für Input-Output-Control):
int ioctl(int fd, int request, ...);
Auch diese Funktion benötigt als ersten Parameter den Dateideskriptor fd des
Devices. Der Parameter request gibt das auf dieses Device anzuwendende
Kommando an, optional gefolgt von weiteren Parametern, die das Kommando
benötigt. Die möglichen Kommandos hängen von dem jeweiligen Device ab, eine
(unvollständige) Liste der Kommandos erhalten Sie mit
man ioctl list
Im Fehlerfall liefert die Funktion ioctl() eine –1, sonst ist der Rückgabewert 0.1
Hinweis:
Wenn Sie die Funktion ioctl() auf ein mit fopen() geöffnetes Device anwenden
wollen, können Sie den Dateideskriptor mit der Funktion fileno() erfragen:
1
Einige Kommandos nutzen den Rückgabewert und liefern deshalb auch andere Werte als 0 oder –1.
6.2 Das CD-ROM-Laufwerk
111
FILE *stream;
ioctl(fileno(stream), ...);
6.2 Das CD-ROM-Laufwerk
Das CD-ROM-Laufwerk lässt sich über das Device /dev/cdrom“ ansprechen.
”
Dabei handelt es sich um einen symbolischen Link auf das tatsächliche Device, z.B. /dev/hdc“. Bevor Sie auf das CD-ROM-Laufwerk zugreifen können,
”
müssen Sie sicherstellen, dass die Zugriffsrechte entsprechend gesetzt sind! Dazu können Sie das Device (nicht den Link!) entweder für alle Benutzer freigeben
(z.B. chmod a+r /dev/hdc), oder – was auf Systemen mit mehreren Benutzern
sinnvoller ist – Sie tragen Ihren Benutzernamen in der Datei /etc/group“ in
”
die Gruppe disk“ ein. Wenn Sie sich als Benutzer root“ anmelden, haben Sie
”
”
natürlich auch Zugriff auf das CD-ROM-Laufwerk, doch rate ich davon dringend
ab.
Die für das CD-ROM-Laufwerk zulässigen ioctl()-Kommandos sind in der
Include-Datei /usr/include/linux/cdrom.h“ aufgelistet.
”
6.2.1 Die CD auswerfen“
”
Als erstes Anwendungsbeispiel für den Umgang mit dem CD-ROM-Laufwerk
soll hier das Programm eject“ vorgestellt werden, das die eingelegte CD aus”
”
wirft“:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
eject.c - CDROM-Laufwerk oeffnen
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<sys/ioctl.h>
<fcntl.h>
<linux/cdrom.h>
int main()
{
int fd;
if ((fd = open("/dev/cdrom", O_RDONLY | O_NONBLOCK))
== -1)
{
perror("eject: Can’t open /dev/cdrom");
return(1);
112
20
21
22
23
24
25
26
27
28
29
30
6 Devices – das Tor zur Hardware
}
if (ioctl(fd, CDROMEJECT) == -1)
{
perror("eject: ioctl() failed");
return(1);
}
close(fd);
return(0);
}
Betrachten wir zunächst die Zeile 15, in der das Device geöffnet wird. Als Flags“
”
ist hier die Oder-Verknüpfung aus O RDONLY und O NONBLOCK angegeben. Letzteres bewirkt, dass die Funktion open() auch ausgeführt werden kann, wenn keine
CD im Laufwerk liegt.
Nach erfolgreichem Öffnen des Devices wird in Zeile 22 mit Hilfe der Funktion
ioctl() das Kommando CDROMEJECT an das Device geschickt. Dieses Kommando benötigt keinen Parameter. Weitere Kommandos dieser Art sind CDROMSTART,
CDROMSTOP, CDROMRESET und CDROMCLOSETRAY.
Sollte das Beispielprogramm eject“ bei Ihnen nicht funktionieren, sondern
”
einen Input/output error“ ausgeben, liegt das vermutlich daran, dass Ihnen
”
der Hot-Plug-Dämon udevd (siehe oben) in die Quere kommt. Auf manchen
Linux-Systemen wird eine CD nach dem Einlegen automatisch durch diesen
Dämon in das Dateisystem eingebunden und das Auswerfen der CD gesperrt.
Testen Sie das Programm daher zunächst ohne CD im Laufwerk.
6.2.2 Fähigkeiten des Laufwerks auslesen
Nicht alle CD-ROM-Laufwerke bieten die Möglichkeit, das Laufwerk selbsttätig
wieder zu schließen – z. B. bei Notebooks muss das Laufwerk meist von Hand“
”
wieder geschlossen werden. Auch andere Kommandos lassen sich nicht auf jeden
Laufwerkstyp anwenden. Daher gibt es das Kommando CDROM GET CAPABILITY,
mit dem Sie die Fähigkeiten des Devices abfragen können. Das folgende Programm cdromcap“ zeigt die Anwendung dieses Kommandos:
”
1
/*
2
cdromcap.c - Faehigkeiten des CDROM-Laufwerks
/
3
*
4
5
# include <stdio.h>
6
# include <unistd.h>
7
# include <sys/ioctl.h>
8
# include <fcntl.h>
6.2 Das CD-ROM-Laufwerk
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
113
# include <linux/cdrom.h>
int main()
{
int fd, caps;
if ((fd = open("/dev/cdrom", O_RDONLY | O_NONBLOCK))
== -1)
{
perror("cdromcap: Can’t open /dev/cdrom");
return(1);
}
if ((caps = ioctl(fd, CDROM_GET_CAPABILITY)) == -1)
{
perror("cdromcap: ioctl() failed");
return(1);
}
printf("Drive is a CD-R: %s, CD-RW: %s, DVD: %s, "
"DVD-R: %s.\n",
(caps & CDC_CD_R)? "yes" : "no",
(caps & CDC_CD_RW)? "yes" : "no",
(caps & CDC_DVD)? "yes" : "no",
(caps & CDC_DVD_R)? "yes" : "no");
printf("It can
"select
(caps &
(caps &
(caps &
close tray: %s, lock: %s, "
disc: %s.\n",
CDC_CLOSE_TRAY)? "no" : "yes",
CDC_LOCK)? "yes" : "no",
CDC_SELECT_DISC)? "yes" : "no");
close(fd);
return(0);
}
Der Rückgabewert der Funktion ioctl() in Zeile 22 enthält bei erfolgreicher
Ausführung des Kommandos CDROM GET CAPABILITY die Fähigkeiten des Devices, wobei jede Fähigkeit durch ein bestimmtes Bit repräsentiert wird. Eine Liste der möglichen Features erhält man mit (Benutzereingaben sind schräg dargestellt):
> grep CDC
#define CDC
#define CDC
#define CDC
/usr/include/linux/cdrom.h
CLOSE TRAY
0x1 /* caddy systems can’t close */
OPEN TRAY
0x2 /* but can eject. */
LOCK
0x4 /* disable manual eject */
114
6 Devices – das Tor zur Hardware
#define
#define
#define
#define
#define
#define
#define
#define
#define
CDC
CDC
CDC
CDC
CDC
CDC
CDC
CDC
CDC
SELECT SPEED
SELECT DISC
MULTI SESSION
MCN
MEDIA CHANGED
PLAY AUDIO
RESET
IOCTLS
DRIVE STATUS
0x8
0x10
0x20
0x40
0x80
0x100
0x200
0x400
0x800
/*
/*
/*
/*
/*
/*
/*
/*
/*
#define CDC GENERIC PACKET 0x1000 /*
#define
#define
#define
#define
#define
CDC
CDC
CDC
CDC
CDC
CD R
CD RW
DVD
DVD R
DVD RAM
0x2000
0x4000
0x8000
0x10000
0x20000
/*
/*
/*
/*
/*
programmable speed */
select disc from juke-box */
read sessions>1 */
Medium Catalog Number */
media changed */
audio functions */
hard reset device */
driver has non-std ioctls */
driver implements drive
status */
driver implements generic
packets */
drive is a CD-R */
drive is a CD-RW */
drive is a DVD */
drive can write DVD-R */
drive can write DVD-RAM */
Achtung: Die Bedeutung von CDC CLOSE TRAY ist sozusagen invertiert“. Ist die”
ses Bit gesetzt, lässt sich das Laufwerk nicht automatisch schließen.
6.2.3 Audio-CDs abspielen
Eine Anwendung des CD-ROM-Laufwerks, die den direkten Device-Zugriff erfordert, ist das Abspielen von Audio-CDs. Dazu benötigen Sie entweder eine
Soundkarte, die mit dem CD-Laufwerk verbunden ist, oder ein Laufwerk mit einem Kopfhörerausgang. Bei Verwendung der Soundkarte sollten Sie zudem ein
Programm zum Einstellen der Audioquellen und der Lautstärke installiert haben,
beispielsweise aumix“.
”
Das Inhaltsverzeichnis einer Audio-CD
Bevor wir jedoch zum Abspielen einer CD kommen, soll zunächst gezeigt werden,
wie das Inhaltsverzeichnis“ einer Audio-CD gelesen wird und in welcher Weise
”
die Positionen der einzelnen Stücke (Tracks) codiert sind. Dazu sei das folgende
Programm betrachtet:
1
2
3
4
5
6
7
8
9
10
/*
cdtoc.c - "Inhaltsverzeichnis" einer Audio-CD
*/
#
#
#
#
#
#
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<errno.h>
<sys/ioctl.h>
<fcntl.h>
<linux/cdrom.h>
6.2 Das CD-ROM-Laufwerk
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
115
int main()
{
int fd, i;
struct cdrom_tochdr toc_hdr;
struct cdrom_tocentry toc_entry;
if ((fd = open("/dev/cdrom", O_RDONLY)) == -1)
{
if (errno == ENOMEDIUM)
fprintf(stderr, "cdtoc: No CD in drive.\n");
else
perror("cdtoc: Can’t open /dev/cdrom");
return(1);
}
if (ioctl(fd, CDROMREADTOCHDR, &toc_hdr) == -1)
{
perror("cdtoc: Can’t get header");
return(1);
}
printf("First track: %d, last track: %d\n",
toc_hdr.cdth_trk0, toc_hdr.cdth_trk1);
for (i=toc_hdr.cdth_trk0; i<=toc_hdr.cdth_trk1; i++)
{
toc_entry.cdte_track = i;
toc_entry.cdte_format = CDROM_MSF;
if (ioctl(fd, CDROMREADTOCENTRY, &toc_entry) == -1)
{
perror("cdtoc: Can’t get table of contents");
return(1);
}
printf(" %2d) %02d:%02d.%02d\n", i,
toc_entry.cdte_addr.msf.minute,
toc_entry.cdte_addr.msf.second,
(toc_entry.cdte_addr.msf.frame*100+37)/75);
}
close(fd);
return(0);
}
In den Zeilen 18 bis 25 wird das Device geöffnet, wobei hier im Gegensatz zu
den beiden vorangegangenen Abschnitten bewusst auf die Option O NONBLOCK
116
6 Devices – das Tor zur Hardware
verzichtet wurde. Dadurch führt der Aufruf der Funktion open() zum Fehler
ENOMEDIUM, wenn keine CD eingelegt ist.
Als Nächstes wird in den Zeilen 27 bis 33 der Kopf ( HDR“ für Header) des Inhalts”
verzeichnisses ( TOC“ für Table of Contents) gelesen. Dies wird durch das ioctl()”
Kommando CDROMREADTOCHDR erreicht, das als dritten Parameter des ioctl()Aufrufs einen Zeiger auf eine Variable vom Typ struct cdrom tochdr erwartet.
Diese Struktur wird in /usr/include/linux/cdrom.h“ definiert und enthält
”
zwei Einträge:
struct cdrom_tochdr
{
__u8 cdth_trk0;
__u8 cdth_trk1;
};
/* start track */
/* end track */
Durch den ioctl()-Aufruf trägt man die Nummer des ersten Stücks (in der Regel ist das 1) und die Nummer des letzten Stücks auf der CD in diese Struktur
ein.
In den Zeilen 35 bis 48 werden dann in der for()-Schleife Informationen zu allen
Stücken eingeholt. Dazu dient das ioctl()-Kommando CDROMREADTOCENTRY,
das als dritten Parameter des ioctl()-Aufrufs einen Zeiger auf eine Variable vom Typ struct cdrom tocentry erwartet. Diese Struktur muss vor dem
ioctl()-Aufruf mit einer gültigen Nummer für das Stück und der Angabe des
Formates für die Position initialisiert werden (Zeile 37 und 38). Als Format stehen CDROM MSF und CDROM LBA zur Verfügung. Ersteres steht für Minute-SecondFrame, Letzteres für Logical Block Address. Während die Adresse im LBA-Format
durch eine Integerzahl repräsentiert wird, bildet die Struktur cdrom msf0 das
MSF-Format ab:
struct
{
__u8
__u8
__u8
};
cdrom_msf0
minute;
second;
frame;
Die Einträge minute und second sind selbsterklärend, der Eintrag frame gibt
den Bruchteil einer Sekunde an, wobei eine Sekunde aus CD FRAMES (= 75) Frames besteht. Der Zusammenhang zwischen LBA- und MSF-Adresse lässt sich beschreiben durch
LBA = (minute · 60 + second − 2) · CD FRAMES + frame
Nach dem ioctl()-Aufruf in Zeile 39 des Programms enthält die Variable
toc entry unter anderem die Position des angegebenen Stücks mit der Num-
6.2 Das CD-ROM-Laufwerk
117
mer i im MSF-Format. Diese wird in der printf()-Anweisung in den Zeilen 44
bis 47 ausgegeben, wobei die Frames in 1 /100 Sekunden umgerechnet werden.
Wiedergabe einer Audio-CD
Bevor Sie eine Audio-CD wiedergeben, stellen Sie bitte sicher, dass das CD-ROMLaufwerk als Audioquelle aktiv ist. Dies ist z. B. mit dem Shell-Programm au”
mix“ möglich:
aumix -c 100
Alternativ bieten manche CD-Laufwerke einen Kopfhörerausgang mit separatem
Lautstärkeregler.
Das Abspielen einer Audio-CD erfolgt mit dem Kommando CDROMPLAYMSF, das
als Parameter einen Zeiger auf eine Struktur vom Typ cdrom msf erwartet:
struct
{
__u8
__u8
__u8
__u8
__u8
__u8
};
cdrom_msf
cdmsf_min0;
cdmsf_sec0;
cdmsf_frame0;
cdmsf_min1;
cdmsf_sec1;
cdmsf_frame1;
/*
/*
/*
/*
/*
/*
start minute */
start second */
start frame */
end minute */
end second */
end frame */
Hier muss im MSF-Format Anfangs- und Endposition des abzuspielenden Teils
der CD angegeben werden. Die Anfangsposition eines Stücks kann wie im vorangegangenen Beispielprogramm mit Hilfe des Kommandos CDROMREADTOCENTRY
erfolgen. Als Ende eines Stücks kann die Anfangsposition des darauf folgenden
Stücks angegeben werden. Die Endposition des letzten Stückes lässt sich über
die Position von CDROM LEADOUT ermitteln. Das folgende Programm spielt eine
Audio-CD vollständig ab:
1
2
3
4
5
6
7
8
9
10
11
/*
cdplay.c - Audio-CD abspielen
*/
#
#
#
#
#
#
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<stdlib.h>
<sys/ioctl.h>
<fcntl.h>
<linux/cdrom.h>
118
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
6 Devices – das Tor zur Hardware
void err_exit(char *err_text, int return_code)
{
perror(err_text);
exit(return_code);
}
int main()
{
int fd;
struct cdrom_tocentry toc_entry;
struct cdrom_msf start_stop;
if ((fd = open("/dev/cdrom", O_RDONLY)) == -1)
err_exit("cdplay: Can’t open /dev/cdrom", 1);
/* Anfang des 1. Stuecks */
toc_entry.cdte_track = 1;
toc_entry.cdte_format = CDROM_MSF;
if (ioctl(fd, CDROMREADTOCENTRY, &toc_entry) == -1)
err_exit("cdplay: ioctl() failed", 1);
start_stop.cdmsf_min0
= toc_entry.cdte_addr.msf.minute;
start_stop.cdmsf_sec0
= toc_entry.cdte_addr.msf.second;
start_stop.cdmsf_frame0
= toc_entry.cdte_addr.msf.frame;
/* Ende des letzten Stuecks */
toc_entry.cdte_track = CDROM_LEADOUT;
toc_entry.cdte_format = CDROM_MSF;
if (ioctl(fd, CDROMREADTOCENTRY, &toc_entry) == -1)
err_exit("cdplay: ioctl() failed", 1);
start_stop.cdmsf_min1
= toc_entry.cdte_addr.msf.minute;
start_stop.cdmsf_sec1
= toc_entry.cdte_addr.msf.second;
start_stop.cdmsf_frame1
= toc_entry.cdte_addr.msf.frame;
if (ioctl(fd, CDROMPLAYMSF, &start_stop) == -1)
err_exit("cdplay: ioctl() failed", 1);
printf("Press <RETURN> to stop playing.\n");
getchar();
6.2 Das CD-ROM-Laufwerk
56
57
58
59
60
61
62
119
if (ioctl(fd, CDROMSTOP) == -1)
err_exit("cdplay: ioctl() failed", 1);
close(fd);
return(0);
}
Nach dem Öffnen des Devices in Zeile 24 fragt das Programm in den Zeilen 28
bis 37 die MSF-Adresse des ersten Stücks ab und trägt sie in die Start-Position der
Variablen start stop ein. Analog wird in den Zeilen 40 bis 49 die Endadresse der
CD erfragt und als Stop-Position in die Variable start stop eingetragen. Danach
erfolgt in Zeile 51 der ioctl()-Aufruf mit dem Kommando CDROMPLAYMSF, der
das Abspielen der CD bewirkt. Mit dem Kommando CDROMSTOP (Zeile 57) lässt
sich das Abspielen vorzeitig beenden.
Weitere Möglichkeiten
Ein echter“ CD-Player bietet neben den Funktionen play“ und stop“ eine pau”
”
”
”
se“-Funktion für eine (kurze) Unterbrechung. Auch das Linux-Device bietet diese
Möglichkeit mit dem ioctl()-Kommando CDROMPAUSE, das keinen weiteren Parameter benötigt. Das Abspielen lässt sich danach mit CDROMRESUME an der gleichen Stelle fortsetzen.
Nachdem es das Abspielen der CD gestartet hat, wartet unser Programm cdplay
auf eine Benutzereingabe. Es bemerkt“ nicht, ob die CD vielleicht schon zu Ende
”
ist. Um den aktuellen Status des CD-Laufwerks abzufragen, dient das ioctl()Kommando CDROMSUBCHNL. Als Parameter wird bei diesem Kommando ein
Zeiger auf eine Variable vom Typ struct cdrom subchnl erwartet. Vor dem
ioctl()-Aufruf muss das Element cdsc format dieser Struktur mit CDROM MSF
oder CDROM LBA initialisiert werden. Nach erfolgreicher Ausführung des Kommandos CDROMSUBCHNL enthält die Struktur unter anderem Informationen über
den Audio-Status, die Nummer des (laufenden) Stücks und die Position im vorgegebenen Format. Das folgende Programm demonstriert das Abfragen und Ausgeben dieser Informationen:
1
2
3
4
5
6
7
8
9
10
/*
cdstat.c - Status des CD-Laufwerks abfragen
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<sys/ioctl.h>
<fcntl.h>
<linux/cdrom.h>
120
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
6 Devices – das Tor zur Hardware
int main()
{
int fd;
char *status_string;
struct cdrom_subchnl subch;
if ((fd = open("/dev/cdrom", O_RDONLY | O_NONBLOCK))
== -1)
{
perror("cdstat: Can’t open /dev/cdrom");
return(1);
}
subch.cdsc_format = CDROM_MSF;
if (ioctl(fd, CDROMSUBCHNL, &subch) == -1)
{
perror("cdstat: ioctl() failed");
return(1);
}
switch(subch.cdsc_audiostatus)
{
case CDROM_AUDIO_PLAY:
status_string = "playing"; break;
case CDROM_AUDIO_PAUSED:
status_string = "paused"; break;
case CDROM_AUDIO_COMPLETED:
status_string = "completed"; break;
case CDROM_AUDIO_ERROR:
status_string = "error"; break;
default: status_string = "---";
}
printf("CD status:\t\t%s\n", status_string);
printf("current track:\t\t%d\n", subch.cdsc_trk);
printf("current position:\t%02d:%02d\n",
subch.cdsc_absaddr.msf.minute,
subch.cdsc_absaddr.msf.second);
close(fd);
return(0);
}
Einige CD-ROM-Laufwerke bieten die Möglichkeit, bei Wiedergabe einer AudioCD die Lautstärke per Software einzustellen. Dazu bietet das Device zwei
ioctl()-Kommandos: CDROMVOLREAD und CDROMVOLCTRL, Ersteres zum Auslesen der Einstellungen und Letzteres zur Modifizierung der Einstellungen. Beide
6.3 Ansteuerung einer Soundkarte
121
Kommandos benötigen als dritten Parameter des ioctl()-Aufrufs den Zeiger
auf eine Variable vom Typ struct cdrom volctrl:
struct
{
__u8
__u8
__u8
__u8
};
cdrom_volctrl
channel0;
channel1;
channel2;
channel3;
/* left channel */
/* right channel */
Nach erfolgreicher Ausführung von CDROMVOLREAD enthält diese Struktur die aktuell eingestellten Werte für alle vier (!) Kanäle – Kanal 2 und Kanal 3 sind in
der Regel nicht belegt und deren Lautstärke ist dementsprechend 0. Mit dem
Kommando CDROMVOLCTRL werden die in der Struktur eingetragenen LautstärkeWerte (zwischen 0 und 255) für die entsprechenden Kanäle eingestellt.
Weitere Informationen zum Ansteuern eines CD-ROM-Laufwerks unter Linux
finden Sie in [8].
6.3 Ansteuerung einer Soundkarte
Eine Soundkarte bietet vielfältige Möglichkeiten: Neben dem Analog-Digital- und
Digital-Analog-Wandler zur Aufnahme und Wiedergabe von Audio-Signalen enthalten die meisten Karten einen Mixer, also eine Art Mischpult, einen Synthesizer
sowie einen Chip (UART) zum Senden und Empfangen von MIDI-Daten. Die verschiedenen Funktionseinheiten einer Soundkarte sind unter Linux auf mehrere
Devices abgebildet:
Device
Beschreibung
/dev/dsp
/dev/audio
/dev/mixer
Schnittstelle zum A/D- und D/A-Wandler
Schnittstelle zum A/D- und D/A-Wandler (8 Bit, log.)
elektronisches Mischpult“
”
Je nach Soundkarte und verwendetem Treiber (Kernel-Modul) stehen weitere
Devices zur Verfügung – beispielsweise /dev/sequencer, /dev/snd/pcm* oder
/dev/sndstat. Die folgenden Abschnitte beschränken sich jedoch auf die ge”
bräuchlichsten“ Devices der Soundkarte.
Bei Verwendung mehrerer Soundkarten, beispielsweise wenn zusätzlich eine TVKarte oder eine WebCam mit eingebautem Mikrofon angeschlossen ist, werden
für jede Karte eigene Device-Dateien angelegt, die mit einem fortlaufenden Index
versehen sind. Beispiel: /dev/dsp0, /dev/dsp1, usw., wobei /dev/dsp in der
Regel ein Link auf /dev/dsp0 ist. Das Gleiche gilt analog für die anderen DeviceDateien wie /dev/mixer.
122
6 Devices – das Tor zur Hardware
6.3.1 OSS, ALSA und ESOUND
Die Devices /dev/dsp und /dev/mixer entsprechen dem ursprünglichen Mechanismus für die Ansteuerung einer Soundkarte unter Linux, dem so genannten
Open Sound System (OSS). Ein Nachteil des OSS besteht darin, dass nicht mehrere Programme parallel auf die Soundkarte zugreifen können. Dies ist beispielsweise erforderlich, wenn während der Wiedergabe von MP3-Musik das Betriebssystem Signaltöne ausgibt. Um diese Einschränkung aufzuheben, wurden inzwischen neue, leistungsfähigere Systeme wie ALSA (Advanced Linux Sound Architecture) und ESounD (Enlightened Sound Daemon) eingeführt. Aus Kompatibilitätsgründen stellen die aktuellen Soundtreiber aber nach wie vor die Devices des OSS
zur Verfügung: die einfachste Möglichkeit für Programmierer, auf die Soundkarte
zuzugreifen.
6.3.2 Der Mixer
Der Mixer einer Soundkarte stellt ein elektronisches Mischpult dar. Er hat die Aufgabe, die verschiedenen Audioquellen wie CD-Laufwerk oder Mikrofon für die
Aufnahme und Wiedergabe zu mischen. Für jeden Kanal des Mixers kann individuell eine Lautstärke eingestellt werden. Über den Mixer wird auch die Aufnahmequelle ausgewählt. Der Mixer lässt sich über das Device /dev/mixer ansteuern, die wichtigsten Definitionen für dieses Device enthält die Datei
/usr/include/linux/soundcard.h
Jedem möglichen Kanal des Mixers ist eine Indexnummer zugeordnet, die Konstante SOUND MIXER NRDEVICES gibt die Anzahl der möglichen Kanäle an (gegenwärtig sind dies 25). Hier eine Liste der Mixer-Kanäle mit den zugehörigen
Index-Nummern und einer Beschreibung:
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
VOLUME
BASS
TREBLE
SYNTH
PCM
SPEAKER
LINE
MIC
CD
IMIX
ALTPCM
RECLEV
IGAIN
OGAIN
LINE1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Lautstärke des Mixerausgangs
Tiefen“ des Mixerausgangs
”
Höhen“ des Mixerausgangs
”
Synthesizer (Ton-Generator)
Digital-Analog-Wandler
PC-Lautsprecher
Line-In“-Buchse
”
Mikrofon
CD-ROM-Laufwerk
Aufnahmepegel
Eingangsverstärkung
Ausgangsverstärkung
weitere Analogeingänge
6.3 Ansteuerung einer Soundkarte
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
SOUND
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
MIXER
LINE2
LINE3
DIGITAL1
DIGITAL2
DIGITAL3
PHONEIN
PHONEOUT
VIDEO
RADIO
MONITOR
123
15
16
17
18
19
20
21
22
23
24
Digitaleingänge
z. B. von einer TV-Karte
Die aktuelle Einstellung eines dieser Kanäle kann mit dem ioctl()-Kommando
MIXER READ() abgefragt und mit MIXER WRITE() verändert werden. Beide Kommandos benötigen als dritten Parameter des ioctl()-Aufrufs den Zeiger auf eine Integer-Variable. Das unterste Byte dieser Variablen enthält die Lautstärke für
den linken Stereo-Kanal, das nächsthöhere Byte repräsentiert die Lautstärke für
den rechten Stereo-Kanal. Beide Werte müssen sich von 0 bis 100 bewegen.
Das folgende Programm fragt die aktuelle Einstellung der Gesamtlautstärke ab
und stellt sie anschließend auf 50 ein:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
set_volume.c - Gesamtlautstaerke lesen u. schreiben
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/soundcard.h>
int main()
{
int fd, level;
if ((fd = open("/dev/mixer", O_RDONLY)) == -1)
{
perror("set_volume: Can’t open device");
return(1);
}
if (ioctl(fd, MIXER_READ(SOUND_MIXER_VOLUME),
&level) == -1)
perror("set_volume: Can’t read master volume");
else
printf("master volume: L=%d, R=%d\n", level & 255,
124
26
27
28
29
30
31
32
33
34
35
6 Devices – das Tor zur Hardware
level >> 8);
level = 50 + (50 << 8);
if (ioctl(fd, MIXER_WRITE(SOUND_MIXER_VOLUME),
&level) == -1)
perror("set_volume: Can’t set master volume");
close(fd);
return(0);
}
Informationen über den Mixer holen
In der Regel unterstützt eine Soundkarte nur einen Teil der 25 möglichen MixerKanäle. Deshalb bietet das Device auch Kommandos, mit denen ein Programm
erfragen kann, welche Kanäle der Mixer tatsächlich besitzt:
ioctl()-Kommando
Bedeutung
MIXER READ(SOUND MIXER DEVMASK)
Liste der unterstützten Kanäle
MIXER READ(SOUND MIXER RECMASK)
Liste der möglichen Aufnahmequellen
MIXER READ(SOUND MIXER STEREODEVS)
Liste der Kanäle, die Stereo
unterstützen
Alle drei Funktionen erwarten als dritten Parameter des ioctl()-Aufrufs den
Zeiger auf eine Integer-Variable. In diese Variable schreiben die Kommandos dann
eine Zahl, bei der jedes Bit für einen möglichen Kanal steht. Ist es gesetzt, wird dieser Kanal unterstützt, anderenfalls ist er bei dieser Soundkarte nicht verfügbar.
Die Bitnummer entspricht dabei der Index-Nummer (s.o.) des Kanals. Das folgende Programm fragt mit MIXER READ(SOUND MIXER RECMASK) die möglichen
Aufnahmequellen des Mixers ab:
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
recsources.c - moegliche Aufnahmequellen des Mixers
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/soundcard.h>
const char *device_names[SOUND_MIXER_NRDEVICES]
= SOUND_DEVICE_NAMES;
6.3 Ansteuerung einer Soundkarte
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
125
int main()
{
int fd, mask, i;
if ((fd = open("/dev/mixer", O_RDONLY)) == -1)
{
perror("recsources: Can’t open device");
return(1);
}
if (ioctl(fd, MIXER_READ(SOUND_MIXER_RECMASK), &mask)
== -1)
perror("recsources: ioctl() failed");
else
{
printf("Supported recording sources:");
for (i=0; i<SOUND_MIXER_NRDEVICES; i++)
if (mask & (1<<i))
printf(" %s,", device_names[i]);
printf("\b.\n");
}
close(fd);
return(0);
}
Bitte beachten Sie die Zeilen 11 und 12 des Programms. Hier wird die Konstante
device names als ein Feld aus Zeichenketten definiert und mit Hilfe des Makros
SOUND DEVICE NAMES initialisiert. Dadurch zeigt das Element device_names[i]
auf eine Zeichenkette, die den Namen des Mixer-Kanals mit der Index-Nummer
i enthält – so zeigt z. B. device_names[6] auf line“. Nach dem Einlesen der
”
Bit-Maske in die Variable mask in Zeile 24 lässt sich so zu jedem gesetzten Bit der
Name des Audiokanals ausgeben (Zeile 30 bis 33).
Wahl der Aufnahmequelle
Das ioctl()-Kommando MIXER READ(SOUND MIXER RECMASK) liefert die Liste der möglichen Aufnahmequellen. Die tatsächlich eingestellte Aufnahmequelle
lässt sich über das ioctl()-Kommando MIXER READ(SOUND MIXER RECSRC)“
”
abfragen und mit dem Kommando MIXER WRITE(SOUND MIXER RECSRC)“ ver”
ändern.1 Beispiel:
1
Da bei diesem Kommando eine Bit-Maske angegeben wird, können prinzipiell auch mehrere Aufnahmequellen gleichzeitig eingestellt werden. Das wird jedoch nicht von allen Soundkarten unterstützt.
126
6 Devices – das Tor zur Hardware
int fd, mask;
mask = SOUND_MASK_CD; /* = 1<<SOUND_MIXER_CD */
if (ioctl(fd, MIXER_WRITE(SOUND_MIXER_RECSRC), &mask) == -1)
perror("Can’t set recording source");
6.3.3 Audiodaten aufnehmen und wiedergeben
Für die Aufnahme und Wiedergabe von Audiosignalen mit Hilfe des A/D- und
des D/A-Wandlers der Soundkarte sind die Device-Dateien /dev/dsp“ und
”
/dev/audio“ vorgesehen. Beide Devices bieten die gleichen Möglichkeiten, un”
terscheiden sich aber in dem voreingestellten Aufnahme-(Sampling-)Format.
Unterstützte Sampling-Formate abfragen
Soundkarten unterstützen in der Regel eine ganze Reihe verschiedener Formate
für die Aufnahme und Wiedergabe von Audiosignalen, angefangen von 8-Bit mit
oder ohne Vorzeichen über 16-Bit (CD-Qualität) bis hin zu MPEG-2-Audio. Nicht
jede Soundkarte ist in der Lage, alle diese Formate zu verarbeiten. Aus diesem
Grund gibt es das ioctl()-Kommando SNDCTL DSP GETFMTS, das die Abfrage
der unterstützten Formate erlaubt. Dieses Kommando liefert eine Bit-Maske, bei
der jedes Bit einem bestimmten Format zugeordnet ist. Die derzeit vorgesehenen
Formate sind in Tabelle 6.1 aufgelistet.
Bei den Formaten µ -law“ und A-law“ handelt es sich um Verfahren aus der
”
”
digitalen Telefonübertragung, mit deren Hilfe 13 Bit Audiodaten ohne Dynamikverlust auf 8 Bit reduziert werden können. Das ADPCM-Verfahren (Adaptive Differential Pulse Code Modulation) ermöglicht die Komprimierung von 64 kBit/s Audiosignalen auf 32 kBit/s.
Das folgende Programm fragt die möglichen Audioformate der Soundkarte ab:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
get_formats.c - moegliche Samplingformate abfragen
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/soundcard.h>
int main(int argc, char *argv[])
{
int fd, mask;
char *dev_name = "/dev/dsp";
6.3 Ansteuerung einer Soundkarte
127
Tabelle 6.1: Mögliche Audioformate und deren Beschreibung
Format
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Bit
AFMT MU LAW
0
AFMT A LAW
1
Beschreibung
8 Bit, komprimiert nach dem
µ -law“-Verfahren
”
8 Bit, komprimiert nach dem
A-law“-Verfahren
”
codiert nach dem ADPCM“-Verfahren
”
8 Bit ohne Vorzeichen (0 bis 255)
AFMT IMA ADPCM
2
AFMT U8
3
AFMT S16 LE
4
16 Bit mit Vorz. (− 32768 bis 32767),
low-Byte zuerst
AFMT S16 BE
5
16 Bit mit Vorz. (− 32768 bis 32767),
high-Byte zuerst
AFMT S8
6
8 Bit mit Vorz. (–128 bis 127)
AFMT U16 LE
7
16 Bit ohne Vorz. (–32768 bis 32767),
low-Byte zuerst
AFMT U16 BE
8
16 Bit ohne Vorz. (–32768 bis 32767),
high-Byte zuerst
AFMT MPEG
9
MPEG-2-Audio
if (argc > 1)
dev_name = argv[1];
if ((fd = open(dev_name, O_RDONLY)) == -1)
{
perror("get_formats: Can’t open device");
return(1);
}
if (ioctl(fd, SNDCTL_DSP_GETFMTS, &mask) == -1)
perror("get_formats: Can’t get supported formats");
else
{
printf("Device ’%s’ supports:\n", dev_name);
if (mask & AFMT_MU_LAW)
printf(" mu-law encoding\n");
if (mask & AFMT_A_LAW)
printf(" a-law encoding\n");
if (mask & AFMT_IMA_ADPCM)
printf(" ADPCM compression\n");
if (mask & AFMT_U8)
128
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
6 Devices – das Tor zur Hardware
printf("
if (mask &
printf("
if (mask &
printf("
if (mask &
printf("
if (mask &
printf("
if (mask &
printf("
if (mask &
printf("
unsigned 8 bit\n");
AFMT_S8)
signed 8 bit\n");
AFMT_S16_LE)
signed 16 bit (little endian)\n");
AFMT_S16_BE)
signed 16 bit (big endian)\n");
AFMT_U16_LE)
unsigned 16 bit (little endian)\n");
AFMT_U16_BE)
unsigned 16 bit (big endian)\n");
AFMT_MPEG)
MPEG-2-Audio encoding\n");
}
close(fd);
return(0);
}
Aufnahme und Wiedergabe
Um Audiosignale aufzuzeichnen und wiederzugeben, sollten zunächst Audioformat und Samplingrate (Abtastfrequenz) eingestellt werden. Des Weiteren sollte festgelegt werden, ob die Aufnahme/Wiedergabe in Stereo oder Mono erfolgen
soll. Für jede dieser Einstellungen gibt es jeweils ein ioctl()-Kommando:
SNDCTL DSP SETFMT – Audioformat einstellen
SNDCTL DSP STEREO – Stereo (1) oder Mono (0)
SNDCTL DSP SPEED – Abtastfrequenz (Samples pro Sekunde)
Vor einer Aufnahme muss ferner mit Hilfe des Mixers die gewünschte Audioquelle gewählt werden (siehe Seite 125). Bei Stereo-Aufnahmen liefert das Device die
Samples beider Kanäle im Wechsel, angefangen mit dem linken Kanal.
Das folgende Programm zeichnet eine Audio-Sequenz mit einer Länge von
100 000 Bytes auf und spielt diese anschließend wieder ab (8 Bit, 22050 Hz, mono):
1
2
3
4
5
6
7
8
9
/*
rec_play.c - Audio-Signal aufnehmen u. wiedergeben
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/soundcard.h>
6.3 Ansteuerung einer Soundkarte
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# define NUM_SAMPLES 100000
unsigned char buffer[NUM_SAMPLES];
int main()
{
int fd, i, format=AFMT_U8;
long length;
char input[16];
if ((fd = open("/dev/dsp", O_RDWR)) == -1)
{
perror("rec_play: Can’t open device");
return(1);
}
if (ioctl(fd, SNDCTL_DSP_SETFMT, &format) == -1)
perror("rec_play: Can’t set format");
i = 0;
if (ioctl(fd, SNDCTL_DSP_STEREO, &i) == -1)
perror("rec_play: Can’t set to mono");
i = 22050;
if (ioctl(fd, SNDCTL_DSP_SPEED, &i) == -1)
perror("rec_play: Can’t set sampling rate");
printf("Press <RETURN> to start recording. ");
fgets(input, 16, stdin);
if ((length = read(fd, buffer, NUM_SAMPLES)) == -1)
{
perror("rec_play: Can’t record audio data");
return(1);
}
printf("done (%ld bytes).\n"
"Press <RETURN> to start playing. ", length);
fgets(input, 16, stdin);
if (write(fd, buffer, length) == -1)
perror("rec_play: Can’t play audio data");
129
130
54
55
56
6 Devices – das Tor zur Hardware
close(fd);
return(0);
}
Nach dem Öffnen des Devices – hier mit O RDWR, um schreiben und lesen zu
können – und der Einstellung der Parameter wartet das Programm auf die
RETURN-Taste und beginnt danach die Aufzeichnung. Anschließend wartet das
Programm erneut auf das Drücken der Taste RETURN, bevor es die aufgezeichneten Daten wieder abspielt.
Die gängigsten Samplingraten für Audiosignale, die von den meisten Soundkarten unterstützt werden, sind 44100 Hz, 22050 Hz, 11025 Hz und 8000 Hz. Audiodaten werden in der Regel nicht als reine Daten“ gespeichert, sondern zu”
sammen mit Informationen über das Format und die Samplingrate. Häufig wird
hier das WAV-Format verwendet, unter Linux/Unix gelegentlich auch das AUFormat. Beide Formate sind im Anhang beschrieben.
6.4
Video for Linux“
”
Inzwischen ist die Multimedia-Welle auch auf Linux übergeschwappt. Es gibt
mittlerweile Kernel-Module, die Video-Quellen wie z. B. eine TV-Karte oder eine WebCam unterstützen. Diese Treiber sind unter dem Begriff Video for Linux“
”
– oder kurz Video4Linux“ – zusammengefasst. In diesem Abschnitt sollen das
”
zugehörige Device vorgestellt und die Aufnahme von Bildern mit diesem Device
erläutert werden. Die Beispielprogramme sind ausgelegt für eine WebCam, lassen
sich aber auch auf eine TV-Karte anwenden.
Von Haus aus unterstützen die meisten Linux-Distributionen leider nur sehr wenige USB-Kameras. Es gibt aber ein Open-Source-Projekt zu dem Kernel-Modul
spca5xx, das mittlerweile mehr als 160 WebCam-Typen unterstützt. Dieses Modul ist als Quelltext für 2.4er und 2.6er Kernels verfügbar, weitere Informationen
finden Sie unter http://mxhaard.free.fr/spca5xx.html.
6.4.1 Eigenschaften des Devices
Video-Hardware wie WebCams und TV-Karten lassen sich über den Dateipfad
/dev/video“ ansprechen. Dabei handelt es sich um einen symbolischen Link
”
auf das Device, im Allgemeinen ist das /dev/video0“. Ist mehr als eine Vi”
deoquelle – z. B. TV-Karte und WebCam – vorhanden, gibt es entsprechend viele Device-Dateien /dev/video0, /dev/video1 usw. Alle für die Programmierung relevanten Definitionen und Deklarationen finden Sie in der Include-Datei
/usr/include/linux/videodev.h“.
”
Die Bandbreite der unterstützten Videoquellen des Devices ist groß, dementsprechend unterschiedlich sind auch Funktionen und Möglichkeiten. Daher bietet das
Device das ioctl()-Kommando VIDIOCGCAP (VIDeo IO-Control Get CAPabilities)
6.4 Video for Linux“
”
131
an, mit dem die Fähigkeiten und Eigenschaften abgefragt werden können. Dieses
Kommando erwartet als dritten Parameter des ioctl()-Aufrufs den Zeiger auf
eine Variable vom Typ struct video capability:
struct video_capability
{
char name[32]; /* Produktbezeichnung */
int type;
/* Bit-Maske aus Eigenschaften */
int channels;
/* Anzahl der Kanaele */
int audios;
/* Anzahl der Audio-Devices */
int maxwidth;
/* maximale Bildbreite */
int maxheight; /* maximale Bildhoehe */
int minwidth;
/* minimale Bildbreite */
int minheight; /* minimale Bildhoehe */
};
Als type“ erhält man eine logische Oder-Verknüpfung aus den Eigenschaf”
ten des angeschlossenen Gerätes – z. B. VID TYPE MONOCHROME, wenn nur
Schwarz/Weiß unterstützt wird, oder VID TYPE TUNER, falls das Gerät einen
Empfänger besitzt, dessen Frequenz via Software eingestellt werden kann. Die
vollständige Liste der möglichen Eigenschaften (mit Beschreibung) erhält man mit
grep VID_TYPE_ /usr/include/linux/videodev.h
Neben den möglichen Einstellungen für das Bildformat lassen sich auch die aktuell gewählten Einstellungen abfragen. Dazu dienen unter anderem die Kommandos VIDIOCGWIN (VIDeo IO-Control Get WINdow) und VIDIOCGPICT (VIDeo IOControl Get PICTure). VIDIOCGWIN erwartet als Parameter den Zeiger auf eine Variable vom Typ struct video window:
struct video_window
{
__u32 x,y; /* Position des Bildausschnitts */
__u32 width,height; /* und Groesse */
__u32 chromakey;
__u32 flags;
struct video_clip *clips; /* nur schreiben */
int clipcount;
};
Das Kommando VIDIOCGPICT erwartet als Parameter den Zeiger auf eine Variable vom Typ struct video picture:
132
6 Devices – das Tor zur Hardware
struct video_picture
{
__u16 brightness;
__u16 hue;
__u16 colour;
__u16 contrast;
__u16 whiteness;
__u16 depth;
__u16 palette;
};
/*
/*
/*
/*
/*
/*
/*
/*
Helligkeit */
Farbwert */
Farbsaettigung */
Kontrast */
nur bei schwarz/weiss */
Bits pro Pixel */
Palette z.B. */
VIDEO_PALETTE_RGB24 */
Das folgende Programm nutzt die o. g. ioctl()-Kommandos VIDIOCGCAP,
VIDIOCGWIN und VIDIOCGPICT, um Informationen über das angeschlossene
Gerät auszugeben:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
videoinfo.c - Informationen ueber /dev/video holen
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/videodev.h>
int main()
{
int fd;
struct video_capability video_cap;
struct video_window video_win;
struct video_picture video_pict;
if ((fd = open("/dev/video", O_RDONLY)) == -1)
{
perror("videoinfo: Can’t open device");
return(1);
}
if (ioctl(fd, VIDIOCGCAP, &video_cap) == -1)
perror("videoinfo: Can’t get capabilities");
else
{
printf("Name:\t\t’%s’\n", video_cap.name);
printf("Minimum size:\t%d x %d\n",
video_cap.minwidth, video_cap.minheight);
6.4 Video for Linux“
”
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
133
printf("Maximum size:\t%d x %d\n",
video_cap.maxwidth, video_cap.maxheight);
}
if (ioctl(fd, VIDIOCGWIN, &video_win) == -1)
perror("videoinfo: Can’t get window information");
else
printf("Current size:\t%d x %d\n", video_win.width,
video_win.height);
if (ioctl(fd, VIDIOCGPICT, &video_pict) == -1)
perror("videoinfo: Can’t get picture information");
else
printf("Current depth:\t%d\n", video_pict.depth);
close(fd);
return(0);
}
Für eine Philips USB-WebCam sieht das z. B. so aus:
> gcc videoinfo.c -o videoinfo
> videoinfo
Name:
’Philips 680 webcam’
Minimum size:
128 x 96
Maximum size:
640 x 480
Current size:
352 x 288
Current depth: 24
6.4.2 Bilder aufzeichnen
Eine sehr einfache Möglichkeit der Aufzeichnung eines Bildes von der Videoquelle besteht im Auslesen des Devices mit der Funktion read(). Das folgende Programm liest ein Bild mit der voreingestellten Größe ein und schreibt es als PPMDatei (Portable PixMap) nach stdout:
1
2
3
4
5
6
7
8
9
10
/*
read_image.c - Bild von WebCam oder TV-Karte lesen
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/videodev.h>
134
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
6 Devices – das Tor zur Hardware
# define MAX_BYTES (640*480*3) /* Bildspeicher */
# define SWAP_RGB_BGR 0 /* 1 = Farbreihenfolge drehen */
int main()
{
int fd;
long length;
struct video_window video_win;
static unsigned char image[MAX_BYTES];
if ((fd = open("/dev/video", O_RDONLY)) == -1)
{
perror("read_image: Can’t open device");
return(1);
}
if (ioctl(fd, VIDIOCGWIN, &video_win) == -1)
{
perror("read_image: Can’t get video window");
return(1);
}
length = video_win.width * video_win.height * 3;
if ((length < 1) || (length > MAX_BYTES))
{
fprintf(stderr, "read_image: Bad image size. "
"Using default values.\n");
video_win.width = 320;
video_win.height = 240;
length = 320 * 240 * 3;
if (ioctl(fd, VIDIOCSWIN, &video_win) == -1)
{
perror("read_image: Can’t set video window");
return(1);
}
}
if (read(fd, image, length) == -1)
{
perror("read_image: Error while reading");
return(1);
}
close(fd);
6.4 Video for Linux“
”
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
if (SWAP_RGB_BGR)
{
int i, tmp;
135
/* Rot und Blau tauschen? */
for (i=0; i<video_win.width*video_win.height*3; i+=3)
{
tmp = image[i];
image[i] = image[i+2];
image[i+2] = tmp;
}
}
printf("P6\n%d %d\n255\n", video_win.width,
video_win.height);
fwrite(image, 3, video_win.width*video_win.height,
stdout);
return(0);
}
Zum Aufzeichnen und Darstellen eines Bildes mit diesem Beispielprogramm
können Sie wie folgt vorgehen:
> gcc read image.c -o read image
> ./read image > aufnahme.ppm
> xview aufnahme.ppm
Alternativ kann die Aufnahme auch mit den Tools gimp“, xv“ oder display“
”
”
”
(aus dem Programm-Paket ImageMagick) dargestellt werden.
Üblicherweise liefern WebCams voreingestellt 24-Bit-Farbbilder in RGB-Format1 ,
je Pixel werden also drei Bytes für die drei Farbanteile geliefert. Verschiedene
WebCams senden die Bytes jedoch in umgekehrter Reihenfolge – wahrscheinlich,
weil sie nicht als einzelne Bytes, sondern als 3-Byte-Werte, beginnend mit dem
niedrigstwertigen, übertragen werden (little endian Codierung). Falls Rot und Blau
vertauscht erscheinen, können Sie in Zeile 12 über die define-Anweisung dafür
sorgen, dass die Farben zurückgetauscht werden. (Bei dem o. g. Kernel-Modul
spca5xx lässt sich über die Option2 force rgb=1“ auch die Reihenfolge der
”
Farbwerte korrigieren.)
Nach dem Öffnen des Devices (Zeile 21) fragt das Programm zunächst das eingestellte Bildformat ab (Zeile 27). Sollte das Format zu groß oder nicht initialisiert
(Werte = 0) sein, wird es ab Zeile 39 auf Default-Werte eingestellt. Dies geschieht
1
2
RGB steht für Red-Green-Blue bzw. Rot-Grün-Blau.
insmod spca5xx.ko force rgb=1“
”
136
6 Devices – das Tor zur Hardware
mit Hilfe des Kommandos VIDIOCSWIN (VIDeo IO-Control Set WINdow), das die
zuvor ausgelesene und modifizierte Struktur video win wieder zurückschreibt.
Das eigentliche Aufzeichnen des Bildes erfolgt mit der read()-Anweisung in Zeile 49. Vor dem Abspeichern als PPM-Datei erfolgt in den Zeilen 56 bis 66 ggf. das
Vertauschen der Farbanteile Rot und Blau (siehe oben).
Es besteht natürlich auch die Möglichkeit, vor der Bildaufzeichnung die Einstellungen für Helligkeit, Kontrast, Farbsättigung, Farbtiefe (Bits pro Pixel) und Farbschema (Palette) explizit einzustellen. Dies geschieht mit Hilfe des Kommandos
VIDIOCSPICT (VIDeo IO-Control Set PICTure). In dem obigen Beispielprogramm
könnten Sie dazu ab Zeile 48 den folgenden Quelltext einfügen:
{
struct video_picture video_pict;
if (ioctl(fd, VIDIOCGPICT, &video_pict) == -1)
{
perror("Can’t get video picture information");
return(1);
}
video_pict.brightness = 39321; /* 60% Helligkeit */
video_pict.contrast = 26214;
/* 40% Kontrast */
video_pict.depth = 24;
video_pict.palette = VIDEO_PALETTE_RGB24;
if (ioctl(fd, VIDIOCSPICT, &video_pict) == -1)
{
perror("Can’t set video picture information");
return(1);
}
}
Bilder mit CAPTURE und mmap() aufzeichnen
Die elegantere“, aber etwas aufwändigere Methode für die Aufzeichnung eines
”
Bildes ist mit Hilfe der Funktionen mmap() und munmap() gegeben:
void *mmap(void *start, size_t length, int prot,
int flags, int fd, off_t offset);
int munmap(void *mem, size_t length);
Beschreibung der Parameter:
start –
length –
gewünschte Zieladresse oder NULL (→ keine Vorgabe)
Größe des Speicherbereichs
6.4 Video for Linux“
”
prot
–
flags
–
fd
–
offset –
137
Zugriffsrechte, Oder-Verknüpfung aus den Bits
PROT READ (Lesen) PROT WRITE (Schreiben)
und PROT EXEC (Ausführen)
Eigenschaften des Speichersegments, z. B. MAP PRIVATE
oder MAP SHARED (Zugriff auch für andere Prozesse)
Datei-Deskriptor des Devices
zu überspringende Bytes des Devices
Als Rückgabewert liefert mmap() die Speicheradresse, wohin die Daten geschrieben werden, oder MAP FAILED im Fehlerfall. Die Adresse und die Größe müssen
der Funktion munmap() als Parameter übergeben werden, um das Speichersegment wieder freizugeben.
Mit mmap() lässt sich der Bildspeicher (Frame Buffer) des Video-Devices in
ein Speichersegment abbilden. Voraussetzung ist allerdings, dass das Device
capture-fähig ist. Dies kann mit dem ioctl()-Kommando VIDIOCGCAP (siehe
Abschnitt 6.4.1) überprüft werden: hier muss in der Bit-Maske type das Bit
VID TYPE CAPTURE gesetzt sein.
Das folgende Programm verwendet mmap(), um ein Bild der Größe 320 × 240 mit
24 Bit pro Pixel einzulesen, und schreibt dieses als PPM-Datei in den Standardausgabekanal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
map_image.c - Ein Bild mit Hilfe von ’mmap()’ lesen
*/
#
#
#
#
#
#
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<sys/mman.h>
<linux/videodev.h>
# define WIDTH 320
# define HEIGHT 240
# define SWAP_RGB_BGR 0
/* 1 = Farbreihenfolge drehen */
int main()
{
int fd, frame, i, tmp;
unsigned char *image;
struct video_mmap vid_mmap;
if ((fd = open("/dev/video", O_RDONLY)) == -1)
{
perror("map_image: Can’t open device");
138
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
6 Devices – das Tor zur Hardware
return(1);
}
if ((image = mmap(NULL, WIDTH*HEIGHT*3, PROT_READ,
MAP_SHARED, fd, 0)) == MAP_FAILED)
{
perror("map_image: mmap() failed");
return(1);
}
vid_mmap.frame = 0;
vid_mmap.width = WIDTH;
vid_mmap.height = HEIGHT;
vid_mmap.format = VIDEO_PALETTE_RGB24;
if (ioctl(fd, VIDIOCMCAPTURE, &vid_mmap) == -1)
{
perror("map_image: Can’t capture to memory");
return(1);
}
frame = 0;
/* muss gleich vid_mmap.frame sein */
if (ioctl(fd, VIDIOCSYNC, &frame) == -1)
{
perror("map_image: Can’t grab frame");
return(1);
}
close(fd);
if (SWAP_RGB_BGR)
/* Rot und Blau tauschen? */
{
for (i=0; i<WIDTH*HEIGHT*3; i+=3)
{
tmp = image[i];
image[i] = image[i+2];
image[i+2] = tmp;
}
}
printf("P6\n%d %d\n255\n", WIDTH, HEIGHT);
fwrite(image, 3, WIDTH*HEIGHT, stdout);
munmap(image, WIDTH*HEIGHT*3);
return(0);
}
6.4 Video for Linux“
”
139
Nach dem Öffnen des Devices (Zeile 22) und dem Abbilden in ein Speichersegment (Zeile 28) wird die Übertragung eines Bildes zunächst in den Zeilen 35 bis
43 mit dem Kommando VIDIOCMCAPTURE vorbereitet und anschließend in den
Zeilen 45 bis 50 mit dem Kommando VIDIOCSYNC gestartet. Als frame ist in beiden Fällen 0 angegeben. Besitzt das Device einen Frame-Buffer, in dem N Bilder
abgelegt werden können, kann hier eine Zahl zwischen 0 und N − 1 angegeben
werden. Damit die richtigen Daten in den Speicher übertragen werden, müsste
man in diesem Fall jedoch einen entsprechenden Offset bei mmap() angeben.
Wie schon im vorigen Beispielprogramm folgt auch hier optional das Vertauschen
der Farb-Bytes für Rot und Blau (ab Zeile 53). Danach wird das Bild wiederum als
PPM-Datei gespeichert.
Soll eine Sequenz von Bildern aufgezeichnet werden, so müssen für jedes Bild
erneut die ioctl()-Kommandos VIDIOCMCAPTURE und VIDIOCSYNC aufgerufen
werden, um den Inhalt des Speichersegmentes zu aktualisieren.
Weitere Einstellmöglichkeiten
Für Geräte, die eine Empfangseinheit (Tuner) besitzen (wie z. B. TV-Karten) oder
über mehrere Videoeingänge verfügen, gibt es ioctl()-Kommandos, mit denen die Videoquelle gewählt, das Videoformat (z. B. NTSC oder PAL) eingestellt und die Empfangsfrequenz vorgegeben werden können. Das Kommando
VIDIOCSCHAN benötigt den Zeiger auf eine Struktur vom Typ video channel,
das Kommando VIDIOCSFREQ erwartet den Zeiger auf eine Variable vom Typ
long.
Es gibt eine Reihe weiterer Kommandos und Funktionen des Video-Devices –
z. B. zur Nutzung der Audio-Fähigkeiten –, die hier jedoch nicht alle im Einzelnen erläutert werden können. An dieser Stelle möchten wir noch einmal auf die
Include-Datei
/usr/include/linux/videodev.h
verweisen, der Sie die entsprechenden ioctl()-Kommandos und die zugehörigen Strukturen entnehmen können.
JPEG-Bilder speichern mit der libjpeg
Bei den bisherigen Beispielprogrammen wurden die aufgezeichneten Bilder als
PPM-Datei gespeichert, weil dieses Format sehr einfach zu erzeugen ist. Üblicherweise sind Bilder jedoch im JPEG-Format, das die Aufnahmen je nach Kompressionseinstellung auf weniger als 10% der Speichergröße reduziert. Die Umwandlung von Bilddaten in das JPEG-Format lässt sich relativ einfach mit der Funktionsbibliothek libjpeg“ realisieren. Das folgende Beispielprogramm zeigt die Er”
weiterung des Programms read image“ um die Verwendung der libjpeg (ab Zei”
le 77):
140
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
6 Devices – das Tor zur Hardware
/*
read_image_jpeg.c - WebCam-Bild als JPEG speichern
*/
#
#
#
#
#
#
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/videodev.h>
<jpeglib.h>
# define MAX_BYTES (640*480*3)
/* Bildspeicher */
# define DEF_WIDTH 320
/* Default-Werte */
# define DEF_HEIGHT 240
# define JPEG_QUALITY 75
# define SWAP_RGB_BGR 0
/* 1 = Farbreihenfolge drehen */
int main()
{
int fd;
long length;
struct video_window video_win;
static unsigned char image[MAX_BYTES];
JSAMPROW row_pointer;
static struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
if ((fd = open("/dev/video", O_RDONLY)) == -1)
{
perror("read_image: Can’t open device");
return(1);
}
if (ioctl(fd, VIDIOCGWIN, &video_win) == -1)
{
perror("read_image: Can’t get video window");
return(1);
}
length = video_win.width * video_win.height * 3;
if ((length < 1) || (length > MAX_BYTES))
6.4 Video for Linux“
”
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
141
{
fprintf(stderr, "read_image: Bad image size. "
"Using default values.\n");
video_win.width = DEF_WIDTH;
video_win.height = DEF_HEIGHT;
length = DEF_WIDTH * DEF_HEIGHT * 3;
if (ioctl(fd, VIDIOCSWIN, &video_win) == -1)
{
perror("read_image: Can’t set video window");
return(1);
}
}
if (read(fd, image, length) == -1)
{
perror("read_image: Error while reading");
return(1);
}
close(fd);
if (SWAP_RGB_BGR)
{
int i, tmp;
/* Rot und Blau tauschen? */
for (i=0; i<video_win.width*video_win.height*3; i+=3)
{
tmp = image[i];
image[i] = image[i+2];
image[i+2] = tmp;
}
}
/* ----- JPEG-Umwandlung vorbereiten -----*/
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
jpeg_stdio_dest(&cinfo, stdout);
cinfo.image_width = video_win.width;
cinfo.image_height = video_win.height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
142
6 Devices – das Tor zur Hardware
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, JPEG_QUALITY, TRUE);
/* ----- JPEG-Umwandlung starten -----*/
jpeg_start_compress(&cinfo, TRUE);
while (cinfo.next_scanline < cinfo.image_height)
{
row_pointer =
&(image[cinfo.next_scanline*cinfo.image_width*3]);
jpeg_write_scanlines(&cinfo, &row_pointer, 1);
}
jpeg_finish_compress(&cinfo);
/* Bild speichern */
jpeg_destroy_compress(&cinfo);
/* aufräumen */
return(0);
}
Zum Übersetzen des Programms muss die JPEG-Bibliothek eingebunden werden:
gcc read_image_jpeg.c -ljpeg -o read_image_jpeg
6.5 Die serielle Schnittstelle
Obwohl die als RS 232“ oder V 24“ bezeichnete serielle Schnittstelle immer mehr
”
”
durch den Universal Serial Bus (USB) verdrängt wird, ist sie noch bei vielen Computern vorhanden oder wird mit Hilfe eines USB/RS232-Umsetzers emuliert.
Es handelt sich dabei um einen weit verbreiteten Standard, der nicht nur bei
Computer-Peripherie zum Einsatz kommt, sondern beispielsweise auch bei verschiedenen Messgeräten oder Relaiskarten (siehe auch [1]).
Unter Linux sind die seriellen Ports1 als Terminal-Devices /dev/ttySn (früher
/dev/cuan) in das Dateisystem eingebunden. Als solche kann man sie mit Hilfe
von Funktionen zur Einstellung der Terminal-Parameter steuern. Von der Shell
aus lässt sich z. B. mit
stty ispeed=38400 < /dev/ttyS0
die Baudrate der ersten seriellen Schnittstelle (COM1) auf 38,4 kBit/s stellen. Die
Devices /dev/ttySn sind in der Regel der Gruppe uucp zugeordnet. Um darauf
1
Das gilt auch für USB-Modems, bei denen der USB quasi nur als Brücke“ für das serielle Protokoll
”
dient.
6.5 Die serielle Schnittstelle
143
zugreifen zu können, müssen Sie sich in der Datei /etc/group“ ebenfalls in
”
diese Gruppe eintragen:
uucp:x:14:uucp,fax,root,fnet,Ihr User-Name
6.5.1 Terminal-Parameter einstellen
Es gibt eine Reihe von Funktionen, mit denen man auf die Einstellungen eines
Terminal-Devices zugreifen kann. Die wichtigsten davon sind tcgetattr() zum
Auslesen und tcsetattr() zum Modifizieren der Parameter:
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int actions,
struct termios *termios_p);
Beiden Funktionen wird neben dem Dateideskriptor des Terminal-Devices der
Zeiger auf eine Variable vom Typ struct termios übergeben. tcsetattr() erwartet zusätzlich den Parameter actions, mit dem man vorgeben kann, wann
die neuen Einstellungen übernommen werden. Mögliche Werte sind hier z. B.
TCSANOW (sofort) und TCSAFLUSH (Änderung erst, wenn alle bis dahin nach fd
geschriebenen Zeichen übertragen wurden).
Die in /usr/include/bits/termios.h“ definierte Struktur struct termios
”
enthält unter anderem die folgenden Einträge:
tcflag_t
tcflag_t
tcflag_t
tcflag_t
c_iflag;
c_oflag;
c_cflag;
c_lflag;
/*
/*
/*
/*
input mode flags */
output mode flags */
control mode flags */
local mode flags */
Jeder dieser Parameter setzt sich aus einer ODER-Verknüpfung verschiedener
Konstanten zusammen; eine Auswahl zeigt die Tabelle 6.2. Die vollständige Liste erhält man mit man tcsetattr“.
”
Um die Einstellungen eines Terminal-Devices zu verändern, werden in der Regel die aktuellen Einstellungen mit tcgetattr() ausgelesen, dann die gewünschten Änderungen vorgenommen und anschließend alle Einstellungen mit
tcsetattr() wieder geschrieben; Beispiel:
int fd;
struct termios term_attr;
if (tcgetattr(fd, &term_attr) == 0)
{
term_attr.c_lflag &= ˜ECHO; /* kein lokales Echo */
tcsetattr(fd, TCSAFLUSH, &term_attr);
}
144
6 Devices – das Tor zur Hardware
Tabelle 6.2: Einige Konstanten für die Terminal-Parameter
Parameter
c iflag
c oflag
c cflag
c lflag
Flag
Bedeutung
INLCR
IGNCR
ICRNL
IUCLC
IMAXBEL
empfangenes NL (new line) in CR (carriage return)
wandeln
empfangenes CR ignorieren
empfangenes CR in NL wandeln
empfangene Groß- in Kleinbuchstaben wandeln
Piepton bei Zeilenende
OPOST
ONLCR
OCRNL
ONLRET
OLCUC
Nachbearbeitung der auszugebenden Zeichen einschalten
NL beim Senden in CR + NL wandeln
CR beim Senden in NL wandeln
kein CR senden
beim Senden Klein- in Großbuchstaben wandeln
Bbaud
CSn
CSTOPB
PARENB
PARODD
CLOCAL
CRTSCTS
CREAD
Geschwindigkeit auf baud Bit/Sek. stellen
Bitbreite auf n stellen (5, 6, 7 oder 8)
zwei Stop-Bits statt eines verwenden
Parity-Bit erzeugen
ungerades Parity-Bit statt gerades
Modem-Steuerleitung (CD) ignorieren
Hardware-Handshake aktivieren
Empfang von Daten aktivieren
ISIG
ICANON
ECHO
bestimmte Zeichen lösen ein Signal aus
Steuerzeichen zulassen
empfangene Zeichen wieder senden
Das folgende Programm realisiert eine typische Kennworteingabe, bei der man
die Eingabe selbst nicht sehen kann, um das Kennwort vor den Augen Dritter zu
schützen:
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
password.c - Passwortabfrage ohne Echo
*/
#
#
#
#
#
include
include
include
include
include
<stdio.h>
<unistd.h>
<string.h>
<fcntl.h>
<termios.h>
int main()
{
int old_flags;
6.5 Die serielle Schnittstelle
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
145
char password[16];
struct termios term_attr;
if (tcgetattr(STDIN_FILENO, &term_attr) != 0)
{
perror("password: tcgetattr() failed");
return(1);
}
/* alte Einst. sichern */
old_flags = term_attr.c_lflag;
term_attr.c_lflag &= ˜ECHO;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0)
perror("password: tcsetattr() failed");
printf("password: ");
scanf("%15s", password);
/* Std.-Eingabe wie vorher */
term_attr.c_lflag = old_flags;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0)
perror("password: tcsetattr() failed");
if (strcmp(password, "secret") == 0)
printf("\npassword accepted.\n");
else
printf("\nwrong password.\n");
return(0);
}
6.5.2 Ein kleines Terminalprogramm
Soll über die serielle Schnittstelle eine Verbindung mit einem externen Gerät – z. B.
einem Telefon-Modem – aufgebaut werden, so müssen zunächst die Geschwindigkeit (Bit pro Sekunde), die Anzahl der zu übertragenden Bits pro Zeichen und
die Anzahl der Stop-Bits korrekt eingestellt werden. Ferner sollte auch das Paritäts-Bit (eine Art Prüfsumme) richtig eingestellt werden. All diese Einstellungen
– ebenso wie die Aktivierung des Hardware-Handshakes CTS/RTS – finden sich
in dem Parameter c cflag der Struktur termios.
Das folgende Programm stellt die Parameter der seriellen Schnittstelle ein
(19200 baud, 8N1, Hardware-Handshake) und schaltet das lokale Echo sowie die
Steuerzeichenerkennung (ICANON) des Standard-Eingabekanals aus. Alle Eingaben werden dann auf die serielle Schnittstelle gesendet und alle von der Schnittstelle empfangenen Daten im Shell-Fenster ausgegeben, bis das Programm mit
ESC beendet wird. Damit verhält sich das Programm wie ein einfaches Terminal-
146
6 Devices – das Tor zur Hardware
Programm – echte“ Terminal-Programme wie minicom können natürlich viel
”
mehr.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
terminal.c - Ein- und Ausgabe ueber die serielle
Schnittstelle
*/
#
#
#
#
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<termios.h>
# define TERM_DEVICE "/dev/ttyS0"
# define TERM_SPEED B19200
/* = COM1 */
/* Bit/Sek */
int main()
{
int fd, old_flags;
ssize_t length;
char buffer[16];
struct termios term_attr;
fd_set input_fdset;
if ((fd = open(TERM_DEVICE, O_RDWR)) == -1)
{
perror("terminal: Can’t open device " TERM_DEVICE);
return(1);
}
/* RS232 konfigurieren */
if (tcgetattr(fd, &term_attr) != 0)
{
perror("terminal: tcgetattr() failed");
return(1);
}
term_attr.c_cflag = TERM_SPEED | CS8 | CRTSCTS
| CLOCAL | CREAD;
term_attr.c_iflag = 0;
term_attr.c_oflag = OPOST | ONLCR;
term_attr.c_lflag = 0;
if (tcsetattr(fd, TCSAFLUSH, &term_attr) != 0)
perror("terminal: tcsetattr() failed");
/* Std.-Eingabe anpassen */
if (tcgetattr(STDIN_FILENO, &term_attr) != 0)
6.5 Die serielle Schnittstelle
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
147
{
perror("terminal: tcgetattr() failed");
return(1);
}
/* alte Einst. sichern */
old_flags = term_attr.c_lflag;
term_attr.c_lflag &= ˜(ICANON | ECHO);
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0)
perror("terminal: tcsetattr() failed");
while (1)
{
FD_ZERO(&input_fdset);
FD_SET(STDIN_FILENO, &input_fdset);
FD_SET(fd, &input_fdset);
if (select(fd+1, &input_fdset, NULL, NULL, NULL) == -1)
perror("terminal: select() failed");
if (FD_ISSET(STDIN_FILENO, &input_fdset))
{
if ((length = read(STDIN_FILENO, buffer, 16)) == -1)
perror("terminal: read() failed");
else
if (buffer[0] == ’\33’) /* Abbruch mit ESC */
break;
else
write(fd, buffer, length);
}
if (FD_ISSET(fd, &input_fdset))
{
if ((length = read(fd, buffer, 16)) == -1)
perror("terminal: read() failed");
else
write(STDOUT_FILENO, buffer, length);
}
}
/* Std.-Eingabe wie vorher */
term_attr.c_lflag = old_flags;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0)
perror("terminal: tcsetattr() failed");
printf("Aborted.\n");
close(fd);
return(0);
}
148
6 Devices – das Tor zur Hardware
Nach dem Öffnen des Terminal-Devices (Zeile 22) wird in den Zeilen 28 bis 39
die serielle Schnittstelle konfiguriert. In Zeile 42 bis 51 erfolgt die Einstellung des
Standard-Eingabekanals, wobei die alten Einstellungen zuvor in der Variablen
old flags gesichert werden. Die while()-Schleife in den Zeilen 53 bis 77 bildet
den Kern des Programms. Hier wird zunächst mit der Funktion select() gewartet, bis an einem der beiden Dateideskriptoren – Standard-Eingabekanal oder
serielle Schnittstelle – Daten eintreffen“. Entsprechend werden entweder Zeichen
”
von der Standardeingabe eingelesen und über die Schnittstelle ausgegeben (Zeile
62 bis 68) oder umgekehrt (Zeile 72 bis 75). Eine Betätigung der Taste ESC beendet
die while()-Schleife durch break (Zeile 66) und stellt die alten Einstellungen für
den Standard-Eingabekanal wieder her (Zeile 78 bis 81).
Die Funktion select() und die dazugehörigen Makros FD ZERO(), FD SET(),
FD CLR() und FD ISSET() bedürfen einiger Erklärungen. Diese Funktionen operieren mit so genannten File Descriptor Sets, das sind Variablen vom Typ fd set
mit einer Länge von 1024 Bit (128 Bytes). Jedes Bit steht dabei für einen Dateideskriptor, z. B. Bit Nr. 2 für den Deskriptor mit dem Wert 2, also den StandardFehlerkanal. Das Makro FD ZERO() löscht alle Bits des angegebenen File Descriptor Sets. Mit FD SET() kann ein Dateideskriptor zu dem Set hinzugefügt und mit
FD CLR() entfernt werden. Mit Hilfe von FD ISSET() lässt sich prüfen, ob ein
Deskriptor in einem Set enthalten ist, d.h. das entsprechende Bit gesetzt ist.
Die Funktion select() erwartet als zweiten, dritten und vierten Parameter jeweils den Zeiger auf ein File Descriptor Set. Sie testet, ob einer der in dem ersten Set enthaltenen Dateideskriptoren bereit zum Lesen oder einer der in dem
zweiten Set enthaltenen Dateideskriptoren bereit zum Schreiben ist. Die Deskriptoren des dritten Sets werden auf Exceptions überprüft. Als ersten Parameter erwartet select() die Nummer des höchsten verwendeten Dateideskriptors
plus 1. Als letzter Parameter kann mit Hilfe der Struktur struct timeval (siehe
/usr/include/sys/time.h) eine maximale Wartezeit eingestellt werden. Die
Funktion select() ermöglicht es somit, auf mehrere dateibezogene Ereignisse
gleichzeitig zu warten – im Falle des Terminal-Programms das Drücken einer Taste oder das Empfangen eines Zeichens über die Schnittstelle.
Noch ein Hinweis zur Einstellung der Schnittstellenparameter: Wenn die Geschwindigkeit oder die Bitbreite eingestellt werden soll, ohne die anderen Einstellungen zu verändern, müssen die relevanten Bits zunächst mit CBAUD bzw. CSIZE
gelöscht werden. Beispiel:
int fd;
struct termios term_attr;
if (tcgetattr(fd, &term_attr) == 0)
{
/* Bits loeschen */
term_attr.c_cflag &= ˜(CBAUD | CSIZE);
6.6 Druckerausgaben
149
/* und neu einstellen */
term_attr.c_cflag |= B19200 | CS8;
tcsetattr(fd, TCSAFLUSH, &term_attr);
}
Die Übertragungsgeschwindigkeit für das Senden und Empfangen von Zeichen
lässt sich übrigens auch separat mit den Funktionen
int cfsetispeed(struct termios *termios_p, int speed);
int cfsetospeed(struct termios *termios_p, int speed);
einstellen.1 Diese Funktionen modifizieren die Geschwindigkeitseinstellungen in
der Struktur termios. Anschließend müssen diese Einstellungen wiederum mit
einem Aufruf der Funktion tcsetattr() aktiviert werden.
Weitere Informationen zur Ansteuerung der seriellen Schnittstelle finden Sie in
Abschnitt 9.3.
6.6 Druckerausgaben
Die Ansteuerung eines Druckers fällt etwas aus der Reihe, weil sie nicht wie die
anderen bisher angesprochenen Geräte über ioctl()-Kommandos funktioniert.
Die Kommunikation mit dem Drucker übernimmt bei Linux ein Hintergrundprozess. Ursprünglich war dies der lpd“ (line printer daemon), der auf aktuel”
len Systemen durch CUPS“ (Common UNIX Printing System) ersetzt wurde. Bei”
de Systeme sind in der Lage, unterschiedliche Dateiformate zu verarbeiten; die
gängigsten Formate sind ASCII (reine Textdateien) und PostScript. Während reine ASCII-Texte bei der Druckerausgabe etwas an Schreibmaschinenseiten erinnern, bietet PostScript nahezu unbegrenzte Möglichkeiten, dem Drucker qualitativ hochwertige Text- und Grafikdarstellungen zu entlocken. Daher hat sich dieses
Format – oder besser gesagt: diese Sprache – als Standard auf UNIX- und LinuxSystemen durchgesetzt.
Um aus der Shell heraus Daten an einen Drucker zu schicken, können Sie das
Programm lpr“ verwenden. Stehen mehrere Drucker zur Verfügung, können Sie
”
mit der Option -PDrucker“ das gewünschte Ausgabegerät angeben:
”
lpr -Pprinter mein Text.txt
Achtung: Zwischen der Option -P“ und der Bezeichnung des Druckers darf kein
”
Leerzeichen stehen! CUPS bietet ein Web-Interface zur Verwaltung von Druckern
und Druckjobs. Dort können Sie auch sehen, welche Drucker eingerichtet sind:
1
Auf manchen Systemen, beispielsweise Cygwin, muss die Geschwindigkeit mit Hilfe der Funktionen cfsetispeed() und cfsetospeed() eingestellt werden; das Einstellen der Übertragungsgeschwindigkeit über c cflag funktioniert dort nicht.
150
6 Devices – das Tor zur Hardware
firefox http://localhost:631/printers
Doch wie erzeugen Sie mit eigenen Programmen eine Druckerausgabe? Dazu bedienen wir uns der Funktion popen() aus Abschnitt 5.5.1 und rufen so das Programm lpr“ auf. Das folgende Beispiel gibt links oben auf der Seite den Text
”
Hallo Welt!“ in großer Schrift (24 Punkte) aus und zeichnet einen Kreis um die”
sen Text.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
printer-demo.c - Druckerausgabe mit popen()
*/
# include <stdio.h>
# include <string.h>
int main()
{
int status;
FILE *stream;
if ((stream = popen("lpr", "w")) == NULL)
{
perror("printer-demo: popen() failed");
return(1);
}
fprintf(stream, "%%!PS\n%%%%BoundingBox: 30 30 565 810\n"
"%%%%Orientation: Portrait\n%%%%EndProlog\n"
"100 700 moveto\n"
"/Times-Roman 24 selectfont\n"
"(Hallo Welt!) show\n"
"currentpoint pop 100 add 2 div\n"
"newpath 708 65 0 360 arc stroke\n"
"showpage\n");
status = pclose(stream);
printf("printder-demo: lpr returned %d.\n", status);
return(0);
}
Wenn Sie die Ausgabe an einen anderen als den Standarddrucker schicken wollen,
müssen Sie in Zeile 13 das Kommando lpr“ um die Option -P“ (siehe oben)
”
”
ergänzen. In den Zeilen 19 bis 26 wird eine PostScript-Sequenz an den Standardeingabekanal von lpr“ gesendet.
”
6.6 Druckerausgaben
151
An dieser Stelle möchten wir Ihnen die Sprache“ PostScript etwas näher bringen.
”
Dazu betrachten Sie bitte die folgende PostScript-Datei:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
%!PS-Adobe-2.0
%%Title: PostScript-Beispiel1
%%Creator: Martin Gräfe
%%BoundingBox: 28 28 567 813
%%Orientation: Portrait
%%Pages: 1
%%EndProlog
%%Page: 1
28.35 28.35 scale
% ab hier in Zentimeter umskalieren
7 25 moveto
% Cursor an Position x=7cm, y=25cm
% Schriftart auf "Helvetica" mit 1cm Höhe:
/Helvetica 1 selectfont
% Text darstellen:
(Dies ist ein Test.) show
0.1 setlinewidth
% Liniendicke auf 0,1cm
0 0 1 setrgbcolor
% Farbe Blau
% Linie unter dem Text zeichnen:
0 -0.2 rmoveto
% 0,2cm nach unten
7 24.8 lineto
% Linie bis zum Textanfang
stroke
% Linie zeichnen
showpage
% gesamte Seite darstellen
Mit dem Programm ghostview“ oder kghostview“ können Sie diese Datei auf
”
”
dem Bildschirm darstellen. Es erscheint oben auf der Seite der Text Dies ist ein
”
Test.“, der mit einer blauen Linie unterstrichen ist. Betrachten wir nun den Aufbau der PostScript-Datei. Die erste Zeile dient zur Erkennung des PostScriptFormats. Danach folgen einige Zeilen als Prolog“, die Aufschluss über Seiten”
format usw. geben. Die BoundingBox“ gibt an, in welchen Bereich auf der Seite
”
sich die zu zeichnenden Elemente befinden. Die Grundmaßeinheit von PostScript
ist dabei 1 /72 Zoll (ca. 0,35 mm), und der Seitenursprung (0;0) liegt in der linken
unteren Ecke des Blatts.
Das %-Zeichen definiert in PostScript den Rest der Zeile als Kommentar und ist
vergleichbar mit //“ in C. Eine Ausnahme bilden zwei %-Zeichen am Zeilen”
152
6 Devices – das Tor zur Hardware
anfang. Diese signalisieren Kontrollinformationen für den PostScript-Interpreter.
Der Prolog am Anfang der Datei darf übrigens weder Leer- noch Kommentarzeilen enthalten, damit er von PostScript-Interpretern wie ghostview“ verarbeitet
”
wird.
Zeile 9 des PostScript-Beispiels signalisiert dem Interpreter, dass hier die Seite mit
der Nummer 1 folgt. Wenn Sie hier beispielsweise eine 5 eingeben, zeigt ghostview in der Seitenübersicht die Seitennummer 5 an, obwohl es nur eine Seite ist.
In Zeile 11 folgt mit scale“ die erste richtige PostScript-Anweisung. Hier wird
”
alles Folgende von der ursprünglichen Maßeinheit auf Zentimeter umskaliert. Bitte beachten Sie, dass PostScript grundsätzlich in der so genannten umgekehrten
polnischen Notation (UPN) arbeitet, d. h. die Parameter eines Kommandos oder eines Operators stehen immer vor dem Kommando bzw. Operator. Die Rechnung
20 − 10“ heißt in PostScript 20 10 sub“. So stehen die X- und Y-Position auch
”
”
vor dem moveto-Kommando in Zeile 13. Die weiteren Zeilen des Beispiels sind
selbsterklärend. Es sei aber noch darauf hingewiesen, dass Zeichen wie ( ) %“ als
”
Text ausgegeben werden können, wenn ihnen ein \“ vorangestellt wird.
”
Dass PostScript nicht einfach nur ein Text-Dateiformat, sondern eine komplexe Programmiersprache mit Funktionen und Schleifenstrukturen ist, soll dieses
zweite Beispiel zeigen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
%!PS-Adobe-2.0
%%Title: PostScript-Beispiel2
%%Creator: Martin Gräfe
%%BoundingBox: 28 28 567 813
%%Orientation: Portrait
%%Pages: 1
%%EndProlog
%%Page: 1
% Schriftart auf "Times-Roman" mit 18pt Höhe:
/Times-Roman 18 selectfont
230 320 moveto
8 {
% das Folgende 8x wiederholen
( Dies ist ein Test. ) show
45 rotate
% um 45◦ nach links drehen
} repeat
gsave
% Position/Skalierung merken
newpath
0.5 1 scale
0.8 setgray
% Position löschen
% alles Folgende halbe Breite
% Grau mit 80% Helligkeit
6.6 Druckerausgaben
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
153
582 475 70 0 360 arc
fill
% Kreis zeichnen
% und ausfüllen
grestore
% alte Position/Skalierung
/buffer 2 string def
% entspricht: char buffer[2];
0 1 5 {
% for (i=0; i<=5; i+=1)
dup
% Zähler vom Stack kopieren
buffer exch 49 add 0 exch put
% buffer[0] = 49+i;
-15 mul 510 add 270 exch moveto % x=270, y=510-15*i
(Test ) show
% printf("Test ");
buffer show
% printf(buffer);
} for
showpage
In den Zeilen 15 bis 18 wird eine Anweisungsfolge mit Hilfe einer repeat-Schleife
acht mal wiederholt. In den Zeilen 32 bis 38 kommt eine for-Schleife zum Einsatz,
deren Zähler von 0 bis 5 läuft und (um 1 erhöht) als Text dargestellt wird. Abbildung 6.1 zeigt das Ergebnis.
st.
Dies ist ein Test.
D
Te
ie
ei
n
si
s
ist
te
in
t.
Dies ist ein Test. D
ie
s
Te
s
Dies ist ein Test. D
ie
D
st.
Te
si
n
st
ei
ei
st
n
si
Te
ie
st.
Test 1
Test 2
Test 3
Test 4
Test 5
Test 6
Dies ist ein Test.
Abbildung 6.1: So kommt das zweite PostScript-Beispiel aus dem Drucker.
Für weitere Informationen und eine umfassendere Beschreibung der Sprache
PostScript sei hier auf entsprechende Tutorials im Internet verwiesen, siehe auch
[9], [10] und [11].
154
6 Devices – das Tor zur Hardware
6.7 Der Universal Serial Bus (USB)
Zwar haben wir in Abschnitt 6.4 schon gezeigt, wie eine USB-Kamera angesteuert
werden kann, wenn ein entsprechender Treiber installiert wurde. Im Folgenden
soll aber gezeigt werden, wie man auf USB-Geräte auch ohne Treiber in Form
eines Kernel-Moduls zugreift – und zwar mit Hilfe der libusb“.
”
Zum Einstieg in dieses Thema zeigt das folgende Programm die Auflistung aller
Busse und der dort angeschlossenen Geräte:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1
/*
find_usb.c - USB-Geräte auflisten
*/
# include <stdio.h>
# include <string.h>
# include <usb.h>
int main()
{
int i;
struct usb_bus *bus;
struct usb_device *udev;
usb_dev_handle *udevhd;
static char buffer[256];
usb_init();
// libusb initialisieren
usb_find_busses();
usb_find_devices();
// alle USBs suchen
// alle Geräte an den USBs suchen
bus = usb_get_busses();
// Zeiger auf ersten USB
i = 1;
while (bus != NULL)
{
printf("%d. Universal Serial Bus:\n", i++);
udev = bus->devices; // 1. Gerät
while (udev != NULL)
{
udevhd = usb_open(udev);
if (udevhd != NULL)
{
if (udev->descriptor.iProduct)
Das Gleiche leistet übrigens das Programm /usr/sbin/lsusb“.
”
6.7 Der Universal Serial Bus (USB)
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
155
usb_get_string_simple(udevhd,
udev->descriptor.iProduct, buffer, 256);
else
buffer[0] = ’\0’;
printf(" %04X : %04X ’%s’\n",
udev->descriptor.idVendor,
udev->descriptor.idProduct,
buffer);
usb_close(udevhd);
}
udev = udev->next; // Zeiger auf nächstes Gerät
}
bus = bus->next;
// Zeiger auf nächsten USB
}
if (i == 1)
printf("No USB found.\n");
return(0);
}
Das Übersetzen dieses Programms erfordert das Einbinden der libusb mit Hilfe
der Option -l“:
”
> gcc find usb.c -lusb -o find usb
> ./find usb
1. Universal Serial Bus:
1307 : 0165 ’USB Mass Storage Device’
0AEC : 3260 ’USB Storage Device’
0000 : 0000 ’EHCI Host Controller’
2. Universal Serial Bus:
03F0 : 4811 ’PSC 1600 series’
046D : C001 ’USB Mouse’
0000 : 0000 ’OHCI Host Controller’
Sehen wir uns das Programm im Detail an: In Zeile 17 wird zunächst die libusb
initialisiert. Danach werden die USB-Busse und -Geräte von der Bibliothek analysiert (Zeile 19 und 20) und die Informationen dazu in den Strukturen usb bus und
usb device abgelegt. Beide Strukturen enthalten jeweils einen Zeiger auf den
nächsten Bus bzw. das nächste Gerät. In zwei verschachtelten while-Schleifen
(Zeile 25 und 29) werden alle gefundenen Busse und Geräte der Reihe nach abgefragt und deren Herstellerkennung (idVendor) und Produkt-Code (idProduct)
ausgegeben. Außerdem wird der Beschreibungstext aus jedem Gerät ausgelesen
(Zeile 35). Dazu muss das Device zunächst geöffnet und später wieder geschlossen werden (Zeile 31 und 43).
156
6 Devices – das Tor zur Hardware
Neben den im Beispielprogramm verwendeten Elementen iProduct, idProduct
und idVendor enthält die Struktur usb device descriptor einige weitere Einträge mit Informationen über das angeschlossene Gerät, wie z. B. die Seriennummer:
struct usb_device_descriptor {
u_int8_t bLength;
u_int8_t bDescriptorType;
u_int16_t bcdUSB;
u_int8_t bDeviceClass;
u_int8_t bDeviceSubClass;
u_int8_t bDeviceProtocol;
u_int8_t bMaxPacketSize0;
u_int16_t idVendor;
u_int16_t idProduct;
u_int16_t bcdDevice;
u_int8_t iManufacturer;
u_int8_t iProduct;
u_int8_t iSerialNumber;
u_int8_t bNumConfigurations;
};
6.7.1 Ansteuerung von USB-Geräten anhand eines Beispiels
Als Beispiel für die Kommunikation mit USB-Geräten zeigen wir in diesem Abschnitt die Ansteuerung eines USB- Raketenwerfers“, der über verschiedene An”
bieter im Internet für knapp 30,– Euro erworben werden kann (siehe Abbildung 6.2). Das Gerät hat die Herstellerkennung 1130hex (Tenx Technology Inc.) und
die Produkt-ID 0202hex.
Das folgende Programm sucht zunächst in der Liste der USB-Devices das richtige
Gerät anhand der Hersteller- und Produkt-ID. Danach lässt es den Raketenwerfer
nach oben schwenken, eine Rakete abfeuern und anschließend wieder nach unten
schwenken.
1
2
3
4
5
6
7
8
9
10
11
/*
missile.c - USB-Raketenwerfer ansteuern
*/
#
#
#
#
include
include
include
include
<stdio.h>
<string.h>
<unistd.h>
<usb.h>
# define ID_PRODUCT 0x0202
# define ID_VENDOR 0x1130
// Kennung des
//
Raketenwerfers
6.7 Der Universal Serial Bus (USB)
157
Abbildung 6.2: Der USB-Raketenwerfer für den Schreibtisch
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# define TIMEOUT 1000
// Millisekunden
char init1[8] = "USBC\0\0\4\0",
init2[8] = "USBC\0\x40\2\0";
/*----- Funktion zum Auffinden des Gerätes -----*/
struct usb_device *find_missile(void)
{
struct usb_bus *bus;
struct usb_device *udev;
bus = usb_get_busses();
// Zeiger auf ersten USB
while (bus != NULL)
{
udev = bus->devices;
// 1. Gerät
while (udev != NULL)
{
if ((udev->descriptor.idVendor == ID_VENDOR)
&& (udev->descriptor.idProduct == ID_PRODUCT))
return(udev);
158
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
6 Devices – das Tor zur Hardware
udev = udev->next;
}
bus = bus->next;
}
return(NULL);
// Zeiger auf nächstes Gerät
// Zeiger auf nächsten USB
}
/*----- Funktion zum Beanspruchen der Interfaces -----*/
int claim_missile(usb_dev_handle *hd)
{
usb_detach_kernel_driver_np(hd, 0); // ggf. Kernelusb_detach_kernel_driver_np(hd, 1); // Treiber abkoppeln
if (usb_set_configuration(hd, 1))
return(1);
if (usb_claim_interface(hd, 1))
return(1);
return(0);
}
/*----- Funktion zum Senden von Kommandos -----*/
int missile_do(usb_dev_handle *hd, int left, int right,
int up, int down, int fire)
{
int n;
static char buffer[64];
n = usb_control_msg(hd,
USB_TYPE_CLASS | USB_RECIP_INTERFACE,
USB_REQ_SET_CONFIGURATION, 0, 1,
init1, sizeof(init1), TIMEOUT);
if (n != sizeof(init1))
return(1);
n = usb_control_msg(hd,
USB_TYPE_CLASS | USB_RECIP_INTERFACE,
USB_REQ_SET_CONFIGURATION, 0, 1,
init2, sizeof(init2), TIMEOUT);
if (n != sizeof(init2))
return(1);
6.7 Der Universal Serial Bus (USB)
159
79
buffer[0] = 0;
// Kommandos in Puffer schreiben
80
buffer[1] = left;
81
buffer[2] = right;
82
buffer[3] = up;
83
buffer[4] = down;
84
buffer[5] = fire;
85
buffer[6] = buffer[7] = 8;
86
for (n=8; n<64; n++) // Rest mit Nullen auffüllen
87
buffer[n] = 0;
88
89
n = usb_control_msg(hd,
90
USB_TYPE_CLASS | USB_RECIP_INTERFACE,
91
USB_REQ_SET_CONFIGURATION, 0, 1,
92
buffer, sizeof(buffer), TIMEOUT);
93
if (n != sizeof(buffer))
94
return(1);
95
96
return(0);
97
}
98
99 /*---------- Hauptprogramm ----------*/
100
101 int main()
102
{
103
struct usb_device *missile;
104
usb_dev_handle *missile_hd;
105
usb_init();
// libusb initialisieren
106
107
108
usb_find_busses();
// alle USBs suchen
109
usb_find_devices();
// alle Geräte an den USBs suchen
110
111
if ((missile = find_missile()) == NULL)
112
{
113
fprintf(stderr, "missile: Device not found!\n");
114
return(1);
115
}
116
missile_hd = usb_open(missile);
// Devide öffnen
117
118
if (missile_hd == NULL)
119
{
120
perror("missile: Can’t open device");
121
return(1);
122
}
160
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
6 Devices – das Tor zur Hardware
if (claim_missile(missile_hd))
// Interface belegen
{
perror("missile: Can’t claim interface");
usb_close(missile_hd);
return(1);
}
//
< > ˆ v F
if (missile_do(missile_hd, 0, 0, 1, 0, 1))
// hoch +
perror("missile: Error sending message"); //
Feuer
usleep(1500000L);
if (missile_do(missile_hd, 0, 0, 0, 0, 0))
perror("missile: Error sending message");
// stopp
usleep(1500000L);
if (missile_do(missile_hd, 0, 0, 0, 1, 0))
perror("missile: Error sending message");
// runter
usleep(1500000L);
if (missile_do(missile_hd, 0, 0, 0, 0, 0))
perror("missile: Error sending message");
// stopp
usb_close(missile_hd);
return(0);
}
Zum Übersetzen des Programms muss auch hier wieder die libusb eingebunden
werden:
gcc missile.c -lusb -o missile
Funktionsweise des Programms
In den Zeilen 18 bis 97 werden drei Hilfsfunktionen definiert. Die Funktion
find missile() (Zeile 20 bis 40) durchsucht die Liste der angeschlossenen USBGeräte nach der richtigen Hersteller- und Produkt-ID und gibt den Zeiger auf das
entsprechende USB-Device zurück – bzw. 0, wenn der Raketenwerfer nicht gefunden wurde. Bevor das Programm mit dem Raketenwerfer kommunizieren kann,
muss es das Interface des Geräts für sich beanspruchen. Dies geschieht in der
Funktion claim missile() (Zeile 44 bis 55). Weil der Raketenwerfer ggf. vom
6.7 Der Universal Serial Bus (USB)
161
Kernel als Human Interface Device (HID) erkannt und daher von einem KernelTreiber belegt“ wird, muss in den Zeilen 46 und 47 zunächst der Kernel-Treiber
”
abgekoppelt“ werden, weil das Programm sonst nicht auf das Interface des Rake”
tenwerfers zugreifen kann.1 Der Raketenwerfer stellt zwei Interfaces bereit (0 und
1), die beide vom Kernel-Treiber getrennt werden müssen. Anschließend wird die
aktive Konfiguration gewählt und das Interface 1 reserviert, das wir für die Kommunikation benutzen wollen.
Die dritte Hilfsfunktion missile do() in den Zeilen 59 bis 97 sendet schließlich die Kommandos an den Raketenwerfer. Als Parameter erwartet die Funktion
neben dem Zeiger auf den Device-Handle je einen Wert für die vier Bewegungsrichtungen und den Auslöser des Raketenwerfers. Für die auszuführenden Bewegungen muss der entsprechende Parameter auf 1 gesetzt werden, die anderen
Parameter müssen auf 0 bleiben.
Bevor man das Kommando zur Bewegung des Raketenwerfers senden kann,
müssen zwei Initialisierungssequenzen an das Gerät geschickt werden, die in den
Zeilen 15 und 16 definiert sind. Das Senden von Steuersequenzen an das USBGerät übernimmt die Funktion usb control msg() in den Zeilen 65, 72 und 89:
int usb_control_msg( usb_dev_handle *dev,
int requesttype,
int request,
int value,
int index,
char *bytes,
int size,
int timeout );
Der Parameter requesttype ist eine Bitmaske, die einerseits die Art der Anforderung (Request) festlegt (hier der Typ CLASS) und andererseits den Empfänger
spezifiziert, in diesem Fall also ein Interface des Gerätes. Der Parameter index
gibt die Nummer des adressierten Interfaces an, hier also 1. Als request wird
das Setzen der Konfiguration mit den Daten angefordert, auf die der Parameter
bytes zeigt. Während size die Anzahl der Bytes angibt, die übertragen werden
sollen, hat value hier keine Bedeutung.
Weitere Informationen zur libusb finden Sie auf den Internetseiten [13] von
Sourceforge.net. Für Programme und Infos zur Ansteuerung des Raketenwerfers
unter Linux verweisen wir auf die Internetseiten von Luke Cole [12].
1
Wurde der Kernel-Treiber bereits vom Interface getrennt, liefern die detach-Funktionen einen Fehler
als Rückgabewert, was wir hier jedoch einfach ignorieren.
Kapitel 7
Netzwerkprogrammierung
Seit den Anfängen von Linux vor mehr als 10 Jahren ist die Netzwerkkommunikation1 fester Bestandteil des Betriebssystems. Während zu dieser Zeit
auf WindowsTM 3.11-Rechner diverse proprietäre Netzwerkprotokolle aufgesetzt
wurden, war Linux von Haus aus in der Lage, über den Standard TCP/IP mit
Workstations, Großrechnern und Servern in Rechenzentren zu kommunizieren.
Im Bereich der PCs und Workstations wurden die proprietären Lösungen fast
vollständig von TCP/IP verdrängt, sodass inzwischen die unterschiedlichen Betriebssysteme in einem Netzwerk die gleiche Sprache“ sprechen.
”
Möglicherweise fragen Sie sich an dieser Stelle, ob Sie sich mit der komplexen Thematik Netzwerkprogrammierung auseinandersetzen sollen, weil Sie vielleicht gar
nicht beabsichtigen, mehrere Computer mit eigenen Programmen zu vernetzen“.
”
Bei Linux ist jedoch die Netzwerkkommunikation mehr als ein Hilfmittel zum
Datenaustausch zwischen zwei Computern; sie ist die logische Fortsetzung der
Interprozesskommunikation, und viele Teile des Systems bauen darauf auf. Wenn
Sie auf dem Desktop Ihres Linux-PCs ein Fenster schließen oder eine Schaltfläche
anklicken, wird dies dem betreffenden Programm von der grafischen Oberfläche
X11 über Funktionen zur Netzwerkkommunikation mitgeteilt. Aus diesem Grund
ist es sinnvoll, dieses Thema selbst bei Stand-Alone-Systemen“ ohne einen Netz”
werkanschluss zu betrachten.
Die Thematik der Netzwerkprogrammierung ist deutlich umfangreicher als beispielsweise die Interprozesskommunikation. Eine vollständige und detaillierte
Beschreibung umfasst leicht mehrere hundert Seiten. Deshalb konzentriert sich
dieses Kapitel auf die geläufigsten Funktionen und Methoden, die Sie immerhin
in die Lage versetzen werden, einen eigenen Webserver zu programmieren!
1
Netzwerkkommunikation wäre die passendere Bezeichnung für dieses Kapitel. In der englischsprachigen Literatur hat sich jedoch der Begriff Network Programming“ etabliert und wurde später
”
wörtlich ins Deutsche übersetzt.
164
7 Netzwerkprogrammierung
7.1 Einführung
Ähnlich wie bei den vorangegangenen Kapiteln soll auch hier der Einstieg in die
Thematik anhand kleiner Beispielprogramme erleichtert werden. Um die einzelnen Schritte in den Programmen nachvollziehen zu können, sind jedoch ein gewisses Grundlagenwissen und etwas Theorie unerlässlich. Insbesondere sollen
die im Umfeld der Netzwerkkommunikation auftauchenden Begriffe sowie das
Prinzip der Kommunikation über eine Netzwerkverbindung erläutert werden.
7.1.1 Begriffe
Ethernet
Der Begriff Ethernet“ wird häufig als Synonym für eine Netzwerkverbindung
”
verwendet. Tatsächlich beschreibt Ethernet ein Verfahren, wie mehrere Teilnehmer auf eine gemeinsame Netzwerkleitung zugreifen können – es handelt sich
dabei also um ein Zugriffsverfahren. Ursprünglich bestand diese Netzwerkleitung
aus einem Koaxialkabel (ähnlich einem Antennenkabel), an das alle Computer
eines Netzwerksegments über je ein T-Stück angeschlossen waren. Damit waren
Datenraten bis 10 MBit/s möglich.
Heute findet man eine solche Verkabelung nur noch selten. Das Koaxialkabel wurde weitestgehend von der bekannten 8-adrigen CAT 5“-Leitung verdrängt, und
”
die Netzwerkstruktur ist heutzutage in der Regel sternförmig, mit Hubs und Switches in den Sternpunkten.
Ethernet definiert also weder die Art der Leitung noch die des Protokolls (s. u.),
das über die Leitung übertragen wird.
Protokoll
Damit sich die Teilnehmer eines Netzwerkes untereinander verstehen“, muss in
”
einem Protokoll festgelegt sein,
wie ein Datenpaket“ auf der Netzwerkverbindung aussieht;
”
wie der Empfänger des Datenpaketes adressiert“ wird;
”
ob und wie auf Übertragungsfehler reagiert wird;
...
Einige solcher Protokolle sind:
IP – Internet Protocol (regelt die Adressierung der Netzwerkteilnehmer);
TCP – Transmission Control Protocol (definiert die gesicherte1 Datenübertragung
zwischen zwei Netzwerkteilnehmern);
1
gesichert“ bedeutet hier: Es können keine Daten verloren gehen.
”
7.1 Einführung
165
UDP – User Datagram Protocol (regelt die Datenübertragung ähnlich TCP, jedoch
nicht gesichert);
FTP – File Transfer Protocol (definiert die Übertragung von Dateien);
telnet – Protokoll zum Login eines Benutzers auf einem entfernten Rechner;
HTTP – Hyper-Text Transfer Protocol (beschreibt den Zugriff auf Web-Seiten);
SMTP – Simple Mail Transfer Protocol (regelt die E-Mail-Übertragung);
PPP – Point to Point Protocol (definiert die Netzwerkverbindung zwischen zwei
Teilnehmern über eine Punkt-zu-Punkt-Verbindung inkl. Authentifizierung
und Zuweisung einer IP-Adresse).
In der Regel trifft man auf eine Verschachtelung mehrerer Protokolle. So laufen“
”
über ein serielles Kabel zwischen PC und analogem Modem beim Aufrufen einer
Internet-Seite gleichzeitig die Protokolle HTTP, TCP, IP und PPP.
Bei den meisten PC-Netzwerken wie auch im Internet kommt die Kombination
TCP und IP zum Einsatz, kurz als TCP/IP bezeichnet. Das IP sorgt dafür, dass
die Daten zum richtigen Teilnehmer gelangen, das TCP stellt sicher, dass alle Datenpakete fehlerfrei und in der richtigen Reihenfolge ankommen. Die Beispiele in
diesem Kapitel beziehen sich alle auf TCP/IP-Verbindungen.
In Abschnitt 7.3.3 werden die Grundlagen des HTTP beschrieben.
Port
In TCP/IP-Netzwerken werden die Netzwerkteilnehmer über ihre IP-Adresse
adressiert. Doch wie funktioniert es, dass die Daten des Web-Servers (also die
HTML-Seiten) im Browser landen, die E-Mails im E-Mail-Client ankommen, die
Dateien vom FTP-Server zum FTP-Client (z. B. xftp) übertragen werden und Benutzername und Passwort bei einem remote login“ (rlogin oder telnet) an der
”
richtigen Stelle ankommen? Dies ist in ähnlicher Weise gelöst, wie auch die richtigen Daten an Drucker, Modem, Monitor, Tastatur, Maus und Scanner ankommen:
Die Geräte hängen an unterschiedlichen Anschlüssen oder auch Ports des Computers. Ebenso enthalten die TCP/IP-Datenpakete neben der IP-Adresse auch eine
Port-Nummer, also eine Information, für welchen Anschluss“ des Computers die
”
Daten bestimmt sind (Abbildung 7.1).
Es gibt eine ganze Reihe von Port-Nummern, die fest für bestimmte Dienste vergeben sind, beispielsweise:
166
7 Netzwerkprogrammierung
TCP/IP
-
Port 1
Port 2
Port 3
Port 4
..
.
- Programm a
- Programm b
- Programm c
- Programm d
IP-Adr.: xxx.xxx.xxx.xxx
Abbildung 7.1: Aufteilung der IP-Verbindungen auf verschiedene Ports
Port
Dienst
21
23
25
53
79
80
6000
FTP-Server
telnet-Server
SMTP-Server (E-Mail-Dienst)
Domain Name Server
Finger“-Server (Infos über Benutzer)
”
Web-Server (HTTP-Server)
X11-Server
Eine umfangreiche Liste der (möglichen) Dienste und deren Port-Nummern finden Sie in der Datei /etc/services“.
”
Verbindung (Connection)
Unter Verbindung“ versteht man in der Netzwerkkommunikation nicht die phy”
sikalische Leitung zwischen zwei Computern (also das Kabel), sondern den Datenpfad, der beispielsweise zwischen einem Browser und einem Webserver aufgebaut
wird.
Neben der verbindungsorientierten Kommunikation, wie sie in diesem Kapitel behandelt wird, gibt es auch verbindungslosen Datentransfer im Netzwerk. Ist z. B. einem Netzwerkteilnehmer die Adresse des Domain Name Servers (DNS) noch nicht
bekannt, fordert er diese mit Hilfe einer Broadcast-Nachricht an, ohne zuvor eine Verbindung aufzubauen. Verbindungslose Kommunikation hat also nichts mit
Wireless LAN zu tun.
7.1.2 Vorbereitung
Die Firewall
Auf aktuellen Linux-Systemen ist in der Regel eine Firewall installiert und auch aktiviert. Diese blockiert aus Sicherheitsgründen verschiedene Netzwerkaktivitäten,
ohne dass Warn- oder Fehlermeldungen ausgegeben werden. Dadurch laufen einige der folgenden Beispielprogramme bei aktivierter Firewall nicht oder nicht
korrekt. Aus diesem Grund sollten Sie zunächst prüfen, ob eine Firewall aktiv ist
7.1 Einführung
167
und diese dann ggf. deaktivieren.1 Bei SuSE-Linux geht das mit Hilfe des Konfigurationstools YAST. Wählen Sie dazu in der Rubrik Sicherheit und Benutzer“
”
den Punkt Firewall“ (Abbildung 7.2).
”
Abbildung 7.2: Konfiguration der Firewall mit YAST unter SuSE
Abbildung 7.3: Abschalten der Firewall
1
Aus Sicherheitsgründen sollten Sie bei Versuchen mit eigenen Netzwerkprogrammen und abgeschalteter Firewall keine Verbindung mit dem Internet haben!
168
7 Netzwerkprogrammierung
Es erscheint dann das Fenster zur Konfiguration, in dem Sie die Schaltfläche Fire”
wall nun stoppen“ wählen (Abbildung 7.3). Danach können Sie die Konfiguration
mit der entsprechenden Schaltfläche abbrechen. Wenn Sie die Einstellung Ser”
vice starten: Bei Systemstart“ aktiviert lassen, wird die Firewall automatisch beim
nächsten Neustart des Rechners wieder aktiv.
Netzwerkdienste aktivieren
Linux stellt bereits einige Netzwerkdienste zur Verfügung, von denen einige in
dem xinetd“ zusammengefasst sind. Dieser Dämon ist in der Grundkonfigurati”
on häufig deaktiviert. Da die im xinetd enthaltenen Dienste aber gerade für erste
Versuche mit Netzwerkprotokollen hilfreich sind, sollten sie den Dämon aktivieren (siehe Abbildung 7.4, Netzwerkdienste (xinetd)“). Abbildung 7.5 zeigt einen
”
Teil der Dienste, die vom xinetd bereitgestellt werden. Hier sollten Sie finger“,
”
echo“ und ftp“ für die Beispiele in den folgenden Abschnitten aktivieren.
”
”
Abbildung 7.4: Konfiguration der Netzwerkdienste mit YaST
7.1 Einführung
169
Abbildung 7.5: Aktivierung der xindet“-Dienste
”
7.1.3 Das Client-Server-Prinzip
Bei der verbindungsorientierten Netzwerkkommunikation, wie sie in den folgenden Abschnitten beschrieben wird, tritt jeweils ein Teilnehmer als Server und der
andere als Client auf. Die Abläufe (Funktionsaufrufe) zum Verbindungsaufbau
sind bei Client und Server ganz unterschiedlich: Der Server meldet seinen Dienst
in der Regel mit einer festen Port-Nummer an (z. B. belegt ein Webserver den Port
Nr. 80). Der Client (Browser) baut eine Verbindung zum Server auf, indem er diesen mit der IP-Adresse und der Port-Nummer adressiert.
Sobald die Verbindung aufgebaut ist, wird dem Client automatisch ebenfalls eine
Port-Nummer zugewiesen, die in der Regel unabhängig von der Port-Nummer
des Servers ist (siehe Abbildung 7.6).
Web- - Port 1052
Browser
IP-Adr.: w.x.y.z
Netzwerk
- Port 80 - Web-
Server
IP-Adr.: a.b.c.d
Abbildung 7.6: Verbindungsorientierte Kommunikation zwischen Client und Server
170
7 Netzwerkprogrammierung
Damit können beide Applikationen, Client und Server, jeweils eindeutig über ihre
IP-Adresse und Port-Nummer adressiert werden.
Übrigens: Client und Server können natürlich auch auf dem gleichen Computer
laufen, also die gleiche IP-Adresse haben.
7.1.4 Sockets
Basis der Netzwerkkommunikation – ob verbindungsorientiert oder verbindungslos – bilden die Sockets (zu Deutsch: Sockel“, Steckdosen“). Sowohl Client
”
”
als auch Server müssen zunächst einen Socket öffnen, bevor eine Verbindung aufgebaut werden kann. Zum Öffnen eines Sockets dient die gleichnamige Funktion
int socket(int domain, int type, int protocol);
Dabei gibt der Parameter domain die Protokollfamilie für die Kommunikation über
diesen Socket an. Für TCP/IP-Verbindungen ist das die Konstante PF INET. Der
zweite Parameter legt die Art der Kommunikation fest, für verbindungsorientierte Kommunikation ist hier die Konstante SOCK STREAM zu wählen. Der dritte Parameter, der das zu benutzende Protokoll bestimmt, kann in der Regel auf 0 gesetzt werden, wodurch automatisch das zu Protokollfamilie und Kommunikationsart passende Protokoll gewählt wird.
Als Rückgabewert liefert socket() einen Dateideskriptor (oder —1 im Fehlerfall) – ein Socket ist also quasi eine Datei. Daher wird er wie eine Datei wieder
geschlossen:
int close(int sock_fd);
Damit sieht das Rahmenprogramm“ für die Netzwerkkommunikation wie folgt
”
aus:
# include <sys/socket.h>
int main(int argc, char *argv[])
{
int sock_fd;
...
sock_fd = socket(PF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
perror("socket() failed");
...
close(sock_fd);
}
Zum Lesen aus oder Schreiben in einen Socket können Sie – wie bei Devices –
die Funktionen read() und write() verwenden. Darüber hinaus gibt es für
7.2 Der TCP/IP-Client
171
die Netzwerkkommunikation spezielle Funktionen, die zusätzliche Möglichkeiten bieten:
int send(int sock_fd, void *data, size_t len, int flags);
int recv(int sock_fd, void *buffer, size_t len, int flags);
Doch bevor wir überhaupt Daten über einen Socket lesen oder schreiben können,
muss eine Verbindung zum Socket des Kommunikationspartners hergestellt werden. Weil dies bei Client und Server unterschiedlich ist, wird in den folgenden
Abschnitten zunächst ein typischer Client und anschließend ein einfacher Server
vorgestellt.
7.2 Der TCP/IP-Client
Für die Demonstration von Netzwerkprogrammierung benötigt man sowohl ein
Client- wie auch ein Server-Programm. Glücklicherweise ist Linux von Haus aus
mit einer Reihe von Server-Programmen auf TCP/IP-Basis ausgestattet, sodass
wir uns zunächst auf das Programmieren eines TCP/IP-Clients konzentrieren
können.
7.2.1 Aufbau einer Verbindung
Als erster Schritt muss, wie bereits erwähnt, ein Socket geöffnet werden. Danach kann das Client-Programm diesen Socket mit einem Socket des ServerProgramms verbinden. Dies geschieht mit der Funktion connect():
int connect(int sock_fd, struct sockaddr *serv_addr,
socklen_t addrlen);
War der Verindungsaufbau erfolgreich, gibt connect() eine 0 zurück, anderenfalls –1. Als ersten Parameter erwartet connect() den Dateideskriptor des
Sockets, der verbunden werden soll. Danach folgen als zweiter und dritter Parameter die (Internet-)Adresse des zu kontaktierenden Servers und die Länge der
Adresse. Als Adresse verwenden wir nicht die in der Deklaration von connect()
angegebene, allgemein gehaltene Struktur sockaddr, sondern die speziell auf IPVerbindungen abgestimmte Variante sockaddr in:
struct sockaddr_in
{
sa_family_t
unsigned short int
struct in_addr
unsigned char
}
sin_family;
sin_port;
sin_addr;
__pad[8];
/*
/*
/*
/*
Adressfamilie */
Port-Nummer */
Internet-Adr. */
auffuellen */
172
7 Netzwerkprogrammierung
Vor dem Aufruf der Funktion connect() müssen in diese Struktur die IP-Adresse
und die Port-Nummer des Servers eingetragen werden. Das erste Element gibt dabei die Adressfamilie an; für eine IP-Adresse ist das AF INET. Als zweites Element
enthält die Struktur die Port-Nummer des Servers. Diese besteht aus zwei Bytes
(unsigned short int), deren Reihenfolge (High Byte, Low Byte) im InternetProtokoll festgelegt ist und von der Architektur des Rechners abweichen kann.
Um die Port-Nummer korrekt in die Struktur einzutragen, sollte unbedingt die
Funktion htons() verwendet werden. Analog dazu sollte auch die IP-Adresse
mit Hilfe der Funktion
int inet_aton(char *text, struct in_addr *addr);
in die Struktur kopiert werden, wobei die Zeichenkette text die IP-Adresse in
der üblichen Schreibweise (z. B. 127.0.0.1“) enthält, die von inet aton() in
”
binäre Form umgewandelt wird. Ist die angegebene IP-Adresse fehlerhaft, liefert
inet aton() eine 0 als Rückgabewert.
Um dieser abstrakten, theoretischen Beschreibung etwas Anschauung zu verleihen, soll folgendes Beispiel den Verbindungsaufbau zu Port 80 der IP-Adresse
127.0.0.1 (localhost) demonstrieren:
#
#
#
#
include
include
include
include
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
int sock_fd;
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(80);
inet_aton("127.0.0.1", &(server_addr.sin_addr));
err = connect(sock_fd, (struct sockaddr *)&server_addr,
sizeof(struct sockaddr_in));
if (err == -1)
perror("connect() failed");
Mit dem Rahmenprogramm“ von Seite 170 kombiniert, ergibt sich daraus ein
”
vollständiges Programm zum Öffnen eines Sockets und Verbinden des Sockets
mit einem Web-Server (falls eingerichtet). Da jedoch keine Daten mit dem Server
ausgetauscht werden, ist dieses Programm nicht besonders nützlich – es kann allenfalls feststellen, ob ein entsprechendes Server-Programm eingerichtet und aktiviert wurde.
7.2 Der TCP/IP-Client
173
7.2.2 Ein Universal“-Client
”
Das folgende Programm öffnet einen Socket und baut die Verbindung zu einem
Server auf. Es erwartet als Kommandozeilenparameter die IP-Adresse und die
Port-Nummer des Servers. Nach erfolgreichem Verbindungsaufbau arbeitet das
Programm ähnlich einem Terminal-Programm: (Tastatur-)Eingaben werden an
den Server geschickt; die Antwort des Servers wird im Shell-Fenster ausgegeben.
Mit Ctrl-D kann die Eingabe – und somit auch das Programm – beendet werden.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
connect.c - einfacher Netzwerk-Client
*/
#
#
#
#
#
#
#
include
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<string.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
int main(int argc, char *argv[])
{
static char buffer[256];
int sock_fd, err, length, port;
struct sockaddr_in server_addr;
fd_set input_fdset;
if (argc != 3)
{
fprintf(stderr, "Usage: connect ip-addr port\n");
return(1);
}
if (sscanf(argv[2], "%d", &port) != 1)
{
fprintf(stderr, "connect: bad argument ’%s’\n",
argv[2]);
return(1);
}
sock_fd = socket(PF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
{
perror("connect: Can’t create new socket");
174
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
7 Netzwerkprogrammierung
return(1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
err = inet_aton(argv[1], &(server_addr.sin_addr));
if (err == 0)
{
fprintf(stderr, "connect: Bad IP-Address ’%s’\n",
argv[1]);
return(1);
}
err = connect(sock_fd, (struct sockaddr *)&server_addr,
sizeof(struct sockaddr_in));
if (err == -1)
{
perror("connect: connect() failed");
return(1);
}
while (1)
{
FD_ZERO(&input_fdset);
FD_SET(STDIN_FILENO, &input_fdset);
FD_SET(sock_fd, &input_fdset);
if (select(sock_fd+1, &input_fdset, NULL, NULL, NULL)
== -1)
perror("connect: select() failed");
if (FD_ISSET(STDIN_FILENO, &input_fdset))
{
if (fgets(buffer, 256, stdin) == NULL)
{
printf("connect: Closing socket.\n");
break;
}
length = strlen(buffer);
send(sock_fd, buffer, length, 0);
}
else
{
length = recv(sock_fd, buffer, 256, 0);
if (length == 0)
{
7.2 Der TCP/IP-Client
81
82
83
84
85
86
87
88
89
90
175
printf("Connection closed by remote host.\n");
break;
}
write(STDOUT_FILENO, buffer, length);
}
}
close(sock_fd);
return(0);
}
Nach dem Auswerten der Kommandozeilenparameter in den Zeilen 20 bis 31
wird in Zeile 33 ein Socket geöffnet, wiederum mit der Protokollfamilie IP
(PF INET) und für verbindungsorientierte Kommunikation (SOCK STREAM). In
den Zeilen 40 bis 42 werden IP-Adresse und Port-Nummer des Servers in die
Adress-Struktur für den Verbindungsaufbau eingetragen.
Die Verbindung zum Server-Programm erfolgt in den Zeilen 50 und 51 mit dem
Aufruf der Funktion connect(). In der while()-Schleife (Zeile 58 bis 86) wird
mit der Funktion select() auf Daten von stdin (Tastatur) und vom adressierten
Server gewartet (siehe auch Seite 110). Die Tastatureingaben werden mit fgets()
zeilenweise gelesen und mit send() zum Server-Programm übertragen (Zeile 68
bis 74). Daten, die der Socket empfängt, werden mit recv() eingelesen und mit
write() ausgegeben (Zeile 78 bis 84).
Zum Test und zur Demonstration soll eine Verbindung zum FTP-Server aufgebaut
und dessen Online-Hilfe aufgerufen werden. Die Benutzereingaben sind schräg
dargestellt – vorausgesetzt, der FTP-Server ist wie in Abschnitt 7.1.2 beschrieben
aktiviert (Benutzereingaben sind schräg dargestellt):
> connect 127.0.0.1 21
220 toshi.at-home FTP server (Version
6.2/OpenBSD/Linux-0.11)
help
214- The following commands are recognized (*
unimplemented).
USER
PORT
STOR
MSAM*
RNTO
NLST
PASS
PASV
APPE
MRSQ*
ABOR
SITE
ACCT*
TYPE
MLFL*
MRCP*
DELE
SYST
SMNT*
STRU
MAIL*
ALLO
CWD
STAT
REIN*
MODE
MSND*
REST
XCWD
HELP
QUIT
RETR
MSOM*
RNFR
LIST
NOOP
214 Direct comments to ftp-bugs@toshi.at-home.
quit
221 Goodbye.
Connection closed by remote host.
MKD
XMKD
RMD
XRMD
PWD
XPWD
CDUP
XCUP
STOU
SIZE
MDTM
176
7 Netzwerkprogrammierung
Neben dem FTP-Server gibt es noch andere Dienste, mit denen Sie Klartext“ spre”
chen können: Versuchen Sie einmal, den SMTP-Dienst (Port 25) oder den UserInformationsdienst (Port 79) zu kontaktieren. Bei Letzterem müssen Sie entweder
einen Benutzernamen eingeben oder einfach RETURN drücken.
Je nach Übertragungsmedium (Ethernet oder analoges Modem) gibt es
unterschiedliche Obergrenzen für die mit einem send()-Aufruf übertragenen
Daten. Es kann also sein, dass send() nicht alle angegebenen Daten überträgt.
Der Rückgabewert von send() liefert die Anzahl der tatsächlich gesendeten
Bytes. Um sicherzugehen, dass alle Bytes zum Server übertragen werden, muss
der Rückgabewert mit der zu sendenden Anzahl an Bytes verglichen und die
Differenz ggf. mit einem zweiten send()-Aufruf übertragen werden.
7.2.3 Rechnernamen in IP-Adressen umwandeln
Bislang haben wir das Ziel immer in Form einer IP-Adresse angegeben, doch diese
ist ja nur in seltenen Fällen bekannt. Meistens kennt man den Rechnernamen oder
den Domain-Namen z. B. in der Form www.hanser.de“.
”
Zur Auflösung des Domain-Namens in eine IP-Adresse – ggf. unter Zuhilfenahme
eines Domain Name Servers – dient die Funktion gethostbyname():
struct hostent *gethostbyname(char *name);
die als Rückgabewert einen Zeiger auf die Struktur hostent liefert:
struct
{
char
char
int
int
char
};
hostent
*h_name;
**h_aliases;
h_addrtype;
h_length;
**h_addr_list;
/*
/*
/*
/*
/*
official name of host */
alias list */
host address type */
length of address */
list of addresses */
Neben den Elementen dieser Struktur ist in den Include-Dateien auch das Element
h addr definiert:
#define h_addr h_addr_list[0]
h addr zeigt somit auf den ersten Eintrag der list of addresses“ h addr list.
”
Dieser ist in der Struktur zwar als Zeiger auf char deklariert, zeigt aber im Falle
einer IP-Adresse auf eine Struktur vom Typ struct in addr, wie sie als drittes
Element in der bereits beschriebenen Struktur sockaddr in enthalten ist.
7.2 Der TCP/IP-Client
177
Das folgende Programm erwartet als Kommandozeilenparameter einen Domainoder Rechnernamen und ermittelt die dazugehörige IP-Adresse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
ip-lookup.c - IP-Adresse einer Domain holen
*/
#
#
#
#
include
include
include
include
<stdio.h>
<string.h>
<arpa/inet.h>
<netdb.h>
int main(int argc, char *argv[])
{
struct hostent *host;
struct in_addr *host_ip;
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
{
fprintf(stderr, "Usage: ip-lookup domain-name\n");
return(1);
}
host = gethostbyname(argv[1]);
if (host == NULL)
{
herror("connect2: Can’t get IP-address");
return(1);
}
host_ip = (struct in_addr *) host->h_addr;
printf("Hostname:\t%s\n", host->h_name);
printf("IP-Address:\t%s\n", inet_ntoa(*host_ip));
return(0);
}
Das Programm übergibt den Kommandozeilenparameter (also den DomainNamen) an die Funktion gethostbyname(), diese liefert dann die zugehörige
hostent-Struktur (Zeile 21). In den Zeilen 29 und 30 wird dann der offizielle“
”
Name der Domain sowie ihre IP-Adresse ausgegeben. Da die Struktur hostent
die IP-Adresse nur in binärer Form enthält, wird diese mit Hilfe der Funktion
inet ntoa() zuvor in eine Zeichenkette umgewandelt.
178
7 Netzwerkprogrammierung
Sollten Sie einen Internet-Zugang haben und gerade online sein, können Sie mit
dem Programm ip-lookup die IP-Adressen beliebiger Domains im Internet erfragen:1
> ip-lookup www.hanser.de
Hostname:
www.hanser.de
IP-Address:
213.183.13.138
Bitte beachten Sie, dass die Struktur hostent bei weiteren Aufrufen von
gethostbyname() unter Umständen überschrieben wird. Daher sollten die
benötigten Daten (beispielsweise die IP-Adresse) für die weitere Verwendung –
wie den Verbindungsaufbau mit connect() – in eine lokale Variable kopiert werden:
struct hostent *server;
struct in_addr *server_ip;
struct sockaddr_in server_addr;
server = gethostbyname("www.hanser.de");
server_ip = (struct in_addr *) server->h_addr;
server_addr.sin_addr.s_addr = server_ip->s_addr;
7.3 Server-Programme
Nachdem gezeigt wurde, wie ein TCP/IP-Client programmiert wird, soll in den
folgenden Abschnitten die Arbeitsweise eines entsprechenden Server-Programms
erläutert werden.
7.3.1 Die Funktionsweise eines Servers
Ebenso wie jeder Client benötigt auch ein Server-Programm zunächst einen
Socket als Basis für die Netzwerkkommunikation. Die anschließenden Schritte
sind jedoch ganz anders als bei einem Client-Programm; Abbildung 7.7 zeigt den
prinzipiellen Ablauf mit den zugehörigen Funktionen.
Hier ist die Beschreibung der einzelnen Funktionsaufrufe im Detail:
int bind(int sock_fd, struct sockaddr *my_addr,
socklen_t addrlen);
Der bind()-Aufruf ist ähnlich dem connect()-Aufruf eines Client-Programms,
mit dem Unterschied, dass bind() als zweiten Parameter die eigene Adresse (IPAdresse + Port-Nummer) erwartet. War der Aufruf erfolgreich, liefert die Funk1
Es gibt natürlich bereits ein Tool unter Linux, das dies (und noch mehr) kann: nslookup.
7.3 Server-Programme
179
socket()
Öffnen eines Sockets
?
bind()
Verknüpfen des Sockets mit
einem IP-Port des Rechners
?
listen()
Auf ein connect() eines
Client-Programms warten
?
accept()
Annehmen der Verbindungsanfrage des Client-Programms
?
send(), recv()
Datenaustausch
Abbildung 7.7: Prinzipieller Ablauf eines TCP/IP-Servers
tion eine 0 als Rückgabewert. Im Fehlerfall – beispielsweise wenn der Port bereits
belegt ist – gibt bind() eine –1 zurück.
Um einen Socket an einen Port zu binden“, muss das Programm (je nach
”
Portnummer) ggf. über root-Rechte verfügen!
Nach Beendigung des Programms bleibt der Port noch eine Zeit lang belegt“
”
Der nächste erforderliche Funktionsaufruf ist listen():
int listen(int sock_fd, int backlog);
Der Parameter backlog gibt an, wie lang die Warteschlange mit connect()Anforderungen an diesem Port maximal werden kann. Auch die Funktion
listen() gibt bei einem Fehler –1 zurück, sonst 0. Zur Annahme einer Verbindung von einem Client ist schließlich noch die Funktion accept() erforderlich:
int accept(int sock_fd, struct sockaddr *addr,
socklen_t *addrlen);
Die Funktion accept() wartet, bis die connect()-Anforderung eines ClientProgramms eintrifft. Dann trägt accept() die Adresse des anklopfenden“ Cli”
180
7 Netzwerkprogrammierung
ents in die Adress-Struktur ein, die als zweiter Parameter angegeben ist. Die Variable, auf die der dritte Parameter zeigt, muss vor dem accept()-Aufruf mit
der Länge der Adress-Struktur initialisiert werden. Nach dem Funktionsaufruf
enthält die Variable die tatsächliche Länge der Client-Adressinformation.
accept() liefert als Rückgabewert den Dateideskriptor eines neuen Sockets,
bzw. –1 im Fehlerfall. Dieser neue Socket stellt den Kommunikationskanal zu
dem Client dar, dessen Verbindungsanforderung angenommen wurde. Die Kommunikation mittels send() und recv() findet also nicht über den zunächst mit
socket() geöffneten Socket statt, sondern über den von accept() gelieferten
Socket. Indem für jede mit accept() angenommene Verbindung ein eigener
Socket geöffnet wird, kann der Server mit mehreren Clients parallel kommunizieren – sonst wären Webserver ja undenkbar. Für gewöhnlich wird für jede dieser Verbindungen ein eigener Kind-Prozess gestartet, während der Eltern-Prozess
erneut mit accept() in Bereitschaft geht, weitere Verbindungen aufzubauen.
7.3.2 Ein interaktiver TCP/IP-Server
Das folgende Server-Programm stellt das Pendant zu dem TCP/IP-Client aus Abschnitt 7.2.2 dar. Als Kommandozeilenparameter muss die Port-Nummer angegeben werden, unter der das Server-Programm seinen Dienst anbietet. Die PortNummer darf natürlich nicht bereits von einem anderen Dienst belegt sein.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
server.c - interaktiver Netzwerk-Server
*/
#
#
#
#
#
#
#
#
include
include
include
include
include
include
include
include
<stdio.h>
<string.h>
<stdlib.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
void err_exit(char *message)
{
perror(message);
exit(1);
}
int main(int argc, char *argv[])
{
static char buffer[256];
7.3 Server-Programme
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
int sock_fd, client_fd, port, err, length;
socklen_t addr_size;
struct sockaddr_in my_addr, client_addr;
fd_set input_fdset;
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
{
fprintf(stderr, "Usage: server port\n");
return(1);
}
if (sscanf(argv[1], "%d", &port) != 1)
{
fprintf(stderr, "server: Bad port number.\n");
return(1);
}
/*--- socket() ---*/
sock_fd = socket(PF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
err_exit("server: Can’t create new socket");
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = INADDR_ANY;
/*--- bind() ---*/
err = bind(sock_fd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr_in));
if (err == -1)
err_exit("server: bind() failed");
/*--- listen() ---*/
err = listen(sock_fd, 1);
if (err == -1)
err_exit("server: listen() failed");
/*--- accept() ---*/
addr_size = sizeof(struct sockaddr_in);
client_fd = accept(sock_fd,
(struct sockaddr *)&client_addr, &addr_size);
if (client_fd == -1)
err_exit("server: accept() failed");
printf("I’m connected from %s\n",
inet_ntoa(client_addr.sin_addr));
181
182
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
7 Netzwerkprogrammierung
while (1)
{
FD_ZERO(&input_fdset);
FD_SET(STDIN_FILENO, &input_fdset);
FD_SET(client_fd, &input_fdset);
if (select(client_fd+1, &input_fdset, NULL, NULL,
NULL) == -1)
err_exit("server: select() failed");
if (FD_ISSET(STDIN_FILENO, &input_fdset))
{
if (fgets(buffer, 256, stdin) == NULL)
{
printf("server: Closing socket.\n");
break;
}
length = strlen(buffer);
send(client_fd, buffer, length, 0);
}
else
{
length = recv(client_fd, buffer, 256, 0);
if (length == 0)
{
printf("Connection closed by remote host.\n");
break;
}
write(STDOUT_FILENO, buffer, length);
}
}
close(client_fd);
close(sock_fd);
return(0);
}
Nach Auswertung der Kommandozeilenparameter in Zeile 28 bis 38 wird in Zeile
40 zunächst ein Socket geöffnet. In den Zeilen 44 bis 46 wird dann die AdressStruktur initialisiert. Durch die Angabe der Konstanten INADDR ANY für die IPAdresse wird der Socket beim folgenden bind()-Aufruf (Zeile 49) automatisch
mit allen IP-Adressen des Computers1 verknüpft. Ist dies nicht erwünscht, muss
hier explizit die IP-Adresse angegeben werden, unter der der Dienst eingerichtet
werden soll.
1
Ist der Computer mit mehreren Netzwerk-Interfaces ausgestattet – beispielsweise eine Netzwerkkarte und ein WLAN-Interface –, haben diese in der Regel unterschiedliche IP-Adressen.
7.3 Server-Programme
183
In den Zeilen 55 bis 64 folgen die Funktionsaufrufe listen() und accept(), mit
denen die Kontaktaufnahme“ durch einen Client vorbereitet wird. Nach einem
”
connect() durch ein Client-Programm kehrt die Funktion accept() mit dem
Dateideskriptor des neuen Sockets zurück, und die Struktur client addr wurde
mit der IP-Adresse und Port-Nummer des Client-Programms gefüllt. Die Funktion inet ntoa() wandelt die binäre IP-Adresse in eine Zeichenkette um (Zeilen
65 und 66).
Wie bereits bei dem in Abschnitt 7.2.2 vorgestellten TCP/IP-Client ermöglicht
auch hier die while()-Schleife in den Zeilen 68 bis 96 die bidirektionale Kommunikation über den Socket.
Das Belegen eines reservierten Ports1 mit einem Dienst erfordert root-Rechte. Damit das Programm trotzdem von einem normalen“ Benutzer ausgeführt werden
”
kann, sind neben dem Kompilieren weitere Schritte erforderlich (vgl. auch Abschnitt 9.1.1):
> gcc server.c -o server
> su
Kennwort:
# chown root server
# chmod a+s server
# exit
Jetzt kann das Programm mit Angabe einer (freien) Port-Nummer gestartet werden. Zum Test soll hier ein HTTP-Server vorgetäuscht“ werden – Voraussetzung
”
ist, dass nicht bereits ein solcher Server (z. B. der Apache Webserver) läuft. Als
Client-Programm dient ein Browser, z. B. Firefox:
> server 80
Jetzt firefox http://localhost/“ in einem anderen Fenster aufrufen.
”
I’m connected from 127.0.0.1
GET / HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; U; Linux i686 (x86 64); en-US;
rv:1.7.10) Gecko/20050715 Firefox/1.0.6 SUSE/1.0.6-16
Accept: text/xml,application/xml,application/xhtml+xml,
text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
1
Die Organisation IANA vergibt und registriert Portnummern für bestimmte Dienste wie z. B. Port 80
für HTTP. Unregistrierte Ports wie beispielsweise 30.000 können ohne besondere Benutzerrechte belegt werden.
184
7 Netzwerkprogrammierung
An dieser Stelle können Sie das Programm mit Ctrl-D beenden oder eine HTTPAntwort eingeben, was jedoch relativ mühsam ist und daher echten“ Webser”
vern überlassen werden sollte. Alternativ kann das Server-Programm natürlich
auch mit dem interaktiven Client-Programm aus Abschnitt 7.2.2 kommunizieren.
Auf diese Weise lässt sich eine Chat“-Verbindung aufbauen, über die sich zwei
”
Benutzer miteinander unterhalten können.
7.3.3 Ein kleiner Webserver
Der Nutzen eines interaktiven TCP/IP-Servers, wie er im vorigen Abschnitt dargestellt wurde, ist relativ eingeschränkt. Typische Server-Programme erlauben
eine Kommunikation mit mehreren Client-Programmen gleichzeitig. Wie man
dies realisiert, soll im Folgenden anhand eines kleinen HTTP-Servers demonstriert werden.
HTTP-Grundlagen
Eine typische HTTP-Anfrage eines Browsers (Firefox) war ja bereits im vorherigen Abschnitt zu sehen. Viele der Angaben, die der Browser seiner Anfrage mit
auf den Weg gibt, werden wir nicht benötigen und auch nicht auswerten. Der
grundsätzliche Aufbau einer HTTP-Anfrage und einer HTTP-Antwort sind in Abbildung 7.8 dargestellt.
HTTP-Anfrage:
HTTP-Antwort:
Anforderungszeile
Antwortzeile
Kopfzeile 1 (optional)
Kopfzeile 1 (optional)
Kopfzeile 2 (optional)
..
.
Kopfzeile 2 (optional)
..
.
Kopfzeile n (optional)
Kopfzeile n (optional)
<Leerzeile>
<Leerzeile>
Anfrage-Körper (optional)
Antwort-Körper (optional)
Abbildung 7.8: Prinzipieller Aufbau von HTTP-Anfragen und -Antworten
Eine HTTP-Anforderungszeile besteht dabei aus drei, durch Leerzeichen getrennte Elemente: Methode, Pfad, Protokoll. Beispiel:
GET /index.html HTTP/1.0
7.3 Server-Programme
185
Mögliche Methoden sind:
GET – Daten (z. B. HTML-Datei) vom Server anfordern.
HEAD – Nur die Kopfzeilen vom Server anfordern. Damit kann beispielsweise
die Länge der Daten und das Format erfragt werden, ohne die Daten selbst zu
übertragen.
POST – Die Inhalte eines HTML-Formulars an den Server übertragen und die
Antwort anfordern.
In der Regel enthält eine HTTP-Anfrage mindestens eine Kopfzeile der Form:
Host: www.hanser.de
Ab HTTP 1.1 ist diese Kopfzeile Pflicht. Sie ermöglicht es, mehrere WWWAdressen auf einem Server (also mit gleicher IP-Adresse) zu verwalten, da der
Server an der Host-Angabe erkennen kann, auf welche WWW-Adresse sich der
in der Anforderungszeile angegebene Pfad bezieht.
Die Methoden GET und HEAD benötigen keinen Anfrage-Körper (Body), während
er bei der Methode POST mit den Inhalten des HTML-Formulars gefüllt ist. In
diesem Fall ist mindestens eine weitere Kopfzeile erforderlich, die die Länge des
Anfrage-Körpers angibt.
Die Antwortzeile des Servers enthält – wie die Anforderungszeile – drei Elemente.
Hier sind es: Protokoll, Status, Textmeldung. Beispiel:
HTTP/1.0 200 OK
oder
HTTP/1.0 404 Not Found
Die möglichen Statuswerte sind der detaillierten Protokollbeschreibung zu entnehmen, wie sie z. B. im Internet zu finden ist. Mit den beiden hier angegebenen
Werten kennen Sie aber bereits die wichtigsten.
Ein Server, der etwas auf sich hält, sollte danach mindestens zwei Kopfzeilen ausgeben: die Art des Anwort-Körpers und dessen Länge, z. B.:
Content-type: text/html
Content-length: 1374
Nach einer Leerzeile (CR + LF!) folgt dann der eigentliche Körper, also beispielsweise die HTML-Datei, die JPEG-Grafik, ...
Hier das Ganze (HTTP-Anfrage und -Antwort) an einem Beispiel:
Browser:
GET /index.html HTTP/1.0
Host: www.kein-inhalt.de
Leerzeile
186
7 Netzwerkprogrammierung
Server:
HTTP/1.0 200 OK
Content-type: text/html
Content-length: 38
Leerzeile
<html><body>Leere Seite.</body></html>
Eine ausführlichere Beschreibung des HTTP-Standards finden Sie im Internet
z. B. unter [14].
Das Programm
Die Beschreibung des HTTP war zwar sehr knapp gehalten und auf das Notwendigste beschränkt, diese Informationen reichen aber aus, um damit den folgenden
kleinen HTTP-Server zu programmieren.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
webserver.c - minimalistischer HTTP-Server
*/
#
#
#
#
#
#
#
#
#
#
include
include
include
include
include
include
include
include
include
include
#
#
#
#
define
define
define
define
<stdio.h>
<string.h>
<unistd.h>
<stdlib.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
<sys/stat.h>
<signal.h>
MY_PORT 80
N_CONNECTIONS 20
HTML_PATH "."
DEFAULT_FILE "index.html"
void err_exit(char *message)
{
perror(message);
exit(1);
}
int get_line(int sock_fd, char *buffer, int length)
{
int i;
7.3 Server-Programme
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
i = 0;
while ((i < length-1) &&
(recv(sock_fd, &(buffer[i]), 1, 0) == 1))
if (buffer[i] == ’\n’)
break;
else
i++;
if ((i > 0) && (buffer[i-1] == ’\r’))
i--;
buffer[i] = ’\0’;
return(i);
}
int is_html(char *filename)
{
if (strcmp(&(filename[strlen(filename)-5]), ".html")
== 0)
return(1);
if (strcmp(&(filename[strlen(filename)-4]), ".htm")
== 0)
return(1);
return(0);
}
size_t file_size(char *filename)
{
struct stat file_info;
if (stat(filename, &file_info) == -1)
return(0);
return(file_info.st_size);
}
void http_service(int client_fd)
{
char buffer[256], cmd[8], url[128], *filename;
int length;
FILE *stream;
if (get_line(client_fd, buffer, 256) == 0)
return;
if (sscanf(buffer, "%7s %127s", cmd, url) < 2)
return;
while (get_line(client_fd, buffer, 256) > 0);
187
188
7 Netzwerkprogrammierung
75
if ((strcmp(cmd, "GET") != 0)
76
&& (strcmp(cmd, "HEAD") != 0))
77
return;
78
79
filename = &(url[1]);
80
if (strlen(filename) == 0)
81
filename = DEFAULT_FILE;
82
83
if ((stream = fopen(filename, "r")) == NULL)
84
{
85
send(client_fd, "HTTP/1.0 404 Not Found\r\n"
86
"Content-type: text/html\r\n"
87
"Content-length: 91\r\n\r\n"
88
"<html><head><title>Error</title></head>"
89
"<body><hr><h2>File not found.</h2><hr>"
90
"</body></html>", 162, 0);
91
return;
92
}
93
send(client_fd, "HTTP/1.0 200 OK\r\n", 17, 0);
94
95
if (is_html(filename))
96
send(client_fd, "Content-type: text/html\r\n", 25, 0);
97
sprintf(buffer, "Content-length: %ld\r\n\r\n",
98
file_size(filename));
99
send(client_fd, buffer, strlen(buffer), 0);
100
if (strcmp(cmd, "GET") == 0)
101
while (!feof(stream))
102
{
103
length = fread(buffer, 1, 256, stream);
104
if (length > 0)
105
send(client_fd, buffer, length, 0);
106
}
107
fclose(stream);
108
return;
109
}
110
111 /*--------------- Hauptprogramm ---------------*/
112
113 int main()
114
{
115
int sock_fd, client_fd, err, pid;
116
struct sockaddr_in my_addr, client_addr;
117
socklen_t addr_size;
118
7.3 Server-Programme
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
189
sock_fd = socket(PF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
err_exit("webserver: Can’t create new socket");
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MY_PORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
err = bind(sock_fd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr_in));
if (err == -1)
err_exit("webserver: bind() failed");
setuid(getuid());
err = listen(sock_fd, N_CONNECTIONS);
if (err == -1)
err_exit("webserver: listen() failed");
if (chdir(HTML_PATH) != 0)
err_exit("webserver: Can’t set HTML path");
signal(SIGCHLD, SIG_IGN);
printf("Type Ctrl-C to stop.\n");
while (1)
{
addr_size = sizeof(struct sockaddr_in);
client_fd = accept(sock_fd,
(struct sockaddr *)&client_addr, &addr_size);
if (client_fd == -1)
err_exit("webserver: accept() failed");
if ((pid = fork()) == -1)
{
fprintf(stderr, "webserver: fork() failed.\n");
return(1);
}
else if (pid == 0)
/* Kind-Prozess */
{
close(sock_fd);
http_service(client_fd);
shutdown(client_fd, SHUT_RDWR);
190
163
164
165
166
167
168
169
170
7 Netzwerkprogrammierung
close(client_fd);
return(0);
}
close(client_fd);
}
return(0);
/* wird nie erreicht */
}
Um den Webserver als normaler“ Benutzer starten zu können, muss auch hier
”
wiederum als Besitzer des Programms root“ eingestellt und das s“-Bit gesetzt
”
”
werden (vgl. Abschnitt 9.1.1). Danach kann man das Programm ohne Kommandozeilenparameter aufrufen. Der Server läuft nun so lange, bis der Prozess durch
Eingabe von Ctrl-C oder durch einen kill-Befehl beendet wird. Um dem Webserver eine HTML-Seite zu entlocken, sollten Sie entweder eine HTML-Datei in das
aktuelle Verzeichnis kopieren oder ein Verzeichnis, das HTML-Dateien enthält,
in Zeile 18 als HTML PATH“ eintragen. Als URL geben Sie dann im Browser
”
http://localhost/HTML-Datei“ ein.
”
Betrachten wir zunächst das Hauptprogramm ab Zeile 113. Zu Beginn wird
gemäß dem bereits vorgestellten Schema zunächst ein Socket geöffnet und anschließend mit bind() mit dem Port 80 (Konstante MY PORT) verknüpft. In Zeile 132 erfolgt dann der Aufruf setuid(getuid()). Er bewirkt, dass das Programm nach der Verknüpfung mit dem Port die über das s“-Bit verliehenen root”
Rechte wieder abgibt. Dadurch könnte das Programm auch bei einer Fehlfunktion
keinen ernsthaften Schaden mehr anrichten (siehe auch Abschnitt 7.5). Erst danach erfolgt der listen()-Aufruf, um den Port in Verbindungsbereitschaft zu
bringen.
Mit Hilfe der Funktion signal() wird in Zeile 141 eingestellt, dass Kind-Prozesse
nach Beendigung sofort aus dem Speicher und aus der Prozessliste entfernt werden, statt abzuwarten, bis der Eltern-Prozess den Exit-Status der Kind-Prozesse
abfragt. Da bei unserem Webserver-Programm jede HTTP-Anfrage einen KindProzess startet, würde andernfalls eine Unmenge so genannter Zombie-Prozesse1
erzeugt.
In der while()-Schleife (Zeilen 145 bis 167) werden Verbindungsanforderungen von Client-Programmen (Browsern) mit accept() angenommen und jeweils in einem eigenen Kind-Prozess mit dem Unterprogramm http service()
abgearbeitet. Danach wird die Verbindung zum Client mit Hilfe der Funktion
shutdown() beendet (Zeile 162) und anschließend der Socket geschlossen (Zeile 163).
Die Funktion http service() ist in den Zeilen 64 bis 109 definiert. Sie liest
zunächst die HTTP-Anforderung ein und prüft auf die Kommandos GET“ und
”
1
Das sind Prozesse, die eigentlich beendet sind, aber noch darauf warten, ihren Status zurückzumelden.
7.4 Das User Datagram Protocol (UDP)
191
HEAD“ (andere werden zur Zeit nicht unterstützt). Schlägt das Öffnen der ange”
forderten Datei in Zeile 83 fehl, so wird eine entsprechende HTTP-Fehlermeldung
an den Client zurückgeschickt (Zeile 85 bis 91). Wenn die angegebene Datei existiert, wird eine positive Rückmeldung generiert (Zeile 94) und geprüft, ob es
sich um eine HTML-Datei handelt (Zeile 95). Dies wird dem Client dann durch
die Information Content-type: text/html“ angezeigt. In jedem Fall wird die
”
Länge der Datei mit Hilfe der Kopfzeile Content-length:“ übermittelt. Han”
delte es sich bei der Anforderung um ein HEAD“, ist die Kommunikation an
”
dieser Stelle beendet. Bei einer GET“-Anforderung folgt die Übertragung der ei”
gentlichen Datei (Zeile 101 bis 106).
7.4 Das User Datagram Protocol (UDP)
In den Abschnitten 7.2 bis 7.3.3 haben wir verbindungsorientierte Sockets auf Basis
des Protokolls TCP verwendet. Der Vorteil dabei ist, dass eine gesicherte Verbindung zwischen zwei Netzwerk-Ports hergestellt wird, bei der keine Daten ver”
loren gehen“ oder in falscher Reihenfolge ankommen können. Im Gegensatz dazu besteht beim Versenden von UDP-Paketen keine Garantie, dass diese wirklich
ankommen. Dennoch bietet UDP-basierte Netzwerkkommunikation zwei Eigenschaften, die sehr nützlich oder sogar notwendig sein können:
Der Datenstrom“ wird nicht blockiert, falls ein Paket nicht oder nur fehlerhaft
”
angekommen ist. Dies ist für zeitkritische Anwendungen wie Internettelefonie
wichtig.
UDP erlaubt es, Nachrichten an mehrere (Multicast) oder an alle (Broadcast)
Netzwerkteilnehmer zu schicken. Das ist mit TCP nicht möglich!
Aus diesem Grund zeigen wir in den folgenden Abschnitten, wie die Kommunikation über UDP funktioniert und wie Broadcast- und Multicast-Nachrichten
versendet werden.
7.4.1 UDP-Nachrichten senden
Bereits beim Öffnen des Sockets muss festgelegt werden, ob das Protokoll TCP
oder UDP verwendet werden soll (vgl. Abschnitt 7.1.4). Für UDP muss man als
Typ SOCK DGRAM“ angeben:
”
int sock fd = socket(PF INET, SOCK DGRAM, 0);
Bei dem Protokoll UDP werden einzelne Pakete versendet, ohne dass zuvor eine
Verbindung“ zwischen Client und Server aufgebaut wird. Daher entfällt bei der
”
UDP-Kommunikation der Funktionsaufruf connect(). Um das Ziel (IP-Adresse
und Port) beim Versenden eines Pakets angeben zu können, müssen Sie bei UDP
die Funktion
192
7 Netzwerkprogrammierung
ssize_t sendto(int sockfd, void *buff, size_t n, int flags,
struct sockaddr *addr, socklen_t addr_len);
verwenden, die im Gegensatz zu send() den Parameter addr für die Zieladresse enthält. Analog dazu gibt es auch eine Funktion zum Empfangen von UDPPaketen:
ssize_t recvfrom(int sockfd, void *buff, size_t n, int flags,
struct sockaddr *addr, socklen_t addr_len);
Hier dient der Parameter addr als Zeiger auf einen Platzhalter“, der mit den
”
Adressinformationen des Absenders der Nachricht gefüllt wird. Das folgende Beispielprogramm udp-client“ verwendet beide Funktionen, um eine Nachricht an
”
einen UDP-Server zu schicken und eine Antwort vom Server zu empfangen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
udp-client.c - Text an UDP-Server schicken
*/
#
#
#
#
#
#
#
#
#
include
include
include
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<string.h>
<sys/poll.h>
<sys/types.h>
<sys/socket.h>
<sys/time.h>
<netinet/in.h>
<arpa/inet.h>
# define BUF_SIZE 1000
# define TIMEOUT 1000
/* Millisekunden */
int main(int argc, char *argv[])
{
int sock_fd, port, length, err;
struct sockaddr_in server_addr, from_addr;
socklen_t addr_size;
struct pollfd pollfd;
static char buffer[BUF_SIZE];
if (argc != 4)
{
fprintf(stderr,
"Usage: udp-client ip-addr port message\n");
return(1);
7.4 Das User Datagram Protocol (UDP)
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
193
}
if (sscanf(argv[2], "%d", &port) != 1)
{
fprintf(stderr, "udp-client: bad port number ’%s’\n",
argv[2]);
return(1);
}
/*--- Socket öffnen ---*/
sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (sock_fd == -1)
{
perror("udp-client: Can’t create new socket");
return(1);
}
/*--- Zieladresse ---*/
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
err = inet_aton(argv[1], &(server_addr.sin_addr));
if (err == 0)
{
fprintf(stderr, "udp-client: Bad IP-Address ’%s’\n",
argv[1]);
return(1);
}
/*--- Nachricht senden ---*/
length = sendto(sock_fd, argv[3], strlen(argv[3]), 0,
(struct sockaddr *)&server_addr,
sizeof(struct sockaddr));
if (length != strlen(argv[3]))
perror("udp-client: sendto() failed");
/*--- auf Antwort warten ---*/
pollfd.fd = sock_fd;
/* Polling vorbereiten */
pollfd.events = POLLIN | POLLPRI;
err = poll(&pollfd, 1, TIMEOUT);
if (err < 0)
perror("udp-client: poll() failed");
else if (err == 0)
printf("<No answer received.>\n");
else
{
addr_size = sizeof(struct sockaddr_in);
length = recvfrom(sock_fd, buffer, BUF_SIZE-1, 0,
194
75
76
77
78
79
80
81
82
83
84
85
86
87
88
7 Netzwerkprogrammierung
(struct sockaddr *)&from_addr,
&addr_size);
if (length == -1)
perror("udp-client: recvfrom() failed");
else
{
buffer[length] = ’\0’;
printf("Response from %s: %s\n",
inet_ntoa(from_addr.sin_addr), buffer);
}
}
close(sock_fd);
return(0);
}
Nach dem Öffnen des Sockets (Zeile 40) trägt das Programm die als Kommandozeilenparameter angegebene IP-Adresse und Portnummer in die Struktur
server addr ein (Zeile 47 bis 49). An diese Adresse wird dann der als dritter
Kommandozeilenparameter übergebene Text gesendet (Zeile 57 bis 59). Anschließend wartet das Programm maximal 1 Sekunde auf eine Antwort vom Server.
Dies geschieht mit Hilfe der Funktion poll() in den Zeilen 64 bis 66. poll()
arbeitet ähnlich wie die Funktion select(), die unter anderem in unseren TCPBeispielen verwendet und auf Seite 110 beschrieben wurde.
Um das Programm zu testen, können Sie den Netzwerkdienst Echo“ (UDP”
Port 7) verwenden. Dieser Dienst ist im Dämon xinetd“ enthalten und lässt sich,
”
wie in Abbildung 7.9 gezeigt, aktivieren (siehe auch Seite 168).
Der Echo“-Dienst sendet alle empfangenen Pakete an den jeweiligen Absender
”
zurück. In Verbindung mit unserem Beispielprogramm sieht das dann so aus:
> gcc udp-client.c -o udp-client
> ./udp-client 127.0.0.1 7 "Dies ist ein Test."
Response from 127.0.0.1: Dies ist ein Test.
7.4.2 Der UDP-Server
Im Vergleich zu dem in Abbildung 7.7 dargestellten Ablauf von Funktionsaufrufen eines TCP-Servers entfallen beim UDP-Server die Funktionen listen() und
accept(), die man nur für verbindungsorientierte Kommunikation benötigt. Außerdem werden zum Senden und Empfangen von Paketen die oben beschriebenen Funktionen sendto() und recvfrom() verwendet. Das folgende Programm
realisiert einen UDP-Server, der alle empfangenen Pakete als Text ausgibt und
einen Bestätigungstext an den Absender schickt. Empfängt der Server ein UDPPaket mit dem Inhalt quit“, wird das Programm beendet.
”
7.4 Das User Datagram Protocol (UDP)
Abbildung 7.9: Aktivierung des Echo“-Dienstes (UDP)
”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
udp-server.c - Server für UDP-Datagramme
*/
#
#
#
#
#
#
#
include
include
include
include
include
include
include
<stdio.h>
<string.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
# define BUF_SIZE 1000
int main(int argc, char *argv[])
{
int sock_fd, client_fd, port, err, length, stop;
struct sockaddr_in my_addr, client_addr;
socklen_t addr_size;
static char buffer[BUF_SIZE];
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
{
195
196
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
7 Netzwerkprogrammierung
fprintf(stderr, "Usage: udp-server port\n");
return(1);
}
if (sscanf(argv[1], "%d", &port) != 1)
{
fprintf(stderr, "udp-server: Bad port number ’%s’.\n",
argv[1]);
return(1);
}
/*--- Socket öffnen ---*/
sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (sock_fd == -1)
{
perror("udp-server: Can’t create new socket");
return(1);
}
/*--- Socket an Port binden ---*/
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = INADDR_ANY;
err = bind(sock_fd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr_in));
if (err == -1)
{
perror("udp-server: bind() failed");
return(1);
}
stop = 0;
while (!stop)
/*--- so lange, bis ’quit’ empf. */
{
/*--- Paket empfangen ---*/
addr_size = sizeof(struct sockaddr_in);
length = recvfrom(sock_fd, buffer, BUF_SIZE-1, 0,
(struct sockaddr *)&client_addr,
&addr_size);
if (length == -1)
perror("udp-server: recvfrom() failed");
else
{
/*--- Paket ausgeben ---*/
buffer[length] = ’\0’;
printf("Datagram from %s:\n%s\n",
inet_ntoa(client_addr.sin_addr), buffer);
if (strcmp(buffer, "quit") == 0)
7.4 Das User Datagram Protocol (UDP)
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
197
{
strcpy(buffer, "Server stopped.");
stop = 1;
}
else
strcpy(buffer, "Message received.");
/*--- Antwort senden ---*/
length = sendto(sock_fd, buffer, strlen(buffer), 0,
(struct sockaddr *)&client_addr,
sizeof(struct sockaddr));
if (length < strlen(buffer))
perror("udp-server: sendto() failed");
}
}
close(client_fd);
close(sock_fd);
return(0);
}
Das Programm erwartet als Kommandozeilenparameter die Nummer des Ports,
an dem der Server Pakete empfangen soll. Wenn Sie hier eine unregistrierte Portnummer (z. B. 30000) angeben, sollte das Programm auch ohne root-Rechte laufen:
> gcc udp-server.c -o udp-server
> ./udp-server 30000
Wenn Sie jetzt in einem anderen Terminal-Fenster das UDP-Client-Programm auf
den Port 30000 anwenden, erhalten Sie die entsprechenden Empfangsbestätigungen:
> ./udp-client 127.0.0.1
Response from 127.0.0.1:
> ./udp-client 127.0.0.1
Response from 127.0.0.1:
30000 Test
Message received.
30000 quit
Server stopped.
7.4.3 Pakete an alle Teilnehmer senden: Broadcast
Wie bereits oben beschrieben, ist es mit UDP möglich, eine Nachricht an alle Teilnehmer eines lokalen Netzwerks zu schicken.1 Dieses Verfahren wird beispielsweise für die Suche von Diensten im Netzwerk verwendet, wenn die IP-Adresse
des Servers noch nicht bekannt ist.
Das Senden und Empfangen von Broadcast-Paketen geschieht mit den gleichen
Funktionen wie das Übertragen von normalen“ Datagrammen, jedoch mit einer
”
1
Damit solche Nachrichten nicht über das Internet an jeden Computer gehen, der gerade online ist,
werden Broadcast-Pakete von Netzwerkroutern nicht weitergeleitet und bleiben daher auf das Local
Area Network (LAN) beschränkt.
198
7 Netzwerkprogrammierung
speziellen Zieladresse. Jeder Netzwerkschnittstelle, die eine gültige IP-Adresse
besitzt, ist automatisch eine entsprechende Broadcast-Adresse zugeordnet. Die
Schnittstelle empfängt UDP-Pakete, die an diese Adresse geschickt werden, so
als ob sie an die eigentliche IP-Adresse geschickt wurden.
Eine Broadcast-Adresse ist dadurch gekennzeichnet, dass alle Bits des Host-Teils
der IP-Adresse – also der Teil, der bei allen Teilnehmern eines Netzwerks unterschiedlich ist – auf 1 gesetzt sind. Beispiel: Haben die Teilnehmer eines lokalen
Netzwerks Adressen der Form 192.168.0.xxx, so lautet die zugehörige BroadcastAdresse 192.168.0.255. Mit dem Programm ifconfig“ können Sie die IP-Adresse
”
und die zugehörige Broadcast-Adresse aller Netzwerkschnittstellen anzeigen lassen:
> /sbin/ifconfig
eth0 Protokoll:Ethernet Hardware Adresse 00:13:D4:85:4C:89
inet Adresse:192.168.0.1 Bcast:192.168.0.255
Maske:255.255.255.0
inet6 Adresse: fe80::213:d4ff:fe85:4c89/64
Gültigkeitsbereich:Verbindung
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:19 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 Sendewarteschlangenlänge:1000
RX bytes:0 (0.0 b) TX bytes:1575 (1.5 Kb)
Interrupt:225 Basisadresse:0x2000
Darüber hinaus gibt es eine besondere Broadcast-Adresse, die von der IP-Adresse
der Schnittstelle unabhängig ist: 255.255.255.255.
Auch wenn das Senden von Broadcast-Paketen mit den bereits beschriebenen
Funktionen für normale“ Pakete geschieht, ist bei Linux eine zusätzliche Siche”
rung gegen das unbeabsichtigte Verschicken eines Broadcasts eingebaut: Zuerst
muss der Socket für Broadcast-Adressen freigeschaltet“ werden. Dies geschieht
”
mit der Funktion setsockopt() und der Option SO BROADCAST.
Um das Programm aus Abschnitt 7.4.1 in die Lage zu versetzen, Broadcast-Pakete
zu verschicken, müssen Sie ab Zeile 47 folgenden Quelltext einfügen:
int i = 1;
if (setsockopt(sock_fd, SOL_SOCKET, SO_BROADCAST, &i,
sizeof(i)) < 0)
perror("udp-client: Can’t set BROADCAST option.");
Wenn Ihr Rechner in einem lokalen Netzwerk z. B. die Adresse 192.168.0.1 hat,
können Sie mit dem folgenden Aufruf den Echo-Dienst aller Teilnehmer des lokalen Netzwerks adressieren:
7.4 Das User Datagram Protocol (UDP)
199
> udp-client 192.168.0.255 7 Hallo
Response from 192.168.0.1: Hallo
Sollen die Antworten aller Rechner im Netzwerk angezeigt werden, müssen der
poll()- und der recvfrom()-Aufruf so lange wiederholt werden, bis keine Antwort mehr eintrifft, so dass die poll()-Funktion einen TIMEOUT liefert. Auf diese Weise erfahren Sie dann automatisch die IP-Adressen aller Rechner im lokalen
Netzwerk, bei denen der Echo-Dienst aktiviert ist!
7.4.4 Multicast-Sockets
Bei den bisher vorgestellten Adressierungsarten des Internet Protokolls (IP) gab es
entweder die Möglichkeit, eine Nachricht gezielt an einen Teilnehmer zu senden
(Unicast), oder ein Paket an alle Netzwerkteilnehmer des lokalen Netzwerks zu
schicken (Broadcast). Der aktuelle IP-Standard sieht eine weitere Möglichkeit der
Adressierung vor, bei dem eine Nachricht an mehrere, aber nicht alle Teilnehmer
gesendet wird: Multicast.
Multicast-Adressen
Multicast-Adressen liegen – anders als die Broadcast-Adressen – in einem speziellen IP-Adressbereich, der unabhängig von der Adresse der Netzwerkschnittstelle ist: 224.0.0.0 bis 239.255.255.255. Dies bedeutet, dass jeder Empfänger von
Multicast-Paketen so eingerichtet sein muss, dass er neben seiner eigentlichen IPAdresse auch Pakete annimmt, die an eine oder mehrere Multicast-Adressen gerichtet sind!
Wie Sockets für den Empfang von Multicast-Paketen eingerichtet werden, zeigt
der nächste Abschnitt. Aber zunächst zum Senden von Multicast-Nachrichten:
Für ausgehende Pakete muss in der Routing-Tabelle eingetragen sein, über welche
Schnittstelle Multicast-Pakete gesendet werden, z. B. eth0:
> su
Kennwort:
# route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0
Weitere Vorbereitungen sind für das Senden von Nachrichten an MulticastAdressen nicht notwendig. Mit dem oben beschriebenen Programm udp-client“
”
lassen sich nun Multicast-Pakete an das lokale Netzwerk verschicken.
Multicast-Sockets einrichten
Es sei zuerst erwähnt, dass der Linux-Kernel Multicast unterstützen muss, um
einen Multicast-Socket einrichten zu können. Dies lässt sich u. a. wie folgt überprüfen:
200
7 Netzwerkprogrammierung
> cat /proc/config.gz | gunzip | grep MULTICAST
CONFIG IP MULTICAST=y
Um ein UDP-Server-Programm Multicast-fähig zu machen, muss die gewünschte
Multicast-Adresse mit Hilfe der Funktion setsockopt() mit dem Socket verknüpft werden, oder, anders ausgedrückt: Der Socket muss der Multicast-Gruppe
hinzugefügt werden:
# define MCAST_ADDR 224.0.0.1
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(MCAST_ADDR);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
&mreq, sizeof(mreq)) < 0)
{
perror("setsockopt() failed");
return(1);
}
7.4.5 UPnP – Universal Plug And Play
Es gibt ein internationales Gremium, das daran arbeitet, Netzwerke Hot-Plug”
And-Play“-fähig zu machen. So wie beim Anschließen einer USB-Maus an einen
freien USB-Port automatisch die entsprechenden Treiber geladen werden und die
Maus als Eingabegerät eingerichtet wird, soll das Gleiche mit Diensten in einem
Netzwerk möglich sein. Der zu diesem Zweck entwickelte Standard heißt Universal Plug And Play (kurz UPnP) und basiert auf UDP-Multicast, HTTP und XML. Da
auch MicrosoftTM in diesem Gremium vertreten ist, bieten WindowsTM -Rechner
bereits einige Dienste über UPnP an. Auch IP-Kameras verschiedener Hersteller
(z. B. Axis oder Pelco) haben UPnP implementiert, um die Kameras und deren
Funktionsumfang automatisch über das Netzwerk ermitteln zu können.
Um nach UPnP-Diensten im lokalen Netzwerk zu suchen, können Sie die folgende Sequenz an die Multicast-Adresse 239.255.255.250 und Port 1900 schicken:
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 2
ST: ssdp:all
<Leerzeile>
Sie sehen, dass es sich dabei um eine HTTP-Anfrage handelt, doch ist als Methode nicht GET, sondern M-SEARCH angegeben. Die Kopfzeile MX:“ gibt die
”
7.4 Das User Datagram Protocol (UDP)
201
Anzahl der Sekunden an, die der UPnP-Server maximal warten soll, bis er eine
Antwort sendet. Damit soll verhindert werden, dass viele Server gleichzeitig antworten. Mit der Kopfzeile ST:“ wird angegeben, nach welchem Dienst gesucht
”
wird (Search Target). Der Wert ssdp:all“ fordert eine Rückmeldung aller Dien”
ste an, während upnp:rootdevice“ nur eine Antwort pro UPnP-Host liefert.
”
Die im lokalen Netzwerk verfügbaren UPnP-Dienste reagieren auf diese Anforderung mit einer HTTP-Antwort entsprechend Abbildung 7.8, wobei die Antworten
in der Regel nur aus den Kopfzeilen ohne Body bestehen. Die wichtigste Kopfzeile
beginnt mit dem Schlüsselwort Location:“, gefolgt von einem URL (Uniform Re”
source Locator) der Form http://IP-Adresse:Port/Pfad“. Über diesen URL können
”
Sie eine XML-Datei mit einer detaillierten Beschreibung des Dienstes abrufen. Das
folgende Programm fragt auf diese Weise die aktiven UPnP-Dienste ab und gibt
die Antworten aus. Es benötigt keine Kommandozeilenparameter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
upnp-search.c - UPnP-Dienste abfragen
*/
#
#
#
#
#
#
#
#
#
include
include
include
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<string.h>
<sys/poll.h>
<sys/types.h>
<sys/socket.h>
<sys/time.h>
<netinet/in.h>
<arpa/inet.h>
# define BUF_SIZE 20000
# define TIMEOUT 3000
/* Millisekunden */
# define UPNP_ADDR "239.255.255.250"
# define UPNP_PORT 1900
int main()
{
int sock_fd, length, i, err;
struct sockaddr_in server_addr, from_addr;
socklen_t addr_size;
struct pollfd pollfd;
static char buffer[BUF_SIZE];
/*--- Socket öffnen ---*/
sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
202
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
7 Netzwerkprogrammierung
if (sock_fd == -1)
{
perror("upnp-search: Can’t create new socket");
return(1);
}
/*--- Zieladresse ---*/
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(UPNP_PORT);
inet_aton(UPNP_ADDR, &(server_addr.sin_addr));
/*--- Anfrage senden ---*/
strcpy(buffer, "M-SEARCH * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"MAN: \"ssdp:discover\"\r\n"
"MX: 2\r\n"
"ST: ssdp:all\r\n"
"\r\n");
length = sendto(sock_fd, buffer, strlen(buffer), 0,
(struct sockaddr *)&server_addr,
sizeof(struct sockaddr));
if (length != strlen(buffer))
{
perror("upnp-search: sendto() failed");
return(1);
}
/*--- auf Antwort warten ---*/
pollfd.fd = sock_fd;
/* Polling vorbereiten */
pollfd.events = POLLIN | POLLPRI;
i = 0;
while (1)
{
err = poll(&pollfd, 1, TIMEOUT);
if (err < 0)
{
perror("upnp-search: poll() failed");
break;
}
else if (err == 0)
/* Timeout erreicht */
{
if (i == 0)
printf("<No response received.>\n");
break;
}
7.4 Das User Datagram Protocol (UDP)
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
203
else
{
addr_size = sizeof(struct sockaddr_in);
length = recvfrom(sock_fd, buffer, BUF_SIZE-1, 0,
(struct sockaddr *)&from_addr,
&addr_size);
if (length == -1)
perror("upnp-search: recvfrom() failed");
else
{
buffer[length] = ’\0’;
printf("\33[1m---- Response from %s:\33[0m\n%s\n",
inet_ntoa(from_addr.sin_addr), buffer);
}
i = 1;
}
}
close(sock_fd);
return(0);
}
Wenn sich z. B. ein WindowsTM -Rechner als Internet-Gateway im lokalen Netzwerk befindet, werden Sie mehrere UPnP-Antworten ähnlich dem folgenden Beispiel erhalten:
> ./upnp-search
---- Response from 192.168.0.1:
HTTP/1.1 200 OK
ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1
USN:uuid:e21500e7-3d2f-45a2-849c-d64707cb66b3::urn:schemas←֓
-upnp-org:device:InternetGatewayDevice:1
Location:http://192.168.0.1:2869/upnphost/udhisapi.dll?con←֓
tent=uuid:e21500e7-3d2f-45a2-849c-d64707cb66b3
Cache-Control:max-age=1800
Server:Microsoft-Windows-NT/5.1 UPnP/1.0
UPnP-Device-Host/1.0
Ext:
Aus der mit ST:“ beginnenden Zeile können Sie entnehmen, dass es sich um
”
ein InternetGatewayDevice“ handelt. Den hinter Location:“ angegebenen URL
”
”
können Sie beispielsweise mit Firefox öffnen, um die XML-Beschreibungsdatei
des Dienstes darzustellen.
Für weitere Informationen zu UPnP möchten wir auf die entsprechenden Spezifikationen [15] und [16] verweisen.
204
7 Netzwerkprogrammierung
7.5 Noch ein Wort zur Sicherheit
Die beschriebenen Techniken und Programmbeispiele versetzen Sie in die Lage,
über das Internet mit der großen, weiten Welt“ zu kommunizieren. Unglück”
licherweise sind es – neben anderen – genau diese Techniken, die Hackern, Viren und Würmern dabei helfen, ihre weniger harmlosen Absichten zu verfolgen.
Sobald Sie einen Dienst über einen TCP/IP-Port einrichten, könnten ungebetene
Gäste“ versuchen, diesen als Hintertür zu Ihrem System zu missbrauchen. Dies
”
gilt vor allem dann, wenn Sie ohne Firewall eine Verbindung zum Internet herstellen.
Am sichersten ist es daher, wenn Sie Client-Server-Programme zunächst auf
Systemen entwickeln, die keine Verbindung nach draußen“ haben. Außerdem
”
sollten Sie beim Erstellen solcher Programme immer berücksichtigen, dass Ihr
Gegenüber“ möglicherweise andere Anfragen oder Antworten schickt als die
”
von Ihrem Programm erwarteten. Ein beliebter Fehler ist beispielsweise ein nicht
abgefangener Pufferüberlauf: Sendet der entfernte Rechner mehr Zeichen, als in
den Empfangspuffer passen, können dadurch andere Bereiche des Programms
mit Daten des fremden Rechners überschrieben werden.
Wenn Sie Programme zur Netzwerkkommunikation auf einem eigenen kleinen
Rechnernetzwerk testen wollen, sollten Sie – falls nicht ohnehin schon geschehen
– IP-Adressen verwenden, die speziell für private Netzwerke reserviert sind und
in der Regel von einem Router nicht weitergeleitet werden. Dies sind die Adressen 192.168.0.1 bis 192.168.0.254. Weitere Informationen zu den verschiedenen IPAdressbereichen finden Sie z. B. unter [17].
Kapitel 8
Grafische Benutzeroberflächen
Unter Linux gibt es eine ganze Reihe von Anwendungen, die ohne grafische
Benutzerschnittstelle ( GUI“ für Graphical User Interface) auskommen. Im Ab”
schnitt 3.4.2 wurde gezeigt, wie eine komfortable Ausgabesteuerung auch im Terminalfenster möglich ist. Die Bedienung eines Programms lässt sich jedoch oft
durch eine grafische Benutzerschnittstelle deutlich vereinfachen – für manche Anwendungen wie z.B. Grafikprogramme ist sie sogar unabdingbar.
Es stehen unter Linux zahlreiche Funktionsbibliotheken für die Programmierung
von grafischen Oberflächen zur Verfügung; die meisten davon setzen auf das X11System (siehe unten) auf. In den folgenden Abschnitten wird eine dieser Bibliotheken vorgestellt. Als Alternative zur Grafikprogrammierung unter X11 – z.B.
für Systeme mit knappen Ressourcen – wird in Abschnitt 8.3 die Verwendung der
libvga für grafische Anwendungen ohne das X11-System demonstriert.
8.1 Die grafische Oberfläche X11
Das X11-System wurde am Massachusetts Institute of Technology (kurz MIT) in Zusammenarbeit mit der Firma DEC1 als Hardware-unabhängige, grafische Schnittstelle entwickelt. 1988 wurde das MIT X Consortium gegründet, das diese Arbeit
fortführte. Nach Auflösung dieses Konsortiums übernahm zunächst The Open
”
Group“ die Weiterentwicklung, bevor sie an die X.Org“ übertragen wurde.
”
X11 ist als Client-Server-System realisiert – der Server bildet die Schnittstelle zur
Hardware, während der Client mit dem Server kommuniziert, um beispielsweise
Grafikobjekte darzustellen oder zu manipulieren. Das Programm XF86 SVGA2 ist
z.B. ein X11-Server (für SVGA-Grafikkarten), der Window-Manager KDE hinge1
Digital Equipment Corporation
2
Mit Einführung der Version 4 wurden die verschiedenen Programme wie XF86 SVGA durch den einheitlichen Server XFree86 abgelöst.
206
8 Grafische Benutzeroberflächen
gen ein X11-Client. Das X11-System zeichnet aus, dass der Client auf einem anderen Rechner laufen kann als der Server, sofern beide über ein TCP/IP-Netzwerk
verbunden sind. So ist es z.B. möglich, dass ein Linux-PC über das Internet mit
einer Unix-Workstation verbunden ist und auf der Workstation ein Grafikprogramm läuft, das am PC bedient wird.
Unter Linux kommt die freie X11-Variante XFree86 zum Einsatz, die ursprünglich für 80x86-Prozessoren entwickelt wurde und als offener Quelltext verfügbar
ist. Die in dem Paket enthaltene Funktionsbibliothek libX11 bietet eine Vielzahl
von Funktionen zur Manipulation von Grafikobjekten. Dennoch eignet sich diese
Bibliothek allein nicht zum Erstellen von Programmen mit grafischer Benutzerschnittstelle. Es gibt eine Reihe weiterer Bibliotheken, die Funktionen für komplexe Bedienelemente wie beispielsweise Menüs beinhalten. Einige dieser Bibliotheken – auch als Toolkits bezeichnet – sind:
Tk
GTK+ (The Gimp Toolkit)
XView / OpenLook (Nachfolger von SunView)
Qt
Motif (bzw. LessTif, ein Open Source Motif-Ersatz)
Athena Widget
Das Look & Feel“ dieser Bibliotheken ist sehr unterschiedlich, was sich auf das
”
Erscheinungsbild der verschiedenen Programme unter Linux auswirkt.
8.2 Das Toolkit GTK+
Ursprünglich wurde GTK als grafische Benutzerschnittstelle für das GNU Image
”
Manipulation Program“ (GIMP) entwickelt. Daraus entstand schließlich ein eigenständiges Projekt und sehr nützliches Toolkit für Bedienerobflächen.
GTK ist objektorientiert: Fenster, Schaltflächen, Menüs usw. sind Objekte (GTKWidgets), deren interner Aufbau für den Programmierer verborgen bleibt. Um die
Eigenschaften eines solchen Objektes – beispielsweise den Titeltext eines Fensters
– zu verändern, werden spezielle Funktionen der GTK-Bibliotheken auf die Objekte angewendet.
8.2.1 GTK 1.2 versus GTK 2.0
Auf aktuellen Linux-Systemen sind in der Regel zwei Versionen des GTK-Toolkits
installiert: 1.2 und 2.0. Eine der wichtigsten Erweiterungen der Version 2.0 ist die
Unterstützung von Unicode bzw. UTF-8. Damit können Texte mit Zeichen aus den
verschiedensten Sprachen dargestellt werden, während GTK 1.2 bereits mit deutschen Umlauten Probleme hatte.
8.2 Das Toolkit GTK+
207
8.2.2 GTK-Programme übersetzen
Für das Übersetzen von GTK-Programmen müssen mehrere Bibliotheken eingebunden werden. Darüber hinaus müssen dem Compiler zusätzliche Pfade für die
Include-Dateien bekannt gemacht werden. Zu diesem Zweck enthält das GTK 2.0Paket ein nützliches Tool: pkg-config. Mit der Option --libs“ liefert das Pro”
gramm die erforderlichen Compiler-Optionen für das Einbinden der Funktionsbibliotheken; die Option --cflags“ bewirkt, dass die Compiler-Optionen für die
”
zusätzlichen Include-Pfade ausgegeben werden. Außerdem muss man das Paket
angeben, dessen Bibliotheken eingebunden werden sollen, also gtk+-2.0“ für
”
GTK 2.0 und gtk+“ für GTK 1.2. (Die meisten Beispielprogramme in diesem Ka”
pitel lassen sich unter beiden GTK-Versionen übersetzen.)
Natürlich können Sie beide Optionen auch gleichzeitig angeben, um mit einem
Aufruf von pkg-config die für GTK-Applikationen notwendigen CompilerParameter zu erhalten. Durch die Verwendung von Hochkommata (‘) werden die
Ausgaben von pkg-config als Kommandozeilenparameter an den C-Compiler
übergeben:
gcc mein_prog.c -o mein_prog \
‘pkg-config --libs --cflags gtk+-2.0‘
Für das Übersetzen des Quelltextes in ein GTK 1.2-Programm kann alternativ das
Programm gtk-config verwendet werden:
gcc mein_prog.c ‘gtk-config --libs --cflags‘ -o mein_prog
Wenn Sie sich bei der Übersetzung der Beispielprogramme eine Menge Tipparbeit
sparen wollen, geben Sie einfach folgendes Universal-Makefile“ ein:
”
1
#
2
# Universal-Makefile für GTK+2.0-Programme
3
#
4
5
default:
6
@echo "Usage: make filename"
7
@echo "Please use name of target without ’.c’!"
8
9
%:
%.c
10
gcc -Wall $< -o $@ \
11
‘pkg-config --libs --cflags gtk+-2.0‘
Anschließend können Sie die Beispielprogramme einfach mit make Dateiname“
”
übersetzen.
208
8 Grafische Benutzeroberflächen
8.2.3 Ein erstes Beispiel
Als Einstieg in die Programmierung von GTK-Applikationen dient das folgende Programm, das ein einfaches Fenster voreingestellter Größe und ohne weitere
Elemente öffnet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
fenster.c - Ein X11-Fenster öffnen
*/
# include <gtk/gtk.h>
int main(int argc, char *argv[])
{
GtkWidget *window;
gtk_init(&argc, &argv);
/* Optionen auswerten */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_widget_show(window);
gtk_main();
/* Verbindung zu X-Server und
Callback-Mechanismus starten */
return(0);
}
Zum Übersetzen des Quelltextes verwenden wir das oben beschriebene Makefile:
make fenster
Nach dem Starten des Programms erscheint – wie erwartet – ein leeres Fenster.
Zum Beenden des Programms müssen Sie in der Shell, aus der Sie es gestartet
haben, Ctrl-C1 eingeben, denn eine andere Möglichkeit haben wir ja bislang noch
nicht in das Programm eingebaut.
Sehen wir uns das Programm einmal genauer an: In Zeile 9 wird das einzige in diesem Beispiel verwendete GTK-Objekt definiert. Wie alle anderen GTKObjekte ist auch window ein Zeiger auf GtkWidget. Die Initialisierung der GTKFunktionen erfolgt durch den Aufruf von gtk init() in Zeile 11. Diese Funktion wertet auch die Kommandozeilenparameter aus und filtert die GKT-Optionen
heraus. Beispielsweise können Sie mit der Option --name“ den Applikationsna”
men ändern, der im Fenstertitel angezeigt wird (siehe Abbildung 8.1):
./fenster --name "Mein Fenster"
1
bzw. Strg-C, je nach Tastatur.
8.2 Das Toolkit GTK+
209
In Zeile 13 wird das Fenster-Objekt erzeugt und in der nächsten Zeile mit der
Funktion gtk widget show() auf sichtbar“ eingestellt. Durch diesen Funktions”
aufruf wird das Fenster noch nicht dargestellt! Dies geschieht erst mit dem Aufruf
von gtk main() in Zeile 16. Erst damit nimmt das Programm Kontakt mit dem
X11-Server auf und startet den Callback-Mechanismus, den wir in Abschnitt 8.2.4
näher erläutern.
Abbildung 8.1: Änderung des Fenstertitels mit der Option --name“
”
Fenstereigenschaften ändern
Selbstverständlich können Sie den Fenstertitel nicht nur mit Hilfe der Kommandozeilenoption --name“, sondern auch aus dem Programm heraus einstellen.
”
Dazu dient die folgende Funktion:
void gtk_window_set_title(GtkWindow
*window,
const gchar *title);
Beispiel:1
gtk_window_set_title(GTK_WINDOW(window), "Fenstertitel");
Analog lässt sich auch die Größe des Fensters mit Hilfe der entsprechenden Funktion einstellen:
void gtk_window_set_default_size(GtkWindow *window,
gint
width,
gint
height);
1
Bitte beachten Sie das Makro GTK WINDOW! Da alle GTK-Widgets vom Typ GtkWidget sind, sich
dahinter aber unterschiedliche Objekte mit unterschiedlichen Strukturen verbergen, werden solche
Makros eingesetzt, um zu prüfen, ob die Funktion auf das Widget anwendbar ist. Wenn ja, wird eine
Typumwandlung der Art (GtkWindow *)window vorgenommen.
210
8 Grafische Benutzeroberflächen
Wenn Sie möchten, dass die Anwender Ihres Programms das Fenster mit der
Maus nicht größer oder nicht kleiner als die Objekte in dem Fenster ziehen
können, so lässt sich dies ebenfalls einstellen:
void gtk_window_set_policy(GtkWindow *window,
gint
allow_shrink,
gint
allow_grow,
gint
auto_shrink);
Für eine feste Fenstergröße, die optimal auf den Fensterhinhalt angepasst ist, sieht
der Aufruf dieser Funktion so aus:
gtk_window_set_policy(GTK_WINDOW(window),
FALSE, FALSE, TRUE);
Bei unserem ersten Beispielprogramm funktioniert dies allerdings noch nicht,
weil das Fenster noch keine Elemente enthält und dadurch die Größe auf wenige Pixel zusammenschrumpft! Voreingestellt ist übrigens, dass die Fenster zwar
größer, aber nicht kleiner als die enthaltenen Elemente gezogen werden können
(Parameter: FALSE, TRUE, TRUE). Dadurch wird sichergestellt, dass alle Elemente vollständig angezeigt werden, der Benutzer das Fenster aber nach Belieben vergrößern kann.
Schließlich haben Sie noch die Möglichkeit einzustellen, an welcher Stelle das
Fenster beim Öffnen erscheinen soll. Als mögliche Positionen können hier unter anderem die Werte GTK WIN POS MOUSE (mittig unter dem Mauszeiger) oder
GTK WIN POS CENTER (mittig auf dem Bildschirm) gewählt werden.
void gtk_window_set_position(GtkWindow
*window,
GtkWindowPosition position);
Selbstverständlich können Sie die Position des Fensters auch konkret (in Pixel)
vorgeben. Dazu dient die Funktion
void gtk_widget_set_uposition(GtkWidget *widget,
gint x, gint y);
Für das Beispielprogramm könnte der Funktionsaufruf wie folgt lauten:
gtk_widget_set_uposition(window, 50, 30);
8.2.4 Das Callback-Prinzip
Bevor wir Bedienelemente in das Fenster einbauen, kommen wir zu dem Mechanismus, mit dem Aktionen ausgeführt werden, wenn Elemente des Fens-
8.2 Das Toolkit GTK+
211
ters angeklickt werden. Wird ein Eingabeelement – z. B. eine Schaltfläche – bedient, löst dies ein Signal aus, und alle diesem Signal zugeordneten CallbackFunktionen werden aufgerufen. Die Funktion gtk main() startet diesen Mechanismus und wird erst beendet, wenn eine der Callback-Funktionen die Funktion
gtk main quit() aufruft. Daher mussten Sie das Beispielprogramm fenster.c“,
”
in dem noch keine Callback-Funktionen implementiert waren, mit Ctrl-C abbrechen.
Im nächsten Schritt erweitern wir nun das Programm um die Verwendung einer
Callback-Funktion zum Schließen des Fensters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
callback.c - Funktionen beim Schließen
des Fensters einstellen
*/
# include <gtk/gtk.h>
int main(int argc, char *argv[])
{
GtkWidget *window;
gtk_init(&argc, &argv);
/* Optionen auswerten */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_widget_show(window);
gtk_main();
/* Verbindung zu X-Server und
Callback-Mechanismus starten */
return(0);
}
Mit Hilfe der Funktion gtk signal connect() in Zeile 16 verknüpfen wir das
GTK-Objekt des Fensters mit der Funktion gtk main quit(). Beachten Sie, dass
hier eine Funktion als Parameter übergeben wird! Mit dem zweiten Parameter
("delete_event") teilen wir der Funktion gtk signal connect() mit, welches Ereignis (Event) die Callback-Funktion auslösen soll – in diesem Fall das
Löschen“ (Schließen) des Fensters.
”
Wenn Sie den Typ des Events in Zeile 16 in key press event“ ändern, wird das
”
Fenster bei einem Tastendruck geschlossen. Der Event-Typ focus out event“
”
führt dagegen zum Schließen des Fensters, sobald es inaktiv wird.
212
8 Grafische Benutzeroberflächen
Der letzte Parameter der Funktion gtk signal connect() ermöglicht es, zusätzliche beliebige Daten an die Callback-Funktion zu übergeben:
gint gtk_signal_connect(GtkObject
*object,
gchar
*name,
GtkSignalFunc func,
gpointer
func_data);
Als Rückgabewert liefert die Funktion eine ID, eine Art Merker, mit dem sich die
Verknüpfung zwischen dem Objekt und der Callback-Funktion eindeutig identifizieren lässt. Dieser Wert wird benötigt, wenn Sie diese Verknüpfung später wieder
aufheben wollen:
void gtk_signal_disconnect(GtkObject *object, gint id);
Nachdem wir die GTK-Funktion gtk main quit() als Callback verwendet haben, definieren wir nun eine eigene Funktion, wie es später auch für Bedienelemente erforderlich ist:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
callback2.c - Funktionen beim Schließen
des Fensters einstellen
*/
# include <stdio.h>
# include <gtk/gtk.h>
/*----- Callback-Funktion -----*/
gint close_win(GtkWidget *widget, GdkEvent *event,
gpointer data)
{
printf("’delete’-Event ausgelöst.\n");
return(FALSE); /* destroy-Event auslösen */
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window;
gtk_init(&argc, &argv);
/* Optionen auswerten */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
8.2 Das Toolkit GTK+
27
28
29
30
31
32
33
34
35
36
37
38
39
213
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(close_win), NULL);
gtk_signal_connect(GTK_OBJECT(window), "destroy",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_widget_show(window);
gtk_main();
return(0);
}
Unsere Callback-Funktion close win() in Zeile 11 bis 16 erhält als ersten Parameter das Widget, das die Funktion aufgerufen hat – in diesem Fall also das
Fenster-Widget. Der zweite Parameter enthält das Ereignis (Event), das den Aufruf der Funktion ausgelöst hat, hier das Schließen des Fensters. Der dritte und
letzte Parameter enthält die optionalen Daten entsprechend dem vierten Parameter der Funktion gtk signal connect() (siehe oben). Davon machen wir aber
zunächst noch keinen Gebrauch. Beendet wird die Callback-Funktion mit dem
Rückgabewert FALSE. Dadurch wird erreicht, dass das Signal nicht gelöscht und
somit automatisch im Anschluss an das delete“-Event ein destroy“-Event aus”
”
gelöst wird. Mit dem Wert TRUE würde man das Signal löschen und kein weiteres
Event auslösen. Sie können also über den Rückgabewert der Callback-Funktion
entscheiden, ob das Fenster tatsächlich geschlossen wird! Diese Möglichkeit wenden wir in Abschnitt 8.2.9 an.
Im Hauptprogramm wird unsere Funktion close win() mit dem delete“-Event
”
des Fensters verknüpft (Zeile 28 und 29). Zusätzlich wird gtk main quit() als
Callback-Funktion für das destroy“-Event eingerichtet (Zeile 31 und 32).
”
Selbstverständlich ließe sich auch für destroy“ wiederum eine eigene Callback”
Funktion einrichten, falls vor dem Schließen sämtlicher Fenster noch Aktionen
erforderlich sind.
8.2.5 Schaltflächen (Buttons)
Viele GTK-Objekte – z. B. die Fenster – enthalten einen so genannten Container, in
den ein weiteres Objekt – z. B. eine Schaltfläche – eingebaut werden kann. Auf
diese Weise werden die GTK-Objekte verschachtelt oder gestapelt. Im folgenden
Beispielprogramm haben wir eine Schaltfläche zum Beenden des Fensters eingebaut.
1
2
3
/*
button.c - Schaltfläche zum Beenden
*/
214
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
8 Grafische Benutzeroberflächen
# include <stdio.h>
# include <gtk/gtk.h>
/*----- Callback-Funktionen -----*/
gint close_win(GtkWidget *widget, GdkEvent *event,
gpointer data)
{
printf("Bitte klicken Sie auf ’Beenden’.\n");
return(TRUE); /* Signal löschen -> kein ’destroy’ */
}
void quit_proc(GtkWidget *widget, gpointer data)
{
gtk_main_quit();
/* gtk_main() beenden */
return;
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *button;
gtk_init(&argc, &argv);
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(close_win), NULL);
gtk_container_set_border_width(GTK_CONTAINER(window),
30);
/* Schaltfläche erzeugen */
button = gtk_button_new_with_label(" Beenden ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(quit_proc), NULL);
8.2 Das Toolkit GTK+
48
49
50
51
52
53
54
55
56
57
58
215
gtk_container_add(GTK_CONTAINER(window), button);
/* Objekte darstellen */
gtk_widget_show(button);
gtk_widget_show(window);
gtk_main();
return(0);
}
Abbildung 8.2 zeigt das Fenster mit der Schaltfläche. Wenn Sie bei diesem Beispielprogramm versuchen, das Fenster zu schließen, erhalten Sie im Shell-Fenster
die Ausgabe Bitte klicken Sie auf ’Beenden’.“
”
Abbildung 8.2: Das Ergebnis des Programms button.c“
”
In den Zeilen 10 bis 21 werden zunächst wieder zwei Callback-Funktionen definiert: eine für das Schließen“-Symbol des Fensters und die zweite für unse”
re Schaltfläche. Beachten Sie bitte, dass die Parameter beider Funktionen unterschiedlich sind! Das liegt an den unterschiedlichen Signalen, die die CallbackFunktionen auslösen. Durch den Rückgabewert TRUE“ in Zeile 14 erreichen
”
wir bei diesem Beispielprogramm, dass unser Programm durch das Schließen“”
Symbol des Fensters nicht beendet wird, sondern explizit die Schaltfläche Been”
den“ verwendet werden muss.
In dem Hauptprogramm ab Zeile 25 wird nach der Initialisierung von GTK (Zeile
29) das Fenster erzeugt (Zeile 33) und die Callback-Funktion für das Schließen des
Fensters eingestellt (Zeile 35 und 36). In Zeile 38 verändern wir die Eigenschaften
des Containers (siehe oben) des Fensters. In diesem Fall vergrößern wir den Rand
innerhalb des Fensters auf 30 Pixel.
Mit der Funktion gtk button new with label() erzeugen wir in Zeile 43 eine
Schaltfläche (Button) und verknüpfen sie anschließend mit der Callback-Funktion
quit proc() (Zeile 45 und 46). Jetzt muss die Schaltfläche noch in den Container
des Fensters eingebaut werden. Dies geschieht in Zeile 48.
Als letzten Schritt müssen wir noch beide GTK-Widgets – das Fenster und die
Schaltfläche – mit gtk widget show() in den Zeilen 52 und 53 sichtbar machen.
216
8 Grafische Benutzeroberflächen
Wenn – wie in diesem Fall – alle Widgets sofort sichtbar sein sollen, erreichen Sie
dies mit folgendem Aufruf:
gtk widget show all(window);
8.2.6 Hinweistexte (Tipps)
Um dem Anwender die Benutzung eines Programms zu erleichtern, haben Sie
die Möglichkeit, GTK-Objekte mit Hinweistexten oder Tipps zur Benutzung zu
versehen. Diese so genannten Tooltips werden automatisch angezeigt, wenn der
Anwender mit dem Mauszeiger über dem Objekt verharrt (siehe Abbildung 8.3).
Wenn Sie in das Programm button.c“ aus dem vorigen Abschnitt folgende Zeilen
”
einfügen (ab Zeile 49), wird die Schaltfläche mit einem entsprechenden Hinweistext versehen:
GtkTooltips* tooltips = gtk_tooltips_new();
gtk_tooltips_set_tip(tooltips, button,
"Zum Beenden hier klicken.", NULL);
Der vierte Parameter der Funktion gtk tooltips set tip() kann für kontextsensitive Hilfefunktionen verwendet oder – falls nicht benötigt – auf NULL gesetzt werden.
Abbildung 8.3: Schaltfläche mit Hinweistext (Tooltips)
8.2.7 Widgets anordnen
Im vorigen Abschnitt haben wir in den Container eines Fensters eine Schaltfläche eingebaut. Damit ist dieser Container belegt – weitere Elemente lassen sich
nicht hinzufügen! Doch wie bekommt man mehr als ein Objekt in das Fenster,
z. B. mehrere Schaltflächen? Dazu müssen wir die Objekte mit Hilfe spezieller
GTK-Widgets in Zeilen, Spalten oder einer Matrix (Tabelle) anordnen. Die folgenden beiden Funktionen erzeugen eine Zeile bzw. eine Spalte, in die beliebig viele
GTK-Widgets eingefügt werden können:
GtkWidget *gtk_hbox_new(gint homogeneous, gint spacing);
GtkWidget *gtk_vbox_new(gint homogeneous, gint spacing);
8.2 Das Toolkit GTK+
217
Beide Funktionen liefern als Rückgabewert ein GTK-Widget vom Typ GtkBox.
Der Parameter homogeneous kann TRUE oder FALSE sein und legt fest, ob alle Elemente der Zeile oder Spalte die gleiche Breite bzw. Höhe haben sollen. Mit
dem Parameter spacing können Sie einen Abstand (in Pixel) zwischen den Objekten einstellen. Nachdem Sie eine solche Box erzeugt haben, können Sie darin Schaltflächen oder andere Widgets einfügen, wahlweise von links nach rechts
(bzw. oben nach unten) oder von rechts nach links (bzw. unten nach oben) – je
nachdem, welche der beiden folgenden Funktionen Sie verwenden:
void gtk_box_pack_start(GtkBox
*box,
GtkWidget *child,
gint
expand,
gint
fill,
gint
padding);
void gtk_box_pack_end(GtkBox
*box,
GtkWidget *child,
gint
expand,
gint
fill,
gint
padding);
Als ersten Parameter erwarten beide Funktionen die Box, in die ein Element
eingefügt werden soll, gefolgt von dem einzufügenden Element. Der Parameter
expand kann TRUE oder FALSE sein und bestimmt, ob die Box automatisch auf die
Fenstergröße ausgedehnt wird. Mit dem vierten Parameter (fill) können Sie einstellen, ob die Größe des einzufügenden Objekts mit der Größe der Box verändert
werden kann. Auch dieser Parameter kann TRUE oder FALSE sein. Der letzte Parameter (padding) erlaubt es, einen Abstand zwischen den Objekten einzubauen.
Der Wert gibt die Distanz in Pixel an. Um einen Eindruck von den Möglichkeiten
zu bekommen, empfiehlt es sich, einfach mal verschiedene Kombinationen für die
Parameter expand, fill und padding auszuprobieren und dabei auch den Wert
für homogeneous beim Erzeugen der Box zu modifizieren.
Bitte beachten Sie, dass Sie auch eine Box als Element in eine andere Box einfügen
können, so dass beliebige Kombinationen von Zeilen und Spalten möglich sind!
Bevor wir die Verwendung dieser Funktionen anhand eines Beispiels erläutern,
möchten wir das GTK-Widget frame vorstellen, das die Gestaltung und Strukturierung von Elementen in einem Fenster erlaubt.
GtkWidget* gtk_frame_new(const gchar *label);
Dieses Widget enthält einen Container, in den ein anderes Widget – beispielsweise
eine Box – eingefügt werden kann, und zeichnet einen Rahmen darum. Dieser
Rahmen wird mit dem als Argument label angegebenen Text beschriftet.
218
8 Grafische Benutzeroberflächen
Das folgende Beispiel öffnet ein Fenster, das eine vertikale Box enthält, die wiederum eine horizontale Box und eine einzelne Schaltfläche beinhaltet. Die horizontale
Box umfasst vier Schaltflächen (siehe Abbildung 8.4).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
boxes.c - Schaltflächen mit vbox und hbox
anordnen
*/
# include <stdio.h>
# include <gtk/gtk.h>
/*----- Callback-Funktionen -----*/
void quit_proc(GtkWidget *widget, gpointer data)
{
gtk_main_quit();
/* gtk_main() beenden */
return;
}
void button_proc(GtkWidget *widget, gpointer data)
{
printf("%s wurde betätigt.\n", (gchar *)data);
return;
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *frame1, *frame2, *box1, *box2,
*button;
int i;
gchar buffer[4][16];
gtk_init(&argc, &argv);
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_container_set_border_width(GTK_CONTAINER(window),
10);
8.2 Das Toolkit GTK+
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
219
/* Frame für die 1. Box erzeugen */
frame1 = gtk_frame_new("box1 (vbox)");
gtk_container_add(GTK_CONTAINER(window), frame1);
/* vertikale Box erzeugen */
box1 = gtk_vbox_new(FALSE, 10);
gtk_container_set_border_width(GTK_CONTAINER(box1), 10);
gtk_container_add(GTK_CONTAINER(frame1), box1);
/* Frame für die 2. Box erzeugen */
frame2 = gtk_frame_new("box2 (hbox)");
gtk_box_pack_start(GTK_BOX(box1), frame2, FALSE, FALSE,
0);
/* horizontale Box erzeugen */
box2 = gtk_hbox_new(TRUE, 10);
gtk_container_set_border_width(GTK_CONTAINER(box2), 10);
gtk_container_add(GTK_CONTAINER(frame2), box2);
/* Schaltflächen erzeugen */
for (i=0; i<4; i++)
{
sprintf(buffer[i], " Button %d ", i+1);
button = gtk_button_new_with_label(buffer[i]);
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(button_proc),
buffer[i]);
gtk_box_pack_start(GTK_BOX(box2), button,
TRUE, TRUE, 0);
}
button = gtk_button_new_with_label(" Beenden ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(quit_proc), NULL);
gtk_box_pack_start(GTK_BOX(box1), button, FALSE, FALSE,
0);
/* Objekte darstellen */
220
86
87
88
89
90
91
8 Grafische Benutzeroberflächen
gtk_widget_show_all(window);
gtk_main();
return(0);
}
In diesem Beispiel verwenden wir eine Callback-Funktion (button proc(), Zeile 17 bis 21) für mehrere Schaltflächen. Mit Hilfe des Parameters data wird der
Callback-Funktion je nach Schaltfläche eine andere Information übergeben – in
diesem Fall der Text der Schaltfläche.
Abbildung 8.4: Anordnung der Schaltflächen mittels vbox und hbox
8.2.8 Text-Labels
In den vorherigen Beispielen haben wir Schaltflächen mit einem Label (also einem
Text) verwendet. Das Label selbst ist ein eigenständiges GTK-Widget, das auch
außerhalb von Schaltflächen platziert werden kann, beispielsweise um zusätzliche
Informationen zu den Bedienelementen eines Programms zu geben – ähnlich den
oben beschriebenen Frames, jedoch ohne zusätzlichen Rahmen. Erzeugt wird ein
Label mit der Funktion:
GtkWidget *gtk_label_new(char *text);
Anschließend kann das Label in der gleichen Weise wie andere Widgets im Fenster oder einer Box platziert werden. Während der Ausführung des Programms
lässt sich der Text des Labels mit der Funktion
void gtk_label_set_text(GtkLabel *label, char *text);
nachträglich verändern.
8.2 Das Toolkit GTK+
221
8.2.9 Dialogfenster
Häufig benötigt man neben dem Hauptfenster weitere Dialogfenster. GTK bietet
dafür eine Funktion, die das neue Fenster erzeugt und bereits mit einer vertikalen
Box für Texte etc. und einer Action-Area“ für Bedienelemente ausstattet:
”
GtkWidget *gtk_dialog_new();
Das nachfolgende Beispielprogramm verwendet Dialogfenster für eine Sicherheitsabfrage beim Schließen des Hauptfensters (Abbildung 8.5).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
dialog.c - Dialogfenster beim Beenden öffnen
*/
# include <gtk/gtk.h>
/*----- Callback-Funktion -----*/
gint close_win(GtkWidget *widget, GdkEvent *event,
gpointer dialog)
{
gtk_widget_show(dialog);
return(TRUE); /* destroy-Event NICHT auslösen */
}
gint close_sub(GtkWidget *widget, GdkEvent *event,
gpointer data)
{
gtk_widget_hide(widget);
return(TRUE); /* destroy-Event NICHT auslösen */
}
void cont_proc(GtkWidget *widget, gpointer subwin)
{
gtk_widget_hide(subwin); /* Dialog schließen */
return;
}
void quit_proc(GtkWidget *widget, gpointer subwin)
{
gtk_widget_show(subwin); /* Dialog öffnen */
return;
}
222
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
8 Grafische Benutzeroberflächen
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *subwin, *label, *button;
gtk_init(&argc, &argv);
/* Optionen auswerten */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_container_set_border_width(GTK_CONTAINER(window),
30);
/* Dialogfenster erzeugen */
subwin = gtk_dialog_new();
gtk_signal_connect(GTK_OBJECT(subwin), "delete_event",
GTK_SIGNAL_FUNC(close_sub), NULL);
gtk_window_set_title(GTK_WINDOW(subwin), "Beenden?");
/* Callback für Hauptfenster einstellen */
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(close_win), subwin);
gtk_signal_connect(GTK_OBJECT(window), "destroy",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
/* Text in das Dialogfenster schreiben */
label = gtk_label_new("Wollen Sie das Programm\n"
"wirklich beenden?");
gtk_box_pack_start(GTK_BOX(GTK_DIALOG(subwin)->vbox),
label, TRUE, TRUE, 20);
gtk_widget_show(label);
/* Schaltflächen für Dialog erzeugen */
button = gtk_button_new_with_label(" Beenden ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(gtk_main_quit),
NULL);
gtk_box_pack_start(
GTK_BOX(GTK_DIALOG(subwin)->action_area),
button, TRUE, FALSE, 0);
gtk_widget_show(button);
8.2 Das Toolkit GTK+
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
223
button = gtk_button_new_with_label(" Abbrechen ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(cont_proc), subwin);
gtk_box_pack_start(
GTK_BOX(GTK_DIALOG(subwin)->action_area),
button, TRUE, FALSE, 0);
gtk_widget_show(button);
/* Schaltfläche für Hauptfenster erzeugen */
button = gtk_button_new_with_label(" Beenden ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(quit_proc), subwin);
gtk_container_add(GTK_CONTAINER(window), button);
/* Fenster darstellen und Callback starten */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
Am Anfang des Quelltextes werden wiederum die Callback-Funktionen für die
verschiedenen Ereignisse/Signale definiert:
close win() wird beim Schließen des Hauptfensters aufgerufen.
close sub() wird beim Schließen des Dialogfensters aufgerufen.
cont proc() bei Betätigung der Schaltfläche Abbrechen“ im Dialogfenster.
”
quit proc() bei Betätigung der Schaltfläche Beenden“ im Hauptfenster.
”
Abbildung 8.5: Sicherheitsabfrage mit Hilfe eines Dialogfensters
224
8 Grafische Benutzeroberflächen
Im Hauptprogramm wird nach dem Hauptfenster auch gleich das Dialogfenster
erzeugt (Zeile 49), obwohl dieses ja erst beim Schließen des Hauptfensters geöffnet
werden soll. Der Trick besteht darin, dass der Dialog bereits vollständig mit Text
und Schaltflächen erzeugt aber noch nicht mit der Funktion gtk widgt show()
sichtbar“ gemacht wird. Dies geschieht erst beim Versuch, das Hauptfenster zu
”
schließen (Zeile 12 und 31).
8.2.10 Auswahlfelder
Bislang haben wir als Eingabeelemente ausschließlich einfache Schaltflächen verwendet. GTK kennt darüber hinaus spezielle Schaltflächen, die der Benutzer einund ausschalten kann. Das folgende Programm zeigt einige dieser Auswahlfelder
(Abbildung 8.6):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
selection.c - verschiedene Auswahlelemente
*/
# include <stdio.h>
# include <string.h>
# include <gtk/gtk.h>
/*----- Callback-Funktionen -----*/
void quit_proc(GtkWidget *widget, gpointer data)
{
gtk_main_quit();
/* gtk_main() beenden */
return;
}
void button_proc(GtkWidget *widget, gpointer data)
{
if (GTK_TOGGLE_BUTTON(widget)->active)
printf("%s-Button: An\n", (char *)data);
else
printf("%s-Button: Aus\n", (char *)data);
return;
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *vbox, *button, *separator;
GSList *group;
8.2 Das Toolkit GTK+
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
225
gtk_init(&argc, &argv);
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_container_set_border_width(GTK_CONTAINER(window),
10);
/* vertikale Box erzeugen */
vbox = gtk_vbox_new(FALSE, 10);
gtk_container_add(GTK_CONTAINER(window), vbox);
/* Toggle- und Check-Schaltflächen erzeugen */
button = gtk_toggle_button_new_with_label("An/Aus");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(button_proc),
"Toggle");
gtk_box_pack_start(GTK_BOX(vbox), button,
FALSE, FALSE, 0);
button = gtk_check_button_new_with_label("An/Aus");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(button_proc),
"Check");
gtk_box_pack_start(GTK_BOX(vbox), button,
FALSE, FALSE, 0);
/* Radio-Buttons erzeugen */
button = gtk_radio_button_new_with_label(NULL,
"Auswahl 1");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(button_proc),
"Radio1");
gtk_box_pack_start(GTK_BOX(vbox), button,
FALSE, FALSE, 0);
group = gtk_radio_button_group(GTK_RADIO_BUTTON(button));
226
8 Grafische Benutzeroberflächen
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
button = gtk_radio_button_new_with_label(group,
"Auswahl 2");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(button_proc),
"Radio2");
gtk_box_pack_start(GTK_BOX(vbox), button,
FALSE, FALSE, 0);
/* Trennlinie einfügen */
separator = gtk_hseparator_new();
gtk_box_pack_start(GTK_BOX(vbox), separator,
FALSE, FALSE, 0);
/* Schaltfläche zum Beenden */
button = gtk_button_new_with_label(" Beenden ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(quit_proc), NULL);
gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE,
0);
/* Objekte darstellen */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
In diesem Beispiel verwenden wir für alle Auswahlfelder die gleiche CallbackFunktion button proc() in Zeile 17 bis 24. Anhand des Parameters data können
wir unterscheiden, welches Element der Benutzer angewählt hat. Dazu haben wir
bei der Verknüpfung der Objekte mit der Callback-Funktion jeweils einen anderen
Text als Kennung an die Funktion gtk signal connect() übergeben (Zeile 53,
60, 70 und 80).
Die Auswahlfelder vom Typ Toggle-Button und Check-Button sind den bereits vorgestellten Schaltflächen ähnlich, bleiben jedoch beim Anklicken so lange aktiv“,
”
bis sie erneut angeklickt werden. In der Callback-Funktion können wir anhand
der Variablen
GTK TOGGLE BUTTON(widget)->active
8.2 Das Toolkit GTK+
227
Abbildung 8.6: Verschiedene Auswahlelemente
abfragen, ob das Auswahlfeld aktiviert oder deaktiviert wurde. Die Toggle- und
Check-Buttons unterscheiden sich lediglich in der grafischen Gestaltung (siehe
Abbildung 8.6).
Etwas anders verhält es sich mit den so genannten Radio-Buttons. Diese Widgets
werden zu einer Gruppe (GSList *group) verküpft, so dass der Anwender nur
genau ein Element dieser Gruppe aktivieren kann, wie bei den Stationstasten eines Radios – daher der Name Radio-Button. In unserem Beispiel haben wir zwei
solcher Auswahlfelder mit der Funktion
GtkWidget *gtk_radio_button_new_with_label(GSList *group,
gchar *label);
erzeugt. Beim ersten Button geben wir als Zeiger auf die Gruppe NULL an, da die
Gruppe noch nicht existiert, zu der dieses Widget hinzugefügt werden soll (Zeile
66 und 67). In Zeile 74 erzeugen wir dann die neue Gruppe und fügen den ersten
Radio-Button hinzu. Für den zweiten Radio-Button geben wir anschließend direkt
die Gruppe an, zu der er hinzugefügt werden soll (Zeile 76 und 77).
Bei Betätigung eines der Radio-Buttons wird unsere Callback-Funktion zweimal
aufgerufen: einmal zum Deaktivieren des zuvor aktiven Buttons und anschließend zum Aktivieren des angeklickten Buttons.
In unserem Beispielprogramm sind die Toggle- und Check-Buttons beim Start des
Programms inaktiv und der erste Radio-Button aktiv. Natürlich können Sie die
Voreinstellung beim Start des Programms auch selbst vorgeben, und zwar mit der
Funktion
gtk_toggle_button_set_active(GtkToggleButton *button,
gint
state);
228
8 Grafische Benutzeroberflächen
8.2.11 Eingabefelder für Text und Zahlen
Neben den Auswahl-Buttons, die nur ein- oder ausgeschaltet werden können, bietet GTK komplexere Eingabefelder bis hin zu mehrzeiligen Textfeldern mit automatischem Scrollen und Cut-and-paste-Funktion. Das folgende Beispiel erzeugt
zwei einzeilige Text-, ein Zahlen- und ein mehrzeiliges Textfeld.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1
/*
input.c - verschiedene Text-Eingabefelder
*/
# include <stdio.h>
# include <gtk/gtk.h>
/*----- Callback-Funktionen -----*/
void quit_proc(GtkWidget *widget, gpointer data)
{
gtk_main_quit();
/* gtk_main() beenden */
return;
}
void entry_proc(GtkWidget *widget, gpointer data)
{
printf("Eingabe in Textfeld %s: %s\n",
(char *)data,
gtk_entry_get_text(GTK_ENTRY(widget)));
return;
}
void spin_proc(GtkWidget *widget, gpointer data)
{
printf("Eingabe in Zahlenfeld: %d\n",
gtk_spin_button_get_value_as_int(data));
return;
}
gboolean text_proc(GtkWidget *view, GdkEvent *event,
gpointer data)
{
GtkTextBuffer *buffer;
GtkTextIter start, end;
char *text;
Dieses Programm lässt sich nur mit GTK 2.0 übersetzen. Für GTK 1.2 muss das Widget GtkTextView
durch die ältere Version GtkText ersetzt werden.
8.2 Das Toolkit GTK+
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
229
buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
gtk_text_buffer_get_start_iter(buffer, &start);
gtk_text_buffer_get_end_iter(buffer, &end);
text = gtk_text_buffer_get_text(buffer, &start, &end,
FALSE);
printf("Textbox:\n%s\n", text);
return(FALSE); /* Signal nicht löschen! */
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *vbox, *button, *entry, *spin, *view;
GtkTextBuffer *buffer;
GtkAdjustment *adj;
gtk_init(&argc, &argv);
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_container_set_border_width(GTK_CONTAINER(window),
10);
/* vertikale Box erzeugen */
vbox = gtk_vbox_new(FALSE, 10);
gtk_container_add(GTK_CONTAINER(window), vbox);
/* Textfelder erzeugen */
entry = gtk_entry_new();
gtk_signal_connect(GTK_OBJECT(entry), "changed",
GTK_SIGNAL_FUNC(entry_proc), "1");
gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE,
0);
gtk_entry_set_text(GTK_ENTRY(entry), "Textfeld 1");
entry = gtk_entry_new_with_max_length(20);
gtk_signal_connect(GTK_OBJECT(entry), "changed",
230
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
8 Grafische Benutzeroberflächen
GTK_SIGNAL_FUNC(entry_proc), "2");
gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE,
0);
gtk_entry_set_text(GTK_ENTRY(entry), "max. 20 Zeichen");
/* nummerisches Feld erzeugen */
adj = (GtkAdjustment *)gtk_adjustment_new(20, 0, 100,
1, 10, 0);
spin = gtk_spin_button_new(adj, 0.0, 0);
gtk_signal_connect(GTK_OBJECT(adj), "value_changed",
GTK_SIGNAL_FUNC(spin_proc), spin);
gtk_box_pack_start(GTK_BOX(vbox), spin, FALSE, FALSE,
0);
/* Textbox erzeugen */
view = gtk_text_view_new();
gtk_signal_connect(GTK_OBJECT(view), "focus_out_event",
GTK_SIGNAL_FUNC(text_proc), NULL);
gtk_widget_set_usize(view, 200, 120);
gtk_text_view_set_editable(GTK_TEXT_VIEW(view), TRUE);
gtk_box_pack_start(GTK_BOX(vbox), view, TRUE, TRUE, 0);
buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
gtk_text_buffer_set_text(buffer, "mehrzeiliger Text\n",
-1);
/* Schaltfläche zum Beenden */
button = gtk_button_new_with_label(" Beenden ");
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(quit_proc), NULL);
gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE,
0);
/* Objekte darstellen */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
8.2 Das Toolkit GTK+
231
Alle Textfelder werden im Programm mit unterschiedlichen Texten vorbelegt, die
direkt beim Öffnen des Fensters zu sehen sind (siehe Abbildung 8.7).
Abbildung 8.7: Eingabefelder für Text und Zahlen
Häufig werden Texteingabefelder erst ausgewertet, wenn eine Schaltfläche betätigt wird, z. B. bei einem Feld für einen Dateinamen kombiniert mit einer Schaltfläche Speichern unter“. In unserem Beispiel haben wir aber auch die Eingabe”
felder mit Callback-Funktionen verknüpft, so dass bereits bei der Eingabe eines
Textes Aktionen erfolgen.
Die Funktion entry proc() (Zeile 16 bis 22) wird aufgerufen, sobald der Text
in einem der einzeiligen Felder geändert wird. Dies erreichen wir durch die Verknüpfung mit dem Signal changed“ in Zeile 73 und 80. Mit Hilfe der Funktion
”
gtk entry get text() geben wir bei jeder Änderung den aktuellen Inhalt des
Feldes aus (Zeile 18 bis 20). Bitte beachten Sie die unterschiedlichen Funktionsaufrufe zum Erzeugen der einzeiligen Textfelder in den Zeilen 72 und 79. Damit
erreichen wir, dass die Anzahl der Zeichen beim zweiten Textfeld auf 20 begrenzt
wird.
In den Zeilen 24 bis 29 ist die Funktion spin proc() definiert, die als CallbackFunktion für das Zahlenfeld (in GTK als Spin-Button bezeichnet) dient. Den Parameter data verwenden wir, um der Funktion einen Zeiger auf den Spin-Button
zu übergeben.
Das Zahlenfeld selbst wird in den Zeilen 88 bis 94 erzeugt und eingerichtet. Für
einen Spin-Button benötigen wir ein so genanntes Adjustment, das in der Zeile 88
mit der Funktion
232
8 Grafische Benutzeroberflächen
GtkObject *gtk_adjustment_new(gfloat
gfloat
gfloat
gfloat
gfloat
gfloat
value,
lower,
upper,
step_increment,
page_increment,
page_size);
neu angelegt wird. Die ersten drei Parameter geben den Startwert (beim Öffnen
des Fensters), den minimalen und den maximalen Wert des Feldes an. Beachten
Sie bitte, dass es sich um Fließkommazahlen handelt; Sie können dieses GTKElement also nicht nur für ganzzahlige Eingabefelder verwenden. Der vierte Parameter gibt an, um wie viel der Wert erhöht/verringert wird, wenn der Benutzer
mit der linken Maustaste auf die Pfeile rechts neben dem Feld klickt (siehe Abbildung 8.7). Beim Klick mit der mittleren Maustaste (oder dem Scrollrad) auf die
Pfeile wird der Wert um den als fünften Parameter angegebenen Betrag vergrößert
bzw. verkleinert. Der letzte Parameter wird zur Zeit nicht genutzt.
In Zeile 90 wird der eigentliche Spin-Button erzeugt und direkt mit dem zuvor angelegten Adjustment adj verknüpft. Der zweite Parameter der Funktion
gkt spin button new() gibt an, wie stark das Hoch- und Runterzählen des
Feldes beschleunigt wird, wenn der Benutzer auf die Pfeiltasten klickt und die
Maustaste gedrückt hält. Der Wertebereich für diesen Parameter bewegt sich von
0 bis 1,0. Mit dem dritten Parameter geben Sie schließlich an, wie viele Nachkommastellen das Zahlenfeld anzeigen soll. In unserem Beispiel ist der Wert 0 – es
werden also nur ganze Zahlen angezeigt. Bitte beachten Sie, dass unsere CallbackFunktion nicht mit dem Spin-Button, sondern dem Adjustment verknüpft wird
(Zeile 91)!
Für das mehrzeilige Textfeld erzeugen wir ein TextView-Widget (Zeile 98) und verknüpfen es mit dem focus out event“ (Zeile 99 und 100). Dadurch wird die
”
Callback-Funktion text proc() (Zeile 31 bis 45) immer dann aufgerufen, wenn
der Cursor in dem mehrzeiligen Textfeld ist und ein anderes Feld (oder Fenster)
angeklickt – die Eingabe also abgeschlossen wird. Da ein TextView keine sinnvoll
voreingestellte Größe hat – ähnlich dem leeren Fenster in unserem ersten GTKBeispiel –, verwenden wir die Funktion
void gtk_widget_set_usize(GtkWidget *widget,
gint
width,
gint
height);
zum Einstellen einer Startbreite und -höhe (Zeile 101). Diese Funktion lässt sich
auch auf andere Widgets anwenden – z. B. auf Schaltflächen.
Wenn Sie das mehrzeilige Textfeld nur für Ausgaben (z. B. Statusinformationen)
nutzen wollen, müssen Sie die Editierbarkeit“ abschalten (Zeile 102):
”
gtk text view set editable(GTK TEXT VIEW(view), FALSE);
8.2 Das Toolkit GTK+
233
Beim Erzeugen des Widgets TextView wird automatisch ein Puffer für die Eingaben angelegt. Bevor wir Text in das Feld schreiben können, müssen wir diesen
Puffer abfragen (Zeile 105). Danach kann mit der Funktion
void gtk_text_buffer_set_text(GtkTextBuffer *buffer,
const gchar
*text,
gint
len);
der gesamte Inhalt des Puffers gelöscht und durch den als zweiten Parameter angegebenen Text ersetzt werden (Zeile 106). Der dritte Parameter gibt die Länge
des neuen Textes an. Ist sie –1, wird sie automatisch ermittelt.
Soll der bereits vorhandene Text im Puffer erhalten bleiben und nur etwas hinzugefügt werden, so können Sie dazu die Funktion
void gtk_text_buffer_insert_at_cursor(GtkTextBuffer *buffer,
const gchar
*text,
gint
len);
verwenden. Das Auslesen des Pufferinhalts (nach Benutzereingaben) kann unter
anderem mit der Funktion
gchar *gtk_text_buffer_get_text(GtkTextBuffer *buffer,
const GtkTextIter *start,
const GtkTextIter *end,
gboolean include_hidden_chars);
erfolgen (Zeile 41). Weil das Textfeld neben den sichtbaren“ Zeichen auch In”
formationen über die Schriftart und -farbe und sogar Grafikelemente enthalten
kann, wird die Position im Puffer nicht einfach als Zahl angegeben, sondern als
GtkTextIter. Daher fragen wir die Anfangs- und Endposition des aktuellen
Textes in der Callback-Funktion mit Hilfe von
void gtk_text_buffer_get_start_iter(GtkTextBuffer *buffer,
GtkTextIter
*start);
void gtk_text_buffer_get_end_iter(GtkTextBuffer *buffer,
GtkTextIter
*end);
ab (Zeile 39 und 40).
8.2.12 Menüs
Zu den wichtigsten Elementen einer grafischen Benutzeroberfläche gehören
Pulldown-Menüs. Ein typisches Menü umfasst bei GTK die in Abbildung 8.8 dargestellten Elemente. Beachten Sie, dass die Elemente in der Menüleiste vom gleichen Typ (MenuItem) sind wie die einzelnen Unterpunkte eines Menüs.
234
8 Grafische Benutzeroberflächen
MenuBar
MenuItem
Menu
Abbildung 8.8: Die Elemente eines GTK-Menüs
Im folgenden Programm realisieren wir eine Menüleiste (MenuBar) mit einem
Menü, bestehend aus drei MenuItems.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
menu.c - Fenster mit Menüleiste
*/
# include <stdio.h>
# include <string.h>
# include <gtk/gtk.h>
GtkWidget *label;
/* für Callback sichtbar! */
/*----- Callback-Funktion -----*/
void menu_proc(GtkWidget *widget, gpointer data)
{
static char buffer[40];
sprintf(buffer, "Menüpunkt ’%s’ gewählt.",
(char *)data);
gtk_label_set_text(GTK_LABEL(label), buffer);
return;
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *vbox, *menubar, *menu, *menuitem;
gtk_init(&argc, &argv);
8.2 Das Toolkit GTK+
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
235
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_window_set_default_size(GTK_WINDOW(window),
250, 150);
/* vertikale Box erzeugen */
vbox = gtk_vbox_new(FALSE, 0);
gtk_container_add(GTK_CONTAINER(window), vbox);
/* Menüs erzeugen und einrichten */
menubar = gtk_menu_bar_new();
gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE,
2);
menu = gtk_menu_new();
menuitem = gtk_menu_item_new_with_label("Laden");
gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
GTK_SIGNAL_FUNC(menu_proc),
"Laden");
gtk_menu_append(GTK_MENU(menu), menuitem);
menuitem = gtk_menu_item_new_with_label("Speichern");
gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
GTK_SIGNAL_FUNC(menu_proc),
"Speichern");
gtk_menu_append(GTK_MENU(menu), menuitem);
menuitem = gtk_menu_item_new_with_label("Beenden");
gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_menu_append(GTK_MENU(menu), menuitem);
menuitem = gtk_menu_item_new_with_label("Datei");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem),
menu);
gtk_menu_bar_append(GTK_MENU_BAR(menubar), menuitem);
/* Label als Leerraum erzeugen */
236
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
8 Grafische Benutzeroberflächen
label = gtk_label_new("");
gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0);
/* Label zur Textanzeige erzeugen */
label = gtk_label_new("Kein Menüpunkt gewählt.");
gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE,
0);
/* Objekte darstellen */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
In den Zeilen 13 bis 21 ist die Callback-Funktion für die Menüpunkte definiert. In
dieser Funktion wird der aufgerufene Menüpunkt mit Hilfe eines Label-Widgets
als Text unten im Fenster ausgegeben (siehe Abbildung 8.9). Um dies zu vereinfachen, haben wir die Variable mit dem GTK-Label als globale Variable definiert
(Zeile 9), so dass auch die Callback-Funktion darauf zugreifen kann.
Abbildung 8.9: Fenster mit Menüzeile und einem Menü
Zur Erstellung des Menüs erzeugen wir in dem Hauptprogramm zuerst die
Menüleiste (Zeile 46) und dann das Menü (Zeile 50). In Zeile 52 bis 67 werden
die drei Menüpunkte mit gtk menu item new() angelegt und mit der Funktion
gtk menu append() dem Pulldown-Menü hinzugefügt. In Zeile 69 erzeugen wir
schließlich das Menü-Item mit dem Text Datei“ als Überschrift und verknüpfen
”
es in Zeile 70 und 71 mit dem Pulldown-Menü. Als letzten Schritt fügen wir dieses
Menü-Item dann in die Menüleiste ein (Zeile 72).
8.2 Das Toolkit GTK+
237
Trennlinien zwischen Menüpunkten
Manchmal sind Menüs für den Anwender übersichtlicher, wenn die Menüpunkte
thematisch gruppiert und die Gruppen durch Trennlinien abgegrenzt sind. Ab
Version 2.0 bietet GTK dafür ein spezielles Widget: GtkSeparatorMenuItem.
Dieses Widget wird in gleicher Weise wie ein Menüpunkt (MenuItem) in das
Pulldown-Menü eingefügt.
Um dies am vorigen Programmbeispiel zu demonstrieren, fügen wir ab Zeile 63
folgende Quelltextzeilen ein:
menuitem = gtk_separator_menu_item_new();
gtk_menu_append(GTK_MENU(menu), menuitem);
Dadurch wird der letzte Menüpunkt ( Beenden“) von den anderen beiden durch
”
eine Trennline abgegrenzt (siehe Abbildung 8.10).
Abbildung 8.10: Menü mit Trennlinie (Separator)
Menüpunkte und Schaltflächen deaktivieren
Unter Umständen ist es erforderlich, dass bestimmte Menüpunkte oder Schaltflächen für den Anwender gesperrt sind. GTK stellt auch für diesen Zweck eine
spezielle Funktion bereit:
void gtk_widget_set_sensitive(GtkWidget *button,
gboolean
sensitive);
Das Widget wird dann grau dargestellt, so dass der Benutzer erkennt, dass es
nicht angewählt werden kann. Um beispielsweise die Schaltfläche button zu deaktivieren, lautet der Funktionsaufruf:
gtk_widget_set_sensitive(button, FALSE);
Mit der gleichen Funktion kann das Widget auch wieder aktiviert werden, wenn
Sie als zweiten Parameter TRUE“ angeben.
”
238
8 Grafische Benutzeroberflächen
8.2.13 Pixmap-Grafiken darstellen
Das Toolkit GTK bietet spezielle Funktionen, um Bitmap-Grafiken zu verarbeiten und darzustellen. Der X11-Server arbeitet intern mit so genannten PixmapStrukturen für die Verarbeitung von farbigen Bildern wie beispielsweise Icons.
Diese Strukturen eignen sich aber nicht für Grafikdateien, daher stellt GTK Funktionen bereit, mit denen sich Bilder im XPM-Format in eine Pixmap umwandeln
lassen:
GdkPixmap* gdk_pixmap_create_from_xpm(GdkWindow *window,
GdkBitmap **mask,
GdkColor
*transparent_color,
const gchar *filename);
GdkPixmap* gdk_pixmap_create_from_xpm_d(GdkWindow *window,
GdkBitmap **mask,
GdkColor
*transparent_color,
gchar
**data);
Der Unterschied zwischen diesen Funktionen besteht in der Quelle für die Grafikdaten: Während die erste Funktion sie aus der XPM-Datei mit dem angegebenen
Dateinamen liest, erwartet die zweite als letzten Parameter einen Zeiger auf die
Daten im Speicher. Dadurch haben Sie die Möglichkeit, kleinere Bilder wie etwa Icons direkt in den Quelltext einzubauen. Bitte beachten Sie, dass sowohl die
Funktionsnamen als auch die Variablentypen mit gdk (statt gtk) beginnen! Es handelt sich dabei um Funktionen aus der libgdk, die auch zum GTK-Paket gehört.
Beim Erstellen der Pixmap berücksichtigen beide Funktionen die Eigenschaften
des X11-Displays bezüglich der Farbtiefe (Bits pro Pixel) und der Art der Farbverwaltung (feste Farbtabelle oder direkter Farbwert für jeden Pixel). Daher benötigen die Funktionen als ersten Parameter das Grafikelement (Typ GdkWindow),
in dem die Pixmap später dargestellt werden soll. Ein solches GdkWindow ist
Bestandteil der Struktur GtkWindow.
Das XPM-Grafikformat unterstützt Transparenz, d. h. ein Farbwert kann als
durchsichtig“ definiert werden. Damit ist es möglich, dass Grafikelemente eine
”
beliebige Kontur haben, wie dies bei Desktop-Icons üblich ist. Die Struktur Pixmap bietet jedoch nicht die Möglichkeit einer transparenten Farbe. Daher legen
beide Funktionen zusätzlich eine Bitmap (1 Bit pro Pixel) mit der Maske an und
schreiben das Bitmap-Objekt in die Variable, auf die der Parameter mask zeigt. In
dieser Maske haben alle Pixel den Wert 0, wo die XPM-Grafik transparent ist. Die
anderen Pixel in der Maske haben den Wert 1. Da in XPM-Dateien für die transparenten Bereiche kein Farbwert“ angegeben ist, die Pixmap in diesen Bereichen
”
jedoch irgendeine Farbe haben muss, verlangen beide Funktionen als dritten Parameter einen Farbwert für diese Bereiche.
8.2 Das Toolkit GTK+
239
Hier ein Beispiel für den Funktionsaufruf zum Laden der XPM-Grafikdatei
tux.xpm“ und Ersetzen der transparenten Farbe durch Weiß:
”
GtkWidget *win;
GdkPixmap *pixmap;
GdkBitmap *mask;
pixmap = gdk_pixmap_create_from_xpm(win->window,
&mask,
&window->style->white,
"tux.xpm");
Um eine so erzeugte GdkPixmap in einem Fenster oder einem anderen Widget
darzustellen, muss daraus ein GTK-Objekt – genauer gesagt ein Objekt vom Typ
GtkPixmap – erzeugt werden. Dazu dient die Funktion
GtkPixmap *gtk_pixmap_new(GdkPixmap *pixmap,
GdkBitmap *mask);
Eine GtkPixmap unterstützt Transparenz, daher verwendet diese Funktion die
zuvor erzeugte Bitmap-Maske, um die transparenten Bereiche auszublenden. Bei
Verwendung dieser Funktion spielt es im Grunde keine Rolle mehr, welche Farbe
Sie für die transparenten Pixel beim Erzeugen der Pixmap gewählt haben, weil
diese Pixel ohnehin ausgeblendet werden.
Das folgende Programm zeigt diese Schritte im Zusammenhang: es erzeugt ein
Fenster, lädt die als Kommandozeilenparameter angegebene XPM-Datei und stellt
die Grafik in dem Fenster dar (siehe Abbildung 8.11).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
view_xpm.c - XPM-Grafikdatei anzeigen
*/
# include <stdio.h>
# include <string.h>
# include <gtk/gtk.h>
int main(int argc, char *argv[])
{
GtkWidget *window, *gtk_pm;
GdkPixmap *pixmap;
GdkBitmap *mask;
int width, height;
/* Kommandozeile auswerten */
240
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
8 Grafische Benutzeroberflächen
gtk_init(&argc, &argv);
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
{
printf("Usage: view_xpm XPM-file\n");
return(1);
}
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_window_set_default_size(GTK_WINDOW(window), 80, 40);
gtk_window_set_title(GTK_WINDOW(window), argv[1]);
gtk_widget_show(window); /* VOR gdk_pixmap_create_... */
/* Pixmap aus XPM-Datei laden */
pixmap = gdk_pixmap_create_from_xpm(window->window,
&mask, &window->style->white, argv[1]);
if (pixmap == NULL)
{
perror("view_xpm: Can’t load Pixmap");
return(1);
}
gdk_window_get_size(pixmap, &width, &height);
printf("Image size: %d x %d\n", width, height);
gtk_pm = gtk_pixmap_new(pixmap, mask);
gtk_container_add(GTK_CONTAINER(window), gtk_pm);
/* Fenster darstellen */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
Beachten Sie bitte, dass wir in Zeile 33 das Fenster bereits mit der Funktion
gtk widget show() auf sichtbar“ einstellen. Dies ist erforderlich, weil zum Er”
zeugen der Pixmap in Zeile 37 die Informationen über die Bit-Tiefe und Farben-
8.2 Das Toolkit GTK+
241
zahl des X11-Displays erforderlich sind. Diese erhält das Fenster-Widget aber erst
mit Aufruf der Funktion gtk widget show().
Abbildung 8.11: XPM-Bilddateien anzeigen mit view xpm
Schaltflächen und Menüs mit Grafik
Nachdem wir anhand des vorigen Beispiels gesehen haben, wie man farbige
Pixmap-Grafiken aus XPM-Dateien erzeugt, zeigen wir Ihnen, wie man auch
Schaltflächen und Menüpunkte mit einer Grafik statt mit einem Textlabel einrichtet.
Zunächst müssen wir zum Erzeugen der Schaltfläche oder des Menüpunkts jeweils eine andere Funktion wählen als bisher:
GtkWidget *gtk_button_new();
GtkWidget *gtk_menu_item_new();
Wie Sie sehen, fehlt bei diesen Funktionsnamen die Endung with label“ –
”
in diesem Fall verwenden wir ja kein Textlabel. Danach benötigen wir eine
GtkPixmap, die wir analog zum vorigen Beispiel aus einer XPM-Datei erzeugen können. Diese GtkPixmap fügen wir dann mit der bereits bekannten Funktion gtk container add() zum Container der Schaltfläche oder des Menüpunkts
hinzu:
GtkWidget button, menuitem, gtk_pixmap;
button = gtk_button_new();
gtk_container_add(GTK_CONTAINER(button), gtk_pixmap);
menuitem = gtk_menu_item_new();
gtk_container_add(GTK_CONTAINER(menuitem), gtk_pixmap);
Wenn Sie eine Schaltfläche mit Text und Grafik versehen wollen, so müssen Sie in
den Container der Schaltfläche zuerst eine vertikale oder horizontale Box (siehe
Abschnitt 8.2.7) einfügen, die Sie dann mit der Grafik und einem Label füllen. Das
Gleiche gilt natürlich auch für Menüpunkte.
242
8 Grafische Benutzeroberflächen
Das folgende Programmbeispiel versieht eine Schaltfläche mit einer Grafik und
einem Textlabel (siehe Abbildung 8.12).
Abbildung 8.12: Schaltfläche mit Grafik und Text in einer hbox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
button-grafik.c - Schaltfläche mit Grafik und Text
*/
# include <stdio.h>
# include <gtk/gtk.h>
static const char *xpm_data[] = {
"16 14 3 1",
" c None",
". c #000000000000",
"X c #FFFFFFFFFFFF",
"
......
",
"
.XXX.X.
",
"
.XXX.XX.
",
"
.XXX.XXX.
",
"
.XXX.....
",
"
.XXXXXXX.
",
"
.XXXXXXX.
",
"
.XXXXXXX.
",
"
.XXXXXXX.
",
"
.XXXXXXX.
",
"
.XXXXXXX.
",
"
.XXXXXXX.
",
"
.........
",
"
"};
/*----- Callback-Funktion -----*/
void button_proc(GtkWidget *widget, gpointer data)
{
printf("Schaltfläche ’neue Datei’ betätigt.\n");
8.2 Das Toolkit GTK+
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
243
return;
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *button, *hbox, *label, *gtk_pm;
GdkPixmap *pixmap;
GdkBitmap *mask;
gtk_init(&argc, &argv);
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_container_set_border_width(GTK_CONTAINER(window),
30);
gtk_widget_show(window);
/* Schaltfläche erzeugen */
button = gtk_button_new();
gtk_signal_connect(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(button_proc), NULL);
gtk_container_add(GTK_CONTAINER(window), button);
/* hbox erzeugen */
hbox = gtk_hbox_new(FALSE, 0);
gtk_container_add(GTK_CONTAINER(button), hbox);
/* Pixmap erzeugen */
pixmap = gdk_pixmap_create_from_xpm_d(window->window,
&mask, &window->style->white,
(gchar **)xpm_data);
gtk_pm = gtk_pixmap_new(pixmap, mask);
gtk_box_pack_start(GTK_BOX(hbox), gtk_pm, FALSE, FALSE,
0);
/* label erzeugen und in hbox stellen */
244
77
78
79
80
81
82
83
84
85
86
87
88
89
8 Grafische Benutzeroberflächen
label = gtk_label_new("neue Datei");
gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE,
4);
/* Objekte darstellen */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
8.2.14 Zeichenflächen
Nachdem wir verschiedene Bedienelemente verwendet und Bilder/Icons aus
Grafikdateien dargestellt haben, wenden wir uns der echten“ Grafikausgabe zu –
”
also dem Zeichnen von Grafikobjekten wie Linien und Kreisen. Für diesen Zweck
stellt GTK ein Widget als Zeichenfläche“ zur Verfügung: GtkDrawingArea:
”
GtkWidget *drawing;
drawing = gtk_drawing_area_new();
gtk_drawing_area_size(GTK_DRAWING_AREA(drawing), 300, 200);
Dabei handelt es sich um ein einfaches X11-Window, das in unsere GTKApplikation eingebettet wird. GTK selbst übernimmt bei diesen Elementen nicht
das Wiederherstellen des Inhalts, wenn ein Teil dieser Zeichenfläche von einem
anderen Fenster überdeckt war. Das muss unser Programm übernehmen. GTK
stellt jedoch entsprechende Signale und Events zur Verfügung, die unser Programm informieren, wann welcher Bereich neu gezeichnet werden muss.
Um diesen Mechanismus zu beschleunigen, legen wir eine Pixmap an, in der gezeichnet wird, und übertragen nur die Bereiche der Pixmap auf die Zeichenfläche,
die es zu aktualisieren gilt. Auf diese Weise vermeiden wir, dass alle Zeichenfunktionen neu ausgeführt werden müssen, wenn eine kleine Ecke der Zeichenfläche durch ein anderes Fenster verdeckt war. Die Ereignisse, die wir mit einer
Callback-Funktion verknüpfen müssen, sind das configure event“ und das ex”
”
pose event“. Das configure event wird ausgelöst, wenn das Fenster zum ersten
Mal geöffnet oder die Größe verändert wird. Wurde ein Teil des Fensters verdeckt
und dann wieder sichtbar, wird das expose event ausgelöst. Beiden CallbackFunktionen übergeben wir einen Zeiger auf die Pixmap zum Zwischenspeichern
der Grafik:
8.2 Das Toolkit GTK+
245
GdkPixmap *pixmap;
gtk_signal_connect(GTK_OBJECT(drawing), "expose_event",
GTK_SIGNAL_FUNC(redraw), &pixmap);
gtk_signal_connect(GTK_OBJECT(drawing), "configure_event",
GTK_SIGNAL_FUNC(configure), &pixmap);
Die Callback-Funktionen sehen wir uns am besten in dem Beispielprogramm an,
das die in Abbildung 8.13 gezeigte Grafik erzeugt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
draw.c - Grafikausgabe mit GTK
*/
# include <gtk/gtk.h>
/*----- Callback-Funktionen -----*/
gint configure(GtkWidget *widget,
GdkEventConfigure *event, gpointer *pixmap)
{
gint width, height, depth;
GdkGC *gc;
GdkColor color;
GdkColormap *colormap;
GdkFont *font;
if (*pixmap != NULL)
gdk_pixmap_unref(*pixmap);
width = widget->allocation.width;
height = widget->allocation.height;
depth = -1; /* Farbtiefe auf default */
*pixmap = gdk_pixmap_new(widget->window, width, height,
depth);
gdk_draw_rectangle(*pixmap, widget->style->white_gc,
TRUE, 0, 0, width, height);
gdk_draw_rectangle(*pixmap, widget->style->black_gc,
FALSE, 0, 0, width-1, height-1);
/* neuer GraphicContext (GC) für Farben */
gc = gdk_gc_new(widget->window);
colormap = gdk_window_get_colormap(widget->window);
/* Vordergrundfarbe orange */
246
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
8 Grafische Benutzeroberflächen
color.red = 0xf000;
color.green = 0xb000;
color.blue = 0x0000;
gdk_color_alloc(colormap, &color);
gdk_gc_set_foreground(gc, &color);
/* 3/4-Kreis zeichnen */
gdk_draw_arc(*pixmap, gc, TRUE, 20, 20, 100, 80,
0*64, 270*64);
/* Vordergrundfarbe blaugrau */
color.red = 0x6000;
color.green = 0x9000;
color.blue = 0xb000;
gdk_color_alloc(colormap, &color);
gdk_gc_set_foreground(gc, &color);
/* Text ausgeben */
font = gdk_font_load(
"-*-times-bold-i-*-*-24-*-*-*-*-*-*-*");
gdk_draw_text(*pixmap, font, gc, 80, 85,
"Dies ist ein Test.", 18);
/* Vordergrundfarbe dunkelgrün */
color.red = 0x0000;
color.green = 0xa000;
color.blue = 0x0000;
gdk_color_alloc(colormap, &color);
gdk_gc_set_foreground(gc, &color);
/* Dreieck zeichnen */
gdk_gc_set_line_attributes(gc, 7, GDK_LINE_SOLID,
GDK_CAP_ROUND, GDK_JOIN_ROUND);
gdk_draw_line(*pixmap, gc, 20, height-20,
width-20, height-20);
gdk_draw_line(*pixmap, gc, 20, height-20,
width/2, height/2);
gdk_draw_line(*pixmap, gc, width/2, height/2,
8.2 Das Toolkit GTK+
247
80
width-20, height-20);
81
82
gdk_gc_destroy(gc);
83
return(TRUE);
84
}
85
86 gint redraw(GtkWidget *widget, GdkEventExpose *event,
87
gpointer *pixmap)
88
{
89
gdk_draw_pixmap(widget->window,
90
widget->style->white_gc,
91
*pixmap,
92
event->area.x, event->area.y,
93
event->area.x, event->area.y,
94
event->area.width, event->area.height);
95
return(FALSE);
96
}
97
98
/*----- Hauptprogramm -----*/
99
100 int main(int argc, char *argv[])
101
{
102
GtkWidget *window, *drawing;
103
GdkPixmap *pixmap;
104
gtk_init(&argc, &argv);
105
106
/* Fenster erzeugen */
107
108
109
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
110
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
111
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
112
pixmap = NULL;
113
114
/* Zeichenfläche erzeugen */
115
116
117
drawing = gtk_drawing_area_new();
118
gtk_signal_connect(GTK_OBJECT(drawing), "expose_event",
119
GTK_SIGNAL_FUNC(redraw), &pixmap);
120
gtk_signal_connect(GTK_OBJECT(drawing),
121
"configure_event",
122
GTK_SIGNAL_FUNC(configure), &pixmap);
123
gtk_drawing_area_size(GTK_DRAWING_AREA(drawing),
248
124
125
126
127
128
129
130
131
132
133
134
8 Grafische Benutzeroberflächen
300, 200);
gtk_container_add(GTK_CONTAINER(window), drawing);
/* Objekte darstellen */
gtk_widget_show_all(window);
gtk_main();
return(0);
}
Abbildung 8.13: Eine Zeichenfläche mit verschiedenen Grafikelementen
Betrachten wir zunächst die Callback-Funktion redraw() in Zeile 86 bis 96. Diese Funktion wird aufgerufen, sobald ein Teil des Fensters neu gezeichnet werden
muss. Als Parameter erhält die Funktion das Widget der Zeichenfläche, das Event,
das die Funktion ausgelöst hat, und unsere Pixmap mit der zwischengespeicherten Grafik. In der Callback-Funktion wird mit gdk draw pixmap() der im Event
angegebene Bereich aus unserer Pixmap in die Zeichenfläche kopiert.
Die eigentlichen Zeichenfunktionen werden in der Funktion configure() in Zeile 9 bis 84 aufgerufen. Diese Callback-Funktion wird beim Starten des Programms
und bei jedem Ändern der Fenstergröße aufgerufen. In Zeile 18 überprüfen wir
zunächst, ob schon eine Pixmap als Zwischenspeicher angelegt wurde. Wenn ja,
wird diese wieder entfernt. Danach fragen wir die aktuelle Größe der Zeichenfläche ab und erzeugen eine neue Pixmap mit dieser Größe (Zeile 23). In Zeile 25
und 26 zeichnen wir ein weiß ausgefülltes Rechteck über die gesamte Zeichenfläche und danach ein schwarzes Rechteck (nicht ausgefüllt) als Umrandung der
Zeichenfläche (Zeile 27 und 28). Sehen wir uns diese Zeichenfunktion etwas näher
an:
8.2 Das Toolkit GTK+
249
void gdk_draw_rectangle(GdkDrawable *drawable,
GdkGC
*gc,
gint
filled, //TRUE or FALSE
gint
x,
gint
y,
gint
width,
gint
height);
Als zweiten Parameter verlangt diese (wie auch fast alle anderen Zeichenfunktionen) einen so genannten Graphics Context (GC). Dieser enthält alle Informationen,
die für das Zeichnen erforderlich sind: Farbe, Strichstärke, Schriftart für Text, Füllmuster usw. Beim Erzeugen der Zeichenfläche werden automatisch mehrere GCs
angelegt und im Objekt hinterlegt – so auch white gc und blackgc.
Für die weiteren Zeichenfunktionen erzeugen wir in Zeile 32 einen neuen Graphics Context, um andere Farben und Strichstärken einstellen zu können.
In Zeile 58 und 59 laden wir eine Schriftart für die Textausgabe in Zeile 60 und 61.
Die Zeichenkette zur Spezifikation der Schriftart sieht etwas kryptisch aus, aber
mit Hilfe des Tools xfontsel“ (siehe Abbildung 8.14) können Sie eine Schriftart
”
wählen, deren Beschreibungs-String dann angezeigt wird.
Abbildung 8.14: Übersicht der Schriftarten unter X11 mit xfontsel“
”
Neben den in unserem Programm verwendeten Zeichenfunktionen stellt GTK
eine Reihe weiterer Funktionen zur Verfügung. Hier eine Übersicht:
gdk_draw_rectangle()
gdk_draw_arc()
gdk_draw_polygon()
gdk_draw_string()
gdk_draw_text()
gdk_draw_pixmap()
gdk_draw_bitmap()
gdk_draw_image()
gdk_draw_points()
gdk_draw_segments()
Die Parameter dieser Funktionen können Sie der Include-Datei <gdk/gdk.h>“
”
entnehmen.
250
8 Grafische Benutzeroberflächen
8.2.15 Zeichenfläche mit Rollbalken
Für große Zeichenflächen empfiehlt es sich, dass der Anwender das Fenster
kleiner ziehen und mit Rollbalken (Scrollbars) am Rand der Zeichenfläche den
gewünschten Bereich anscrollen kann. Bei GTK erreichen Sie das mit Hilfe eines Scrolled Window, in das die Zeichenfläche eingebettet wird. Das folgende Programm realisiert eine solche Zeichenfläche mit fester Größe und Rollbalken am
Rand (siehe Abbildung 8.15):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
draw2.c - Zeichenfläche mit Rollbalken
*/
# include <gtk/gtk.h>
# define WIDTH 500
# define HEIGHT 500
/* Größe der Zeichenfläche */
/*----- Callback-Funktionen -----*/
gint configure(GtkWidget *widget,
GdkEventConfigure *event, gpointer *pixmap)
{
gint depth, i;
if (*pixmap != NULL)
gdk_pixmap_unref(*pixmap);
depth = -1; /* Farbtiefe auf default */
*pixmap = gdk_pixmap_new(widget->window, WIDTH, HEIGHT,
depth);
gdk_draw_rectangle(*pixmap, widget->style->white_gc,
TRUE, 0, 0, WIDTH, HEIGHT);
for (i=0; i<=50; i++)
gdk_draw_line(*pixmap, widget->style->black_gc,
10*i, 0, WIDTH-1-10*i, HEIGHT-1);
return(TRUE);
}
gint redraw(GtkWidget *widget, GdkEventExpose *event,
gpointer *pixmap)
{
gdk_draw_pixmap(widget->window,
widget->style->white_gc,
*pixmap,
event->area.x, event->area.y,
8.2 Das Toolkit GTK+
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
251
event->area.x, event->area.y,
event->area.width, event->area.height);
return(FALSE);
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *scr_win, *drawing;
GdkPixmap *pixmap;
gtk_init(&argc, &argv);
/* Fenster erzeugen */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_window_set_default_size(GTK_WINDOW(window),
300, 200);
/* Fenster mit Rollbalken */
scr_win = gtk_scrolled_window_new(NULL, NULL);
gtk_container_add(GTK_CONTAINER(window), scr_win);
/* Zeichenfläche erzeugen */
pixmap = NULL;
drawing = gtk_drawing_area_new();
gtk_signal_connect(GTK_OBJECT(drawing), "expose_event",
GTK_SIGNAL_FUNC(redraw), &pixmap);
gtk_signal_connect(GTK_OBJECT(drawing),
"configure_event",
GTK_SIGNAL_FUNC(configure), &pixmap);
gtk_drawing_area_size(GTK_DRAWING_AREA(drawing),
500, 500);
gtk_scrolled_window_add_with_viewport(
GTK_SCROLLED_WINDOW(scr_win), drawing);
/* Objekte darstellen */
252
81
82
83
84
85
86
87
8 Grafische Benutzeroberflächen
gtk_widget_show_all(window);
gtk_main();
return(0);
}
Abbildung 8.15: Zeichenfläche mit fester Größe und Rollbalken
8.2.16 Dateiauswahlfenster
Als letztes GTK-Objekt möchten wir ein Dateiauswahlfenster vorstellen. Dieses
Widget wird mit
GtkWidget *gtk_file_selection_new(const gchar *title);
erzeugt und enthält verschiedene Schaltflächen zum Löschen oder Umbenennen
von Dateien sowie das Eingabefeld für den Dateinamen und zwei Listen für die
Verzeichnisse und die Dateien (siehe Abbildung 8.16).
Nach dem Erzeugen des Dateiauswahlfensters mit
GtkWidget* filesel = gtk_file_selection_new("Datei laden");
können Sie auf die Schaltflächen für OK“ und für Abbrechen“ als Elemente der
”
”
GtkFileSelection-Struktur zugreifen:
GTK_FILE_SELECTION(filesel)->ok_button
GTK_FILE_SELECTION(filesel)->cancel_button
Beide Schaltflächen müssen mit einer entsprechenden Callback-Funktion verknüpft werden. Wenn der Benutzer den OK-Button betätigt hat, können Sie den
gewählten Dateinamen mit der Funktion
8.2 Das Toolkit GTK+
253
Abbildung 8.16: Das GTK-Dateiauswahlfenster
gchar *gtk_file_selection_get_filename(
GtkFileSelection *filesel);
auswerten. Das folgende Programm erzeugt ein kleines Fenster mit einer Datei
”
laden“-Schaltfläche. Durch Anklicken dieser Schaltfläche öffnet sich das Dateiauswahlfenster. Wählt der Benutzer eine Datei aus, gibt das Programm den Dateinamen im Shell-Fenster aus.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
file_select.c - Dateiauswahlfenster
*/
# include <stdio.h>
# include <gtk/gtk.h>
/*----- Callback-Funktion -----*/
void file_ok_proc(GtkWidget *widget, gpointer data)
{
printf("Datei laden: ’%s’\n",
gtk_file_selection_get_filename(
GTK_FILE_SELECTION(data)));
gtk_widget_hide(data);
254
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
8 Grafische Benutzeroberflächen
return;
}
/*----- Hauptprogramm -----*/
int main(int argc, char *argv[])
{
GtkWidget *window, *filesel, *button;
gtk_init(&argc, &argv);
/* Fenster einrichten */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(gtk_main_quit), NULL);
gtk_container_set_border_width(GTK_CONTAINER(window),
30);
/* Dateiauswahlfenster erzeugen */
filesel = gtk_file_selection_new ("Datei laden");
gtk_signal_connect(GTK_OBJECT(filesel), "delete_event",
GTK_SIGNAL_FUNC(gtk_widget_hide),
&filesel);
gtk_signal_connect(
GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button),
"clicked", GTK_SIGNAL_FUNC(file_ok_proc), filesel);
gtk_signal_connect_object(
GTK_OBJECT(GTK_FILE_SELECTION(filesel)->cancel_button),
"clicked", GTK_SIGNAL_FUNC(gtk_widget_hide),
GTK_OBJECT(filesel));
/* Schaltfläche erzeugen */
button = gtk_button_new_with_label(" Datei laden ");
gtk_signal_connect_object(GTK_OBJECT(button), "clicked",
GTK_SIGNAL_FUNC(gtk_widget_show),
GTK_OBJECT(filesel));
gtk_container_add(GTK_CONTAINER(window), button);
gtk_widget_show(button);
/* Hauptfenster darstellen */
8.2 Das Toolkit GTK+
60
61
62
63
64
65
255
gtk_widget_show(window);
gtk_main();
return(0);
}
In den Zeilen 44 und 52 verwenden wir eine andere Funktion als bisher zur Verknüpfung eines GTK-Elements mit einer Callback-Funktion:
guint gtk_signal_connect_object(GtkObject
*object,
const gchar *name,
GtkSignalFunc func,
GtkObject
*slot_object);
Im Unterschied zu gtk signal connect() (vgl. Seite 212) wird der CallbackFunktion nur ein Parameter übergeben: das slot object. Auf diese Weise
können wir die Funktionen gtk widget show() und gtk widget hide() direkt als Callback-Funktionen einsetzen (Zeile 46 und 53), um das angegebene
Widget darzustellen oder auszublenden. Insofern benötigen wir nur eine einzige Callback-Funktion (Zeile 10 bis 17) für das Programm. Wie bereits im Beispiel aus Abschnitt 8.2.9 werden auch hier alle GTK-Objekte schon bei Programmstart erzeugt; das Dateiauswahlfenster wird zunächst jedoch nicht mit
gtk widget show() sichtbar“ gemacht. Dies geschieht erst bei Betätigung der
”
Schaltfläche Datei laden“.
”
8.2.17 Umlaute und Sonderzeichen
Wie bereits in Abschnitt 8.2.1 erwähnt, unterstützt das Toolkit GTK+ in der Version 2.0 UTF-8, um Sonderzeichen wie deutsche Umlaute darzustellen. Daher
müssen Sie beim Schreiben von Programmen darauf achten, dass diese im UTF8-Format abgespeichert werden. Dies bedeutet aber gleichzeitig, dass Buchstaben
und Zeichen mehrere Bytes lang sein können! So besteht das Wort für“ beispiels”
weise im UTF-8-Format aus vier Zeichen: f“ 0xC3 0xBC r“.
”
”
8.2.18 Wie geht es weiter?
Im Rahmen dieses Kapitels konnten wir nicht alle Funktionen und Objekte von
GTK vorstellen und erläutern, sondern lediglich einen Einstieg in das Thema geben. Eine vollständige Beschreibung der vielen weiteren Möglichkeiten des Toolkits finden Sie in den Tutorials der GTK-Homepage [19] und in weiterführender
Literatur, z. B. [18].
256
8 Grafische Benutzeroberflächen
8.3 Grafik ohne X11 mit der SVGALIB
Für Grafikausgaben unter Linux ist das X11-System der Standard. Nicht zuletzt
aufgrund der Client-Server-Struktur und der Unterstützung für viele verschiedene Grafikkarten ist das System jedoch sehr mächtig – für sehr kleine LinuxRechner wie Embedded Computer unter Umständen zu mächtig. Mit Hilfe der
Funktionsbibliothek libvga“ (das Paket wird auch als SVGALIB“ bezeichnet) ist
”
”
eine einfache Grafikausgabe auch ohne das X11-System möglich. Allerdings sei
darauf hingewiesen, dass die libvga natürlich kein gleichwertiger Ersatz für X11
sein kann. Sie bietet nur einen begrenzten Funktionsumfang, beispielsweise für
das Zeichnen von Linien und das Einstellen der Farben. Es gibt aber mittlerweile
sogar ein 3D-Toolkit für die Darstellung komplexer 3D-Elemente mit der libvga.
(Zum Download der SVGALIB siehe [20].)
8.3.1 Besonderheiten beim Arbeiten mit der libvga
Um die Grafikmodi einstellen und auf den Grafikspeicher zugreifen zu können,
muss die libvga entsprechende Hardware-Zugriffe ausführen. Diese sind nor”
malen“ Benutzern nicht gestattet, sodass Programme, die die libvga benutzen,
zum Teil root-Privilegien benötigen (siehe auch Kapitel 9). Dies birgt naturgemäß
das Risiko eines Systemabsturzes bei fehlerhaften Programmen. Seit der Version 1.9.0 wurde der Teil der libvga, der auf die Hardware zugreift, als KernelModul ( svgalib helper“) ausgelagert. Die Programme kommunizieren mit die”
sem Kernel-Modul über das Device /dev/svga und benötigen dementsprechend
auch nur die Zugriffsrechte für dieses Device.
Des Weiteren ist zu beachten, dass bei Verwendung hochauflösender Grafikmodi
auf nicht unterstützten Grafikkarten der ursprüngliche Textmodus möglicherweise nicht wiederhergestellt werden kann und so ein Neustart des Rechners erforderlich wird.
Aus diesem Grund sollte man Programme für die libvga auf Rechnern mit laufendem X11-System entwickeln, auch wenn dies zunächst widersinnig erscheint. In
der Regel ist der X11-Server jedoch in der Lage, die Grafikeinstellungen für die
X11-Oberfläche wiederherzustellen. Sollte ein auf die libvga aufsetzendes Programm nicht automatisch zur X11-Oberfläche zurückkehren, kann man mit der
Tastenkombination Strg-Alt-F7 nachhelfen“.
”
Wenn libvga-Programme ohne einen X11-Server im Hintergrund entwickelt werden, sollte man zumindest mit dem Shell-Kommando
savetextmode
zuvor die Einstellungen des Konsole-Textmodus sichern. Werden die Einstellungen von einem fehlerhaften libvga-Programm verstellt, sodass das Programm
nach Beendigung den Textmodus nicht mehr korrekt einstellt, können mit dem
Shell-Kommando
8.3 Grafik ohne X11 mit der SVGALIB
257
textmode
die zuvor mit savetextmode gesicherten Einstellungen wiederhergestellt werden. Der Befehl muss jedoch unter Umständen blind“ eingegeben werden, wenn
”
der Textmodus zerstört wurde. Sicherer ist auf jeden Fall die Verwendung eines
X11-Servers im Hintergrund.
Noch ein Hinweis zur Verwendung der libvga: Die Funktionsbibliothek benutzt
die Signale SIGUSR1 und SIGUSR2. Programme, die auf die libvga aufsetzen,
dürfen diese Signale daher nicht verwenden.
8.3.2 Ein erstes Beispiel
Nach der langen Vorrede soll jetzt ein einfaches Beispiel die Verwendung der libvga demonstrieren:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
lines.c - Linien mit der SVGALIB darstellen
*/
# include <stdio.h>
# include <vga.h>
int main()
{
int i;
if (vga_init())
return(1);
if (vga_setmode(G640x480x16))
return(1);
vga_clear();
for (i=0; i<64; i++)
{
vga_setcolor(i % 16);
vga_drawline(i*10, 0, 639-i*10, 479);
}
vga_getch();
return(0);
}
/* auf Taste warten */
258
8 Grafische Benutzeroberflächen
Um aus diesem Quelltext ein lauffähiges Programm zu generieren, muss zum
einen die libvga beim Übersetzen mit eingebunden werden. Zum anderen muss
man dem Programm ermöglichen, auf die Register der Grafik-Hardware zuzugreifen (vgl. Abschnitt 9.1.1):
> gcc lines.c -lvga -o lines
> su
Kennwort:
# chown root lines
# chmod a+s lines
# exit
Danach kann das Programm mit lines“ gestartet werden und sollte das in Ab”
bildung 8.17 dargestellte Bild erzeugen.1 Durch Drücken einer beliebigen Taste
wird es beendet.
Abbildung 8.17: Linien auf der Konsole zeichnen mit der libvga
Bei diesem Beispiel wird als Erstes die Funktion vga init() aufgerufen (Zeile 12), mit der die Benutzung der libvga initialisiert wird. Kehrt diese Funktion
mit einem Wert 6= 0 zurück, ist der Initialisierungsvorgang gescheitert. In Zeile 15 wird die Grafikauflösung eingestellt, in diesem Fall 640 × 480 Punkte mit
16 Farben. Einige der möglichen anderen Modi sind:
1
Auf einer der getesteten Linux-Installationen ließen sich die libvga-Programme trotz dieser Einstellungen nur von dem Benutzer root“ ausführen.
”
8.3 Grafik ohne X11 mit der SVGALIB
Modus
0
1
4
5
6
9
10
11
12
13
18
19
21
22
259
Alias
Auflösung
Farben
TEXT
G320x200x16
G640x480x16
G320x200x256
G320x240x256
G640x480x2
G640x480x256
G800x600x256
G1024x768x256
G1280x1024x256
G640x480x64K
G640x480x16M
G800x600x64K
G800x600x16M
–
320 × 200
640 × 480
320 × 200
320 × 240
640 × 480
640 × 480
800 × 600
1024 × 768
1280 × 1024
640 × 480
640 × 480
800 × 600
800 × 600
–
16
16
256
256
2
256
256
256
256
65.536
16.777.216
65.536
16.777.216
Doch Vorsicht! Nur wenn Ihre Grafikkarte von der libvga unterstützt wird und
den angegebenen Modus auch ermöglicht, sollten höhere Auflösungen verwendet
werden. Unproblematisch sind die Modi 1, 4, 5, 6 und 9. Diese sollten auf allen
VGA-Karten laufen. Mit dem Modus TEXT“ kann auf den Konsole-Textmodus
”
umgeschaltet werden, um beispielsweise (Fehler-)Meldungen auszugeben. Statt
den Grafikmodus im Programm fest vorzugeben, kann auch mit der Funktion
int vga getdefaultmode(void);
die Voreinstellung abgefragt werden. Diese lässt sich vom Anwender mit Hilfe
der Umgebungsvariablen SVGALIB DEFAULT MODE vorgeben, z.B.:
export SVGALIB DEFAULT MODE=G640x480x16
Die Geometrie und Farbenzahl des eingestellten Modus lässt sich über die Funktionen vga getxdim() und vga getydim() feststellen:
int mode, width, height, colors;
mode = vga_getdefaultmode();
if (vga_setmode(mode))
return(1);
width = vga_getxdim();
height = vga_getydim();
colors = vga_getcolors();
Doch zurück zum Beispielprogramm: Nach dem Einstellen des Grafikformates wird in Zeile 18 zunächst der Grafikspeicher gelöscht. Danach werden mit
vga drawline() Linien in der jeweils mit vga setcolor() eingestellten Farbe
260
8 Grafische Benutzeroberflächen
gezeichnet. Bei einer Farbenzahl von bis zu 256 ist als Parameter der Farbindex
einzusetzen – z.B. 0 für die Hintergrundfarbe. Bei True-Color-Modi (65536 Farben
und mehr) muss stattdessen die Funktion vga setrgbcolor() verwendet werden:
void vga_setrgbcolor(int red, int green, int blue);
Damit werden die Farbanteile für Rot, Grün und Blau direkt angegeben, wobei
sich die Werte zwischen 0 und 63 bewegen müssen. Im 16-Farben-Modus sind
die voreingestellten Farben: Schwarz, Dunkelblau, Dunkelgrün, Türkis, Dunkelrot, Violett, Braun, Hellgrau, Dunkelgrau, Blau, Grün, Helltürkis, Rot, Hellviolett,
Orange und Weiß.
In Zeile 26 wird mit der Funktion vga getch() auf eine Tastatureingabe gewartet,
bevor das Programm beendet wird.
8.3.3 Mit Perspektive: 3D-Funktionen zeichnen
Als Anwendungsbeispiel für die libvga soll im Folgenden ein Programm zum
Zeichnen von dreidimensionalen Funktionen vorgestellt werden:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
plot3d.c - 3D-Funktion zeichnen
*/
# include <stdio.h>
# include <vga.h>
# define f(x, y) (0.1/(0.1+(x)*(x)+(y)*(y)))
int main()
{
int i, j, xpos, ypos, xpos_old, ypos_old;
double x, y, z;
if (vga_init())
return(1);
if (vga_setmode(G640x480x16))
return(1);
vga_setpalette(0, 16, 44, 63);
vga_setpalette(1, 0, 0, 0);
vga_clear();
vga_setcolor(1);
/* hellblau */
/* schwarz */
8.3 Grafik ohne X11 mit der SVGALIB
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
261
for (i=0; i<=40; i++)
/*
for (j=0; j<=200; j++)
{
x = 0.01*j-1.0;
y = 1.0-0.05*i;
z = f(x, y)-2;
xpos = 320+900*x/(y+4);
ypos = -600*z/(y+4);
if (j > 0)
vga_drawline(xpos_old,
xpos_old = xpos;
ypos_old = ypos;
}
for (j=0; j<=40; j++)
/*
for (i=0; i<=200; i++)
{
x = 0.05*j-1.0;
y = 1.0-0.01*i;
z = f(x, y)-2;
xpos = 320+900*x/(y+4);
ypos = -600*z/(y+4);
if (i > 0)
vga_drawline(xpos_old,
xpos_old = xpos;
ypos_old = ypos;
}
vga_getch();
return(0);
}
horizontale Linien */
ypos_old, xpos, ypos);
vertikale Linien */
ypos_old, xpos, ypos);
/* auf Taste warten */
Wird dieses Programm analog zu Abschnitt 8.3.2 übersetzt und gestartet, sollte
die in Abbildung 8.18 dargestellte Grafik erscheinen.
Da hier die Programmierung mit der libvga im Vordergrund steht, soll die für
die 3D-Darstellung erforderliche Mathematik nur im Ansatz beschrieben werden.
Die zu zeichnende Funktion f ( x, y) ist in Zeile 8 definiert – hier der Einfachheit
halber mit Hilfe einer define-Anweisung. Das Programm ist so ausgelegt, dass
sich die Werte für x und y zwischen –1 und 1 bewegen. Der Funktionswert liegt
im Intervall von 0 bis 1. Bei der hier gewählten 3D-Darstellung verläuft die xAchse von links nach rechts und die y-Achse von vorn nach hinten (senkrecht zur
Ebene des Monitors). Der Funktionswert entspricht der z-Koordinate, die bei der
3D-Darstellung von unten nach oben läuft.1
1
Bei den meisten 3D-Bibliotheken verläuft die z-Achse senkrecht zur Monitor-Ebene, bei 3DFunktionen wird der Funktionswert jedoch im Allgemeinen auf der z-Achse abgetragen.
262
8 Grafische Benutzeroberflächen
Abbildung 8.18: 3D-Funktion plotten mit der libvga
In den Zeilen 25 bis 51 werden die Gitternetzlinien gezeichnet, zuerst die horizontalen, dann die vertikalen Linien. In den Zeilen 32 und 33 bzw. 45 und 46 werden
die 3D-Koordinaten ( x, y, z) in Bildschirmkoordinaten xpos und ypos umgerechnet. Dabei beruht die Perspektive im Prinzip auf der Anwendung des Strahlensatzes.
8.3.4 Ein kleines Malprogramm
Bislang haben wir die libvga nur zur Grafikausgabe verwendet – abgesehen
von der Tastaturabfrage am Programmende. Natürlich unterstützt auch die
libvga die Maus als Eingabegerät. Dazu muss der Bibliothek jedoch mitgeteilt werden, welche Art Maus an welchem Anschluss verwendet wird, z.B.
eine Microsoft-kompatible Maus an COM1 oder eine MouseSystems 3-TastenMaus am PS/2-Anschluss. Die Konfiguration der Maus sollte in der Datei
/etc/vga/libvga.config“ vom Benutzer eingestellt sein und in der Regel
”
vom Programm beibehalten werden. Die Mausunterstützung wird in diesem Fall
aktiviert mit der Funktion
vga setmousesupport(1);
Alternativ können Maustyp und Schnittstelle mit der Funktion mouse init()
auch explizit vorgegeben werden:
int mouse_init(char *dev, int type, int samplerate);
8.3 Grafik ohne X11 mit der SVGALIB
263
Beispiel:
mouse_init("/dev/mouse", MOUSE_MICROSOFT, 60);
Dabei sollte entweder vga setmousesupport() oder mouse init() verwendet
werden, nicht beide Funktionen. Ein typischer Wert für die samplerate ist 60,
alternativ kann auch MOUSE DEFAULTSAMPLERATE eingesetzt werden.
Nach Einschalten der Mausunterstützung erscheint kein Mauszeiger! Diesen
muss das Programm selbst realisieren. Die libvga stellt lediglich die Funktionen mouse getx() und mouse gety() zur Abfrage der Mausposition sowie
mouse getbutton() zum Auswerten der Maustasten zur Verfügung.
Die Mausunterstützung funktioniert nur, wenn der Benutzer Lese- und Schreibzugriff auf das entsprechende Device hat (z.B. /dev/psaux“ für eine Maus
”
am PS/2-Anschluss). Ferner muss der Aufruf von vga setmousesupport()
bzw. mouse init() vor vga setmode() erfolgen.
Das folgende kleine Malprogramm soll die Benutzung der Mausfunktionen der
libvga demonstrieren:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
vga_draw.c - SVGALIB-Malprogramm
*/
# include <stdio.h>
# include <vga.h>
# include <vgamouse.h>
int main()
{
int x, y, x_old, y_old, i, color;
static unsigned char background[21][21];
if (vga_getmousetype() == MOUSE_NONE)
{
fprintf(stderr, "No mouse configured.\n");
return(1);
}
if (vga_init())
return(1);
vga_setmousesupport(1);
/* Maus vorbereiten */
264
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
8 Grafische Benutzeroberflächen
if (vga_setmode(G640x480x16))
return(1);
vga_setpalette(0, 24, 48, 60);
vga_setpalette(1, 0, 0, 0);
/* Hellblau */
/* Schwarz */
vga_clear();
for (i=0; i<448; i++)
/* Palette zeichnen */
{
vga_setcolor(1+i/32);
vga_drawline(0, i, 32, i);
}
vga_setcolor(1);
/* Box mit aktueller Farbe */
vga_drawline(1, 478, 1, 449);
vga_drawline(1, 478, 31, 478);
vga_drawline(31, 478, 31, 449);
vga_drawline(1, 449, 31, 449);
color = 5;
/* aktuelle Farbe */
vga_setcolor(color);
for (i=451; i<477; i++)
vga_drawline(3, i, 29, i);
x = x_old = 320;
y = y_old = 240;
/* Startkoordinaten */
mouse_setposition(x, y);
mouse_setxrange(10, 629);
mouse_setyrange(10, 469);
mouse_setscale(32);
/* Maus einstellen */
for (i=0; i<21; i++)
vga_getscansegment(background[i],
x-10, y-10+i, 21);
vga_setcolor(1);
vga_drawline(x-10, y, x+10, y);
vga_drawline(x, y-10, x, y+10);
do
{
mouse_waitforupdate();
for (i=0; i<21; i++)
vga_drawscansegment(background[i],
x-10, y-10+i, 21);
8.3 Grafik ohne X11 mit der SVGALIB
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
265
x = mouse_getx();
y = mouse_gety();
if (mouse_getbutton() == MOUSE_LEFTBUTTON)
{
if (x > 32)
/* Zeichenflaeche */
{
vga_setcolor(color);
vga_drawline(x_old, y_old, x, y);
x_old = x;
y_old = y;
}
else
/* Farbauswahl */
{
color = y/32+1;
vga_setcolor(color);
for (i=451; i<477; i++)
vga_drawline(3, i, 29, i);
}
}
else
{
x_old = x;
y_old = y;
}
for (i=0; i<21; i++)
vga_getscansegment(background[i],
x-10, y-10+i, 21);
vga_setcolor(1);
vga_drawline(x-10, y, x+10, y);
vga_drawline(x, y-10, x, y+10);
}
while (mouse_getbutton() != MOUSE_RIGHTBUTTON);
return(0);
}
In Zeile 12 wird ein Feld von 21 × 21 Farbwerten angelegt. Dieses wird zum
Sichern des Hintergrundes unter dem Mauszeiger“ verwendet. Die Mausun”
terstützung wird in Zeile 23 aktiviert. Danach erfolgt das Zeichnen der Farbpalette
am linken Bildschirmrand. Mit mouse setposition() wird in Zeile 51 die Anfangsposition des Mauszeigers initialisiert. In den beiden folgenden Zeilen wird
der Bereich, in dem sich die Mauskoordinaten bewegen können, eingeschränkt.
Das ist normalerweise nicht erforderlich, dann muss man sich jedoch um das
Clipping“ (Abschneiden) des Mauszeigers an den Bildschirmrändern kümmern.
”
266
8 Grafische Benutzeroberflächen
Als letzte Vorbereitung wird in Zeile 54 die Geschwindigkeit“ der Maus einge”
stellt: Je kleiner der Parameter von mouse setscale() ist, desto schneller bewegt
sich anschließend der Mauszeiger.
Mit der for-Schleife (Zeile 56 bis 58) wird die Grafik in dem Bereich gesichert,
der durch das Zeichnen des Mauszeigers verändert wird. Dazu wird die Funktion
vga scansegment() verwendet, mit der die Farbwerte von mehreren aufeinanderfolgenden Bildpunkten ausgelesen werden können:
void vga_scansegment(unsigned char *colors,
int x, int y, int width);
Der Mauszeiger selbst – hier als Fadenkreuz ausgeführt – wird in den Zeilen 60 und 61 gezeichnet. Danach folgt der Kern des Programms: die do-whileSchleife, in der die Mausposition abgefragt und auf das Drücken der Maustasten reagiert wird. Das Programm kann durch Betätigung der rechten Maustaste beendet werden (siehe Zeile 100). Am Anfang des Schleifenrumpfes hält die
Funktion mouse waitforupdate() den Prozess so lange an, bis eine Mausbewegung (bzw. das Drücken einer Maustaste) erfolgt. Mit der for()-Schleife in
den Zeilen 66 bis 68 wird dann zunächst die ursprüngliche Grafik unter dem
Mauszeiger wiederhergestellt. Dazu bedient sich das Programm der Funktion
vga drawscansegment(), die als Pendant zu vga getscansegment() eine Folge von Farbwerten auf eine Reihe von Bildschirmpunkten überträgt. Danach folgt
die Auswertung der Mausposition und -tasten (Zeile 69 bis 92), das Sichern des
Hintergrundes an der neuen Mausposition (Zeile 93 bis 95) und zuletzt das Neuzeichnen des Mauszeigers (Zeile 96 bis 98).
8.3.5 Erweiterte Funktionen mit der libvgagl
Wie Sie vermutlich bereits bemerkt haben, ist der Funktionsumfang der libvga
relativ gering; sie erlaubt das Zeichnen von Punkten und Linien – Funktionen
zur Darstellung von Kreisen oder Text enthält sie nicht. Werden komplexere Grafikfunktionen benötigt, so kann auf eine Erweiterung der libvga zurückgegriffen
werden: die libvgagl. Diese Funktionsbibliothek setzt auf die libvga auf und bietet
eine Vielzahl weiterer Grafikfunktionen. Sie ermöglicht auch die Verwendung von
Grafikbeschleunigern (Blitter oder Accelerator). Leider setzt die libvgagl eine Grafikauflösung mit 8, 16, 24 oder 32 Bit pro Pixel voraus, und auch hier werden einige
Grafikmodi nicht voll unterstützt. Es gibt jedoch einen Trick, mit dem man Funktionen der libvgagl auf alle von der libvga zur Verfügung gestellten Grafikmodi
anwenden kann. Dazu muss zunächst in einem virtuellen Grafikspeicher gezeichnet und dann mit der Funktion gl copyscreen() die Grafik in den Bildspeicher
kopiert werden. Das folgende Programm nutzt dieses Verfahren, um verschiedene Funktionen der libvgagl bei einem Grafikmodus von 640 × 480 Punkten und
16 Farben anzuwenden:
8.3 Grafik ohne X11 mit der SVGALIB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
267
/*
vgagl_demo.c - komplexere Grafik mit der LIBVGAGL
*/
# include <stdio.h>
# include <vga.h>
# include <vgagl.h>
int main()
{
int mode;
GraphicsContext gc;
if (vga_init())
return(1);
mode = G640x480x16;
if (vga_setmode(mode))
return(1);
gl_setcontextvga(mode);
gl_getcontext(&gc);
/* GraphicsContext holen */
vga_setpalette(0, 58, 58, 58);
vga_clear();
gl_setcontextvgavirtual(mode);
gl_setfont(8, 8, gl_font8x8); /* Text vorbereiten */
gl_setwritemode(FONT_COMPRESSED
| WRITEMODE_OVERWRITE);
gl_setfontcolors(15, 1);
gl_write(10, 10, "Dies ist ein Test.");
gl_printf(10, 20, "Breite=%d", vga_getxdim());
gl_circle(320, 240, 100, 1);
gl_fillbox(10, 400, 100, 70, 5);
gl_copyscreen(&gc);
/* Grafik ins Video-RAM */
vga_getch();
/* auf Taste warten */
return(0);
}
268
8 Grafische Benutzeroberflächen
Beim Übersetzen des Programms muss auch die libvgagl eingebunden werden.
Dies sollte vor dem Einbinden der libvga geschehen:
gcc vgagl demo.c -lvgagl -lvga -o vgagl demo
Bevor das Programm gestartet werden kann, müssen auch für dieses Beispiel
zunächst die Schritte aus Abschnitt 8.3.2 durchgeführt werden, um den Zugriff
auf die Hardware zu ermöglichen.
In Zeile 12 des Quelltextes wird eine Variable vom Typ GraphicsContext (gc)
definiert. Anders als bei X11 und GTK beinhaltet diese Struktur keine Informationen über den Zeichenmodus, sondern beschreibt die Eigenschaften des eingestellten Grafikmodus (Breite, Höhe, Anzahl der Farben, . . . ). Nachdem die Grafik
wie bei den vorherigen Beispielprogrammen initialisiert wurde (Zeile 14 bis 19),
wird in Zeile 20 und 21 der gleiche Grafikmodus auch für die Funktionen der libvgagl eingestellt und der zugehörige Graphics Context gesichert. Vor Benutzung
der libvgagl-Funktionen wird in Zeile 26 dann auf einen virtuellen Bildspeicher
der gleichen Größe und Farbtiefe umgeschaltet. Erst in Zeile 38 wird schließlich
der Inhalt des virtuellen Bildspeichers in den tatsächlichen Grafikspeicher kopiert.
Hier benötigt man als Ziel“ für die Funktion gl copyscreen() den zuvor gesi”
cherten Graphics Context.
Wie schon an diesem kleinen Beispielprogramm erkennbar ist, stellt die libvgagl
deutlich komfortablere Funktionen als die libvga zur Verfügung. So kann beispielsweise mit gl printf() ein formatierter Text in die Grafik geschrieben werden. Auch das Zeichnen von Kreisen und ausgefüllten Rechtecken ist mit jeweils
einem Befehl möglich.
8.3.6 Weitere Informationsquellen
Die Nutzung der libvga, insbesondere der libvgagl, konnte im Rahmen dieses
Buches nur angerissen werden. Weitere, ausführlichere Informationen bieten zum
einen die man“-Hilfeseiten
”
man svgalib
man vgagl
man libvga.config
Zum anderen stellt die Organisation, die die libvga pflegt und weiterentwickelt,
eine Internet-Seite (siehe [20]) mit Beispielen und einem Tutorial zur Verfügung.
Die Version 1.9.25 enthält zusätzlich die Funktionsibliothek lib3dkit“ zum Zeich”
nen von 3D-Objekten mit Schattierung und Lichteffekten. Abbildung 8.19 zeigt
den Screenshot des Demo-Programms plane“, das ebenfalls in dem Quelltextpa”
ket der Version 1.9.25 enthalten ist.
8.3 Grafik ohne X11 mit der SVGALIB
269
Abbildung 8.19: Die lib3dkit“ erlaubt das Zeichnen komplexer 3D-Objekte – hier am Bei”
spiel des Demo-Programms plane“.
”
Kapitel 9
Hardware-Programmierung
In der Anfangszeit der Home-Computer“ – wie etwa eines C 64“ – war es durch”
”
aus üblich, durch direkte Zugriffe auf die Hardware des Computers (unter Umgehung des Betriebssystems) die maximale Performance der Programme zu erreichen. Bei modernen, komplexen Betriebssystemen auf heutigen Computern ist
solche Hardware-nahe Programmierung eigentlich verpönt – das Betriebssystem
verweigert sie dem normalen“ Benutzer sogar: Zugriffe auf Adressen außerhalb
”
des zugewiesenen Speichers oder gar auf I/O-Ports werden strikt mit der Meldung Segmentation fault“ (bzw. Speicherzugriffsfehler“) abgelehnt.
”
”
Möchte man jedoch auf Hardware zugreifen, für die es (noch) keinen Treiber gibt,
oder will man einfach mal ausprobieren, was bestimmte Hardware-Komponenten
alles hergeben“ können, so sind direkte Hardware-Zugriffe eine mögliche Alter”
native. Aber Vorsicht:
Direkte (Schreib-)Zugriffe auf die Hardware des Computers können zu
Systemabstürzen und damit unter Umständen auch zu Datenverlust führen!
Generell sollte daher für das Schreiben solcher Programme gelten: Be careful!“
”
9.1 Hardware-nahe Programme schreiben
Wie bereits oben erwähnt, können Programme nicht ohne Weiteres direkt auf
Hardware-Komponenten zugreifen, aus Sicherheitsgründen wird dies vom Betriebssystem verhindert. Um dennoch in eigenen Programmen solche Zugriffe zu
realisieren, muss man bestimmte Bedingungen erfüllen – bis hin zu einer speziellen Option für den C-Compiler. In den folgenden Abschnitten werden die für die
Hardware-nahe Programmierung erforderlichen Schritte erläutert.
272
9 Hardware-Programmierung
9.1.1 Eigene Programme mit root-Rechten ausstatten
Grundsätzlich ist es nur dem Betriebssystem, also dem Kernel, und dem Benutzer
root“ gestattet, auf die Hardware zuzugreifen. Doch wie ist es dann möglich,
”
dass jeder normale“ Benutzer die grafische Oberfläche X11 starten kann, die ja
”
massiv auf die Grafik-Chips des Computers zugreift?1
Unter Linux gibt es neben den Zugriffsrechten Lesen (R), Schreiben (W) und
Ausführen (X) auch das Recht set user ID“ (S). Ist dieses Bit bei einer ausführbaren
”
Datei gesetzt, so erhält der ausführende Prozess für die Dauer der Ausführung die
Benutzer-ID dieser Datei. Gehört also ein Programm z. B. dem Benutzer root“,
”
werden dem ausführenden Prozess root-Privilegien verliehen. Die Vergabe der
Zugriffsrechte erfolgt mit dem Programm chmod“, der Besitzer einer Datei kann
”
mit chown“ geändert werden. Die einzelnen Schritte, mit denen ein eigenes Pro”
gramm mit root-Rechten ausgestattet wird, sehen dann folgendermaßen aus:
> gcc mein prog.c -o mein prog
> su
Kennwort:
# chown root:root mein prog
# chmod a+s mein prog
# exit
Wenn Sie das Programm mein prog“ jetzt als normaler Benutzer starten, läuft
”
es dennoch mit root-Privilegien – und kann entsprechenden Schaden anrichten!
Aus diesem Grund sollte ein solches Programm diese Privilegien mit Hilfe des
Funktionsaufrufs setuid(getuid());“ wieder abgeben, sobald sie nicht mehr
”
benötigt werden.
9.1.2 Zugriff auf I/O-Ports freischalten
Als zweite Sicherung gegen (unbeabsichtigte) Zugriffe auf Komponenten des
Computers wird auch dem Benutzer root zunächst der Zugriff auf die Hardware verweigert (→ Speicherzugriffsfehler). Zugriffe auf I/O-Ports müssen zuvor
explizit freigeschaltet werden. Dafür stehen zwei Funktionen zur Verfügung:
int iopl(int level);
int ioperm(unsigned long from, unsigned long num,
int turn_on);
Mit der Funktion iopl() kann der Level der Zugriffsmöglichkeiten auf I/OPorts von 0 (keine Erlaubnis) bis 3 (voller Zugriff) eingestellt werden. Eine Beschränkung des Adressbereiches ist mit iopl() nicht möglich. Die Funktion
1
Der von den X-Server-Programmen benutzte Mechanismus wurde mittlerweile durch ein komplexeres Verfahren ersetzt. Ältere Versionen nutzten jedoch das hier vorgestellte Prinzip.
9.1 Hardware-nahe Programme schreiben
273
ioperm() gibt dagegen einen bestimmten I/O-Adressbereich für Zugriffe frei,
und zwar num Bytes ab der Adresse from. Der Parameter turn on kann hier 1
(Zugriff freischalten) oder 0 (Zugriff sperren) sein.
9.1.3 Zugriff auf die I/O-Ports
Die Hardware-Komponenten des Computers sind nicht wie das RAM über normale Speicherzugriffe zugänglich, sondern nur mit Hilfe spezieller (Assembler-)
Befehle erreichbar. Für C-Programme werden eine Reihe von Makros zur Verfügung gestellt, die diese Befehle auf C-Funktionen abbilden. Tabelle 9.1 gibt eine
Übersicht dieser Funktionen.
Tabelle 9.1: Makros für den Zugriff auf I/O-Ports
Makro
Beschreibung
inb(Adresse)
inw(Adresse)
inl(Adresse)
insb(Adresse)
insw(Adresse)
insl(Adresse)
Der Inhalt des I/O-Bereichs an der Adresse
Adresse wird ausgelesen. Dabei wird je nach
Endung des Makros ein Byte (b), Word (w) oder
Long-Word (l) zurückgegeben, wahlweise auch
mit Vorzeichen (s).
outb(Wert,Adresse)
outw(Wert,Adresse)
outl(Wert,Adresse)
outsb(Wert,Adresse)
outsw(Wert,Adresse)
outsl(Wert,Adresse)
Der Wert Wert wird in den I/O-Bereich an die
Adresse Adresse geschrieben. Dabei wird Wert je
nach Endung des Makros als Byte (b), Word (w)
oder Long-Word (l) interpretiert, wahlweise
auch mit Vorzeichen (s).
inb p(Adresse)
inw p(Adresse)
inl p(Adresse)
outb p(Wert,Adresse)
outw p(Wert,Adresse)
outl p(Wert,Adresse)
Im Unterschied zu den Makros ohne p“ wird
”
bei diesen Funktionen die Ausführung etwas
verzögert, um langsamer“ Hardware
”
Rechnung zu tragen.
Es handelt sich bei diesen Funktionen um Inline Makros, die nur verfügbar sind,
wenn das Programm mit der Option -O oder -O2 übersetzt wird. Auf aktuellen Linux-Systemen sind diese Makros in der Include-Datei <sys/io.h>“ de”
finiert, auf älteren Linux-Distributionen werden stattdessen die beiden Dateien
<asm/io.h>“ und <unistd.h>“ benötigt.
”
”
274
9 Hardware-Programmierung
9.2 Ansteuerung des Parallelports
Als Beispiel für (relativ risikolose) Hardware-Zugriffe soll in den folgenden Abschnitten der Parallelport (also die Druckerschnittstelle) angesteuert werden. Dieser Anschluss bietet mehrere digitale Ein- und Ausgänge und eignet sich daher
gut zur Ansteuerung externer, digitaler Hardware.
9.2.1 Beschreibung des Parallelports
Die Parallel- oder Druckerschnittstelle verfügt über acht Datenausgänge, vier
Steuerausgänge und fünf Statusleitungen (Eingänge). Die Eingänge sind über interne Widerstände (2 kΩ. . . 5 kΩ) auf 5 V gelegt, um auch bei offenen Anschlüssen
einen definierten Pegel zu erreichen. Abbildung 9.1 zeigt die Anschlussbelegung
der Schnittstelle, wie sie sich darstellt, wenn man von außen auf die Buchse am
PC schaut.
13 12 11 10 9 8 7 6 5 4 3 2 1
25 24 23 22 21 20 19 18 17 16 15 14
Pin Nr.
Typ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 – 25
Ausgang
Ausgang
Ausgang
Ausgang
Ausgang
Ausgang
Ausgang
Ausgang
Ausgang
Eingang
Eingang
Eingang
Eingang
Ausgang
Eingang
Ausgang
Ausgang
—
—
Funktion
Steuerleitung 0 (invertiert)
Datenleitung 0
Datenleitung 1
Datenleitung 2
Datenleitung 3
Datenleitung 4
Datenleitung 5
Datenleitung 6
Datenleitung 7
Statusleitung 6
Statusleitung 7 (invertiert)
Statusleitung 5
Statusleitung 4
Steuerleitung 1 (invertiert)
Statusleitung 3
Steuerleitung 2
Steuerleitung 3 (invertiert)
Masse (0 V)
in der Regel auf Masse
Abbildung 9.1: Anschlussbelegung des Parallelports
9.2 Ansteuerung des Parallelports
275
Die I/O-Basisadresse, an der der Parallelport angesprochen werden kann, ist entweder 3BChex , 378hex oder 278hex. An dieser Adresse liegt das Datenregister; ein
Byte, das in dieses Register geschrieben wird, liegt dann an den Datenleitungen
0 bis 7 an. An der nächsthöheren Adresse (Basis-Adresse + 1) liegt das Statusregister, das den Zustand der Statusleitungen enthält. Die Steuerleitungen können
über das Steuerregister (Basisadresse + 2) verändert werden.
9.2.2 Die Adresse des Parallelports suchen
Beim Booten des Linux-Systems sucht“ der Kernel an den oben genannten
”
Adressen nach Parallelports und merkt sich diese für die Devices /dev/lpn. Leider gibt der Kernel diese Adressen für normale“ Programme nicht preis, sodass
”
wir den Parallelport selbst ausfindig machen müssen.
Dieser Vorgang ist relativ einfach: Man schreibt an die möglichen Basisadressen
eine 0 und schaut anschließend nach, ob dieser Inhalt erhalten bleibt. Liegt an dieser Stelle ein Parallelport – genauer gesagt: das Datenregister eines Parallelports
–, wird der Wert über die Datenleitungen ausgegeben und kann auch wieder gelesen werden. Befindet sich an der zu testenden Basisadresse keine Hardware,
erhält man beim Auslesen in der Regel den Wert 255. Das folgende Programm
testet die möglichen Adressen und gibt diejenige aus, an der ein Parallelport gefunden wurde:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
find_port.c
*/
# include <stdio.h>
# if defined __GLIBC__
#
include <sys/io.h>
# else
#
include <unistd.h>
#
include <asm/io.h>
# endif
int main()
{
int i, port = 0;
const int base_adr[3] = {0x3bc, 0x378, 0x278};
if (iopl(3) != 0)
{
perror("find_port: Can’t set I/O permissions");
return(1);
276
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
9 Hardware-Programmierung
}
for (i=0; i<3; i++)
{
outb_p(0, base_adr[i]);
if (inb_p(base_adr[i]) == 0)
port = base_adr[i];
}
if (port == 0)
{
fprintf(stderr,
"find_port: No parallel port found.\n");
return(1);
}
printf("Parallel port found at 0x%x.\n", port);
return(0);
}
Beim Übersetzen des Programms ist die Compiler-Option -O“ (oder -O2“) zu
”
”
verwenden:
gcc -O find port.c -o find port
Bevor das Programm gestartet werden kann, muss noch das Zugriffsrecht set user
ID eingestellt werden (siehe Abschnitt 9.1.1).
9.2.3 Ein Beispiel: LED-Lauflicht“
”
Als kleines Anwendungsbeispiel für die Ansteuerung des Parallelports soll hier
ein Lauflicht mit Leuchtdioden (LEDs) realisiert werden. Abbildung 9.2 zeigt das
Schaltbild; die Widerstände sollten je nach Leuchtdiodentyp zwischen 680 Ω und
1 kΩ ausgelegt sein. Dass man diese Schaltung auch ganz ohne Platine realisieren
kann, zeigt Abbildung 9.3.
Das folgende Programm lässt einen Leuchtpunkt“ zwischen den beiden Enden
”
der Leuchtdiodenreihe hin- und herwandern. Synchron dazu wird die Position
auch im Terminalfenster dargestellt:
1
2
3
4
5
6
/*
LED-line.c
*/
# include <stdio.h>
# include <unistd.h>
9.2 Ansteuerung des Parallelports
277
Parallelport
2
3
4
5
6
7
8
9
18
Abbildung 9.2: Schaltbild für das LED-Lauflicht
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# if defined __GLIBC__
#
include <sys/io.h>
# else
#
include <asm/io.h>
# endif
int get_lp_base(void)
{
int i, base_adr[3] = {0x3bc, 0x378, 0x278};
for (i=0; i<3; i++)
{
outb_p(0, base_adr[i]);
if (inb_p(base_adr[i]) == 0)
return(base_adr[i]);
}
return(0);
}
278
9 Hardware-Programmierung
Abbildung 9.3: Das LED-Lauflicht – ganz ohne Platine. Zusätzlich sind zwei Taster an den
Statusleitungen 5 und 6 des Parallelports angeschlossen.
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int main()
{
int i, j, port_adr;
char light[12] = "
0 \r";
if (iopl(3) != 0)
{
perror("LED-line: Can’t set I/O permissions");
return(1);
}
if ((port_adr = get_lp_base()) == 0)
{
fprintf(stderr,
"LED-line: No parallel port found.\n");
return(1);
}
printf("Parallel port found at 0x%x.\n", port_adr);
i = 0;
j = 1;
while (1)
9.3 Modem-Steuerleitungen abfragen
49
50
51
52
53
54
55
56
57
58
59
279
{
outb(1<<i, port_adr);
write(1, &(light[i]), 11-i);
usleep(100000L);
/* 100000 Mikrosek. warten */
i += j;
if ((i == 0) || (i == 7))
j = -j;
}
return(0);
}
Zum Übersetzen des Programms ist auch hier wieder die Option -O“ erforder”
lich. Vor dem Starten des Programms als normaler“ Benutzer müssen ferner die
”
Zugriffsrechte gemäß Abschnitt 9.1.1 eingestellt werden.
9.3 Modem-Steuerleitungen abfragen
Ähnlich wie der Parallelport lässt sich auch der COM-Port (die serielle Schnittstelle RS 232) des Computers ansteuern. Beim Anschluss eines externen Gerätes
an dieser Schnittstelle kann es notwendig sein, den Pegel auf den Steuerleitungen
abzufragen, z.B. um festzustellen, ob das angeschlossene Gerät bereit ist, Daten
anzunehmen. Tabelle 9.2 beschreibt die Funktion dieser Steuerleitungen.
Tabelle 9.2: Bedeutung der (Modem-)Steuerleitungen
Leitung
Funktion
CTS
Clear To Send – das angeschlossene Gerät ist bereit, Daten
anzunehmen, d.h. es erteilt Sendeerlaubnis.
DSR
Data Set Ready – das angeschlossene Gerät ist bereit, dem
Computer Daten zu senden.
RI
Ring Indicator – eingehender Anruf (nur Modem).
CD
Carrier Detect – Modem-Trägersignal erkannt,
Verbindung zum Modem der Gegenstelle besteht (nur
Modem).
Der Status dieser Steuerleitungen wird in dem Modem Status Register (MSR) des
UART1 abgebildet. Die Registeradressen und Bit-Nummern der Steuerleitungen
sind in der Include-Datei <linux/serial reg.h>“ definiert. Die Basisadres”
se des UART lässt sich mit Hilfe des ioctl()-Kommandos TIOCGSERIAL abfra1
Universal Asynchronous Receiver Transmitter – ein Chip zur Ansteuerung der seriellen Schnittstelle
280
9 Hardware-Programmierung
gen (siehe auch Kapitel 6). Der entsprechende ioctl()-Aufruf benötigt dazu den
Zeiger auf eine Variable vom Typ struct serial struct. In diese Variable trägt
das Device unter anderem die Basisadresse des UART und die Interrupt-Nummer
(IRQ) ein.
Das folgende Programm erwartet als Kommandozeilenparameter den Pfadnamen
für das Device der seriellen Schnittstelle, z.B. /dev/ttyS0“. Es öffnet das ange”
gebene Device, holt mit ioctl() die Basisadresse des zugehörigen UART und
liest dann in einer Endlosschleife das Modem-Status-Register aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
serialinfo.c - Informationen ueber die
serielle Schnittstelle holen
*/
#
#
#
#
#
#
#
#
#
#
#
#
include <stdio.h>
include <unistd.h>
include <string.h>
include <fcntl.h>
include <sys/ioctl.h>
include <linux/serial.h>
include <linux/serial_reg.h>
if defined __GLIBC__
include <sys/io.h>
else
include <asm/io.h>
endif
int main(int argc, char *argv[])
{
int fd, base_adr, msr;
struct serial_struct serial_port;
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
{
printf("Usage: serialinfo device\n");
return(0);
}
if ((fd = open(argv[1], O_RDWR)) == -1)
{
perror("serialinfo: Can’t open device");
return(1);
}
9.3 Modem-Steuerleitungen abfragen
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
281
if (ioctl(fd, TIOCGSERIAL, &serial_port) == -1)
{
perror("serialinfo: ioctl() failed");
return(1);
}
base_adr = serial_port.port;
printf("Device:\t%s (COM%d)\nPort:\t0x%x\n"
"IRQ:\t%d\n", argv[1], serial_port.line+1,
serial_port.port, serial_port.irq);
ioperm(base_adr+UART_MSR, 1, 1);
printf("Line status:\nCD RI DSR CTS\n");
while(1)
{
msr = inb(base_adr+UART_MSR);
printf("\r %d
%d
%d
%d",
(msr & UART_MSR_DCD)? 1 : 0,
(msr & UART_MSR_RI)? 1 : 0,
(msr & UART_MSR_DSR)? 1 : 0,
(msr & UART_MSR_CTS)? 1 : 0);
fflush(stdout);
usleep(100000L);
}
ioperm(base_adr+UART_MSR, 1, 0);
close(fd);
return(0);
}
Nach Öffnen des Devices, dessen Pfadname als Kommandozeilenparameter angegeben werden muss, erfolgt in Zeile 36 der ioctl()-Aufruf, mit dem die Variable
serial port initialisiert wird. Diese Struktur enthält dann unter anderem die
Basisadresse der Schnittstelle (Zeile 41).
Im Gegensatz zu den vorherigen Beispielprogrammen muss dieses Programm
nur auf eine I/O-Adresse zugreifen, sodass sich hier zum Freischalten die Funktion ioperm() anbietet (Zeile 46). Wie die Beispiele zuvor muss auch dieses Programm mit der Option -O“ übersetzt und mit root-Rechten ausgestattet werden
”
(siehe Abschnitt 9.1.1). Danach kann es z.B. mit
serialinfo /dev/ttyS0
für die Abfrage der ersten seriellen Schnittstelle (COM1) gestartet werden.
Kapitel 10
Beispielprojekte
In den Kapiteln 3 bis 9 haben wir gezeigt, wie man auf Devices zugreift, wie
Interprozess- und Netzwerkkommunikation funktionieren und wie man eine grafische Oberfläche erstellt. Echte“ Software-Projekte beinhalten in der Regel eine
”
Kombination aus mehreren dieser Themen. Aus diesem Grund wird in den folgenden Abschnitten anhand von zwei Beispielprojekten gezeigt, wie man solche
Projekte aufbaut und strukturiert und wie das Zusammenspiel der in den vorangegangenen Kapiteln vorgestellten Themen funktioniert.
10.1 WebCam: Video-Übertragung per HTTP
Als erstes Beispielprojekt erstellen wir eine Applikation, die aus einer USBKamera (oder anderen Video-Quelle) eine echte Webcam“ macht. Das Videobild
”
soll also über das Netzwerk mit einem Webbrowser wie Firefox dargestellt werden. Dieses Beispielprojekt beinhaltet folgende Themen:
Auswertung der Kommandozeilenparameter mit getopt()
Dateizugriffe inkl. stat() und remove()
Prozesse und Signale (fork(), signal())
Ansteuerung des Video4Linux-Devices
Verwendung der libjpeg zur JPEG-Kompression
Netzwerkkommunikation mit TCP und HTTP inkl. Authentifizierung
Aufteilung des Quelltextes und Erstellen eines Makefiles
284
10 Beispielprojekte
10.1.1 Wie die Bilder laufen lernen
In Abschnitt 7.3.3 hatten wir bereits ein kleines Webserver-Programm vorgestellt,
das es ermöglicht, HTML-Seiten und Grafiken über Webbrowser abzurufen. Doch
wie kann das Live-Videobild einer Kamera als bewegtes Bild“ dargestellt wer”
den? Für diesen Zweck wurde das so genannte Server-push-Prinzip entwickelt.
Fordert der Browser die Grafikdatei eines solchen Live-Videobilds an, sendet der
Webserver eine Multipart-Antwort und signalisiert dem Browser damit, dass die
angeforderte Grafik nicht aus einer einzelnen Datei, sondern einer Folge von Bildern besteht. Danach folgen die einzelnen Bilder, die jeweils durch eine Leerzeile
und das mit boundary=. . .“ definierte Schlüsselwort getrennt werden:
”
HTTP/1.1 200 OK
Content-type: multipart/x-mixed-replace;boundary=Schlüsselwort
Leerzeile
--Schlüsselwort
Content-type: image/jpeg
Content-length: Anzahl Bytes des 1. Bildes
Leerzeile
Bilddaten (JPEG): 1. Bild
Leerzeile
--Schlüsselwort
Content-type: image/jpeg
Content-length: Anzahl Bytes des 2. Bildes
Leerzeile
Bilddaten (JPEG): 2. Bild
Leerzeile
usw.
6
Bild 1
?
6
Bild 2
?
Mit Ausnahme des Internet-ExplorersTM von MicrosoftTM unterstützen alle aktuellen Browser (Firefox, Mozilla, Netscape) diese Technik, um Video-Sequenzen
darzustellen.
10.1.2 Strukturierung der Quelltexte
Die verschiedenen Funktionen unseres Webcam-Projektes lassen sich gut auf
mehrere Quelltexte aufteilen, um die einzelnen Dateien übersichtlicher zu halten:
webcam.c – Hauptprogramm mit Auswertung der Kommandozeilenparameter
und Initialisierung der libusb und des HTTP-Netzwerkports
http service.c – Abarbeitung der HTTP-Anfragen inkl. Erzeugen der MultipartAntwort für das Live-Video
usbcam.c – Ansteuerung der USB-Kamera mit Speicherung der Einzelbilder als
JPEG-Datei
10.1 WebCam: Video-Übertragung per HTTP
285
Zu den letzten beiden Quelltexten gehört auch je eine Header-Datei ( .h“), in der
”
die Funktionsaufrufe deklariert sind. Werfen wir zunächst einen Blick auf das
Hauptprogramm webcam.c“:
”
1
/*
2
webcam.c - Webserver für USB-Kamera
3
- Hauptteil 4
/
*
5
6
# include <stdio.h>
7
# include <string.h>
8
# include <stdlib.h>
9
# include <unistd.h>
10 # include <sys/types.h>
11 # include <sys/socket.h>
12 # include <netinet/in.h>
13 # include <arpa/inet.h>
14 # include <sys/stat.h>
15 # include <signal.h>
16 # include "http_service.h"
17 # include "usbcam.h"
18
19 # define IP_PORT 80
20 # define N_CONNECTIONS 10
21
22 void err_exit(char *message)
23
{
24
perror(message);
25
exit(1);
26
}
27
28 /*--------------- Hauptprogramm ---------------*/
29
30 int main(int argc, char *argv[])
31
{
32
int option, sock_fd, client_fd, cam_fd, err, pid,
33
delay, swap_RGB;
34
char *dev_name, *auth;
35
socklen_t addr_size;
36
struct sockaddr_in my_addr, client_addr;
37
38
/*---- Defaultwerte für die Einstellungen ----*/
39
swap_RGB = 0;
/* Rot/Blau tauschen? */
40
41
dev_name = "/dev/video";
/* Video4Linux device */
286
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
10 Beispielprojekte
auth = "";
delay = 100;
/* Username:Passwort, base64-codiert */
/* Wartezeit zwischen den Bildern */
/*---- Kommandozeilenparameter auswerten ----*/
while ((option = getopt(argc, argv, "hsa:d:")) >= 0)
switch (option)
{
case ’h’:
printf("Usage: %s [-s] [-d #] [-a str] [device]\n"
"-s
: swap colours from BGR to RGB\n"
"-a str : set HTTP authentication code\n"
"-d #
: set delay between images\n",
argv[0]);
return(0);
case ’s’: swap_RGB = 1;
break;
case ’a’: auth = optarg;
break;
case ’d’: sscanf(optarg, "%d", &delay);
break;
case ’?’: return(1);
/* unbekannte Option */
}
if (argc-optind > 1)
{
fprintf(stderr, "webcam: Bad arguments.\n");
return(1);
}
if (argc-optind == 1)
dev_name = argv[optind];
/*---- Socket öffnen und an Port binden ----*/
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
err_exit("webcam: Can’t create new socket");
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(IP_PORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
err = bind(sock_fd, (struct sockaddr *)&my_addr,
10.1 WebCam: Video-Übertragung per HTTP
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
287
sizeof(struct sockaddr_in));
if (err == -1)
err_exit("webcam: bind() failed");
setuid(getuid());
/* root-Rechte abgeben */
err = listen(sock_fd, N_CONNECTIONS);
if (err == -1)
err_exit("webcam: listen() failed");
/*---- Kamera öffnen und initialisieren ----*/
if ((cam_fd = init_cam(dev_name)) < 0)
return(1);
signal(SIGCHLD, SIG_IGN);
/* keine Zombie-Prozesse */
signal(SIGPIPE, SIG_IGN);
/* wenn Browser beendet */
printf("HTTP service started.\n"
"Press Ctrl-C to stop.\n");
while (1)
/*---- Endlosschleife ----*/
{
addr_size = sizeof(struct sockaddr_in);
client_fd = accept(sock_fd,
(struct sockaddr *)&client_addr, &addr_size);
if (client_fd == -1)
err_exit("webcam: accept() failed");
if ((pid = fork()) == -1)
{
fprintf(stderr, "webcam: fork() failed.\n");
return(1);
}
else if (pid == 0)
/* Kind-Prozess */
{
while (http_service(client_fd, cam_fd, delay,
swap_RGB, auth));
shutdown(client_fd, SHUT_RDWR);
close(client_fd);
return(0);
}
close(client_fd);
/* Eltern-Prozess */
288
130
131
132
133
10 Beispielprojekte
}
return(0);
/* wird nie erreicht */
}
In den Zeilen 16 und 17 werden die Header-Dateien zu den beiden anderen Quelltexten eingebunden, um die darin deklarierten Funktionen dem Hauptprogramm
bekannt zu machen. Nachdem in Zeile 40 bis 43 zunächst die Einstellungen des
Programms mit Default-Werten belegt werden, folgt die Auswertung der Kommandozeilenparameter mit Hilfe der Funktion getopt() gemäß Abschnitt 3.1.3.
Das Einrichten des Webserver-Ports 80 in den Zeilen 77 bis 94 ist bereits aus dem
Programmbeispiel in Abschnitt 7.3.3 bekannt.
In Zeile 98 folgt das Öffnen und Initialisieren des Video4Linux-Devices (USBKamera oder TV-Karte) mit Hilfe der Funktion init cam(), die im Quelltext
usbcam.c“ definiert ist (siehe unten). Den ersten der beiden anschließenden
”
signal()-Aufrufe kennen wir vom Webserver-Beispiel aus Abschnitt 7.3.3. Mit
dem zweiten Aufruf (Zeile 103) wird zusätzlich verhindert, dass beim Beenden
der Netzwerkverbindung durch den Browser – z. B. durch Schließen des Browsers
– der Kind-Prozess des Webservers ebenfalls sofort beendet wird. Dies kommt bei
der Übertragung der Videodaten zum Tragen, die ja so lange läuft, bis der Browser
beendet oder eine andere Webseite aufgerufen wird.
Im Vergleich zu dem in Abschnitt 7.3.3 beschriebenen Webserver machen wir
in diesem Programm Gebrauch von den erweiterten Funktionen des Standards
HTTP 1.1. Darin ist definiert, dass eine Verbindung zwischen Browser und
Webserver nicht nach der HTTP-Antwort beendet werden muss, sondern weitere HTTP-Anfragen und -Antworten über die gleiche Verbindung geschickt werden können.1 Aus diesem Grund werden hier in jedem Kind-Prozess die HTTPAnfragen in einer while()-Schleife (Zeile 123) abgearbeitet, bis der Browser die
Verbindung abbricht oder ein Fehler auftritt. Die Funktion http service(), die
in dem gleichnamigen Quelltext definiert ist, liefert einen Rückgabewert von 1,
wenn die Verbindung bestehen bleiben soll, und eine 0, wenn die Verbindung abgebrochen wurde.
Sehen wir uns als Nächstes den Quelltext http service.c“ an:
”
1
/*
2
http_service.c - HTTP-Server für WebCam
/
3
*
4
5
# include <stdio.h>
6
# include <string.h>
7
# include <unistd.h>
8
# include <sys/types.h>
9
# include <sys/socket.h>
1
Der Browser signalisiert dies durch die Kopfzeile Connection: keep-alive“, siehe Seite 183.
”
10.1 WebCam: Video-Übertragung per HTTP
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
289
# include <sys/stat.h>
# include <signal.h>
# include "usbcam.h"
# define START_FILE "start.html"
# define TMP_PATH "./tmp_"
# define BUFSIZE 2000
/*--------------- get_line() ---------------*/
int get_line(int sock_fd, char *buffer, int length)
{
int i;
i = 0;
while ((i < length-1)
&& (recv(sock_fd, &(buffer[i]), 1, 0) == 1))
if (buffer[i] == ’\n’)
break;
else
i++;
if ((i > 0) && (buffer[i-1] == ’\r’))
i--;
buffer[i] = ’\0’;
return(i);
}
/*--------------- file_size() ---------------*/
size_t file_size(char *filename)
{
struct stat file_info;
if (stat(filename, &file_info) == -1)
return(0);
return(file_info.st_size);
}
/*-------------- send_video() --------------*/
void send_video(int client_fd, int cam_fd, char *buffer,
int bufsize, char *cmd, int swap_RGB, int delay)
{
int length;
290
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
10 Beispielprojekte
FILE *stream;
static char tmp_name[32];
send(client_fd, "HTTP/1.1 200 OK\r\n"
"Content-type: multipart/x-mixed-replace;"
"boundary=next-jpeg-image-data\r\n\r\n", 90, 0);
if (strcmp(cmd, "HEAD") == 0)
return;
snprintf(tmp_name, 31, "%s%d.jpg", TMP_PATH,
getpid());
while (1)
/* Schleife, bis Verbindung abbricht */
{
if (get_image(cam_fd, tmp_name, swap_RGB))
break;
if ((stream = fopen(tmp_name, "r")) == NULL)
break;
sprintf(buffer, "--next-jpeg-image-data\r\n"
"Content-type: image/jpeg\r\n"
"Content-length: %ld\r\n\r\n",
file_size(tmp_name));
if (send(client_fd, buffer, strlen(buffer), 0) < 0)
{
fclose(stream);
break;
}
while (!feof(stream))
{
length = fread(buffer, 1, bufsize, stream);
if (length > 0)
if (send(client_fd, buffer, length, 0) <= 0)
{
fclose(stream);
remove(tmp_name);
return;
}
}
fclose(stream);
send(client_fd, "\r\n", 2, 0); /* ’Part’ Ende */
usleep(delay*1000L);
}
remove(tmp_name);
10.1 WebCam: Video-Übertragung per HTTP
291
98
return;
99
}
100
101
/*-------------- http_service() --------------*/
102
103 int http_service(int client_fd, int cam_fd, int delay,
104
int swap_RGB, char *auth)
105
{
106
static char buffer[BUFSIZE], cmd[8], url[256];
107
char *filename;
108
int length, auth_ok;
109
FILE *stream;
110
111
auth_ok = (auth[0] == ’\0’); /* mit Passwort? */
112
113
if (get_line(client_fd, buffer, BUFSIZE) == 0)
114
return(0);
115
116
if (sscanf(buffer, "%7s %255s", cmd, url) < 2)
117
return(0);
118
119
/*---- Kopfzeilen auswerten -----*/
120
121
while (get_line(client_fd, buffer, BUFSIZE) > 0)
122
if (strncasecmp(buffer, "Authorization:", 14) == 0)
123
if ((!auth_ok) && (strncmp(&(buffer[21]), auth,
124
strlen(auth)) == 0))
125
auth_ok = 1;
126
127
if ((strcmp(cmd, "GET") != 0)
128
&& (strcmp(cmd, "HEAD") != 0))
129
return(0);
130
/*---- Authentifizierung -----*/
131
132
if (!auth_ok)
/* Zugangsdaten OK? */
133
134
{
135
strcpy(buffer,
136
"HTTP/1.1 401 Authorization Required\r\n"
137
"WWW-Authenticate: Basic realm=\"WebCam\"\r\n"
138
"Content-Type: text/html\r\n"
139
"Content-Length: 42\r\n\r\n"
140
"<html><body>Falsche Kennung!</body></html>");
141
send(client_fd, buffer, strlen(buffer), 0);
292
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
10 Beispielprojekte
return(1);
}
filename = &(url[1]);
/* ’/’ am Anfang entfernen */
if (strcasecmp(filename, "video.jpg") == 0)
{
send_video(client_fd, cam_fd, buffer, BUFSIZE, cmd,
swap_RGB, delay);
return(0);
}
/*---- einfache Datei übertragen -----*/
if (strlen(filename) == 0)
filename = START_FILE;
if ((stream = fopen(filename, "r")) == NULL)
{
send(client_fd, "HTTP/1.1 404 Not Found\r\n"
"Content-type: text/html\r\n"
"Content-length: 91\r\n\r\n"
"<html><head><title>Error</title></head>"
"<body><hr><h2>File not found.</h2><hr>"
"</body></html>/r/n", 164, 0);
return(1);
}
sprintf(buffer, "HTTP/1.1 200 OK\r\n"
"Content-length: %ld\r\n\r\n",
file_size(filename));
if (send(client_fd, buffer, strlen(buffer), 0) <= 0)
return(0);
if (strcmp(cmd, "GET") == 0)
while (!feof(stream))
{
length = fread(buffer, 1, BUFSIZE, stream);
if (length > 0)
if (send(client_fd, buffer, length, 0) <= 0)
return(0);
}
fclose(stream);
return(1);
}
10.1 WebCam: Video-Übertragung per HTTP
293
Die Funktionen get line() (Zeile 20 bis 35) und file size() (Zeile 39 bis 46)
sind ja bereits aus dem Webserver-Beispiel (Abschnitt 7.3.3) bekannt. Neu hinzugekommen ist die Funktion send video(). Sie generiert die Multipart-Antwort
gemäß Abschnitt 10.1.1 und sendet zyklisch die Videobilder als JPEG-Daten an
den Browser. Dazu wird in Zeile 69 mit der im Quelltext usbcam.c“ definier”
ten Funktion get image() ein einzelnes Bild von der Kamera/TV-Karte abgerufen und als JPEG-Datei gespeichert. Der Dateiname für die temporäre Bilddatei wird in Zeile 64 aus der Prozess-ID des Kind-Prozesses generiert. Dadurch
wird erreicht, dass parallel laufende Video-Übertragungen (Zugriff von mehreren
Browsern auf das Video) jeweils unabhängige temporäre Dateien benutzen. Nach
Abbruch der Verbindung zum Browser wird diese Datei mit remove() wieder
gelöscht (Zeile 89 und 97).
Ab Zeile 103 beginnt die Funktion http service(), die die HTTP-Anfragen abarbeitet. Auch diese Funktion ist gegenüber unserem Webserver-Beispiel aus Abschnitt 7.3.3 erweitert: In Zeile 121 bis 125 werden die Kopfzeilen ausgewertet,
wobei nach einer HTTP-Authentifizierung gesucht wird. Wie diese Art der Zugriffskontrolle funktioniert, beschreiben wir in Abschnitt 10.1.3. Außerdem wird
in Zeile 147 bis 152 geprüft, ob als URL video.jpg“ angegeben wurde. Wenn ja,
”
wird anstelle einer HTML- oder Bild-Datei das Live-Videobild mit Hilfe der Funktion send video() gesendet.
In der zum Quelltext http service.c“ gehörenden Header-Datei ist lediglich die
”
Funktion http service() deklariert, da nur diese aus den anderen Quelltexten
heraus aufgerufen wird:
1
2
3
4
5
6
/*
http_service.h - HTTP-Server für WebCam
*/
int http_service(int client_fd, int cam_fd, int delay,
int swapRGB, char *pass);
In dem dritten Quelltext usbcam.c“ sind die Funktionen zusammengefasst, die
”
das Video4Linux-Device ansteuern:
1
2
3
4
5
6
7
8
9
10
11
12
/*
usbcam.c - Kamera-Ansteuerung für WebCam
*/
#
#
#
#
#
#
include
include
include
include
include
include
<stdio.h>
<unistd.h>
<fcntl.h>
<sys/ioctl.h>
<linux/videodev.h>
<jpeglib.h>
# define WIDTH 320
/* Bildgröße QVGA */
294
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
10 Beispielprojekte
# define HEIGHT 240
# define IMGSIZE (WIDTH*HEIGHT*3L)
# define JPEG_QUALITY 70
/* Bildspeicher */
/*--------------- init_cam() ---------------*/
int init_cam(char *device_name)
{
int fd;
struct video_window video_win;
if ((fd = open(device_name, O_RDONLY)) == -1)
{
perror("webcam: Can’t open video device");
return(-1);
}
if (ioctl(fd, VIDIOCGWIN, &video_win) == -1)
{
perror("webcam: Can’t get video window");
return(-1);
}
video_win.width = WIDTH;
video_win.height = HEIGHT;
if (ioctl(fd, VIDIOCSWIN, &video_win) == -1)
{
perror("webcam: Can’t set video window");
return(-1);
}
return(fd);
}
/*--------------- get_image() ---------------*/
int get_image(int cam_fd, char *tmpfile, int swap_RGB)
{
int i, tmp;
long len;
static unsigned char image[IMGSIZE];
FILE *stream;
JSAMPROW row_pointer;
10.1 WebCam: Video-Übertragung per HTTP
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
static struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
len = read(cam_fd, image, IMGSIZE);
if (len < IMGSIZE)
{
perror("webcam: Error while reading");
if (len < 0)
return(1);
}
if (swap_RGB)
/* Rot und Blau tauschen? */
for (i=0; i<IMGSIZE; i+=3)
{
tmp = image[i];
image[i] = image[i+2];
image[i+2] = tmp;
}
/* ----- JPEG-Umwandlung vorbereiten -----*/
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
if ((stream = fopen(tmpfile, "w")) == NULL)
{
perror("webcam: Can’t open temporary file.");
return(1);
}
jpeg_stdio_dest(&cinfo, stream);
cinfo.image_width = WIDTH;
cinfo.image_height = HEIGHT;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, JPEG_QUALITY, TRUE);
/* ----- JPEG-Umwandlung starten -----*/
jpeg_start_compress(&cinfo, TRUE);
295
296
101
102
103
104
105
106
107
108
109
110
111
112
113
10 Beispielprojekte
while (cinfo.next_scanline < HEIGHT)
{
row_pointer = &(image[cinfo.next_scanline*WIDTH*3]);
jpeg_write_scanlines(&cinfo, &row_pointer, 1);
}
jpeg_finish_compress(&cinfo);
/* Bild speichern */
jpeg_destroy_compress(&cinfo);
/* aufräumen */
fclose(stream);
return(0);
}
Bildgröße und JPEG-Kompressionsgrad sind mit define-Anweisungen fest eingestellt (Zeile 12 bis 15). In der Funktion init cam() wird das Device geöffnet
und das Bildformat eingestellt (Zeile 19 bis 46). Die Funktion get image() ab
Zeile 50 liest ein Bild aus der Videoquelle und wandelt es in eine JPEG-Datei um
(siehe auch Seite 139).
Beide in diesem Quelltext definierte Funktionen werden auch in der zugehörigen Header-Datei deklariert und somit den anderen Programmteilen bekannt gemacht:
1
2
3
4
5
6
7
/*
usbcam.h - Kamera-Ansteuerung für WebCam
*/
int init_cam(char *device_name);
int get_image(int cam_fd, char *tmpfile, int swap_RGB);
Um die verschiedenen Quelltexte zu einem Programm zu übersetzen, müssen insgesamt vier gcc-Aufrufe ausgeführt werden. Daher bietet sich hier die Verwendung des Tools make“ an (vgl. Abschnitt 1.4.5). Ein passendes Makefile“ sieht
”
”
dann so aus:
1
2
3
4
5
6
7
8
9
10
#
# Makefile für WebCam
#
OBJ = webcam.o http_service.o usbcam.o
HDR = http_service.h usbcam.h
LIB = -ljpeg
webcam: $(OBJ)
gcc $ˆ $(LIB) -o $@
10.1 WebCam: Video-Übertragung per HTTP
11
12
13
14
15
16
%.o:
297
%.c $(HDR)
gcc -Wall -c $<
clean:
rm -f $(OBJ)
Zum Übersetzen des gesamten Projektes müssen Sie nun einfach nur make“ ein”
geben.
Bevor Sie das Programm starten, sollten Sie noch eine HTML-Datei mit dem Namen start.html“ anlegen, die der Webserver dann als Startseite anzeigt, wenn
”
man im Browser den URL http://localhost“ eingibt. Um das Video-Bild auf
”
dieser Web-Seite darzustellen, müssen Sie in die HTML-Datei ein IMG-Tag der
Form
<img src="video.jpg">
einfügen. Optional ist die Angabe der Bildgröße möglich, damit der Browser
schon vor dem Empfang des Video-Streams die Position und Größe des Bildes
korrekt darstellen kann. Eine entsprechende HTML-Datei sieht dann beispielsweise so aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<title>WebCam</title>
</head>
<body bgcolor="#e0deda">
<p>
<h1 align=center>WebCam</h1>
<h3 align=center>aus "C und Linux", Hanser Verlag</h3>
<hr>
<table align=center border=2>
<tr><td>
<img src="video.jpg" width=320 height=240>
</td></tr>
</table>
</body>
</html>
Da unser WebCam-Programm den für HTTP-Server registrierten Port 80 verwendet, muss es über root-Rechte verfügen. Dazu können Sie das Programm entweder
als Benutzer root“ oder mit dem Tool sudo“ starten – oder Sie setzen das S“-Bit
”
”
”
gemäß Abschnitt 9.1.1. Sobald das Programm läuft, können Sie von jedem Rechner im Netzwerk mit einem Browser auf das Videobild der Kamera/TV-Karte zugreifen. Abbildung 10.1 zeigt das Ergebnis am Beispiel des Browsers Firefox und
einer TV-Karte als Videoquelle.
298
10 Beispielprojekte
Abbildung 10.1: TV-Karte als Videoquelle für das Programm webcam“
”
10.1.3 Die HTTP-Authentifizierung
Da Sie vielleicht nicht wünschen, dass jeder Benutzer im lokalen Netzwerk (heimlich) das Videobild Ihrer USB-Kamera abrufen kann, haben wir eine Besonderheit
in das WebCam-Programm eingebaut: eine Authentifizierung mit Benutzername
und Passwort. Dazu verwenden wir den ab Version 1.1 des HTTP-Standards definierten Mechanismus, den alle Browser unterstützen. Eine ankommende HTTPAnfrage wird dabei zunächst mit folgender Antwort abgewiesen:
HTTP/1.1 401 Authorization Required
WWW-Authenticate: Basic realm="Name"
Daraufhin öffnet der Browser ein kleines Fenster zur Eingabe von Benutzername
und Passwort. Dabei wird der in der HTTP-Antwort als Name angegebene Text
in dem Fenster angezeigt, damit der Benutzer weiß, welche Webseite hier eine
Zugangskennung verlangt. Nach Eingabe der Zugangsdaten sendet der Browser
seine HTTP-Anfrage erneut an den Webserver, diesmal jedoch mit einer zusätzlichen Kopfzeile der Form:
Authorization: Basic Zugangscode
Der hinter dem Schlüsselwort Basic“ angegebene Zugangscode entspricht den
”
durch einen Doppelpunkt getrennten Benutzernamen und Passwort, jedoch nicht
10.1 WebCam: Video-Übertragung per HTTP
299
im Klartext sondern Base64-codiert. Stimmt der Code mit dem im Webserver hinterlegten überein, wird die Webseite an den Browser geschickt. Andernfalls wiederholt der Webserver die Authentifizierungsanforderung. In den Zeilen 121 bis
125 und 133 bis 143 des Quelltextes http service.c“ ist diese Authentifizierung
”
realisiert.
Bei unserem WebCam-Programm können Sie mit der Option -a“ einen Zugangs”
code angeben, der für das Abrufen von Inhalten erforderlich sein soll. Mit Hilfe
des Programms uuencode“ aus dem Paket sharutils“ können Sie Benutzername
”
”
und Passwort in die Base64-Zeichenfolge für die HTTP-Authentifizierung umcodieren. Zwischen dem Benutzernamen und dem Passwort muss dabei ein Doppelpunkt stehen:
> echo -n gast:pass | uuencode -m foo
begin-base64 644 foo
Z2FzdDpwYXNz
====
> su
Kennwort:
s
# ./webcam -a Z2FzdDpwYXNz
HTTP service started.
Press Ctrl-C to stop.
Auf diese Weise gestartet, verlangt der Webserver die Zugangskennung gast“
”
und pass“, bevor Daten übertragen werden.
”
Noch ein Hinweis zur Sicherheit:
Die vorgestellte HTTP-Authentifizierung ist kein sehr sicheres Verfahren, da die
in Base64-codierten Zugangsdaten problemlos wieder in Klartext zurückgewandelt werden können ( uudecode“). Wird der Datenverkehr zwischen Browser und
”
Webserver von irgendjemandem angezapft“ und mitgeschnitten – auch als Man”
In-The-Middle-Attacke bezeichnet –, kann derjenige relativ leicht die Zugangskennung entschlüsseln. Daher verwenden professionelle Internetanbieter für die
Übertragung von Kennwörtern immer eine verschlüsselte Verbindung ( https“),
”
bei der sich allein aus den über das Netzwerk übertragenen Daten der Benutzername und das Passwort nicht rekonstruieren lassen.
300
10 Beispielprojekte
10.2 Telefonbuch mit automatischer Anwahl
Als zweites Software-Projekt soll das Programm telefonbuch“ vorgestellt wer”
den. Es zeigt die Verwendung der ncurses-Bibliothek zur Ein- und Ausgabesteuerung im Shell-Fenster sowie die Ansteuerung eines Telefon-Modems. Die
Einbindung von Shell-Programmen über einen zweiten Prozess und die zugehörige Interprozesskommunikation sind ebenfalls Bestandteil dieses Projektes.
Auch wenn Sie einen DSL-Internetzugang haben, besitzen Sie vielleicht noch ein
serielles oder USB-Modem, das Sie mit Hilfe dieses Programms als Wählhilfe“
”
reaktivieren können.
10.2.1 Ziel des Projektes
Das Shell-Programm soll eine (unsortierte) Textdatei mit Namen und Telefonnummern einlesen und sortiert im Terminal-Fenster darstellen. Die Textdatei soll pro
Zeile einen Namen und – mit einem oder mehreren Tabulatoren getrennt – die
zugehörige Telefonnummer enthalten. Ähnlich wie bei einem Mobiltelefon soll
hier über die Eingabe des Anfangsbuchstabens automatisch der erste passende
Eintrag angesprungen“ werden; mit den Cursor-Tasten kann man sich in der
”
Liste vor und zurück bewegen. Durch Drücken der Enter-Taste wird die Telefonnummer des markierten Eintrags mit einem analogen Telefonmodem gewählt.
Während des Wählvorgangs, der bei den meisten Modems akustisch ausgegeben wird, kann man den Hörer eines nachgeschalteten“ Telefons (siehe Abbil”
dung 10.2) abnehmen. Das Modem legt nach Anwahl der Nummer automatisch
auf und übergibt so die aufgebaute Verbindung an das Telefon.1
N F N
Abbildung 10.2: Anschluss von Analog-Modem und Telefon.
1
Voraussetzung dafür ist, dass das Modem über eine 4-adrige Leitung angeschlossen ist.
10.2 Telefonbuch mit automatischer Anwahl
301
10.2.2 Strukturierung des Projektes
Auch dieses Beispielprojekt lässt sich in drei Teile zerlegen: das Hauptprogramm,
die Funktionen zur Ansteuerung des Modems und die Benutzerschnittstelle –
hier mit der ncurses-Bibliothek realisiert. Jedes dieser Module besteht aus einer
Include-Datei mit Deklarationen und einer .c“-Datei mit den Funktionsdefinitio”
nen.
10.2.3 Das Hauptprogramm
Die Datei telefonbuch.c“ beinhaltet neben der Definition von main() die Funk”
tion read phonebook() zum Einlesen der Telefonliste sowie die globale Variable
phonebook, in der das Telefonbuch abgelegt wird:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
telefonbuch.c - automatische Anwahl mit Modem
*/
#
#
#
#
#
#
include
include
include
include
include
include
<stdio.h>
<string.h>
<stdlib.h>
"telefonbuch.h"
"modem.h"
"eingabe.h"
char phonebook[MAX_ENTRIES][2][MAX_CHARS];
int get_entry(char *buffer, char *dest)
{
char c;
int i;
i = 0;
while (((c = buffer[i]) != ’\0’)
&& (c != ’\t’) && (c != ’\n’))
{
if (i < MAX_CHARS-1)
dest[i] = c;
i++;
}
if (i < MAX_CHARS)
dest[i] = ’\0’;
else
dest[MAX_CHARS-1] = ’\0’;
return(i);
302
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
10 Beispielprojekte
}
int read_phonebook(char *name)
{
int i, k;
char buffer[80];
FILE *stream;
strcpy(buffer, "sort ");
strcat(buffer, name);
if ((stream = popen(buffer, "r")) == NULL)
{
perror("telefonbuch");
return(0);
}
i = 0;
while (fgets(buffer, 80, stream) != NULL)
if (strlen(buffer) > 1)
{
/* Namen einlesen */
k = get_entry(buffer, phonebook[i][0]);
if (buffer[k] != ’\t’)
{
fprintf(stderr, "fehlerhafter Eintrag:"
" %s\n", buffer);
pclose(stream);
return(0);
}
while (buffer[++k] == ’\t’);
/* Nummer einlesen */
get_entry(&(buffer[k]), phonebook[i][1]);
if (++i == MAX_ENTRIES)
{
fprintf(stderr, "Telefonbuch zu lang.\n");
pclose(stream);
return(i);
}
}
pclose(stream);
if (i == 0)
fprintf(stderr, "kein Eintrag gefunden.\n");
return(i);
10.2 Telefonbuch mit automatischer Anwahl
303
76
}
77
78
79 int main(int argc, char *argv[])
80
{
81
int n, error;
82
83
if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
84
{
85
printf("telefonbuch - automatische Anwahl mit"
86
" einem analogen Modem.\n");
87
printf("Aufruf: telefonbuch Datei\n");
88
return(1);
89
}
90
91
if ((n = read_phonebook(argv[1])) < 1)
92
return(1);
93
94
if (error = open_modem())
95
{
96
fprintf(stderr, "telefonbuch: open_modem(): %s\n",
97
strerror(error));
98
exit(1);
99
}
100
if (error = reset_modem())
101
102
{
103
fprintf(stderr, "telefonbuch: reset_modem(): %s\n",
104
strerror(error));
105
exit(1);
106
}
107
108
error = eingabe(n);
109
110
close_modem();
111
return(error);
112
}
In Zeile 12 wird das Telefonbuch als Feld mit MAX ENTRIES Einträgen und je zwei
Zeichenketten mit MAX CHARS Zeichen pro Eintrag definiert. Beide Konstanten
sind in telefonbuch.h festgelegt. Die erste Zeichenkette jedes Eintrags enthält
den Namen, die zweite Zeichenkette die Telefonnummer (ohne Sonderzeichen
wie /“ oder -“). Die Hilfsfunktion get entry() in den Zeilen 14 bis 32 wird
”
”
von der Funktion read phonebook() zum Auswerten der Telefonliste verwen-
304
10 Beispielprojekte
det. Sie kopiert Zeichen aus der Zeichenkette buffer nach dest, bis sie auf ein
Tabulator- oder Zeilenvorschubzeichen stößt.
Die Funktion read phonebook() (Zeile 34 bis 76) ruft zunächst mit der Funktion
popen() das Shell-Programm sort“ auf; als Kommandozeilenparameter wird
”
der Dateiname der Telefonliste übergeben. Als Rückgabewert liefert popen() den
Stream mit den Ausgaben von sort, also die alphabetisch sortierte Telefonliste.
In der while()-Schleife (Zeile 49 bis 69) werden die sortierten Einträge der Telefonliste in das Feld phonebook eingelesen. Die Funktion read phonebook()
wird danach beendet und gibt die Anzahl der eingelesenen Einträge zurück.
In der Funktion main() (ab Zeile 79) werden zunächst die Kommandozeilenparameter geprüft und anschließend die Telefonliste eingelesen. Danach wird die
Schnittstelle zum Modem geöffnet und das Modem (sicherheitshalber) zurückgesetzt (Zeile 101). Dazu dienen zwei in modem.h deklarierte und in modem.c definierte Funktionen. In Zeile 108 wird dann die Benutzerschnittstelle gestartet, die
in eingabe.c und eingabe.h definiert bzw. deklariert ist.
Wie bereits erwähnt, wird neben telefonbuch.c“ auch eine Include-Datei te”
”
lefonbuch.h“ benötigt, in der die globalen Konstanten definiert und das Feld
phonebook deklariert sind:
1
2
3
4
5
6
7
8
/*
telefonbuch.h
*/
# define MAX_ENTRIES 500
# define MAX_CHARS 40
extern char phonebook[MAX_ENTRIES][2][MAX_CHARS];
10.2.4 Funktionen zur Ansteuerung des Modems
Der Zugriff auf die serielle Schnittstelle sowie die Ansteuerung des Modems über
diese Schnittstelle sind in dem Modul modem.c“ realisiert:1
”
1
/*
2
modem.c - Funktionen zur Ansteuerung des Modems
3
*/
4
1
In diesem Teil wird der so genannte AT-Befehlssatz“ verwendet und auf Modem-Register zugegrif”
fen. Die AT-Befehle wurden von der Firma Hayes eingeführt und haben sich als De-facto-Standard
etabliert – man spricht auch vom Hayes-Standard“ und von Hayes-kompatibel“. Tatsächlich gibt
”
”
es Unterschiede zwischen den Modems der verschiedenen Hersteller. Die hier verwendeten Befehle
sollte dennoch jedes Modem verstehen. Für weitere Informationen schauen Sie in das Handbuch Ihres Modems oder auf die im Anhang angegebene Internet-Seite. Sie können auch mit dem Programm
terminal.c aus Kapitel 6 einmal direkt AT-Befehle an das Modem senden und sich die Antwort“
”
des Modems ansehen.
10.2 Telefonbuch mit automatischer Anwahl
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#
#
#
#
#
#
#
include
include
include
include
include
include
include
<errno.h>
<string.h>
<unistd.h>
<fcntl.h>
<termios.h>
<sys/select.h>
<sys/time.h>
# define MODEM_DEV "/dev/modem"
# define SPEED B19200
int modem_fd = -1;
/* Datei-Deskriptor d. Modems */
int open_modem()
{
if ((modem_fd = open(MODEM_DEV, O_RDWR)) == -1)
return(errno);
else
return(0);
}
void close_modem()
{
if (modem_fd != -1)
{
close(modem_fd);
modem_fd = -1;
}
return;
}
int reset_modem()
{
struct termios term_attr;
fd_set fdset;
struct timeval timeout;
/* RS232 konfigurieren */
if (tcgetattr(modem_fd, &term_attr) != 0)
return(errno);
term_attr.c_cflag = CS8 | CLOCAL | CREAD;
term_attr.c_iflag = 0;
term_attr.c_oflag = 0;
term_attr.c_lflag = 0;
305
306
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
10 Beispielprojekte
cfsetospeed(&term_attr, SPEED);
cfsetispeed(&term_attr, SPEED);
if (tcsetattr(modem_fd, TCSAFLUSH, &term_attr) != 0)
return(errno);
/* Modem testen */
if (write(modem_fd, "\r\n", 2) != 2)
return(errno);
usleep(200000);
if (write(modem_fd, "ATZ\r\n", 5) != 5)
return(errno);
usleep(500000);
FD_ZERO(&fdset);
FD_SET(modem_fd, &fdset);
timeout.tv_sec = 2;
timeout.tv_usec = 0;
/* 2 Sek. Timeout */
/* Antwort vom Modem? */
if (select(modem_fd+1, &fdset, NULL, NULL,
&timeout) <= 0)
return(EBUSY);
term_attr.c_cflag |= CRTSCTS;
if (tcsetattr(modem_fd, TCSAFLUSH, &term_attr)
!= 0)
return(errno);
if (write(modem_fd, "ATS7=2\r\n", 8) != 8)
return(errno);
usleep(200000);
return(0);
}
int dial(char *number)
{
int l;
if (write(modem_fd, "ATDT ", 5) != 5)
return(errno);
l = strlen(number);
if (write(modem_fd, number, l) != l)
return(errno);
10.2 Telefonbuch mit automatischer Anwahl
93
94
95
96
307
if (write(modem_fd, "\r\n", 2) != 2)
return(errno);
return(0);
}
Die Funktionen open modem() und close modem() (Zeile 18 bis 34) dienen
zum Öffnen und Schließen des Devices. In der Funktion reset modem() werden zunächst die Parameter der zuvor geöffneten Schnittstelle eingestellt. Danach
wird in Zeile 54 eine Zeilenende-Sequenz (CR-LF) an das Modem geschickt. Dies
ist erforderlich, damit sich das Modem auf die RS232-Parameter (Baudrate, Bits/Zeichen, Parity usw.) einstellen kann. Im Anschluss wird das Modem zurückgesetzt (Zeile 57). Dieses Zurücksetzen quittiert das Modem im Allgemeinen mit
OK“. In den Zeilen 61 bis 70 wird maximal zwei Sekunden auf diese Quittung
”
gewartet. Kommt vom Modem keine Antwort, bricht die Funktion mit dem Fehler EBUSY ( device or ressource busy“) ab (Zeile 70). Erst danach wird an der
”
Schnittstelle das Hardware-Handshake RTS/CTS eingeschaltet (Zeile 72 bis 75).
Wäre dies bereits vor dem Senden der ersten Zeichen an das Modem geschehen
und wäre das Modem nicht bereit, Zeichen zu empfangen (z. B. weil es ausgeschaltet ist), würde der Prozess spätestens beim Aufruf von close() hängen, und
das Programm ließe sich nur noch mittels Ctrl-C abbrechen.
In Zeile 77 wird das Register S7 des Modems auf den Wert 2 gestellt. Damit erreicht man, dass das Modem zwei Sekunden nach dem Wählen der Telefonnummer auflegt und die Verbindung an das Telefon übergibt.1
Die Funktion dial() veranlasst das Modem, die als Zeichenkette übergebene
Telefonnummer anzuwählen. Alle vier Funktionen aus modem.c“ sind auch
”
in modem.h“ deklariert und werden so für die anderen Programm-Module
”
verfügbar gemacht:
1
2
3
4
5
6
7
8
/*
modem.h - Funktionen zur Ansteuerung des Modems
*/
int open_modem(void);
void close_modem(void);
int reset_modem(void);
int dial(char *number);
10.2.5 Die Benutzerschnittstelle
Die Funktionen zur Steuerung der Ein- und Ausgaben des Programms sind in
dem Modul eingabe.c“ zusammengefasst:
”
1
Register S7 gibt die Zeit an, die das Modem maximal auf einen Trägerton des angerufenen Modems
wartet.
308
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
10 Beispielprojekte
/*
eingabe.c - curses-Benutzerschnittstelle
*/
#
#
#
#
#
#
include
include
include
include
include
include
<string.h>
<unistd.h>
<ncurses.h>
<ctype.h>
"telefonbuch.h"
"modem.h"
/*---- Hilfsfunktionen ----*/
void show_list(int n, int pos)
{
int i, start;
start = (pos < LINES/2)? 0 : pos - LINES/2;
erase();
move(0, 0);
attrset(A_NORMAL);
for (i=start; i<pos; i++)
printw("%-40s %s\n", phonebook[i][0],
phonebook[i][1]);
attrset(A_REVERSE);
printw("%-40s %-38s\n", phonebook[pos][0],
phonebook[pos][1]);
i++;
attrset(A_NORMAL);
while ((i < n) && (i < LINES+start))
{
printw("%-40s %s\n", phonebook[i][0],
phonebook[i][1]);
i++;
}
refresh();
return;
}
int search(int n, char letter)
{
int i;
letter = toupper(letter);
10.2 Telefonbuch mit automatischer Anwahl
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
for (i=0; i<n; i++)
if (toupper(phonebook[i][0][0]) >= letter)
return(i);
return(n-1);
}
void show_message(char *message)
{
int width;
WINDOW *win;
width = strlen(message)+4;
win = newwin(5, width, LINES/2-6, (COLS-width)/2);
box(win, 0, 0);
mvwprintw(win, 2, 2, "%s", message);
wrefresh(win);
sleep(3);
delwin(win);
touchline(stdscr, LINES/2-6, 5);
refresh();
return;
}
/*---- Hauptfunktion ----*/
int eingabe(int n)
{
int c, pos;
WINDOW *win;
if ((win = initscr()) == NULL)
{
fprintf(stderr, "initscr() fehlgeschlagen.\n");
return(1);
}
cbreak();
noecho();
curs_set(0);
keypad(win, TRUE);
pos = 0;
show_list(n, pos);
/* Cursor unsichtbar */
/* Sondertasten auswerten */
309
310
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
10 Beispielprojekte
while ((c = getch()) != ’\33’) /* ESC = Abbruch */
if ((c >= ’A’) && (c <= ’z’))
{
pos = search(n, c);
show_list(n, pos);
}
else if (c == ’\n’)
/* Auswahl */
{
dial(phonebook[pos][1]);
show_message("Bitte Hörer abnehmen!");
}
else if (c == KEY_UP)
/* Zeile hoch */
{
if (pos > 0)
show_list(n, --pos);
}
else if (c == KEY_DOWN)
/* Zeile runter */
{
if (pos < n-1)
show_list(n, ++pos);
}
else if (c == KEY_PPAGE)
/* Seite hoch */
{
pos -= LINES-1;
if (pos < 0)
pos = 0;
show_list(n, pos);
}
else if (c == KEY_NPAGE)
/* Seite runter */
{
pos += LINES-1;
if (pos >= n)
pos = n-1;
show_list(n, pos);
}
erase();
refresh();
endwin();
return(0);
}
In den Zeilen 14 bis 38 wird eine Hilfsfunktion definiert, die den Inhalt des Fensters neu schreibt und dabei den Eintrag mit dem Index pos durch invertierte
Darstellung hervorhebt (siehe auch Abbildung 10.3 auf Seite 313). Der Parameter
10.2 Telefonbuch mit automatischer Anwahl
311
n gibt die Anzahl der Einträge im Telefonbuch an. Es ist zu beachten, dass die
Funktionen erase() (Zeile 19) und printw() der libncurses zunächst nur die
Datenstrukturen des Fensters modifizieren – ohne sichtbaren Effekt. Erst mit Aufruf der Funktion refresh() in Zeile 36 wird der Inhalt der Datenstrukturen auf
das Fenster übertragen. Das Einlesen von Tastatureingaben – beispielsweise mit
getch() – bewirkt ebenfalls die Aktualisierung des Fensterinhaltes.
Die in den Zeilen 40 bis 49 definierte Funktion search() sucht den ersten Eintrag im Telefonbuch, der mit dem Zeichen letter beginnt. Diese Hilfsfunktion
wird verwendet, um durch Eintippen des Anfangsbuchstabens auf den ersten dazu passenden Eintrag zu springen.
Die dritte Hilfsfunktion show message() stellt für drei Sekunden den in message
angegebenen Text in einem eingerahmten Fenster dar. Nach dem Löschen dieses
Fensters mit delwin() in Zeile 62 muss dem darunterliegenden Hauptfenster
signalisiert werden, welche Zeilen durch das zweite Fenster überschrieben wurden. Dies geschieht hier mit der Funktion touchline(). Alternativ kann die
Funktion touchwin() verwendet werden, die das gesamte Fenster zum Neuzeichnen markiert.1
Kern der Benutzerschnittstelle ist die ab Zeile 70 definierte Funktion eingabe().
Sie initialisiert zunächst das Hauptfenster und stellt dessen Eigenschaften ein
(Zeile 75 bis 84). Danach wird die Telefonliste mit der oben definierten Funktion show list() dargestellt, wobei der erste Eintrag selektiert ist. Ab Zeile 89
beginnt die Hauptschleife, in der Tastatureingaben eingelesen und ausgewertet
werden. Folgende Eingaben sind möglich:
Eingabe
Funktion
’A’–’Z’
↓, ↑
PgDn, PgUp
Enter
Esc
ersten Eintrag mit diesem Anfangsbuchstaben selektieren
nächsten bzw. vorhergehenden Eintrag selektieren
eine Seite vor- bzw. zurückblättern
selektierte Telefonnummer anwählen
Programm beenden
Von den in eingabe.c“ definierten Funktionen wird lediglich die Hauptfunkti”
on, also eingabe(), im Hauptprogramm verwendet. Dementsprechend fällt die
Include-Datei eingabe.h“ relativ kurz aus:
”
1
/*
2
eingabe.h - curses-Benutzerschnittstelle
3
*/
4
5
int eingabe(int n);
1
touchline() und touchwin() bewirken selbst keine Aktualisierung des Fensterinhaltes. Sie mar-
kieren die Datenstrukturen des Fenster so, dass die betroffenen Zeilen bzw. das gesamte Fenster beim
nächsten Aufruf von refresh() neu geschrieben werden.
312
10 Beispielprojekte
Als Letztes fehlt nur noch das Makefile“, mit dessen Hilfe sich das Projekt kom”
pilieren lässt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#
# Makefile für ’telefonbuch’
#
OBJ = telefonbuch.o modem.o eingabe.o
HDR = telefonbuch.h modem.h eingabe.h
LIB = -lncurses
telefonbuch:
%.o:
$(OBJ)
gcc $ˆ $(LIB) -o $@
%.c $(HDR)
gcc -c $<
clean:
rm -f $(OBJ)
Nach erfolgreichem Übersetzen des Quelltextes kann das Programm wie folgt aufgerufen werden:
telefonbuch Dateiname
wobei Dateiname der Name der Telefonliste ist. Abbildung 10.3 zeigt das Erscheinungsbild des Programms.
10.2.6 To Do
Dieses Beispielprogramm ist sicherlich ausbaufähig. Die folgenden interessanten
Ergänzungen könnte man sich für das Programm vorstellen:
Unterstützung einer Konfigurationsdatei, in der z. B. die Modem-Schnittstelle,
Baud-Rate usw. eingetragen und damit ohne erneutes Kompilieren verändert
werden können.
Umstellung auf eine dynamische Speicherreservierung für das Telefonbuch
anstelle einer festen Obergrenze für die Anzahl der Einträge.
Eine Online-Hilfe für das Programm
Automatisches Anpassen der Darstellung an die Konstante COLS, sodass auch
weniger als 80 Zeichen pro Zeile möglich sind.
10.2 Telefonbuch mit automatischer Anwahl
Abbildung 10.3: Das Programm telefonbuch“ in Aktion.
”
313
Anhang
A1 – Daten zum Buch im Internet
Um Ihnen beim Ausprobieren der zahlreichen Beispielprogramme aus diesem
Buch Tipparbeit zu ersparen, sind die Quelltexte aller Beispiele1 sowie weitere
nützliche Dateien als Download im Internet verfügbar. Unter der Adresse
http://downloads.hanser.de
gelangen Sie zu einem Eingabeformular, in das Sie die ISBN-Nummer oder den
Titel dieses Buches eintragen können.
Im Download-Bereich sind folgende Daten verfügbar:
–
spca5xx.tar.gz
–
svgalib-1.9.25.tar.gz –
3.Auflage.tar.gz
–
Quelltexte.tar.gz
alle Beispielquelltexte nach Kapiteln geordnet
Treiber für USB-Kameras (Quelltext)
Quelltext der SVGALIB inkl. Demos
Abschnitte aus der 3. Auflage, die nicht
in die 4. Auflage übernommen wurden
A2 – Das X11-Toolkit XView
Das Toolkit XView/OpenLook“ als Nachfolger von SunView“ wird leider nicht
”
”
weiterentwickelt oder gepflegt. Daher ist es auf (fast) keiner aktuellen LinuxDistribution mehr enthalten. Aus diesem Grund wurden die Kapitel und Beispielprogramme der 3. Auflage dieses Buchs, die sich mit XView befassten, nicht
mehr in die aktuelle Auflage übernommen. Im Download-Bereich (siehe Anhang
A1) sind jedoch sowohl die entsprechenden Seiten der 3. Auflage im PDF- und
PostScript-Format als auch die Beispielprogramme und ein Installationspaket für
die XView-Dateien enthalten.
1
Als Beispiele“ bezeichnen wir die vollständigen Programme, die im Buch mit Zeilennummern am
”
linken Rand versehen sind.
316
Anhang
A3 – Aufbau einer WAV-Audiodatei
Die meisten Audio-Samples sind im WAV(E)-Format gespeichert, einem Standard
von Microsoft und IBM, der erstmals unter Windows 3.1 implementiert wurde.
Wie viele andere Audio- oder Grafikformate ist auch das WAVE-Format in so genannte Chunks aufgeteilt – Abschnitte, die durch ein Schlüsselwort am Anfang
gekennzeichnet sind, gefolgt von der Länge dieses Abschnittes. Der Vorteil solcher Formate liegt in der Erweiterbarkeit ohne Kompatibilitätsverlust. Enthält die
Datei einen Chunk, der dem Programm nicht bekannt ist – beispielsweise mit
Informationen über Autor und Erstelldatum –, kann dieser Abschnitt mit Hilfe der Längeninformation einfach überlesen“ und somit ignoriert werden. Das
”
Schlüsselwort jedes Chunks umfasst immer vier Zeichen. Die Längeninformation
besteht ebenfalls aus vier Bytes.
Eine WAV-Datei beginnt immer mit den vier Zeichen RIFF“, gefolgt von vier By”
tes für die Länge der gesamten Datei, abzüglich der acht Bytes für das RIFF“
”
und die Längeninformation. Alle Zahlenwerte wie auch die Längenangaben beginnen immer mit dem niederwertigsten Byte (little endian encoding). Es folgt die
Zeichenfolge WAVE“; damit umfasst dieser Dateikopf insgesamt 12 Bytes.
”
Neben dieser Kopfinformation enthält eine WAV-Datei mindestens zwei Chunks:
die Audioparameter und die Audiodaten selbst. Die Audioparameter folgen in
der Regel unmittelbar auf die Kopfinformation (also ab dem 13. Byte) und sind
durch die Zeichenfolge fmt⊔“ gekennzeichnet (Achtung: Das letzte Zeichen ist
”
ein Leerzeichen!). Nach diesen acht Bytes folgen vier Bytes für die Länge dieses
Chunks. Sie beträgt normalerweise 16 Bytes und setzt sich wie folgt zusammen:
Parameter
Länge
Bemerkung
Codierungsart
Anzahl Kanäle
Samplingrate
Bytes / Sekunde
Bytes / Sample
Bits / Sample
2 Bytes
2 Bytes
4 Bytes
4 Bytes
2 Bytes
2 Bytes
1=linear, 2=ADPCM, 6=A-law, 7=µ -law
1=mono, 2=stereo
Samples pro Kanal / Sekunde
Speicherbedarf / Sekunde
=(Bytes / Sekunde) / Samplingrate
8 (unsigned char) oder 16 (short)
Ist der Chunk länger als 16 Bytes, enthalten die zusätzlichen Bytes weitere Informationen, die aber ignoriert werden können.
Der letzte Abschnitt einer WAV-Datei ist immer der Daten-Chunk. Dieser wird
durch das Schlüsselwort data“ eingeleitet, wiederum gefolgt von der Länge
”
dieses Chunks, die der Restlänge der Datei entsprechen sollte. Bei StereoAufzeichnungen folgen die Samples für den linken und rechten Kanal im Wechsel,
angefangen mit dem linken Kanal. Bei 16 Bit pro Sample kommt auch hier wieder
das niederwertige Byte zuerst.
Das folgende Beispiel zeigt eine WAV-Datei mit 20 Samples bei 22050 Hz, mono
und 8 Bit pro Sample (linear codiert):
Anhang
317
’R’ ’I’ ’F’ ’F’ 0x38 0x00 0x00 0x00 ’W’ ’A’ ’V’ ’E’
’f’ ’m’ ’t’ ’ ’ 0x10 0x00 0x00 0x00
0x01 0x00
/* Codierung: linear */
0x01 0x00
/* 1 Kanal (mono) */
0x22 0x56 0x00 0x00
/* 22050 Samples/s */
0x22 0x56 0x00 0x00
/* 22050 Bytes/s */
0x01 0x00
/* 1 Byte pro Sample */
0x08 0x00
/* 8 Bit pro Sample */
’d’ ’a’ ’t’ ’a’ 0x14 0x00 0x00 0x00
0x80 0xCA 0xF9 0xF9 0xCA 0x7F 0x35 0x06 0x06 0x35
0x7F 0xCA 0xF9 0xF9 0xCA 0x7F 0x35 0x06 0x06 0x35
A4 – Aufbau einer AU-Audiodatei
Etwas seltener als das WAVE-Format wird für Audio-Daten das AU-Format verwendet, welches eher aus der Unix-Welt stammt (Sun, NeXT, DEC). Bei diesem
Format ist den Sampling-Daten ein Header vorangestellt, der Auskunft über die
Audio-Parameter gibt. Dieser Kopf ist wie folgt aufgebaut:
Pos.
0
4
8
12
16
20
24
Parameter
Bemerkung
.snd“
”
Offset
Länge
Format
Samplingrate
Kanäle
Info
Schlüsselwort zur Kennzeichnung
Position der Samplingdaten in der Datei
Anzahl der Daten-Bytes
1=8-Bit µ -law, 2=8-Bit linear, 3=16-Bit linear
Abtastfrequenz in Hz
1 (mono) oder 2 (stereo)
Beschreibung (Zeichenkette)
Im Gegensatz zum WAVE-Format werden beim AU-Format alle Zahlen in umgekehrter Reihenfolge gespeichert, also beginnend mit dem höchstwertigen Byte.
Auch das Format für 8-Bit-Samples ist anders: die Samples werden als char gespeichert, also mit Vorzeichen! Der Wertebereich für 8-Bit-Signale geht somit von
–128 bis +127.
A5 – Linux-Programmierung unter Windows: Cygwin
Vielleicht ist Ihnen einmal der Gedanke gekommen, dass Sie ein unter Linux erstelltes Programm gern auch unter WindowsTM nutzen würden. Insbesondere
wenn Sie sich mit Linux-Spezifischem wie Device-Programmierung auseinandergesetzt haben, möchten Sie das Gleiche nicht für das Erstellen eines vergleichbaren Windows-Programms neu erlernen, sondern möglichst dieselben Verfahren
oder sogar denselben Quelltext verwenden.
318
Anhang
Genau zu diesem Zweck wurde vor einiger Zeit das Projekt Cygwin“ ins Leben
”
gerufen. Dabei handelt es sich nicht um einen Emulator, sondern um eine Linuxkompatible Umgebung unter Windows, inklusive der Linux-Shell bash und dem
Compiler gcc. Das Herzstück von Cygwin sind DLLs (Dynamic Link Libraries),
die die Linux-Systemfunktionen unter Windows realisieren. Damit können LinuxProgramme, die unter Cygwin kompiliert wurden, direkt unter Windows (in
der Eingabeaufforderung) gestartet werden! Lediglich die cygwin.dll muss
im Windows-Suchpfad für DLLs vorhanden sein oder im gleichen Verzeichnis
wie das Linux-Programm stehen. Verwendet ein Programm zusätzliche LinuxBibliotheken wie beispielsweise die libm oder die libncurses, müssen auch die
entsprechenden Cygwin-DLLs beim Starten des Programms verfügbar sein.
Um Linux-Quelltexte unter Cygwin zu kompilieren, müssen Sie die vollständige
Cygwin-Umgebung installiert haben. Diese können Sie kostenlos von der Homepage des Cygwin-Projektes
http://www.cygwin.com
laden und installieren. Ein Großteil der in diesem Buch beschriebenen Beispielprogramme lässt sich problemlos unter Cygwin kompilieren und steht dann auch
als Windows-Programm zur Verfügung.
Beispiele für die Leistungsfähigkeit von Cygwin sind die Linux-Programme
mkisofs“ und cdrecord“, die – unter Cygwin kompiliert – auch unter Win”
”
dows einwandfrei funktionieren und mit einer Größe von nur wenigen 100 kByte
das Brennen von Daten-CDs ermöglichen.
Es wurden inzwischen auch ein X11-Server, verschiedene X11-Toolkits (GTK+,
XView) und auch die libusb nach Cygwin portiert. Auch die in Kapitel 7 beschriebenen Beispielprogramme zur Netzwerkkommunikation sind alle unter
Cygwin lauffähig. Einschränkungen gibt es hingegen bei einigen Devices – beispielsweise /dev/dsp“ hat nur eingeschränkte Funktionalität, /dev/mixer“
”
”
und /dev/video“ sind gar nicht verfügbar – und bei der Interprozesskommu”
nikation mittels Shared Memory (vgl. Abschnitt 5.4.3).
Literaturverzeichnis
[1] Jürgen Plate: Linux Hardware Hackz“, Hanser Verlag, 2007
”
[2] Jürgen Plate: Linux Server für Intranet und Internet“, Hanser Verlag, 2000
”
[3] Homepage der Eclipse Entwicklungsumgebung:
http://www.eclipse.org/
[4] Homepage des C Development Tooling“ für Eclipse:
”
http://www.eclipse.org/cdt
[5] Tutorial für die Eclipse Entwicklungsumgebung mit CDT:
http://ittk.falb.at/pt/unterlagen/Eclipse_CDT_Tutorial.pdf
[6] Linux Man-Pages online lesen:
http://linux.die.net/man/
http://linuxmanpages.com/
http://man-wiki.net/index.php/Main_Page
http://wwwcip.informatik.uni-erlangen.de/man
Deutsche Man-Pages zum Download:
http://www.infodrom.org/projects/manpages-de/changes-0.4.←֓
php3
[7] Tutorials zur libpthread im Internet:
http://www.laptev.org/doc/pthreads.html
http://users.actcom.co.il/˜choo/lupg/tutorials/multi-thre←֓
ad/multi-thread.html
[8] Edward A. Falk: Summary of CDROM ioctl calls“,
”
http://www.kernel.org/doc/Documentation/ioctl/cdrom.txt
[9] Peter Vollenweider: Die PostScript-Sprache“
”
http://www.id.uzh.ch/cl/zinfo/pdf/Einfuehrung.pdf
[10] Claudio Marxer: Postscript“
”
http://informatik.unibas.ch/lehre/fs09/cs506/_Downloads/c←֓
s506-20090423.pdf
320
Literaturverzeichnis
[11] Oliver Thilmann: PostScript“
”
http://linuxfocus.org/Deutsch/May1998/article43.html
[12] Linux-Treiber für USB Raketenwerfer:
http://lukecole.name/usb_missile_launcher.php
[13] Dokumentation der libusb“:
”
http://libusb.sourceforge.net/api-1.0/index.html
[14] James Marshall: HTTP Made Really Easy“:
”
http://www.jmarshall.com/easy/http/
[15] UPnP-Dokumentationen:
http://www.upnp.org/resources/documents.asp
[16] UPnP Device Architecture“:
”
http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitectu←֓
re-v1.0.pdf
[17] Wikipedia.de, Stichwort IP-Adressen“:
”
http://de.wikipedia.org/wiki/IP-Adresse#Besondere_IP-Adre←֓
ssen
[18] Matthias Warkus: Das GTK+/GNOME-Entwicklerhandbuch“, dpunkt
”
Verlag, 2008
[19] GKT-Tutorials im Internet (englisch):
GTK 1.2: http://www.gtk.org/tutorial1.2/
GTK 2.0: http://library.gnome.org/devel/gtk-tutorial/2.17/
GKT-Referenz: http://www.gtk.org/documentation.html
[20] Homepage der SVGALIB: http://www.svgalib.org
Dort kann unter Development versions“ die Version 1.9.25 als Quelltext
”
bezogen werden (kostenlos).
Stichwortverzeichnis
3D 260, 268
A-law 126, 316
a.out 8
Abtastfrequenz 128
Action-Area 221
Adjustment 231
alarm() 87
Anjuta 14, 31
apt 4
argc 43, 44
argv 43, 44
AT-Befehle 304
AU-Datei 317
AU-Format 130
Audioformat 126
Aufnahmequelle 125
Auswahlfelder 224
Authentifizierung 298
Base64 299
bindtextdomain() 58
Body 185
Box 217
Breakpoint 11, 18
Broadcast 166, 197
Browser 183
Buttons 213
C-Compiler 8
Callback 210
capture 137
cbreak() 63
CDT 14, 39
cfsetispeed 149
cfsetospeed 149
Check-Button 226
Client-Server-Prinzip 169
Client-Server-System 205
clone() 101
close() 108
Connection 166
Container 213
CUPS 149
curses 61
Cygwin 1, 317
Datei-Deskriptor 68
dbkg 4
ddd 10
Debugger 10
Device 107
dgettext() 55
Dialog 221
Dienst 165
DrawingArea 244
Drucker 149
dup2() 94
Echo 195
Eclipse 14, 39
Editor 6
Eigenschaften 75
Eingabefelder 228
endwin() 64
Entwicklungsumgebung
11, 31
322
Stichwortverzeichnis
EOF 71
errno 51
Escape-Sequenz 60
Ethernet 164
Event 211, 213
exec-Familie 81
exit() 51
Grafikbeschleuniger 266
Graphics Context 249, 268
grep 29
GTK+ 206
gtk-config 207
GUI 21
gzip 51
fclose() 70
Fehlermeldungen 50
Fehlersuche 16
feof() 71
ferror() 71
fflush() 73, 85
fgetc() 71
fgets() 73
FIFO 95
FILE 28
fileno() 68, 110
find 29
Firefox 183
Firewall 166, 204
fopen() 69
fork() 82
fprintf() 73
fputc() 71
fputs() 73
frame 217
fread() 72
fscanf() 73
fseek() 73
Funktionsbibliothek 19
fwrite() 72
Handshake 307
Hardware 271
HTTP 184, 283
HTTP-Server 184
GC 249
gcc 2, 8, 16
gdb 10, 17
gepufferte Ein-/Ausgabe
getch() 64
getopt() 46
getppid() 79
gettext() 55
getyx() 64
ghostview 151
Grafikauflösung 258
Label 220
LANG 54
Langtextoption 45
Lautstärke 120
LBA 116
libjpeg 139
libm 20
libncurses 2
libpthread 103
Library 19
libvga 256
I/O-Ports 271, 273
IDE 11
Include-Datei 28
initscr() 63
Inode 107
ioctl() 110
ioperm() 272
iopl() 272
IP-Adresse 165
JPEG 139
Kanal 121, 122
kate 7
kdbg 10, 19
KDevelop 14, 36
keypad() 64
Keyword 26
kill() 90
Kommandozeilenparameter
67
44, 45
Stichwortverzeichnis
323
libvgagl 266
little endian 316
locale 53
lpd 149
lpr 149
main() 43
make 2, 8, 23
Makefile 9, 24
man 26, 51
MANPATH 51
Maus 262
Maus-Typ 262
Menü 233
Message Object 57
Methode 185
MIDI 107
Mixer 122
mkfifo() 95
mmap() 73, 136
Modem 300, 304
Modem-Steuerleitungen 279
MSF 116
msgfmt 59
Multicast 199
Multipart 284
Multi-Tasking 79
munmap() 73, 136
mvprintw() 64
ncurses 61, 300
nedit 7
Netzwerkkommunkation
noecho() 63
Objekt 206
Objektdatei 8, 23
objektorientiert 206
open() 108
opendir() 77
Option -g 11
Parallelport 274
pclose() 100
perror() 50, 55
163
Perspektive 260
Pipe 73, 91
pipe() 91
Pixmap 238
pkg-config 207
Platzhalter 47
poll() 194
popen() 100, 304
Port 165
Portable Message 58
Portable Pixmap 133
POSIX 103
PostScript 149
PPM 133
pread() 109
Primzahlen 14
Protokoll 164
Protokollfamilie 170
Prozess-ID 79
PS/2 262
Puffer 67
Pulldown-Menüs 233
pwrite() 109
Rückgabewert 44
Radio-Button 227
Raketenwerfer 156
read() 92, 109
readdir() 77
recvfrom() 192
RGB-Format 135
Rollbalken 250
RS232 142
Samplingrate 128
Schaltflächen 213
sched yield() 86
Scrolled Window 250
Scrollen 61
Sektion 26
select() 110, 148
sendto() 192
Server-Programm 178
Server-push-Prinzip 284
setlocale() 54
324
Shared Memory 97
Shell-Programm 43
shmat() 97
shmctl() 97
shmdt() 97
shmget() 97
Sicherheit 204
SIG DFL 88
SIG IGN 88
Signal 86, 211
signal() 88
Signal-Handler 88
sleep() 85
Socket 170
Soundkarte 121
spca5xx 130
Spin-Button 231
stat() 75
stderr 50
Steuersequenzen 60
Stream 68
strerror() 51
strip 11
SuSE 2
svgalib 256
system() 80
Task 79
tee 69
Terminal-Device 142
Textdomain 56
textdomain() 58
Thread 79, 103
Timeout 87
tkman 26
Toggle-Button 226
Toolkit 206
Tooltips 216
Transparenz 238
TV-Karte 130
u-law 126, 316
UART 279
Ubuntu 4
udev 108
Stichwortverzeichnis
UDP 191
Umlaute 255
ungetc() 71
Unicode 206
UPnP 200
USB 154
User ID 76
usleep() 85
UTF-8 206, 255
uuencode 299
V24 142
Verbindung 166
verbindungsorientiert
Verzeichnis 77
Video 130
wait() 89
waitpid() 89
Warnmeldung 16
Warnung 16
Warteschleifen 85
WAV-Datei 316
WAV-Format 130
WebCam 130, 283
Webserver 184
WEXITSTATUS() 90
Widget 206
WNOHANG 90
write() 109
X11 205
X11-Display 238
xgettext 58
xinetd 195
xman 26
XPM-Format 238
XView 315
xwpe 14
xxgdb 10
YaST
2, 3, 167
Zahlenformat 55
Zeichenfläche 244
Zugriffsverfahren 164
166
C UND LINUX = DREAM TEAM //
■ Erfahren Sie, wie Sie mit eigenen C-Programmen die Möglichkeiten von Linux
ausnutzen können.
■ Nutzen Sie die praktischen Lösungen für häufig auftauchende Aufgabenstellungen.
■ Viele kleine Beispiele und zwei umfangreiche Beispielprojekte geben Ihnen
Anregungen für eigene Programme.
■ Im Internet: Die Quelltexte zum Buch, Tools und weiterführende Informationen
C UND LINUX // Wer Erfahrungen in der C-Programmierung hat, kann die vielen
Möglichkeiten von Linux kennen lernen und ausschöpfen. Dieses Buch zeigt wie
und enthält praktische Lösungen für häufig auftauchende Aufgabenstellungen.
Als Grundlage wird zunächst der Umgang mit wichtigen Werkzeugen erklärt, die in
jeder Linux-Distribution enthalten sind: Editor, Compiler, Debugger, Make usw.
Danach führt der Autor Sie anhand vieler Beispiele in diese Themen ein:
• Zugriff auf Dateien und Verzeichnisse mit C
• Programme mit parallel laufenden Prozessen schreiben
• USB-Geräte, CD-Laufwerk, Soundkarte und WebCams ansteuern
• Programme mit grafischer Benutzeroberfläche erstellen
• Direkte Hardware-Zugriffe programmieren
• Eigene Hilfe-Seiten erstellen
• Die automatische Anpassung auf die eingestellte Landessprache
Wie diese Themen in »echten« Software-Projekten zusammenspielen, können Sie
an zwei umfangreichen Beispielprojekten nachverfolgen.
Die 4. Auflage wurde vollständig überarbeitet. Neu aufgenommen wurden Abschnitte
zu den Entwicklungsumgebungen KDevelop und Eclipse, zur Kommunikation mit
USB-Geräten, zu Broadcast und Multicast und zu Universal Plug And Play (UPnP).
//… enthält eine Vielzahl an wichtigen Informationen zur Systemprogrammierung unter
Linux. [...] Schöne Beispiele, die Funktionsaufrufe gut kommentiert und eine interessante
Themensammlung. // c-plusplus.de
Martin GRÄFE promovierte im Bereich Elektrotechnik/Mikroelektronik. Im Rahmen
seiner Ingenieurstätigkeit entwickelt er Software für Unix-Workstations und seit
1995 auch für Linux-Systeme.
www.hanser.de/computer
ISBN 978-3-446-42176-9
Linux-User
C-Programmierer
9
783446 421769