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 Einfuhrung ¨ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 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 . . . . . . . . . . . . . . . . Fur ¨ 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
Weiterfuhrende ¨ 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 Ruckgabewert ¨ der Funktion main() . . . . . . . . . 3.1.1 Die Bedeutung des Ruckgabewertes ¨ 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 fur ¨ 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 offnen ¨ 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¨ahigkeiten des Laufwerks auslesen . . . . . . . . . . . . . . 112
6.2.3 Audio-CDs abspielen . . . . . . . . . . . . . . . . . . . . . . 114 Ansteuerung einer Soundkarte . . . . . . . . . . . . . . . . . . . . . 121 6.3.1 6.3.2
6.4
Devices offnen ¨ 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¨aten anhand eines Beispiels . . . 156
7 Netzwerkprogrammierung . . . . . . . . . . . . . . . . . . . . . . . . . . 163 7.1 Einfuhrung ¨ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 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¨achen . . . . . . . . . . . . . . . . . . . . . . . . 205 8.1 Die grafische Oberfl¨ache X11 . . . . . . . . . . . . . . . . . . . . . . 205 8.2
Das Toolkit GTK+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 8.2.1 GTK 1.2 versus GTK 2.0 . . . . . . . . . . . . . . . . . . . . . 206 8.2.2
GTK-Programme ubersetzen ¨ . . . . . . . . . . . . . . . . . . 207
8.2.3 8.2.4
Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 208 Das Callback-Prinzip . . . . . . . . . . . . . . . . . . . . . . . 210
8.2.5 8.2.6
Schaltfl¨achen (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 fur ¨ Text und Zahlen . . . . . . . . . . . . . . . 228 8.2.12 Menus ¨ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 8.2.13 Pixmap-Grafiken darstellen . . . . . . . . . . . . . . . . . . . 238 8.2.14 Zeichenfl¨achen . . . . . . . . . . . . . . . . . . . . . . . . . . 244 8.2.15 Zeichenfl¨ache 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-Ubertragung 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: Fur ¨ den Anwender ist die Unterstutzung ¨ von USB- und anderen Plug&Play-Ger¨aten hinzugekommen, fur ¨ 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¨amtliche Quelltexte wurden an die LSB angepasst und das Kapitel uber ¨ die Ansteuerung von Ger¨aten wurde um das Thema USB erweitert. Daruber ¨ hinaus sind in dem Kapitel Netzwerkprogrammierung“ Beispiele zu UDP, Broadcast und Multicast ” erg¨anzt 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¨altigen Moglichkeiten ¨ des Betriebssystems in eigenen Programmen nutzen mochte. ¨ 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 Fruhjahr ¨ 2010
Martin Gr¨afe
Kapitel 1
Einfuhrung ¨ 1.1 Warum gerade C“? ” Unter Linux ist mittlerweile eine Vielzahl von Programmiersprachen verfugbar, ¨ angefangen von Pascal und Fortran uber ¨ 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¨amtliche fur ¨ die systemnahe Programmierung erforderlichen Include-Dateien unter C zur Verfugung. ¨ Aus diesem Grund lassen sich die Moglichkeiten ¨ des Betriebssystems (und der Hardware) mit C so vollst¨andig wie mit keiner anderen Programmiersprache nutzen.2 Da C eine kompakte, relativ maschinennahe“ Programmiersprache ist, sind C” Programme effizient und schonen die System-Ressourcen. Fur ¨ das Ausfuhren ¨ 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 Gluck ¨ 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 Einfuhrung ¨
1.2 Bevor es losgeht . . . . . . mussen ¨ die notwendigen Tools und Dateien installiert sein. In der Anfangszeit von Linux, als man nach der Installation erst einmal den Kernel nach eigenen Wunschen ¨ neu kompilierte, waren die Werkzeuge fur ¨ 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¨altlichen Distributionen sind diese Pakete nicht einmal mehr enthalten, sondern mussen ¨ aus dem Internet nachgeladen werden. Um die Beispiele in diesem Buch ubersetzen ¨ zu konnen, ¨ benotigen ¨ Sie das Paket mit dem C-Compiler gcc“ sowie das Programm make“. Außerdem werden fur ¨ ” ” das Einbinden von Funktionsbibliotheken in eigene Programme die zugehorigen ¨ Include-Dateien benotigt, ¨ die h¨aufig 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, benotigen ¨ Sie zus¨atzlich das zugehorige ¨ Entwicklungspaket“. Je ” nach Distribution heißt dieses Paket beispielsweise ncurses-dev-Version“
” oder
ncurses-devel-Version“. ” Im Folgenden soll exemplarisch fur ¨ die Distributionen SuSE“ und Ubuntu“ ge” ” zeigt werden, wie Pakete nachinstalliert werden konnen. ¨
1.2.1 Paketverwaltung unter SuSE-Linux In der Linux-Distribution von SuSE1 ist das Tool YaST“ (Abkurzung ¨ fur ¨ Yet ” ” Another Setup Tool“) enthalten, das fur ¨ alle wichtigen Systemeinstellungen zust¨andig ist. Dieses kann entweder aus dem Menu¨ 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 loschen“ ¨ in ” der Rubrik Software“ wird die Paketverwaltung geoffnet ¨ (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 (Abkurzung ¨ fur ¨ RedHat Packet Manager“) vor. Daher konnen ¨ einzelne Pakete auch mit dem Be” fehl rpm -i Paket-Datei“ installiert werden. ” 1
SuSE steht fur ¨ 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 Einfuhrung ¨
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. Fur ¨ die Auswahl und Installation der Pakete bringt der GNOMEDesktop unter Ubuntu ein spezielles Tool mit, das uber ¨ das GNOME-Menu¨ aufgerufen werden kann (siehe Abbildung 1.2).
¨ Abbildung 1.2: Offnen der GNOME-Paketverwaltung bei Ubuntu-Linux
In der Startansicht bietet das Werkzeug nur das Installieren oder Entfernen gan¨ zer Anwendungen an, ohne die einzelnen Pakete im Detail aufzulisten. Uber den Menupunkt ¨ Datei/Erweitert“ l¨asst sich die Darstellung erweitern, sodass ” ¨ die einzelnen Pakete aufgelistet werden konnen ¨ (siehe Abbildung 1.3). Ahnlich wie bei YaST unter SuSE-Linux ist auch hier die Moglichkeit ¨ gegeben, nach bestimmten Paketen anhand von Stichworten zu suchen (Schaltfl¨ache Suche“ unten ” links). Wenn Sie Pakete von Hand“ installieren wollen, so gibt es dafur ¨ 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 fur ¨ die GNOME-Paketverwaltung
5
6
1 Einfuhrung ¨
1.3 Die Werkzeuge In diesem Abschnitt stellen wir in kurzer Form die fur ¨ das Programmieren erforderlichen Werkzeuge vor. In Abschnitt 1.4 wird der Umgang mit diesen Werkzeugen dann anhand von Beispielen erl¨autert. Den Schwerpunkt bilden dabei die fur ¨ Unix und Linux ublichen ¨ 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, benotigt ¨ man naturlich ¨ zun¨achst 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 uber ¨ eine eigene grafische Benutzeroberfl¨ache verfugen. ¨ Letztere sind in der Regel komfortabler, weil sie uber ¨ Syntax-Highlighting verfugen, ¨ benotigen ¨ aber auch weit mehr Ressourcen. Editoren fur ¨ die Textkonsole: vim (steht fur ¨ VI improved“) ” emacs pico joe jedit ... Editoren mit grafischer Oberfl¨ache: kate (Bestandteil von KDE) gedit (Bestandteil von GNOME) nedit xemacs ... Der Editor kate“ bietet außerdem die Moglichkeit, ¨ C-Funktionen einzuklap” ” pen“, um den Quelltext ubersichtlicher ¨ darzustellen (siehe Abbildung 1.4). Nicht alle hier erw¨ahnten Editoren sind in jeder Linux-Distribution enthalten. Ggf. mussen ¨ die zugehorigen ¨ Pakete erst aus dem Internet geladen und gem¨aß Abschnitt 1.2 installiert werden.
1.3 Die Werkzeuge
Abbildung 1.4: Zwei Editoren fur ¨ Programmierer: nedit“ und kate“ ” ”
7
8
1 Einfuhrung ¨
1.3.2 Der GNU C-Compiler gcc Kernstuck ¨ der Software-Entwicklung ist der C-Compiler selbst, also der gcc bzw. g++ (fur ¨ C++-Programme). Der gcc ist ein so genannter Cross-Compiler, mit dem man im Grunde auch Programme fur ¨ 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 ausfuhrbares ¨ 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¨aßigeren Namen w¨ahlen 1 wollen. Dies geschieht mit Hilfe der Option -o“: ” gcc Quelltext -o Ausgabedatei
Beispiel: gcc hello.c -o hello
Bei diesem Aufruf fuhrt ¨ der gcc zwei Schritte durch: das eigentliche Kompilieren ¨ (Ubersetzen) und das Linken zu einem ausfuhrbaren ¨ Programm. Letzterer sorgt z.B. dafur, ¨ dass Funktionsaufrufe wie printf() mit den entsprechenden Funktionen aus der dynamischen Bibliothek libc“ verknupft ¨ werden. Wird ein Pro” gramm auf mehrere Quelltexte aufgeteilt, so mussen ¨ die einzelnen Programmteile zun¨achst nur ubersetzt ¨ werden, ohne den Linker aufzurufen. Dazu wird beim ¨ Ubersetzen die Option -c“ angegeben. Der Compiler erzeugt in diesem Fall nur ” eine Objektdatei, die automatisch die Endung .o“ erh¨alt. 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 benotigen. ¨ Eine vollst¨andige Beschreibung erh¨alt 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 ¨ Fur ¨ das Ubersetzen kleinerer Programme benotigt ¨ 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 Da¨ teien nicht nur ubersichtlicher, ¨ es ist dann auch moglich, ¨ bei Anderungen nur diejenigen Dateien neu zu ubersetzen, ¨ die ge¨andert wurden. Genau hier setzt das 1
Sie sollten fur ¨ erste Versuche nicht den Namen test“ w¨ahlen, da ein gleichnamiges Programm schon ” Bestandteil der Shell ist!
1.3 Die Werkzeuge
9
Programm make“ an. Es pruft, ¨ ob sich die Quellen eines Programmteils ge¨andert ” haben, und ubersetzt ¨ diesen Teil dann neu. Das Tool make“ benotigt ¨ dazu eine Datei, in der die Abh¨angigkeiten 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 mussen ¨ mit einem oder mehreren Tabulatoren ( echte“ ” Tabs, keine Leerzeichen!) eingeruckt ¨ sein, w¨ahrend die Zieldatei immer am Zeilenanfang stehen muss. Eine solche Make-Datei kann beliebig viele Zieldateien mit den zugehorigen ¨ 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¨angigkeiten pruft ¨ und ggf. weitere, fur ¨ 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¨aufig ein Eintrag der folgenden Form: clean: rm -f *.o
Mit dem Aufruf make clean“ werden dann Objekt-Dateien, die man nicht mehr ” benotigt, ¨ geloscht. ¨ Man beachte, dass hier keine Quelldateien angegeben sind, was dazu fuhrt, ¨ dass die Anweisung immer ausgefuhrt ¨ wird. Fur ¨ eine ausfuhrliche ¨ Anleitung siehe auch man make“. ”
10
1 Einfuhrung ¨
1.3.4 Fur ¨ die Fehlersuche: Die Debugger Nur selten l¨auft 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. Ursprunglich ¨ gab es eine relativ rudiment¨are grafische Oberfl¨ache fur ¨ den GNU Debugger namens xxgdb“. Dieses Projekt wur” de aber vor geraumer Zeit durch ein von Grund auf neu gestaltetes Tool ersetzt, den DDD“ (Abkurzung ¨ fur ¨ Data Display Debugger“, siehe Abbildung 1.5). Der ” ” DDD ist kein eigenst¨andiger Debugger sondern eine grafische Benutzeroberfl¨ache fur ¨ den GNU Debugger gdb. Das Tool wurde ubrigens ¨ in Deutschland entwickelt!
Abbildung 1.5: Ein elektronischer Kammerj¨ager: 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
ausfuhrbare ¨ Programm mit dem zugehorigen ¨ Quelltext in Verbindung bringen kann. Diese Zusatzinformationen fugt ¨ 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 konnen ¨ Sie nun so genannte Breakpoints setzen, das Programm schrittweise ausfuhren ¨ und den Inhalt von Variablen anzeigen. Kommt es zur Laufzeit des Programms zu einem Fehler, der die Ausfuhrung ¨ 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 gefuhrt ¨ hat. ¨ Ubrigens: nach erfolgreicher Fehlerbeseitigung lassen sich die fur ¨ die Ausfuhrung ¨ des Programms nicht notwendigen Debug-Zusatzinformationen mit strip hello
wieder entfernen, ohne das Programm neu zu ubersetzen. ¨ Dadurch reduziert sich die Große ¨ des Programms offtmals erheblich.
1.3.5 Integrierte Entwicklungsumgebungen Als Alternative zur direkten“ Verwendung der bisher vorgestellten Werkzeuge ” gibt es die Moglichkeit, ¨ mit einer integrierten Entwicklungsumgebung1 zu arbeiten. Dabei handelt es sich um ein Programm, das neben einem Quelltext-Editor auch eine grafische Schnittstelle fur ¨ Compiler, Debugger usw. bietet. Insbesondere Umsteiger aus der Windows-Welt“ finden mit Hilfe solcher Programme h¨aufig ” leichter den Einstieg in die Linux-Programmierung. Man sollte jedoch beachten, dass Entwicklungsumgebungen keine Compiler, sondern eben nur Umgebungen ¨ sind und zum Ubersetzen und Linken des Quelltextes wieder auf den C-Compiler gcc zuruckgreifen. ¨ Der Vorteil solcher Programme ist, dass das Wechseln zwischen den Werkzeugen ¨ Editor, Compiler, Debugger usw. entf¨allt. Tritt beispielsweise beim Ubersetzen des Programms ein Fehler auf, wird automatisch die entsprechende Zeile im Quelltext markiert.
1
auch als IDE“ fur ¨ Integrated Development Environment bezeichnet ”
12
1 Einfuhrung ¨
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 a` la Turbo-Pascal: xwpe
13
14
1 Einfuhrung ¨
Unter Linux sind verschiedene Entwicklungsumgebungen frei verfugbar; ¨ 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, menugef ¨ uhrt ¨ und mit einer mehr oder weniger umfangreichen Online-Hilfe ausgestattet, die uber ¨ den entsprechenden Menupunkt ¨ aufgerufen werden kann. Die aktuellen Versionen von KDevelop, Eclipse (inkl. CDT) und Anjuta sind recht umfangreich und benotigen ¨ mehr als 50 MByte. KDevelop bringt es mit den zugehorigen ¨ Dokumentationspaketen sogar auf mehrere 100 MByte – ein Grund, bei nicht so leistungsstarken Rechnern doch auf die schlanken“ Kommandozeilen” Programme zuruckzugreifen. ¨ Eine Ausnahme bildet das nicht so bekannte Tool xwpe“ (Abbildung 1.9), das ” deutlich weniger Ressourcen benotigt, ¨ aber auch nicht so komfortabel ist wie beispielsweise KDevelop. Die Entwicklungsumgebung Eclipse f¨allt etwas aus der Reihe: Das in Java programmierte Tool bildet eine Art Framework“. Fur ¨ die Entwicklung von ” Java-Programmen wird zus¨atzlich das Paket JDT“ (Java Development Tooling) ” benotigt, ¨ fur ¨ 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¨aufig 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 Ubersetzen 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 # include "primz_math.h" int main() { int zahl; for (zahl=1; zahl 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 man -k sleep sleep (1) - delay for a specified amount of time sleep (3) - Sleep for the specified number of seconds
Sektion 1 enth¨alt hier die Beschreibung des Shell-Kommandos, Sektion 3 die der gleichnamigen C-Funktion. In diesem Fall kann uber ¨ die Angabe der Sektion gezielt eine der beiden Seiten abgerufen werden: man 3 sleep
Abbildung 1.13: xman – eine grafische Benutzerschnittstelle fur ¨ 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 verfugbaren ¨ Hilfe-Texte ” auf. Durch Anklicken der entsprechenden Funktion konnen ¨ Sie die gewunschte ¨ Beschreibung abrufen.
28
1 Einfuhrung ¨ 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 benotigen ¨ 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 fur ¨ 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 Weiterfuhrende ¨ 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 nutzlicher ¨ Informationen beschaffen. Hilfreich ist hier auch die Verwendung des Programms grep“, mit dem Sie alle ” Include-Dateien nach bestimmten Zeichenketten durchsuchen konnen, ¨ 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¨asst sich relativ schnell die gesuchte Definition aufspuren. ¨
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. Dafur ¨ wurden die Programme Anjuta“ und KDevelop“ ausgew¨ahlt. Abschließend reißen wir noch ” ” kurz den Einstieg in das Tool Eclipse“ an. ”
2.1 Anjuta Die Entwicklungsumgebung Anjuta“ ist an die Oberfl¨ache GNOME ange” lehnt und unterstutzt ¨ daher auch im Besonderen die Erstellung von GNOMEApplikationen. Shell-Programme ohne grafische Oberfl¨ache lassen sich selbstverst¨andlich ebenfalls mit Anjuta erstellen. Anjuta ist viel zu umfangreich, um in diesem Buch vollst¨andig beschrieben werden zu konnen. ¨ In den folgenden Abschnitten soll stattdessen Schritt fur ¨ 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
offnet ¨ sich das Hauptfenster, das eine ganze Reihe von Menus ¨ und Schaltfl¨achen, aber noch kein Editor-Fenster fur ¨ die Eingabe von Quelltexten enth¨alt. Um ein neues Programm zu erstellen, sollte man zun¨achst ein neues Projekt anlegen. Dies
32
2 Arbeiten mit einer Entwicklungsumgebung
geschieht mit dem Menupunkt ¨ 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 Grundgerust ¨ des Projekts erstellt.
Abbildung 2.1: Die verschiedenen Projekttypen bei Anjuta
Als Projekttyp w¨ahlen Sie bitte Generic/Terminal Projcet“ aus (siehe Abbil” dung 2.1) und geben als Projektname primzahl“ ein. Das anschließend erschei” nende Eingabefeld fur ¨ eine Projektbeschreibung konnen ¨ Sie leer lassen. Unter Projekt Optionen“ sollten Sie die Unterstutzung ¨ von Gettext deaktivieren. Da” nach werden Sie um die Best¨atigung Ihrer Eingaben gebeten:
2.1 Anjuta
33
¨ Bestatigen Sie die folgenden Informationen: Projektname: Projekttyp: Zieltyp: Quelltyp: Version: Autor: Sprache: Gettext Support:
primzahl GENERIC EXECUTABLE primzahl 0.1 C Nein
Mit der Schaltfl¨ache Abschließen“ best¨atigen Sie die Einstellungen und gelangen ” wieder zum Hauptfenster von Anjuta, das jetzt ein Fenster mit der Projektstruktur und eines mit Compiler-Ausgaben enth¨alt (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 Projektubersicht ¨ ” wird ein Editor-Fenster geoffnet, ¨ in dem diese Datei bearbeitet werden kann. Ersetzen Sie hier bitte den von Anjuta standardm¨aß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 int ist_primzahl(int zahl) { int teiler=2; while (teiler 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 uber ¨ das Desktop-Menu¨ gestartet werden (bei SuSE-Linux Entwicklung/Entwicklungsumgebung/KDevelop C/C++“) oder aus einer Shell ” uber ¨ das Kommando kdevelop“. ” Um ein neues Projekt in KDevelop anzulegen, w¨ahlen Sie den Menupunkt ¨ Pro” ject/New Project...“. Danach offnet ¨ sich ein Fenster zur Auswahl des Projekttyps (siehe Abbildung 2.4). W¨ahlen 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 konnen ¨ 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
fur ¨ Header- und Quelltextdateien an. Auch hier konnen ¨ Sie einfach auf next“ ” bzw. finish“ klicken. Danach befinden Sie sich wieder in dem Hauptfenster ” von KDevelop mit geoffnetem ¨ 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 uber ¨ File/Save“. ” Als N¨achstes benotigen ¨ wir noch den Quelltext mit der Funktion ist primzahl(). Dazu w¨ahlen Sie bitte den Menupunkt ¨ 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¨atigen dieses Dialogs mit OK“ offnet ¨ sich ein Fenster mit dem ” Automake Manager“. Best¨atigen Sie auch diesen Dialog mit OK“. ” ” Um alle zum Projekt gehorenden ¨ Dateien aufzulisten, klicken Sie die Schaltfl¨ache File List“ am linken Rand des Hauptfensters an. Danach konnen ¨ Sie die neue Da” tei primz math.c“ durch einen Klick auf den Dateinamen in dem Editorbereich ” offnen. ¨ 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 fur ¨ unser kleines Projekt eingegeben, und das KDevelop-Hauptfenster musste ¨ sich wie in Abbildung 2.6 darstellen. ¨ Fur ¨ das Ubersetzen des Primzahlprogramms wird die Bibliothek libm“ benotigt, ¨ ” es muss also die Compiler-Option -lm“ angegeben werden. Dies geschieht ” bei KDevelop uber ¨ den Menupunkt ¨ 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: Uber die Project Options“ konnen ¨ die benotigten ¨ Libraries angegeben ” werden (hier -lm“). ”
2.3 Eclipse + C Development Tooling (CDT)
39
Wenn Sie hier die Configure Options“ ausw¨ahlen, erhalten Sie unter anderem ein ” Eingabefeld fur ¨ die Linker flags (LDFLAGS)“. Dort muss fur ¨ das Primzahlpro” gramm die Option -lm“ eingetragen werden. ” Um die Quelltexte in ein ausfuhrbares ¨ Programm zu ubersetzen, ¨ w¨ahlen Sie nun den Menupunkt ¨ 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 Ubersetzen der Quelltexte kann das Primzahlprogramm nun uber ¨ den Menupunkt ¨ Build/Execute Program“ oder uber ¨ das Zahnrad-Icon uber ¨ ” dem Editorbereich gestartet werden. Die Programmausgaben – in diesem Fall also die Primzahlen bis 97 – erscheinen in einem separaten Terminal-Fenster, das von KDevelop geoffnet ¨ wird.
2.3 Eclipse + C Development Tooling (CDT) Im Folgenden wird der Einstieg in die Arbeit mit der Eclipse-Umgebung beschrieben, fur ¨ weiterfuhrende ¨ 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 ursprunglich ¨ fur ¨ die Entwicklung von Java-Programmen konzipiert. Es ist jedoch so angelegt, dass die programmiersprachenabh¨angigen Teile als Plug-ins“ geladen werden. So gibt ” es unter anderem das C Development Tooling (CDT) als Plug-in fur ¨ die Programmierung in C. Um mit Eclipse C-Programme entwickeln zu konnen, ¨ benotigen ¨ Sie zwingend dieses Paket. Sollte Ihre Distribution das CDT nicht beinhalten, konnen ¨ 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, offnet ¨ sich das in Abbildung 2.9 dargestellte Auswahlfenster. Fur ¨ die n¨achsten Schritte w¨ahlen 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¨atzliche Plug-ins wie das CDT in Eclipse einzubinden, kann aus dem Menu¨ der Eclipse-Workbench der Eintrag Help/Software Updates/Manage ” Configuration“ gew¨ahlt 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 Verknupfungen ¨ fur ¨ verschiedene Aktionen. Hier kann fur ¨ ein links selektiertes Plug-in die Aktion Aktivieren“ bzw. Enable“ gew¨ahlt ” ” werden.
2.3.2 Ein neues Projekt anlegen ¨ Ahnlich den beiden zuvor vorgestellten Entwicklungsumgebungen bietet auch Eclipse einen Wizard“ fur ¨ das Erstellen eines neuen Projekts an. W¨ahlt man den ” Menupunkt ¨ File/New/Project...“ aus, o¨ ffnet sich das Auswahlfenster fur ¨ die Art ”
2.3 Eclipse + C Development Tooling (CDT)
41
des Projekts. Nach erfolgreicher Installation des CDT kann hier Standard Make ” C Project“ gew¨ahlt werden. Danach mussen ¨ Sie in einem weiteren Fenster den Projektnamen eingeben. Ist das neue Projekt angelegt, konnen ¨ Quelltexte neu erstellt ( File/New“) oder ” bereits als Textdatei vorliegende Quelltexte importiert werden ( File/Import“). ” Haben Sie den Quelltext eingegeben oder importiert, konnen ¨ Sie uber ¨ den Menupunkt ¨ Project/Build Project“ das Programm kompilieren und uber ¨ den ” Menupunkt ¨ Run“ ausfuhren. ¨ ”
Kapitel 3
Kommandozeilenprogramme In der Linux-Welt gibt es eine Vielzahl leistungsf¨ahiger Programme, die ohne grafische Benutzerschnittstelle auskommen. Die Steuerung dieser Programme erfolgt uber ¨ die Kommandozeilenparameter und die Standardein- und -ausgabe (vgl. Abschnitt 4.1.2). Der Vorteil liegt zum einen in dem gegenuber ¨ einer grafischen Steuerung deutlich geringeren Programmieraufwand – h¨aufig bei gleichem Nutzen. Zum anderen eroffnet ¨ sich dadurch die Moglichkeit, ¨ verschiedene Programme uber ¨ Pipes oder Shell-Skripte sehr einfach miteinander zu verknupfen. ¨ Das macht die Linux-Shell zu einem m¨achtigen Werkzeug. Voraussetzung ist jedoch, dass sich die Programme – wie unter Linux/Unix ublich ¨ – mit Hilfe geeigneter Parameter und Optionen entsprechend steuern lassen. In diesem Kapitel wird die Auswertung der Kommandozeilenparameter und die Bedeutung des Ruckgabewertes ¨ beschrieben, ebenso wie typische Optionen und (Fehler-)Meldungen von Linux-Programmen. Diese Konventionen sollten im ¨ Ubrigen auch fur ¨ X11-Programme gelten (vgl. Kapitel 8), da auch diese Programme h¨aufig 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 Moglichkeiten ¨ fur ¨ erweiterte Ausgabesteuerung.
3.1 Parameter und Ruckgabewert ¨ der Funktion main() Werfen wir zun¨achst einen Blick auf die typische Deklaration der Funktion main(), die gewissermaßen das Kernstuck ¨ 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 Ruckgabewert ¨ vom Typ Integer. Aber welche Bedeutung hat dieser Ruckgabewert, ¨ wo doch das Programm nach der Ruckkehr ¨ aus main() beendet ist?
3.1.1 Die Bedeutung des Ruckgabewertes ¨ 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¨auft. ” Nach Beendigung des Programms ls“ wird der Shell-Prozess daruber ¨ infor” miert, dass das Programm beendet wurde und mit welchem Resultat. Und genau dieses Resultat entspricht dem Ruckgabewert ¨ der Funktion main(), wie sie im Programm ls“ realisiert ist. ” Bei erfolgreicher Beendigung des Programms sollte immer der Wert 0 zuruck¨ gegeben werden. Ist ein Fehler aufgetreten, wurden z.B. falsche Parameter angegeben, so sollte das Programm dies durch einen Ruckgabewert ¨ 6= 0 signalisieren, damit auch der Eltern-Prozess entsprechend reagieren kann.
In einem Shell-Skript konnen ¨ Sie mit $?“ den Ruckgabewert ¨ des zuletzt aus” gefuhrten ¨ Programms abfragen (die Benutzereingaben sind schr¨ag 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¨achst sei einmal erw¨ahnt, dass die Ubergabeparameter der Funktion main() nicht zwingend argc“ und argv“ heißen mussen ¨ – die Wahl der Variablenna” ” men ist fur ¨ main() ebenso wie fur ¨ jede andere Funktion beliebig. Wenn es jedoch nicht Ihr oberstes Bestreben ist, den Quelltext unleserlich zu gestalten, sollten Sie ¨ auf eine Anderung dieser etablierten Bezeichnungen unbedingt verzichten. 1 Die Variable 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¨amlich den Namen (und ggf. den Pfad) des Programms selbst. Das Element argv[1] zeigt auf die 1
argc ist tats¨achlich eine Variable und keine Konstante. Es kann unter bestimmten Umst¨anden zweckm¨aßig sein, ihren Wert zu ver¨andern.
3.1 Parameter und Ruckgabewert ¨ der Funktion main()
45
Zeichenkette mit dem ersten Kommandozeilenparameter. Das letzte Element von argv[] enth¨alt eine Null. argv
- argv[0] argv[1] argv[2]
.. . argv[ N ] 0L
- Programmname - 1. Parameter - 2. Parameter - letzter Parameter
¨ Abbildung 3.1: Ubergabe der Kommandozeilenparameter mittels argv
¨ Ubrigens: Das Feld argv[] darf vom Programm auch ver¨andert werden. So konnen ¨ z.B. Elemente entfernt und die nachfolgenden Eintr¨age aufgeruckt“ ¨ wer” den. Verschiedene Funktionsbibliotheken, z. B. die libgtk, machen davon Gebrauch.
3.1.3 Auswerten der Kommandozeilenparameter Am Anfang jedes Programms sollten zun¨achst die Kommandozeilenparameter ausgewertet werden. Man unterscheidet hier zwischen Argumenten und Optionen. Bei den Argumenten handelt es sich meistens um Dateinamen, w¨ahrend Optionen als eine Art Schalter“ zu betrachten sind, uber ¨ die sich das Verhalten des Pro” gramms steuern l¨asst. 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¨amlich die zu loschenden ¨ Dateien –, w¨ahrend die Option -f“ das Programm rm ” veranlasst, falls eine der angegebenen Dateien nicht existiert, keine Fehlermeldung auszugeben. Es sei noch erw¨ahnt, dass Optionen ihrerseits auch wieder Argumente besitzen konnen ¨ – ein Beispiel hierfur ¨ 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 fur ¨ 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 ublichen ¨ 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 gluck¨ licherweise 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 ubergeben, ¨ die alle zul¨assigen Optionen enth¨alt. Ein :“ hin” ter einem Optionszeichen in optstring bedeutet, dass diese Option ein Argument erfordert (z.B. "o:" fur ¨ die Option -o Dateiname“). Zwei Doppelpunkte ” zeigen an, dass das Argument fur ¨ diese Option optional, also nicht notwendig ist. Die Funktion getopt() sortiert zun¨achst das Feld argc[] so, dass die Optionen nach vorn und die Argumente nach hinten geschoben werden. Als Ruckgabewert ¨ 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¨achste 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() fuhrt ¨ automatisch eine Fehlerbehandlung durch, d.h. bei ungultigen ¨ Optionen oder fehlenden Optionsargumenten werden entsprechende Fehlermeldungen ausgegeben. Ein kleines Beispiel soll das Einlesen der Kommandozeilenparameter und damit die Arbeitsweise von getopt() erl¨autern: 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 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 Ruckgabewert ¨ 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¨assige 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). Stoßt ¨ getopt() ” auf eine unzul¨assige, also in der Optionsliste nicht angegebene Option, liefert die Funktion das Zeichen ?“, was bei diesem Beispielprogramm zu einem Abbruch ” mit Fehlercode 1 fuhrt ¨ (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¨aufig macht man bei der Angabe von Dateinamen Gebrauch von so genannten Platzhaltern, insbesondere wenn ein Befehl auf mehrere Dateien mit bestimmten ¨ Ubereinstimmungen im Dateinamen angewendet werden soll. Beispiel: rm *.o
wird alle Dateien loschen, ¨ die auf .o“ enden. Doch was geschieht, wenn sol” che Platzhalter angewendet werden? W¨ahrend es bei manchen Betriebssystemen Aufgabe der Programme selbst ist, diese Platzhalter aufzulosen“, ¨ also durch die ” entsprechenden Dateinamen zu ersetzen, ubernimmt ¨ unter Linux die Shell diese Aufgabe (z.B. die bash). Das bedeutet fur ¨ das obige Beispiel, dass die Shell vor der Ausfuhrung ¨ des Programms rm“ die Kommandozeile ersetzt, z.B. durch: ” rm primz_haupt.o primz_math.o
Dies erspart dem Programmierer unter Linux den Programmieraufwand fur ¨ das Durchsuchen des Verzeichnisses nach ubereinstimmenden ¨ Dateinamen. Auf der anderen Seite sollte der Programmierer aber auch berucksichtigen, ¨ dass ein Kommandozeilenparameter unter Umst¨anden durch mehrere Dateinamen ersetzt wird. In diesem Zusammenhang ist folgende Syntax fur ¨ die Kommandozeilenparameter eines Programms moglichst ¨ 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 fur ¨ die Eingabedatei (um sich Tipparbeit zu sparen): mein_programm primz*.c
so wird die Shell dies moglicherweise ¨ ersetzen durch: mein_programm primz_haupt.c primz_math.c
und damit wurde ¨ unbeabsichtigt die zweite Datei als Ausgabedatei geoffnet ¨ und uberschrieben! ¨ Besser ist es daher, fur ¨ 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 Moglichkeit ¨ keine Rolle spielen, da die Shell beim Ersetzen eines Platzhalters durch mehrere Dateinamen keine bestimmte Reihenfolge einh¨alt.1
3.2 Konventionen fur ¨ Kommandozeilenprogramme Um dem Linux-Anwender die Kommandozeilen-Bedienung von Programmen zu erleichtern, sollten sich alle Programme an gewisse Linux-typische Konventionen halten. Das sind naturlich ¨ keine Vorschriften“ – es obliegt letztendlich der Ver” antwortung des Programmierers, inwieweit er sich daran h¨alt. Es handelt sich dabei mehr um nutzliche ¨ Tipps fur ¨ das Erstellen gut bedienbarer Programme, die eine Vielzahl von Anwendern erreichen sollen.
3.2.1 Ein Muss: Die Hilfe-Option H¨aufig stoßt ¨ man bei freier Software (public domain) auf kommandozeilenbasierte Programme, die laut Beschreibung (z.B. im Internet) genau die gesuchte Losung ¨ fur ¨ ein bestimmtes Problem darstellen. Doch wie war noch gleich die Syntax fur ¨ die Argumente und Optionen? H¨aufig bleibt da nur die Suche nach der zugehori¨ gen README“-Datei. ” Gerade wenn man das Programm schon einmal angewendet hat, sich aber z.B. nicht mehr ganz sicher uber ¨ 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 fur ¨ Kommandozeilenprogramme
49
etwas umst¨andlich. Daher sollte jedes Programm uber ¨ eine Kurzhilfe verfugen, ¨ 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 konnen. ¨ Ein schones ¨ Beispiel ist das Programm dd“, das eine ” ” ganze Reihe von Optionen besitzt, die nicht immer leicht zu merken sind. Mit dd ” --help“ erh¨alt man jedoch die folgende ausfuhrliche ¨ 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 .
Hier erh¨alt der Anwender die Information, was das Programm leistet, welche Argumente und Optionen es kennt und an wen man mogliche ¨ 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 fur ¨ 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 ur weitere Informationen. Versuchen Sie ≫cp --help≪ f¨
Die Fehlermeldung gibt hier nicht nur unmissverst¨andlich den Grund des Fehlers an, sondern auch den Namen des Programms selbst. Zus¨atzlich 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.
Fur ¨ gewohnlich ¨ 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 Offnen, Lesen oder Schreiben von Dateien ist es außerdem sinnvoll, die Funktion perror() aufzurufen, um den genauen Grund des Dateizugrifffehlers anzugeben:
3.2 Konventionen fur ¨ Kommandozeilenprogramme
51
void perror(const char *s);
Diese Funktion gibt die als Parameter angegebene Zeichenkette in den StandardFehlerkanal aus, fugt ¨ 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 ubergeben, ¨ 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¨ache in einem Fenster dargestellt werden, so kann der Fehlertext auch uber ¨ die Funktion strerror() ermittelt werden. Als Parameter mussen ¨ 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 fuhrt, ¨ ein entsprechender Ruckgabewert ¨ 6= 0 gesetzt werden. Tritt der Fehler nicht direkt in main(), sondern innerhalb einer Unterfunktion auf, so kann dies mit der Funktion exit(R¨ uckgabewert);
geschehen.
3.2.3 Eigene Manpages erstellen Zu einem guten Programm gehort ¨ 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¨andi” gen Pfad gehort ¨ 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¨angt. H¨aufig sind die Dateien mit gzip komprimiert, was an der Endung .gz“ zu erkennen ist. ” Manpages sind reine Textdateien und konnen ¨ mit jedem Editor erstellt werden. Neben dem eigentlichen Text enthalten die Dateien Steuersequenzen, die u.a. die
52
3 Kommandozeilenprogramme
Formatierung beeinflussen. Sie mussen ¨ am Zeilenanfang stehen und beginnen mit einem .“. Des Weiteren gibt es einige Steuersequenzen, die mit \“ beginnen und ” ” z. B. Texthervorhebungen bewirken. Diese Sequenzen mussen ¨ nicht zwingend am Zeilenanfang stehen. Die wichtigsten fasst Tabelle 3.1 zusammen. Tabelle 3.1: Beschreibung einiger Steuersequenzen fur ¨ man-Hilfeseiten Sequenz
Beschreibung
.\" .TH .SH .fi .nf
Kommentarzeile Titel, Kopf- und Fußzeile festlegen ¨ Uberschrift fur ¨ den n¨achsten Abschnitt ab hier: Blocksatz ab hier: keine Formatierung
\fR \fB \fI \-
normaler Text Fettschrift kursiv (oder unterstrichen) Gedankenstrich
Der Sequenz fur ¨ Titel, Kopf- und Fußzeile konnen ¨ bis zu funf ¨ Angaben folgen: .TH "Name" "Sektion" "Datum" "Author / Version" "Thema"
Beispiel: .TH SLEEP 3 "Apr. 1993" GNU "Linux Programmer’s Manual"
¨ Die Steuerungssequenz fur ¨ eine Uberschrift benotigt ¨ als einzige Angabe den ¨ Uberschrift-Text. Als Beispiel soll hier eine kurze Manpage fur ¨ 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¨ afe" "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¨ ur das erste Kapitel des Buches "\fIC und Linux\fR". .nf .SH AUTOR Martin Gr¨ afe (
[email protected])
Als primzahl.1“ abgespeichert l¨asst sich diese Hilfeseite mit ” man -l primzahl.1
3.3 Programme mehrsprachig auslegen
53
formatieren und anzeigen (Abbildung 3.2). Die Option -l“ ermoglicht ¨ 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¨asst 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¨angst vorbei. Auch die aktuellen Linux-Distributionen pr¨asentieren sich hierzulande in deutschem Gewand“, ” d. h. Menus ¨ und Dialog-Boxen sind zum Großteil deutschsprachig. Die Unterstutzung ¨ l¨anderabh¨angiger 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 Kurzel ¨ en“ steht hier fur ¨ englisch“, die Erweiterung US“ kennzeichnet, ” ” ” dass es sich um nordamerikanisches Englisch handelt. Diese Angabe kann noch um die Spezifikation des ISO-Zeichensatzes erg¨anzt werden, z.B.: > export LANG=de DE.ISO-8859-1
Die von Linux unterstutzten ¨ L¨andereinstellungen findet man als Verzeichnisse unter dem Pfad /usr/share/locale“. ” Die Auswahl der Sprach-/L¨andereinstellung 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 fur ¨ alle Kategorien sein muss. Der Parameter locale muss entweder eine gultige ¨ L¨anderkennzeichnung wie de DE“ enthalten oder eine leere Zeichenkette ("") sein, wenn die L¨anderkenn” zeichnung von der Umgebungsvariablen LANG ubernommen ¨ 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 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 Ubersetzen mit gcc sprache.c -o sprache“ kann das Programm ” aufgerufen werden: > sprache PI=3.142 PI=3,142
Offensichtlich fuhrt ¨ die gleiche printf()-Anweisung in Zeile 13 des Quelltextes zu einem anderen Ergebnis als in Zeile 11. Das Dezimal-Trennzeichen wird in die¨ sem Beispiel von .“ auf ,“ umgeschaltet. Erreicht wird das durch Andern 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 fuhrt. ¨ In diesem Fall muss beim ¨ Ubersetzen des Quelltextes die Bibliothek explizit angegeben werden, also z. B. gcc sprache.c -lintl -o sprache“. ”
Anstatt die L¨andereinstellung 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 Ruckgabewert ¨ 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 fur ¨ 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¨aß LC MESSAGES zu ubersetzen. ¨ Naturlich ¨ handelt es sich ¨ hierbei nicht um echte Ubersetzungsfunktionen, vielmehr muss der Programmierer zuvor die ubersetzten ¨ 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¨achst 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 # include int main() { setlocale(LC_MESSAGES, ""); printf("%s\n", dgettext("grep", "out of memory")); return(0); }
¨ Nach dem Ubersetzen mit gcc sprache2.c -o sprache2“ kann das Pro” gramm mit verschiedenen Einstellungen fur ¨ 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´ emoire ´ epuis´ ee.
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 Ubersetzungsdateien 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 Ubersetzungdatei nicht, gibt die Funktion einen Zeiger auf die Meldung selbst (also auf msgid) zuruck. ¨ Daher sollte als msgid immer der entsprechende Text in Englisch verwendet werden, sodass bei einer nicht unterstutzten ¨ Sprache englische Meldungen erscheinen. ¨ Bei den Ubersetzungsdateien, den so genannten Message-Object-Dateien, handelt es sich um spezielle Bin¨ardateien, die fur ¨ gewohnlich ¨ unter /usr/share/locale/L¨anderkennung/LC MESSAGES/Textdomain.mo
stehen, also z.B.: /usr/share/locale/de/LC MESSAGES/grep.mo
Wird als L¨anderkennung de DE“ angegeben, sucht das System zun¨achst im ent” sprechenden Verzeichnis nach der betreffenden Message-Object-Datei. Bleibt die Suche ohne Erfolg, wird automatisch die L¨anderkennung auf de“ reduziert und ” erneut versucht, den entsprechenden Pfad zu offnen. ¨ Auf diese Weise l¨asst sich ¨ z.B. fur ¨ schweizerisches Deutsch de CH“ ein anderer Text einstellen, w¨ahrend fur ”¨ andere Dialekte (z.B. de AT“ fur ¨ Osterreich), fur ¨ die keine speziellen Meldungen ” vorgesehen sind, auf die Datei unter der ubergeordneten“ ¨ L¨anderkennung 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 Fur ¨ das Erstellen eigener Message-Object-Dateien sind die Pakete gettext“ und ” gettext-devel“ erforderlich. Stellen Sie sicher, dass diese Pakete installiert sind. ”
Als Beispiel fur ¨ die Erzeugung und Verwendung eigener Message-Object-Dateien soll das kleine Programm sprache3“ dienen, das abh¨angig 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 # include 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¨auterten 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 fur ¨ alle folgenden gettext()-Aufrufe. Mit Hilfe des Programms xgettext“ konnen ¨ 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 \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 konnen ¨ 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 Ubersetzung der Textmeldungen. Dazu mussen ¨ Sie hinter dem Schlusselwort ¨ msgstr in den Zeilen 21 und 25 jeweils ¨ die Ubersetzung der Texte in den vorangegangenen Zeilen eintragen, fur ¨ 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 zugehorigen ¨ MO-Dateien in die entsprechenden Verzeichnisse des Linux-Systems kopiert werden, also z. B. /usr/share/locale/L¨anderkennung/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 Moglichkeiten ¨ 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 Moglichkeit, ¨ 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¨ahig waren, stellten diese Sequenzen die einzige Moglichkeit ¨ dar, Textbereiche zu unterstreichen, in Fett oder Kursiv zu drucken oder die Schriftart zu wechseln. Tabelle 3.2 zeigt eine Auswahl moglicher ¨ ESC-Sequenzen, die jedoch nicht alle von s¨amtlichen Terminalprogrammen unterstutzt ¨ 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 fur ¨ 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 Grun ¨ Schriftfarbe Gelb Schriftfarbe Blau Schriftfarbe Violett Schriftfarbe Turkis ¨ Hintergrundfarbe Schwarz Hintergrundfarbe Rot Hintergrundfarbe Grun ¨ Hintergrundfarbe Gelb Hintergrundfarbe Blau Hintergrundfarbe Violett Hintergrundfarbe Turkis ¨ Cursor horizontal positionieren Cursor vertikal positionieren Signalton Tabulator (horizontal) Seitenvorschub (Terminal-Fenster loschen) ¨
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¨ache X11 l¨auft. Die ” im vorherigen Abschnitt beschriebenen ESC-Sequenzen reichen fur ¨ eine derartige Bildschirmsteuerung bei Weitem nicht aus. Fur ¨ 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 Einfugen ¨ oder Loschen ¨ von Zeichen innerhalb einer Zeile mit Verschieben der Zeichen rechts vom Cursor und das Abspeichern des gesamten Fensterinhaltes in eine Datei. Die vollst¨andige Behandlung der ncurses-Bibliothek wurde ¨ den Rahmen dieses Buches sprengen; ich mochte ¨ hier lediglich einen Einstieg in die Programmierung solcher Anwendungen geben. Die vollst¨andige 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 uber ¨ 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 ubersetzen, ¨ 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) geloscht, ¨ und oben links erscheint die aktuelle Cursor-Position ( 0, 1)“ invers dargestellt. Sie konnen ¨ den Cursor jetzt mit Hilfe der Cursor” Tasten beliebig uber ¨ das Fenster bewegen und an der aktuellen Position Text eingeben. Auch das Loschen ¨ von Zeichen mit Entf oder des gesamten Inhalts mit F1 ist moglich. ¨ Durch Drucken ¨ der Taste Ende wird das Programm beendet. Schauen wir uns zun¨achst das Hauptprogramm“ (Zeile 20 bis 64) an. Bevor ir” gendwelche Ein- oder Ausgaben uber ¨ Funktionen der ncurses-Bibliothek erfolgen konnen, ¨ muss das Ausgabefenster initialisiert werden. Dies geschieht mit der Funktion initscr() in Zeile 25, die bei erfolgreicher Ausfuhrung ¨ einen Zeiger auf die WINDOW-Struktur der Bibliothek zuruckliefert. ¨ 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¨achstes 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 gedruckt ¨ wird. Dabei wird mit der switch()-Funktion in Zeile 36 bis 58 gepruft, ¨ ob es sich bei der Eingabe um eine der vier Cursortasten oder eine der Sondertasten Entfernen, Loschen ¨ (Backspace), Einfugen, ¨ Pos1 oder Funktionstaste 1 handelt. Eine vollst¨andige Liste der von der ncurses-Bibliothek unterstutzten ¨ Sondertasten erh¨alt 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 zuruck. ¨ Es handelt sich bei dieser Funktion um ein Makro, daher kann der Inhalt der Variablen x und y ver¨andert werden, ohne dass ein Zeiger auf die Variablen ubergeben ¨ wird; es wird also kein vorangestelltes &“ benotigt. ¨ ”
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¨achst 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 ursprungliche ¨ Stelle verschoben (Zeile 16). Mit den Funktionen attron() attroff() (Zeile 13 und 15) konnen ¨ Textattribute ein- und ausgeschaltet werden. Tabelle 3.3 zeigt einige der moglichen ¨ 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 uber ¨ die ncurses-eigenen Funktionen erfolgen, also z.B. uber ¨ printw() und nicht etwa uber ¨ printf().
Farbige Textdarstellung mit der ncurses-Bibliothek Naturlich ¨ kann mit der ncurses-Bibliothek auch farbiger Text ausgegeben werden – sofern das Terminal oder die Console Farben unterstutzen. ¨ 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 # 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 offnen. ¨
r+
Datei zum Lesen und Schreiben offnen. ¨
w
Datei zum Schreiben offnen. ¨ Falls die Datei bereits existiert, wird ihr Inhalt geloscht. ¨
w+
Datei zum Lesen und Schreiben offnen. ¨ Falls die Datei noch nicht existiert, wird sie neu angelegt.
a
Datei zum Schreiben offnen. ¨ Der Dateizeiger steht am Ende der Datei. Der alte Inhalt geht nicht verloren.
a+
Datei zum Lesen und Schreiben offnen. ¨ 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 geoffnet, ¨ kann entsprechend der mode-Angabe auf diese Datei zugegriffen werden. Wird kein weiterer Zugriff benotigt, ¨ 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 fur ¨ 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 1 kein wahlfreier Zugriff (engl.: random access) auf die einzelnen Zeichen moglich. ¨ 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¨achste 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 Ruckgabewert ¨ 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 Ruckgabewert ¨ 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¨ager ist voll), wird auch hier die Konstante EOF zuruckgegeben. ¨ Bei fehlerfreier Ausfuhrung ¨ liefert die Funktion das geschriebene Zeichen zuruck. ¨ 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);
Naturlich ¨ mussen ¨ die Dateien zuvor – wie in Abschnitt 4.1.3 beschrieben – geoffnet ¨ werden. Das Erreichen des Dateiendes kann auch mit der Funktion feof(Stream) abgefragt werden, ein Zugriffsfehler l¨asst sich mit ferror(Stream) feststellen. Die Funktion ungetc() erlaubt es, ein mit fgetc() gelesenes Zeichen wieder in den Puffer zuruckzuschreiben, ¨ sodass es fur ¨ den n¨achsten Lesezugriff wieder bereitsteht: int ungetc(int c, FILE *stream); 1
Die Funktion fseek() ermoglicht ¨ 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 ubliche ¨ 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¨andert. ” ” Durch die Verwendung der Funktion ungetc() wird hier der Programmieraufwand deutlich verringert, da Sonderf¨alle wie \r\r\n“ oder ein \r“ am Datei” ” ende nicht gesondert berucksichtigt ¨ werden mussen. ¨ Durch ungetc() wird das auf \r“ folgende Zeichen – sofern es kein Zeilenvorschub ist – einfach fur ¨ den ” n¨achsten Durchlauf der while()-Schleife wieder zur Verfugung ¨ gestellt. Datens¨atze lesen/schreiben: fread() und fwrite() Ist die L¨ange 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);
¨ L¨angen- und Offset-Angaben verwenDer Typ size t wird im Allgemeinen fur det 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 Große ¨ eines Datensatzes (in Bytes) Anzahl der Datens¨atze Datei, aus der gelesen bzw. in die geschrieben wird
Als Ruckgabewert ¨ liefern beide Funktionen die Anzahl der gelesenen bzw. geschriebenen Datens¨atze (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 Zuruckspringen ¨ 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 Ruckgabewert ¨ von fseek() ist 0 bei fehlerfreier Ausfuhrung ¨ 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¨alen oder bei Pipes der Fall.
Weitere Funktionen Es gibt eine ganze Reihe weiterer Funktionen fur ¨ den Dateizugriff, die wir hier nicht alle behandeln konnen. ¨ Auch an dieser Stelle sei noch einmal auf die man“” Seiten hingewiesen. Einige der h¨aufig verwendeten Funktionen soll die folgende ¨ Ubersicht aufzeigen: fflush() Leert den Dateipuffer. Alle Zeichen im Schreibpuffer werden in die Datei (das physikalische Medium) geschrieben. Nur anzuwenden auf Dateien, die zum Schreiben geoffnet ¨ 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 konnen. ¨ 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 fugt ¨ am Anfang jeder Zeile eine Zeilennummer ein. Die Ausgabe der so erweiterten Dateien erfolgt entweder uber ¨ 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 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 uber ¨ eine Datei oder ein Verzeichnis einzuholen, z.B. die L¨ange 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¨alt folgende Eintr¨age:
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 zugehoriger ¨ Inode (Block mit Dateikopf) Zugriffsrechte Anzahl der Hard Links Benutzer-ID Gruppen-ID Typ des Devices (falls es ein Device ist) L¨ange in Bytes Blockgroße ¨ Anzahl der belegten Blocke ¨ Uhrzeit u. Datum des letzten Zugriffs Uhrzeit u. Datum des letzten Schreibzugriffs ¨ Uhrzeit u. Datum der letzten Anderung der Zugriffsrechte
Mit Hilfe der folgenden Makros l¨asst 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¨are“ 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 fur ¨ den als Argument angegebenen Pfadnamen die Informationen uber ¨ Namen, L¨ange, Typ, Zugriffsrechte, Besitzer der Datei (User ID) und ¨ Zeitpunkt der letzten Anderung. 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> <sys/stat.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¨age eines Verzeichnisses aufgelistet werden – a¨ hnlich wie beim Programm ls“ –, ist dies mit Hilfe der Funktionen ” DIR *opendir(char *name); int closedir(DIR *dir); struct dirent *readdir(DIR *dir);
¨ moglich. ¨ Ahnlich, wie Dateien vor dem Lesen geoffnet ¨ werden mussen, ¨ ist auch ¨ hier zun¨achst das Offnen des Verzeichnisses mittels opendir() erforderlich. Ruckgabewert ¨ 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() konnen ¨ dann nacheinander alle Eintr¨age des Verzeichnisses gelesen werden, wobei readdir() einen Zeiger auf eine Struktur vom Typ dirent liefert. Diese enth¨alt 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 zuruck. ¨
78
4 Dateien und Verzeichnisse
Das folgende Programm get dir“ verh¨alt sich wie ls -U1“, es zeigt alle Eintr¨age ” ” 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> <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. Fur ¨ den Anwender bedeutet dies, dass er mehrere Anwendungen gleichzeitig nutzen kann – z. B. eine Textverarbeitung, w¨ahrend ein MP3-Decoder Musik abspielt – sogar w¨ahrend andere Anwender weitere Programme auf dem gleichen System laufen lassen. Dem Programmierer eroffnet ¨ sich durch die Multi-Tasking-F¨ahigkeit die Moglich¨ keit, verschiedene Aufgaben eines Programms auf mehrere Prozesse aufzuteilen. Um diese Moglichkeit ¨ sinnvoll nutzen zu konnen, ¨ 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) konnen ¨ 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 verknupft. ¨ So liefert die C-Funktion getppid() die Prozess-ID des Eltern-Prozesses, und das Beenden eines Kind-Prozesses wird dem zugehorigen ¨ Eltern-Prozess durch ein Signal angezeigt. Jeder Prozess erh¨alt eine eindeutige Identifikationsnummer: die Prozess-ID. Jede ID wird nur einmal vergeben. Selbst wenn der zugehorige ¨ Prozess beendet wurde, wird dessen ID nicht neu belegt. Die Reihenfolge der IDs entspricht der Reihenfolge, in der die Prozesse erzeugt wurden: je junger“ ¨ ein Prozess, desto großer ¨ seine ” ID. Eine Liste aller existierenden Prozesse erh¨alt 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¨auft. ” Man erkennt daran, dass jedes aus einer Shell heraus gestartete Programm einen eigenen Prozess erh¨alt und nicht etwa in dem Shell-Prozess l¨auft.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¨at fur ¨ das eigene Programm zu nutzen. Beispiel: Sie berechnen mit Ihrem Programm eine Reihe von Zahlen, die Sie auch gern grafisch darstellen wurden. ¨ Sie konnen ¨ dann naturlich ¨ 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);
moglich. ¨ Die Funktion system() ubernimmt ¨ hier gleich mehrere Aufgaben: sie startet eine neue Shell /bin/sh (in einem eigenen Prozess) und l¨asst diese Shell das in command angegebene Kommando ausfuhren. ¨ Ferner wartet system() auf die Beendigung des Kind-Prozesses, sodass Ihr Programm erst danach fortgesetzt wird. Als Ruckgabewert ¨ liefert system() entweder 127 fur ¨ den Fall, dass die Shell nicht gestartet werden kann, oder den Ruckgabewert ¨ des ausgefuhrten ¨ Kommandos. Wurde das Kommando fehlerfrei ausgefuhrt, ¨ sollte system() den Wert 0 zuruck¨ geben. Das folgende Beispielprogramm zeigt die Verwendung der Funktion system(). Es stellt mit Hilfe des Programms gnuplot fur ¨ funf ¨ Sekunden eine Sinus-Kurve dar (dazu mussen ¨ Sie naturlich ¨ 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 gehort ¨ dieser Abschnitt gar nicht in das Kapitel Interprozesskommunikation, da die Funktionen der exec-Familie keine neuen Prozesse erzeugen. Sie werden jedoch h¨aufig in Verbindung mit fork() (s. u.) als Ersatz fur ¨ 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 fuhren ¨ – a¨ 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 auszufuhrende ¨ Programm l¨auft 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 Ausfuhrung ¨ einer exec()-Funktion nicht mehr in das ursprungliche ¨ Programm zuruck. ¨ Lediglich bei Auftreten eines Fehlers, weil z.B. das angegebene Programm nicht existiert, kehren die exec()Funktionen mit dem Ruckgabewert ¨ –1 zuruck. ¨ 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 ubergeben ¨ werden, d.h. fur ¨ jedes Argument eine Zeichenkette. Das erste Argument muss dabei den Programmnamen selbst enthalten! Ein v“ ” dagegen bedeutet, dass die Argumente als Vektor ubergeben ¨ werden, und zwar
82
5 Interprozesskommunikation
in der Form, wie sie auch die Funktion main() erh¨alt (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¨ahrend die Funktionen ohne ein p“ in der Endung ” den vollst¨andigen Pfad des auszufuhrenden ¨ Programmes benotigen ¨ (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¨atzlich zu den Argumenten fur ¨ das aufzurufende Programm einen Vektor mit Umgebungsvariablen benotigen, ¨ 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 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 Ruckgabewert ¨ die Prozess-ID des neu erzeugten Kind-Prozesses. In dem Kind-Prozess selbst liefert die Funktion eine 0 als Ruckgabewert; ¨ konnte kein Kind-Prozess erzeugt werden, ist der Ruckgabewert ¨ –1:
5.2 Neue Prozesse starten
83
# include <stdio.h> # include 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¨alt eine neue Prozess-ID, benutzt jedoch zun¨achst den gleichen Speicher wie der Eltern-Prozess. Erst wenn einer der beiden Prozesse in den Speicher schreibt, also z.B. eine Variable ver¨andert, wird der entsprechende Speicherbereich dupliziert und fur ¨ beide Prozesse getrennt weitergefuhrt. ¨ 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 mussen. ¨ Nach Ausfuhrung ¨ der Funktion fork() existieren also zwei Prozesse, die sich zun¨achst allein durch die Prozess-ID unterscheiden – und durch den Ruckgabe¨ wert von fork(). Bei jedem Schreib-Zugriff wird jedoch der Unterschied beider Prozesse groß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 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 ubersetzen ¨ und ausfuhren, ¨ 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 ursprunglich ¨ 1 auf 2 ver¨andert 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¨achtnis 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 fur ¨ beide Prozesse konstant; durch den Schreibzugriff wird jedoch die ¨ entsprechende Seite des virtuellen Speichers kopiert und die Anderung nur in der Kopie durchgefuhrt. ¨ Anhand dieses Beispielprogramms l¨asst 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¨ag 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¨andert. 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¨achst 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“ verknupft ¨ 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¨alt die Ausgabedatei zun¨achst alle Ausgaben des KindProzesses (der aufgrund der kurzeren ¨ Wartezeit bei der Funktion sleep() in Zeile 17 immer vor dem Eltern-Prozess beendet wird), gefolgt von allen Ausgaben des Eltern-Prozesses – beide jeweils angefuhrt ¨ 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¨ahig sind, ist es ublich, ¨ mit Hilfe von Warteschleifen auf bestimmte Ereignisse – etwa das Drucken ¨ einer Taste – zu warten. Unter Linux sollte man jedoch nach Moglichkeit ¨ auf solche Warteschleifen verzichten und stattdessen den Ablauf des Programms mit Hilfe von Signalen (siehe n¨achsten Abschnitt) steuern. Jede Warteschleife kostet Rechenzeit, die sonst fur ¨ andere Prozesse zur Verfugung ¨ stehen wurde. ¨ Soll ein Programm nur eine bestimmte Zeit lang warten, ohne dies von einem Ereignis abh¨angig zu machen, konnen ¨ 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 fur ¨ µ und damit ”
86
5 Interprozesskommunikation
fur ¨ 10 −6 ) ubergeben. ¨ Beachten Sie jedoch, dass das Eintreffen eines nicht geblockten Signals (siehe Abschnitt 5.3) diese Funktionen ungeachtet der verbleibenden Wartezeit sofort beendet. L¨asst 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 Ausfuhrung ¨ des Programms und stellt den Prozess an das Ende der Liste der auszufuhrenden ¨ 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 # include # 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 fur ¨ die Ablaufsteuerung von Prozessen und fur ¨ 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 Ubersicht uber ¨ die wichtigsten Signale:
1
Zum Ausfuhren ¨ des Programms mussen ¨ Sie uber ¨ Lesezugriff (r) auf das Device /dev/cdrom“ ” verfugen. ¨
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 fur ¨ 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¨aufig erforderlich, eine maximale Wartezeit zu realisieren, fur ¨ 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 Ruckgabe¨ wert 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 fuhrt ¨ das Signal SIGALRM zum Abbruch des Programms (Prozesses) mit Ausgabe der Meldung Der Wecker klingelt“ (bzw. Alarm clock“) ” ” und einem Ruckgabewert ¨ ungleich 0. Erg¨anzt man das Beispiel-Programm aus Abschnitt 5.2.4 um die Funktion alarm(), kann damit ein Timeout fur ¨ 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 # include # 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¨asst sich die Reaktion eines Prozesses auf ein bestimmtes Signal modifizieren:1 signal(int signum, void *handler());
Diese Funktion richtet fur ¨ 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 fur ¨ ignore) bewirkt, dass das Signal blockiert wird, also keine Auswirkung zeigt, w¨ahrend SIG DFL (DFL steht fur ¨ default) den fur ¨ das entsprechende Signal voreingestellten Signal-Handler einrichtet. Die Funktion signal() liefert als Ruckgabewert ¨ den zuvor mit diesem Signal verknupften ¨ 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 ubergeben. ¨ Damit besteht die Moglichkeit, ¨ den gleichen Signal-Handler fur ¨ verschiedene Signale zu benutzen und dann anhand des Parameters festzustellen, welches Signal tats¨achlich gesetzt wurde. Im folgenden Programmbeispiel wurde der Quelltext aus dem vorangegangenen Abschnitt um das Einrichten eines Signal-Handlers fur ¨ das Signal SIGALRM erg¨anzt: 1 2 3 4 5 6 1
/* cdwait3.c - Auf das Einlegen einer CD-ROM warten. */ # include <stdio.h> # include Die Deklaration der Funktion signal() ist hier etwas vereinfacht dargestellt, die exakte Deklaration erh¨alt 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> <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 Ruckgabewert ¨ 1 ab.
5.3.3 Auf die Beendigung eines Kind-Prozesses warten H¨aufig 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 ausgefuhrt ¨ wird und der Eltern-Prozess erst danach fortfahren soll. Zu diesem Zweck stehen die Funktionen wait() und waitpid() zur Verfugung: ¨ 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 Ruckgabewert ¨ 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 gepruft ¨ wird, ob der angegebene Prozess beendet wurde, ohne jedoch auf dessen Beendigung zu warten. Der Ruckgabewert ¨ 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> <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¨atzlich lassen sich aber nur Signale an einen Prozess des gleichen Benutzers schicken, es sei denn, das Programm l¨auft 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 zuruck, ¨ 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 moglich, ¨ einen anderen Prozess vorzeitig zu beenden.
5.4 Datenaustausch zwischen Prozessen Wie wir bereits in Abschnitt 5.2.3 gesehen haben, konnen ¨ Eltern- und KindProzess nicht ohne weiteres uber ¨ Variablen Informationen austauschen – sobald einer der Prozesse in den zun¨achst gemeinsamen Speicher schreibt, wird dieser dupliziert und ist fortan fur ¨ beide Prozesse getrennt. Daher bedarf es anderer Verfahren, um einen Datenaustausch zwischen Prozessen zu realisieren, der uber ¨ das Setzen von Signalen hinausgeht. Linux bietet hierfur ¨ drei verschiedene Moglichkeiten ¨ an: Pipes, FIFOs und Shared Memory. Diese werden in den folgenden Abschnitten vorgestellt und erl¨autert.
5.4.1 Pipes Eine Pipe ist eine Art virtuelle Datei“, dargestellt uber ¨ 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) benotigt, ¨ in das die Funktion pipe() die erzeugten Deskriptoren schreibt. pipe() liefert den Ruckgabewert ¨ 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 # 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¨achst eine Pipe erzeugt; die zugehorigen ¨ 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 benotigt ¨ 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 benotigte ¨ Deskriptor fd[0] geschlossen (Zeile 34). In Zeile 23 wird jetzt innerhalb des Kind-Prozesses aus der Pipe gelesen. Die Funktion read() verh¨alt sich a¨ hnlich wie fread(), benotigt ¨ 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¨achst 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). Fur ¨ eine bidirektionale Kommunikation zwischen zwei Prozessen mussen ¨ zwei Pipes geoffnet ¨ 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¨aufig die Ein” und Ausgaben dieses Programms uber ¨ den Eltern-Prozess laufen. Dazu gibt es die Moglichkeit, ¨ 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 # 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 benotigt, ¨ da diese jetzt als Standardeinund -ausgabe zur Verfugung ¨ 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¨ahrt.
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 wurde ¨ 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¨achst nur in den Dateipuffer geschrieben und sind somit fur ¨ den Eltern-Prozess noch nicht verfugbar. ¨ Erst wenn der Puffer (8192 Bytes) voll ist oder der Kind-Prozess beendet wird, werden die Ausgaben tats¨achlich in die Pipe geschrieben.
5.4.2 FIFOs Als Alternative zu Pipes konnen ¨ 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 ublichen ¨ Funktionen fur ¨ Dateizugriffe angesprochen werden konnen. ¨ 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 konnen, ¨ die auch fur ¨ 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 # 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¨aren Datei besteht und welchen Vorteil FIFOs fur ¨ die Prozesskommunikation bringen. Ein wesenlicher Unterschied besteht darin, dass eine FIFO-Datei kein Ende“ hat. Liest man ein Zeichen aus einer leeren, ” regul¨aren Datei (z.B. mit fgetc()), erh¨alt 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 offnet. ¨ Da-
5.4 Datenaustausch zwischen Prozessen
97
durch lassen sich zwei Prozesse mit Hilfe von FIFO-Dateien synchronisieren, was mit regul¨aren Dateien nur schwer moglich ¨ ist.
5.4.3 Shared Memory Pipes und FIFOs bieten die Moglichkeit, ¨ einen sequenziellen Datenstrom zwischen zwei Prozessen auszutauschen. Wird dagegen der wahlfreie Zugriff “ (random ” access) eines Prozesses auf Datenstrukturen eines anderen Prozesses benotigt, ¨ 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 losen ¨ 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 Schlusselwort ¨ IPC PRIVATE angegeben werden, um ” shmget() zu veranlassen, neuen Speicher zu reservieren. Der Parameter size gibt die Große ¨ des zu reservierenden Segments an, wobei shmget() immer auf die n¨achsten 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 fur ¨ das reservierte Shared-Memory-Segment oder eine –1, falls das Segment nicht reserviert werden konnte. Achtung: Tats¨achlich 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 uber ¨ 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. Uber shmaddr kann hier eine feste Adresse vorgegeben werden. In der Regel sollte dieser Parameter jedoch 0 sein, wodurch shmat() veranlasst wird, den n¨achsten freien Speicherblock zu reservieren. Als shmflg kann man entweder eine 0 fur ¨
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 Ruckgabewert ¨ erh¨alt 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 Große ¨ PAGE SIZE (4 kByte) zur Verfugung, ¨ den auch andere Prozesse nutzen konnen. ¨ 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 aufgefuhrten ¨ Programmausschnitt den Funktionsaufruf shmctl() bemerkt. Diese Funktion erlaubt es, mit dem SharedMemory-Segment verschiedene Aktionen auszufuhren ¨ – eine davon ist IPC RMID, mit der das Segment entfernt wird. Damit wird verhindert, dass die dem Segment zugeordneten Datenstrukturen als Speicherleichen“ zuruckbleiben. ¨ ” 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 loschen“ ¨ markiert. Das ” tats¨achliche Loschen ¨ des Shared-Memory-Segments und die Freigabe des Speichers erfolgen erst, wenn alle Prozesse, die das Segment anbanden, dieses wieder gelost ¨ haben!
5.4 Datenaustausch zwischen Prozessen
99
Das Losen ¨ 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 benotigt, ¨ 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> <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 ausgefuhrt, ¨ werden alle an einen Prozess angebundenen Shared-MemorySegmente beim Beenden des Prozesses automatisch gelost. ¨
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 Moglichkeiten, ¨ einen neuen Prozess zu erzeugen. Diese alternativen Verfahren vereinfachen dabei die Kommunikation zwischen Eltern- und Kind-Prozess.
5.5.1 popen() und pclose() Ganz a¨ hnlich wie die in Abschnitt 5.2.1 vorgestellte Funktion system() fuhrt ¨ auch die Funktion popen() das als Zeichenkette ubergebene ¨ Kommando in einer Shell aus: FILE *popen(const char *command, const char *type);
Gleichzeitig offnet ¨ diese Funktion aber auch eine Pipe und gibt diese als Zeiger auf einen Stream (Achtung: gepufferte Ein-/Ausgabe!) zuruck. ¨ Mit dem Parameter type l¨asst 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 zuruck, ¨ sodass Eltern- und Kind-Prozess parallel laufen. Die Funktion pclose() wartet auf die Beendigung des Kind-Prozesses und liefert dessen Ruckgabewert ¨ (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 a¨ hnlich wie fork(), erlaubt jedoch, dass sich Eltern- und Kind-Prozess den Speicher teilen. Dadurch konnen ¨ beide Prozesse beispielsweise uber ¨ 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 auszufuhrende ¨ Funktion. Diese Funktion muss vom Typ int sein und kann maximal einen Parameter besitzen. Der zweite Parameter von clone() gibt die Adresse des Stacks fur ¨ den Kind-Prozess an; weil der Stack bei PCs von oben nach unten gefullt ¨ 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¨alt 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 auszufuhrende ¨ Funktion ubergeben ¨ wird. Als Ruckgabewert ¨ 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¨andert und so eine Information an den Eltern-Prozess ubertr¨ ¨ agt: 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 ubersetzen ¨ 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 fur ¨ den Stack des KindProzesses mit 10.000 Bytes angelegt. Die im Kind-Prozess ausgefuhrte ¨ Funktion ist in den Zeilen 15 bis 20 definiert. Sie erweitert den Inhalt der Variablen buffer um die als Parameter ubergebene ¨ 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¨ahnt, ist clone() eine Linux-spezifische Funktion. Wenn Sie ein Programm portabel, d. h. auf andere Unix-Systeme ubertragbar ¨ halten wollen, sollten Sie auf die Verwendung von clone() verzichten.
5.5.3 POSIX-Threads Eine weitere Moglichkeit, ¨ Multi-Thread-Programme zu erstellen, besteht in der Verwendung der Funktionsbibliothek libpthread“. Diese Bibliothek stellt nicht ” nur Funktionen fur ¨ die Erzeugung von Prozessen, sondern auch fur ¨ eine komfortable Interprozesskommunikation bereit. Die Abkurzung ¨ pthread“ steht fur ¨ POSIX Threads“; aber was ist POSIX“ ei” ” ” gentlich? Es handelt sich dabei ebenfalls um eine Abkurzung, ¨ und zwar fur ¨ 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 fur ¨ Dateizugriff, Prozesse und Ein-/Ausgabe, sondern auch Shell-Befehle und die Arbeitsweise der Shell selbst. Es wurde ¨ den Rahmen dieses Buches sprengen, den POSIX-Standard fur ¨ die Interprozesskommunikation, so wie er in der libpthread implementiert ist, vollst¨andig 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 ¨ den neu erzeugTyp pthread t. In dieser Struktur werden Informationen uber ten Prozess abgelegt. Der zweite Parameter zeigt auf eine Variable vom Typ ¨ Eigenschaften des neu zu erzeugenden pthread attr t, in der die gewunschten Prozesses eingetragen werden konnen. ¨ Wird statt eines Zeigers der Wert NULL ubergeben, ¨ verwendet pthread create() die Standard-Eigenschaften fur ¨ neue Threads. ¨ Ahnlich wie bei clone() wird auch bei pthread create() ein Zeiger auf die vom Kind-Prozess auszufuhrende ¨ Funktion ubergeben ¨ – hier als dritter Parameter – sowie ein Zeiger auf eine Variable, die dieser Funktion als Parameter ubergeben ¨ werden soll. Die im Kind-Prozess auszufuhrende ¨ Funktion sollte mit pthread exit() beendet werden: void pthread_exit(void *return_val);
Als Parameter wird bei pthread exit() ein Zeiger auf den Ruckgabewert ¨ uber¨ geben. Dieser Ruckgabewert ¨ konnte ¨ 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 Ruckgabewert ¨ 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 Abkurzung ¨ POSIX sollte wahrscheinlich die N¨ahe zu Unix verdeutlichen. ”
5.5 Alternative Verfahren zur Erzeugung von Prozessen
105
Diese Funktion wartet, bis der zum Parameter thread gehorende ¨ Prozess been¨ des det wurde, und tr¨agt dann in *return val den Zeiger auf den Ruckgabewert 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 fur ¨ 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 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¨arken von Linux (und anderen Unix-Varianten) sind die Devices. Bei welchem anderen Betriebssystem konnen ¨ 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¨aten 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¨aufig als Ger¨ate“ bezeichnet – verfugbar. ¨ Diese besitzen einen ” Eintrag im Dateisystem, fur ¨ gewohnlich ¨ im Verzeichnis /dev“. Die erste IDE” Festplatte ist beispielsweise mit /dev/hda1“ verknupft. ¨ Die mit den Devices ” verknupften ¨ Eintr¨age 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 fur ¨ Keyboards und andere elektronische Musikinstrumente
2
cat /dev/sequencer > midi-file“ (Abbruch der Aufzeichnung mit Ctrl-C) ” Fur ¨ (E)IDE-Festplatten: dd if=/dev/hda of=mbr bs=512 count=1“ ”
3
108
6 Devices – das Tor zur Hardware
¨ Uber zwei Ger¨atenummern, der major“ und der minor device number“, sind diese ” ” Inodes mit dem entsprechenden Treiber verknupft; ¨ die Nummern konnen ¨ 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
Fur ¨ Ger¨atetreiber, die als Kernel-Modul ausgefuhrt ¨ sind, ist die Verknup¨ fung 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“ verknupft. ¨ ” Aufgrund der wachsenden Zahl von Hot-Plug-and-Play“-Ger¨aten1 wie USB” Festplatten und USB-Kameras musste dieses starre“ Device-Konzept von Linux ” in den letzten Jahren um ein dynamisches Ger¨atemanagement erweitert werden. Dies ubernimmt ¨ udev, genauer gesagt der D¨amon udevd. Siehe hierzu auch man udev“. Doch fur ¨ die in diesem Kapitel vorgestellten Methoden und Beispiel” programme spielt es keine Rolle, ob das jeweilige Ger¨at fest im System verankert ist (wie ein internes CD-ROM-Laufwerk) oder dynamisch eingebunden wird.
6.1.1 Devices offnen ¨ und schließen Der Vorteil, der sich durch die Integration der Devices in das Dateisystem ergibt, liegt auf der Hand: Man kann mit g¨angigen Dateioperationen auf HardwareKomponenten zugreifen. So ist es z. B. moglich, ¨ 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¨asst sich diese Datei wieder abspielen: cat test.au > /dev/audio“. Manche Ger¨atetreiber stellen ein zus¨atzli” ches Device zur Verfugung, ¨ das Informationen uber ¨ die Hardware und den Treiber liefert, Beispiel: cat /dev/sndstat“. ” Fur ¨ den Programmierer bedeutet dies, dass er mit Hilfe der ublichen ¨ Funktionen fur ¨ den Dateizugriff auch die Devices ansprechen kann. Dies gilt naturlich ¨ auch ¨ fur ¨ das Offnen und Schließen eines Devices, das mit den Befehlen fopen() bzw. fclose() fur ¨ gepufferte Ein-/Ausgabe moglich ¨ ist (vgl. Kapitel 4). H¨aufiger verwendet man fur ¨ den Zugriff auf Devices jedoch die entsprechenden Befehle fur ¨ ungepufferte Ein-/Ausgaben: int open(char *pathname, int flags); int close(int fd); 1
Ger¨ate, die bei laufendem Betrieb angeschlossen werden konnen. ¨
6.1 Das Device-Konzept von Linux
109
Die Funktion open() benotigt ¨ als ersten Parameter den zu offnenden ¨ 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 Ruckgabewert ¨ liefert open() entweder den neuen Dateideskriptor, also eine positive, ganze Zahl, oder –1, falls die angegebene Datei nicht geoffnet ¨ werden konnte. Die Funktion close() schließt die Datei, deren Deskriptor als Parameter angegeben ist. W¨ahrend 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¨aten wie Soundkarte oder WebCam, bei denen die Daten nur mit einer bestimmten Datenrate eintreffen“, wurde ¨ das Fullen ¨ des Puffers mit ei” ner Wartezeit verbunden sein. Solche Devices werden in der Regel durch character special files repr¨asentiert – im Gegensatz zu den block special files, wie etwa fur ¨ das Device einer Festplatte.
6.1.2 Ungepuffertes Lesen und Schreiben Im Gegensatz zur gepufferten Ein-/Ausgabe, fur ¨ die es zahlreiche Schreib/Lesefunktionen unterschiedlicher Komplexit¨at gibt, stehen fur ¨ die ungepufferte Ein- und Ausgabe nur je zwei Funktionen zur Verfugung: ¨ 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 Ruckgabewert ¨ liefern beide Funktionen die Anzahl der tats¨achlich 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¨achlich geschriebenen Bytes zuruck ¨ 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¨achlich gelesenen Zeichen h¨aufig 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 verfugbar ¨ 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 fur ¨ 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 uber ¨ die Schreib-/Lesefunktionen dennoch nur ein kleiner Teil der Moglichkeiten ¨ 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 Abkurzung ¨ steht fur ¨ Input-Output-Control): int ioctl(int fd, int request, ...);
Auch diese Funktion benotigt ¨ 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 benotigt. ¨ Die moglichen ¨ Kommandos h¨angen von dem jeweiligen Device ab, eine (unvollst¨andige) Liste der Kommandos erhalten Sie mit man ioctl list
Im Fehlerfall liefert die Funktion ioctl() eine –1, sonst ist der Ruckgabewert ¨ 0.1 Hinweis: Wenn Sie die Funktion ioctl() auf ein mit fopen() geoffnetes ¨ Device anwenden wollen, konnen ¨ Sie den Dateideskriptor mit der Funktion fileno() erfragen: 1
Einige Kommandos nutzen den Ruckgabewert ¨ 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¨asst sich uber ¨ das Device /dev/cdrom“ ansprechen. ” Dabei handelt es sich um einen symbolischen Link auf das tats¨achliche Device, z.B. /dev/hdc“. Bevor Sie auf das CD-ROM-Laufwerk zugreifen konnen, ¨ ” mussen ¨ Sie sicherstellen, dass die Zugriffsrechte entsprechend gesetzt sind! Dazu konnen ¨ Sie das Device (nicht den Link!) entweder fur ¨ 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 ” ” naturlich ¨ auch Zugriff auf das CD-ROM-Laufwerk, doch rate ich davon dringend ab. Die fur ¨ das CD-ROM-Laufwerk zul¨assigen ioctl()-Kommandos sind in der Include-Datei /usr/include/linux/cdrom.h“ aufgelistet. ”
6.2.1 Die CD auswerfen“ ” Als erstes Anwendungsbeispiel fur ¨ 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> <sys/ioctl.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¨achst die Zeile 15, in der das Device geoffnet ¨ wird. Als Flags“ ” ist hier die Oder-Verknupfung ¨ aus O RDONLY und O NONBLOCK angegeben. Letzteres bewirkt, dass die Funktion open() auch ausgefuhrt ¨ werden kann, wenn keine CD im Laufwerk liegt. ¨ Nach erfolgreichem Offnen des Devices wird in Zeile 22 mit Hilfe der Funktion ioctl() das Kommando CDROMEJECT an das Device geschickt. Dieses Kommando benotigt ¨ 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¨amon udevd (siehe oben) in die Quere kommt. Auf manchen Linux-Systemen wird eine CD nach dem Einlegen automatisch durch diesen D¨amon in das Dateisystem eingebunden und das Auswerfen der CD gesperrt. Testen Sie das Programm daher zun¨achst ohne CD im Laufwerk.
6.2.2 F¨ahigkeiten des Laufwerks auslesen Nicht alle CD-ROM-Laufwerke bieten die Moglichkeit, ¨ das Laufwerk selbstt¨atig 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¨ahigkeiten des Devices abfragen konnen. ¨ 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 7 # include <sys/ioctl.h> 8 # include
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 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 Ruckgabewert ¨ der Funktion ioctl() in Zeile 22 enth¨alt bei erfolgreicher Ausfuhrung ¨ des Kommandos CDROM GET CAPABILITY die F¨ahigkeiten des Devices, wobei jede F¨ahigkeit durch ein bestimmtes Bit repr¨asentiert wird. Eine Liste der moglichen ¨ Features erh¨alt man mit (Benutzereingaben sind schr¨ag 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¨asst 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 benotigen ¨ Sie entweder eine Soundkarte, die mit dem CD-Laufwerk verbunden ist, oder ein Laufwerk mit einem Kopfhorerausgang. ¨ Bei Verwendung der Soundkarte sollten Sie zudem ein Programm zum Einstellen der Audioquellen und der Lautst¨arke installiert haben, beispielsweise aumix“. ” Das Inhaltsverzeichnis einer Audio-CD Bevor wir jedoch zum Abspielen einer CD kommen, soll zun¨achst gezeigt werden, wie das Inhaltsverzeichnis“ einer Audio-CD gelesen wird und in welcher Weise ” die Positionen der einzelnen Stucke ¨ (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> <errno.h> <sys/ioctl.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 <stdlib.h> <sys/ioctl.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 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 Offnen des Devices in Zeile 24 fragt das Programm in den Zeilen 28 bis 37 die MSF-Adresse des ersten Stucks ¨ ab und tr¨agt 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¨asst sich das Abspielen vorzeitig beenden. Weitere Moglichkeiten ¨ Ein echter“ CD-Player bietet neben den Funktionen play“ und stop“ eine pau” ” ” ” se“-Funktion fur ¨ eine (kurze) Unterbrechung. Auch das Linux-Device bietet diese Moglichkeit ¨ mit dem ioctl()-Kommando CDROMPAUSE, das keinen weiteren Parameter benotigt. ¨ Das Abspielen l¨asst 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 Ausfuhrung ¨ des Kommandos CDROMSUBCHNL enth¨alt die Struktur unter anderem Informationen uber ¨ den Audio-Status, die Nummer des (laufenden) Stucks ¨ 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> <sys/ioctl.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 Moglichkeit, ¨ bei Wiedergabe einer AudioCD die Lautst¨arke 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 benotigen ¨ 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 Ausfuhrung ¨ von CDROMVOLREAD enth¨alt diese Struktur die aktuell eingestellten Werte fur ¨ alle vier (!) Kan¨ale – Kanal 2 und Kanal 3 sind in der Regel nicht belegt und deren Lautst¨arke ist dementsprechend 0. Mit dem Kommando CDROMVOLCTRL werden die in der Struktur eingetragenen Lautst¨arkeWerte (zwischen 0 und 255) fur ¨ die entsprechenden Kan¨ale eingestellt. Weitere Informationen zum Ansteuern eines CD-ROM-Laufwerks unter Linux finden Sie in [8].
6.3 Ansteuerung einer Soundkarte Eine Soundkarte bietet vielf¨altige Moglichkeiten: ¨ 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 Verfugung ¨ – beispielsweise /dev/sequencer, /dev/snd/pcm* oder /dev/sndstat. Die folgenden Abschnitte beschr¨anken sich jedoch auf die ge” br¨auchlichsten“ Devices der Soundkarte. Bei Verwendung mehrerer Soundkarten, beispielsweise wenn zus¨atzlich eine TVKarte oder eine WebCam mit eingebautem Mikrofon angeschlossen ist, werden fur ¨ 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 fur ¨ 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 ursprunglichen ¨ Mechanismus fur ¨ 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 konnen. ¨ Dies ist beispielsweise erforderlich, wenn w¨ahrend der Wiedergabe von MP3-Musik das Betriebssystem Signaltone ¨ ausgibt. Um diese Einschr¨ankung aufzuheben, wurden inzwischen neue, leistungsf¨ahigere Systeme wie ALSA (Advanced Linux Sound Architecture) und ESounD (Enlightened Sound Daemon) eingefuhrt. ¨ Aus Kompatibilit¨atsgrunden ¨ stellen die aktuellen Soundtreiber aber nach wie vor die Devices des OSS zur Verfugung: ¨ die einfachste Moglichkeit ¨ fur ¨ 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 fur ¨ die Aufnahme und Wiedergabe zu mischen. Fur ¨ jeden Kanal des Mixers kann indivi¨ duell eine Lautst¨arke eingestellt werden. Uber den Mixer wird auch die Aufnahmequelle ausgew¨ahlt. Der Mixer l¨asst sich uber ¨ das Device /dev/mixer ansteuern, die wichtigsten Definitionen fur ¨ dieses Device enth¨alt die Datei /usr/include/linux/soundcard.h
Jedem moglichen ¨ Kanal des Mixers ist eine Indexnummer zugeordnet, die Kon¨ Kan¨ale an (gestante SOUND MIXER NRDEVICES gibt die Anzahl der moglichen genw¨artig sind dies 25). Hier eine Liste der Mixer-Kan¨ale mit den zugehorigen ¨ 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¨arke des Mixerausgangs Tiefen“ des Mixerausgangs ” Hohen“ ¨ des Mixerausgangs ” Synthesizer (Ton-Generator) Digital-Analog-Wandler PC-Lautsprecher Line-In“-Buchse ” Mikrofon CD-ROM-Laufwerk
Aufnahmepegel Eingangsverst¨arkung Ausgangsverst¨arkung weitere Analogeing¨ange
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¨ange
z. B. von einer TV-Karte
Die aktuelle Einstellung eines dieser Kan¨ale kann mit dem ioctl()-Kommando MIXER READ() abgefragt und mit MIXER WRITE() ver¨andert werden. Beide Kommandos benotigen ¨ als dritten Parameter des ioctl()-Aufrufs den Zeiger auf eine Integer-Variable. Das unterste Byte dieser Variablen enth¨alt die Lautst¨arke fur ¨ den linken Stereo-Kanal, das n¨achsthohere ¨ Byte repr¨asentiert die Lautst¨arke fur ¨ den rechten Stereo-Kanal. Beide Werte mussen ¨ sich von 0 bis 100 bewegen. Das folgende Programm fragt die aktuelle Einstellung der Gesamtlautst¨arke 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> <sys/ioctl.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 <sys/ioctl.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 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¨achst Audioformat und Samplingrate (Abtastfrequenz) eingestellt werden. Des Weiteren sollte festgelegt werden, ob die Aufnahme/Wiedergabe in Stereo oder Mono erfolgen soll. Fur ¨ 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 gewunschte ¨ Audioquelle gew¨ahlt werden (siehe Seite 125). Bei Stereo-Aufnahmen liefert das Device die Samples beider Kan¨ale im Wechsel, angefangen mit dem linken Kanal. Das folgende Programm zeichnet eine Audio-Sequenz mit einer L¨ange 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> <sys/ioctl.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 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 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 Offnen des Devices – hier mit O RDWR, um schreiben und lesen zu konnen ¨ – 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 Drucken ¨ der Taste RETURN, bevor es die aufgezeichneten Daten wieder abspielt. Die g¨angigsten Samplingraten fur ¨ Audiosignale, die von den meisten Soundkarten unterstutzt ¨ 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 uber ¨ das Format und die Samplingrate. H¨aufig 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 ubergeschwappt. ¨ Es gibt mittlerweile Kernel-Module, die Video-Quellen wie z. B. eine TV-Karte oder eine WebCam unterstutzen. ¨ Diese Treiber sind unter dem Begriff Video for Linux“ ” – oder kurz Video4Linux“ – zusammengefasst. In diesem Abschnitt sollen das ” zugehorige ¨ Device vorgestellt und die Aufnahme von Bildern mit diesem Device erl¨autert werden. Die Beispielprogramme sind ausgelegt fur ¨ eine WebCam, lassen sich aber auch auf eine TV-Karte anwenden. Von Haus aus unterstutzen ¨ 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 unterstutzt. ¨ Dieses Modul ist als Quelltext fur ¨ 2.4er und 2.6er Kernels verfugbar, ¨ 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 uber ¨ 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 fur ¨ die Programmierung relevanten Definitionen und Deklarationen finden Sie in der Include-Datei /usr/include/linux/videodev.h“. ” Die Bandbreite der unterstutzten ¨ Videoquellen des Devices ist groß, dementsprechend unterschiedlich sind auch Funktionen und Moglichkeiten. ¨ Daher bietet das Device das ioctl()-Kommando VIDIOCGCAP (VIDeo IO-Control Get CAPabilities)
6.4 Video for Linux“ ”
131
an, mit dem die F¨ahigkeiten und Eigenschaften abgefragt werden konnen. ¨ 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¨alt man eine logische Oder-Verknupfung ¨ aus den Eigenschaf” ten des angeschlossenen Ger¨ates – z. B. VID TYPE MONOCHROME, wenn nur Schwarz/Weiß unterstutzt ¨ wird, oder VID TYPE TUNER, falls das Ger¨at einen Empf¨anger besitzt, dessen Frequenz via Software eingestellt werden kann. Die vollst¨andige Liste der moglichen ¨ Eigenschaften (mit Beschreibung) erh¨alt man mit grep VID_TYPE_ /usr/include/linux/videodev.h
Neben den m¨oglichen Einstellungen fur ¨ das Bildformat lassen sich auch die aktuell gew¨ahlten 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 uber ¨ das angeschlossene Ger¨at 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> <sys/ioctl.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); }
Fur ¨ 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 Moglichkeit ¨ 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 Groß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> <sys/ioctl.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 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. ¨ Ublicherweise liefern WebCams voreingestellt 24-Bit-Farbbilder in RGB-Format1 , je Pixel werden also drei Bytes fur ¨ 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, ubertragen ¨ werden (little endian Codierung). Falls Rot und Blau vertauscht erscheinen, konnen ¨ Sie in Zeile 12 uber ¨ die define-Anweisung dafur ¨ sorgen, dass die Farben zuruckgetauscht ¨ werden. (Bei dem o. g. Kernel-Modul spca5xx l¨asst sich uber ¨ die Option2 force rgb=1“ auch die Reihenfolge der ” Farbwerte korrigieren.) ¨ Nach dem Offnen des Devices (Zeile 21) fragt das Programm zun¨achst 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 fur ¨ Red-Green-Blue bzw. Rot-Grun-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 zuruckschreibt. 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 naturlich ¨ auch die Moglichkeit, ¨ vor der Bildaufzeichnung die Einstellungen fur ¨ Helligkeit, Kontrast, Farbs¨attigung, 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 konnten ¨ Sie dazu ab Zeile 48 den folgenden Quelltext einfugen: ¨ { 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¨andigere Methode fur ¨ 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 –
gewunschte ¨ Zieladresse oder NULL (→ keine Vorgabe) Große ¨ des Speicherbereichs
6.4 Video for Linux“ ”
prot
–
flags
–
fd – offset –
137
Zugriffsrechte, Oder-Verknupfung ¨ aus den Bits PROT READ (Lesen) PROT WRITE (Schreiben) und PROT EXEC (Ausfuhren) ¨ Eigenschaften des Speichersegments, z. B. MAP PRIVATE oder MAP SHARED (Zugriff auch fur ¨ andere Prozesse) Datei-Deskriptor des Devices zu uberspringende ¨ Bytes des Devices
Als Ruckgabewert ¨ liefert mmap() die Speicheradresse, wohin die Daten geschrieben werden, oder MAP FAILED im Fehlerfall. Die Adresse und die Große ¨ mussen ¨ der Funktion munmap() als Parameter ubergeben ¨ werden, um das Speichersegment wieder freizugeben. Mit mmap() l¨asst sich der Bildspeicher (Frame Buffer) des Video-Devices in ein Speichersegment abbilden. Voraussetzung ist allerdings, dass das Device capture-f¨ahig ist. Dies kann mit dem ioctl()-Kommando VIDIOCGCAP (siehe Abschnitt 6.4.1) uberpr ¨ uft ¨ werden: hier muss in der Bit-Maske type das Bit VID TYPE CAPTURE gesetzt sein. Das folgende Programm verwendet mmap(), um ein Bild der Groß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> <sys/ioctl.h> <sys/mman.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 Offnen des Devices (Zeile 22) und dem Abbilden in ein Speicherseg¨ ment (Zeile 28) wird die Ubertragung eines Bildes zun¨achst 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¨allen 0 angegeben. Besitzt das Device einen Frame-Buffer, in dem N Bilder abgelegt werden konnen, ¨ kann hier eine Zahl zwischen 0 und N − 1 angegeben werden. Damit die richtigen Daten in den Speicher ubertragen ¨ werden, musste ¨ 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 fur ¨ Rot und Blau (ab Zeile 53). Danach wird das Bild wiederum als PPM-Datei gespeichert. Soll eine Sequenz von Bildern aufgezeichnet werden, so mussen ¨ fur ¨ jedes Bild erneut die ioctl()-Kommandos VIDIOCMCAPTURE und VIDIOCSYNC aufgerufen werden, um den Inhalt des Speichersegmentes zu aktualisieren. Weitere Einstellmoglichkeiten ¨ Fur ¨ Ger¨ate, die eine Empfangseinheit (Tuner) besitzen (wie z. B. TV-Karten) oder uber ¨ mehrere Videoeing¨ange verfugen, ¨ gibt es ioctl()-Kommandos, mit denen die Videoquelle gew¨ahlt, das Videoformat (z. B. NTSC oder PAL) eingestellt und die Empfangsfrequenz vorgegeben werden konnen. ¨ Das Kommando VIDIOCSCHAN benotigt ¨ 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¨ahigkeiten –, die hier jedoch nicht alle im Einzelnen erl¨autert werden konnen. ¨ An dieser Stelle mochten ¨ wir noch einmal auf die Include-Datei /usr/include/linux/videodev.h
verweisen, der Sie die entsprechenden ioctl()-Kommandos und die zugehori¨ gen Strukturen entnehmen konnen. ¨ 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. Ublicherweise sind Bilder jedoch im JPEG-Format, das die Aufnahmen je nach Kompressionseinstellung auf weniger als 10% der Speichergroße ¨ reduziert. Die Umwandlung von Bilddaten in das JPEG-Format l¨asst 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> <sys/ioctl.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 <string.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 uber ¨ die serielle Schnittstelle eine Verbindung mit einem externen Ger¨at – z. B. einem Telefon-Modem – aufgebaut werden, so mussen ¨ zun¨achst die Geschwindigkeit (Bit pro Sekunde), die Anzahl der zu ubertragenden ¨ Bits pro Zeichen und die Anzahl der Stop-Bits korrekt eingestellt werden. Ferner sollte auch das Parit¨ats-Bit (eine Art Prufsumme) ¨ 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¨alt sich das Programm wie ein einfaches Terminal-
146
6 Devices – das Tor zur Hardware
Programm – echte“ Terminal-Programme wie minicom konnen ¨ naturlich ¨ 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>
# 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 Offnen 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¨achst 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 uber ¨ die Schnittstelle ausgegeben (Zeile 62 bis 68) oder umgekehrt (Zeile 72 bis 75). Eine Bet¨atigung der Taste ESC beendet die while()-Schleife durch break (Zeile 66) und stellt die alten Einstellungen fur ¨ den Standard-Eingabekanal wieder her (Zeile 78 bis 81). Die Funktion select() und die dazugehorigen ¨ Makros FD ZERO(), FD SET(), FD CLR() und FD ISSET() bedurfen ¨ einiger Erkl¨arungen. Diese Funktionen operieren mit so genannten File Descriptor Sets, das sind Variablen vom Typ fd set mit einer L¨ange von 1024 Bit (128 Bytes). Jedes Bit steht dabei fur ¨ einen Dateideskriptor, z. B. Bit Nr. 2 fur ¨ den Deskriptor mit dem Wert 2, also den StandardFehlerkanal. Das Makro FD ZERO() loscht ¨ alle Bits des angegebenen File Descriptor Sets. Mit FD SET() kann ein Dateideskriptor zu dem Set hinzugefugt ¨ und mit FD CLR() entfernt werden. Mit Hilfe von FD ISSET() l¨asst sich prufen, ¨ 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 uberpr ¨ uft. ¨ Als ersten Parameter erwartet select() die Nummer des hochsten ¨ 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() ermoglicht ¨ es somit, auf mehrere dateibezogene Ereignisse gleichzeitig zu warten – im Falle des Terminal-Programms das Drucken ¨ einer Taste oder das Empfangen eines Zeichens uber ¨ die Schnittstelle. Noch ein Hinweis zur Einstellung der Schnittstellenparameter: Wenn die Geschwindigkeit oder die Bitbreite eingestellt werden soll, ohne die anderen Einstellungen zu ver¨andern, mussen ¨ die relevanten Bits zun¨achst mit CBAUD bzw. CSIZE geloscht ¨ 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 Ubertragungsgeschwindigkeit fur ¨ das Senden und Empfangen von Zeichen l¨asst sich ubrigens ¨ 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 mussen ¨ 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¨allt etwas aus der Reihe, weil sie nicht wie die anderen bisher angesprochenen Ger¨ate uber ¨ ioctl()-Kommandos funktioniert. Die Kommunikation mit dem Drucker ubernimmt ¨ bei Linux ein Hintergrundprozess. Ursprunglich ¨ 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¨angigsten Formate sind ASCII (reine Textdateien) und PostScript. W¨ahrend reine ASCII-Texte bei der Druckerausgabe etwas an Schreibmaschinenseiten erinnern, bietet PostScript nahezu unbegrenzte Moglichkeiten, ¨ 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, konnen ¨ Sie das Programm lpr“ verwenden. Stehen mehrere Drucker zur Verfugung, ¨ konnen ¨ Sie ” mit der Option -PDrucker“ das gewunschte ¨ Ausgabeger¨at 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 konnen ¨ Sie auch sehen, welche Drucker eingerichtet sind: 1
Auf manchen Systemen, beispielsweise Cygwin, muss die Geschwindigkeit mit Hilfe der Funktio¨ nen cfsetispeed() und cfsetospeed() eingestellt werden; das Einstellen der Ubertragungsgeschwindigkeit uber ¨ 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, mussen ¨ Sie in Zeile 13 das Kommando lpr“ um die Option -P“ (siehe oben) ” ” erg¨anzen. In den Zeilen 19 bis 26 wird eine PostScript-Sequenz an den Standardeingabekanal von lpr“ gesendet. ”
6.6 Druckerausgaben
151
An dieser Stelle mochten ¨ wir Ihnen die Sprache“ PostScript etwas n¨aher 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¨ afe %%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¨ ohe: /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“ konnen ¨ 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 uber ¨ 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 1 ist dabei /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 fur ¨ den PostScript-Interpreter. Der Prolog am Anfang der Datei darf ubrigens ¨ 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 Seitenubersicht ¨ 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 ursprunglichen ¨ Maßeinheit auf Zentimeter umskaliert. Bitte beachten Sie, dass PostScript grunds¨atzlich 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¨arend. Es sei aber noch darauf hingewiesen, dass Zeichen wie ( ) %“ als ” Text ausgegeben werden konnen, ¨ 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¨ afe %%BoundingBox: 28 28 567 813 %%Orientation: Portrait %%Pages: 1 %%EndProlog %%Page: 1 % Schriftart auf "Times-Roman" mit 18pt H¨ ohe: /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¨ oschen % 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 ullen % und ausf¨
grestore
% alte Position/Skalierung
/buffer 2 string def
% entspricht: char buffer[2];
0 1 5 { % for (i=0; i # 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 ate an den USBs suchen // alle Ger¨
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¨ at while (udev != NULL) { udevhd = usb_open(udev); if (udevhd != NULL) { if (udev->descriptor.iProduct)
Das Gleiche leistet ubrigens ¨ 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¨ achstes Ger¨ at } bus = bus->next; // Zeiger auf n¨ achsten USB } if (i == 1) printf("No USB found.\n"); return(0); }
¨ Das Ubersetzen 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¨achst die libusb initialisiert. Danach werden die USB-Busse und -Ger¨ate 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¨achsten Bus bzw. das n¨achste Ger¨at. In zwei verschachtelten while-Schleifen (Zeile 25 und 29) werden alle gefundenen Busse und Ger¨ate der Reihe nach abgefragt und deren Herstellerkennung (idVendor) und Produkt-Code (idProduct) ausgegeben. Außerdem wird der Beschreibungstext aus jedem Ger¨at ausgelesen (Zeile 35). Dazu muss das Device zun¨achst geoffnet ¨ und sp¨ater 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¨alt die Struktur usb device descriptor einige weitere Eintr¨age mit Informationen uber ¨ das angeschlossene Ger¨at, 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¨aten anhand eines Beispiels Als Beispiel fur ¨ die Kommunikation mit USB-Ger¨aten zeigen wir in diesem Abschnitt die Ansteuerung eines USB- Raketenwerfers“, der uber ¨ verschiedene An” bieter im Internet fur ¨ knapp 30,– Euro erworben werden kann (siehe Abbildung 6.2). Das Ger¨at hat die Herstellerkennung 1130hex (Tenx Technology Inc.) und die Produkt-ID 0202hex. Das folgende Programm sucht zun¨achst in der Liste der USB-Devices das richtige Ger¨at anhand der Hersteller- und Produkt-ID. Danach l¨asst 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> <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 fur ¨ 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¨ ates -----*/ 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) { at udev = bus->devices; // 1. Ger¨ 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¨ achstes Ger¨ at achsten USB // Zeiger auf n¨
} /*----- 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 ˆ 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 Ubersetzen 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¨ate nach der richtigen Hersteller- und Produkt-ID und gibt den Zeiger auf das entsprechende USB-Device zuruck ¨ – bzw. 0, wenn der Raketenwerfer nicht gefunden wurde. Bevor das Programm mit dem Raketenwerfer kommunizieren kann, muss es das Interface des Ger¨ats fur ¨ 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¨achst 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 mussen. ¨ Anschließend wird die aktive Konfiguration gew¨ahlt und das Interface 1 reserviert, das wir fur ¨ 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 fur ¨ die vier Bewegungsrichtungen und den Ausloser ¨ des Raketenwerfers. Fur ¨ die auszufuhrenden ¨ Bewegungen muss der entsprechende Parameter auf 1 gesetzt werden, die anderen Parameter mussen ¨ auf 0 bleiben. Bevor man das Kommando zur Bewegung des Raketenwerfers senden kann, mussen ¨ zwei Initialisierungssequenzen an das Ger¨at geschickt werden, die in den Zeilen 15 und 16 definiert sind. Das Senden von Steuersequenzen an das USBGer¨at ubernimmt ¨ 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¨anger spezifiziert, in diesem Fall also ein Interface des Ger¨ates. 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¨ahrend size die Anzahl der Bytes angibt, die ubertragen ¨ werden sollen, hat value hier keine Bedeutung. Weitere Informationen zur libusb finden Sie auf den Internetseiten [13] von Sourceforge.net. Fur ¨ 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 Ruckgabewert, ¨ was wir hier jedoch einfach ignorieren.
Kapitel 7
Netzwerkprogrammierung Seit den Anf¨angen von Linux vor mehr als 10 Jahren ist die Netzwerkkommunikation1 fester Bestandteil des Betriebssystems. W¨ahrend zu dieser Zeit auf WindowsTM 3.11-Rechner diverse propriet¨are Netzwerkprotokolle aufgesetzt wurden, war Linux von Haus aus in der Lage, uber ¨ den Standard TCP/IP mit Workstations, Großrechnern und Servern in Rechenzentren zu kommunizieren. Im Bereich der PCs und Workstations wurden die propriet¨aren Losungen ¨ fast vollst¨andig von TCP/IP verdr¨angt, sodass inzwischen die unterschiedlichen Betriebssysteme in einem Netzwerk die gleiche Sprache“ sprechen. ” Moglicherweise ¨ 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¨ache anklicken, wird dies dem betreffenden Programm von der grafischen Oberfl¨ache X11 uber ¨ 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¨andige und detaillierte Beschreibung umfasst leicht mehrere hundert Seiten. Deshalb konzentriert sich dieses Kapitel auf die gel¨aufigsten Funktionen und Methoden, die Sie immerhin in die Lage versetzen werden, einen eigenen Webserver zu programmieren! 1
Netzwerkkommunikation w¨are die passendere Bezeichnung fur ¨ dieses Kapitel. In der englischsprachigen Literatur hat sich jedoch der Begriff Network Programming“ etabliert und wurde sp¨ater ” w¨ortlich ins Deutsche ubersetzt. ¨
164
7 Netzwerkprogrammierung
7.1 Einfuhrung ¨ ¨ Ahnlich 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 konnen, ¨ sind jedoch ein gewisses Grundlagenwissen und etwas Theorie unerl¨asslich. Insbesondere sollen die im Umfeld der Netzwerkkommunikation auftauchenden Begriffe sowie das Prinzip der Kommunikation uber ¨ eine Netzwerkverbindung erl¨autert werden.
7.1.1 Begriffe Ethernet Der Begriff Ethernet“ wird h¨aufig als Synonym fur ¨ eine Netzwerkverbindung ” verwendet. Tats¨achlich beschreibt Ethernet ein Verfahren, wie mehrere Teilnehmer auf eine gemeinsame Netzwerkleitung zugreifen konnen ¨ – es handelt sich dabei also um ein Zugriffsverfahren. Ursprunglich ¨ bestand diese Netzwerkleitung aus einem Koaxialkabel (¨ahnlich einem Antennenkabel), an das alle Computer eines Netzwerksegments uber ¨ je ein T-Stuck ¨ angeschlossen waren. Damit waren Datenraten bis 10 MBit/s moglich. ¨ Heute findet man eine solche Verkabelung nur noch selten. Das Koaxialkabel wurde weitestgehend von der bekannten 8-adrigen CAT 5“-Leitung verdr¨angt, und ” die Netzwerkstruktur ist heutzutage in der Regel sternformig, ¨ mit Hubs und Switches in den Sternpunkten. Ethernet definiert also weder die Art der Leitung noch die des Protokolls (s. u.), das uber ¨ die Leitung ubertragen ¨ 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¨anger des Datenpaketes adressiert“ wird; ” ¨ ob und wie auf Ubertragungsfehler reagiert wird; ... Einige solcher Protokolle sind: IP – Internet Protocol (regelt die Adressierung der Netzwerkteilnehmer); TCP – Transmission Control Protocol (definiert die gesicherte1 Datenubertragung ¨ zwischen zwei Netzwerkteilnehmern); 1
gesichert“ bedeutet hier: Es konnen ¨ keine Daten verloren gehen. ”
7.1 Einfuhrung ¨
165
UDP – User Datagram Protocol (regelt die Datenubertragung ¨ a¨ hnlich TCP, jedoch nicht gesichert); ¨ FTP – File Transfer Protocol (definiert die Ubertragung 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-Ubertragung); PPP – Point to Point Protocol (definiert die Netzwerkverbindung zwischen zwei Teilnehmern uber ¨ eine Punkt-zu-Punkt-Verbindung inkl. Authentifizierung und Zuweisung einer IP-Adresse). In der Regel trifft man auf eine Verschachtelung mehrerer Protokolle. So laufen“ ” uber ¨ 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 dafur, ¨ 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 uber ¨ 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) ubertragen ¨ werden und Benutzername und Passwort bei einem remote login“ (rlogin oder telnet) an der ” richtigen Stelle ankommen? Dies ist in a¨ hnlicher Weise gelost, ¨ wie auch die richtigen Daten an Drucker, Modem, Monitor, Tastatur, Maus und Scanner ankommen: Die Ger¨ate h¨angen an unterschiedlichen Anschlussen ¨ oder auch Ports des Computers. Ebenso enthalten die TCP/IP-Datenpakete neben der IP-Adresse auch eine Port-Nummer, also eine Information, fur ¨ welchen Anschluss“ des Computers die ” Daten bestimmt sind (Abbildung 7.1). Es gibt eine ganze Reihe von Port-Nummern, die fest fur ¨ 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 uber ¨ Benutzer) ” Web-Server (HTTP-Server) X11-Server
Eine umfangreiche Liste der (moglichen) ¨ 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 Sicherheitsgrunden ¨ verschiedene Netzwerkaktivit¨aten, 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¨achst prufen, ¨ ob eine Firewall aktiv ist
7.1 Einfuhrung ¨
167
und diese dann ggf. deaktivieren.1 Bei SuSE-Linux geht das mit Hilfe des Konfigurationstools YAST. W¨ahlen 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 Sicherheitsgrunden ¨ 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¨ache Fire” wall nun stoppen“ w¨ahlen (Abbildung 7.3). Danach konnen ¨ Sie die Konfiguration mit der entsprechenden Schaltfl¨ache abbrechen. Wenn Sie die Einstellung Ser” vice starten: Bei Systemstart“ aktiviert lassen, wird die Firewall automatisch beim n¨achsten Neustart des Rechners wieder aktiv. Netzwerkdienste aktivieren Linux stellt bereits einige Netzwerkdienste zur Verfugung, ¨ von denen einige in dem xinetd“ zusammengefasst sind. Dieser D¨amon ist in der Grundkonfigurati” on h¨aufig deaktiviert. Da die im xinetd enthaltenen Dienste aber gerade fur ¨ erste Versuche mit Netzwerkprotokollen hilfreich sind, sollten sie den D¨amon 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“ fur ¨ die Beispiele in den folgenden Abschnitten aktivieren. ” ”
Abbildung 7.4: Konfiguration der Netzwerkdienste mit YaST
7.1 Einfuhrung ¨
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¨aufe (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¨angig 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 konnen ¨ beide Applikationen, Client und Server, jeweils eindeutig uber ¨ ihre IP-Adresse und Port-Nummer adressiert werden. ¨ Ubrigens: Client und Server konnen ¨ naturlich ¨ 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 mussen ¨ zun¨achst einen Socket offnen, ¨ bevor eine Verbindung auf¨ gebaut werden kann. Zum Offnen eines Sockets dient die gleichnamige Funktion int socket(int domain, int type, int protocol);
Dabei gibt der Parameter domain die Protokollfamilie fur ¨ die Kommunikation uber ¨ diesen Socket an. Fur ¨ TCP/IP-Verbindungen ist das die Konstante PF INET. Der zweite Parameter legt die Art der Kommunikation fest, fur ¨ verbindungsorientierte Kommunikation ist hier die Konstante SOCK STREAM zu w¨ahlen. 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¨ahlt wird. Als Ruckgabewert ¨ 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“ fur ¨ 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 konnen ¨ Sie – wie bei Devices – die Funktionen read() und write() verwenden. Daruber ¨ hinaus gibt es fur ¨
7.2 Der TCP/IP-Client
171
die Netzwerkkommunikation spezielle Funktionen, die zus¨atzliche Moglichkei¨ ten 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 uberhaupt ¨ Daten uber ¨ einen Socket lesen oder schreiben konnen, ¨ muss eine Verbindung zum Socket des Kommunikationspartners hergestellt werden. Weil dies bei Client und Server unterschiedlich ist, wird in den folgenden Abschnitten zun¨achst ein typischer Client und anschließend ein einfacher Server vorgestellt.
7.2 Der TCP/IP-Client Fur ¨ die Demonstration von Netzwerkprogrammierung benotigt ¨ man sowohl ein Client- wie auch ein Server-Programm. Glucklicherweise ¨ ist Linux von Haus aus mit einer Reihe von Server-Programmen auf TCP/IP-Basis ausgestattet, sodass wir uns zun¨achst auf das Programmieren eines TCP/IP-Clients konzentrieren konnen. ¨
7.2.1 Aufbau einer Verbindung Als erster Schritt muss, wie bereits erw¨ahnt, ein Socket geoffnet ¨ 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 zuruck, ¨ 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¨ange 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() mussen ¨ in diese Struktur die IP-Adresse und die Port-Nummer des Servers eingetragen werden. Das erste Element gibt dabei die Adressfamilie an; fur ¨ eine IP-Adresse ist das AF INET. Als zweites Element enth¨alt 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 ublichen ¨ Schreibweise (z. B. 127.0.0.1“) enth¨alt, die von inet aton() in ” bin¨are Form umgewandelt wird. Ist die angegebene IP-Adresse fehlerhaft, liefert inet aton() eine 0 als Ruckgabewert. ¨ 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> <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¨andiges Programm zum Offnen 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 nutzlich ¨ – 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 offnet ¨ 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 a¨ 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> <string.h> <sys/types.h> <sys/socket.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 geoffnet, ¨ wiederum mit der Protokollfamilie IP (PF INET) und fur ¨ verbindungsorientierte Kommunikation (SOCK STREAM). In den Zeilen 40 bis 42 werden IP-Adresse und Port-Nummer des Servers in die Adress-Struktur fur ¨ 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 ubertragen ¨ (Zeile 68 bis 74). Daten, die der Socket empf¨angt, 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¨ag dargestellt – vorausgesetzt, der FTP-Server ist wie in Abschnitt 7.1.2 beschrieben aktiviert (Benutzereingaben sind schr¨ag 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
[email protected]. 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 konnen: ¨ Versuchen Sie einmal, den SMTP-Dienst (Port 25) oder den UserInformationsdienst (Port 79) zu kontaktieren. Bei Letzterem mussen ¨ Sie entweder einen Benutzernamen eingeben oder einfach RETURN drucken. ¨ ¨ Je nach Ubertragungsmedium (Ethernet oder analoges Modem) gibt es unterschiedliche Obergrenzen fur ¨ die mit einem send()-Aufruf ubertragenen ¨ Daten. Es kann also sein, dass send() nicht alle angegebenen Daten ubertr¨ ¨ agt. Der Ruckgabewert ¨ von send() liefert die Anzahl der tats¨achlich gesendeten Bytes. Um sicherzugehen, dass alle Bytes zum Server ubertragen ¨ werden, muss der Ruckgabewert ¨ mit der zu sendenden Anzahl an Bytes verglichen und die Differenz ggf. mit einem zweiten send()-Aufruf ubertragen ¨ 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¨allen bekannt. Meistens kennt man den Rechnernamen oder den Domain-Namen z. B. in der Form www.hanser.de“. ” Zur Auflosung ¨ 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 Ruckgabewert ¨ 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 dazugehorige ¨ 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>
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 ubergibt ¨ den Kommandozeilenparameter (also den DomainNamen) an die Funktion gethostbyname(), diese liefert dann die zugehorige ¨ 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¨arer Form enth¨alt, 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, konnen ¨ 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¨anden uberschrieben ¨ wird. Daher sollten die benotigten ¨ Daten (beispielsweise die IP-Adresse) fur ¨ 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¨autert werden.
7.3.1 Die Funktionsweise eines Servers Ebenso wie jeder Client benotigt ¨ auch ein Server-Programm zun¨achst einen Socket als Basis fur ¨ die Netzwerkkommunikation. Die anschließenden Schritte sind jedoch ganz anders als bei einem Client-Programm; Abbildung 7.7 zeigt den prinzipiellen Ablauf mit den zugehorigen ¨ 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 a¨ 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 naturlich ¨ bereits ein Tool unter Linux, das dies (und noch mehr) kann: nslookup.
7.3 Server-Programme
179
socket()
¨ Offnen eines Sockets
? bind()
Verknupfen ¨ 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 Ruckgabewert. ¨ Im Fehlerfall – beispielsweise wenn der Port bereits belegt ist – gibt bind() eine –1 zuruck. ¨ Um einen Socket an einen Port zu binden“, muss das Programm (je nach ” Portnummer) ggf. uber ¨ root-Rechte verfugen! ¨ Nach Beendigung des Programms bleibt der Port noch eine Zeit lang belegt“ ”
Der n¨achste 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 zuruck, ¨ 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¨agt 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¨ange der Adress-Struktur initialisiert werden. Nach dem Funktionsaufruf enth¨alt die Variable die tats¨achliche L¨ange der Client-Adressinformation. accept() liefert als Ruckgabewert ¨ 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 uber ¨ den zun¨achst mit socket() geoffneten ¨ Socket statt, sondern uber ¨ den von accept() gelieferten Socket. Indem fur ¨ jede mit accept() angenommene Verbindung ein eigener Socket geoffnet ¨ wird, kann der Server mit mehreren Clients parallel kommunizieren – sonst w¨aren Webserver ja undenkbar. Fur ¨ gewohnlich ¨ wird fur ¨ jede dieser Verbindungen ein eigener Kind-Prozess gestartet, w¨ahrend 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 naturlich ¨ 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> <sys/types.h> <sys/socket.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¨achst ein Socket geoffnet. ¨ In den Zeilen 44 bis 46 wird dann die AdressStruktur initialisiert. Durch die Angabe der Konstanten INADDR ANY fur ¨ die IPAdresse wird der Socket beim folgenden bind()-Aufruf (Zeile 49) automatisch mit allen IP-Adressen des Computers1 verknupft. ¨ Ist dies nicht erwunscht, ¨ 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 zuruck, ¨ und die Struktur client addr wurde mit der IP-Adresse und Port-Nummer des Client-Programms gefullt. ¨ Die Funktion inet ntoa() wandelt die bin¨are IP-Adresse in eine Zeichenkette um (Zeilen 65 und 66). Wie bereits bei dem in Abschnitt 7.2.2 vorgestellten TCP/IP-Client ermoglicht ¨ auch hier die while()-Schleife in den Zeilen 68 bis 96 die bidirektionale Kommunikation uber ¨ den Socket. Das Belegen eines reservierten Ports1 mit einem Dienst erfordert root-Rechte. Damit das Programm trotzdem von einem normalen“ Benutzer ausgefuhrt ¨ 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¨auscht“ werden – Voraussetzung ” ist, dass nicht bereits ein solcher Server (z. B. der Apache Webserver) l¨auft. 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 fur ¨ bestimmte Dienste wie z. B. Port 80 fur ¨ HTTP. Unregistrierte Ports wie beispielsweise 30.000 konnen ¨ ohne besondere Benutzerrechte belegt werden.
184
7 Netzwerkprogrammierung
An dieser Stelle konnen ¨ Sie das Programm mit Ctrl-D beenden oder eine HTTPAntwort eingeben, was jedoch relativ muhsam ¨ ist und daher echten“ Webser” vern uberlassen ¨ werden sollte. Alternativ kann das Server-Programm naturlich ¨ auch mit dem interaktiven Client-Programm aus Abschnitt 7.2.2 kommunizieren. Auf diese Weise l¨asst sich eine Chat“-Verbindung aufbauen, uber ¨ die sich zwei ” Benutzer miteinander unterhalten konnen. ¨
7.3.3 Ein kleiner Webserver Der Nutzen eines interaktiven TCP/IP-Servers, wie er im vorigen Abschnitt dargestellt wurde, ist relativ eingeschr¨ankt. 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 benotigen ¨ und auch nicht auswerten. Der grunds¨atzliche 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)
Anfrage-Korper ¨ (optional)
Antwort-Korper ¨ (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
Mogliche ¨ Methoden sind: GET – Daten (z. B. HTML-Datei) vom Server anfordern. HEAD – Nur die Kopfzeilen vom Server anfordern. Damit kann beispielsweise die L¨ange der Daten und das Format erfragt werden, ohne die Daten selbst zu ubertragen. ¨ POST – Die Inhalte eines HTML-Formulars an den Server ubertragen ¨ und die Antwort anfordern. In der Regel enth¨alt eine HTTP-Anfrage mindestens eine Kopfzeile der Form: Host: www.hanser.de
Ab HTTP 1.1 ist diese Kopfzeile Pflicht. Sie ermoglicht ¨ 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 benotigen ¨ keinen Anfrage-Korper ¨ (Body), w¨ahrend er bei der Methode POST mit den Inhalten des HTML-Formulars gefullt ¨ ist. In diesem Fall ist mindestens eine weitere Kopfzeile erforderlich, die die L¨ange des Anfrage-Korpers ¨ angibt. Die Antwortzeile des Servers enth¨alt – 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 moglichen ¨ 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¨alt, sollte danach mindestens zwei Kopfzeilen ausgeben: die Art des Anwort-Korpers ¨ und dessen L¨ange, z. B.: Content-type: text/html Content-length: 1374
Nach einer Leerzeile (CR + LF!) folgt dann der eigentliche Korper, ¨ 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 Leere Seite.
Eine ausfuhrlichere ¨ 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¨ankt, 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> <stdlib.h> <sys/types.h> <sys/socket.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 "Error" 89 "File not found." 90 "", 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 konnen, ¨ 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¨auft 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¨alt, in Zeile 18 als HTML PATH“ eintragen. Als URL geben Sie dann im Browser ” http://localhost/HTML-Datei“ ein. ” Betrachten wir zun¨achst das Hauptprogramm ab Zeile 113. Zu Beginn wird gem¨aß dem bereits vorgestellten Schema zun¨achst ein Socket geoffnet ¨ und an¨ In Zeischließend mit bind() mit dem Port 80 (Konstante MY PORT) verknupft. le 132 erfolgt dann der Aufruf setuid(getuid()). Er bewirkt, dass das Programm nach der Verknupfung ¨ mit dem Port die uber ¨ das s“-Bit verliehenen root” Rechte wieder abgibt. Dadurch konnte ¨ 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, wurde ¨ 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¨achst die HTTP-Anforderung ein und pruft ¨ auf die Kommandos GET“ und ” 1
Das sind Prozesse, die eigentlich beendet sind, aber noch darauf warten, ihren Status zuruckzumel¨ den.
7.4 Das User Datagram Protocol (UDP)
191
¨ HEAD“ (andere werden zur Zeit nicht unterstutzt). ¨ Schl¨agt das Offnen der ange” forderten Datei in Zeile 83 fehl, so wird eine entsprechende HTTP-Fehlermeldung an den Client zuruckgeschickt ¨ (Zeile 85 bis 91). Wenn die angegebene Datei existiert, wird eine positive Ruckmeldung ¨ generiert (Zeile 94) und gepruft, ¨ 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¨ange der Datei mit Hilfe der Kopfzeile Content-length:“ ubermittelt. ¨ Han” delte es sich bei der Anforderung um ein HEAD“, ist die Kommunikation an ” ¨ dieser Stelle beendet. Bei einer GET“-Anforderung folgt die Ubertragung 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 konnen. ¨ Im Gegensatz dazu besteht beim Versenden von UDP-Paketen keine Garantie, dass diese wirklich ankommen. Dennoch bietet UDP-basierte Netzwerkkommunikation zwei Eigenschaften, die sehr nutzlich ¨ oder sogar notwendig sein konnen: ¨ Der Datenstrom“ wird nicht blockiert, falls ein Paket nicht oder nur fehlerhaft ” angekommen ist. Dies ist fur ¨ zeitkritische Anwendungen wie Internettelefonie wichtig. UDP erlaubt es, Nachrichten an mehrere (Multicast) oder an alle (Broadcast) Netzwerkteilnehmer zu schicken. Das ist mit TCP nicht moglich! ¨ Aus diesem Grund zeigen wir in den folgenden Abschnitten, wie die Kommunikation uber ¨ UDP funktioniert und wie Broadcast- und Multicast-Nachrichten versendet werden.
7.4.1 UDP-Nachrichten senden ¨ Bereits beim Offnen des Sockets muss festgelegt werden, ob das Protokoll TCP oder UDP verwendet werden soll (vgl. Abschnitt 7.1.4). Fur ¨ 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¨allt bei der ” UDP-Kommunikation der Funktionsaufruf connect(). Um das Ziel (IP-Adresse und Port) beim Versenden eines Pakets angeben zu konnen, ¨ mussen ¨ 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 fur ¨ die Zieladresse enth¨alt. 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 gefullt ¨ 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> <string.h> <sys/poll.h> <sys/types.h> <sys/socket.h> <sys/time.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 ¨ offnen ---*/ 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("\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 Offnen des Sockets (Zeile 40) tr¨agt 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 ubergebene ¨ 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 a¨ hnlich wie die Funktion select(), die unter anderem in unseren TCPBeispielen verwendet und auf Seite 110 beschrieben wurde. Um das Programm zu testen, konnen ¨ Sie den Netzwerkdienst Echo“ (UDP” Port 7) verwenden. Dieser Dienst ist im D¨amon xinetd“ enthalten und l¨asst sich, ” wie in Abbildung 7.9 gezeigt, aktivieren (siehe auch Seite 168). Der Echo“-Dienst sendet alle empfangenen Pakete an den jeweiligen Absender ” zuruck. ¨ 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 fur ¨ verbindungsorientierte Kommunikation benotigt. ¨ 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¨atigungstext an den Absender schickt. Empf¨angt 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¨ ur UDP-Datagramme */ # # # # # # #
include include include include include include include
<stdio.h> <string.h> <sys/types.h> <sys/socket.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 ¨ offnen ---*/ 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¨atigungen: > ./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 moglich, ¨ eine Nachricht an alle Teilnehmer eines lokalen Netzwerks zu schicken.1 Dieses Verfahren wird beispielsweise fur ¨ 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 Ubertragen von normalen“ Datagrammen, jedoch mit einer ” 1
Damit solche Nachrichten nicht uber ¨ 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¨ankt.
198
7 Netzwerkprogrammierung
speziellen Zieladresse. Jeder Netzwerkschnittstelle, die eine gultige ¨ IP-Adresse besitzt, ist automatisch eine entsprechende Broadcast-Adresse zugeordnet. Die Schnittstelle empf¨angt 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 zugehorige ¨ BroadcastAdresse 192.168.0.255. Mit dem Programm ifconfig“ konnen ¨ Sie die IP-Adresse ” und die zugehorige ¨ 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¨ ultigkeitsbereich: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¨ ange:1000 RX bytes:0 (0.0 b) TX bytes:1575 (1.5 Kb) Interrupt:225 Basisadresse:0x2000
Daruber ¨ hinaus gibt es eine besondere Broadcast-Adresse, die von der IP-Adresse der Schnittstelle unabh¨angig ist: 255.255.255.255. Auch wenn das Senden von Broadcast-Paketen mit den bereits beschriebenen Funktionen fur ¨ normale“ Pakete geschieht, ist bei Linux eine zus¨atzliche Siche” rung gegen das unbeabsichtigte Verschicken eines Broadcasts eingebaut: Zuerst muss der Socket fur ¨ 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, mussen ¨ Sie ab Zeile 47 folgenden Quelltext einfugen: ¨ 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, konnen ¨ 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, mussen ¨ 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 Moglichkeit, ¨ 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 Moglichkeit ¨ 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¨angig von der Adresse der Netzwerkschnittstelle ist: 224.0.0.0 bis 239.255.255.255. Dies bedeutet, dass jeder Empf¨anger 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 fur ¨ den Empfang von Multicast-Paketen eingerichtet werden, zeigt der n¨achste Abschnitt. Aber zun¨achst zum Senden von Multicast-Nachrichten: Fur ¨ ausgehende Pakete muss in der Routing-Tabelle eingetragen sein, uber ¨ 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 fur ¨ 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¨ahnt, dass der Linux-Kernel Multicast unterstutzen ¨ muss, um einen Multicast-Socket einrichten zu konnen. ¨ Dies l¨asst sich u. a. wie folgt uber¨ prufen: ¨
200
7 Netzwerkprogrammierung
> cat /proc/config.gz | gunzip | grep MULTICAST CONFIG IP MULTICAST=y
Um ein UDP-Server-Programm Multicast-f¨ahig zu machen, muss die gewunschte ¨ Multicast-Adresse mit Hilfe der Funktion setsockopt() mit dem Socket verknupft ¨ werden, oder, anders ausgedruckt: ¨ Der Socket muss der Multicast-Gruppe hinzugefugt ¨ 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¨ahig 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¨at eingerichtet wird, soll das Gleiche mit Diensten in einem Netzwerk moglich ¨ 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 uber ¨ UPnP an. Auch IP-Kameras verschiedener Hersteller (z. B. Axis oder Pelco) haben UPnP implementiert, um die Kameras und deren Funktionsumfang automatisch uber ¨ das Netzwerk ermitteln zu konnen. ¨ Um nach UPnP-Diensten im lokalen Netzwerk zu suchen, konnen ¨ 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
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 Ruckmeldung ¨ aller Dien” ste an, w¨ahrend upnp:rootdevice“ nur eine Antwort pro UPnP-Host liefert. ” Die im lokalen Netzwerk verfugbaren ¨ 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 Schlusselwort ¨ Location:“, gefolgt von einem URL (Uniform Re” ¨ source Locator) der Form http://IP-Adresse:Port/Pfad“. Uber diesen URL konnen ¨ ” 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 benotigt ¨ 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> <string.h> <sys/poll.h> <sys/types.h> <sys/socket.h> <sys/time.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 ¨ offnen ---*/ 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("\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 a¨ 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 konnen ¨ Sie entnehmen, dass es sich um ” ein InternetGatewayDevice“ handelt. Den hinter Location:“ angegebenen URL ” ” konnen ¨ Sie beispielsweise mit Firefox offnen, ¨ um die XML-Beschreibungsdatei des Dienstes darzustellen. Fur ¨ weitere Informationen zu UPnP mochten ¨ 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, uber ¨ das Internet mit der großen, weiten Welt“ zu kommunizieren. Ungluck¨ ” licherweise sind es – neben anderen – genau diese Techniken, die Hackern, Viren und Wurmern ¨ dabei helfen, ihre weniger harmlosen Absichten zu verfolgen. Sobald Sie einen Dienst uber ¨ einen TCP/IP-Port einrichten, konnten ¨ ungebetene G¨aste“ versuchen, diesen als Hintertur ¨ 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¨achst auf Systemen entwickeln, die keine Verbindung nach draußen“ haben. Außerdem ” sollten Sie beim Erstellen solcher Programme immer berucksichtigen, ¨ dass Ihr Gegenuber“ ¨ moglicherweise ¨ andere Anfragen oder Antworten schickt als die ” von Ihrem Programm erwarteten. Ein beliebter Fehler ist beispielsweise ein nicht abgefangener Pufferuberlauf: ¨ Sendet der entfernte Rechner mehr Zeichen, als in den Empfangspuffer passen, konnen ¨ dadurch andere Bereiche des Programms mit Daten des fremden Rechners uberschrieben ¨ 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 fur ¨ 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¨achen Unter Linux gibt es eine ganze Reihe von Anwendungen, die ohne grafische Benutzerschnittstelle ( GUI“ fur ¨ Graphical User Interface) auskommen. Im Ab” schnitt 3.4.2 wurde gezeigt, wie eine komfortable Ausgabesteuerung auch im Terminalfenster moglich ¨ ist. Die Bedienung eines Programms l¨asst sich jedoch oft durch eine grafische Benutzerschnittstelle deutlich vereinfachen – fur ¨ manche Anwendungen wie z.B. Grafikprogramme ist sie sogar unabdingbar. Es stehen unter Linux zahlreiche Funktionsbibliotheken fur ¨ die Programmierung von grafischen Oberfl¨achen zur Verfugung; ¨ 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. fur ¨ Systeme mit knappen Ressourcen – wird in Abschnitt 8.3 die Verwendung der libvga fur ¨ grafische Anwendungen ohne das X11-System demonstriert.
8.1 Die grafische Oberfl¨ache X11 Das X11-System wurde am Massachusetts Institute of Technology (kurz MIT) in Zusammenarbeit mit der Firma DEC1 als Hardware-unabh¨angige, grafische Schnittstelle entwickelt. 1988 wurde das MIT X Consortium gegrundet, ¨ das diese Arbeit fortfuhrte. ¨ Nach Auflosung ¨ dieses Konsortiums ubernahm ¨ zun¨achst The Open ” Group“ die Weiterentwicklung, bevor sie an die X.Org“ ubertragen ¨ wurde. ” X11 ist als Client-Server-System realisiert – der Server bildet die Schnittstelle zur Hardware, w¨ahrend der Client mit dem Server kommuniziert, um beispielsweise Grafikobjekte darzustellen oder zu manipulieren. Das Programm XF86 SVGA2 ist z.B. ein X11-Server (fur ¨ SVGA-Grafikkarten), der Window-Manager KDE hinge1
Digital Equipment Corporation
2
Mit Einfuhrung ¨ der Version 4 wurden die verschiedenen Programme wie XF86 SVGA durch den einheitlichen Server XFree86 abgelost. ¨
206
8 Grafische Benutzeroberfl¨achen
gen ein X11-Client. Das X11-System zeichnet aus, dass der Client auf einem anderen Rechner laufen kann als der Server, sofern beide uber ¨ ein TCP/IP-Netzwerk verbunden sind. So ist es z.B. moglich, ¨ dass ein Linux-PC uber ¨ das Internet mit einer Unix-Workstation verbunden ist und auf der Workstation ein Grafikprogramm l¨auft, das am PC bedient wird. Unter Linux kommt die freie X11-Variante XFree86 zum Einsatz, die ursprung¨ lich fur ¨ 80x86-Prozessoren entwickelt wurde und als offener Quelltext verfugbar ¨ 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 fur ¨ komplexe Bedienelemente wie beispielsweise Menus ¨ 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+ Ursprunglich ¨ wurde GTK als grafische Benutzerschnittstelle fur ¨ das GNU Image ” Manipulation Program“ (GIMP) entwickelt. Daraus entstand schließlich ein eigenst¨andiges Projekt und sehr nutzliches ¨ Toolkit fur ¨ Bedienerobfl¨achen. GTK ist objektorientiert: Fenster, Schaltfl¨achen, Menus ¨ usw. sind Objekte (GTKWidgets), deren interner Aufbau fur ¨ den Programmierer verborgen bleibt. Um die Eigenschaften eines solchen Objektes – beispielsweise den Titeltext eines Fensters – zu ver¨andern, 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 Unterstutzung ¨ von Unicode bzw. UTF-8. Damit konnen ¨ Texte mit Zeichen aus den verschiedensten Sprachen dargestellt werden, w¨ahrend GTK 1.2 bereits mit deutschen Umlauten Probleme hatte.
8.2 Das Toolkit GTK+
207
8.2.2 GTK-Programme ubersetzen ¨ ¨ Fur ¨ das Ubersetzen von GTK-Programmen mussen ¨ mehrere Bibliotheken eingebunden werden. Daruber ¨ hinaus mussen ¨ dem Compiler zus¨atzliche Pfade fur ¨ die Include-Dateien bekannt gemacht werden. Zu diesem Zweck enth¨alt das GTK 2.0Paket ein nutzliches ¨ Tool: pkg-config. Mit der Option --libs“ liefert das Pro” gramm die erforderlichen Compiler-Optionen fur ¨ das Einbinden der Funktionsbibliotheken; die Option --cflags“ bewirkt, dass die Compiler-Optionen fur ¨ die ” zus¨atzlichen Include-Pfade ausgegeben werden. Außerdem muss man das Paket angeben, dessen Bibliotheken eingebunden werden sollen, also gtk+-2.0“ fur ¨ ” GTK 2.0 und gtk+“ fur ¨ GTK 1.2. (Die meisten Beispielprogramme in diesem Ka” pitel lassen sich unter beiden GTK-Versionen ubersetzen.) ¨ Naturlich ¨ konnen ¨ Sie beide Optionen auch gleichzeitig angeben, um mit einem Aufruf von pkg-config die fur ¨ GTK-Applikationen notwendigen CompilerParameter zu erhalten. Durch die Verwendung von Hochkommata (‘) werden die Ausgaben von pkg-config als Kommandozeilenparameter an den C-Compiler ubergeben: ¨ gcc mein_prog.c -o mein_prog \ ‘pkg-config --libs --cflags gtk+-2.0‘
¨ Fur ¨ das Ubersetzen 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 Ubersetzung der Beispielprogramme eine Menge Tipparbeit sparen wollen, geben Sie einfach folgendes Universal-Makefile“ ein: ” 1 # ur GTK+2.0-Programme 2 # Universal-Makefile f¨ 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 konnen ¨ Sie die Beispielprogramme einfach mit make Dateiname“ ” ubersetzen. ¨
208
8 Grafische Benutzeroberfl¨achen
8.2.3 Ein erstes Beispiel Als Einstieg in die Programmierung von GTK-Applikationen dient das folgende Programm, das ein einfaches Fenster voreingestellter Große ¨ und ohne weitere Elemente offnet: ¨ 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 ¨ offnen */ # include 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 Ubersetzen 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 mussen ¨ Sie in der Shell, aus der Sie es gestartet haben, Ctrl-C1 eingeben, denn eine andere Moglichkeit ¨ 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 konnen ¨ Sie mit der Option --name“ den Applikationsna” men a¨ 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¨achsten 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¨aher erl¨autern.
¨ Abbildung 8.1: Anderung des Fenstertitels mit der Option --name“ ”
Fenstereigenschaften a¨ ndern Selbstverst¨andlich konnen ¨ 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¨asst sich auch die Groß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 prufen, ¨ ob die Funktion auf das Widget anwendbar ist. Wenn ja, wird eine Typumwandlung der Art (GtkWindow *)window vorgenommen.
210
8 Grafische Benutzeroberfl¨achen
Wenn Sie mochten, ¨ dass die Anwender Ihres Programms das Fenster mit der Maus nicht großer ¨ oder nicht kleiner als die Objekte in dem Fenster ziehen konnen, ¨ so l¨asst sich dies ebenfalls einstellen: void gtk_window_set_policy(GtkWindow *window, gint allow_shrink, gint allow_grow, gint auto_shrink);
Fur ¨ eine feste Fenstergroß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¨alt und dadurch die Große ¨ auf wenige Pixel zusammenschrumpft! Voreingestellt ist ubrigens, ¨ dass die Fenster zwar großer, ¨ aber nicht kleiner als die enthaltenen Elemente gezogen werden konnen ¨ (Parameter: FALSE, TRUE, TRUE). Dadurch wird sichergestellt, dass alle Elemente vollst¨andig angezeigt werden, der Benutzer das Fenster aber nach Belieben vergroßern ¨ kann. Schließlich haben Sie noch die Moglichkeit ¨ einzustellen, an welcher Stelle das ¨ Fenster beim Offnen erscheinen soll. Als mogliche ¨ Positionen konnen ¨ hier unter anderem die Werte GTK WIN POS MOUSE (mittig unter dem Mauszeiger) oder GTK WIN POS CENTER (mittig auf dem Bildschirm) gew¨ahlt werden. void gtk_window_set_position(GtkWindow *window, GtkWindowPosition position);
Selbstverst¨andlich konnen ¨ 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);
Fur ¨ das Beispielprogramm konnte ¨ 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 ausgefuhrt ¨ werden, wenn Elemente des Fens-
8.2 Das Toolkit GTK+
211
ters angeklickt werden. Wird ein Eingabeelement – z. B. eine Schaltfl¨ache – bedient, lost ¨ 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¨achsten 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 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 verknupfen ¨ wir das GTK-Objekt des Fensters mit der Funktion gtk main quit(). Beachten Sie, dass hier eine Funktion als Parameter ubergeben ¨ wird! Mit dem zweiten Parameter ("delete_event") teilen wir der Funktion gtk signal connect() mit, welches Ereignis (Event) die Callback-Funktion auslosen ¨ soll – in diesem Fall das Loschen“ ¨ (Schließen) des Fensters. ” Wenn Sie den Typ des Events in Zeile 16 in key press event“ a¨ ndern, wird das ” Fenster bei einem Tastendruck geschlossen. Der Event-Typ focus out event“ ” fuhrt ¨ dagegen zum Schließen des Fensters, sobald es inaktiv wird.
212
8 Grafische Benutzeroberfl¨achen
Der letzte Parameter der Funktion gtk signal connect() ermoglicht ¨ es, zus¨atzliche beliebige Daten an die Callback-Funktion zu ubergeben: ¨ gint gtk_signal_connect(GtkObject *object, gchar *name, GtkSignalFunc func, gpointer func_data);
Als Ruckgabewert ¨ liefert die Funktion eine ID, eine Art Merker, mit dem sich die Verknupfung ¨ zwischen dem Objekt und der Callback-Funktion eindeutig identifizieren l¨asst. Dieser Wert wird benotigt, ¨ wenn Sie diese Verknupfung ¨ sp¨ater 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¨ater auch fur ¨ 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 /*----- Callback-Funktion -----*/ gint close_win(GtkWidget *widget, GdkEvent *event, gpointer data) { printf("’delete’-Event ausgel¨ ost.\n"); return(FALSE); /* destroy-Event ausl¨ osen */ } /*----- 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¨alt als ersten Parameter das Widget, das die Funktion aufgerufen hat – in diesem Fall also das Fenster-Widget. Der zweite Parameter enth¨alt das Ereignis (Event), das den Aufruf der Funktion ausgelost ¨ hat, hier das Schließen des Fensters. Der dritte und letzte Parameter enth¨alt die optionalen Daten entsprechend dem vierten Parameter der Funktion gtk signal connect() (siehe oben). Davon machen wir aber zun¨achst noch keinen Gebrauch. Beendet wird die Callback-Funktion mit dem Ruckgabewert ¨ FALSE. Dadurch wird erreicht, dass das Signal nicht geloscht ¨ und somit automatisch im Anschluss an das delete“-Event ein destroy“-Event aus” ” gelost ¨ wird. Mit dem Wert TRUE wurde ¨ man das Signal loschen ¨ und kein weiteres Event auslosen. ¨ Sie konnen ¨ also uber ¨ den Ruckgabewert ¨ der Callback-Funktion entscheiden, ob das Fenster tats¨achlich geschlossen wird! Diese Moglichkeit ¨ wenden wir in Abschnitt 8.2.9 an. Im Hauptprogramm wird unsere Funktion close win() mit dem delete“-Event ” des Fensters verknupft ¨ (Zeile 28 und 29). Zus¨atzlich wird gtk main quit() als Callback-Funktion fur ¨ das destroy“-Event eingerichtet (Zeile 31 und 32). ” Selbstverst¨andlich ließe sich auch fur ¨ destroy“ wiederum eine eigene Callback” Funktion einrichten, falls vor dem Schließen s¨amtlicher Fenster noch Aktionen erforderlich sind.
8.2.5 Schaltfl¨achen (Buttons) Viele GTK-Objekte – z. B. die Fenster – enthalten einen so genannten Container, in den ein weiteres Objekt – z. B. eine Schaltfl¨ache – eingebaut werden kann. Auf diese Weise werden die GTK-Objekte verschachtelt oder gestapelt. Im folgenden Beispielprogramm haben wir eine Schaltfl¨ache zum Beenden des Fensters eingebaut. 1 2 3
/* ache zum Beenden button.c - Schaltfl¨ */
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¨achen
# include <stdio.h> # include /*----- Callback-Funktionen -----*/ gint close_win(GtkWidget *widget, GdkEvent *event, gpointer data) { printf("Bitte klicken Sie auf ’Beenden’.\n"); return(TRUE); /* Signal l¨ oschen -> 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¨ ache 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¨ache. 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¨achst wieder zwei Callback-Funktionen definiert: eine fur ¨ das Schließen“-Symbol des Fensters und die zweite fur ¨ unse” re Schaltfl¨ache. Beachten Sie bitte, dass die Parameter beider Funktionen unterschiedlich sind! Das liegt an den unterschiedlichen Signalen, die die CallbackFunktionen auslosen. ¨ Durch den Ruckgabewert ¨ 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¨ache 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 fur ¨ das Schließen des Fensters eingestellt (Zeile 35 und 36). In Zeile 38 ver¨andern wir die Eigenschaften des Containers (siehe oben) des Fensters. In diesem Fall vergroß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¨ache (Button) und verknupfen ¨ sie anschließend mit der Callback-Funktion quit proc() (Zeile 45 und 46). Jetzt muss die Schaltfl¨ache noch in den Container des Fensters eingebaut werden. Dies geschieht in Zeile 48. Als letzten Schritt mussen ¨ wir noch beide GTK-Widgets – das Fenster und die Schaltfl¨ache – mit gtk widget show() in den Zeilen 52 und 53 sichtbar machen.
216
8 Grafische Benutzeroberfl¨achen
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 Moglichkeit, ¨ GTK-Objekte mit Hinweistexten oder Tipps zur Benutzung zu versehen. Diese so genannten Tooltips werden automatisch angezeigt, wenn der Anwender mit dem Mauszeiger uber ¨ dem Objekt verharrt (siehe Abbildung 8.3). Wenn Sie in das Programm button.c“ aus dem vorigen Abschnitt folgende Zeilen ” einfugen ¨ (ab Zeile 49), wird die Schaltfl¨ache mit einem entsprechenden Hinweistext versehen: GtkTooltips* tooltips = gtk_tooltips_new(); gtk_tooltips_set_tip(tooltips, button, "Zum Beenden hier klicken.", NULL);
¨ kontextDer vierte Parameter der Funktion gtk tooltips set tip() kann fur sensitive Hilfefunktionen verwendet oder – falls nicht benotigt ¨ – auf NULL gesetzt werden.
Abbildung 8.3: Schaltfl¨ache mit Hinweistext (Tooltips)
8.2.7 Widgets anordnen Im vorigen Abschnitt haben wir in den Container eines Fensters eine Schaltfl¨ache eingebaut. Damit ist dieser Container belegt – weitere Elemente lassen sich nicht hinzufugen! ¨ Doch wie bekommt man mehr als ein Objekt in das Fenster, z. B. mehrere Schaltfl¨achen? Dazu mussen ¨ 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 eingefugt ¨ werden konnen: ¨ 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 Ruckgabewert ¨ 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. Hohe ¨ haben sollen. Mit dem Parameter spacing konnen ¨ Sie einen Abstand (in Pixel) zwischen den Objekten einstellen. Nachdem Sie eine solche Box erzeugt haben, konnen ¨ Sie darin Schaltfl¨achen oder andere Widgets einfugen, ¨ 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 eingefugt ¨ werden soll, gefolgt von dem einzufugenden ¨ Element. Der Parameter expand kann TRUE oder FALSE sein und bestimmt, ob die Box automatisch auf die Fenstergroße ¨ ausgedehnt wird. Mit dem vierten Parameter (fill) konnen ¨ Sie einstellen, ob die Große ¨ des einzufugenden ¨ Objekts mit der Große ¨ der Box ver¨andert 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 Moglichkeiten ¨ zu bekommen, empfiehlt es sich, einfach mal verschiedene Kombinationen fur ¨ die Parameter expand, fill und padding auszuprobieren und dabei auch den Wert fur ¨ homogeneous beim Erzeugen der Box zu modifizieren. Bitte beachten Sie, dass Sie auch eine Box als Element in eine andere Box einfugen ¨ konnen, ¨ so dass beliebige Kombinationen von Zeilen und Spalten moglich ¨ sind! Bevor wir die Verwendung dieser Funktionen anhand eines Beispiels erl¨autern, mochten ¨ 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¨alt einen Container, in den ein anderes Widget – beispielsweise eine Box – eingefugt ¨ werden kann, und zeichnet einen Rahmen darum. Dieser Rahmen wird mit dem als Argument label angegebenen Text beschriftet.
218
8 Grafische Benutzeroberfl¨achen
Das folgende Beispiel offnet ¨ ein Fenster, das eine vertikale Box enth¨alt, die wiederum eine horizontale Box und eine einzelne Schaltfl¨ache beinhaltet. Die horizontale Box umfasst vier Schaltfl¨achen (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¨ achen mit vbox und hbox anordnen */ # include <stdio.h> # include /*----- 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¨ atigt.\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¨ ur 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¨ ur 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¨ achen erzeugen */ for (i=0; ivbox), label, TRUE, TRUE, 20); gtk_widget_show(label); /* Schaltfl¨ achen f¨ ur 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¨ ache f¨ ur 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 fur ¨ 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¨atigung der Schaltfl¨ache Abbrechen“ im Dialogfenster.
”
quit proc() bei Bet¨atigung der Schaltfl¨ache Beenden“ im Hauptfenster.
”
Abbildung 8.5: Sicherheitsabfrage mit Hilfe eines Dialogfensters
224
8 Grafische Benutzeroberfl¨achen
Im Hauptprogramm wird nach dem Hauptfenster auch gleich das Dialogfenster erzeugt (Zeile 49), obwohl dieses ja erst beim Schließen des Hauptfensters geoffnet ¨ werden soll. Der Trick besteht darin, dass der Dialog bereits vollst¨andig mit Text und Schaltfl¨achen 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¨achen verwendet. GTK kennt daruber ¨ hinaus spezielle Schaltfl¨achen, 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 /*----- 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); achen erzeugen */ /* Toggle- und Check-Schaltfl¨ 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¨achen
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¨ ugen */ separator = gtk_hseparator_new(); gtk_box_pack_start(GTK_BOX(vbox), separator, FALSE, FALSE, 0); ache zum Beenden */ /* Schaltfl¨ 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 fur ¨ alle Auswahlfelder die gleiche CallbackFunktion button proc() in Zeile 17 bis 24. Anhand des Parameters data konnen ¨ wir unterscheiden, welches Element der Benutzer angew¨ahlt hat. Dazu haben wir bei der Verknupfung ¨ der Objekte mit der Callback-Funktion jeweils einen anderen Text als Kennung an die Funktion gtk signal connect() ubergeben ¨ (Zeile 53, 60, 70 und 80). Die Auswahlfelder vom Typ Toggle-Button und Check-Button sind den bereits vorgestellten Schaltfl¨achen a¨ hnlich, bleiben jedoch beim Anklicken so lange aktiv“, ” bis sie erneut angeklickt werden. In der Callback-Funktion konnen ¨ 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¨alt es sich mit den so genannten Radio-Buttons. Diese Widgets werden zu einer Gruppe (GSList *group) verkupft, ¨ 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 hinzugefugt ¨ werden soll (Zeile 66 und 67). In Zeile 74 erzeugen wir dann die neue Gruppe und fugen ¨ den ersten Radio-Button hinzu. Fur ¨ den zweiten Radio-Button geben wir anschließend direkt die Gruppe an, zu der er hinzugefugt ¨ werden soll (Zeile 76 und 77). Bei Bet¨atigung 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. Naturlich ¨ konnen ¨ 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¨achen
8.2.11 Eingabefelder fur ¨ Text und Zahlen Neben den Auswahl-Buttons, die nur ein- oder ausgeschaltet werden konnen, ¨ 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 /*----- 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¨asst sich nur mit GTK 2.0 ubersetzen. ¨ Fur ¨ GTK 1.2 muss das Widget GtkTextView durch die a¨ 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¨ oschen! */ } /*----- 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¨achen
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); ache zum Beenden */ /* Schaltfl¨ 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 Offnen des Fensters zu sehen sind (siehe Abbildung 8.7).
Abbildung 8.7: Eingabefelder fur ¨ Text und Zahlen
H¨aufig werden Texteingabefelder erst ausgewertet, wenn eine Schaltfl¨ache bet¨atigt wird, z. B. bei einem Feld fur ¨ einen Dateinamen kombiniert mit einer Schaltfl¨ache Speichern unter“. In unserem Beispiel haben wir aber auch die Eingabe” felder mit Callback-Funktionen verknupft, ¨ 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¨andert wird. Dies erreichen wir durch die Verknupfung ¨ mit dem Signal changed“ in Zeile 73 und 80. Mit Hilfe der Funktion ” ¨ gtk entry get text() geben wir bei jeder Anderung 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 fur ¨ das Zahlenfeld (in GTK als Spin-Button bezeichnet) dient. Den Parameter data verwenden wir, um der Funktion einen Zeiger auf den Spin-Button zu ubergeben. ¨ Das Zahlenfeld selbst wird in den Zeilen 88 bis 94 erzeugt und eingerichtet. Fur ¨ einen Spin-Button benotigen ¨ wir ein so genanntes Adjustment, das in der Zeile 88 mit der Funktion
232
8 Grafische Benutzeroberfl¨achen
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 Offnen des Fensters), den minimalen und den maximalen Wert des Feldes an. Beachten Sie bitte, dass es sich um Fließkommazahlen handelt; Sie konnen ¨ dieses GTKElement also nicht nur fur ¨ ganzzahlige Eingabefelder verwenden. Der vierte Parameter gibt an, um wie viel der Wert erhoht/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 funften ¨ Parameter angegebenen Betrag vergroß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 verknupft. ¨ Der zweite Parameter der Funktion gkt spin button new() gibt an, wie stark das Hoch- und Runterz¨ahlen des Feldes beschleunigt wird, wenn der Benutzer auf die Pfeiltasten klickt und die Maustaste gedruckt ¨ h¨alt. Der Wertebereich fur ¨ 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 verknupft ¨ wird (Zeile 91)! Fur ¨ das mehrzeilige Textfeld erzeugen wir ein TextView-Widget (Zeile 98) und verknupfen ¨ 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 Große ¨ hat – a¨ 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 -hohe ¨ (Zeile 101). Diese Funktion l¨asst sich auch auf andere Widgets anwenden – z. B. auf Schaltfl¨achen. Wenn Sie das mehrzeilige Textfeld nur fur ¨ Ausgaben (z. B. Statusinformationen) nutzen wollen, mussen ¨ 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 fur ¨ die Eingaben angelegt. Bevor wir Text in das Feld schreiben konnen, ¨ mussen ¨ 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 geloscht ¨ und durch den als zweiten Parameter angegebenen Text ersetzt werden (Zeile 106). Der dritte Parameter gibt die L¨ange des neuen Textes an. Ist sie –1, wird sie automatisch ermittelt. Soll der bereits vorhandene Text im Puffer erhalten bleiben und nur etwas hinzugefugt ¨ werden, so konnen ¨ 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 uber ¨ 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 Menus ¨ Zu den wichtigsten Elementen einer grafischen Benutzeroberfl¨ache gehoren ¨ Pulldown-Menus. ¨ Ein typisches Menu¨ umfasst bei GTK die in Abbildung 8.8 dargestellten Elemente. Beachten Sie, dass die Elemente in der Menuleiste ¨ vom gleichen Typ (MenuItem) sind wie die einzelnen Unterpunkte eines Menus. ¨
234
8 Grafische Benutzeroberfl¨achen
MenuBar MenuItem Menu
Abbildung 8.8: Die Elemente eines GTK-Menus ¨
Im folgenden Programm realisieren wir eine Menuleiste ¨ (MenuBar) mit einem Menu, ¨ 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
/* uleiste menu.c - Fenster mit Men¨ */ # include <stdio.h> # include <string.h> # include GtkWidget *label;
/* f¨ ur Callback sichtbar! */
/*----- Callback-Funktion -----*/ void menu_proc(GtkWidget *widget, gpointer data) { static char buffer[40]; sprintf(buffer, "Men¨ upunkt ’%s’ gew¨ ahlt.", (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¨ us 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¨achen
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¨ upunkt gew¨ ahlt."); 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 fur ¨ die Menupunkte ¨ definiert. In dieser Funktion wird der aufgerufene Menupunkt ¨ 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 Menuzeile ¨ und einem Menu¨
Zur Erstellung des Menus ¨ erzeugen wir in dem Hauptprogramm zuerst die Menuleiste ¨ (Zeile 46) und dann das Menu¨ (Zeile 50). In Zeile 52 bis 67 werden die drei Menupunkte ¨ mit gtk menu item new() angelegt und mit der Funktion ¨ In Zeile 69 erzeugen wir gtk menu append() dem Pulldown-Menu¨ hinzugefugt. ¨ schließlich das Menu-Item ¨ mit dem Text Datei“ als Uberschrift und verknupfen ¨ ” es in Zeile 70 und 71 mit dem Pulldown-Menu. ¨ Als letzten Schritt fugen ¨ wir dieses Menu-Item ¨ dann in die Menuleiste ¨ ein (Zeile 72).
8.2 Das Toolkit GTK+
237
Trennlinien zwischen Menupunkten ¨ Manchmal sind Menus ¨ fur ¨ den Anwender ubersichtlicher, ¨ wenn die Menupunkte ¨ thematisch gruppiert und die Gruppen durch Trennlinien abgegrenzt sind. Ab Version 2.0 bietet GTK dafur ¨ ein spezielles Widget: GtkSeparatorMenuItem. Dieses Widget wird in gleicher Weise wie ein Menupunkt ¨ (MenuItem) in das Pulldown-Menu¨ eingefugt. ¨ Um dies am vorigen Programmbeispiel zu demonstrieren, fugen ¨ wir ab Zeile 63 folgende Quelltextzeilen ein: menuitem = gtk_separator_menu_item_new(); gtk_menu_append(GTK_MENU(menu), menuitem);
Dadurch wird der letzte Menupunkt ¨ ( Beenden“) von den anderen beiden durch ” eine Trennline abgegrenzt (siehe Abbildung 8.10).
Abbildung 8.10: Menu¨ mit Trennlinie (Separator)
Menupunkte ¨ und Schaltfl¨achen deaktivieren Unter Umst¨anden ist es erforderlich, dass bestimmte Menupunkte ¨ oder Schaltfl¨achen fur ¨ den Anwender gesperrt sind. GTK stellt auch fur ¨ 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¨ahlt werden kann. Um beispielsweise die Schaltfl¨ache 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¨achen
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 fur ¨ die Verarbeitung von farbigen Bildern wie beispielsweise Icons. Diese Strukturen eignen sich aber nicht fur ¨ 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 fur ¨ die Grafikdaten: W¨ahrend 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 Moglichkeit, ¨ 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 gehort. ¨ Beim Erstellen der Pixmap berucksichtigen ¨ beide Funktionen die Eigenschaften des X11-Displays bezuglich ¨ der Farbtiefe (Bits pro Pixel) und der Art der Farbverwaltung (feste Farbtabelle oder direkter Farbwert fur ¨ jeden Pixel). Daher benoti¨ gen die Funktionen als ersten Parameter das Grafikelement (Typ GdkWindow), in dem die Pixmap sp¨ater dargestellt werden soll. Ein solches GdkWindow ist Bestandteil der Struktur GtkWindow. Das XPM-Grafikformat unterstutzt ¨ Transparenz, d. h. ein Farbwert kann als durchsichtig“ definiert werden. Damit ist es moglich, ¨ dass Grafikelemente eine ” beliebige Kontur haben, wie dies bei Desktop-Icons ublich ¨ ist. Die Struktur Pixmap bietet jedoch nicht die Moglichkeit ¨ einer transparenten Farbe. Daher legen beide Funktionen zus¨atzlich 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 fur ¨ 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 fur ¨ diese Bereiche.
8.2 Das Toolkit GTK+
239
Hier ein Beispiel fur ¨ 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 unterstutzt ¨ 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 fur ¨ die transparenten Pixel beim Erzeugen der Pixmap gew¨ahlt haben, weil diese Pixel ohnehin ausgeblendet werden. Das folgende Programm zeigt diese Schritte im Zusammenhang: es erzeugt ein Fenster, l¨adt 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 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¨achen
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 uber ¨ die Bit-Tiefe und Farben-
8.2 Das Toolkit GTK+
241
zahl des X11-Displays erforderlich sind. Diese erh¨alt das Fenster-Widget aber erst mit Aufruf der Funktion gtk widget show().
Abbildung 8.11: XPM-Bilddateien anzeigen mit view xpm
Schaltfl¨achen und Menus ¨ 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¨achen und Menupunkte ¨ mit einer Grafik statt mit einem Textlabel einrichtet. Zun¨achst mussen ¨ wir zum Erzeugen der Schaltfl¨ache oder des Menupunkts ¨ jeweils eine andere Funktion w¨ahlen 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 benotigen ¨ wir eine GtkPixmap, die wir analog zum vorigen Beispiel aus einer XPM-Datei erzeugen konnen. ¨ Diese GtkPixmap fugen ¨ wir dann mit der bereits bekannten Funkti¨ on gtk container add() zum Container der Schaltfl¨ache oder des Menupunkts 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¨ache mit Text und Grafik versehen wollen, so mussen ¨ Sie in den Container der Schaltfl¨ache zuerst eine vertikale oder horizontale Box (siehe Abschnitt 8.2.7) einfugen, ¨ die Sie dann mit der Grafik und einem Label fullen. ¨ Das Gleiche gilt naturlich ¨ auch fur ¨ Menupunkte. ¨
242
8 Grafische Benutzeroberfl¨achen
Das folgende Programmbeispiel versieht eine Schaltfl¨ache mit einer Grafik und einem Textlabel (siehe Abbildung 8.12).
Abbildung 8.12: Schaltfl¨ache 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¨ ache mit Grafik und Text */ # include <stdio.h> # include 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) { atigt.\n"); ache ’neue Datei’ bet¨ printf("Schaltfl¨
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¨ ache 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¨achen
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¨achen 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. Fur ¨ diesen Zweck stellt GTK ein Widget als Zeichenfl¨ache“ zur Verfugung: ¨ 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 ubernimmt ¨ bei diesen Elementen nicht das Wiederherstellen des Inhalts, wenn ein Teil dieser Zeichenfl¨ache von einem anderen Fenster uberdeckt ¨ war. Das muss unser Programm ubernehmen. ¨ GTK stellt jedoch entsprechende Signale und Events zur Verfugung, ¨ 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 ubertragen ¨ nur die Bereiche der Pixmap auf die Zeichenfl¨ache, die es zu aktualisieren gilt. Auf diese Weise vermeiden wir, dass alle Zeichenfunktionen neu ausgefuhrt ¨ werden mussen, ¨ wenn eine kleine Ecke der Zeichenfl¨ache durch ein anderes Fenster verdeckt war. Die Ereignisse, die wir mit einer Callback-Funktion verknupfen ¨ mussen, ¨ sind das configure event“ und das ex” ” ¨ wenn das Fenster zum ersten pose event“. Das configure event wird ausgelost, Mal geoffnet ¨ oder die Große ¨ ver¨andert wird. Wurde ein Teil des Fensters verdeckt und dann wieder sichtbar, wird das expose event ausgelost. ¨ Beiden CallbackFunktionen ubergeben ¨ 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 /*----- 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¨ ur 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¨achen
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); un */ /* Vordergrundfarbe dunkelgr¨ 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¨ ache 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¨achen
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¨ache mit verschiedenen Grafikelementen
Betrachten wir zun¨achst 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¨alt die Funktion das Widget der Zeichenfl¨ache, das Event, das die Funktion ausgelost ¨ 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¨ache 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 Andern der Fenstergroße ¨ aufgerufen. In Zeile 18 uberpr ¨ ufen ¨ wir zun¨achst, ob schon eine Pixmap als Zwischenspeicher angelegt wurde. Wenn ja, wird diese wieder entfernt. Danach fragen wir die aktuelle Große ¨ der Zeichenfl¨ache ab und erzeugen eine neue Pixmap mit dieser Große ¨ (Zeile 23). In Zeile 25 und 26 zeichnen wir ein weiß ausgefulltes ¨ Rechteck uber ¨ die gesamte Zeichenfl¨ache und danach ein schwarzes Rechteck (nicht ausgefullt) ¨ als Umrandung der Zeichenfl¨ache (Zeile 27 und 28). Sehen wir uns diese Zeichenfunktion etwas n¨aher 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¨alt alle Informationen, die fur ¨ das Zeichnen erforderlich sind: Farbe, Strichst¨arke, Schriftart fur ¨ Text, Full¨ muster usw. Beim Erzeugen der Zeichenfl¨ache werden automatisch mehrere GCs angelegt und im Objekt hinterlegt – so auch white gc und blackgc. Fur ¨ die weiteren Zeichenfunktionen erzeugen wir in Zeile 32 einen neuen Graphics Context, um andere Farben und Strichst¨arken einstellen zu konnen. ¨ In Zeile 58 und 59 laden wir eine Schriftart fur ¨ 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) konnen ¨ Sie eine Schriftart ” w¨ahlen, deren Beschreibungs-String dann angezeigt wird.
¨ Abbildung 8.14: Ubersicht der Schriftarten unter X11 mit xfontsel“ ”
Neben den in unserem Programm verwendeten Zeichenfunktionen stellt GTK ¨ eine Reihe weiterer Funktionen zur Verfugung. ¨ Hier eine Ubersicht: 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 konnen ¨ Sie der Include-Datei “ ” entnehmen.
250
8 Grafische Benutzeroberfl¨achen
8.2.15 Zeichenfl¨ache mit Rollbalken Fur ¨ große Zeichenfl¨achen empfiehlt es sich, dass der Anwender das Fenster kleiner ziehen und mit Rollbalken (Scrollbars) am Rand der Zeichenfl¨ache den gewunschten ¨ Bereich anscrollen kann. Bei GTK erreichen Sie das mit Hilfe eines Scrolled Window, in das die Zeichenfl¨ache eingebettet wird. Das folgende Programm realisiert eine solche Zeichenfl¨ache mit fester Groß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
/* ache mit Rollbalken draw2.c - Zeichenfl¨ */ # include # define WIDTH 500 # define HEIGHT 500
/* Gr¨ oße der Zeichenfl¨ ache */
/*----- 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; istyle->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); ache erzeugen */ /* Zeichenfl¨ 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¨achen
gtk_widget_show_all(window); gtk_main(); return(0); }
Abbildung 8.15: Zeichenfl¨ache mit fester Große ¨ und Rollbalken
8.2.16 Dateiauswahlfenster Als letztes GTK-Objekt mochten ¨ wir ein Dateiauswahlfenster vorstellen. Dieses Widget wird mit GtkWidget *gtk_file_selection_new(const gchar *title);
erzeugt und enth¨alt verschiedene Schaltfl¨achen zum Loschen ¨ oder Umbenennen von Dateien sowie das Eingabefeld fur ¨ den Dateinamen und zwei Listen fur ¨ die Verzeichnisse und die Dateien (siehe Abbildung 8.16). Nach dem Erzeugen des Dateiauswahlfensters mit GtkWidget* filesel = gtk_file_selection_new("Datei laden");
konnen ¨ Sie auf die Schaltfl¨achen fur ¨ OK“ und fur ¨ Abbrechen“ als Elemente der ” ” GtkFileSelection-Struktur zugreifen: GTK_FILE_SELECTION(filesel)->ok_button GTK_FILE_SELECTION(filesel)->cancel_button
Beide Schaltfl¨achen mussen ¨ mit einer entsprechenden Callback-Funktion verknupft ¨ werden. Wenn der Benutzer den OK-Button bet¨atigt hat, konnen ¨ Sie den gew¨ahlten 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¨ache. Durch Anklicken dieser Schaltfl¨ache o¨ ffnet sich das Dateiauswahlfenster. W¨ahlt 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 /*----- 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¨achen
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¨ ache 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 Verknupfung ¨ 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 ubergeben: ¨ das slot object. Auf diese Weise konnen ¨ 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 benotigen ¨ wir nur eine einzige Callback-Funktion (Zeile 10 bis 17) fur ¨ 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¨achst jedoch nicht mit gtk widget show() sichtbar“ gemacht. Dies geschieht erst bei Bet¨atigung der ” Schaltfl¨ache Datei laden“. ”
8.2.17 Umlaute und Sonderzeichen Wie bereits in Abschnitt 8.2.1 erw¨ahnt, unterstutzt ¨ das Toolkit GTK+ in der Version 2.0 UTF-8, um Sonderzeichen wie deutsche Umlaute darzustellen. Daher mussen ¨ 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 konnen! ¨ So besteht das Wort fur“ ¨ 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¨autern, sondern lediglich einen Einstieg in das Thema geben. Eine vollst¨andige Beschreibung der vielen weiteren Moglichkeiten ¨ des Toolkits finden Sie in den Tutorials der GTK-Homepage [19] und in weiterfuhrender ¨ Literatur, z. B. [18].
256
8 Grafische Benutzeroberfl¨achen
8.3 Grafik ohne X11 mit der SVGALIB Fur ¨ Grafikausgaben unter Linux ist das X11-System der Standard. Nicht zuletzt aufgrund der Client-Server-Struktur und der Unterstutzung ¨ fur ¨ viele verschiedene Grafikkarten ist das System jedoch sehr m¨achtig – fur ¨ sehr kleine LinuxRechner wie Embedded Computer unter Umst¨anden zu m¨achtig. Mit Hilfe der Funktionsbibliothek libvga“ (das Paket wird auch als SVGALIB“ bezeichnet) ist ” ” eine einfache Grafikausgabe auch ohne das X11-System moglich. ¨ Allerdings sei darauf hingewiesen, dass die libvga naturlich ¨ kein gleichwertiger Ersatz fur ¨ X11 sein kann. Sie bietet nur einen begrenzten Funktionsumfang, beispielsweise fur ¨ das Zeichnen von Linien und das Einstellen der Farben. Es gibt aber mittlerweile sogar ein 3D-Toolkit fur ¨ 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 konnen, ¨ muss die libvga entsprechende Hardware-Zugriffe ausfuhren. ¨ Diese sind nor” malen“ Benutzern nicht gestattet, sodass Programme, die die libvga benutzen, zum Teil root-Privilegien benotigen ¨ (siehe auch Kapitel 9). Dies birgt naturgem¨aß 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 uber ¨ das Device /dev/svga und benotigen ¨ dementsprechend auch nur die Zugriffsrechte fur ¨ dieses Device. Des Weiteren ist zu beachten, dass bei Verwendung hochauflosender ¨ Grafikmodi auf nicht unterstutzten ¨ Grafikkarten der ursprungliche ¨ Textmodus moglicherwei¨ se nicht wiederhergestellt werden kann und so ein Neustart des Rechners erforderlich wird. Aus diesem Grund sollte man Programme fur ¨ die libvga auf Rechnern mit laufendem X11-System entwickeln, auch wenn dies zun¨achst widersinnig erscheint. In der Regel ist der X11-Server jedoch in der Lage, die Grafikeinstellungen fur ¨ die X11-Oberfl¨ache wiederherzustellen. Sollte ein auf die libvga aufsetzendes Programm nicht automatisch zur X11-Oberfl¨ache zuruckkehren, ¨ 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, konnen ¨ 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¨anden blind“ eingegeben werden, wenn ” der Textmodus zerstort ¨ 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, durfen ¨ 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 int main() { int i; if (vga_init()) return(1); if (vga_setmode(G640x480x16)) return(1); vga_clear(); for (i=0; i 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 Drucken ¨ 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 zuruck, ¨ ist der Initialisierungsvorgang gescheitert. In Zeile 15 wird die Grafikauflosung ¨ eingestellt, in diesem Fall 640 × 480 Punkte mit 16 Farben. Einige der moglichen ¨ anderen Modi sind:
1
Auf einer der getesteten Linux-Installationen ließen sich die libvga-Programme trotz dieser Einstellungen nur von dem Benutzer root“ ausfuhren. ¨ ”
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
Auflosung ¨
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 unterstutzt ¨ wird und den angegebenen Modus auch ermoglicht, ¨ sollten hohere ¨ Auflosungen ¨ 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¨asst 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¨asst sich uber ¨ 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 zuruck ¨ zum Beispielprogramm: Nach dem Einstellen des Grafikformates wird in Zeile 18 zun¨achst der Grafikspeicher geloscht. ¨ Danach werden mit vga drawline() Linien in der jeweils mit vga setcolor() eingestellten Farbe
260
8 Grafische Benutzeroberfl¨achen
gezeichnet. Bei einer Farbenzahl von bis zu 256 ist als Parameter der Farbindex einzusetzen – z.B. 0 fur ¨ 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 fur ¨ Rot, Grun ¨ und Blau direkt angegeben, wobei sich die Werte zwischen 0 und 63 bewegen mussen. ¨ Im 16-Farben-Modus sind die voreingestellten Farben: Schwarz, Dunkelblau, Dunkelgrun, ¨ Turkis, ¨ Dunkelrot, Violett, Braun, Hellgrau, Dunkelgrau, Blau, Grun, ¨ Hellturkis, ¨ 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 fur ¨ 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 # 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 # else # include # include # 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 # include
9.2 Ansteuerung des Parallelports
277
Parallelport 2 3 4 5 6 7 8 9 18 Abbildung 9.2: Schaltbild fur ¨ 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 # endif int get_lp_base(void) { int i, base_adr[3] = {0x3bc, 0x378, 0x278}; for (i=0; i include include if defined __GLIBC__ include <sys/io.h> else include 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 Offnen 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¨alt 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“ ubersetzt ¨ und mit root-Rechten ausgestattet werden ” (siehe Abschnitt 9.1.1). Danach kann es z.B. mit serialinfo /dev/ttyS0
fur ¨ 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¨ache 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-Ubertragung 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 uber ¨ 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 ermoglicht, ¨ HTML-Seiten und Grafiken uber ¨ Webbrowser abzurufen. Doch wie kann das Live-Videobild einer Kamera als bewegtes Bild“ dargestellt wer” den? Fur ¨ 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 Schlusselwort ¨ getrennt werden: ” HTTP/1.1 200 OK Content-type: multipart/x-mixed-replace;boundary=Schlusselwort ¨
Leerzeile --Schlusselwort ¨ Content-type: image/jpeg Content-length: Anzahl Bytes des 1. Bildes
Leerzeile Bilddaten (JPEG): 1. Bild Leerzeile --Schlusselwort ¨ 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 unterstutzen ¨ 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 ubersichtlicher ¨ 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 fur ¨ das Live-Video usbcam.c – Ansteuerung der USB-Kamera mit Speicherung der Einzelbilder als JPEG-Datei
¨ 10.1 WebCam: Video-Ubertragung per HTTP
285
Zu den letzten beiden Quelltexten gehort ¨ auch je eine Header-Datei ( .h“), in der ” die Funktionsaufrufe deklariert sind. Werfen wir zun¨achst einen Blick auf das Hauptprogramm webcam.c“: ” 1 /* 2 webcam.c - Webserver f¨ ur USB-Kamera 3 - Hauptteil 4 */ 5 6 # include <stdio.h> 7 # include <string.h> 8 # include <stdlib.h> 9 # include 10 # include <sys/types.h> 11 # include <sys/socket.h> 12 # include 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¨ ur 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 ¨ offnen 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-Ubertragung 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 ¨ offnen 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¨achst die Einstellungen des Programms mit Default-Werten belegt werden, folgt die Auswertung der Kommandozeilenparameter mit Hilfe der Funktion getopt() gem¨aß 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 Offnen 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¨atzlich 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 Ubertragung der Videodaten zum Tragen, die ja so lange l¨auft, 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 uber ¨ die gleiche Verbindung geschickt wer1 Aus diesem Grund werden hier in jedem Kind-Prozess die HTTPden konnen. ¨ Anfragen 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 Ruckgabewert ¨ von 1, wenn die Verbindung bestehen bleiben soll, und eine 0, wenn die Verbindung abgebrochen wurde. Sehen wir uns als N¨achstes den Quelltext http service.c“ an: ” 1 /* 2 http_service.c - HTTP-Server f¨ ur WebCam / 3 * 4 5 # include <stdio.h> 6 # include <string.h> 7 # include 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-Ubertragung 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) 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 "Falsche Kennung!"); 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 ¨ ubertragen -----*/ 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" "Error" "File not found." "/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) if (send(client_fd, buffer, length, 0) <sys/ioctl.h> <jpeglib.h>
# define WIDTH 320
oße QVGA */ /* Bildgr¨
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-Ubertragung 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 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 ubertragen ¨ 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 zuruckgewan¨ delt werden konnen ¨ ( 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 entschlusseln. ¨ Daher verwenden professionelle Internetanbieter fur ¨ die ¨ Ubertragung von Kennwortern ¨ immer eine verschlusselte ¨ Verbindung ( https“), ” bei der sich allein aus den uber ¨ das Netzwerk ubertragenen ¨ 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 uber ¨ einen zweiten Prozess und die zugehori¨ ge 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¨ahlhilfe“ ” reaktivieren konnen. ¨
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 ¨ zugehorige ¨ Telefonnummer enthalten. Ahnlich wie bei einem Mobiltelefon soll hier uber ¨ die Eingabe des Anfangsbuchstabens automatisch der erste passende Eintrag angesprungen“ werden; mit den Cursor-Tasten kann man sich in der ” Liste vor und zuruck ¨ bewegen. Durch Drucken ¨ der Enter-Taste wird die Telefonnummer des markierten Eintrags mit einem analogen Telefonmodem gew¨ahlt. W¨ahrend des W¨ahlvorgangs, der bei den meisten Modems akustisch ausgegeben wird, kann man den Horer ¨ eines nachgeschalteten“ Telefons (siehe Abbil” dung 10.2) abnehmen. Das Modem legt nach Anwahl der Nummer automatisch auf und ubergibt ¨ so die aufgebaute Verbindung an das Telefon.1
N F N
Abbildung 10.2: Anschluss von Analog-Modem und Telefon.
1
Voraussetzung dafur ¨ ist, dass das Modem uber ¨ eine 4-adrige Leitung angeschlossen ist.
10.2 Telefonbuch mit automatischer Anwahl
301
10.2.2 Strukturierung des Projektes Auch dieses Beispielprojekt l¨asst 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¨agen und je zwei Zeichenketten mit MAX CHARS Zeichen pro Eintrag definiert. Beide Konstanten sind in telefonbuch.h festgelegt. Die erste Zeichenkette jedes Eintrags enth¨alt 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 stoßt. ¨ Die Funktion read phonebook() (Zeile 34 bis 76) ruft zun¨achst mit der Funktion popen() das Shell-Programm sort“ auf; als Kommandozeilenparameter wird ” der Dateiname der Telefonliste ubergeben. ¨ Als Ruckgabewert ¨ 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¨age der Telefonliste in das Feld phonebook eingelesen. Die Funktion read phonebook() wird danach beendet und gibt die Anzahl der eingelesenen Eintr¨age zuruck. ¨ In der Funktion main() (ab Zeile 79) werden zun¨achst die Kommandozeilenparameter gepruft ¨ und anschließend die Telefonliste eingelesen. Danach wird die Schnittstelle zum Modem geoffnet ¨ und das Modem (sicherheitshalber) zuruckge¨ setzt (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¨ahnt, wird neben telefonbuch.c“ auch eine Include-Datei te” ” lefonbuch.h“ benotigt, ¨ 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 uber ¨ 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 eingefuhrt ¨ und haben sich als De-facto-Standard etabliert – man spricht auch vom Hayes-Standard“ und von Hayes-kompatibel“. Tats¨achlich gibt ” ” es Unterschiede zwischen den Modems der verschiedenen Hersteller. Die hier verwendeten Befehle sollte dennoch jedes Modem verstehen. Fur ¨ weitere Informationen schauen Sie in das Handbuch Ihres Modems oder auf die im Anhang angegebene Internet-Seite. Sie konnen ¨ 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> <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) "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= 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 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¨age im Telefonbuch an. Es ist zu beachten, dass die Funktionen erase() (Zeile 19) und printw() der libncurses zun¨achst 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 ubertragen. ¨ 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 fur ¨ drei Sekunden den in message angegebenen Text in einem eingerahmten Fenster dar. Nach dem Loschen ¨ dieses Fensters mit delwin() in Zeile 62 muss dem darunterliegenden Hauptfenster signalisiert werden, welche Zeilen durch das zweite Fenster uberschrieben ¨ 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¨achst 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 moglich: ¨ Eingabe
Funktion
’A’–’Z’ ↓, ↑ PgDn, PgUp Enter Esc
ersten Eintrag mit diesem Anfangsbuchstaben selektieren n¨achsten bzw. vorhergehenden Eintrag selektieren eine Seite vor- bzw. zuruckbl¨ ¨ attern selektierte Telefonnummer anw¨ahlen Programm beenden
Von den in eingabe.c“ definierten Funktionen wird lediglich die Hauptfunkti” on, also eingabe(), im Hauptprogramm verwendet. Dementsprechend f¨allt 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¨achsten 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¨asst: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# # Makefile f¨ ur ’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 $