Text
                    martin GRÄFE

C UND
LINUX
DIE MÖGLICHKEITEN DES
BETRIEBSSYSTEMS MIT EIGENEN
PROGRAMMEN NUTZEN

4. Auflage


Gräfe C und Linux C v Bleiben Sie einfach auf dem Laufenden: www.hanser.de/newsletter Sofort anmelden und Monat für Monat die neuesten Infos und Updates erhalten.

Martin Gräfe C und Linux Die Möglichkeiten des Betriebssystems mit eigenen Programmen nutzen 4., vollständig überarbeitete und erweiterte Auflage
Dr.-Ing. Martin Gräfe, geboren 1968 in Hagen, studierte Elektrotechnik an der Universität Dortmund. Dort war er nach Abschluss des Studiums als wissenschaftlicher Mitarbeiter tätig und promovierte 1998 auf dem Gebiet der Mikroelektronik. Bereits während des Studiums befasste sich Martin Gräfe mit C-Programmierung unter Unix und seit 1995 schließlich auch mit Linux. Im Rahmen seiner Promotion und seiner Tätigkeit als Ingenieur entwickelte er verschiedene Programme zur Simulation elektronischer Schaltungen und Übertragungssysteme. Alle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestem Wissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mit keiner Verpflichtung oder Garantie irgendeiner Art verbunden. Autoren und Verlag übernehmen infolgedessen keine juristische Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen – oder Teilen davon – entsteht, auch nicht für die Verletzung von Patentrechten und anderen Rechten Dritter, die daraus resultieren könnten. Autoren und Verlag übernehmen deshalb keine Gewähr dafür, dass die beschriebenen Verfahren frei von Schutzrechten Dritter sind. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Buch berechtigt deshalb auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Bibliografische Information der Deutschen Nationalbibliothek: Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar. Dieses Werk ist urheberrechtlich geschützt. Alle Rechte, auch die der Übersetzung, des Nachdruckes und der Vervielfältigung des Buches, oder Teilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung des Verlages in irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren) – auch nicht für Zwecke der Unterrichtsgestaltung – reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. © 2010 Carl Hanser Verlag München Wien (www.hanser.de) Lektorat: Margarete Metzger Herstellung: Irene Weilhart Copy editing: Manfred Sommer, München Umschlagdesign: Marc Müller-Bremer, www.rebranding.de, München Umschlagrealisation: Stephan Rönigk Datenbelichtung, Druck und Bindung: Kösel, Krugzell Ausstattung patentrechtlich geschützt. Kösel FD 351, Patent-Nr. 0748702 Printed in Germany ISBN 978-3-446-42176-9
Inhaltsverzeichnis 1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1 1.2 1 Warum gerade C“? . . . . . . . . . . . . . . . . . . . . . . . . . . . . ” Bevor es losgeht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 1.2.1 1.2.2 Paketverwaltung unter SuSE-Linux . . . . . . . . . . . . . . Paketinstallation bei Ubuntu . . . . . . . . . . . . . . . . . . 2 4 Die Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 Der Editor – die Qual der Wahl . . . . . . . . . . . . . . . . . 6 6 1.3.2 Der GNU C-Compiler gcc . . . . . . . . . . . . . . . . . . . . 8 1.3.3 1.3.4 Ablaufsteuerung mit GNU make . . . . . . . . . . . . . . . . Für die Fehlersuche: Die Debugger . . . . . . . . . . . . . . . 8 10 1.3.5 Integrierte Entwicklungsumgebungen . . . . . . . . . . . . . Der Umgang mit Compiler, Debugger und make“ anhand von Bei” spielen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.1 Primzahlen berechnen . . . . . . . . . . . . . . . . . . . . . . 11 14 14 1.4.2 1.4.3 Fehlersuche mit dem gcc . . . . . . . . . . . . . . . . . . . . . Fehlersuche mit dem GNU Debugger . . . . . . . . . . . . . 16 17 1.4.4 1.4.5 Funktionsbibliotheken verwenden . . . . . . . . . . . . . . . Quelltexte aufteilen . . . . . . . . . . . . . . . . . . . . . . . . 19 21 Weiterführende Informationen . . . . . . . . . . . . . . . . . . . . . 1.5.1 Die Unix-Online-Hilfen man“, xman“ und tkman“ . . . . ” ” ” 1.5.2 Ein Blick hinter die Kulissen: Die Include-Dateien . . . . . . 25 26 2 Arbeiten mit einer Entwicklungsumgebung . . . . . . . . . . . . . . . . 2.1 Anjuta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 1.3 1.4 1.5 2.1.1 2.1.2 Ein neues Projekt anlegen . . . . . . . . . . . . . . . . . . . . Eingabe der Quelltexte . . . . . . . . . . . . . . . . . . . . . . 28 31 33
VI Inhaltsverzeichnis 2.1.3 Kompilieren und Starten des Beispiels . . . . . . . . . . . . . KDevelop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 36 Eclipse + C Development Tooling (CDT) . . . . . . . . . . . . . . . . 2.3.1 Plug-ins einbinden . . . . . . . . . . . . . . . . . . . . . . . . 39 40 2.3.2 Ein neues Projekt anlegen . . . . . . . . . . . . . . . . . . . . 40 3 Kommandozeilenprogramme . . . . . . . . . . . . . . . . . . . . . . . . . 43 2.2 2.3 3.1 Parameter und Rückgabewert der Funktion main() . . . . . . . . . 3.1.1 Die Bedeutung des Rückgabewertes von main() . . . . . . 43 44 3.1.2 3.1.3 Die Variablen argc und argv . . . . . . . . . . . . . . . . . . Auswerten der Kommandozeilenparameter . . . . . . . . . 44 45 3.1.4 Achtung: Platzhalter! . . . . . . . . . . . . . . . . . . . . . . . Konventionen für Kommandozeilenprogramme . . . . . . . . . . . 47 48 3.2.1 3.2.2 Ein Muss: Die Hilfe-Option . . . . . . . . . . . . . . . . . . . Fehlermeldungen . . . . . . . . . . . . . . . . . . . . . . . . . 48 50 3.2.3 Eigene Manpages erstellen . . . . . . . . . . . . . . . . . . . . 51 Programme mehrsprachig auslegen . . . . . . . . . . . . . . . . . . Ausgabesteuerung im Terminal-Fenster . . . . . . . . . . . . . . . . 53 60 3.4.1 3.4.2 ANSI-Steuersequenzen . . . . . . . . . . . . . . . . . . . . . Die ncurses“-Bibliothek . . . . . . . . . . . . . . . . . . . . . ” 60 61 4 Dateien und Verzeichnisse . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1 Die Arbeit mit Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . 67 67 3.2 3.3 3.4 4.1.1 4.1.2 Gepufferte Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . stdin, stdout und stderr . . . . . . . . . . . . . . . . . . . 67 68 4.1.3 4.1.4 Dateien öffnen und schließen . . . . . . . . . . . . . . . . . . Lesen aus und Schreiben in Dateien . . . . . . . . . . . . . . 69 70 4.2 4.1.5 Ein Beispiel: Zeilen nummerieren . . . . . . . . . . . . . . . Eigenschaften von Dateien oder Verzeichnissen auswerten . . . . . 74 75 4.3 Verzeichnisse einlesen . . . . . . . . . . . . . . . . . . . . . . . . . . 77 5 Interprozesskommunikation . . . . . . . . . . . . . . . . . . . . . . . . . 5.1 Prozessverwaltung unter Linux . . . . . . . . . . . . . . . . . . . . . 79 79 5.2 Neue Prozesse starten . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.1 Shell-Programme aufrufen mit system() . . . . . . . . . . . 80 80 5.2.2 81 Die Funktionen der exec-Familie . . . . . . . . . . . . . . .
Inhaltsverzeichnis 5.2.3 5.2.4 5.3 5.4 5.5 VII Einen Kind-Prozess erzeugen mit fork() . . . . . . . . . . . Warteschleifen . . . . . . . . . . . . . . . . . . . . . . . . . . 82 85 Signale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.1 Die Weckfunktion alarm() . . . . . . . . . . . . . . . . . . . 86 87 5.3.2 5.3.3 Einen Signal-Handler einrichten . . . . . . . . . . . . . . . . Auf die Beendigung eines Kind-Prozesses warten . . . . . . 88 89 5.3.4 Signale setzen mit kill() . . . . . . . . . . . . . . . . . . . . Datenaustausch zwischen Prozessen . . . . . . . . . . . . . . . . . . 90 91 5.4.1 5.4.2 91 95 Pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . FIFOs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4.3 Shared Memory . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Alternative Verfahren zur Erzeugung von Prozessen . . . . . . . . . 100 5.5.1 5.5.2 popen() und pclose() . . . . . . . . . . . . . . . . . . . . . 100 5.5.3 POSIX-Threads . . . . . . . . . . . . . . . . . . . . . . . . . . 103 Die fork()-Alternative clone() . . . . . . . . . . . . . . . 101 6 Devices – das Tor zur Hardware . . . . . . . . . . . . . . . . . . . . . . . 107 6.1 Das Device-Konzept von Linux . . . . . . . . . . . . . . . . . . . . . 107 6.1.1 6.1.2 6.2 6.1.3 Devices steuern mit ioctl() . . . . . . . . . . . . . . . . . . 110 Das CD-ROM-Laufwerk . . . . . . . . . . . . . . . . . . . . . . . . . 111 6.2.1 6.2.2 6.3 6.5 OSS, ALSA und ESOUND . . . . . . . . . . . . . . . . . . . . 122 Der Mixer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 6.3.3 Audiodaten aufnehmen und wiedergeben . . . . . . . . . . 126 Video for Linux“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 ” 6.4.1 Eigenschaften des Devices . . . . . . . . . . . . . . . . . . . . 130 6.4.2 Bilder aufzeichnen . . . . . . . . . . . . . . . . . . . . . . . . 133 Die serielle Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . . 142 6.5.1 6.5.2 6.6 Die CD auswerfen“ . . . . . . . . . . . . . . . . . . . . . . . 111 ” Fähigkeiten des Laufwerks auslesen . . . . . . . . . . . . . . 112 6.2.3 Audio-CDs abspielen . . . . . . . . . . . . . . . . . . . . . . 114 Ansteuerung einer Soundkarte . . . . . . . . . . . . . . . . . . . . . 121 6.3.1 6.3.2 6.4 Devices öffnen und schließen . . . . . . . . . . . . . . . . . . 108 Ungepuffertes Lesen und Schreiben . . . . . . . . . . . . . . 109 Terminal-Parameter einstellen . . . . . . . . . . . . . . . . . 143 Ein kleines Terminalprogramm . . . . . . . . . . . . . . . . . 145 Druckerausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
VIII 6.7 Inhaltsverzeichnis Der Universal Serial Bus (USB) . . . . . . . . . . . . . . . . . . . . . . 154 6.7.1 Ansteuerung von USB-Geräten anhand eines Beispiels . . . 156 7 Netzwerkprogrammierung . . . . . . . . . . . . . . . . . . . . . . . . . . 163 7.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 7.2 7.1.1 7.1.2 Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 Vorbereitung . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 7.1.3 7.1.4 Das Client-Server-Prinzip . . . . . . . . . . . . . . . . . . . . 169 Sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 Der TCP/IP-Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 7.2.1 Aufbau einer Verbindung . . . . . . . . . . . . . . . . . . . . 171 7.2.2 7.2.3 7.3 Server-Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 7.3.1 Die Funktionsweise eines Servers . . . . . . . . . . . . . . . 178 7.3.2 7.4 7.5 Ein Universal“-Client . . . . . . . . . . . . . . . . . . . . . . 173 ” Rechnernamen in IP-Adressen umwandeln . . . . . . . . . . 176 Ein interaktiver TCP/IP-Server . . . . . . . . . . . . . . . . . 180 7.3.3 Ein kleiner Webserver . . . . . . . . . . . . . . . . . . . . . . 184 Das User Datagram Protocol (UDP) . . . . . . . . . . . . . . . . . . . . 191 7.4.1 7.4.2 UDP-Nachrichten senden . . . . . . . . . . . . . . . . . . . . 191 Der UDP-Server . . . . . . . . . . . . . . . . . . . . . . . . . . 194 7.4.3 7.4.4 Pakete an alle Teilnehmer senden: Broadcast . . . . . . . . . 197 Multicast-Sockets . . . . . . . . . . . . . . . . . . . . . . . . . 199 7.4.5 UPnP – Universal Plug And Play . . . . . . . . . . . . . . . . . 200 Noch ein Wort zur Sicherheit . . . . . . . . . . . . . . . . . . . . . . 204 8 Grafische Benutzeroberflächen . . . . . . . . . . . . . . . . . . . . . . . . 205 8.1 Die grafische Oberfläche X11 . . . . . . . . . . . . . . . . . . . . . . 205 8.2 Das Toolkit GTK+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 8.2.1 GTK 1.2 versus GTK 2.0 . . . . . . . . . . . . . . . . . . . . . 206 8.2.2 GTK-Programme übersetzen . . . . . . . . . . . . . . . . . . 207 8.2.3 8.2.4 Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 208 Das Callback-Prinzip . . . . . . . . . . . . . . . . . . . . . . . 210 8.2.5 8.2.6 Schaltflächen (Buttons) . . . . . . . . . . . . . . . . . . . . . . 213 Hinweistexte (Tipps) . . . . . . . . . . . . . . . . . . . . . . . 216 8.2.7 8.2.8 Widgets anordnen . . . . . . . . . . . . . . . . . . . . . . . . 216 Text-Labels . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Inhaltsverzeichnis IX 8.2.9 Dialogfenster . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 8.2.10 Auswahlfelder . . . . . . . . . . . . . . . . . . . . . . . . . . 224 8.2.11 Eingabefelder für Text und Zahlen . . . . . . . . . . . . . . . 228 8.2.12 Menüs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 8.2.13 Pixmap-Grafiken darstellen . . . . . . . . . . . . . . . . . . . 238 8.2.14 Zeichenflächen . . . . . . . . . . . . . . . . . . . . . . . . . . 244 8.2.15 Zeichenfläche mit Rollbalken . . . . . . . . . . . . . . . . . . 250 8.2.16 Dateiauswahlfenster . . . . . . . . . . . . . . . . . . . . . . . 252 8.2.17 Umlaute und Sonderzeichen . . . . . . . . . . . . . . . . . . 255 8.2.18 Wie geht es weiter? . . . . . . . . . . . . . . . . . . . . . . . . 255 8.3 Grafik ohne X11 mit der SVGALIB . . . . . . . . . . . . . . . . . . . 256 8.3.1 Besonderheiten beim Arbeiten mit der libvga . . . . . . . . . 256 8.3.2 8.3.3 Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 257 Mit Perspektive: 3D-Funktionen zeichnen . . . . . . . . . . . 260 8.3.4 Ein kleines Malprogramm . . . . . . . . . . . . . . . . . . . . 262 8.3.5 8.3.6 Erweiterte Funktionen mit der libvgagl . . . . . . . . . . . . 266 Weitere Informationsquellen . . . . . . . . . . . . . . . . . . 268 9 Hardware-Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . 271 9.1 Hardware-nahe Programme schreiben . . . . . . . . . . . . . . . . . 271 9.1.1 9.1.2 9.2 9.1.3 Zugriff auf die I/O-Ports . . . . . . . . . . . . . . . . . . . . 273 Ansteuerung des Parallelports . . . . . . . . . . . . . . . . . . . . . 274 9.2.1 9.2.2 9.3 Eigene Programme mit root-Rechten ausstatten . . . . . . . . 272 Zugriff auf I/O-Ports freischalten . . . . . . . . . . . . . . . 272 Beschreibung des Parallelports . . . . . . . . . . . . . . . . . 274 Die Adresse des Parallelports suchen . . . . . . . . . . . . . 275 9.2.3 Ein Beispiel: LED-Lauflicht“ . . . . . . . . . . . . . . . . . . 276 ” Modem-Steuerleitungen abfragen . . . . . . . . . . . . . . . . . . . . 279 10 Beispielprojekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 10.1 WebCam: Video-Übertragung per HTTP . . . . . . . . . . . . . . . . 283 10.1.1 Wie die Bilder laufen lernen . . . . . . . . . . . . . . . . . . . 284 10.1.2 Strukturierung der Quelltexte . . . . . . . . . . . . . . . . . . 284 10.1.3 Die HTTP-Authentifizierung . . . . . . . . . . . . . . . . . . 298 10.2 Telefonbuch mit automatischer Anwahl . . . . . . . . . . . . . . . . 300 10.2.1 Ziel des Projektes . . . . . . . . . . . . . . . . . . . . . . . . . 300
X Inhaltsverzeichnis 10.2.2 Strukturierung des Projektes . . . . . . . . . . . . . . . . . . 301 10.2.3 Das Hauptprogramm . . . . . . . . . . . . . . . . . . . . . . 301 10.2.4 Funktionen zur Ansteuerung des Modems . . . . . . . . . . 304 10.2.5 Die Benutzerschnittstelle . . . . . . . . . . . . . . . . . . . . 307 10.2.6 To Do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312 Anhang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315 A1 – Daten zum Buch im Internet . . . . . . . . . . . . . . . . . . . . . . . 315 A2 – Das X11-Toolkit XView . . . . . . . . . . . . . . . . . . . . . . . . . . 315 A3 – Aufbau einer WAV-Audiodatei . . . . . . . . . . . . . . . . . . . . . 316 A4 – Aufbau einer AU-Audiodatei . . . . . . . . . . . . . . . . . . . . . . 317 A5 – Linux-Programmierung unter Windows: Cygwin . . . . . . . . . . 317 Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Vorwort Seit der 1. Auflage dieses Buches sind nun fast acht Jahre vergangen. In dieser Zeit hat sich Linux auf verschiedenen Gebieten weiterentwickelt: Für den Anwender ist die Unterstützung von USB- und anderen Plug&Play-Geräten hinzugekommen, für den Programmierer hat eine Standardisierung der unterschiedlichen Linux-Distributionen stattgefunden; es wurde die so genannte Linux Standard Base (kurz LSB) entwickelt. Beiden Entwicklungen wird in dieser 4. Auflage Rechnung getragen. Sämtliche Quelltexte wurden an die LSB angepasst und das Kapitel über die Ansteuerung von Geräten wurde um das Thema USB erweitert. Darüber hinaus sind in dem Kapitel Netzwerkprogrammierung“ Beispiele zu UDP, Broadcast und Multicast ” ergänzt worden. Dieses Buch wendet sich sowohl an den Programmier-Einsteiger, der Grundkenntnisse in der Programmiersprache C“ besitzt, als auch an den fortgeschrit” tenen Programmierer, der die vielfältigen Möglichkeiten des Betriebssystems in eigenen Programmen nutzen möchte. Dabei geht es vor allem um die Linuxspezifischen Themen, wie z. B. die Ansteuerung von Devices. Durch zahlreiche einfache Beispielprogramme soll der Einstieg in diese Themen erleichtert werden. Als Programmiersprache kommt ausschließlich ANSI-C zum Einsatz. Haiger, im Frühjahr 2010 Martin Gräfe

Kapitel 1 Einführung 1.1 Warum gerade C“? ” Unter Linux ist mittlerweile eine Vielzahl von Programmiersprachen verfügbar, angefangen von Pascal und Fortran über Skript- oder Interpretersprachen wie TCL und Perl bis hin zu objektorientierten Compilersprachen wie C++ und Java. Jede dieser Programmiersprachen hat ihre Vor- und Nachteile, C“ kommt jedoch ” eine besondere Bedeutung zu, da fast das gesamte Betriebssystem in ANSI-C geschrieben ist.1 Mit den Kernel-Quellen stehen dadurch sämtliche für die systemnahe Programmierung erforderlichen Include-Dateien unter C zur Verfügung. Aus diesem Grund lassen sich die Möglichkeiten des Betriebssystems (und der Hardware) mit C so vollständig wie mit keiner anderen Programmiersprache nutzen.2 Da C eine kompakte, relativ maschinennahe“ Programmiersprache ist, sind C” Programme effizient und schonen die System-Ressourcen. Für das Ausführen der Programme ist weder ein Interpreter noch eine Laufzeitumgebung wie bei Java erforderlich. Trotzdem sind die Quelltexte portabel – viele der Beispielprogramme in diesem Buch laufen auch unter kommerziellen Unix-Varianten (z. B. Solaris von Sun oder HPUX von Hewlett Packard) und mit Cygwin3 sogar unter WindowsTM . Mit den Kernel-Quellen und vielen Open Source-Programmen hat der Programmierer Zugriff auf eine nahezu unbegrenzte Menge an Quelltexten, aus denen er sich den einen oder anderen Programmierkniff abschauen kann – und darf . Denn in der Welt der offenen Quelltexte muss zum Glück das Rad nicht immer wieder neu erfunden werden. 1 Mit Ausnahme einiger Hardware-naher oder zeitkritischer Programmteile, die in Assembler programmiert wurden. 2 Außer vielleicht in Assembler, was aber keine echte Alternative zu Hochsprachen darstellt. 3 siehe Anhang A5 ab Seite 317
2 1 Einführung 1.2 Bevor es losgeht . . . . . . müssen die notwendigen Tools und Dateien installiert sein. In der Anfangszeit von Linux, als man nach der Installation erst einmal den Kernel nach eigenen Wünschen neu kompilierte, waren die Werkzeuge für die C-Programmierung fester Bestandteil der Linux-Distributionen und wurden in der Regel automatisch mit installiert. Inzwischen ist das Kompilieren des Kernels dank der Modularisierung nicht mehr notwendig, und so werden bei vielen Distributionen die Pakete zur Software-Entwicklung nicht mehr automatisch installiert. Bei einigen auf CD erhältlichen Distributionen sind diese Pakete nicht einmal mehr enthalten, sondern müssen aus dem Internet nachgeladen werden. Um die Beispiele in diesem Buch übersetzen zu können, benötigen Sie das Paket mit dem C-Compiler gcc“ sowie das Programm make“. Außerdem werden für ” ” das Einbinden von Funktionsbibliotheken in eigene Programme die zugehörigen Include-Dateien benötigt, die häufig in separaten Paketen enthalten sind. Beispiel: Die Funktionsbibliothek libncurses“ wird bei den meisten Distributionen als Vor” einstellung installiert. Wenn Sie diese Bibliothek in einem eigenen Programm benutzen wollen, benötigen Sie zusätzlich das zugehörige Entwicklungspaket“. Je ” nach Distribution heißt dieses Paket beispielsweise ncurses-dev-Version“ ” oder ncurses-devel-Version“. ” Im Folgenden soll exemplarisch für die Distributionen SuSE“ und Ubuntu“ ge” ” zeigt werden, wie Pakete nachinstalliert werden können. 1.2.1 Paketverwaltung unter SuSE-Linux In der Linux-Distribution von SuSE1 ist das Tool YaST“ (Abkürzung für Yet ” ” Another Setup Tool“) enthalten, das für alle wichtigen Systemeinstellungen zuständig ist. Dieses kann entweder aus dem Menü des Windowmanagers oder als Benutzer root“ aus einer Shell mit dem Kommando /sbin/yast2“ aufgeru” ” fen werden. Durch einen Klick auf das Icon Software installieren oder löschen“ in ” der Rubrik Software“ wird die Paketverwaltung geöffnet (siehe Abbildung 1.1). ” Findet man das gesuchte Paket nicht unter den angezeigten Paketgruppen, kann man den Filter“ (oben links) auf Suche“ umstellen und ein entsprechendes ” ” Stichwort eingeben. Die Pakete der Distribution von SuSE liegen im RPM-Format (Abkürzung für RedHat Packet Manager“) vor. Daher können einzelne Pakete auch mit dem Be” fehl rpm -i Paket-Datei“ installiert werden. ” 1 SuSE steht für Software- und Systementwicklung. So heißt das kleine Unternehmen, das diese Distribution zusammenstellt und inzwischen von der Firma Novell aufgekauft wurde.
1.2 Bevor es losgeht . . . Abbildung 1.1: Die Paketverwaltung unter SuSE mit YaST 3
4 1 Einführung 1.2.2 Paketinstallation bei Ubuntu Ubuntu-Linux ist von der Linux-Distribution Debian abgeleitet und verwendet die gleichen Pakete. Anders als bei SuSE sind die Pakete daher im Debian-eigenen DEB-Format. Für die Auswahl und Installation der Pakete bringt der GNOMEDesktop unter Ubuntu ein spezielles Tool mit, das über das GNOME-Menü aufgerufen werden kann (siehe Abbildung 1.2). Abbildung 1.2: Öffnen der GNOME-Paketverwaltung bei Ubuntu-Linux In der Startansicht bietet das Werkzeug nur das Installieren oder Entfernen ganzer Anwendungen an, ohne die einzelnen Pakete im Detail aufzulisten. Über den Menüpunkt Datei/Erweitert“ lässt sich die Darstellung erweitern, sodass ” die einzelnen Pakete aufgelistet werden können (siehe Abbildung 1.3). Ähnlich wie bei YaST unter SuSE-Linux ist auch hier die Möglichkeit gegeben, nach bestimmten Paketen anhand von Stichworten zu suchen (Schaltfläche Suche“ unten ” links). Wenn Sie Pakete von Hand“ installieren wollen, so gibt es dafür bei Ubuntu zwei ” Kommandozeilenprogramme: apt-get“ und dbkg“: ” ” sudo dpkg -i Paket-Datei sudo apt-get install Paket-Datei
1.2 Bevor es losgeht . . . Abbildung 1.3: Erweiterte Ansicht für die GNOME-Paketverwaltung 5
6 1 Einführung 1.3 Die Werkzeuge In diesem Abschnitt stellen wir in kurzer Form die für das Programmieren erforderlichen Werkzeuge vor. In Abschnitt 1.4 wird der Umgang mit diesen Werkzeugen dann anhand von Beispielen erläutert. Den Schwerpunkt bilden dabei die für Unix und Linux üblichen Kommandozeilen-Tools. Den Umgang mit einer integrierten Entwicklungsumgebung beschreibt Kapitel 2. 1.3.1 Der Editor – die Qual der Wahl Um ein Programm zu schreiben, benötigt man natürlich zunächst einen Editor, mit dem man den Quelltext eingibt. Bei Verwendung einer integrierten Entwicklungsumgebung (siehe Abschnitt 1.3.5) ist bereits ein solcher Editor in diese Umgebung eingebaut, doch viele Programmierer verwenden stattdessen ihren Lieb” lingseditor“ – wovon es unter Linux eine ganze Menge gibt. Dabei kann man zwei Kategorien unterscheiden: Editoren, die auf der Konsole bzw. in einem Terminalfenster (wie XTerm) laufen, und Editoren, die über eine eigene grafische Benutzeroberfläche verfügen. Letztere sind in der Regel komfortabler, weil sie über Syntax-Highlighting verfügen, benötigen aber auch weit mehr Ressourcen. Editoren für die Textkonsole: vim (steht für VI improved“) ” emacs pico joe jedit ... Editoren mit grafischer Oberfläche: kate (Bestandteil von KDE) gedit (Bestandteil von GNOME) nedit xemacs ... Der Editor kate“ bietet außerdem die Möglichkeit, C-Funktionen einzuklap” ” pen“, um den Quelltext übersichtlicher darzustellen (siehe Abbildung 1.4). Nicht alle hier erwähnten Editoren sind in jeder Linux-Distribution enthalten. Ggf. müssen die zugehörigen Pakete erst aus dem Internet geladen und gemäß Abschnitt 1.2 installiert werden.
1.3 Die Werkzeuge Abbildung 1.4: Zwei Editoren für Programmierer: nedit“ und kate“ ” ” 7
8 1 Einführung 1.3.2 Der GNU C-Compiler gcc Kernstück der Software-Entwicklung ist der C-Compiler selbst, also der gcc bzw. g++ (für C++-Programme). Der gcc ist ein so genannter Cross-Compiler, mit dem man im Grunde auch Programme für andere Betriebssysteme oder HardwarePlattformen (also andere Prozessoren) entwickeln kann. Der einfachste Aufruf des Compilers lautet: gcc Quelltext So aufgerufen, wird der Quelltext kompiliert, assembliert und gelinkt, sodass ein ausführbares Programm entsteht. Dieses Programm wird voreingestellt unter dem Dateinamen a.out“ abgespeichert. In der Regel wird man diese Voreinstel” lung nicht verwenden, sondern einen anderen, zweckmäßigeren Namen wählen 1 wollen. Dies geschieht mit Hilfe der Option -o“: ” gcc Quelltext -o Ausgabedatei Beispiel: gcc hello.c -o hello Bei diesem Aufruf führt der gcc zwei Schritte durch: das eigentliche Kompilieren (Übersetzen) und das Linken zu einem ausführbaren Programm. Letzterer sorgt z.B. dafür, dass Funktionsaufrufe wie printf() mit den entsprechenden Funktionen aus der dynamischen Bibliothek libc“ verknüpft werden. Wird ein Pro” gramm auf mehrere Quelltexte aufgeteilt, so müssen die einzelnen Programmteile zunächst nur übersetzt werden, ohne den Linker aufzurufen. Dazu wird beim Übersetzen die Option -c“ angegeben. Der Compiler erzeugt in diesem Fall nur ” eine Objektdatei, die automatisch die Endung .o“ erhält. Ein Beispiel hierzu fin” det sich in Abschnitt 1.4.5. Der gcc besitzt eine Vielzahl weiterer Optionen, von denen wir in diesem Buch nur einen kleinen Teil benötigen. Eine vollständige Beschreibung erhält man mit man gcc“. Die Reihenfolge der Parameter und Optionen ist beim gcc – bis auf ” wenige Ausnahmen – beliebig. 1.3.3 Ablaufsteuerung mit GNU make Für das Übersetzen kleinerer Programme benötigt man in der Regel nur den CCompiler wie im vorherigen Abschnitt beschrieben. Bei umfangreicheren Projekten sollten Sie den Quelltext in mehrere Teile zerlegen. Dadurch werden die Dateien nicht nur übersichtlicher, es ist dann auch möglich, bei Änderungen nur diejenigen Dateien neu zu übersetzen, die geändert wurden. Genau hier setzt das 1 Sie sollten für erste Versuche nicht den Namen test“ wählen, da ein gleichnamiges Programm schon ” Bestandteil der Shell ist!
1.3 Die Werkzeuge 9 Programm make“ an. Es prüft, ob sich die Quellen eines Programmteils geändert ” haben, und übersetzt diesen Teil dann neu. Das Tool make“ benötigt dazu eine Datei, in der die Abhängigkeiten der Quell” und Zieldateien und die Anweisungen (Compiler-Aufrufe) eingetragen sind. Ein Eintrag in dieser Datei, dem so genannten Makefile, sieht wie folgt aus: Zieldatei: Quelldatei1 Quelldatei2 . . . Anweisung1 Anweisung2 ... Beispiel: hello: hello.c gcc hello.c -o hello Alle Anweisungszeilen müssen mit einem oder mehreren Tabulatoren ( echte“ ” Tabs, keine Leerzeichen!) eingerückt sein, während die Zieldatei immer am Zeilenanfang stehen muss. Eine solche Make-Datei kann beliebig viele Zieldateien mit den zugehörigen Quelldateien und Anweisungen enthalten. Zur Veranschaulichung sei auf das Beispiel in Abschnitt 1.4.5 verwiesen. Wird die Make-Datei Makefile“ oder makefile“ genannt, so kann make“ ohne ” ” ” Parameter aufgerufen werden. Andernfalls lautet der Aufruf: make -f Make-Datei Sind in dem Makefile mehrere Zieldateien angegeben, kann durch die Eingabe von make Zieldatei gezielt eine dieser Dateien erzeugt werden, wobei make auch hier automatisch die Abhängigkeiten prüft und ggf. weitere, für die angegebene Zieldatei erforderliche Dateien neu erzeugt. Ohne Angabe der Zieldatei wird immer die erste Datei im Makefile erzeugt. An dieser Stelle sei noch darauf hingewiesen, dass es sich bei dem Ziel nicht unbedingt um eine Datei handeln muss. So findet sich in Makefiles häufig ein Eintrag der folgenden Form: clean: rm -f *.o Mit dem Aufruf make clean“ werden dann Objekt-Dateien, die man nicht mehr ” benötigt, gelöscht. Man beachte, dass hier keine Quelldateien angegeben sind, was dazu führt, dass die Anweisung immer ausgeführt wird. Für eine ausführliche Anleitung siehe auch man make“. ”
10 1 Einführung 1.3.4 Für die Fehlersuche: Die Debugger Nur selten läuft ein Programm auf Anhieb einwandfrei. Schnell schleichen sich Fehler ein, im Programmiererjargon Bugs“ (Wanzen1 ) genannt. Zur Lokalisie” rung und Beseitigung der Bugs greift man zu einem Debugger“ (Entwanzer). ” Unter Linux hat der Programmierer in die Wahl zwischen dem textbasierten GNU Debugger gdb“ – dem Urvater“ der Debugger unter Linux – und verschiede” ” nen grafischen Front-Ends. Ursprünglich gab es eine relativ rudimentäre grafische Oberfläche für den GNU Debugger namens xxgdb“. Dieses Projekt wur” de aber vor geraumer Zeit durch ein von Grund auf neu gestaltetes Tool ersetzt, den DDD“ (Abkürzung für Data Display Debugger“, siehe Abbildung 1.5). Der ” ” DDD ist kein eigenständiger Debugger sondern eine grafische Benutzeroberfläche für den GNU Debugger gdb. Das Tool wurde übrigens in Deutschland entwickelt! Abbildung 1.5: Ein elektronischer Kammerjäger: der DDD“ ” Um einen Fehler in einem Programm mit Hilfe des Debuggers zu finden, muss das Programm Zusatzinformationen enthalten, mit deren Hilfe der Debugger das 1 Dieser Ausdruck stammt noch aus der Zeit der Relais-Computer. Hier hatte sich einmal eine Wanze zwischen die Relaiskontakte verirrt und dadurch Rechenfehler verursacht.
1.3 Die Werkzeuge 11 ausführbare Programm mit dem zugehörigen Quelltext in Verbindung bringen kann. Diese Zusatzinformationen fügt der gcc mit Hilfe der Option -g ein: gcc -g hello.c -o hello Anschließend kann der Debugger aufgerufen werden, z. B.: ddd hello Hier können Sie nun so genannte Breakpoints setzen, das Programm schrittweise ausführen und den Inhalt von Variablen anzeigen. Kommt es zur Laufzeit des Programms zu einem Fehler, der die Ausführung sofort abbricht – beispielsweise eine Speicher-Zugriffsverletzung (Segmentation fault) oder eine Division durch null –, so zeigt der Debugger die entsprechende Zeile im Quelltext an, die zu diesem Fehler geführt hat. Übrigens: nach erfolgreicher Fehlerbeseitigung lassen sich die für die Ausführung des Programms nicht notwendigen Debug-Zusatzinformationen mit strip hello wieder entfernen, ohne das Programm neu zu übersetzen. Dadurch reduziert sich die Größe des Programms offtmals erheblich. 1.3.5 Integrierte Entwicklungsumgebungen Als Alternative zur direkten“ Verwendung der bisher vorgestellten Werkzeuge ” gibt es die Möglichkeit, mit einer integrierten Entwicklungsumgebung1 zu arbeiten. Dabei handelt es sich um ein Programm, das neben einem Quelltext-Editor auch eine grafische Schnittstelle für Compiler, Debugger usw. bietet. Insbesondere Umsteiger aus der Windows-Welt“ finden mit Hilfe solcher Programme häufig ” leichter den Einstieg in die Linux-Programmierung. Man sollte jedoch beachten, dass Entwicklungsumgebungen keine Compiler, sondern eben nur Umgebungen sind und zum Übersetzen und Linken des Quelltextes wieder auf den C-Compiler gcc zurückgreifen. Der Vorteil solcher Programme ist, dass das Wechseln zwischen den Werkzeugen Editor, Compiler, Debugger usw. entfällt. Tritt beispielsweise beim Übersetzen des Programms ein Fehler auf, wird automatisch die entsprechende Zeile im Quelltext markiert. 1 auch als IDE“ für Integrated Development Environment bezeichnet ”
12 1 Einführung Abbildung 1.6: KDevelop – Entwicklungsumgebung des KDE Abbildung 1.7: Das Entwicklungs-Framework Eclipse
1.3 Die Werkzeuge Abbildung 1.8: Die GNOME-Entwicklungsumgebung Anjuta Abbildung 1.9: Entwicklungsumgebung à la Turbo-Pascal: xwpe 13
14 1 Einführung Unter Linux sind verschiedene Entwicklungsumgebungen frei verfügbar; einige dieser Linux-IDEs sind: KDevelop (Entwicklungsumgebung des KDE), siehe Abbildung 1.6 Eclipse + C Development Tooling“ (kurz CDT), siehe Abbildung 1.7 ” Anjuta (Entwicklungswerkzeug des GNOME-Projektes), siehe Abbildung 1.8 xwpe (X-Window Programming Environment) Alle vier Programme sind mausgesteuert, menügeführt und mit einer mehr oder weniger umfangreichen Online-Hilfe ausgestattet, die über den entsprechenden Menüpunkt aufgerufen werden kann. Die aktuellen Versionen von KDevelop, Eclipse (inkl. CDT) und Anjuta sind recht umfangreich und benötigen mehr als 50 MByte. KDevelop bringt es mit den zugehörigen Dokumentationspaketen sogar auf mehrere 100 MByte – ein Grund, bei nicht so leistungsstarken Rechnern doch auf die schlanken“ Kommandozeilen” Programme zurückzugreifen. Eine Ausnahme bildet das nicht so bekannte Tool xwpe“ (Abbildung 1.9), das ” deutlich weniger Ressourcen benötigt, aber auch nicht so komfortabel ist wie beispielsweise KDevelop. Die Entwicklungsumgebung Eclipse fällt etwas aus der Reihe: Das in Java programmierte Tool bildet eine Art Framework“. Für die Entwicklung von ” Java-Programmen wird zusätzlich das Paket JDT“ (Java Development Tooling) ” benötigt, für C-Programme entsprechend das Paket CDT“. ” In Kapitel 2 beschreiben wir die Arbeit mit einer integrierten Entwicklungsumgebung anhand der Programme Anjuta und KDevelop eingehender. 1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen ” Häufig sagt ein Beispiel mehr als tausend Worte; auch bei der Beschreibung von Programmierwerkzeugen ist das nicht anders. Aus diesem Grund wird in den folgenden Abschnitten die Handhabung der zuvor beschriebenen Werkzeuge anhand einfacher Beispiele demonstriert. 1.4.1 Primzahlen berechnen Das folgende kleine C-Programm berechnet die Primzahlen von 1 bis 100, Zahlen also, die sich nur durch 1 und sich selbst teilen lassen.1 Es soll im Folgenden dazu dienen, das Übersetzen und die Fehlersuche mit den bereits vorgestellten Programmierwerkzeugen zu verdeutlichen. 1 Mathematisch exakt betrachtet, sind Primzahlen diejenigen Zahlen, die genau zwei Teiler besitzen.
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen ” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 15 /* primzahl.c */ # include <stdio.h> int ist_primzahl(int zahl) { int teiler=2; while (teiler*teiler <= zahl) { if (zahl % teiler == 0) return(0); /* ’zahl’ ist keine Primzahl */ teiler++; } return(1); /* ’zahl’ ist eine Primzahl */ } int main() { int zahl; for (zahl=1; zahl<=100; zahl++) if (ist_primzahl(zahl)) printf("%d\n", zahl); return(0); } Nach Eingabe dieses Quelltextes in einen Editor und Abspeichern als primzahl.c“ ” lässt sich das Programm mit gcc primzahl.c -o primzahl übersetzen. Danach existiert eine ausführbare Datei mit dem Namen primzahl“. ” Der gcc stellt die Zugriffsrechte direkt so ein, dass der Besitzer der Datei (also Sie) diese ausführen darf. Mit ls -F“ wird dies durch ein *“ hinter dem Da” ” teinamen gekennzeichnet. Jetzt können Sie das Programm direkt starten. Dies geschieht im Normalfall einfach durch Eingabe des Programmnamens. Sollte das aktuelle Verzeichnis .“ nicht in dem Suchpfad für ausführbare ” Dateien eingetragen sein – dies ist bei den meisten Linux-Distributionen der Fall –, muss dem Programmnamen ein ./“ vorangestellt werden. Alternativ kann ” mit dem Kommando export PATH=$PATH:.“ das aktuelle Verzeichnis in dem ” Suchpfad eingetragen werden.
16 1 Einführung Jetzt sollte in Ihrem Terminal-Fenster eine Liste der Primzahlen von 1 bis 97 erscheinen. 1.4.2 Fehlersuche mit dem gcc Nicht immer läuft das Ganze so reibungslos. Schnell hat sich ein Bug eingeschlichen, und das Programm funktioniert nicht richtig. Doch bevor Sie jetzt einen Debugger bemühen, sollte zunächst noch einmal der C-Compiler zu Rate gezogen werden. Um dies zu demonstrieren, ändern Sie die Zeile 24 des Programms wie folgt ab 24 for (zahl=1; zahl=100; zahl++) ↑ und kompilieren es erneut. Wenn Sie es jetzt starten, passiert nichts. Das Programm rechnet und rechnet, ohne jemals eine Ausgabe zu erzeugen. Mit der Tastenkombination Strg“ (bzw. Ctrl“) und c“ können Sie das Programm abbre” ” ” chen. Was ist geschehen? Durch Entfernen des <“ ist aus dem Vergleich zahl<=100“ ” ” eine Zuweisung geworden, d.h. zahl wird bei jedem Schleifendurchlauf auf 100 gesetzt. Anders als z. B. bei BASIC oder Pascal liefert in C eine Zuweisung ebenso wie ein Vergleich einen Wert zurück, nämlich den zugewiesenen Wert (hier 100). Die for-Anweisung prüft nun bei jedem Schleifendurchlauf, ob der Rückgabewert falsch“ ist (was der Zahl 0 entspricht), um in diesem Fall die Schleife zu ” beenden. Da jetzt der Rückgabewert immer 100 ist – und damit ungleich 0 –, wird die for-Schleife nie beendet, und zahl bleibt immer 100. Wie lässt sich nun dieser in C“ relativ häufige Fehler mit Hilfe des C-Compilers ” gcc finden? Dazu geben Sie bitte folgende Zeile ein: gcc -Wall primzahl.c -o primzahl Sie erhalten dann eine Warnung des Compilers bezüglich der fehlerhaften Zeile 24: primzahl.c:24: warning: suggest parentheses around assignment used as truth value Der Compiler weist also darauf hin, dass hier eine in Klammern gesetzte Zuweisung als Wahrheitswert ( wahr“ oder falsch“) verwendet wird. Doch was bedeu” ” tet die Option -Wall“? Hiermit kann die Art der Warnmeldungen des Compilers ” eingestellt werden. Die Syntax dieser Option lautet -WWarnungen“, wobei War” nungen z.B. implicit“ (Verwendung einer nicht deklarierten Funktion) oder unu” ” sed“ (Deklaration von Variablen, die nicht benutzt werden) sein kann – oder eben auch all“ für alle Warnungen. ” Viele Tippfehler“ in einem Quelltext lassen sich auf diese Weise relativ schnell ” finden, ohne das kompilierte Programm überhaupt zu starten.
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen ” 17 1.4.3 Fehlersuche mit dem GNU Debugger In diesem Abschnitt wird die Bedienung des GNU Debuggers an dem PrimzahlBeispiel von oben demonstriert. Dabei kommt zunächst die KommandozeilenVersion gdb“ zum Einsatz. Anschließend wird gezeigt, wie mit dem grafischen ” Front-End kdbg“ gearbeitet wird. ” Um den Einsatz eines Debuggers zu demonstrieren, soll in das PrimzahlProgramm ein anderer Fehler eingebaut werden. Dazu korrigieren Sie zunächst die Zeile 24 und bringen so den Quelltext wieder in Übereinstimmung mit dem in Abschnitt 1.4.1 abgedruckten Listing. Dann ändern Sie bitte Zeile 9 wie folgt ab: 9 int teiler=0; ↑ Danach übersetzen Sie das Programm, diesmal jedoch mit der Option -g“, um ” die Verwendung eines Debuggers zu ermöglichen (vgl. auch Abschnitt 1.3.4): gcc -Wall -g primzahl.c -o primzahl Wenn Sie das Programm jetzt starten, erhalten Sie die Meldung Gleitkomma-Ausnahme Zur Fehleranalyse rufen Sie bitte den gdb auf und geben das Primzahl-Programm als Parameter an. Der Debugger lädt dann automatisch das Programm, führt es aber nicht sofort aus. Um das Programm zu starten, muss das gdb-Kommando run“ eingegeben werden (die Benutzereingaben sind schräg dargestellt): ” > gdb primzahl GNU gdb 4.18 Copyright 1998 Free Software Foundation, Inc. (gdb) run Starting program: /home/martin/TeX/Linux-Buch/primzahl Program received signal SIGFPE, Arithmetic exception. 0x8048326 in ist primzahl (zahl=1) at primzahl.c:13 13 (gdb) if (zahl % teiler == 0) Der Debugger stoppt die Ausführung des Programms automatisch beim Auftreten des Fehlers (hier Arithmetic exception“ = Gleitkomma-Ausnahme) und zeigt ” uns die zugehörige Zeile im Quelltext mitsamt Zeilennummer an. Um der Ursache für den Fehler jetzt auf den Grund zu gehen, kann man sich unter anderem Variablen-Inhalte anzeigen lassen, und zwar durch das Kommando print“: ”
18 1 Einführung (gdb) print zahl $1 = 1 (gdb) print teiler $2 = 0 Hier sieht man, dass bei der Operation zahl % teiler“ eine Division durch ” Null erfolgt, was zum Abbruch des Programms führt. Also muss der Startwert für teiler mit einer Zahl > 0 initialisiert werden. Nach erfolgreicher Fehlersuche können Sie den Debugger mit quit“ beenden. ” Breakpoints Nicht immer führt ein Fehler im Programm zu einem Abbruch, der wie oben gezeigt analysiert werden kann. Häufig läuft das Programm, liefert jedoch nicht das gewünschte Ergebnis. Um dies bei unserem Beispielprogramm zu erreichen, ändern Sie bitte die Zeile 9 wie folgt: 9 int teiler; ↑ Wenn Sie es jetzt kompilieren (bitte wieder mit der Option -g) und ausführen, so erhalten Sie eine Liste aller Zahlen von 1 bis 100. Die Funktion ist primzahl() funktioniert nicht mehr. Um in einem solchen Fall dem Fehler auf die Spur zu kommen, muss das Programm während der Ausführung zwischendurch angehalten werden, um z.B. Variablen-Inhalte zu überprüfen. Genau dies ist mit Breakpoints möglich. Über das gdb-Kommando break“ lässt sich das Programm z.B. ” bei jedem Aufruf der Funktion ist primzahl() unterbrechen: > gdb primzahl GNU gdb 4.18 Copyright 1998 Free Software Foundation, Inc. (gdb) break ist primzahl Breakpoint 1 at 0x8048306: file primzahl.c, line 11. (gdb) run Starting program: /home/martin/TeX/Linux-Buch/primzahl Breakpoint 1, ist primzahl (zahl=1) at primzahl.c:11 11 while (teiler*teiler <= zahl) (gdb) print teiler $1 = 1073972219 Die Variable teiler hat hier offensichtlich einen unsinnigen Wert – sie wurde vor Verwendung in Zeile 11 nicht initialisiert. Neben der Verwendung von Breakpoints ist auch ein schrittweises Ausführen des Programms mit Hilfe von next“ und nexti“ häufig sehr hilfreich. Außerdem ” ” bietet der Debugger eine Vielzahl weiterer Möglichkeiten, die sich am besten am
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen ” 19 realen Problem“ erforschen lassen. Hilfestellung erfahren Sie mit dem Komman” do help“. ” Verwendung des grafischen Front-Ends kdbg Das Arbeiten mit dem DDD und dem kdbg hat gegenüber dem gdb den Vorteil, dass die aktuell ausgeführte Programmzeile sowie die Breakpoints direkt im zugehörigen Quelltext markiert werden. Die Fehlersuche wird dadurch deutlich übersichtlicher. Ebenso wie beim gdb kann auch beim Aufruf der grafischen Front-Ends DDD und kdbg das zu analysierende Programm als Parameter angegeben werden: > kdbg primzahl Der kdbg lädt nicht nur das Programm, sondern öffnet auch direkt den Quelltext und markiert die Zeile, in der die Ausführung des Programms beginnen wird (also die Funktion main()). Gestartet wird das Programm über das Menü Ausführen“ oder die ensprechende Schaltfläche unter der Menüleiste, sie” he Abbildung 1.10. Abbildung 1.10: Die Ausführung des Programms im kdbg“ starten. ” Tritt während der Ausführung des Programms ein Fehler auf – beispielsweise die Gleitkomma-Ausnahme“ wie in dem Beispiel oben –, so wird die verursachende ” Zeile direkt im Quelltext markiert (siehe Abbildung 1.11). Zur Anzeige der aktuellen Werte der Variablen kann der Menüpunkt View/Lokale Variablen“ gewählt ” werden. Breakpoints können Sie mit dem Menüpunkt Haltepunkt“ setzen. ” 1.4.4 Funktionsbibliotheken verwenden Dem C-Programmierer stehen für viele verschiedene Aufgabenstellungen umfangreiche Funktionsbibliotheken (engl. Libraries) zur Verfügung; beispielswei-
20 1 Einführung Abbildung 1.11: Der kdbg markiert die Zeile, die zum Abbruch des Programms führt, direkt im Quelltext. se bei der Grafik-Programmierung wird hiervon intensiv Gebrauch gemacht. Die Verwendung solcher Bibliotheken soll in diesem Abschnitt anhand der Mathematik-Bibliothek libm“ demonstriert werden, die all jene Funktionen ” enthält, die über die Grundrechenarten hinausgehen. Auch hierzu wird wieder unser Primzahl-Programm als Beispiel dienen. Bitte bringen Sie es zunächst wieder in die ursprüngliche Form gemäß Abschnitt 1.4.1. In Zeile 11 wird überprüft, ob die Variable teiler schon größer als die Quadratwurzel aus zahl ist, denn Nicht-Primzahlen besitzen mindestens einen Teiler, der kleiner als ihre Quadratwurzel ist. In der bisherigen Version des Programms wurde diese Abbruchbedingung der while()-Schleife mit Hilfe einer Multiplikation umschrieben. Bitte ändern Sie diese Zeile nun wie folgt ab: 11 while (teiler <= sqrt(zahl)) und greifen damit auf die Quadratwurzel-Funktion sqrt() aus der mathematischen Bibliothek zu. Damit der C-Compiler weiß, von welchem Typ der Parameter und der Rückgabewert dieser Funktion sind (hier double), muss die entsprechende Include-Datei eingebunden werden:
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen ” 6 21 # include <math.h> Für das Übersetzen des Programms muss dem C-Compiler die Bibliothek angegeben werden, auf die das Programm zugreift – in diesem Fall also die MathematikBibliothek libm. Das geschieht mit Hilfe der Option -lBibliothek“, wobei das jeder ” Bibliothek vorangestellte lib“ weggelassen wird: ” gcc primzahl.c -lm -o primzahl Jetzt können Sie das Programm wie zuvor starten – mit dem gleichen Resultat. Es können beliebig viele Bibliotheken gleichzeitig verwendet werden, indem die Option -l“ mehrfach angegeben wird. Sollte sich eine Bibliothek nicht in dem ” dafür standardmäßig vorgesehenen Verzeichnis befinden (/usr/lib), kann mit der Option -L“ ein zusätzlicher Suchpfad angegeben werden. Auch diese Option ” lässt sich mehrfach verwenden. Beispiel: gcc -L/usr/X11R6/lib -L/usr/openwin/lib my_program.c -lX11 -lxview -lolgx -lm -o my_program Die Bibliothek libc, die Funktionen wie printf() enthält, wird übrigens automatisch eingebunden und muss nicht explizit angegeben werden. Übrigens: Eigene Funktionsbibliotheken kann man mit dem Shell-Befehl ar“ er” stellen. Details dazu erhalten Sie mit man ar“. ” 1.4.5 Quelltexte aufteilen Während sich das in dem Abschnitt 1.4.1 vorgestellte Beispielprogramm zur Primzahlberechnung noch geeignet an einem Stück“ programmieren ließ, werden ” C-Programme größeren Umfangs recht schnell unübersichtlich, was wiederum leicht zu vermeidbaren Fehlern führt. Daher sollten bei der Erstellung eines Programms direkt zu Beginn Funktionseinheiten festgelegt werden, denen sich die einzelnen Programmteile bzw. Funktionen zuordnen lassen. Abbildung 1.12 zeigt, wie diese Strukturierung für ein Programm aussehen könnte, welches sowohl eine grafische Oberfläche ( GUI“ für Graphical User Interface) als auch mathematische ” Berechnungsroutinen enthält. Der Quelltext für das Programm my prog“ wird hier in die Dateien main.c“, ” ” gui.c“ und mathfunc.c“ aufgeteilt. Die Include-Dateien gui.h“ und math” ” ” ” func.h“ enthalten die Deklarationen der in gui.c“ bzw. mathfunc.c“ definierten ” ” Funktionen, soweit sie in den anderen Programmteilen verwendet werden. Um diese Vorgehensweise zu veranschaulichen, sei noch einmal das Programm zur Primzahlberechnung herangezogen. Obwohl eine Aufteilung dieses kurzen Quelltextes keinen Gewinn an Übersichtlichkeit bringt, lässt sich daran das Prinzip gut demonstrieren. Dazu zerlegen wir den Quelltext in einen Hauptteil primz haupt.c“: ”
22 1 Einführung Hauptprogramm grafische Oberfläche math. Funktionen main.c gui.h mathfunc.h gui.c mathfunc.c ? ? main.o ? gui.o q ? mathfunc.o ) ausführbares Programm my prog Abbildung 1.12: Beispiel für die strukturierte Aufteilung eines Quelltextes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* primz_haupt.c */ # include <stdio.h> # include "primz_math.h" int main() { int zahl; for (zahl=1; zahl<=100; zahl++) if (ist_primzahl(zahl)) printf("%d\n", zahl); return(0); } – in einen mathematischen Teil primz math.c“: ” 1 /* 2 primz_math.c 3 */ 4 # include <math.h> 5 6 7 int ist_primzahl(int zahl) 8 {
1.4 Der Umgang mit Compiler, Debugger und make“ anhand von Beispielen ” 9 10 11 12 13 14 15 16 17 18 23 int teiler=2; while (teiler <= sqrt(zahl)) { if (zahl % teiler == 0) return(0); /* ’zahl’ ist keine Primzahl */ teiler++; } return(1); /* ’zahl’ ist eine Primzahl */ } – und in die zugehörige Include-Datei primz math.h“: ” 1 /* 2 primz_math.h / 3 * 4 5 int ist_primzahl(int zahl); Der Programmteil primz math.c“ stellt die Funktion ist primzahl()“ zur ” ” Verfügung, deren Parameter und Rückgabewert in primz math.h“ deklariert ” sind. Durch die include-Anweisung in Zeile 6 der Datei primz haupt.c“ wird ” diese Deklaration im Hauptteil verfügbar gemacht. Doch wie erzeugt man nun aus diesen Quelldateien das ausführbare Programm? Dazu müssen zunächst die einzelnen Programmteile in Objektdateien übersetzt werden: gcc -c primz_haupt.c gcc -c primz_math.c Diese Compiler-Aufrufe erzeugen die beiden Dateien primz haupt.o“ und ” primz math.o“, aus denen wiederum mit der Anweisung ” gcc primz haupt.o primz math.o -lm -o primzahl das ausführbare Programm primzahl“ generiert wird. Man beachte, dass erst bei ” dem letzten Schritt das Einbeziehen der Mathematik-Bibliothek (Option -lm“) ” erfolgt. Für das vollständige Übersetzen des Programms benötigen wir statt eines gccAufrufs nun drei Aufrufe. Genau hier kommt das Programm make“ ins Spiel, das ” diesen Ablauf intelligent automatisiert – intelligent deshalb, weil nur die wirklich erforderlichen Schritte durchgeführt werden. Die Make-Datei für das PrimzahlBeispiel könnte wie folgt aussehen: 1 2 # # Makefile
24 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1 Einführung # primzahl: primz_haupt.o primz_math.o gcc primz_haupt.o primz_math.o -lm \ -o primzahl primz_haupt.o: primz_haupt.c primz_math.h gcc -c primz_haupt.c primz_math.o: primz_haupt.c gcc -c primz_math.c clean: rm -f primz_haupt.o primz_math.o Wird diese Make-Datei als Makefile“ oder makefile“ abgespeichert, so kann das ” ” Programm primzahl“ einfach durch den Aufruf ” make erzeugt werden. Da es sich bei dem Programm make“ um ein gängiges Werk” zeug handelt, könnte man den Quelltext für das Primzahl-Programm in dieser Form (z.B. als tar-Archiv verschnürt) an andere Linux- oder Unix-Anwender weitergeben und ziemlich sicher sein, dass sie in der Lage wären, das Programm zu kompilieren. Das obige Beispiel für ein Makefile zeigt die rudimentäre Grundfunktion von make“. Das Werkzeug bietet aber noch viele Möglichkeiten, die das Erstellen ” von Makefiles für wirklich große Projekte – wie beispielsweise den Linux-Kernel – vereinfachen. Einen kleinen Einblick soll Ihnen das folgende Makefile geben, das ebenfalls das Primzahl-Programm kompiliert: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # # Makefile2 # TARGET OBJECTS HEADERS LIBS = = = = primzahl primz_haupt.o primz_math.o primz_math.h -lm $(TARGET): $(OBJECTS) gcc $ˆ $(LIBS) -o $@ primz_%.o: primz_%.c $(HEADERS) gcc -c $< -o $@
1.5 Weiterführende Informationen 15 16 17 25 clean: rm -f $(OBJECTS) In den Zeilen 5 bis 8 werden Konstanten definiert – ähnlich den #defineAnweisungen in C. Der Vorteil ist, dass die projektspezifischen Dateinamen nur noch hier vorkommen; im restlichen Makefile tauchen nur mehr die Namen der Konstanten auf! In den Zeilen 10 und 11 wird festgelegt, wie das eigentliche Ziel (hier das Programm primzahl“) zu generieren ist. Dabei werden die zuvor definierten Kon” stanten mit $(Name)“ eingesetzt. In Zeile 11 steht anstelle der Objektdateien hin” ter dem gcc der Ausdruck $ˆ“. Dies bewirkt, dass alle in der Zeile 10 angegebe” nen Quelldateien an dieser Stelle eingesetzt werden. Zusätzlich steht am Ende der Zeile 11 der Ausdruck $@“. Dieser wird automatisch durch das zu dieser Regel ” gehörende Ziel ersetzt – in diesem Fall also $(TARGET)“ bzw. primzal“. ” ” Besonders interessant sind die Zeilen 13 und 14. Hier wird nämlich keine bestimmte Zieldatei beschrieben, sondern alle Dateien, die mit primz “ anfangen ” und auf .o“ enden! Damit gilt diese Regel für die Objektdateien primz haupt.o“ ” ” und primz math.o“. Bei der Angabe der Quelldateien hinter dem Doppelpunkt ” steht wiederum der Ausdruck primz_%“. Das Prozentzeichen wird an dieser ” Stelle durch den gleichen Text ersetzt wie in der Zieldatei vor dem Doppelpunkt. Damit wird den verschiedenen Zieldateien automatisch die passende Quelldatei zugeordnet. In Zeile 14 steht als Quelldatei für den C-Compiler der Ausdruck $<“. Das Programm make ersetzt diesen Ausdruck durch die erste der in Zeile 13 ” angegebenen Quelldateien, also primz_%.c“. ” Auf diese Weise lassen sich Makefiles für Programme mit vielen Objektdateien auf wenige Zeilen reduzieren! 1.5 Weiterführende Informationen Im Folgenden werden zahlreiche Werkzeuge und Funktionsbibliotheken angesprochen, deren vollständige Beschreibung den Rahmen des Buches bei weitem sprengen würde. Daher konzentrieren wir uns darauf, für die vielen verschiedenen Bereiche (Device-Programmierung, Prozesskommunikation, Netzwerk- und Grafikprogrammierung usw.) jeweils einen Einstieg zu bieten. Wenn Sie die verschiedenen Gebiete mit eigenen Programmen weiter vertiefen, werden Sie immer wieder auf Fragen stoßen, deren Antwort Sie nicht in unserem Buch finden. Aus diesem Grund soll in den folgenden Abschnitten gezeigt werden, wie man sich anhand der zu Linux gehörenden Hilfe- und Include-Dateien an die Möglichkeiten des Systems weiter herantasten“ kann. ”
26 1 Einführung 1.5.1 Die Unix-Online-Hilfen man“, xman“ und tkman“ ” ” ” Eine der wichtigsten Hilfen zur Beschaffung weiterer Informationen ist das Programm man“ (Kurzform für Manual). Neben der Dokumentation fast aller Linux” Programme stellt man“ auch eine ausführliche Beschreibung der meisten C” Funktionen zur Verfügung. Die Beschreibungstexte werden auch als Man-Pages“ ” bezeichnet. Leider beinhalten einige Linux-Distributionen nur einen Teil der ManPages, oder es wird voreingestellt nur eine kleine Auswahl installiert.1 Bei SuSELinux müssen beispielsweise die beiden Pakete man“ und man-pages“ aus” ” gewählt werden, um Man-Pages lesen zu können (siehe auch Abschnitt 1.2.1), wobei die Man-Pages zu X11 in einem separaten Paket namens xorg-x11-man“ ” enthalten sind. Damit sie sich korrekt darstellen lassen, werden außerdem die Tools groff“ und less“ benötigt, was aber durch die Paketabhängigkeiten au” ” tomatisch berücksichtigt werden sollte. Zur Anzeige der Man-Page einer C-Funktion oder eines Linux-Programms kann man“ einfach mit dem Programm- oder Funktionsnamen als Parameter aufgeru” fen werden. So liefert beispielsweise man gcc eine Beschreibung des C-Compilers mit seinen Parametern. In gleicher Weise erhält man mit man fprintf eine Erläuterung der entsprechenden C-Funktion (aus der libc) und damit eng verwandter Funktionen. Bitte beachten Sie auch den Hinweis SIEHE AUCH“ ” (bzw. SEE ALSO“) jeweils am Ende der Hilfe-Seiten. ” Häufig kommt es jedoch vor, dass Sie nicht genau wissen, wie die Funktion oder das Programm heißt, das Sie einsetzen möchten. Beispiel: Sie wollen einen Grafikbereich kopieren und suchen eine entsprechende Funktion aus der GrafikFunktionsbibliothek. In diesem Fall können mit der Option -k“ alle Hilfeseiten ” nach einem Keyword (Schlüsselwort) durchsucht werden:1 > man -k copy area XCopyArea (3x) XCopyPlane (3x) - copy areas - copy areas Die erste Angabe in jeder Zeile gibt den genauen Namen der Funktion (oder des Programms) an. Danach folgt in Klammern die Sektion, in der diese Funktion beschrieben ist. Als Letztes folgt eine stichwortartige Beschreibung der Funktion. 1 Glüchlicherweise gibt es eine vollständige Online-Sammlung der Man-Pages im Internet, siehe [6]. 1 Vorausgesetzt, dass eine entsprechende Datenbank mit mandb“ angelegt wurde. Alternativ zu man ” ” -k“ kann auch der Befehl whatis“ verwendet werden, der wiederum eine mit makewhatis“ er” ” stellte Datenbank benötigt.
1.5 Weiterführende Informationen 27 Mit man XCopyArea“ könnte in diesem Beispiel dann die konkrete Beschreibung ” dieser Funktion angefordert werden. Manchmal existieren zu einem Funktions-/Programmnamen mehrere Beschreibungen in unterschiedlichen Sektionen, beispielsweise dann, wenn es eine CFunktion und ein Shell-Programm gleichen Namens gibt: > man -k sleep sleep (1) - delay for a specified amount of time sleep (3) - Sleep for the specified number of seconds Sektion 1 enthält hier die Beschreibung des Shell-Kommandos, Sektion 3 die der gleichnamigen C-Funktion. In diesem Fall kann über die Angabe der Sektion gezielt eine der beiden Seiten abgerufen werden: man 3 sleep Abbildung 1.13: xman – eine grafische Benutzerschnittstelle für man“ ” In der Tabelle 1.1 sind die Sektionen der Online-Hilfe aufgelistet. Um die Suche nach der richtigen Beschreibung zu vereinfachen, kann auch eine grafische Benutzerschnittstelle wie xman“ oder tkman“ verwendet werden (siehe Abbil” ” dung 1.13). xman“ listet – nach Sektionen geordnet – alle verfügbaren Hilfe-Texte ” auf. Durch Anklicken der entsprechenden Funktion können Sie die gewünschte Beschreibung abrufen.
28 1 Einführung Tabelle 1.1: Beschreibung der Sektionen der Online-Hilfe man“ ” Sektion 1 2 3 4 5 6 7 8 Beschreibung Shell-Kommandos System-Aufrufe Funktionen Devices Dateiformate Spiele Verschiedenes System-Administration 1.5.2 Ein Blick hinter die Kulissen: Die Include-Dateien Nicht immer bietet die Online-Hilfe man“ zu den C-Funktionen auch die genaue ” Beschreibung der relevanten Strukturen. So liefern oder benötigen fast alle Funktionen der gepufferten Ein-/Ausgabe (z.B. fopen(), fclose(), fprintf(), . . . ) einen Zeiger auf eine Struktur vom Typ FILE“. Schaut man einmal in die für diese ” Klasse von Funktionen relevante Include-Datei /usr/include/stdio.h“, so findet ” sich dort die Typendefinition: typedef struct _IO_FILE FILE; FILE“ ist also identisch mit der Struktur struct IO FILE“. Diese Struktur wie” ” derum ist in der Include-Datei /usr/include/libio.h“ definiert: ” struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */
1.5 Weiterführende Informationen 29 char *_IO_backup_base; /* Pointer to 1st valid char. of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _blksize; _IO_off_t _old_offset; /* This used to be _offset but it’s too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; Wie man sieht, ist hier nicht mit Kommentaren gespart worden. Auf diese Weise kann man sich eine ganze Reihe nützlicher Informationen beschaffen. Hilfreich ist hier auch die Verwendung des Programms grep“, mit dem Sie alle ” Include-Dateien nach bestimmten Zeichenketten durchsuchen können, z.B.: > grep ’typedef .* FILE;’ /usr/include/*.h /usr/include/stdio.h:typedef struct IO FILE FILE; Ist nicht bekannt, ob die entsprechende Include-Datei in einem Unterverzeichnis (z.B. sys“ oder linux“) liegt, so kann grep auch mit dem Befehl find“ kombiniert ” ” ” werden: grep ’typedef .* FILE;’ ‘find /usr/include -follow -name "*.h"‘ Damit lässt sich relativ schnell die gesuchte Definition aufspüren.

Kapitel 2 Arbeiten mit einer Entwicklungsumgebung Nachdem in Kapitel 1 der Umgang mit den Programmierwerkzeugen auf der ” Kommandozeile“ beschrieben wurde, soll in diesem Kapitel das Arbeiten mit einer integrierten Entwicklungsumgebung gezeigt werden. Dafür wurden die Programme Anjuta“ und KDevelop“ ausgewählt. Abschließend reißen wir noch ” ” kurz den Einstieg in das Tool Eclipse“ an. ” 2.1 Anjuta Die Entwicklungsumgebung Anjuta“ ist an die Oberfläche GNOME ange” lehnt und unterstützt daher auch im Besonderen die Erstellung von GNOMEApplikationen. Shell-Programme ohne grafische Oberfläche lassen sich selbstverständlich ebenfalls mit Anjuta erstellen. Anjuta ist viel zu umfangreich, um in diesem Buch vollständig beschrieben werden zu können. In den folgenden Abschnitten soll stattdessen Schritt für Schritt die Erstellung eines einfachen Beispielprogramms unter dieser Entwicklungsumgebung gezeigt werden. 2.1.1 Ein neues Projekt anlegen Nach dem Starten der Entwicklungsumgebung mit dem gleichnamigen Befehl > anjuta öffnet sich das Hauptfenster, das eine ganze Reihe von Menüs und Schaltflächen, aber noch kein Editor-Fenster für die Eingabe von Quelltexten enthält. Um ein neues Programm zu erstellen, sollte man zunächst ein neues Projekt anlegen. Dies
32 2 Arbeiten mit einer Entwicklungsumgebung geschieht mit dem Menüpunkt Datei/neues Projekt“. Damit wird der Anwen” ” dungsdruide“ gestartet, der in einem Dialog die Art des neu zu erstellenden Projekts und dessen Eigenschaften abfragt und daraus das Grundgerüst des Projekts erstellt. Abbildung 2.1: Die verschiedenen Projekttypen bei Anjuta Als Projekttyp wählen Sie bitte Generic/Terminal Projcet“ aus (siehe Abbil” dung 2.1) und geben als Projektname primzahl“ ein. Das anschließend erschei” nende Eingabefeld für eine Projektbeschreibung können Sie leer lassen. Unter Projekt Optionen“ sollten Sie die Unterstützung von Gettext deaktivieren. Da” nach werden Sie um die Bestätigung Ihrer Eingaben gebeten:
2.1 Anjuta 33 Bestätigen Sie die folgenden Informationen: Projektname: Projekttyp: Zieltyp: Quelltyp: Version: Autor: Sprache: Gettext Support: primzahl GENERIC EXECUTABLE primzahl 0.1 C Nein Mit der Schaltfläche Abschließen“ bestätigen Sie die Einstellungen und gelangen ” wieder zum Hauptfenster von Anjuta, das jetzt ein Fenster mit der Projektstruktur und eines mit Compiler-Ausgaben enthält (siehe Abbildung 2.2). Abbildung 2.2: Das neu angelegte Projekt primzahl“ ” 2.1.2 Eingabe der Quelltexte Durch einen Doppelklick auf main.c“ in dem Fenster mit der Projektübersicht ” wird ein Editor-Fenster geöffnet, in dem diese Datei bearbeitet werden kann. Ersetzen Sie hier bitte den von Anjuta standardmäßig vorbereiteten Hello-WorldQuelltext durch Folgendes:
34 2 Arbeiten mit einer Entwicklungsumgebung /* main.c */ # include <stdio.h> # include "primzahl.h" int main() { int zahl; for (zahl=1; zahl<=100; zahl++) if (ist_primzahl(zahl)) printf("%d\n", zahl); return(0); } Über den Menüpunkt Datei/Speichern“ können Sie die Änderungen speichern. ” Als Nächstes soll der Quelltext der Funktion ist primzahl()“ in einer zweiten Da” tei erstellt werden. Dazu wählen Sie den Menüpunkt Datei/Neu“ und geben in ” das neue Editor-Fenster folgenden Quelltext ein: /* primzahl.c */ # include <math.h> int ist_primzahl(int zahl) { int teiler=2; while (teiler <= sqrt(zahl)) { if (zahl % teiler == 0) return(0); /* ’zahl’ ist keine Primzahl */ teiler++; } return(1); /* ’zahl’ ist eine Primzahl */ } Speichern Sie diese Datei dann mit Datei/Speichern unter...“ im Verzeichnis ” Projects/primzahl/src“ als primzahl.c“ ab. ” ” In der gleichen Weise erstellen Sie die zugehörige Include-Datei primzahl.h“: ”
2.1 Anjuta 35 /* primzahl.h */ int ist_primzahl(int zahl); Jetzt können die neu angelegten Dateien dem Projekt hinzugefügt werden. Dazu wählen Sie den Menüpunkt Projekt/Add File/Source-Datei“ für die Datei ” primzahl.c“ und Projekt/Add File/Include-Datei“ für die Datei primzahl.h“. ” ” ” Jetzt sollte die Struktur des Projektes so aussehen, wie in Abbildung 2.3 dargestellt. Abbildung 2.3: Die fertige Struktur des Projektes primzahl“ ” 2.1.3 Kompilieren und Starten des Beispiels Um jetzt aus den Quelltexten des Beispielprojektes primzahl“ ein ausführba” res Programm zu erzeugen, wählen Sie bitte den Menüpunkt Generieren/Ge” nerieren“. Danach erscheint im Ausgabefenster am unteren Rand eine Reihe von Compiler-Meldungen, die – wenn alles richtig gemacht wurde – mit folgendem Text enden: Übersetzungsvorgang beendet..........erfolgreich Benötigte Zeit: 2 Sek. Zum Starten des Beispielprogramms können Sie jetzt das in Anjuta eingebaute Terminal-Fenster verwenden. Klicken Sie dazu bitte auf den Reiter Terminal“ am ” unteren Rand des Ausgabe-Fensters und geben in dem Fenster
36 2 Arbeiten mit einer Entwicklungsumgebung > primzahl ein. Es erscheinen die Primzahlen von 1 bis 97. 2.2 KDevelop Die Desktopumgebung KDE bringt ihr eigenes Entwicklungswerkzeug mit: KDevelop. Das Tool kann über das Desktop-Menü gestartet werden (bei SuSE-Linux Entwicklung/Entwicklungsumgebung/KDevelop C/C++“) oder aus einer Shell ” über das Kommando kdevelop“. ” Um ein neues Projekt in KDevelop anzulegen, wählen Sie den Menüpunkt Pro” ject/New Project...“. Danach öffnet sich ein Fenster zur Auswahl des Projekttyps (siehe Abbildung 2.4). Wählen Sie hier bitte den Typ C/Simple Hello world pro” gram“ aus und tragen als Application name“ den Projektnamen primzahlen“ ” ” ein. Abbildung 2.4: Die Auswahl des Projekttyps bei KDevelop Nach der Auswahl des Projekttyps fragt KDevelop nach General Options“ – ” hier können Sie einfach auf next“ klicken – und nach dem Version control sy” ” stem“, das Sie auf none“ stellen sollten. Danach zeigt KDevelop die Templates ”
2.2 KDevelop 37 für Header- und Quelltextdateien an. Auch hier können Sie einfach auf next“ ” bzw. finish“ klicken. Danach befinden Sie sich wieder in dem Hauptfenster ” von KDevelop mit geöffnetem Editorbereich. Dort ist bereits ein Hello world“” Quelltext eingetragen. Ersetzen Sie diesen bitte durch den Primzahlen-Quelltext von Seite 34 oben. Danach speichern Sie die Datei bitte über File/Save“. ” Als Nächstes benötigen wir noch den Quelltext mit der Funktion ist primzahl(). Dazu wählen Sie bitte den Menüpunkt File/new“ und geben als Dateinamen ” primz math.c“ ein (Abbildung 2.5). ” Abbildung 2.5: Eine weitere Quelltextdatei in KDevelop anlegen Nach dem Bestätigen dieses Dialogs mit OK“ öffnet sich ein Fenster mit dem ” Automake Manager“. Bestätigen Sie auch diesen Dialog mit OK“. ” ” Um alle zum Projekt gehörenden Dateien aufzulisten, klicken Sie die Schaltfläche File List“ am linken Rand des Hauptfensters an. Danach können Sie die neue Da” tei primz math.c“ durch einen Klick auf den Dateinamen in dem Editorbereich ” öffnen. Geben Sie hier bitte den entsprechenden Quelltext von Seite 34 unten ein. Legen Sie bitte in gleicher Weise die Header-Datei primz math.h“ an und geben ” dort Folgendes ein: /* primz_math.h */ int ist_primzahl(int zahl); Damit sind nun alle Quelltexte für unser kleines Projekt eingegeben, und das KDevelop-Hauptfenster müsste sich wie in Abbildung 2.6 darstellen. Für das Übersetzen des Primzahlprogramms wird die Bibliothek libm“ benötigt, ” es muss also die Compiler-Option -lm“ angegeben werden. Dies geschieht ” bei KDevelop über den Menüpunkt Project/Project Options“. Das Fenster mit ” den Projektoptionen zeigt am linken Rand eine Auswahl von Optionsgruppen (vgl. Abbildung 2.7).
38 2 Arbeiten mit einer Entwicklungsumgebung Abbildung 2.6: Das Hauptfenster von KDevelop nach Eingabe aller Dateien Abbildung 2.7: Über die Project Options“ können die benötigten Libraries angegeben ” werden (hier -lm“). ”
2.3 Eclipse + C Development Tooling (CDT) 39 Wenn Sie hier die Configure Options“ auswählen, erhalten Sie unter anderem ein ” Eingabefeld für die Linker flags (LDFLAGS)“. Dort muss für das Primzahlpro” gramm die Option -lm“ eingetragen werden. ” Um die Quelltexte in ein ausführbares Programm zu übersetzen, wählen Sie nun den Menüpunkt Build/Build Project“. KDevelop fragt an dieser Stelle nach, ” ob mit automake“ ein Makefile automatisch erstellt werden soll (siehe Abbil” dung 2.8). Abbildung 2.8: KDevelop bietet den Einsatz von automake“ an. ” Nach erfolgreichem Übersetzen der Quelltexte kann das Primzahlprogramm nun über den Menüpunkt Build/Execute Program“ oder über das Zahnrad-Icon über ” dem Editorbereich gestartet werden. Die Programmausgaben – in diesem Fall also die Primzahlen bis 97 – erscheinen in einem separaten Terminal-Fenster, das von KDevelop geöffnet wird. 2.3 Eclipse + C Development Tooling (CDT) Im Folgenden wird der Einstieg in die Arbeit mit der Eclipse-Umgebung beschrieben, für weiterführende Details sei auf die umfangreiche Dokumentation und die Tutorials im Internet verwiesen (z. B. [3] und [5]). Das Programmpaket Eclipse ist in Java geschrieben und wurde ursprünglich für die Entwicklung von Java-Programmen konzipiert. Es ist jedoch so angelegt, dass die programmiersprachenabhängigen Teile als Plug-ins“ geladen werden. So gibt ” es unter anderem das C Development Tooling (CDT) als Plug-in für die Programmierung in C. Um mit Eclipse C-Programme entwickeln zu können, benötigen Sie zwingend dieses Paket. Sollte Ihre Distribution das CDT nicht beinhalten, können Sie das Plug-in von der Eclipse-Hompage laden. In diesem Fall beachten Sie bitte die Hinweise in Abschnitt 2.3.1. Wird Eclipse zum ersten Mal gestartet, öffnet sich das in Abbildung 2.9 dargestellte Auswahlfenster. Für die nächsten Schritte wählen Sie bitte das Icon Work” bench“ aus. So gelangen Sie in das Hauptfenster von Eclipse.
40 2 Arbeiten mit einer Entwicklungsumgebung Abbildung 2.9: Das Auswahlfenster beim ersten Start von Eclipse 2.3.1 Plug-ins einbinden Um zusätzliche Plug-ins wie das CDT in Eclipse einzubinden, kann aus dem Menü der Eclipse-Workbench der Eintrag Help/Software Updates/Manage ” Configuration“ gewählt werden. In dem Konfigurationsfenster muss anschließend der Button zur Anzeige der nicht aktivierten Funktionen ( Show Disabled ” Features“) angeklickt werden: Im linken Teil des Fensters erscheint dann die Liste mit Plug-ins und im rechten ein Beschreibungstext mit Verknüpfungen für verschiedene Aktionen. Hier kann für ein links selektiertes Plug-in die Aktion Aktivieren“ bzw. Enable“ gewählt ” ” werden. 2.3.2 Ein neues Projekt anlegen Ähnlich den beiden zuvor vorgestellten Entwicklungsumgebungen bietet auch Eclipse einen Wizard“ für das Erstellen eines neuen Projekts an. Wählt man den ” Menüpunkt File/New/Project...“ aus, öffnet sich das Auswahlfenster für die Art ”
2.3 Eclipse + C Development Tooling (CDT) 41 des Projekts. Nach erfolgreicher Installation des CDT kann hier Standard Make ” C Project“ gewählt werden. Danach müssen Sie in einem weiteren Fenster den Projektnamen eingeben. Ist das neue Projekt angelegt, können Quelltexte neu erstellt ( File/New“) oder ” bereits als Textdatei vorliegende Quelltexte importiert werden ( File/Import“). ” Haben Sie den Quelltext eingegeben oder importiert, können Sie über den Menüpunkt Project/Build Project“ das Programm kompilieren und über den ” Menüpunkt Run“ ausführen. ”

Kapitel 3 Kommandozeilenprogramme In der Linux-Welt gibt es eine Vielzahl leistungsfähiger Programme, die ohne grafische Benutzerschnittstelle auskommen. Die Steuerung dieser Programme erfolgt über die Kommandozeilenparameter und die Standardein- und -ausgabe (vgl. Abschnitt 4.1.2). Der Vorteil liegt zum einen in dem gegenüber einer grafischen Steuerung deutlich geringeren Programmieraufwand – häufig bei gleichem Nutzen. Zum anderen eröffnet sich dadurch die Möglichkeit, verschiedene Programme über Pipes oder Shell-Skripte sehr einfach miteinander zu verknüpfen. Das macht die Linux-Shell zu einem mächtigen Werkzeug. Voraussetzung ist jedoch, dass sich die Programme – wie unter Linux/Unix üblich – mit Hilfe geeigneter Parameter und Optionen entsprechend steuern lassen. In diesem Kapitel wird die Auswertung der Kommandozeilenparameter und die Bedeutung des Rückgabewertes beschrieben, ebenso wie typische Optionen und (Fehler-)Meldungen von Linux-Programmen. Diese Konventionen sollten im Übrigen auch für X11-Programme gelten (vgl. Kapitel 8), da auch diese Programme häufig aus der Shell (d.h. aus einem Terminal-Fenster heraus) gestartet werden. Weitere Themen des Kapitels sind die automatische Anpassung an die eingestellte Landessprache und die Möglichkeiten für erweiterte Ausgabesteuerung. 3.1 Parameter und Rückgabewert der Funktion main() Werfen wir zunächst einen Blick auf die typische Deklaration der Funktion main(), die gewissermaßen das Kernstück eines jeden C-Programms darstellt: int main(int argc, char *argv[])
44 3 Kommandozeilenprogramme Es handelt sich bei main() also um eine Funktion mit einen Rückgabewert vom Typ Integer. Aber welche Bedeutung hat dieser Rückgabewert, wo doch das Programm nach der Rückkehr aus main() beendet ist? 3.1.1 Die Bedeutung des Rückgabewertes von main() Unter Linux kann kein Prozess von allein“ entstehen, sondern jeder Prozess hat ” einen Eltern“-Prozess, der den Kind“-Prozess startet. Wenn Sie in einer Shell ” ” (also z.B. in einem Terminal-Fenster) einen Befehl wie ls“ eingeben, startet der ” Shell-Prozess einen neuen Kind-Prozess, in dem das Programm ls“ abläuft. ” Nach Beendigung des Programms ls“ wird der Shell-Prozess darüber infor” miert, dass das Programm beendet wurde und mit welchem Resultat. Und genau dieses Resultat entspricht dem Rückgabewert der Funktion main(), wie sie im Programm ls“ realisiert ist. ” Bei erfolgreicher Beendigung des Programms sollte immer der Wert 0 zurückgegeben werden. Ist ein Fehler aufgetreten, wurden z.B. falsche Parameter angegeben, so sollte das Programm dies durch einen Rückgabewert 6= 0 signalisieren, damit auch der Eltern-Prozess entsprechend reagieren kann. In einem Shell-Skript können Sie mit $?“ den Rückgabewert des zuletzt aus” geführten Programms abfragen (die Benutzereingaben sind schräg dargestellt): > ls xyz; echo "Resultat von ls: $?" ls: xyz: Datei oder Verzeichnis nicht gefunden Resultat von ls: 1 3.1.2 Die Variablen argc und argv Zunächst sei einmal erwähnt, dass die Übergabeparameter der Funktion main() nicht zwingend argc“ und argv“ heißen müssen – die Wahl der Variablenna” ” men ist für main() ebenso wie für jede andere Funktion beliebig. Wenn es jedoch nicht Ihr oberstes Bestreben ist, den Quelltext unleserlich zu gestalten, sollten Sie auf eine Änderung dieser etablierten Bezeichnungen unbedingt verzichten. Die Variable1 argc gibt die Anzahl der Kommandozeilenparameter einschließlich des Programmnamens selbst an. Somit hat argc mindestens den Wert 1. Die Variable argv ist ein Zeiger auf ein Feld (engl. Array) mit der in argc angegebenen Anzahl von Elementen (siehe Abbildung 3.1). Das erste Element dieses Feldes (also argv[0]) ist wiederum ein Zeiger auf eine Zeichenkette, nämlich den Namen (und ggf. den Pfad) des Programms selbst. Das Element argv[1] zeigt auf die 1 argc ist tatsächlich eine Variable und keine Konstante. Es kann unter bestimmten Umständen zweckmäßig sein, ihren Wert zu verändern.
3.1 Parameter und Rückgabewert der Funktion main() 45 Zeichenkette mit dem ersten Kommandozeilenparameter. Das letzte Element von argv[] enthält eine Null. argv - argv[0] argv[1] argv[2] .. . argv[ N ] 0L - Programmname - 1. Parameter - 2. Parameter - letzter Parameter Abbildung 3.1: Übergabe der Kommandozeilenparameter mittels argv Übrigens: Das Feld argv[] darf vom Programm auch verändert werden. So können z.B. Elemente entfernt und die nachfolgenden Einträge aufgerückt“ wer” den. Verschiedene Funktionsbibliotheken, z. B. die libgtk, machen davon Gebrauch. 3.1.3 Auswerten der Kommandozeilenparameter Am Anfang jedes Programms sollten zunächst die Kommandozeilenparameter ausgewertet werden. Man unterscheidet hier zwischen Argumenten und Optionen. Bei den Argumenten handelt es sich meistens um Dateinamen, während Optionen als eine Art Schalter“ zu betrachten sind, über die sich das Verhalten des Pro” gramms steuern lässt. Unter Unix hat es sich etabliert, dass Optionen jeweils aus einem Zeichen bestehen und durch ein vorangestelltes -“ gekennzeichnet sind. ” Beispiel: rm -f primz_haupt.o primz_math.o Bei diesem Beispiel sind primz haupt“ und primz math“ die Argumente – ” ” nämlich die zu löschenden Dateien –, während die Option -f“ das Programm rm ” veranlasst, falls eine der angegebenen Dateien nicht existiert, keine Fehlermeldung auszugeben. Es sei noch erwähnt, dass Optionen ihrerseits auch wieder Argumente besitzen können – ein Beispiel hierfür ist die Option -o“ des C-Compilers gcc ” (siehe Seite 8). In der Linux-Welt setzten sich zunehmend Langtextoptionen durch, etwa in der Form --help“. Diese sind zwar leichter lesbar, bedeuten für den Anwender aber ” entsprechend mehr Tipparbeit. Daher wird im Folgenden nur die kurze Form betrachtet. Will man sich bei der Auswertung der Parameter an die üblichen Konventionen halten, also u.a. eine beliebige Reihenfolge der Optionen zulassen, so kann das manuelle“ Auswerten der Kommandozeile schon mit relativ viel Aufwand ver” bunden sein, insbesondere dann, wenn auch Optionen vorgesehen sind, die selbst
46 3 Kommandozeilenprogramme wieder Argumente erfordern. Die C-Funktionsbibliothek libc bietet hier glücklicherweise eine Funktion, die dem Programmierer eine ganze Menge Arbeit abnimmt: int getopt(int argc, char *argv[], char *optstring); Neben den bekannten Parametern argc und argv wird dieser Funktion die Zeichenkette optstring übergeben, die alle zulässigen Optionen enthält. Ein :“ hin” ter einem Optionszeichen in optstring bedeutet, dass diese Option ein Argument erfordert (z.B. "o:" für die Option -o Dateiname“). Zwei Doppelpunkte ” zeigen an, dass das Argument für diese Option optional, also nicht notwendig ist. Die Funktion getopt() sortiert zunächst das Feld argc[] so, dass die Optionen nach vorn und die Argumente nach hinten geschoben werden. Als Rückgabewert liefert die Funktion entweder die erste in der Kommandozeile angegebene Option oder –1, falls keine Option angegeben wurde. Bei jedem weiteren Aufruf liefert getopt() die jeweils nächste angegebene Option in der Kommandozeile. Wurden alle Optionen eingelesen, liefert getopt() –1. Danach gibt die globale Variable optind den Index des ersten Kommandozeilenargumentes im Feld argv an. Die Funktion getopt() führt automatisch eine Fehlerbehandlung durch, d.h. bei ungültigen Optionen oder fehlenden Optionsargumenten werden entsprechende Fehlermeldungen ausgegeben. Ein kleines Beispiel soll das Einlesen der Kommandozeilenparameter und damit die Arbeitsweise von getopt() erläutern: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* read_args.c */ # include <stdio.h> # include <unistd.h> int main(int argc, char *argv[]) { int option, i; char *out_filename=NULL; while ((option = getopt(argc, argv, "ho:")) >= 0) switch (option) { case ’h’ : printf("Usage: %s [-o output-file] " "[input-file ...]\n", argv[0]); return(0); case ’o’ : out_filename = optarg; break; case ’?’ : return(1); /* unbekannte Option */
3.1 Parameter und Rückgabewert der Funktion main() 22 23 24 25 26 27 28 29 30 31 47 } for (i=optind; i<argc; i++) printf("’%s’\n", argv[i]); if (out_filename) printf("output is ’%s’\n", out_filename); return(0); } Der dritte Parameter beim Aufruf von getopt() in Zeile 13, also die Optionsliste "ho:", gibt zwei zulässige Optionen vor: -h“ und -o“, wobei Letztere ein ” ” Argument erfordert. Erkennt getopt() die Option -o“, so zeigt die globale Va” riable optarg“ auf das Argument dieser Option (siehe Zeile 19). Stößt getopt() ” auf eine unzulässige, also in der Optionsliste nicht angegebene Option, liefert die Funktion das Zeichen ?“, was bei diesem Beispielprogramm zu einem Abbruch ” mit Fehlercode 1 führt (Zeile 21). Nach Abarbeitung aller Optionen mit Hilfe der while-Schleife (Zeile 13 bis 22) erfolgt die Auswertung der Argumente (Zeile 24 und 25), die von getopt() ans Ende des Feldes argv[] gestellt wurden. 3.1.4 Achtung: Platzhalter! Häufig macht man bei der Angabe von Dateinamen Gebrauch von so genannten Platzhaltern, insbesondere wenn ein Befehl auf mehrere Dateien mit bestimmten Übereinstimmungen im Dateinamen angewendet werden soll. Beispiel: rm *.o wird alle Dateien löschen, die auf .o“ enden. Doch was geschieht, wenn sol” che Platzhalter angewendet werden? Während es bei manchen Betriebssystemen Aufgabe der Programme selbst ist, diese Platzhalter aufzulösen“, also durch die ” entsprechenden Dateinamen zu ersetzen, übernimmt unter Linux die Shell diese Aufgabe (z.B. die bash). Das bedeutet für das obige Beispiel, dass die Shell vor der Ausführung des Programms rm“ die Kommandozeile ersetzt, z.B. durch: ” rm primz_haupt.o primz_math.o Dies erspart dem Programmierer unter Linux den Programmieraufwand für das Durchsuchen des Verzeichnisses nach übereinstimmenden Dateinamen. Auf der anderen Seite sollte der Programmierer aber auch berücksichtigen, dass ein Kommandozeilenparameter unter Umständen durch mehrere Dateinamen ersetzt wird. In diesem Zusammenhang ist folgende Syntax für die Kommandozeilenparameter eines Programms möglichst zu vermeiden:
48 3 Kommandozeilenprogramme mein programm Eingabedatei [Ausgabedatei] Das Programm kann also wahlweise mit einem oder mit zwei Argumenten aufgerufen werden. Bei Angabe des zweiten Arguments wird dieses als Ausgabedatei interpretiert. Anderenfalls werden die Ausgaben z.B. an den StandardAusgabekanal geschickt. Verwendet der Anwender jetzt dieses Programm in Verbindung mit einem Platzhalter für die Eingabedatei (um sich Tipparbeit zu sparen): mein_programm primz*.c so wird die Shell dies möglicherweise ersetzen durch: mein_programm primz_haupt.c primz_math.c und damit würde unbeabsichtigt die zweite Datei als Ausgabedatei geöffnet und überschrieben! Besser ist es daher, für eine optionale Ausgabedatei eine entsprechende Option mit Argument vorzusehen, wie in dem Beispiel aus Abschnitt 3.1.3. Auch sollte die Reihenfolge der Argumente nach Möglichkeit keine Rolle spielen, da die Shell beim Ersetzen eines Platzhalters durch mehrere Dateinamen keine bestimmte Reihenfolge einhält.1 3.2 Konventionen für Kommandozeilenprogramme Um dem Linux-Anwender die Kommandozeilen-Bedienung von Programmen zu erleichtern, sollten sich alle Programme an gewisse Linux-typische Konventionen halten. Das sind natürlich keine Vorschriften“ – es obliegt letztendlich der Ver” antwortung des Programmierers, inwieweit er sich daran hält. Es handelt sich dabei mehr um nützliche Tipps für das Erstellen gut bedienbarer Programme, die eine Vielzahl von Anwendern erreichen sollen. 3.2.1 Ein Muss: Die Hilfe-Option Häufig stößt man bei freier Software (public domain) auf kommandozeilenbasierte Programme, die laut Beschreibung (z.B. im Internet) genau die gesuchte Lösung für ein bestimmtes Problem darstellen. Doch wie war noch gleich die Syntax für die Argumente und Optionen? Häufig bleibt da nur die Suche nach der zugehörigen README“-Datei. ” Gerade wenn man das Programm schon einmal angewendet hat, sich aber z.B. nicht mehr ganz sicher über eine Option ist, ist das Nachforschen in Hilfetexten 1 Die bash sortiert die Namen alphabetisch. Das muss aber nicht der vom Anwender beabsichtigten Reihenfolge entsprechen.
3.2 Konventionen für Kommandozeilenprogramme 49 etwas umständlich. Daher sollte jedes Programm über eine Kurzhilfe verfügen, die sozusagen in das Programm eingebaut ist. Aus dieser Hilfe sollten der Zweck des Programms sowie die Syntax des Kommandozeilenaufrufs hervorgehen. Ferner sollte man diese Hilfe entweder durch die Option -h“ oder die Langversion ” --help“ aufrufen können. Ein schönes Beispiel ist das Programm dd“, das eine ” ” ganze Reihe von Optionen besitzt, die nicht immer leicht zu merken sind. Mit dd ” --help“ erhält man jedoch die folgende ausführliche Beschreibung: Usage: dd [OPTION]... Copy a file, converting and formatting according to the options. bs=BYTES cbs=BYTES conv=KEYWORDS count=BLOCKS ibs=BYTES if=FILE obs=BYTES of=FILE seek=BLOCKS skip=BLOCKS --help --version force ibs=BYTES and obs=BYTES convert BYTES bytes at a time convert the file as per the comma separated keyword list copy only BLOCKS input blocks read BYTES bytes at a time read from FILE instead of stdin write BYTES bytes at a time write to FILE instead of stdout skip BLOCKS obs-sized blocks at start of output skip BLOCKS ibs-sized blocks at start of input display this help and exit output version information and exit BYTES may be followed by the following multiplicative suffixes: xM M, c 1, w 2, b 512, kD 1000, k 1024, MD 1,000,000, M 1,048,576, GD 1,000,000,000, G 1,073,741,824, and so on for T, P, E, Z, Y. Each KEYWORD may be: ascii ebcdic ibm block unblock lcase notrunc ucase swab noerror sync from EBCDIC to ASCII from ASCII to EBCDIC from ASCII to alternated EBCDIC pad newline-terminated records with spaces to cbs-size replace trailing spaces in cbs-size records with newli change upper case to lower case do not truncate the output file change lower case to upper case swap every pair of input bytes continue after read errors pad every input block with NULs to ibs-size Report bugs to <bug-fileutils@gnu.org>. Hier erhält der Anwender die Information, was das Programm leistet, welche Argumente und Optionen es kennt und an wen man mögliche Bugs berichten kann.
50 3 Kommandozeilenprogramme 3.2.2 Fehlermeldungen Genauso wichtig wie eine Kurzhilfe sind sinnvolle und aufschlussreiche Fehlermeldungen, die den Anwender schnell die Ursache des (Bedien-)Fehlers finden lassen. Ein Beispiel für eine unzureichende Fehlermeldung ist die Ausgabe: Bad arguments. Welche Kommandozeilenargumente hat das Programm nicht verstanden? Erscheint diese Meldung bei Abarbeitung eines Shell-Skriptes, ist nicht einmal klar, welches Programm mit fehlerhaften Argumenten aufgerufen worden ist! Dies erschwert die Fehlersuche und -beseitigung ungemein. Wie eine Fehlermeldung sinnvollerweise aussehen sollte, sei hier einmal am Beispiel des Programms cp“ gezeigt: ” > cp primzahl.c cp: Fehlende Zieldatei Versuchen Sie ≫cp --help≪ für weitere Informationen. Die Fehlermeldung gibt hier nicht nur unmissverständlich den Grund des Fehlers an, sondern auch den Namen des Programms selbst. Zusätzlich wird sogar auf die eingebaute Hilfe-Option verwiesen. Fehlermeldungen sollten immer an den Fehlerausgabekanal (stderr, siehe auch Kapitel 4) geleitet werden, damit auch beim Umleiten der Standardausgabe in eine Datei mittels >“ der Anwender die Fehlermeldung sofort zu sehen“ ” ” bekommt. Für gewöhnlich werden in C-Programmen Fehlermeldungen mit Anweisungen der Form fprintf(stderr, "%s: Too many arguments.\n", argv[0]); ausgegeben. Optional kann der Programmname (argv[0]) mit Hilfe der Funktion basename(argv[0])“ um Pfadangaben bereinigt werden, sodass z.B. statt ” /home/martin/bin/my prog: Too many arguments.“ hier nur my prog: ” ” Too many arguments.“ ausgegeben wird. Diese Funktion ist in der IncludeDatei string.h deklariert. Bei Fehlermeldungen im Zusammenhang mit dem Öffnen, Lesen oder Schreiben von Dateien ist es außerdem sinnvoll, die Funktion perror() aufzurufen, um den genauen Grund des Dateizugrifffehlers anzugeben:
3.2 Konventionen für Kommandozeilenprogramme 51 void perror(const char *s); Diese Funktion gibt die als Parameter angegebene Zeichenkette in den StandardFehlerkanal aus, fügt einen Doppelpunkt an und gibt dahinter die Beschreibung der Fehlerursache – z.B. Datei oder Verzeichnis nicht gefunden“ – aus. ” Wird der Funktion perror() eine leere Zeichenkette übergeben, so erfolgt nur die Ausgabe der Fehlerursache, ein Doppelpunkt wird in diesem Fall nicht vorangestellt. Soll die Fehlerursache nicht (nur) ausgegeben, sondern z.B. bei Programmen mit grafischer Bedienoberfläche in einem Fenster dargestellt werden, so kann der Fehlertext auch über die Funktion strerror() ermittelt werden. Als Parameter müssen Sie hier die globale Variable errno angeben: # include <errno.h> # include <string.h> char *error_text = strerror(errno); Wie bereits in Abschnitt 3.1.1 beschrieben, sollte im Falle eines Fehlers, der zum Abbruch des Programms führt, ein entsprechender Rückgabewert 6= 0 gesetzt werden. Tritt der Fehler nicht direkt in main(), sondern innerhalb einer Unterfunktion auf, so kann dies mit der Funktion exit(Rückgabewert); geschehen. 3.2.3 Eigene Manpages erstellen Zu einem guten Programm gehört auch eine man“-Hilfeseite oder auch Man” ” page“. Solche Dateien finden sich in der Regel in dem Verzeichnis /usr/man“ ” oder /usr/local/man“. In der Umgebungsvariablen MANPATH sind die Ver” zeichnisse eingetragen, die das Programm man“ durchsucht. Zu dem vollständi” gen Pfad gehört das Unterverzeichnis, dessen Name sich aus man“ + Sekti” onsnummer zusammensetzt. So findet sich z. B. der Hilfetext zum Shell-Befehl sleep“ in Sektion 1 unter: ” /usr/man/man1/sleep.1 Am Ende des Dateinamens ist noch einmal die entsprechende Sektionsnummer angehängt. Häufig sind die Dateien mit gzip komprimiert, was an der Endung .gz“ zu erkennen ist. ” Manpages sind reine Textdateien und können mit jedem Editor erstellt werden. Neben dem eigentlichen Text enthalten die Dateien Steuersequenzen, die u.a. die
52 3 Kommandozeilenprogramme Formatierung beeinflussen. Sie müssen am Zeilenanfang stehen und beginnen mit einem .“. Des Weiteren gibt es einige Steuersequenzen, die mit \“ beginnen und ” ” z. B. Texthervorhebungen bewirken. Diese Sequenzen müssen nicht zwingend am Zeilenanfang stehen. Die wichtigsten fasst Tabelle 3.1 zusammen. Tabelle 3.1: Beschreibung einiger Steuersequenzen für man-Hilfeseiten Sequenz Beschreibung .\" .TH .SH .fi .nf Kommentarzeile Titel, Kopf- und Fußzeile festlegen Überschrift für den nächsten Abschnitt ab hier: Blocksatz ab hier: keine Formatierung \fR \fB \fI \- normaler Text Fettschrift kursiv (oder unterstrichen) Gedankenstrich Der Sequenz für Titel, Kopf- und Fußzeile können bis zu fünf Angaben folgen: .TH "Name" "Sektion" "Datum" "Author / Version" "Thema" Beispiel: .TH SLEEP 3 "Apr. 1993" GNU "Linux Programmer’s Manual" Die Steuerungssequenz für eine Überschrift benötigt als einzige Angabe den Überschrift-Text. Als Beispiel soll hier eine kurze Manpage für das PrimzahlProgramm aus Kapitel 1 dienen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 .\" primzahl.1 .TH primzahl 1 "Feb. 2002" "M. Gräfe" "C und Linux" .SH NAME primzahl \- Primzahlen von 1 bis 100 berechnen. .SH SYNTAX primzahl .SH BESCHREIBUNG .fi Das Programm \fBprimzahl\fR berechnet alle Primzahlen zwischen 1 und 100. Es dient als Beispielprogramm für das erste Kapitel des Buches "\fIC und Linux\fR". .nf .SH AUTOR Martin Gräfe (AuM.Graefe@t-online.de) Als primzahl.1“ abgespeichert lässt sich diese Hilfeseite mit ” man -l primzahl.1
3.3 Programme mehrsprachig auslegen 53 formatieren und anzeigen (Abbildung 3.2). Die Option -l“ ermöglicht es, direkt ” den Dateinamen der Manpage anzugeben, die dargestellt werden soll. Ist die Manpage fertiggestellt, kann sie mit gzip komprimiert und in das entsprechende Verzeichnis (z.B.: /usr/local/man/man1) kopiert werden. Danach lässt sie sich jederzeit mit man primzahl anzeigen. Abbildung 3.2: Ergebnis der selbst geschriebenen Manpage 3.3 Programme mehrsprachig auslegen Die Zeiten, in denen alle Computer-Programme und auch die Betriebssysteme ausschließlich in Englisch waren, sind längst vorbei. Auch die aktuellen Linux-Distributionen präsentieren sich hierzulande in deutschem Gewand“, ” d. h. Menüs und Dialog-Boxen sind zum Großteil deutschsprachig. Die Unterstützung länderabhängiger Formate wird bei Linux unter dem Begriff locale zusammengefasst. Das gleichnamige Shell-Programm liefert Informationen zu den aktuellen Einstellungen:
54 3 Kommandozeilenprogramme > locale LANG=de DE LC CTYPE=de DE LC NUMERIC=de DE LC TIME=de DE LC COLLATE=de DE LC MONETARY=de DE LC MESSAGES=de DE LC ALL= Mit Hilfe der Umgebungsvariablen LANG“ kann allgemein die Auswahl der Lan” dessprache erfolgen, z.B.: > export LANG=en US Das Kürzel en“ steht hier für englisch“, die Erweiterung US“ kennzeichnet, ” ” ” dass es sich um nordamerikanisches Englisch handelt. Diese Angabe kann noch um die Spezifikation des ISO-Zeichensatzes ergänzt werden, z.B.: > export LANG=de DE.ISO-8859-1 Die von Linux unterstützten Ländereinstellungen findet man als Verzeichnisse unter dem Pfad /usr/share/locale“. ” Die Auswahl der Sprach-/Ländereinstellung erfolgt in C-Programmen mit Hilfe der Funktion setlocale(): char *setlocale(int category, char *locale); wobei category eine der Kategorien LC COLLATE, LC CTYPE, LC MESSAGES, LC MONETARY, LC NUMERIC, LC TIME oder LC ALL für alle Kategorien sein muss. Der Parameter locale muss entweder eine gültige Länderkennzeichnung wie de DE“ enthalten oder eine leere Zeichenkette ("") sein, wenn die Länderkenn” zeichnung von der Umgebungsvariablen LANG übernommen werden soll. Das folgende kleine Beispiel soll die Wirkung des setlocale()-Aufrufs verdeutlichen: 1 2 3 4 5 6 7 8 /* sprache.c */ # include <stdio.h> # include <locale.h> int main()
3.3 Programme mehrsprachig auslegen 9 10 11 12 13 14 15 16 55 { setlocale(LC_NUMERIC, "en_US"); printf("PI=%.3f\n", 3.141593); setlocale(LC_NUMERIC, "de_DE"); printf("PI=%.3f\n", 3.141593); return(0); } Nach dem Übersetzen mit gcc sprache.c -o sprache“ kann das Programm ” aufgerufen werden: > sprache PI=3.142 PI=3,142 Offensichtlich führt die gleiche printf()-Anweisung in Zeile 13 des Quelltextes zu einem anderen Ergebnis als in Zeile 11. Das Dezimal-Trennzeichen wird in diesem Beispiel von .“ auf ,“ umgeschaltet. Erreicht wird das durch Ändern der ” ” Locale Category LC NUMERIC“, die – wie der Name schon sagt – das Zahlenformat ” bei Ein- und Ausgaben steuert. Auf manchen Systemen wird die Funktionsbibliothek libintl“ nicht automatisch ” eingebunden, so dass das Einbinden der Funktionen zur Internationalisierung von Programmen zu einer Fehlermeldung führt. In diesem Fall muss beim Übersetzen des Quelltextes die Bibliothek explizit angegeben werden, also z. B. gcc sprache.c -lintl -o sprache“. ” Anstatt die Ländereinstellung wie in dem Beispiel explizit vorzugeben, sollten Programme diejenige Einstellung verwenden, die der Benutzer mit Hilfe der Umgebungsvariable LANG voreingestellt hat, indem als Parameter locale eine leere Zeichenkette angegeben wird: setlocale(LC_ALL, ""); Die Funktion setlocale() liefert als Rückgabewert einen Zeiger auf die Zeichenkette mit der entsprechenden Landeskennung. Alle von der libc generierten Meldungen, z.B. mit Hilfe der Funktion perror(), werden automatisch in der durch die Umgebungsvariable LANG vorgegebenen Sprache ausgegeben. Doch wie kann man das Gleiche für Meldungen erreichen, die mittels printf() oder fprintf() ausgegeben werden? Dies ist Aufgabe der C-Funktionen gettext() und dgettext():
56 3 Kommandozeilenprogramme char *gettext(char *msgid); char *dgettext(char *textdomain, char *msgid); Beide Funktionen dienen dazu, eine Meldung, hier als msgid bezeichnet, in die Landessprache gemäß LC MESSAGES zu übersetzen. Natürlich handelt es sich hierbei nicht um echte Übersetzungsfunktionen, vielmehr muss der Programmierer zuvor die übersetzten Texte in einer separaten Datei abgelegt haben. Die Texte werden nach der Textdomain gegliedert, in der Regel handelt es sich dabei um den Programmnamen selbst. Doch schauen wir uns zunächst das folgende kleine Programm an: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* sprache2.c */ # include <stdio.h> # include <locale.h> # include <libintl.h> int main() { setlocale(LC_MESSAGES, ""); printf("%s\n", dgettext("grep", "out of memory")); return(0); } Nach dem Übersetzen mit gcc sprache2.c -o sprache2“ kann das Pro” gramm mit verschiedenen Einstellungen für die Umgebungsvariable LANG gestartet werden:1 > export LANG=de DE > sprache2 Speicher ist alle. > export LANG=en US > sprache2 out of memory > export LANG=fr FR > sprache2 Mémoire épuisée. Das Programm greift mit der Anweisung 1 Sollte dieses Beispiel auf Ihrem System immer nur die Meldung out of memory“ liefern, so stellen ” Sie bitte sicher, dass das Programm grep“ installiert ist. ”
3.3 Programme mehrsprachig auslegen 57 dgettext("grep", "out of memory") auf die Textdomain grep“ zu und damit auf die Übersetzungsdateien des ” Programms grep“. Diese Dateien werden nach der Meldung (msgid) out ” ” of memory“ durchsucht, und wenn diese Meldung gefunden wird, liefert dgettext() das Pendant in der eingestellten Sprache. Findet dgettext() die Meldung in der entsprechenden Übersetzungdatei nicht, gibt die Funktion einen Zeiger auf die Meldung selbst (also auf msgid) zurück. Daher sollte als msgid immer der entsprechende Text in Englisch verwendet werden, sodass bei einer nicht unterstützten Sprache englische Meldungen erscheinen. Bei den Übersetzungsdateien, den so genannten Message-Object-Dateien, handelt es sich um spezielle Binärdateien, die für gewöhnlich unter /usr/share/locale/Länderkennung/LC MESSAGES/Textdomain.mo stehen, also z.B.: /usr/share/locale/de/LC MESSAGES/grep.mo Wird als Länderkennung de DE“ angegeben, sucht das System zunächst im ent” sprechenden Verzeichnis nach der betreffenden Message-Object-Datei. Bleibt die Suche ohne Erfolg, wird automatisch die Länderkennung auf de“ reduziert und ” erneut versucht, den entsprechenden Pfad zu öffnen. Auf diese Weise lässt sich z.B. für schweizerisches Deutsch de CH“ ein anderer Text einstellen, während für ” andere Dialekte (z.B. de AT“ für Österreich), für die keine speziellen Meldungen ” vorgesehen sind, auf die Datei unter der übergeordneten“ Länderkennung de“ ” ” zugegriffen wird. Bitte verstehen Sie das Beispielprogramm nur als Test! Sie sollten mit eigenen Programmen nicht die Message-Object-Dateien anderer Programme nutzen. Message-Object-Dateien erstellen Für das Erstellen eigener Message-Object-Dateien sind die Pakete gettext“ und ” gettext-devel“ erforderlich. Stellen Sie sicher, dass diese Pakete installiert sind. ” Als Beispiel für die Erzeugung und Verwendung eigener Message-Object-Dateien soll das kleine Programm sprache3“ dienen, das abhängig von der eingestellten ” Landessprache die Meldung Hello world!“ bzw. Hallo Welt!“ ausgibt: ” ”
58 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 3 Kommandozeilenprogramme /* sprache3.c */ # include <stdio.h> # include <locale.h> # include <libintl.h> int main() { bindtextdomain("sprache3", "."); textdomain("sprache3"); setlocale(LC_MESSAGES, ""); printf("%s\n", gettext("Hello world!")); printf("%s\n", gettext("This is a test.")); return(0); } Neben den bereits zuvor erläuterten Aufrufen der Funktion setlocale() und gettext() finden sich hier die Funktionen bindtextdomain() (Zeile 11) und textdomain() (Zeile 12). Mit der bindtextdomain()-Anweisung wird hier festgelegt, dass die Message-Object-Dateien zur Textdomain sprache3 nicht unter /usr/share/locale“, sondern im aktuellen Verzeichnis ( .“) zu finden ” ” sind. Die Anweisung textdomain("sprache3"); definiert sprache3 als aktuelle Textdomain für alle folgenden gettext()-Aufrufe. Mit Hilfe des Programms xgettext“ können aus dem Quelltext alle Aufrufe von ” gettext() und dgettext() in eine Portable-Message-Datei extrahiert werden: xgettext -o sprache3.po sprache3.c Das Programm xgettext erzeugt daraufhin die (editierbare) Portable-MessageDatei sprache.po“ mit folgendem Inhalt: ” 1 # SOME DESCRIPTIVE TITLE. 2 # Copyright (C) YEAR THE PACKAGE’S COPYRIGHT HOLDER 3 # This file is distributed under the same license as the PA 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. 5 # 6 #, fuzzy 7 msgid "" 8 msgstr "" 9 "Project-Id-Version: PACKAGE VERSION\n" 10 "Report-Msgid-Bugs-To: \n" 11 "POT-Creation-Date: 2010-01-31 14:58+0100\n"
3.3 Programme mehrsprachig auslegen 12 13 14 15 16 17 18 19 20 21 22 23 24 25 59 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: Quelltexte/Kapitel3/sprache3.c:14 msgid "Hello world!" msgstr "" #: Quelltexte/Kapitel3/sprache3.c:15 msgid "This is a test." msgstr "" In den ersten Zeilen dieser Datei sind einige Kommentare vorbereitet, unter anderem zum Autor und Erstellungsjahr. Hier können Sie bei echten“ Software” Projekten Ihre Daten eintragen. Ab Zeile 9 folgen automatisch generierte Informationen zur Datei. Wichtig sind hier die Zeilen 16 und 17, die die Codierung der Datei angeben. In Zeile 16 sollten Sie als Zeichensatz ISO-8859-1“ eintragen, also ” 16 "Content-Type: text/plain; charset=ISO-8859-1\n" Was jetzt noch fehlt, ist die eigentliche Übersetzung der Textmeldungen. Dazu müssen Sie hinter dem Schlüsselwort msgstr in den Zeilen 21 und 25 jeweils die Übersetzung der Texte in den vorangegangenen Zeilen eintragen, für Deutsch z. B.: 19 20 21 22 23 24 25 #: Quelltexte/Kapitel3/sprache3.c:14 msgid "Hello world!" msgstr "Hallo Welt!" #: Quelltexte/Kapitel3/sprache3.c:15 msgid "This is a test." msgstr "Dies ist ein Test." Aus der Portable-Message-Datei kann nun mit Hilfe des Programms msgfmt“ ” die Message-Object-Datei sprache.mo“ generiert und im vorgesehenen Ver” zeichnis abgelegt werden: > > > > msgfmt -o sprache3.mo sprache3.po mkdir de mkdir de/LC MESSAGES mv sprache3.mo de/LC MESSAGES Danach spricht“ das Programm sprache3“ deutsch (und englisch): ” ”
60 3 Kommandozeilenprogramme > export LANG=de DE > sprache3 Hallo Welt! Dies ist ein Test. > export LANG=en US > sprache3 Hello world! This is a test. Wird ein Programm korrekt installiert, sollten die zugehörigen MO-Dateien in die entsprechenden Verzeichnisse des Linux-Systems kopiert werden, also z. B. /usr/share/locale/Länderkennung/LC MESSAGES/ Dann kann der bindtextdomain()-Aufruf entfallen ( sprache3.c“, Zeile 11). ” 3.4 Ausgabesteuerung im Terminal-Fenster In den bisherigen Beispielen wurden Textausgaben im Terminal-Fenster durch einfache printf()-Anweisungen erreicht. Manchmal ist es jedoch von Vorteil, bestimmte Ausgaben hervorzuheben, z.B. durch Fettschrift oder durch Verwendung einer anderen Schriftfarbe. Auch das Positionieren von Ausgaben an bestimmten Stellen innerhalb des Terminal-Fensters kann hilfreich sein. In den folgenden Abschnitten werden zwei Möglichkeiten aufgezeigt, eine solche Ausgabesteuerung innerhalb eines Terminal-Fensters, d.h. ohne Verwendung einer grafischen Benutzerschnittstelle, zu erreichen. 3.4.1 ANSI-Steuersequenzen Eine sehr einfache Möglichkeit, Textausgaben hervorzuheben und zu positionieren, ist die Verwendung von Steuersequenzen nach dem ANSI-Standard. Es handelt sich dabei insofern um einen Standard, als auch Drucker sowie andere Betriebssysteme – beispielsweise DOS bei Verwendung des Treibers ANSI.SYS – die Sequenzen (zumindest teilweise) verstehen“. ” Diese Steuersequenzen werden fast ausschließlich mit einem ESC-Zeichen (ASCII-Code 27dez bzw. 33okt ) eingeleitet, weshalb sie auch als Escape-Sequenzen bezeichnet werden. Als Drucker nur bedingt grafikfähig waren, stellten diese Sequenzen die einzige Möglichkeit dar, Textbereiche zu unterstreichen, in Fett oder Kursiv zu drucken oder die Schriftart zu wechseln. Tabelle 3.2 zeigt eine Auswahl möglicher ESC-Sequenzen, die jedoch nicht alle von sämtlichen Terminalprogrammen unterstützt werden. Diese Steuersequenzen lassen sich direkt mit einer printf()-Anweisung ausgeben, z. B.: printf("\033[31mDieser Text ist rot.\033[0m\n");
3.4 Ausgabesteuerung im Terminal-Fenster 61 Tabelle 3.2: ANSI-Steuersequenzen für die Terminal-/Druckerausgabe ESC-Sequenz Beschreibung \033[m \033[0m \033[1m \033[4m \033[30m \033[31m \033[32m \033[33m \033[34m \033[35m \033[36m \033[40m \033[41m \033[42m \033[43m \033[44m \033[45m \033[46m \033[SpalteG \033[ZeileH \007 \011 \014 normaler Text normaler Text Fettschrift unterstreichen Schriftfarbe Schwarz Schriftfarbe Rot Schriftfarbe Grün Schriftfarbe Gelb Schriftfarbe Blau Schriftfarbe Violett Schriftfarbe Türkis Hintergrundfarbe Schwarz Hintergrundfarbe Rot Hintergrundfarbe Grün Hintergrundfarbe Gelb Hintergrundfarbe Blau Hintergrundfarbe Violett Hintergrundfarbe Türkis Cursor horizontal positionieren Cursor vertikal positionieren Signalton Tabulator (horizontal) Seitenvorschub (Terminal-Fenster löschen) 3.4.2 Die ncurses“-Bibliothek ” Vielleicht haben Sie sich schon einmal gefragt, wie ein Editor-Programm in der Art des vi“ (siehe Seite 6) arbeitet, das ohne die grafische Oberfläche X11 läuft. Die ” im vorherigen Abschnitt beschriebenen ESC-Sequenzen reichen für eine derartige Bildschirmsteuerung bei Weitem nicht aus. Für diesen Zweck gibt es unter Unix die curses -Bibliothek, die unter Linux in der weiterentwickelten und frei kopierbaren Version n curses vorliegt. Neben der Cursor-Positionierung und der Texthervorhebung bietet diese Funktionsbibliothek auch relativ komplexe Funktionen wie das Verschieben (Scrollen) des Fensterinhaltes um n Zeilen, das Einfügen oder Löschen von Zeichen innerhalb einer Zeile mit Verschieben der Zeichen rechts vom Cursor und das Abspeichern des gesamten Fensterinhaltes in eine Datei. Die vollständige Behandlung der ncurses-Bibliothek würde den Rahmen dieses Buches sprengen; ich möchte hier lediglich einen Einstieg in die Programmierung solcher Anwendungen geben. Die vollständige Liste aller Funktionen der ncursesBibliothek erhalten Sie mit man ncurses“ und man -k curses“. ” ”
62 3 Kommandozeilenprogramme Als Einstieg sei hier das folgende Programm betrachtet, das das freie Bewegen des Cursors über das Terminal-Fenster sowie die Eingabe von Zeichen an der gerade aktuellen Cursor-Position erlaubt – wie dies bei einem Editor-Programm der Fall ist. Gleichzeitig wird oben links immer die aktuelle Cursor-Position angezeigt: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 /* curses_bsp.c */ # include <stdio.h> # include <curses.h> void print_pos(WINDOW *win) { int x, y; getyx(win, y, x); attron(A_REVERSE); mvprintw(0, 0, "(%2d, attroff(A_REVERSE); move(y, x); return; } int main() { int c, x, y; WINDOW *win; /* Cursor-Position darst. */ /* aktuelle Pos. abfragen */ /* inverse Darstellung ein */ %2d)", x, y); /* inverse Darstellung aus */ /* Cursor wieder an alte Pos. */ /* Hauptprogramm */ if ((win = initscr()) == NULL) return(1); cbreak(); noecho(); keypad(win, TRUE); /* Sondertasten auswerten */ move(1, 0); print_pos(win); while ((c = getch()) != KEY_END) /* Ende = Abbruch */ { switch(c) { case KEY_UP: getyx(win, y, x); move(y-1, x); break; case KEY_DOWN: getyx(win, y, x); move(y+1, x);
3.4 Ausgabesteuerung im Terminal-Fenster 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 63 break; getyx(win, y, x); move(y, x-1); break; case KEY_RIGHT: getyx(win, y, x); move(y, x+1); break; case KEY_DC: delch(); break; case KEY_BACKSPACE: getyx(win, y, x); move(y, x-1); delch(); break; case KEY_IC: insch(’ ’); break; case KEY_HOME: getyx(win, y, x); move(y, 0); break; case KEY_F(1): clear(); move(1, 0); break; default: if (c < KEY_MIN) addch(c); } print_pos(win); case KEY_LEFT: } endwin(); return(0); } Um dieses Programm zu übersetzen, muss die ncurses-Bibliothek mit der Option -l“ explizit eingebunden werden: ” gcc curses bsp.c -lncurses -o curses bsp Wenn Sie das Programm starten, wird der Inhalt des Terminal-Fensters (oder der Linux-Konsole) gelöscht, und oben links erscheint die aktuelle Cursor-Position ( 0, 1)“ invers dargestellt. Sie können den Cursor jetzt mit Hilfe der Cursor” Tasten beliebig über das Fenster bewegen und an der aktuellen Position Text eingeben. Auch das Löschen von Zeichen mit Entf oder des gesamten Inhalts mit F1 ist möglich. Durch Drücken der Taste Ende wird das Programm beendet. Schauen wir uns zunächst das Hauptprogramm“ (Zeile 20 bis 64) an. Bevor ir” gendwelche Ein- oder Ausgaben über Funktionen der ncurses-Bibliothek erfolgen können, muss das Ausgabefenster initialisiert werden. Dies geschieht mit der Funktion initscr() in Zeile 25, die bei erfolgreicher Ausführung einen Zeiger auf die WINDOW-Struktur der Bibliothek zurückliefert. Mit den Funktionen cbreak() (Zeile 27) und noecho() (Zeile 28) wird die Betriebsart“ eingestellt; in ” diesem Fall soll jeder Tastendruck sofort an das Programm weitergeleitet werden und keine automatische Ausgabe des eingegebenen Zeichens erfolgen. Als letzte Konfigurationseinstellung wird in Zeile 29 die Auswertung von Sondertasten mit
64 3 Kommandozeilenprogramme der Funktion keypad() aktiviert. Mit der move()-Anweisung in Zeile 31 wird der Cursor in die Position y = 1, x = 0 gebracht. Als Nächstes erfolgt ein Aufruf der Funktion print pos(), die in den Zeilen 8 bis 18 definiert wird und die aktuelle Cursor-Position oben links in dem Fenster ausgibt. In der darauf folgenden while()-Schleife (Zeile 34 bis 60) werden mittels getch() so lange Zeichen von der Tastatur eingelesen, bis die Ende-Taste gedrückt wird. Dabei wird mit der switch()-Funktion in Zeile 36 bis 58 geprüft, ob es sich bei der Eingabe um eine der vier Cursortasten oder eine der Sondertasten Entfernen, Löschen (Backspace), Einfügen, Pos1 oder Funktionstaste 1 handelt. Eine vollständige Liste der von der ncurses-Bibliothek unterstützten Sondertasten erhält man mit grep KEY /usr/include/curses.h In Zeile 57 werden alle Zeichen, deren Wert kleiner als der erste Sondertastencode (KEY MIN) ist, an der aktuellen Position ausgegeben. Die Funktion getyx() liefert die aktuelle Cursor-Position in die Variablen x und y zurück. Es handelt sich bei dieser Funktion um ein Makro, daher kann der Inhalt der Variablen x und y verändert werden, ohne dass ein Zeiger auf die Variablen übergeben wird; es wird also kein vorangestelltes &“ benötigt. ” Vor Ende des Hauptprogramms muss noch das ncurses-Fenster wieder ge” schlossen“ werden. Dies geschieht mit der Funktion endwin() in Zeile 62 des Quelltextes. Die Funktion print pos(), die in den Quelltextzeilen 8 bis 18 definiert ist, sichert zunächst mit getyx() die aktuelle Cursor-Position in die Variablen x und y (Zeile 12). Diese Position wird dann mit Hilfe von mvprintw() an der festen Position (0,0) – also links oben – ausgegeben (Zeile 14). Danach wird der Cursor mit move() wieder an die ursprüngliche Stelle verschoben (Zeile 16). Mit den Funktionen attron() attroff() (Zeile 13 und 15) können Textattribute ein- und ausgeschaltet werden. Tabelle 3.3 zeigt einige der möglichen Attribute. Tabelle 3.3: Einige Textattribute der ncurses-Bibliothek Attribut Beschreibung A A A A A A normaler Text unterstreichen inverser Text (Weiß auf Schwarz) blinkender Text reduzierter Kontrast Fettschrift STANDOUT UNDERLINE REVERSE BLINK DIM BOLD
3.4 Ausgabesteuerung im Terminal-Fenster 65 Bei Verwendung der ncurses-Bibliothek sollten alle Ein- und Ausgaben über die ncurses-eigenen Funktionen erfolgen, also z.B. über printw() und nicht etwa über printf(). Farbige Textdarstellung mit der ncurses-Bibliothek Natürlich kann mit der ncurses-Bibliothek auch farbiger Text ausgegeben werden – sofern das Terminal oder die Console Farben unterstützen. Das folgende kleine Programm gibt Text mit unterschiedlicher Vorder- und Hintergrundfarbe aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /* curses_bsp2.c - Farbdarstellung mit ncurses */ # include <stdio.h> # include <curses.h> int main() { int x, y; WINDOW *win; if ((win = initscr()) == NULL) return(1); start_color(); /* Farbausgabe initialisieren */ cbreak(); noecho(); init_pair(1, COLOR_BLACK, COLOR_WHITE); init_pair(2, COLOR_YELLOW, COLOR_BLUE); init_pair(3, COLOR_RED, COLOR_CYAN); color_set(1, NULL); /* Hintergrund weiß */ for (y=0; y<LINES; y++) for (x=0; x<COLS; x++) mvaddch(y, x, ’ ’); box(win, 0, 0); /* Fenster einrahmen */ mvprintw(2, 2, "Farbkombination 1"); color_set(2, NULL); mvprintw(3, 2, "Farbkombination 2"); color_set(3, NULL); mvprintw(4, 2, "Farbkombination 3");
66 35 36 37 38 39 40 3 Kommandozeilenprogramme getch(); /* auf eine Taste warten */ endwin(); return(0); } Bevor Farben für den Text und den Hintergrund eingestellt werden können, muss zunächst die Verwendung von Farben initialisiert werden. Das geschieht in Zeile 15 mit der Funktion start color(). Es wird empfohlen, diese Funktion unmittelbar nach initscr() aufzurufen. Als Nächstes sollten die Farbenpaare (Vorder- und Hintergrundfarbe) definiert werden, die man verwenden möchte (hier in den Zeilen 19 bis 21). Ein solches Farbenpaar lässt sich dann mit der Funktion color set() aktivieren (Zeile 23). In den Zeilen 24 bis 26 wird der gesamte Fensterinhalt auf die Hintergrundfarbe der Farbkombination 2 – hier also Weiß – einstellen. Man beachte, dass die globalen Variablen LINES und COLS von der ncurses-Bibliothek mit der Größe des Fensters initialisiert werden. Mit der Funktion box() in Zeile 28 wird das Fenster dann noch eingerahmt“. Anschließend werden Textzeilen in den unterschiedli” chen Farbkombinationen ausgegeben. Das Ergebnis ist in Abbildung 3.3 zu sehen. Abbildung 3.3: Farbausgabe mit der ncurses-Bibliothek
Kapitel 4 Dateien und Verzeichnisse In diesem Kapitel werden die für den Zugriff auf Dateien und Verzeichnisse relevanten C-Funktionen betrachtet. Ein Großteil der hier behandelten Funktionen entspricht dem ANSI-C-Standard und ist daher auf andere Betriebssysteme übertragbar. Es werden aber auch Linux- bzw. Unix-spezifische Funktionen angesprochen, die über diesen Standard hinausgehen. 4.1 Die Arbeit mit Dateien Die folgenden Abschnitte befassen sich zunächst mit den Grundlagen von Dateizugriffen. Es werden die relevanten C-Funktionen vorgestellt und deren Verwendung anhand von Beispielen demonstriert. 4.1.1 Gepufferte Ein-/Ausgabe In C-Programmen werden in der Regel Funktionen zum so genannten gepufferten Dateizugriff verwendet. Dabei wird für jede geöffnete Datei ein Puffer von 8192 Bytes1 im Hauptspeicher angelegt. Beim Schreiben in die Datei werden zunächst die Zeichen nur in dem Puffer abgelegt, bis dieser voll ist. Erst dann wird der Inhalt des Puffers tatsächlich auf das physikalische Medium (z.B. die Festplatte) geschrieben. Da eine Datei zum Lesen und zum Schreiben geöffnet werden kann, gibt es immer zwei Puffer: einen Lese- und einen Schreibpuffer. Beim Schließen einer geöffneten Datei werden automatisch die noch im Puffer befindlichen Zeichen auf das Medium geschrieben und dann der Speicher für den Puffer freigegeben. 1 Die Größe des Puffers ist als Konstante BUFSIZ definiert.
68 4 Dateien und Verzeichnisse Vorteil der gepufferten Ein-/Ausgabe ist die deutlich gesteigerte Performance der Dateizugriffe, da nicht für jedes Zeichen ein Zugriff auf das physikalische Medium erfolgen muss. Wie in dem Abschnitt 5.2.3 noch gezeigt wird, ist es sinnvoll, sich als Programmierer der Tatsache bewusst zu sein, dass eine solche Pufferung erfolgt. Datei-Deskriptoren Zu jeder geöffneten Datei – ob gepuffert oder nicht – gehört ein so genannter Datei-Deskriptor (engl.: file descriptor). Es handelt sich dabei um eine Integer-Zahl ≥ 0. Einige Datei-Funktionen, z.B. für die ungepufferte Ein-/Ausgabe, benötigen oder liefern einen solchen Datei-Deskriptor. Beispiele: open(), close(), fstat(), ioctl() usw. Dateizeiger vom Typ Stream Neben den Datei-Deskriptoren gibt es eine weitere Möglichkeit, eine geöffnete Datei mit einer Variablen zu assoziieren: durch einen Zeiger auf eine Struktur vom Typ FILE, die häufig als Stream – also als Datenstrom“ – bezeichnet wird. Ein ” solcher Zeiger ist jedoch nur für gepufferte Dateien verfügbar. Funktionen, die einen Stream liefern oder als Parameter benötigen, sind unter anderem: fopen(), fclose(), fread(), fwrite(), fprintf() und fscanf(). Wie bereits oben erwähnt, ist zu jedem Stream auch ein Datei-Deskriptor verfügbar. Diesen liefert die Funktion fileno(). 4.1.2 stdin, stdout und stderr In C sind drei Streams automatisch verfügbar, ohne dass diese explizit geöffnet werden: Stream Datei-Deskriptor Funktion stdin stdout stderr STDIN FILENO STDOUT FILENO STDERR FILENO Standard-Eingabekanal Standard-Ausgabekanal Ausgabekanal für Fehlermeldungen Daten, die ein Programm an stdout oder stderr schickt, werden in dem zugehörigen Terminal-Fenster ausgegeben. Eingaben über die Tastatur sind über stdin verfügbar. Genau genommen sind diese drei Streams mit dem Device verbunden, das sich hinter dem Terminal verbirgt, also z.B. /dev/tty1“ für die ” erste Linux-Konsole oder /dev/pts/1“ für ein X-Terminalfenster. ” In der Shell hat der Anwender jedoch die Möglichkeit, jede dieser drei StandardKanäle mit Hilfe der Umlenksymbole <“ und >“ mit einer Datei zu verbinden, ” ” z.B.: primzahl > primzahlen.txt
4.1 Die Arbeit mit Dateien 69 Bei diesem Aufruf öffnet die Shell (also z.B. das Programm /bin/bash) die Datei primzahlen.txt“ zum Schreiben. Der Datei-Zeiger stdout wird auf die” sen Stream eingestellt, sodass alle an stdout geschickten Ausgaben in der Datei primzahlen.txt“ gespeichert werden. Nach Beendigung des Programms ” wird diese Datei von der Shell wieder geschlossen. An stderr geschickte Daten – in der Regel Fehlermeldungen – werden bei diesem Beispiel nach wie vor im Terminal-Fenster angezeigt und nicht in der Datei gespeichert! Sollen die Ausgaben eines Programms sowohl in einer Datei gespeichert als auch gleichzeitig in dem Terminal ausgegeben werden, kann dies mit Hilfe des Programms tee“ (sprich tie“) erfolgen: ” ” primzahl | tee primzahlen.txt Die Funktionen printf() und scanf() verwenden stdout bzw. stdin als Ausbzw. Eingabekanal. 4.1.3 Dateien öffnen und schließen Neben den Standard-Ein-/Ausgabekanälen muss in der Regel auch auf weitere Dateien zugegriffen werden. Dazu müssen sie zunächst geöffnet und sollten auch wieder geschlossen werden, sobald kein weiterer Zugriff mehr erforderlich ist. Wir werden in diesem Kapitel nur das Öffnen von Dateien als Stream verwenden, also für gepufferte Ein-/Ausgabe. Das ungepufferte Öffnen von Dateien und die Verwendung von Datei-Deskriptoren behandeln wir in den Kapiteln 5 und 6 ausführlich. Mit der Funktion fopen() wird die in path angegebene Datei geöffnet: FILE *fopen(char *path, char *mode); Als Rückgabewert liefert fopen() entweder einen Zeiger auf den Stream – also auf eine Struktur vom Typ FILE – oder NULL1 für den Fall, dass sich die Datei nicht öffnen ließ. Der Parameter mode ist ein Zeiger auf eine Zeichenkette, die beschreibt, wie die Datei zu öffnen ist. Mögliche Werte für mode sind in Tabelle 4.1 aufgeführt. Für das ordnungsgemäße Öffnen einer Datei muss überprüft werden, ob der Rückgabewert NULL ist – die Datei sich also nicht öffnen ließ – und ggf. eine Fehlermeldung ausgegeben wird. Ein typischer Quelltextausschnitt sieht dann wie folgt aus: # include <stdio.h> # include <string.h> 1 Die Konstante NULL entspricht dem Ausdruck ((void *)0)“. ”
70 4 Dateien und Verzeichnisse Tabelle 4.1: Bedeutung des mode-Parameters der Funktion fopen() Mode Bedeutung r Datei zum Lesen öffnen. r+ Datei zum Lesen und Schreiben öffnen. w Datei zum Schreiben öffnen. Falls die Datei bereits existiert, wird ihr Inhalt gelöscht. w+ Datei zum Lesen und Schreiben öffnen. Falls die Datei noch nicht existiert, wird sie neu angelegt. a Datei zum Schreiben öffnen. Der Dateizeiger steht am Ende der Datei. Der alte Inhalt geht nicht verloren. a+ Datei zum Lesen und Schreiben öffnen. Der Dateizeiger steht am Ende der Datei. Der alte Inhalt geht nicht verloren. char *filename; FILE *in_stream; if ((in_stream = fopen(filename, "r")) == NULL) { fprintf(stderr, "%s: Can’t open file ’%s’ for " "input: ", basename(argv[0]), filename); perror(""); exit(1); } Wurde eine Datei erfolgreich geöffnet, kann entsprechend der mode-Angabe auf diese Datei zugegriffen werden. Wird kein weiterer Zugriff benötigt, sollte die Datei mit fclose(Stream); wieder geschlossen werden. Zwar werden bei Beendigung eines C-Programms alle noch offenen Dateien automatisch wieder geschlossen, trotzdem sollte man Dateien, auf die nicht mehr zugegriffen wird, unmittelbar mit fclose() schließen, um Ressourcen zu schonen. 4.1.4 Lesen aus und Schreiben in Dateien Die C-Standard-Funktionsbibliothek libc bietet eine ganze Reihe von Funktionen für den Schreib-/Lesezugriff auf Dateien an. Die wichtigsten dieser Funktionen werden in diesem Abschnitt angesprochen.
4.1 Die Arbeit mit Dateien 71 Eine Datei wird in C als sequenzieller Datenstrom betrachtet, bestehend aus einer Folge von Bytes. Anders als bei einer Zeichenkette ist bei Dateien ohne weiteres kein wahlfreier Zugriff (engl.: random access) auf die einzelnen Zeichen möglich.1 Stattdessen wird bei Schreib- und Lesezugriffen der Dateizeiger um die Zahl der gelesenen oder geschriebenen Zeichen verschoben. Zeichenweise zugreifen: fgetc() und fputc() Die einfachste Art des Dateizugriffs erfolgt mit den Funktionen fgetc() und fputc(), die jeweils das nächste Zeichen aus der Datei lesen bzw. in die Datei schreiben: int fgetc(FILE *stream); int fputc(int c, FILE *stream); Die Funktion fgetc() liefert als Rückgabewert das ausgelesene Zeichen, jedoch nicht als Typ char (1 Byte), sondern als int (4 Bytes). Dadurch kann die Funktion das Erreichen des Dateiendes bzw. das Auftreten eines Fehlers durch den speziellen Rückgabewert EOF ( End Of File“ = –1) signalisieren. Die Funktion fputc() ” schreibt das Zeichen c, umgewandelt zum 1-Byte-Wert ohne Vorzeichen, in die angegebene Datei. Tritt ein Fehler auf (z.B. der Datenträger ist voll), wird auch hier die Konstante EOF zurückgegeben. Bei fehlerfreier Ausführung liefert die Funktion das geschriebene Zeichen zurück. Das folgende Beispiel soll die Verwendung der Funktionen fgetc() und fputc() demonstrieren. Mit Hilfe einer while()-Schleife wird hier die Datei in stream nach out stream kopiert: int c; FILE *in_stream, *out_stream; while ((c = fgetc(in_stream)) != EOF) fputc(c, out_stream); Natürlich müssen die Dateien zuvor – wie in Abschnitt 4.1.3 beschrieben – geöffnet werden. Das Erreichen des Dateiendes kann auch mit der Funktion feof(Stream) abgefragt werden, ein Zugriffsfehler lässt sich mit ferror(Stream) feststellen. Die Funktion ungetc() erlaubt es, ein mit fgetc() gelesenes Zeichen wieder in den Puffer zurückzuschreiben, sodass es für den nächsten Lesezugriff wieder bereitsteht: int ungetc(int c, FILE *stream); 1 Die Funktion fseek() ermöglicht hier scheinbar“ einen wahlfreien Zugriff, sie ist aber nicht auf ” jede Art von Dateien anwendbar.
72 4 Dateien und Verzeichnisse Folgendes Beispielprogramm ersetzt unter Verwendung der Funktion ungetc() in Textdateien das unter DOS/Windows übliche Zeilenende \r\n“ durch ein ” \n“, wie es bei Unix/Linux verwendet wird: ” int c, c2; while ((c = fgetc(stdin)) != EOF) { if (c == ’\r’) { if ((c2 = fgetc(stdin)) == ’\n’) c = c2; else ungetc(c2, stdin); } fputc(c, stdout); } Einzelne \r“, auf die kein Zeilenvorschub ( \n“) folgt, werden nicht verändert. ” ” Durch die Verwendung der Funktion ungetc() wird hier der Programmieraufwand deutlich verringert, da Sonderfälle wie \r\r\n“ oder ein \r“ am Datei” ” ende nicht gesondert berücksichtigt werden müssen. Durch ungetc() wird das auf \r“ folgende Zeichen – sofern es kein Zeilenvorschub ist – einfach für den ” nächsten Durchlauf der while()-Schleife wieder zur Verfügung gestellt. Datensätze lesen/schreiben: fread() und fwrite() Ist die Länge eines Datensatzes in einer Datei bekannt, so kann der Dateizugriff mit den Funktionen fread() und fwrite() vereinfacht werden: size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream); Der Typ size t wird im Allgemeinen für Längen- und Offset-Angaben verwendet und entspricht unsigned long int. Die Parameter der Funktionen fread() und fwrite() haben die folgende Bedeutung: ptr size nmemb stream Zeiger auf den Ziel- bzw. Quell-Speicherbereich Größe eines Datensatzes (in Bytes) Anzahl der Datensätze Datei, aus der gelesen bzw. in die geschrieben wird Als Rückgabewert liefern beide Funktionen die Anzahl der gelesenen bzw. geschriebenen Datensätze (nicht der Bytes!).
4.1 Die Arbeit mit Dateien 73 Dateizeiger verschieben mit fseek() Wie bereits zuvor beschrieben, wird eine Datei als sequenzieller Datenstrom behandelt, bei dem die Zeichen genau in der Reihenfolge gelesen werden, in der sie zuvor in die Datei geschrieben wurden. Es gibt jedoch einige Funktionen, die das Verstellen des Dateizeigers – und damit das Vor- und Zurückspringen innerhalb der Datei – erlauben. Die wichtigste dieser Funktionen ist fseek(): int fseek(FILE *stream, long offset, int whence); Die Funktion fseek() verschiebt den Dateizeiger um offset Zeichen, wobei der Parameter whence angibt, von wo aus der Dateizeiger verschoben werden soll: SEEK SET SEEK CUR SEEK END Position relativ zum Dateianfang Verschiebung relativ zur aktuellen Position Position relativ zum Dateiende Der Rückgabewert von fseek() ist 0 bei fehlerfreier Ausführung und –1 im Fehlerfall. Es gibt Streams, auf die die Funktion fseek() nicht angewendet werden kann! Das ist z.B. bei den Standardein- und -ausgabekanälen oder bei Pipes der Fall. Weitere Funktionen Es gibt eine ganze Reihe weiterer Funktionen für den Dateizugriff, die wir hier nicht alle behandeln können. Auch an dieser Stelle sei noch einmal auf die man“” Seiten hingewiesen. Einige der häufig verwendeten Funktionen soll die folgende Übersicht aufzeigen: fflush() Leert den Dateipuffer. Alle Zeichen im Schreibpuffer werden in die Datei (das physikalische Medium) geschrieben. Nur anzuwenden auf Dateien, die zum Schreiben geöffnet sind. fgets() und fputs() Zeilenweise lesen/schreiben. fgets() liest aus der Datei bis zum Zeilenende ( \n“) oder Dateiende. fputs() schreibt eine Zeichenkette in ” die Datei. fprintf() und fscanf() Formatiertes Lesen aus bzw. Schreiben in eine Datei. Die Funktionen sind analog zu printf() und scanf() mit dem Unterschied, dass statt der Standardein- und -ausgabe beliebige Dateien angegeben werden können. mmap() und munmap() Mit diesen Funktionen kann eine Datei oder der Teil einer Datei in den Hauptspeicher kopiert werden.
74 4 Dateien und Verzeichnisse 4.1.5 Ein Beispiel: Zeilen nummerieren Der Umgang mit Dateien soll im Folgenden anhand eines kleinen Beispielprogramms veranschaulicht werden. Das Programm number“ liest die als Argu” mente angegebenen Textdateien ein und fügt am Anfang jeder Zeile eine Zeilennummer ein. Die Ausgabe der so erweiterten Dateien erfolgt entweder über den Standard-Ausgabekanal oder, falls die Option -o“ angegeben wird, in eine Aus” gabedatei. Das Programm verwendet dazu die Funktionen fopen(), fclose(), fgets() und fprintf(). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /* number.c - Add line numbers to text file(s). */ # include <stdio.h> # include <unistd.h> int main(int argc, char *argv[]) { int option, i, line; char *output_file=NULL; static char buffer[256]; FILE *in_stream, *out_stream; while ((option = getopt(argc, argv, "ho:")) > 0) if (option == ’h’) { printf("Usage: number [-o output-file] " "input-file ...\n"); return(0); } else if (option == ’o’) output_file = optarg; else return(1); if (optind == argc) { fprintf(stderr, "number: Missing file name. " "Type ’number -h’ for help.\n"); return(1); } if (output_file == NULL) out_stream = stdout;
4.2 Eigenschaften von Dateien oder Verzeichnissen auswerten 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 75 else if ((out_stream = fopen(output_file, "w")) == NULL) { perror("number: Can’t open output file"); return(1); } line = 1; for (i=optind; i<argc; i++) { if ((in_stream = fopen(argv[i], "r")) == NULL) { fprintf(stderr, "%s: Can’t open file ’%s’ " "for input: ", argv[0], argv[i]); perror(""); return(1); } while (fgets(buffer, 256, in_stream) != NULL) fprintf(out_stream, "%3d %s", line++, buffer); fclose(in_stream); } if (output_file != NULL) fclose(out_stream); return(0); } 4.2 Eigenschaften von Dateien oder Verzeichnissen auswerten Oftmals ist es erforderlich, Informationen über eine Datei oder ein Verzeichnis einzuholen, z.B. die Länge oder die Zugriffsrechte. Dazu dient die Funktion stat(): int stat(char *file_name, struct stat *status); Diese Funktion untersucht“ die Datei (oder das Verzeichnis) mit dem in ” file name angegebenen Namen (und Pfad) und speichert die Informationen in der Struktur vom Typ stat, auf die der Parameter status zeigt. Diese Struktur enthält folgende Einträge:
76 4 Dateien und Verzeichnisse st st st st st st st st st st st st st dev ino mode nlink uid gid rdev size blksize blocks atime mtime ctime Device zugehöriger Inode (Block mit Dateikopf) Zugriffsrechte Anzahl der Hard Links Benutzer-ID Gruppen-ID Typ des Devices (falls es ein Device ist) Länge in Bytes Blockgröße Anzahl der belegten Blöcke Uhrzeit u. Datum des letzten Zugriffs Uhrzeit u. Datum des letzten Schreibzugriffs Uhrzeit u. Datum der letzten Änderung der Zugriffsrechte Mit Hilfe der folgenden Makros lässt sich anhand des Elements st mode der Dateityp feststellen: Makro S S S S S S S ISLNK(st mode) ISREG(st mode) ISDIR(st mode) ISCHR(st mode) ISBLK(st mode) ISFIFO(st mode) ISSOCK(st mode) ergibt wahr“, falls die Datei. . . ” ein symbolischer Link ist eine reguläre“ Datei ist ” ein Verzeichnis ist ein character device ist ein block device ist ein FIFO ist ein socket ist Das folgende Beispielprogramm status“ zeigt die Anwendung der Funktion ” stat(). Es liefert für den als Argument angegebenen Pfadnamen die Informationen über Namen, Länge, Typ, Zugriffsrechte, Besitzer der Datei (User ID) und Zeitpunkt der letzten Änderung. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* status.c - get status of file or directory. */ # # # # # include include include include include <stdio.h> <unistd.h> <sys/stat.h> <time.h> <string.h> int main(int argc, char *argv[]) { struct stat status; if ((argc != 2) || (strcmp(argv[1], "-h") == 0))
4.3 Verzeichnisse einlesen 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 77 { printf("Usage: status filename\n"); return(1); } if (stat(argv[1], &status) != 0) { perror("status: Can’t get file status"); return(1); } printf("file/dir name:\t’%s’\n", argv[1]); printf("file/dir size:\t%ld bytes\n", status.st_size); if (S_ISDIR(status.st_mode)) printf("type:\t\tdirectory\n"); else if (S_ISREG(status.st_mode)) printf("type:\t\tregular file\n"); printf("protection:\t%o\n", status.st_mode & 0x1ff); printf("owner:\t\t%d\n", status.st_uid); printf("last modified:\t%s", ctime(&(status.st_mtime))); return(0); } 4.3 Verzeichnisse einlesen Sollen alle Einträge eines Verzeichnisses aufgelistet werden – ähnlich wie beim Programm ls“ –, ist dies mit Hilfe der Funktionen ” DIR *opendir(char *name); int closedir(DIR *dir); struct dirent *readdir(DIR *dir); möglich. Ähnlich, wie Dateien vor dem Lesen geöffnet werden müssen, ist auch hier zunächst das Öffnen des Verzeichnisses mittels opendir() erforderlich. Rückgabewert dieser Funktion ist ein Zeiger auf eine Struktur vom Typ DIR (analog zur Funktion fopen(), die den Zeiger auf eine Struktur vom Typ FILE liefert). Mit Hilfe der Funktion readdir() können dann nacheinander alle Einträge des Verzeichnisses gelesen werden, wobei readdir() einen Zeiger auf eine Struktur vom Typ dirent liefert. Diese enthält unter anderem das Element d name, also eine Zeichenkette mit dem Namen des Verzeichnis-Eintrags. Ist das Ende des Verzeichnisses erreicht, gibt readdir() einen Nullzeiger zurück.
78 4 Dateien und Verzeichnisse Das folgende Programm get dir“ verhält sich wie ls -U1“, es zeigt alle Einträge ” ” des als Argument angegebenen Verzeichnisses unsortiert an: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /* get_dir.c */ # # # # include include include include <stdio.h> <dirent.h> <sys/types.h> <string.h> int main(int argc, char *argv[]) { DIR *directory; struct dirent *dir_entry; if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) { printf("Usage: get_dir path\n"); return(1); } if ((directory = opendir(argv[1])) == NULL) { perror("get_dir: Can’t open directory"); return(1); } while ((dir_entry = readdir(directory)) != NULL) printf("%s\n", dir_entry->d_name); closedir(directory); return(0); }
Kapitel 5 Interprozesskommunikation Linux ist ein Multi-Tasking-Betriebssystem, d. h. mehrere Prozesse werden (scheinbar) gleichzeitig abgearbeitet. Für den Anwender bedeutet dies, dass er mehrere Anwendungen gleichzeitig nutzen kann – z. B. eine Textverarbeitung, während ein MP3-Decoder Musik abspielt – sogar während andere Anwender weitere Programme auf dem gleichen System laufen lassen. Dem Programmierer eröffnet sich durch die Multi-Tasking-Fähigkeit die Möglichkeit, verschiedene Aufgaben eines Programms auf mehrere Prozesse aufzuteilen. Um diese Möglichkeit sinnvoll nutzen zu können, muss er sich auch mit der Kommunikation der Prozesse, also dem Datenaustausch zwischen verschiedenen Prozessen, auseinandersetzen. 5.1 Prozessverwaltung unter Linux Neue Prozesse (auch als Tasks oder Threads bezeichnet) können unter Linux nicht aus dem Nichts“ entstehen, sondern werden von einem Eltern-Prozess (engl.: pa” rent process) gestartet – mit Ausnahme des INIT-Prozesses, der beim Hochfahren des Systems gestartet wird und selbst keinen Eltern-Prozess besitzt. Eltern- und Kind-Prozess (engl.: child process) bleiben miteinander verknüpft. So liefert die C-Funktion getppid() die Prozess-ID des Eltern-Prozesses, und das Beenden eines Kind-Prozesses wird dem zugehörigen Eltern-Prozess durch ein Signal angezeigt. Jeder Prozess erhält eine eindeutige Identifikationsnummer: die Prozess-ID. Jede ID wird nur einmal vergeben. Selbst wenn der zugehörige Prozess beendet wurde, wird dessen ID nicht neu belegt. Die Reihenfolge der IDs entspricht der Reihenfolge, in der die Prozesse erzeugt wurden: je jünger“ ein Prozess, desto größer seine ” ID. Eine Liste aller existierenden Prozesse erhält man mit ps -A“. An erster Stel”
80 5 Interprozesskommunikation le steht hier der Prozess init“, der immer die Prozess-ID 1 besitzt. Ganz unten ” in der Liste steht in der Regel der Prozess, in dem das Programm ps“ selbst läuft. ” Man erkennt daran, dass jedes aus einer Shell heraus gestartete Programm einen eigenen Prozess erhält und nicht etwa in dem Shell-Prozess läuft.1 5.2 Neue Prozesse starten Bevor man sich mit Interprozesskommunikation befasst, muss man wissen, wie ein neuer Kind-Prozess erzeugt wird. In diesem Abschnitt werden zwei Methoden zur Erzeugung eines neuen Prozesses vorgestellt. Außerdem wird angerissen, was beim Erzeugen eines Prozesses geschieht und was man als Programmierer dabei beachten muss. 5.2.1 Shell-Programme aufrufen mit system() Manchmal kann es sinnvoll sein, innerhalb eines eigenen Programms ein anderes Programm zu starten, um dessen Funktionalität für das eigene Programm zu nutzen. Beispiel: Sie berechnen mit Ihrem Programm eine Reihe von Zahlen, die Sie auch gern grafisch darstellen würden. Sie können dann natürlich die grafische Ausgabe selbst programmieren, was im Allgemeinen einen nicht unerheblichen Aufwand bedeutet, oder rufen von Ihrem Programm aus ein bereits existierendes Programm zur Visualisierung von Daten (z.B. gnuplot) auf. Dies ist mit Hilfe der Funktion int system(char *command); möglich. Die Funktion system() übernimmt hier gleich mehrere Aufgaben: sie startet eine neue Shell /bin/sh (in einem eigenen Prozess) und lässt diese Shell das in command angegebene Kommando ausführen. Ferner wartet system() auf die Beendigung des Kind-Prozesses, sodass Ihr Programm erst danach fortgesetzt wird. Als Rückgabewert liefert system() entweder 127 für den Fall, dass die Shell nicht gestartet werden kann, oder den Rückgabewert des ausgeführten Kommandos. Wurde das Kommando fehlerfrei ausgeführt, sollte system() den Wert 0 zurückgeben. Das folgende Beispielprogramm zeigt die Verwendung der Funktion system(). Es stellt mit Hilfe des Programms gnuplot für fünf Sekunden eine Sinus-Kurve dar (dazu müssen Sie natürlich das Paket gnuplot installiert haben): 1 Es sei denn, man stellt ein exec“ voran. ”
5.2 Neue Prozesse starten 1 2 3 4 5 6 7 8 9 10 11 12 81 /* sinus.c */ # include <stdio.h> # include <stdlib.h> int main() { system("echo ’plot sin(x); pause 5’ | gnuplot"); return(0); } 5.2.2 Die Funktionen der exec-Familie Eigentlich gehört dieser Abschnitt gar nicht in das Kapitel Interprozesskommunikation, da die Funktionen der exec-Familie keine neuen Prozesse erzeugen. Sie werden jedoch häufig in Verbindung mit fork() (s. u.) als Ersatz für die zuvor beschriebene Funktion system() verwendet (vgl. hierzu auch man 3 system“). ” Es gibt insgesamt sechs Funktionen der exec-Familie, die sich vor allem durch Abweichungen in der Art der Parameter unterscheiden: int int int int int int execl(char *path, char *arg, ...); execlp(char *file, char *arg, ...); execle(char *path, char *arg , ..., char *envp[]); execv(char *path, char *argv[]); execvp(char *file, char *argv[]); execve(char *path, char *argv[], char *envp[]); Alle diese Funktionen führen – ähnlich wie die Funktion system() – ein Programm aus, dessen Name in dem Parameter file bzw. path angegeben ist. Im Unterschied zu system() wird keine neue Shell gestartet, sondern das auszuführende Programm läuft in dem gleichen Prozess, der die exec-Funktion aufgerufen hat. Bei Beendigung des Programms wird auch dieser Prozess beendet, d.h. der Prozess kehrt nach erfolgreicher Ausführung einer exec()-Funktion nicht mehr in das ursprüngliche Programm zurück. Lediglich bei Auftreten eines Fehlers, weil z.B. das angegebene Programm nicht existiert, kehren die exec()Funktionen mit dem Rückgabewert –1 zurück. Man erkennt, dass der Funktionsname jeweils aus exec“ plus eine Endung aus ” ein oder zwei Buchstaben gebildet wird. Ein l“ in der Endung bedeutet da” bei, dass die Argumente (also die Kommandozeilenparameter) der aufzurufenden Funktion als Liste übergeben werden, d.h. für jedes Argument eine Zeichenkette. Das erste Argument muss dabei den Programmnamen selbst enthalten! Ein v“ ” dagegen bedeutet, dass die Argumente als Vektor übergeben werden, und zwar
82 5 Interprozesskommunikation in der Form, wie sie auch die Funktion main() erhält (vgl. Abschnitt 3.1.2). Auch hier entspricht das erste Argument, also argv[0], dem Namen des aufzurufenden Programms. Ein p“ in der Endung des Funktionsnamens weist darauf hin, dass diese Funkti” on alle in der Umgebunsvariablen PATH eingetragenen Pfade nach dem angegebenen Programm durchsucht, während die Funktionen ohne ein p“ in der Endung ” den vollständigen Pfad des auszuführenden Programmes benötigen (daher wurde der Parameter bei diesen Funktionen als path und nicht als file bezeichnet). Das e“ in der Endung der Funktionen execle() und execve() bedeutet, dass ” diese Funktionen zusätzlich zu den Argumenten für das aufzurufende Programm einen Vektor mit Umgebungsvariablen benötigen, wie sie an das Programm weitergegeben werden sollen. Die anderen Funktionen behalten die aktuellen Umgebungsvariablen bei. Die beiden folgenden Programmausschnitte sollen die Verwendung der execFunktionen demonstrieren: Beispiel 1: # include <stdio.h> # include <unistd.h> char *argv[4], *env[2]; argv[0] = "/bin/ls"; argv[1] = "--color"; argv[2] = "/home/martin"; argv[3] = NULL; env[0] = "LS_COLORS=fi=00:di=01"; env[1] = NULL; execve("/bin/ls", argv, env); perror("execve() failed"); Beispiel 2: execlp("ls", "ls", "-F", "/home/martin", NULL); 5.2.3 Einen Kind-Prozess erzeugen mit fork() Die zentrale Funktion, mit der unter Linux/Unix ein Kind-Prozess erzeugt wird, ist fork(). Die Funktion hat keinen Parameter; sie liefert als Rückgabewert die Prozess-ID des neu erzeugten Kind-Prozesses. In dem Kind-Prozess selbst liefert die Funktion eine 0 als Rückgabewert; konnte kein Kind-Prozess erzeugt werden, ist der Rückgabewert –1:
5.2 Neue Prozesse starten 83 # include <stdio.h> # include <unistd.h> int child_pid; if ((child_pid = fork()) == 0) printf("Dies ist der Kind-Prozess.\n"); else if (child_pid > 0) printf("Kind-Prozess %d wurde erzeugt.\n", child_pid); else perror("fork() failed"); Doch was genau passiert beim Aufruf der Funktion fork()? Linux legt eine neue Task-Struktur an, die eine Kopie der Struktur des Eltern-Prozesses darstellt. Dieser Prozess erhält eine neue Prozess-ID, benutzt jedoch zunächst den gleichen Speicher wie der Eltern-Prozess. Erst wenn einer der beiden Prozesse in den Speicher schreibt, also z.B. eine Variable verändert, wird der entsprechende Speicherbereich dupliziert und für beide Prozesse getrennt weitergeführt. Diese Methode bezeichnet man als copy-on-write. Dadurch wird erreicht, dass beide Prozesse scheinbar getrennten Speicher benutzen, ohne bereits bei der Erzeugung des Kind-Prozesses den gesamten Speicher des Eltern-Prozesses kopieren zu müssen. Nach Ausführung der Funktion fork() existieren also zwei Prozesse, die sich zunächst allein durch die Prozess-ID unterscheiden – und durch den Rückgabewert von fork(). Bei jedem Schreib-Zugriff wird jedoch der Unterschied beider Prozesse größer. Das folgende Beispielprogramm new child“ soll diesen Vor” gang veranschaulichen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /* new_child.c - einen Kind-Prozess erzeugen */ # include <stdio.h> # include <unistd.h> int main() { int child_pid, test; test = 1; printf("\t\ttest=%d, &test=%p\n", test, &test); if ((child_pid = fork()) == 0) { sleep(1);
84 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 5 Interprozesskommunikation printf("Kind-Prozess:\ttest=%d, &test=%p\n", test, &test); test = 2; printf("Kind-Prozess:\ttest=%d, &test=%p\n", test, &test); } else { printf("Eltern-Prozess:\ttest=%d, &test=%p\n", test, &test); sleep(2); printf("Eltern-Prozess:\ttest=%d, &test=%p\n", test, &test); } return(0); } Wenn wir dieses Programm übersetzen und ausführen, erhalten wir die Ausgabe: Eltern-Prozess: Kind-Prozess: Kind-Prozess: Eltern-Prozess: test=1, test=1, test=1, test=2, test=1, &test=0xbffff8c0 &test=0xbffff8c0 &test=0xbffff8c0 &test=0xbffff8c0 &test=0xbffff8c0 Man sieht, dass der Inhalt der Variablen test durch die Anweisung in Zeile 19 des Quelltextes innerhalb des Kind-Prozesses von ursprünglich 1 auf 2 verändert wird; im Eltern-Prozess bleibt jedoch auch danach der alte Wert erhalten, obwohl die Adresse der Variablen (also die Speicherstelle) im Eltern- und Kind-Prozess die gleiche zu sein scheint! Man muss sich hier ins Gedächtnis rufen, dass Linux ein Betriebssystem mit einer virtuellen Speicherverwaltung ( virtual memory“) ist. ” Es handelt sich bei den Adressen also nicht um physikalische Speicherstellen, sondern um Offsets innerhalb einer Seite ( Page“) des virtuellen Speichers. So bleibt ” die Adresse für beide Prozesse konstant; durch den Schreibzugriff wird jedoch die entsprechende Seite des virtuellen Speichers kopiert und die Änderung nur in der Kopie durchgeführt. Anhand dieses Beispielprogramms lässt sich ein weiterer interessanter Effekt demonstrieren. Leiten Sie dazu die Ausgabe des Programms in eine Datei um, und geben Sie diese dann aus (die Benutzereingaben sind schräg dargestellt): > new child > ausgabe > cat ausgabe test=1, &test=0xbffff8c0 Kind-Prozess: test=1, &test=0xbffff8c0
5.2 Neue Prozesse starten Kind-Prozess: test=2, test=1, Eltern-Prozess: test=1, Eltern-Prozess: test=1, 85 &test=0xbffff8c0 &test=0xbffff8c0 &test=0xbffff8c0 &test=0xbffff8c0 Die Reihenfolge der Ausgaben hat sich verändert. Außerdem erfolgt die Ausgabe vor Erzeugung des Kind-Prozesses (Quelltext Zeile 13) doppelt! Was ist passiert? Sobald der Standard-Ausgabekanal durch das Umlenk-Symbol >“ in eine Datei ” geleitet wird, erfolgt eine Pufferung der Daten (vgl. Abschnitt 4.1.1); ohne Umleiten der Ausgabe ist das nicht der Fall. Durch die Pufferung werden die Zeichen nicht sofort in die angegebene Datei geschrieben, sondern zunächst im Puffer zwischengespeichert – so auch die Ausgabe der printf()-Anweisung in Zeile 13. Nach Erzeugung des Kind-Prozesses benutzen beide Prozesse den gleichen Puffer weiter. Schreibt einer der beiden Prozesse weitere Zeichen in den Puffer, wird dieser zuvor dupliziert, d.h. sein gesamter Inhalt kopiert. Dadurch liegt die Ausgabe der printf()-Anweisung jetzt in zwei getrennten Puffern, die beide mit der Datei ausgabe“ verknüpft sind. ” Sowohl Eltern- als auch Kind-Prozess schreiben jetzt weitere Zeilen in ihren jeweiligen Puffer, ohne dass wirklich Zeichen in die angegebene Datei geschrieben werden. Erst wenn einer der beiden Prozesse beendet wird, dieser also beim return(0) in Zeile 33 angelangt ist, wird sein Pufferinhalt in die Datei geschrieben. Dadurch enthält die Ausgabedatei zunächst alle Ausgaben des KindProzesses (der aufgrund der kürzeren Wartezeit bei der Funktion sleep() in Zeile 17 immer vor dem Eltern-Prozess beendet wird), gefolgt von allen Ausgaben des Eltern-Prozesses – beide jeweils angeführt von der Ausgabe der printf()Anweisung in Zeile 13. Will man das Duplizieren des Pufferinhaltes bei Erzeugung eines neuen KindProzesses verhindern, kann man mit Hilfe der Funktion fflush() (siehe Abschnitt 4.1.4) den Pufferinhalt unmittelbar vor Erzeugen des Kind-Prozesses in die Datei schreiben und den Puffer selbst leeren. 5.2.4 Warteschleifen Beim Programmieren unter Betriebssystemen, die nicht Multitasking-fähig sind, ist es üblich, mit Hilfe von Warteschleifen auf bestimmte Ereignisse – etwa das Drücken einer Taste – zu warten. Unter Linux sollte man jedoch nach Möglichkeit auf solche Warteschleifen verzichten und stattdessen den Ablauf des Programms mit Hilfe von Signalen (siehe nächsten Abschnitt) steuern. Jede Warteschleife kostet Rechenzeit, die sonst für andere Prozesse zur Verfügung stehen würde. Soll ein Programm nur eine bestimmte Zeit lang warten, ohne dies von einem Ereignis abhängig zu machen, können dazu die Funktionen sleep() und usleep() verwendet werden. Diesen Funktionen wird als Parameter die zu wartende Zeit in Sekunden bzw. Mikrosekunden (das u“ in usleep() steht für µ und damit ”
86 5 Interprozesskommunikation für 10 −6 ) übergeben. Beachten Sie jedoch, dass das Eintreffen eines nicht geblockten Signals (siehe Abschnitt 5.3) diese Funktionen ungeachtet der verbleibenden Wartezeit sofort beendet. Lässt sich einmal eine Warteschleife nicht oder nur mit viel Aufwand umgehen, sollte die Schleife einen Aufruf der Funktion sched yield() enthalten. Diese Funktion unterbricht die Ausführung des Programms und stellt den Prozess an das Ende der Liste der auszuführenden Prozesse. Dadurch wird der Rechenzeitbedarf der Warteschleife deutlich reduziert. Als Beispiel sei das folgende Programm betrachtet, das wartet, bis eine CD-ROM oder Audio-CD in das CD-Laufwerk eingelegt wird:1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /* cdwait.c - Auf das Einlegen einer CD-ROM warten. */ # include <unistd.h> # include <fcntl.h> # include <sched.h> int main() { int fd; while ((fd = open("/dev/cdrom", O_RDONLY)) == -1) sched_yield(); close(fd); return(0); } 5.3 Signale Ein wichtiges Hilfsmittel für die Ablaufsteuerung von Prozessen und für die Kommunikation zwischen Prozessen sind die Signale. Linux kennt 31 verschiedene Signale, die in der Include-Datei /usr/include/bits/signum.h aufgelistet sind. Hier eine Übersicht über die wichtigsten Signale: 1 Zum Ausführen des Programms müssen Sie über Lesezugriff (r) auf das Device /dev/cdrom“ ” verfügen.
5.3 Signale 87 Signalname SIGINT SIGKILL SIGUSR1 SIGUSR2 SIGALRM SIGTERM SIGCHLD Signal-Nr. 2 9 10 12 14 15 17 Bedeutung Unterbrechungsanforderung (Ctrl-C) Prozess beenden (nicht blockierbar) benutzerdefiniert, zur freien Verwendung benutzerdefiniert, zur freien Verwendung für die Weckfunktion Prozess beenden (blockierbar) Kind-Prozess wurde beendet Ein Prozess kann einem anderen Prozess ein Signal schicken“, um ihn z.B. auf ” ein Ereignis aufmerksam zu machen. 5.3.1 Die Weckfunktion alarm() Wenn ein Prozess auf ein Ereignis wartet, ist es häufig erforderlich, eine maximale Wartezeit zu realisieren, für den Fall, dass das Ereignis nicht eintrifft. Um ein solches Timeout-Verhalten zu realisieren, kann die Funktion alarm() verwendet werden: unsigned int alarm(unsigned int seconds); Die Funktion startet einen Zeitgeber, der nach Ablauf der mit seconds angegebenen Zeit (in Sekunden) dem Prozess das Signal SIGALRM schickt. Als Rückgabewert liefert alarm() die Restzeit, die bei dem vorangegangenen alarm()-Aufruf noch verblieben ist, bzw. 0, wenn zuvor kein Aufruf der Funktion alarm() erfolgte. Als Voreinstellung führt das Signal SIGALRM zum Abbruch des Programms (Prozesses) mit Ausgabe der Meldung Der Wecker klingelt“ (bzw. Alarm clock“) ” ” und einem Rückgabewert ungleich 0. Ergänzt man das Beispiel-Programm aus Abschnitt 5.2.4 um die Funktion alarm(), kann damit ein Timeout für das Einlegen einer CD realisiert werden: 1 2 3 4 5 6 7 8 9 10 11 12 /* cdwait2.c - Auf das Einlegen einer CD-ROM warten. */ # include <unistd.h> # include <fcntl.h> # include <sched.h> int main() { int fd;
88 13 14 15 16 17 18 19 20 5 Interprozesskommunikation alarm(10); /* Timeout: 10 Sekunden */ while ((fd = open("/dev/cdrom", O_RDONLY)) == -1) sched_yield(); close(fd); return(0); } 5.3.2 Einen Signal-Handler einrichten In dem vorangegangenen Beispiel wird das Programm durch das Signal SIGALRM beendet. Mit Hilfe der Funktion signal() lässt sich die Reaktion eines Prozesses auf ein bestimmtes Signal modifizieren:1 signal(int signum, void *handler()); Diese Funktion richtet für das Signal mit der Nummer signum einen so genannten Signal-Handler ein. Der Parameter handler() kann dabei entweder eine der Konstanten SIG IGN und SIG DFL oder der Name einer benutzerdefinierten Funktion sein. SIG IGN (IGN steht für ignore) bewirkt, dass das Signal blockiert wird, also keine Auswirkung zeigt, während SIG DFL (DFL steht für default) den für das entsprechende Signal voreingestellten Signal-Handler einrichtet. Die Funktion signal() liefert als Rückgabewert den zuvor mit diesem Signal verknüpften Signal-Handler bzw. die Konstante SIG ERR, falls der Signal-Handler nicht eingerichtet werden konnte. Bei Verwendung einer benutzerdefinierten Funktion als Signal-Handler muss diese vom Typ void sein und einen Parameter vom Typ int haben. Mit diesem Parameter wird dem Signal-Handler die Signalnummer übergeben. Damit besteht die Möglichkeit, den gleichen Signal-Handler für verschiedene Signale zu benutzen und dann anhand des Parameters festzustellen, welches Signal tatsächlich gesetzt wurde. Im folgenden Programmbeispiel wurde der Quelltext aus dem vorangegangenen Abschnitt um das Einrichten eines Signal-Handlers für das Signal SIGALRM ergänzt: 1 2 3 4 5 6 1 /* cdwait3.c - Auf das Einlegen einer CD-ROM warten. */ # include <stdio.h> # include <unistd.h> Die Deklaration der Funktion signal() ist hier etwas vereinfacht dargestellt, die exakte Deklaration erhält man mit man 2 signal“. ”
5.3 Signale 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # # # # include include include include 89 <stdlib.h> <fcntl.h> <sched.h> <signal.h> void my_handler(int signum) { fprintf(stderr, "cdwait3: timeout\n"); exit(1); } int main() { int fd; signal(SIGALRM, my_handler); alarm(10); /* Signal-Handler einrichten */ /* Timeout: 10 Sek. */ while ((fd = open("/dev/cdrom", O_RDONLY)) == -1) sched_yield(); close(fd); return(0); } Wird dieses Programm gestartet und innerhalb der darauf folgenden 10 Sekunden keine CD eingelegt, so gibt das Programm die Meldung cdwait3: timeout“ ” aus und bricht mit dem Rückgabewert 1 ab. 5.3.3 Auf die Beendigung eines Kind-Prozesses warten Häufig ist es erforderlich, dass ein Eltern-Prozess auf die Beendigung eines Kind-Prozesses wartet, z.B. wenn in dem Kind-Prozess mit execlp() (vgl. Abschnitt 5.2.2) ein Programm ausgeführt wird und der Eltern-Prozess erst danach fortfahren soll. Zu diesem Zweck stehen die Funktionen wait() und waitpid() zur Verfügung: pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); Die Funktion wait() wartet auf die Beendigung eines Kind-Prozesses und liefert dessen Prozess-ID als Rückgabewert bzw. –1 bei einem Fehler. Die Funktion waitpid() wartet auf die Beendigung des Kind-Prozesses mit der in pid angegebenen Prozess-ID. Beide Funktionen speichern Statusinformationen zu dem
90 5 Interprozesskommunikation beendeten Prozess in die Variable status, sofern hier nicht die Konstante NULL angegeben wurde. Als options kann entweder eine 0 oder eine Kombination aus den Konstanten WNOHANG und WUNTRACED angegeben werden. WNOHANG bewirkt, dass lediglich geprüft wird, ob der angegebene Prozess beendet wurde, ohne jedoch auf dessen Beendigung zu warten. Der Rückgabewert des beendeten Kind-Prozesses kann aus der Statusinformation mit Hilfe des Makros WEXITSTATUS() ermittelt werden. Das folgende Programm, das in einem Kind-Prozess den Shell-Befehl ls“ auf” ruft, soll die Handhabung von wait() und WEXITSTATUS() verdeutlichen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* my_ls.c - "ls" in einem Kind-Prozess ausfuehren */ # # # # include include include include <stdio.h> <unistd.h> <sys/types.h> <sys/wait.h> int main(int argc, char *argv[]) { pid_t pid; int status; if ((pid = fork()) == 0) { /* Kind-Prozess */ execlp("ls", "ls", "-F", argv[1], NULL); perror("my_ls: execlp() failed"); return(1); } wait(&status); /* Eltern-Prozess */ printf("Child process exited with return code %d.\n", WEXITSTATUS(status)); return(0); } 5.3.4 Signale setzen mit kill() Ein Kind-Prozess kann dem Eltern-Prozess nicht nur seine Beendigung signali” sieren“. Man kann Signale auch zur Kommunikation zwischen Eltern- und KindProzess verwenden, etwa um ein bestimmtes Ereignis zu melden. Grundsätzlich lassen sich aber nur Signale an einen Prozess des gleichen Benutzers schicken, es sei denn, das Programm läuft mit root-Privilegien. Das Setzen von Signalen erfolgt mit der Funktion kill():
5.4 Datenaustausch zwischen Prozessen 91 int kill(pid_t pid, int signum); wobei pid die Prozess-ID des Prozesses angibt, bei dem das Signal signum gesetzt werden soll. Kann es gesetzt werden, gibt kill() eine 0 zurück, anderenfalls eine –1 (z. B. wenn der angegebene Prozess nicht existiert). Durch Verwendung des Signals SIGKILL oder SIGTERM ist es mit Hilfe dieser Funktion auch möglich, einen anderen Prozess vorzeitig zu beenden. 5.4 Datenaustausch zwischen Prozessen Wie wir bereits in Abschnitt 5.2.3 gesehen haben, können Eltern- und KindProzess nicht ohne weiteres über Variablen Informationen austauschen – sobald einer der Prozesse in den zunächst gemeinsamen Speicher schreibt, wird dieser dupliziert und ist fortan für beide Prozesse getrennt. Daher bedarf es anderer Verfahren, um einen Datenaustausch zwischen Prozessen zu realisieren, der über das Setzen von Signalen hinausgeht. Linux bietet hierfür drei verschiedene Möglichkeiten an: Pipes, FIFOs und Shared Memory. Diese werden in den folgenden Abschnitten vorgestellt und erläutert. 5.4.1 Pipes Eine Pipe ist eine Art virtuelle Datei“, dargestellt über zwei Datei-Deskriptoren ” (siehe Abschnitt 4.1.1) – einer zum Lesen und der andere zum Schreiben. Erzeugt wird eine Pipe mit Hilfe der gleichnamigen Funktion: int pipe(int filedes[2]); Als Parameter wird ein Feld aus zwei Datei-Deskriptoren (Integer-Variablen) benötigt, in das die Funktion pipe() die erzeugten Deskriptoren schreibt. pipe() liefert den Rückgabewert 0 bei Erfolg oder –1 bei einem Fehler. Zur Verdeutlichung der Anwendung von pipe() sei das folgende Programm betrachtet, bei dem der Eltern-Prozess eine Zeichenkette an den Kind-Prozess sen” det“: 1 2 3 4 5 6 7 8 9 /* pipe.c - Interprozesskommunikation mit einer Pipe */ # include <stdio.h> # include <unistd.h> # include <sys/wait.h> int main()
92 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 5 Interprozesskommunikation { int fd[2], l; char buffer[80]; if (pipe(fd) != 0) { perror("pipe: pipe() failed"); return(1); } if (fork() == 0) { close(fd[1]); /* Kind-Prozess */ if ((l = read(fd[0], buffer, 79)) == -1) perror("pipe: read() failed"); else { buffer[l] = ’\0’; printf("Received string: ’%s’\n", buffer); } close(fd[0]); return(0); } close(fd[0]); sleep(1); write(fd[1], "Test!", 5); wait(NULL); /* Eltern-Prozess */ close(fd[1]); return(0); } In Zeile 14 wird zunächst eine Pipe erzeugt; die zugehörigen Datei-Deskriptoren werden in das Feld fd[] geschrieben. Danach wird in Zeile 20 mit fork() der Kind-Prozess gestartet. In dem Kind-Prozess wird der zweite Datei-Deskriptor der Pipe geschlossen (Zeile 22), weil dieser nur vom Eltern-Prozess benötigt wird. Das Schließen von fd[1] im Kind-Prozess hat wegen des copy-on-writeMechanismus (vgl. Abschnitt 5.2.3) keinen Einfluss auf den gleichen Deskriptor im Eltern-Prozess. Analog wird im Eltern-Prozess der dort nicht benötigte Deskriptor fd[0] geschlossen (Zeile 34). In Zeile 23 wird jetzt innerhalb des Kind-Prozesses aus der Pipe gelesen. Die Funktion read() verhält sich ähnlich wie fread(), benötigt anstelle eines Streams jedoch einen Datei-Deskriptor (eine genaue Beschreibung der Funktionen read()
5.4 Datenaustausch zwischen Prozessen 93 und write() folgt in Kapitel 6). Nach Ausgabe der Zeichenkette mit printf() in Zeile 28 wird der zweite Datei-Deskriptor der Pipe geschlossen (Zeile 30) und der Kind-Prozess beendet. Der Eltern-Prozess wartet in diesem Beispiel zunächst eine Sekunde lang (Zeile 35) und schreibt danach eine Zeichenkette in die Pipe (Zeile 36). Anschließend wartet der Eltern-Prozess auf die Beendigung des Kind-Prozesses (Zeile 37), bevor der zweite Datei-Deskriptor geschlossen und das Programm beendet wird (Zeile 39 und 40). Für eine bidirektionale Kommunikation zwischen zwei Prozessen müssen zwei Pipes geöffnet werden. Die Verwendung von Pipes als Standardein- und -ausgabe Wenn mit Hilfe einer Funktion der exec-Familie (siehe Abschnitt 5.2.2) in einem Kind-Prozess ein externes“ Programm aufgerufen wird, sollen häufig die Ein” und Ausgaben dieses Programms über den Eltern-Prozess laufen. Dazu gibt es die Möglichkeit, die Standardein- und -ausgabe eines Prozesses durch je eine Pipe zu ersetzen. Das folgende Programm ruft in einem Kind-Prozess das Programm sort“ auf und schickt eine Zeichenkette, bestehend aus mehreren Zeilen, an die ” Standardeingabe von sort. Die Standardausgabe von sort wird wiederum vom Eltern-Prozess gelesen und ausgegeben: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* pipe2.c - Pipes als Standardein- und -ausgabe */ # include <stdio.h> # include <unistd.h> # include <sys/wait.h> int main() { int fd1[2], fd2[2], l; char buffer[80]; if ((pipe(fd1) != 0) || (pipe(fd2) != 0)) { perror("pipe2: pipe() failed"); return(1); } if (fork() == 0) {
94 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 5 Interprozesskommunikation close(fd1[1]); /* Kind-Prozess */ close(fd2[0]); if ((dup2(fd1[0], STDIN_FILENO) == -1) || (dup2(fd2[1], STDOUT_FILENO) == -1)) { perror("pipe2: dup2() failed"); return(1); } close(fd1[0]); close(fd2[1]); execlp("sort", "sort", NULL); perror("pipe2: execlp() failed"); return(1); } close(fd1[0]); close(fd2[1]); /* Eltern-Prozess */ write(fd1[1], "These\nlines\nshall\nbe\nsorted\n", 28); close(fd1[1]); wait(NULL); if ((l = read(fd2[0], buffer, 79)) == -1) perror("pipe2: read() failed"); else write(STDOUT_FILENO, buffer, l); close(fd2[0]); return(0); } Besonders interessant sind die Zeilen 24 und 25. Hier werden mit Hilfe der Funktion dup2() ( duplicate to“) die Datei-Deskriptoren der Pipes auf die der ” Standardein- bzw. -ausgabe des Kind-Prozesses kopiert. Danach werden fd1[0] und fd2[1] im Kind-Prozess nicht mehr benötigt, da diese jetzt als Standardeinund -ausgabe zur Verfügung stehen. Abbildung 5.1 veranschaulicht die Funktion der vier Datei-Deskriptoren bei diesem Beispiel-Programm. Durch Schließen des Deskriptors fd1[1] in Zeile 41 wird dem Kind-Prozess – und damit dem Programm sort – das Ende der Eingaben signalisiert. Dadurch beginnt sort mit der Sortierung und Ausgabe in die Pipe fd2[]. Nach Ausgabe der letzten Zeile wird das Programm sort und der Kind-Prozess beendet, sodass der Eltern-Prozess nach der wait()-Anweisung in Zeile 42 fortfährt.
5.4 Datenaustausch zwischen Prozessen fd1[1] 95 fd1[0] - stdin ElternProzess KindProzess fd2[0]  fd2[1] stdout Abbildung 5.1: Verwendung der Pipes in dem Programm pipe2“ ” Hinweis: Ohne die close()-Anweisung in Zeile 41 würde sort auf weitere Eingaben warten und den Kind-Prozess (und somit auch den Eltern-Prozess) blockieren! Wird der Standardausgabekanal eines Prozesses durch eine Pipe ersetzt, erfolgt automatisch eine Pufferung der auszugebenden Zeichen. Dadurch werden Ausgaben nicht sofort in die Pipe, sondern zunächst nur in den Dateipuffer geschrieben und sind somit für den Eltern-Prozess noch nicht verfügbar. Erst wenn der Puffer (8192 Bytes) voll ist oder der Kind-Prozess beendet wird, werden die Ausgaben tatsächlich in die Pipe geschrieben. 5.4.2 FIFOs Als Alternative zu Pipes können zur Kommunikation zwischen zwei (oder mehr) Prozessen so genannte FIFOs ( first-in-first-out“) verwendet werden. Dabei han” delt es sich im Prinzip ebenfalls um Pipes, die jedoch einen Eintrag im Dateisystem haben und daher mit den üblichen Funktionen für Dateizugriffe angesprochen werden können. Eine FIFO-Datei wird mit der Funktion mkfifo() (oder dem gleichnamigen Shell-Befehl) erzeugt: int mkfifo(char *pathname, mode_t mode); Die Zeichenkette pathname gibt an, welchen Pfad- und Dateinamen die FIFODatei erhalten soll. Der Parameter mode bestimmt die Zugriffsrechte der FIFODatei, wobei nur solche Rechte gesetzt werden können, die auch für den aktuellen Prozess gesetzt sind (siehe auch man umask“). Das folgende Beispiel-Programm ” zeigt die Erzeugung und Handhabung einer FIFO-Datei: 1 2 3 4 5 6 7 /* fifo.c - Datenaustausch mit Hilfe einer FIFO-Datei */ # include <stdio.h> # include <unistd.h> # include <sys/stat.h>
96 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 5 Interprozesskommunikation int main() { char buffer[80]; FILE *stream; if (mkfifo("my_fifo", 0600) != 0) { perror("fifo: mkfifo() failed"); return(1); } if (fork() == 0) { /* Kind-Prozess */ if ((stream = fopen("my_fifo", "w")) == NULL) perror("fifo: Can’t open FIFO for writing"); else { fprintf(stream, "Can you hear me?\n"); fclose(stream); } return(0); } /* Eltern-Prozess */ if ((stream = fopen("my_fifo", "r")) == NULL) perror("fifo: Can’t open FIFO for reading"); else { fgets(buffer, 80, stream); printf("Child process sent: %s", buffer); fclose(stream); } remove("my_fifo"); return(0); } Vielleicht fragen Sie sich an dieser Stelle, worin der Unterschied zwischen einer FIFO-Datei und einer regulären Datei besteht und welchen Vorteil FIFOs für die Prozesskommunikation bringen. Ein wesenlicher Unterschied besteht darin, dass eine FIFO-Datei kein Ende“ hat. Liest man ein Zeichen aus einer leeren, ” regulären Datei (z.B. mit fgetc()), erhält man den Dateiende-Marker EOF. Bei einer FIFO-Datei wird der Prozess so lange angehalten, bis ein Zeichen in die FIFO-Datei geschrieben wurde. Auch beim Schreiben in die FIFO-Datei wird der Prozess blockiert, bis ein anderer Prozess die FIFO-Datei zum Lesen öffnet. Da-
5.4 Datenaustausch zwischen Prozessen 97 durch lassen sich zwei Prozesse mit Hilfe von FIFO-Dateien synchronisieren, was mit regulären Dateien nur schwer möglich ist. 5.4.3 Shared Memory Pipes und FIFOs bieten die Möglichkeit, einen sequenziellen Datenstrom zwischen zwei Prozessen auszutauschen. Wird dagegen der wahlfreie Zugriff “ (random ” access) eines Prozesses auf Datenstrukturen eines anderen Prozesses benötigt, so bietet sich die Verwendung von Shared Memory an. Dabei handelt es sich um Speicher, der von mehreren Prozessen parallel genutzt werden kann. Hier eine Liste der relevanten Funktionen: Name Verwendung shmget() shmat() shmdt() shmctl() Ein Shared-Memory-Segment anfordern Ein Shared-Memory-Segment an einen Prozess anbinden Ein Shared-Memory-Segment von einem Prozess lösen Steuerung von Shared-Memory-Segmenten Als erster Schritt muss die Reservierung eines Shared-Memory-Segments mit der Funktion shmget() erfolgen: int shmget(key_t key, int size, int shmflg); Als key“ sollte hier das Schlüsselwort IPC PRIVATE angegeben werden, um ” shmget() zu veranlassen, neuen Speicher zu reservieren. Der Parameter size gibt die Größe des zu reservierenden Segments an, wobei shmget() immer auf die nächsten 4 kByte (PAGE SIZE) aufrundet. Da wir in das Segment schreiben und aus dem Segment lesen wollen, muss als shmflg der Ausdruck SHM_R | SHM_W“ angegeben werden. Die Funktion shmget() liefert dann ent” weder eine ID-Nummer für das reservierte Shared-Memory-Segment oder eine –1, falls das Segment nicht reserviert werden konnte. Achtung: Tatsächlich erfolgt bei dem Aufruf von shmget() nur das Anlegen der erforderlichen Datenstrukturen, der Speicher selbst wird erst reserviert, wenn das Segment mit shmat() an einen Prozess angebunden wird! Im zweiten Schritt sollte das neu eingerichtete Shared-Memory-Segment, identifiziert über seine ID-Nummer, mit Hilfe der Funktion shmat() an mindestens einen Prozess angebunden werden: void *shmat(int shmid, void *shmaddr, int shmflg); Der Parameter shmid stellt die von shmget() gelieferte ID-Nummer dar. Über shmaddr kann hier eine feste Adresse vorgegeben werden. In der Regel sollte dieser Parameter jedoch 0 sein, wodurch shmat() veranlasst wird, den nächsten freien Speicherblock zu reservieren. Als shmflg kann man entweder eine 0 für
98 5 Interprozesskommunikation Schreib- und Lesezugriff oder die Konstante SHM_RDONLY angeben, falls diesem Prozess nur das Lesen aus dem Shared-Memory-Segment gestattet werden soll. Als Rückgabewert erhält man die Adresse des reservierten Speichers oder –1 im Fehlerfall. Ein typischer Programmausschnitt zur Anforderung von Shared-Memory sieht somit wie folgt aus: int shmem_id; void *shmem_addr; if ((shmem_id = shmget(IPC_PRIVATE, PAGE_SIZE, SHM_R | SHM_W)) == -1) { perror("shmget() failed"); exit(1); } if ((shmem_addr = shmat(shmem_id, 0, 0)) == (void *)-1) { perror("shmat() failed"); shmctl(shmem_id, IPC_RMID, 0); exit(1); } Danach steht dem Prozess ab der Adresse shmem addr ein Speicherblock von der Größe PAGE SIZE (4 kByte) zur Verfügung, den auch andere Prozesse nutzen können. Wird nun mit fork() ein Kind-Prozess erzeugt, erbt dieser automatisch auch das angebundene Shared-Memory-Segment, sodass der Kind-Prozess ohne weitere Maßnahmen in diesen Speicher schreiben kann! Vielleicht haben Sie in dem oben aufgeführten Programmausschnitt den Funktionsaufruf shmctl() bemerkt. Diese Funktion erlaubt es, mit dem SharedMemory-Segment verschiedene Aktionen auszuführen – eine davon ist IPC RMID, mit der das Segment entfernt wird. Damit wird verhindert, dass die dem Segment zugeordneten Datenstrukturen als Speicherleichen“ zurückbleiben. ” Nach erfolgreichem shmget()-Aufruf sollte am Ende des Programms das auf diese Weise angelegte Segment mit shmctl() und IPC RMID unbedingt wieder entfernt werden. Hinweis: Durch IPC RMID wird das Segment als zu löschen“ markiert. Das ” tatsächliche Löschen des Shared-Memory-Segments und die Freigabe des Speichers erfolgen erst, wenn alle Prozesse, die das Segment anbanden, dieses wieder gelöst haben!
5.4 Datenaustausch zwischen Prozessen 99 Das Lösen eines Shared-Memory-Segments geschieht automatisch bei Beenden des Prozesses oder vorher durch Aufruf der Funktion: int shmdt(void *shmaddr); Als Parameter wird die Adresse des Speicherblocks benötigt, wie sie von der Funktion shmat() geliefert wurde. Das folgende Beispiel-Programm richtet ein Shared-Memory-Segment ein und beschreibt dieses vom Kind-Prozess aus. Der Eltern-Prozess liest nach Beendigung des Kind-Prozesses den Inhalt dieses Segments und gibt ihn aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /* sharedmem.c */ # # # # # include include include include include <stdio.h> <unistd.h> <sys/shm.h> <sys/ipc.h> <sys/wait.h> int main() { int shmem_id; char *buffer; if ((shmem_id = shmget(IPC_PRIVATE, 80, SHM_R | SHM_W)) == -1) { perror("sharedmem: shmget() failed"); return(1); } if ((buffer = shmat(shmem_id, 0, 0)) == (char *)-1) { perror("sharedmem: shmat() failed"); shmctl(shmem_id, IPC_RMID, 0); return(1); } buffer[0] = ’\0’; /* Puffer initialisieren */ if (fork() == 0) { /* Kind-Prozess */ sprintf(buffer, "Message from child process"); shmdt(buffer);
100 35 36 37 38 39 40 41 42 43 44 45 5 Interprozesskommunikation return(0); } wait(NULL); /* Eltern-Prozess */ printf("Child process wrote into buffer: ’%s’\n", buffer); shmdt(buffer); shmctl(shmem_id, IPC_RMID, 0); return(0); } Die shmdt()-Aufrufe in den Zeilen 33 und 41 sind im Grunde obsolet. Wie bereits ausgeführt, werden alle an einen Prozess angebundenen Shared-MemorySegmente beim Beenden des Prozesses automatisch gelöst. 5.5 Alternative Verfahren zur Erzeugung von Prozessen Wie die Beispiele zur Interprozesskommunikation gezeigt haben, erfordert der Datenaustauch zwischen Eltern-Prozess und dem mit fork() erzeugten KindProzess einen gewissen Aufwand. Unter Linux gibt es jedoch neben den bisher beschriebenen Funktionen fork() und system() weitere Möglichkeiten, einen neuen Prozess zu erzeugen. Diese alternativen Verfahren vereinfachen dabei die Kommunikation zwischen Eltern- und Kind-Prozess. 5.5.1 popen() und pclose() Ganz ähnlich wie die in Abschnitt 5.2.1 vorgestellte Funktion system() führt auch die Funktion popen() das als Zeichenkette übergebene Kommando in einer Shell aus: FILE *popen(const char *command, const char *type); Gleichzeitig öffnet diese Funktion aber auch eine Pipe und gibt diese als Zeiger auf einen Stream (Achtung: gepufferte Ein-/Ausgabe!) zurück. Mit dem Parameter type lässt sich einstellen, ob der Ausgabe- oder der Eingabekanal der Shell in die Pipe umgelenkt wird. Dementsprechend kann aus dem Stream gelesen (type= r“) oder in den Stream geschrieben (type= w“) werden. Anders als ” ” bei der Funktion system() wartet popen() nicht auf die Beendigung des KindProzesses, sondern kehrt unmittelbar zurück, sodass Eltern- und Kind-Prozess parallel laufen. Die Funktion pclose() wartet auf die Beendigung des Kind-Prozesses und liefert dessen Rückgabewert (Exit-Status):
5.5 Alternative Verfahren zur Erzeugung von Prozessen 101 int pclose(FILE *stream); Das folgende Beispielprogramm zeigt die Verwendung der Funktionen popen() und pclose(). Das Programm gibt alle im aktuellen Verzeichnis enthaltenen Unterverzeichnisse aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /* popen.c - Unterverzeichnisse anzeigen */ # include <stdio.h> # include <string.h> int main() { int status; FILE *stream; char buffer[40]; if ((stream = popen("ls -F", "r")) == NULL) { perror("popen: popen() failed"); return(1); } while (fgets(buffer, 40, stream) != NULL) if (buffer[strlen(buffer)-2] == ’/’) printf("%s", buffer); status = pclose(stream); printf("(ls returned %d.)\n", status); return(0); } 5.5.2 Die fork()-Alternative clone() Unter Linux – und nur dort – gibt es alternativ zu fork() die Funktion clone(). Diese Funktion arbeitet ähnlich wie fork(), erlaubt jedoch, dass sich Eltern- und Kind-Prozess den Speicher teilen. Dadurch können beide Prozesse beispielsweise über globale Variablen Informationen austauschen. Der Aufruf von clone() ist leider etwas komplizierter als der von fork(): int clone(int *function, void *child_stack, int flags, void *arg);
102 5 Interprozesskommunikation Als ersten Parameter erwartet clone() einen Zeiger auf die im parallelen Prozess auszuführende Funktion. Diese Funktion muss vom Typ int sein und kann maximal einen Parameter besitzen. Der zweite Parameter von clone() gibt die Adresse des Stacks für den Kind-Prozess an; weil der Stack bei PCs von oben nach unten gefüllt wird, muss dieser Parameter auf das obere Ende eines hinreichend großen Speicherblocks zeigen. Der mit flags bezeichnete Parameter erlaubt es, Optionen wie CLONE VM (Eltern- und Kind-Prozess teilen sich den virtuellen Speicher) anzugeben. Außerdem enthält flags die Signale, die bei Beendigung des Kind-Prozesses gesendet werden sollen. Als letzter Parameter kann ein beliebiger Zeiger angegeben werden, der an die im Kind-Prozess auszuführende Funktion übergeben wird. Als Rückgabewert liefert clone() die Prozess-ID des KindProzesses bzw. –1 im Fehlerfall. Um die Anwendung der Funktion clone() zu verdeutlichen, sei das folgende Programm betrachtet, das einen neuen Prozess erzeugt, der eine globale Variable verändert und so eine Information an den Eltern-Prozess überträgt: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /* clone.c - Kind-Prozess mit clone() erzeugen */ # # # # # include include include include include <stdio.h> <string.h> <sched.h> <signal.h> <sys/wait.h> static char buffer[80] = "Can you hear me?"; static char stack[10000]; int child_function(void *text) { printf("child is running.\n"); strcat(buffer, text); return(0); } int main() { int status; printf("buffer=’%s’\n", buffer); printf("running child...\n");
5.5 Alternative Verfahren zur Erzeugung von Prozessen 29 30 31 32 33 34 35 36 37 38 39 40 41 103 if (clone(&child_function, &(stack[10000]), CLONE_VM | SIGCHLD, " Yes!") == -1) { perror("clone: clone() failed"); return(1); } wait(&status); printf("child returned %d.\n", status); printf("buffer=’%s’\n", buffer); return(0); } Wenn Sie diesen Quelltext übersetzen und das Programm anschließend starten, sollte die folgende Ausgabe erscheinen: > clone buffer=’Can you hear me?’ running child... child is running. child returned 0. buffer=’Can you hear me? Yes!’ In Zeile 11 des Programms wird die globale Variable buffer definiert und mit einer Zeichenkette initialisiert. In Zeile 13 ist der Speicher für den Stack des KindProzesses mit 10.000 Bytes angelegt. Die im Kind-Prozess ausgeführte Funktion ist in den Zeilen 15 bis 20 definiert. Sie erweitert den Inhalt der Variablen buffer um die als Parameter übergebene Zeichenkette text. Im Hauptprogramm wird der Inhalt von buffer einmal vor dem Starten des Kind-Prozesses und einmal nach dessen Beendigung ausgegeben. Wie bereits erwähnt, ist clone() eine Linux-spezifische Funktion. Wenn Sie ein Programm portabel, d. h. auf andere Unix-Systeme übertragbar halten wollen, sollten Sie auf die Verwendung von clone() verzichten. 5.5.3 POSIX-Threads Eine weitere Möglichkeit, Multi-Thread-Programme zu erstellen, besteht in der Verwendung der Funktionsbibliothek libpthread“. Diese Bibliothek stellt nicht ” nur Funktionen für die Erzeugung von Prozessen, sondern auch für eine komfortable Interprozesskommunikation bereit. Die Abkürzung pthread“ steht für POSIX Threads“; aber was ist POSIX“ ei” ” ” gentlich? Es handelt sich dabei ebenfalls um eine Abkürzung, und zwar für Port”
104 5 Interprozesskommunikation able Operating System Interface“ 1. Dieser IEEE-Standard ist der Versuch eines Konsortiums von Herstellern, ein einheitliches Unix zu entwickeln. Dabei beschreibt POSIX nicht nur C-Funktionen für Dateizugriff, Prozesse und Ein-/Ausgabe, sondern auch Shell-Befehle und die Arbeitsweise der Shell selbst. Es würde den Rahmen dieses Buches sprengen, den POSIX-Standard für die Interprozesskommunikation, so wie er in der libpthread implementiert ist, vollständig zu beschreiben. Hier soll lediglich der Einstieg in die Programmierung mit POSIXThreads gegeben werden. Ein (englisches) Tutorial zu den Funktionen der libpthread finden Sie unter [7]. Neue Prozesse werden mit der Funktion pthread create() erzeugt: int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *function, void *arg); Als ersten Parameter erwartet diese Funktion den Zeiger auf eine Variable vom Typ pthread t. In dieser Struktur werden Informationen über den neu erzeugten Prozess abgelegt. Der zweite Parameter zeigt auf eine Variable vom Typ pthread attr t, in der die gewünschten Eigenschaften des neu zu erzeugenden Prozesses eingetragen werden können. Wird statt eines Zeigers der Wert NULL übergeben, verwendet pthread create() die Standard-Eigenschaften für neue Threads. Ähnlich wie bei clone() wird auch bei pthread create() ein Zeiger auf die vom Kind-Prozess auszuführende Funktion übergeben – hier als dritter Parameter – sowie ein Zeiger auf eine Variable, die dieser Funktion als Parameter übergeben werden soll. Die im Kind-Prozess auszuführende Funktion sollte mit pthread exit() beendet werden: void pthread_exit(void *return_val); Als Parameter wird bei pthread exit() ein Zeiger auf den Rückgabewert übergeben. Dieser Rückgabewert könnte beispielsweise ein Fehler-Code oder ExitStatus des Kind-Prozesses sein. Alternativ zu pthread exit() kann man den Kind-Prozess auch mit return() beenden. In diesem Fall muss ebenfalls ein Zeiger auf den Rückgabewert als Parameter von return() verwendet werden. Die dritte Funktion aus der libpthread, die hier betrachtet werden soll, ersetzt quasi die Funktion waitpid(): int pthread_join(pthread_t thread, void **return_val); 1 Das X“ in der Abkürzung POSIX sollte wahrscheinlich die Nähe zu Unix verdeutlichen. ”
5.5 Alternative Verfahren zur Erzeugung von Prozessen 105 Diese Funktion wartet, bis der zum Parameter thread gehörende Prozess beendet wurde, und trägt dann in *return val den Zeiger auf den Rückgabewert des Prozesses ein. Das folgende Programm verdeutlicht die Verwendung dieser drei Funktionen aus der libpthread. Es handelt sich dabei um das clone()-Beispiel aus dem vorigen Abschnitt, das für die Verwendung von POSIX-Threads umgeschrieben wurde (und damit nicht nur unter Linux, sondern auch auf anderen POSIX-kompatiblen Systemen laufen sollte): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /* pthread.c - Kind-Prozess mit der libpthread */ # include <stdio.h> # include <string.h> # include <pthread.h> static char buffer[80] = "Can you hear me?"; void *child_function(void *text) { static int status; printf("child is running.\n"); strcat(buffer, text); status = 0; pthread_exit(&status); } int main() { int *status_ptr; pthread_t child_thread; printf("buffer=’%s’\n", buffer); printf("running child...\n"); if (pthread_create(&child_thread, NULL, &child_function, " Yes!")) { fprintf(stderr, "pthread: pthread_create() failed.\n"); return(1); } if (pthread_join(child_thread, (void *)&status_ptr))
106 38 39 40 41 42 43 44 45 5 Interprozesskommunikation fprintf(stderr, "pthread: pthread_join() failed.\n"); else printf("child returned %d.\n", *status_ptr); printf("buffer=’%s’\n", buffer); return(0); } Beim Kompilieren dieses Programms muss die libpthread mit eingebunden werden: gcc pthread.c -lpthread -o pthread Wenn Sie das Programm starten, sollte die gleiche Ausgabe wie bei dem clone()Beispiel erscheinen: > pthread buffer=’Can you hear me?’ running child... child is running. child returned 0. buffer=’Can you hear me? Yes!’
Kapitel 6 Devices – das Tor zur Hardware Eine der großen Stärken von Linux (und anderen Unix-Varianten) sind die Devices. Bei welchem anderen Betriebssystem können Sie mit einer Kommandozeile eine MIDI1 -Sequenz aufzeichnen2 oder etwa den Master-Boot-Record (MBR) einer Festplatte auslesen?3 Devices bilden die Schnittstelle zwischen der Hardware (z. B. dem CD-ROM-Laufwerk) und dem Applikationsprogramm (z. B. einem Brennprogramm wie cdrecord“). ” Dieses Kapitel soll einen Einblick in den Umgang mit Devices aus Sicht des Programmierers vermitteln. Es werden exemplarisch das CD-ROM-Laufwerk, Soundkarte, Video-Device (z. B. eine WebCam) und die serielle Schnittstelle RS 232 angesprochen. Auch die direkte Ansteuerung von USB-Geräten mit Hilfe der libusb wird gezeigt. 6.1 Das Device-Konzept von Linux Ein Großteil der Hardware-Komponenten wie Festplatten, CD-ROM-Laufwerk, Soundkarte, serielle und parallele Schnittstellen sind unter Linux als Devices – im Deutschen häufig als Geräte“ bezeichnet – verfügbar. Diese besitzen einen ” Eintrag im Dateisystem, für gewöhnlich im Verzeichnis /dev“. Die erste IDE” Festplatte ist beispielsweise mit /dev/hda1“ verknüpft. Die mit den Devices ” verknüpften Einträge im Dateisystem – auch als Inodes bezeichnet – sind vom Typ character special file“ oder block special file“ (siehe auch Abschnitt 4.2). Bei ” ” ls -l“ wird dieser Typ entsprechend durch ein c“ oder b“ am Zeilenanfang ” ” ” gekennzeichnet. 1 Music Instrument Digital Interface – Schnittstelle für Keyboards und andere elektronische Musikinstrumente 2 cat /dev/sequencer > midi-file“ (Abbruch der Aufzeichnung mit Ctrl-C) ” Für (E)IDE-Festplatten: dd if=/dev/hda of=mbr bs=512 count=1“ ” 3
108 6 Devices – das Tor zur Hardware Über zwei Gerätenummern, der major“ und der minor device number“, sind diese ” ” Inodes mit dem entsprechenden Treiber verknüpft; die Nummern können ebenfalls mit ls -l“ angezeigt werden: ” brw-rw---- 1 root disk ⌊ block special file 3, 1 Nov | ⌊ minor device number 8 1999 /dev/hda1 ⌊ major device number Für Gerätetreiber, die als Kernel-Modul ausgeführt sind, ist die Verknüpfung zwischen der major device number und dem Modul in einer der Dateien /etc/modules.conf“ oder /etc/modprobe.conf“ registriert. Beispiel: ” ” alias block-major-2 floppy Hier werden alle block special files, die eine major device number von 2 haben, mit dem Kernel-Modul floppy.o“ verknüpft. ” Aufgrund der wachsenden Zahl von Hot-Plug-and-Play“-Geräten1 wie USB” Festplatten und USB-Kameras musste dieses starre“ Device-Konzept von Linux ” in den letzten Jahren um ein dynamisches Gerätemanagement erweitert werden. Dies übernimmt udev, genauer gesagt der Dämon udevd. Siehe hierzu auch man udev“. Doch für die in diesem Kapitel vorgestellten Methoden und Beispiel” programme spielt es keine Rolle, ob das jeweilige Gerät fest im System verankert ist (wie ein internes CD-ROM-Laufwerk) oder dynamisch eingebunden wird. 6.1.1 Devices öffnen und schließen Der Vorteil, der sich durch die Integration der Devices in das Dateisystem ergibt, liegt auf der Hand: Man kann mit gängigen Dateioperationen auf HardwareKomponenten zugreifen. So ist es z. B. möglich, in einer Shell mit dem Kommando cat /dev/audio > test.au“ ohne ein spezielles Programm eine Audio-Datei ” aufzuzeichnen – vorausgesetzt, man hat eine Soundkarte installiert und unter Linux korrekt eingerichtet. In gleicher Weise lässt sich diese Datei wieder abspielen: cat test.au > /dev/audio“. Manche Gerätetreiber stellen ein zusätzli” ches Device zur Verfügung, das Informationen über die Hardware und den Treiber liefert, Beispiel: cat /dev/sndstat“. ” Für den Programmierer bedeutet dies, dass er mit Hilfe der üblichen Funktionen für den Dateizugriff auch die Devices ansprechen kann. Dies gilt natürlich auch für das Öffnen und Schließen eines Devices, das mit den Befehlen fopen() bzw. fclose() für gepufferte Ein-/Ausgabe möglich ist (vgl. Kapitel 4). Häufiger verwendet man für den Zugriff auf Devices jedoch die entsprechenden Befehle für ungepufferte Ein-/Ausgaben: int open(char *pathname, int flags); int close(int fd); 1 Geräte, die bei laufendem Betrieb angeschlossen werden können.
6.1 Das Device-Konzept von Linux 109 Die Funktion open() benötigt als ersten Parameter den zu öffnenden Datei- bzw. Device-Namen. Als zweiter Parameter kann eine der Konstanten O RDONLY (nur lesen), O WRONLY (nur schreiben), O RDWR (lesen und schreiben) angegeben werden. Als Rückgabewert liefert open() entweder den neuen Dateideskriptor, also eine positive, ganze Zahl, oder –1, falls die angegebene Datei nicht geöffnet werden konnte. Die Funktion close() schließt die Datei, deren Deskriptor als Parameter angegeben ist. Während sich Dateizugriffe durch die Verwendung von Schreib- und Lesepuffern deutlich beschleunigen lassen, bringt diese Pufferung bei Devices im Allgemeinen keine Vorteile. Ein Lesepuffer kann bei Devices sogar eher hinderlich sein – insbesondere bei Geräten wie Soundkarte oder WebCam, bei denen die Daten nur mit einer bestimmten Datenrate eintreffen“, würde das Füllen des Puffers mit ei” ner Wartezeit verbunden sein. Solche Devices werden in der Regel durch character special files repräsentiert – im Gegensatz zu den block special files, wie etwa für das Device einer Festplatte. 6.1.2 Ungepuffertes Lesen und Schreiben Im Gegensatz zur gepufferten Ein-/Ausgabe, für die es zahlreiche Schreib/Lesefunktionen unterschiedlicher Komplexität gibt, stehen für die ungepufferte Ein- und Ausgabe nur je zwei Funktionen zur Verfügung: ssize_t read(int fd, void *buffer, size_t count); ssize_t pread(int fd, void *buffer, size_t count, off_t offset); ssize_t write(int fd, void *buffer, size_t count); ssize_t pwrite(int fd, void *buffer, size_t count, off_t offset); Die Funktionen read() und pread() lesen aus der Datei mit dem Deskriptor fd die Anzahl count Bytes und schreiben sie in den Speicher an die mit buffer angegebene Stelle. Dabei liest pread() nicht ab der aktuellen Position in der Datei, sondern ab der durch offset angegebenen Position. Als Rückgabewert liefern beide Funktionen die Anzahl der tatsächlich gelesenen Bytes bzw. –1 im Fehlerfall. Analog werden die Funktionen write() und pwrite() verwendet, um count Bytes aus dem Speicher ab Adresse buffer in die Datei mit dem Deskriptor fd zu schreiben. Zuvor setzt pwrite() die aktuelle Position in der Datei auf offset. Beide Funktionen geben die Anzahl der tatsächlich geschriebenen Bytes zurück oder –1, falls ein Fehler aufgetreten ist.
110 6 Devices – das Tor zur Hardware Insbesondere beim Lesen aus einem character special file mit read() oder pread() ist die Anzahl der tatsächlich gelesenen Zeichen häufig geringer als die mit count angegebene Zahl. Dies deutet nicht auf einen Fehler hin, sondern liegt lediglich daran, dass im Moment noch keine weiteren Zeichen verfügbar sind. Im Zusammenhang mit dem Lesen aus einem Device ist auch die Funktion select() interessant: int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); Mit dieser Funktion kann bis zu einer vorgegebenen, maximalen Zeit gewartet werden, bis mindestens ein Dateideskriptor bereit zum Lesen oder zum Schreiben ist. Ein Beispiel für die Verwendung dieser Funktion finden Sie in Abschnitt 6.5. 6.1.3 Devices steuern mit ioctl() Wenn auch die Einbindung der Devices in das Dateisystem den Lese- und Schreibzugriff auf die verschiedenen Hardware-Komponenten deutlich vereinfacht, kann über die Schreib-/Lesefunktionen dennoch nur ein kleiner Teil der Möglichkeiten der Devices genutzt werden. So kann man z. B. zwar eine Audiodatei von der Soundkarte aufzeichnen, aber zum Einstellen der Sampling-Rate ist ein erweiterter Zugriff auf das Device erforderlich. Dies ist Aufgabe der Funktion ioctl() (die Abkürzung steht für Input-Output-Control): int ioctl(int fd, int request, ...); Auch diese Funktion benötigt als ersten Parameter den Dateideskriptor fd des Devices. Der Parameter request gibt das auf dieses Device anzuwendende Kommando an, optional gefolgt von weiteren Parametern, die das Kommando benötigt. Die möglichen Kommandos hängen von dem jeweiligen Device ab, eine (unvollständige) Liste der Kommandos erhalten Sie mit man ioctl list Im Fehlerfall liefert die Funktion ioctl() eine –1, sonst ist der Rückgabewert 0.1 Hinweis: Wenn Sie die Funktion ioctl() auf ein mit fopen() geöffnetes Device anwenden wollen, können Sie den Dateideskriptor mit der Funktion fileno() erfragen: 1 Einige Kommandos nutzen den Rückgabewert und liefern deshalb auch andere Werte als 0 oder –1.
6.2 Das CD-ROM-Laufwerk 111 FILE *stream; ioctl(fileno(stream), ...); 6.2 Das CD-ROM-Laufwerk Das CD-ROM-Laufwerk lässt sich über das Device /dev/cdrom“ ansprechen. ” Dabei handelt es sich um einen symbolischen Link auf das tatsächliche Device, z.B. /dev/hdc“. Bevor Sie auf das CD-ROM-Laufwerk zugreifen können, ” müssen Sie sicherstellen, dass die Zugriffsrechte entsprechend gesetzt sind! Dazu können Sie das Device (nicht den Link!) entweder für alle Benutzer freigeben (z.B. chmod a+r /dev/hdc), oder – was auf Systemen mit mehreren Benutzern sinnvoller ist – Sie tragen Ihren Benutzernamen in der Datei /etc/group“ in ” die Gruppe disk“ ein. Wenn Sie sich als Benutzer root“ anmelden, haben Sie ” ” natürlich auch Zugriff auf das CD-ROM-Laufwerk, doch rate ich davon dringend ab. Die für das CD-ROM-Laufwerk zulässigen ioctl()-Kommandos sind in der Include-Datei /usr/include/linux/cdrom.h“ aufgelistet. ” 6.2.1 Die CD auswerfen“ ” Als erstes Anwendungsbeispiel für den Umgang mit dem CD-ROM-Laufwerk soll hier das Programm eject“ vorgestellt werden, das die eingelegte CD aus” ” wirft“: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* eject.c - CDROM-Laufwerk oeffnen */ # # # # # include include include include include <stdio.h> <unistd.h> <sys/ioctl.h> <fcntl.h> <linux/cdrom.h> int main() { int fd; if ((fd = open("/dev/cdrom", O_RDONLY | O_NONBLOCK)) == -1) { perror("eject: Can’t open /dev/cdrom"); return(1);
112 20 21 22 23 24 25 26 27 28 29 30 6 Devices – das Tor zur Hardware } if (ioctl(fd, CDROMEJECT) == -1) { perror("eject: ioctl() failed"); return(1); } close(fd); return(0); } Betrachten wir zunächst die Zeile 15, in der das Device geöffnet wird. Als Flags“ ” ist hier die Oder-Verknüpfung aus O RDONLY und O NONBLOCK angegeben. Letzteres bewirkt, dass die Funktion open() auch ausgeführt werden kann, wenn keine CD im Laufwerk liegt. Nach erfolgreichem Öffnen des Devices wird in Zeile 22 mit Hilfe der Funktion ioctl() das Kommando CDROMEJECT an das Device geschickt. Dieses Kommando benötigt keinen Parameter. Weitere Kommandos dieser Art sind CDROMSTART, CDROMSTOP, CDROMRESET und CDROMCLOSETRAY. Sollte das Beispielprogramm eject“ bei Ihnen nicht funktionieren, sondern ” einen Input/output error“ ausgeben, liegt das vermutlich daran, dass Ihnen ” der Hot-Plug-Dämon udevd (siehe oben) in die Quere kommt. Auf manchen Linux-Systemen wird eine CD nach dem Einlegen automatisch durch diesen Dämon in das Dateisystem eingebunden und das Auswerfen der CD gesperrt. Testen Sie das Programm daher zunächst ohne CD im Laufwerk. 6.2.2 Fähigkeiten des Laufwerks auslesen Nicht alle CD-ROM-Laufwerke bieten die Möglichkeit, das Laufwerk selbsttätig wieder zu schließen – z. B. bei Notebooks muss das Laufwerk meist von Hand“ ” wieder geschlossen werden. Auch andere Kommandos lassen sich nicht auf jeden Laufwerkstyp anwenden. Daher gibt es das Kommando CDROM GET CAPABILITY, mit dem Sie die Fähigkeiten des Devices abfragen können. Das folgende Programm cdromcap“ zeigt die Anwendung dieses Kommandos: ” 1 /* 2 cdromcap.c - Faehigkeiten des CDROM-Laufwerks / 3 * 4 5 # include <stdio.h> 6 # include <unistd.h> 7 # include <sys/ioctl.h> 8 # include <fcntl.h>
6.2 Das CD-ROM-Laufwerk 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 113 # include <linux/cdrom.h> int main() { int fd, caps; if ((fd = open("/dev/cdrom", O_RDONLY | O_NONBLOCK)) == -1) { perror("cdromcap: Can’t open /dev/cdrom"); return(1); } if ((caps = ioctl(fd, CDROM_GET_CAPABILITY)) == -1) { perror("cdromcap: ioctl() failed"); return(1); } printf("Drive is a CD-R: %s, CD-RW: %s, DVD: %s, " "DVD-R: %s.\n", (caps & CDC_CD_R)? "yes" : "no", (caps & CDC_CD_RW)? "yes" : "no", (caps & CDC_DVD)? "yes" : "no", (caps & CDC_DVD_R)? "yes" : "no"); printf("It can "select (caps & (caps & (caps & close tray: %s, lock: %s, " disc: %s.\n", CDC_CLOSE_TRAY)? "no" : "yes", CDC_LOCK)? "yes" : "no", CDC_SELECT_DISC)? "yes" : "no"); close(fd); return(0); } Der Rückgabewert der Funktion ioctl() in Zeile 22 enthält bei erfolgreicher Ausführung des Kommandos CDROM GET CAPABILITY die Fähigkeiten des Devices, wobei jede Fähigkeit durch ein bestimmtes Bit repräsentiert wird. Eine Liste der möglichen Features erhält man mit (Benutzereingaben sind schräg dargestellt): > grep CDC #define CDC #define CDC #define CDC /usr/include/linux/cdrom.h CLOSE TRAY 0x1 /* caddy systems can’t close */ OPEN TRAY 0x2 /* but can eject. */ LOCK 0x4 /* disable manual eject */
114 6 Devices – das Tor zur Hardware #define #define #define #define #define #define #define #define #define CDC CDC CDC CDC CDC CDC CDC CDC CDC SELECT SPEED SELECT DISC MULTI SESSION MCN MEDIA CHANGED PLAY AUDIO RESET IOCTLS DRIVE STATUS 0x8 0x10 0x20 0x40 0x80 0x100 0x200 0x400 0x800 /* /* /* /* /* /* /* /* /* #define CDC GENERIC PACKET 0x1000 /* #define #define #define #define #define CDC CDC CDC CDC CDC CD R CD RW DVD DVD R DVD RAM 0x2000 0x4000 0x8000 0x10000 0x20000 /* /* /* /* /* programmable speed */ select disc from juke-box */ read sessions>1 */ Medium Catalog Number */ media changed */ audio functions */ hard reset device */ driver has non-std ioctls */ driver implements drive status */ driver implements generic packets */ drive is a CD-R */ drive is a CD-RW */ drive is a DVD */ drive can write DVD-R */ drive can write DVD-RAM */ Achtung: Die Bedeutung von CDC CLOSE TRAY ist sozusagen invertiert“. Ist die” ses Bit gesetzt, lässt sich das Laufwerk nicht automatisch schließen. 6.2.3 Audio-CDs abspielen Eine Anwendung des CD-ROM-Laufwerks, die den direkten Device-Zugriff erfordert, ist das Abspielen von Audio-CDs. Dazu benötigen Sie entweder eine Soundkarte, die mit dem CD-Laufwerk verbunden ist, oder ein Laufwerk mit einem Kopfhörerausgang. Bei Verwendung der Soundkarte sollten Sie zudem ein Programm zum Einstellen der Audioquellen und der Lautstärke installiert haben, beispielsweise aumix“. ” Das Inhaltsverzeichnis einer Audio-CD Bevor wir jedoch zum Abspielen einer CD kommen, soll zunächst gezeigt werden, wie das Inhaltsverzeichnis“ einer Audio-CD gelesen wird und in welcher Weise ” die Positionen der einzelnen Stücke (Tracks) codiert sind. Dazu sei das folgende Programm betrachtet: 1 2 3 4 5 6 7 8 9 10 /* cdtoc.c - "Inhaltsverzeichnis" einer Audio-CD */ # # # # # # include include include include include include <stdio.h> <unistd.h> <errno.h> <sys/ioctl.h> <fcntl.h> <linux/cdrom.h>
6.2 Das CD-ROM-Laufwerk 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 115 int main() { int fd, i; struct cdrom_tochdr toc_hdr; struct cdrom_tocentry toc_entry; if ((fd = open("/dev/cdrom", O_RDONLY)) == -1) { if (errno == ENOMEDIUM) fprintf(stderr, "cdtoc: No CD in drive.\n"); else perror("cdtoc: Can’t open /dev/cdrom"); return(1); } if (ioctl(fd, CDROMREADTOCHDR, &toc_hdr) == -1) { perror("cdtoc: Can’t get header"); return(1); } printf("First track: %d, last track: %d\n", toc_hdr.cdth_trk0, toc_hdr.cdth_trk1); for (i=toc_hdr.cdth_trk0; i<=toc_hdr.cdth_trk1; i++) { toc_entry.cdte_track = i; toc_entry.cdte_format = CDROM_MSF; if (ioctl(fd, CDROMREADTOCENTRY, &toc_entry) == -1) { perror("cdtoc: Can’t get table of contents"); return(1); } printf(" %2d) %02d:%02d.%02d\n", i, toc_entry.cdte_addr.msf.minute, toc_entry.cdte_addr.msf.second, (toc_entry.cdte_addr.msf.frame*100+37)/75); } close(fd); return(0); } In den Zeilen 18 bis 25 wird das Device geöffnet, wobei hier im Gegensatz zu den beiden vorangegangenen Abschnitten bewusst auf die Option O NONBLOCK
116 6 Devices – das Tor zur Hardware verzichtet wurde. Dadurch führt der Aufruf der Funktion open() zum Fehler ENOMEDIUM, wenn keine CD eingelegt ist. Als Nächstes wird in den Zeilen 27 bis 33 der Kopf ( HDR“ für Header) des Inhalts” verzeichnisses ( TOC“ für Table of Contents) gelesen. Dies wird durch das ioctl()” Kommando CDROMREADTOCHDR erreicht, das als dritten Parameter des ioctl()Aufrufs einen Zeiger auf eine Variable vom Typ struct cdrom tochdr erwartet. Diese Struktur wird in /usr/include/linux/cdrom.h“ definiert und enthält ” zwei Einträge: struct cdrom_tochdr { __u8 cdth_trk0; __u8 cdth_trk1; }; /* start track */ /* end track */ Durch den ioctl()-Aufruf trägt man die Nummer des ersten Stücks (in der Regel ist das 1) und die Nummer des letzten Stücks auf der CD in diese Struktur ein. In den Zeilen 35 bis 48 werden dann in der for()-Schleife Informationen zu allen Stücken eingeholt. Dazu dient das ioctl()-Kommando CDROMREADTOCENTRY, das als dritten Parameter des ioctl()-Aufrufs einen Zeiger auf eine Variable vom Typ struct cdrom tocentry erwartet. Diese Struktur muss vor dem ioctl()-Aufruf mit einer gültigen Nummer für das Stück und der Angabe des Formates für die Position initialisiert werden (Zeile 37 und 38). Als Format stehen CDROM MSF und CDROM LBA zur Verfügung. Ersteres steht für Minute-SecondFrame, Letzteres für Logical Block Address. Während die Adresse im LBA-Format durch eine Integerzahl repräsentiert wird, bildet die Struktur cdrom msf0 das MSF-Format ab: struct { __u8 __u8 __u8 }; cdrom_msf0 minute; second; frame; Die Einträge minute und second sind selbsterklärend, der Eintrag frame gibt den Bruchteil einer Sekunde an, wobei eine Sekunde aus CD FRAMES (= 75) Frames besteht. Der Zusammenhang zwischen LBA- und MSF-Adresse lässt sich beschreiben durch LBA = (minute · 60 + second − 2) · CD FRAMES + frame Nach dem ioctl()-Aufruf in Zeile 39 des Programms enthält die Variable toc entry unter anderem die Position des angegebenen Stücks mit der Num-
6.2 Das CD-ROM-Laufwerk 117 mer i im MSF-Format. Diese wird in der printf()-Anweisung in den Zeilen 44 bis 47 ausgegeben, wobei die Frames in 1 /100 Sekunden umgerechnet werden. Wiedergabe einer Audio-CD Bevor Sie eine Audio-CD wiedergeben, stellen Sie bitte sicher, dass das CD-ROMLaufwerk als Audioquelle aktiv ist. Dies ist z. B. mit dem Shell-Programm au” mix“ möglich: aumix -c 100 Alternativ bieten manche CD-Laufwerke einen Kopfhörerausgang mit separatem Lautstärkeregler. Das Abspielen einer Audio-CD erfolgt mit dem Kommando CDROMPLAYMSF, das als Parameter einen Zeiger auf eine Struktur vom Typ cdrom msf erwartet: struct { __u8 __u8 __u8 __u8 __u8 __u8 }; cdrom_msf cdmsf_min0; cdmsf_sec0; cdmsf_frame0; cdmsf_min1; cdmsf_sec1; cdmsf_frame1; /* /* /* /* /* /* start minute */ start second */ start frame */ end minute */ end second */ end frame */ Hier muss im MSF-Format Anfangs- und Endposition des abzuspielenden Teils der CD angegeben werden. Die Anfangsposition eines Stücks kann wie im vorangegangenen Beispielprogramm mit Hilfe des Kommandos CDROMREADTOCENTRY erfolgen. Als Ende eines Stücks kann die Anfangsposition des darauf folgenden Stücks angegeben werden. Die Endposition des letzten Stückes lässt sich über die Position von CDROM LEADOUT ermitteln. Das folgende Programm spielt eine Audio-CD vollständig ab: 1 2 3 4 5 6 7 8 9 10 11 /* cdplay.c - Audio-CD abspielen */ # # # # # # include include include include include include <stdio.h> <unistd.h> <stdlib.h> <sys/ioctl.h> <fcntl.h> <linux/cdrom.h>
118 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 6 Devices – das Tor zur Hardware void err_exit(char *err_text, int return_code) { perror(err_text); exit(return_code); } int main() { int fd; struct cdrom_tocentry toc_entry; struct cdrom_msf start_stop; if ((fd = open("/dev/cdrom", O_RDONLY)) == -1) err_exit("cdplay: Can’t open /dev/cdrom", 1); /* Anfang des 1. Stuecks */ toc_entry.cdte_track = 1; toc_entry.cdte_format = CDROM_MSF; if (ioctl(fd, CDROMREADTOCENTRY, &toc_entry) == -1) err_exit("cdplay: ioctl() failed", 1); start_stop.cdmsf_min0 = toc_entry.cdte_addr.msf.minute; start_stop.cdmsf_sec0 = toc_entry.cdte_addr.msf.second; start_stop.cdmsf_frame0 = toc_entry.cdte_addr.msf.frame; /* Ende des letzten Stuecks */ toc_entry.cdte_track = CDROM_LEADOUT; toc_entry.cdte_format = CDROM_MSF; if (ioctl(fd, CDROMREADTOCENTRY, &toc_entry) == -1) err_exit("cdplay: ioctl() failed", 1); start_stop.cdmsf_min1 = toc_entry.cdte_addr.msf.minute; start_stop.cdmsf_sec1 = toc_entry.cdte_addr.msf.second; start_stop.cdmsf_frame1 = toc_entry.cdte_addr.msf.frame; if (ioctl(fd, CDROMPLAYMSF, &start_stop) == -1) err_exit("cdplay: ioctl() failed", 1); printf("Press <RETURN> to stop playing.\n"); getchar();
6.2 Das CD-ROM-Laufwerk 56 57 58 59 60 61 62 119 if (ioctl(fd, CDROMSTOP) == -1) err_exit("cdplay: ioctl() failed", 1); close(fd); return(0); } Nach dem Öffnen des Devices in Zeile 24 fragt das Programm in den Zeilen 28 bis 37 die MSF-Adresse des ersten Stücks ab und trägt sie in die Start-Position der Variablen start stop ein. Analog wird in den Zeilen 40 bis 49 die Endadresse der CD erfragt und als Stop-Position in die Variable start stop eingetragen. Danach erfolgt in Zeile 51 der ioctl()-Aufruf mit dem Kommando CDROMPLAYMSF, der das Abspielen der CD bewirkt. Mit dem Kommando CDROMSTOP (Zeile 57) lässt sich das Abspielen vorzeitig beenden. Weitere Möglichkeiten Ein echter“ CD-Player bietet neben den Funktionen play“ und stop“ eine pau” ” ” ” se“-Funktion für eine (kurze) Unterbrechung. Auch das Linux-Device bietet diese Möglichkeit mit dem ioctl()-Kommando CDROMPAUSE, das keinen weiteren Parameter benötigt. Das Abspielen lässt sich danach mit CDROMRESUME an der gleichen Stelle fortsetzen. Nachdem es das Abspielen der CD gestartet hat, wartet unser Programm cdplay auf eine Benutzereingabe. Es bemerkt“ nicht, ob die CD vielleicht schon zu Ende ” ist. Um den aktuellen Status des CD-Laufwerks abzufragen, dient das ioctl()Kommando CDROMSUBCHNL. Als Parameter wird bei diesem Kommando ein Zeiger auf eine Variable vom Typ struct cdrom subchnl erwartet. Vor dem ioctl()-Aufruf muss das Element cdsc format dieser Struktur mit CDROM MSF oder CDROM LBA initialisiert werden. Nach erfolgreicher Ausführung des Kommandos CDROMSUBCHNL enthält die Struktur unter anderem Informationen über den Audio-Status, die Nummer des (laufenden) Stücks und die Position im vorgegebenen Format. Das folgende Programm demonstriert das Abfragen und Ausgeben dieser Informationen: 1 2 3 4 5 6 7 8 9 10 /* cdstat.c - Status des CD-Laufwerks abfragen */ # # # # # include include include include include <stdio.h> <unistd.h> <sys/ioctl.h> <fcntl.h> <linux/cdrom.h>
120 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 6 Devices – das Tor zur Hardware int main() { int fd; char *status_string; struct cdrom_subchnl subch; if ((fd = open("/dev/cdrom", O_RDONLY | O_NONBLOCK)) == -1) { perror("cdstat: Can’t open /dev/cdrom"); return(1); } subch.cdsc_format = CDROM_MSF; if (ioctl(fd, CDROMSUBCHNL, &subch) == -1) { perror("cdstat: ioctl() failed"); return(1); } switch(subch.cdsc_audiostatus) { case CDROM_AUDIO_PLAY: status_string = "playing"; break; case CDROM_AUDIO_PAUSED: status_string = "paused"; break; case CDROM_AUDIO_COMPLETED: status_string = "completed"; break; case CDROM_AUDIO_ERROR: status_string = "error"; break; default: status_string = "---"; } printf("CD status:\t\t%s\n", status_string); printf("current track:\t\t%d\n", subch.cdsc_trk); printf("current position:\t%02d:%02d\n", subch.cdsc_absaddr.msf.minute, subch.cdsc_absaddr.msf.second); close(fd); return(0); } Einige CD-ROM-Laufwerke bieten die Möglichkeit, bei Wiedergabe einer AudioCD die Lautstärke per Software einzustellen. Dazu bietet das Device zwei ioctl()-Kommandos: CDROMVOLREAD und CDROMVOLCTRL, Ersteres zum Auslesen der Einstellungen und Letzteres zur Modifizierung der Einstellungen. Beide
6.3 Ansteuerung einer Soundkarte 121 Kommandos benötigen als dritten Parameter des ioctl()-Aufrufs den Zeiger auf eine Variable vom Typ struct cdrom volctrl: struct { __u8 __u8 __u8 __u8 }; cdrom_volctrl channel0; channel1; channel2; channel3; /* left channel */ /* right channel */ Nach erfolgreicher Ausführung von CDROMVOLREAD enthält diese Struktur die aktuell eingestellten Werte für alle vier (!) Kanäle – Kanal 2 und Kanal 3 sind in der Regel nicht belegt und deren Lautstärke ist dementsprechend 0. Mit dem Kommando CDROMVOLCTRL werden die in der Struktur eingetragenen LautstärkeWerte (zwischen 0 und 255) für die entsprechenden Kanäle eingestellt. Weitere Informationen zum Ansteuern eines CD-ROM-Laufwerks unter Linux finden Sie in [8]. 6.3 Ansteuerung einer Soundkarte Eine Soundkarte bietet vielfältige Möglichkeiten: Neben dem Analog-Digital- und Digital-Analog-Wandler zur Aufnahme und Wiedergabe von Audio-Signalen enthalten die meisten Karten einen Mixer, also eine Art Mischpult, einen Synthesizer sowie einen Chip (UART) zum Senden und Empfangen von MIDI-Daten. Die verschiedenen Funktionseinheiten einer Soundkarte sind unter Linux auf mehrere Devices abgebildet: Device Beschreibung /dev/dsp /dev/audio /dev/mixer Schnittstelle zum A/D- und D/A-Wandler Schnittstelle zum A/D- und D/A-Wandler (8 Bit, log.) elektronisches Mischpult“ ” Je nach Soundkarte und verwendetem Treiber (Kernel-Modul) stehen weitere Devices zur Verfügung – beispielsweise /dev/sequencer, /dev/snd/pcm* oder /dev/sndstat. Die folgenden Abschnitte beschränken sich jedoch auf die ge” bräuchlichsten“ Devices der Soundkarte. Bei Verwendung mehrerer Soundkarten, beispielsweise wenn zusätzlich eine TVKarte oder eine WebCam mit eingebautem Mikrofon angeschlossen ist, werden für jede Karte eigene Device-Dateien angelegt, die mit einem fortlaufenden Index versehen sind. Beispiel: /dev/dsp0, /dev/dsp1, usw., wobei /dev/dsp in der Regel ein Link auf /dev/dsp0 ist. Das Gleiche gilt analog für die anderen DeviceDateien wie /dev/mixer.
122 6 Devices – das Tor zur Hardware 6.3.1 OSS, ALSA und ESOUND Die Devices /dev/dsp und /dev/mixer entsprechen dem ursprünglichen Mechanismus für die Ansteuerung einer Soundkarte unter Linux, dem so genannten Open Sound System (OSS). Ein Nachteil des OSS besteht darin, dass nicht mehrere Programme parallel auf die Soundkarte zugreifen können. Dies ist beispielsweise erforderlich, wenn während der Wiedergabe von MP3-Musik das Betriebssystem Signaltöne ausgibt. Um diese Einschränkung aufzuheben, wurden inzwischen neue, leistungsfähigere Systeme wie ALSA (Advanced Linux Sound Architecture) und ESounD (Enlightened Sound Daemon) eingeführt. Aus Kompatibilitätsgründen stellen die aktuellen Soundtreiber aber nach wie vor die Devices des OSS zur Verfügung: die einfachste Möglichkeit für Programmierer, auf die Soundkarte zuzugreifen. 6.3.2 Der Mixer Der Mixer einer Soundkarte stellt ein elektronisches Mischpult dar. Er hat die Aufgabe, die verschiedenen Audioquellen wie CD-Laufwerk oder Mikrofon für die Aufnahme und Wiedergabe zu mischen. Für jeden Kanal des Mixers kann individuell eine Lautstärke eingestellt werden. Über den Mixer wird auch die Aufnahmequelle ausgewählt. Der Mixer lässt sich über das Device /dev/mixer ansteuern, die wichtigsten Definitionen für dieses Device enthält die Datei /usr/include/linux/soundcard.h Jedem möglichen Kanal des Mixers ist eine Indexnummer zugeordnet, die Konstante SOUND MIXER NRDEVICES gibt die Anzahl der möglichen Kanäle an (gegenwärtig sind dies 25). Hier eine Liste der Mixer-Kanäle mit den zugehörigen Index-Nummern und einer Beschreibung: SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER VOLUME BASS TREBLE SYNTH PCM SPEAKER LINE MIC CD IMIX ALTPCM RECLEV IGAIN OGAIN LINE1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Lautstärke des Mixerausgangs Tiefen“ des Mixerausgangs ” Höhen“ des Mixerausgangs ” Synthesizer (Ton-Generator) Digital-Analog-Wandler PC-Lautsprecher Line-In“-Buchse ” Mikrofon CD-ROM-Laufwerk Aufnahmepegel Eingangsverstärkung Ausgangsverstärkung weitere Analogeingänge
6.3 Ansteuerung einer Soundkarte SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND SOUND MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER MIXER LINE2 LINE3 DIGITAL1 DIGITAL2 DIGITAL3 PHONEIN PHONEOUT VIDEO RADIO MONITOR 123 15 16 17 18 19 20 21 22 23 24 Digitaleingänge z. B. von einer TV-Karte Die aktuelle Einstellung eines dieser Kanäle kann mit dem ioctl()-Kommando MIXER READ() abgefragt und mit MIXER WRITE() verändert werden. Beide Kommandos benötigen als dritten Parameter des ioctl()-Aufrufs den Zeiger auf eine Integer-Variable. Das unterste Byte dieser Variablen enthält die Lautstärke für den linken Stereo-Kanal, das nächsthöhere Byte repräsentiert die Lautstärke für den rechten Stereo-Kanal. Beide Werte müssen sich von 0 bis 100 bewegen. Das folgende Programm fragt die aktuelle Einstellung der Gesamtlautstärke ab und stellt sie anschließend auf 50 ein: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* set_volume.c - Gesamtlautstaerke lesen u. schreiben */ # # # # # include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/soundcard.h> int main() { int fd, level; if ((fd = open("/dev/mixer", O_RDONLY)) == -1) { perror("set_volume: Can’t open device"); return(1); } if (ioctl(fd, MIXER_READ(SOUND_MIXER_VOLUME), &level) == -1) perror("set_volume: Can’t read master volume"); else printf("master volume: L=%d, R=%d\n", level & 255,
124 26 27 28 29 30 31 32 33 34 35 6 Devices – das Tor zur Hardware level >> 8); level = 50 + (50 << 8); if (ioctl(fd, MIXER_WRITE(SOUND_MIXER_VOLUME), &level) == -1) perror("set_volume: Can’t set master volume"); close(fd); return(0); } Informationen über den Mixer holen In der Regel unterstützt eine Soundkarte nur einen Teil der 25 möglichen MixerKanäle. Deshalb bietet das Device auch Kommandos, mit denen ein Programm erfragen kann, welche Kanäle der Mixer tatsächlich besitzt: ioctl()-Kommando Bedeutung MIXER READ(SOUND MIXER DEVMASK) Liste der unterstützten Kanäle MIXER READ(SOUND MIXER RECMASK) Liste der möglichen Aufnahmequellen MIXER READ(SOUND MIXER STEREODEVS) Liste der Kanäle, die Stereo unterstützen Alle drei Funktionen erwarten als dritten Parameter des ioctl()-Aufrufs den Zeiger auf eine Integer-Variable. In diese Variable schreiben die Kommandos dann eine Zahl, bei der jedes Bit für einen möglichen Kanal steht. Ist es gesetzt, wird dieser Kanal unterstützt, anderenfalls ist er bei dieser Soundkarte nicht verfügbar. Die Bitnummer entspricht dabei der Index-Nummer (s.o.) des Kanals. Das folgende Programm fragt mit MIXER READ(SOUND MIXER RECMASK) die möglichen Aufnahmequellen des Mixers ab: 1 2 3 4 5 6 7 8 9 10 11 12 13 /* recsources.c - moegliche Aufnahmequellen des Mixers */ # # # # # include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/soundcard.h> const char *device_names[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_NAMES;
6.3 Ansteuerung einer Soundkarte 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 125 int main() { int fd, mask, i; if ((fd = open("/dev/mixer", O_RDONLY)) == -1) { perror("recsources: Can’t open device"); return(1); } if (ioctl(fd, MIXER_READ(SOUND_MIXER_RECMASK), &mask) == -1) perror("recsources: ioctl() failed"); else { printf("Supported recording sources:"); for (i=0; i<SOUND_MIXER_NRDEVICES; i++) if (mask & (1<<i)) printf(" %s,", device_names[i]); printf("\b.\n"); } close(fd); return(0); } Bitte beachten Sie die Zeilen 11 und 12 des Programms. Hier wird die Konstante device names als ein Feld aus Zeichenketten definiert und mit Hilfe des Makros SOUND DEVICE NAMES initialisiert. Dadurch zeigt das Element device_names[i] auf eine Zeichenkette, die den Namen des Mixer-Kanals mit der Index-Nummer i enthält – so zeigt z. B. device_names[6] auf line“. Nach dem Einlesen der ” Bit-Maske in die Variable mask in Zeile 24 lässt sich so zu jedem gesetzten Bit der Name des Audiokanals ausgeben (Zeile 30 bis 33). Wahl der Aufnahmequelle Das ioctl()-Kommando MIXER READ(SOUND MIXER RECMASK) liefert die Liste der möglichen Aufnahmequellen. Die tatsächlich eingestellte Aufnahmequelle lässt sich über das ioctl()-Kommando MIXER READ(SOUND MIXER RECSRC)“ ” abfragen und mit dem Kommando MIXER WRITE(SOUND MIXER RECSRC)“ ver” ändern.1 Beispiel: 1 Da bei diesem Kommando eine Bit-Maske angegeben wird, können prinzipiell auch mehrere Aufnahmequellen gleichzeitig eingestellt werden. Das wird jedoch nicht von allen Soundkarten unterstützt.
126 6 Devices – das Tor zur Hardware int fd, mask; mask = SOUND_MASK_CD; /* = 1<<SOUND_MIXER_CD */ if (ioctl(fd, MIXER_WRITE(SOUND_MIXER_RECSRC), &mask) == -1) perror("Can’t set recording source"); 6.3.3 Audiodaten aufnehmen und wiedergeben Für die Aufnahme und Wiedergabe von Audiosignalen mit Hilfe des A/D- und des D/A-Wandlers der Soundkarte sind die Device-Dateien /dev/dsp“ und ” /dev/audio“ vorgesehen. Beide Devices bieten die gleichen Möglichkeiten, un” terscheiden sich aber in dem voreingestellten Aufnahme-(Sampling-)Format. Unterstützte Sampling-Formate abfragen Soundkarten unterstützen in der Regel eine ganze Reihe verschiedener Formate für die Aufnahme und Wiedergabe von Audiosignalen, angefangen von 8-Bit mit oder ohne Vorzeichen über 16-Bit (CD-Qualität) bis hin zu MPEG-2-Audio. Nicht jede Soundkarte ist in der Lage, alle diese Formate zu verarbeiten. Aus diesem Grund gibt es das ioctl()-Kommando SNDCTL DSP GETFMTS, das die Abfrage der unterstützten Formate erlaubt. Dieses Kommando liefert eine Bit-Maske, bei der jedes Bit einem bestimmten Format zugeordnet ist. Die derzeit vorgesehenen Formate sind in Tabelle 6.1 aufgelistet. Bei den Formaten µ -law“ und A-law“ handelt es sich um Verfahren aus der ” ” digitalen Telefonübertragung, mit deren Hilfe 13 Bit Audiodaten ohne Dynamikverlust auf 8 Bit reduziert werden können. Das ADPCM-Verfahren (Adaptive Differential Pulse Code Modulation) ermöglicht die Komprimierung von 64 kBit/s Audiosignalen auf 32 kBit/s. Das folgende Programm fragt die möglichen Audioformate der Soundkarte ab: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* get_formats.c - moegliche Samplingformate abfragen */ # # # # # include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/soundcard.h> int main(int argc, char *argv[]) { int fd, mask; char *dev_name = "/dev/dsp";
6.3 Ansteuerung einer Soundkarte 127 Tabelle 6.1: Mögliche Audioformate und deren Beschreibung Format 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Bit AFMT MU LAW 0 AFMT A LAW 1 Beschreibung 8 Bit, komprimiert nach dem µ -law“-Verfahren ” 8 Bit, komprimiert nach dem A-law“-Verfahren ” codiert nach dem ADPCM“-Verfahren ” 8 Bit ohne Vorzeichen (0 bis 255) AFMT IMA ADPCM 2 AFMT U8 3 AFMT S16 LE 4 16 Bit mit Vorz. (− 32768 bis 32767), low-Byte zuerst AFMT S16 BE 5 16 Bit mit Vorz. (− 32768 bis 32767), high-Byte zuerst AFMT S8 6 8 Bit mit Vorz. (–128 bis 127) AFMT U16 LE 7 16 Bit ohne Vorz. (–32768 bis 32767), low-Byte zuerst AFMT U16 BE 8 16 Bit ohne Vorz. (–32768 bis 32767), high-Byte zuerst AFMT MPEG 9 MPEG-2-Audio if (argc > 1) dev_name = argv[1]; if ((fd = open(dev_name, O_RDONLY)) == -1) { perror("get_formats: Can’t open device"); return(1); } if (ioctl(fd, SNDCTL_DSP_GETFMTS, &mask) == -1) perror("get_formats: Can’t get supported formats"); else { printf("Device ’%s’ supports:\n", dev_name); if (mask & AFMT_MU_LAW) printf(" mu-law encoding\n"); if (mask & AFMT_A_LAW) printf(" a-law encoding\n"); if (mask & AFMT_IMA_ADPCM) printf(" ADPCM compression\n"); if (mask & AFMT_U8)
128 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 6 Devices – das Tor zur Hardware printf(" if (mask & printf(" if (mask & printf(" if (mask & printf(" if (mask & printf(" if (mask & printf(" if (mask & printf(" unsigned 8 bit\n"); AFMT_S8) signed 8 bit\n"); AFMT_S16_LE) signed 16 bit (little endian)\n"); AFMT_S16_BE) signed 16 bit (big endian)\n"); AFMT_U16_LE) unsigned 16 bit (little endian)\n"); AFMT_U16_BE) unsigned 16 bit (big endian)\n"); AFMT_MPEG) MPEG-2-Audio encoding\n"); } close(fd); return(0); } Aufnahme und Wiedergabe Um Audiosignale aufzuzeichnen und wiederzugeben, sollten zunächst Audioformat und Samplingrate (Abtastfrequenz) eingestellt werden. Des Weiteren sollte festgelegt werden, ob die Aufnahme/Wiedergabe in Stereo oder Mono erfolgen soll. Für jede dieser Einstellungen gibt es jeweils ein ioctl()-Kommando: SNDCTL DSP SETFMT – Audioformat einstellen SNDCTL DSP STEREO – Stereo (1) oder Mono (0) SNDCTL DSP SPEED – Abtastfrequenz (Samples pro Sekunde) Vor einer Aufnahme muss ferner mit Hilfe des Mixers die gewünschte Audioquelle gewählt werden (siehe Seite 125). Bei Stereo-Aufnahmen liefert das Device die Samples beider Kanäle im Wechsel, angefangen mit dem linken Kanal. Das folgende Programm zeichnet eine Audio-Sequenz mit einer Länge von 100 000 Bytes auf und spielt diese anschließend wieder ab (8 Bit, 22050 Hz, mono): 1 2 3 4 5 6 7 8 9 /* rec_play.c - Audio-Signal aufnehmen u. wiedergeben */ # # # # # include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/soundcard.h>
6.3 Ansteuerung einer Soundkarte 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 # define NUM_SAMPLES 100000 unsigned char buffer[NUM_SAMPLES]; int main() { int fd, i, format=AFMT_U8; long length; char input[16]; if ((fd = open("/dev/dsp", O_RDWR)) == -1) { perror("rec_play: Can’t open device"); return(1); } if (ioctl(fd, SNDCTL_DSP_SETFMT, &format) == -1) perror("rec_play: Can’t set format"); i = 0; if (ioctl(fd, SNDCTL_DSP_STEREO, &i) == -1) perror("rec_play: Can’t set to mono"); i = 22050; if (ioctl(fd, SNDCTL_DSP_SPEED, &i) == -1) perror("rec_play: Can’t set sampling rate"); printf("Press <RETURN> to start recording. "); fgets(input, 16, stdin); if ((length = read(fd, buffer, NUM_SAMPLES)) == -1) { perror("rec_play: Can’t record audio data"); return(1); } printf("done (%ld bytes).\n" "Press <RETURN> to start playing. ", length); fgets(input, 16, stdin); if (write(fd, buffer, length) == -1) perror("rec_play: Can’t play audio data"); 129
130 54 55 56 6 Devices – das Tor zur Hardware close(fd); return(0); } Nach dem Öffnen des Devices – hier mit O RDWR, um schreiben und lesen zu können – und der Einstellung der Parameter wartet das Programm auf die RETURN-Taste und beginnt danach die Aufzeichnung. Anschließend wartet das Programm erneut auf das Drücken der Taste RETURN, bevor es die aufgezeichneten Daten wieder abspielt. Die gängigsten Samplingraten für Audiosignale, die von den meisten Soundkarten unterstützt werden, sind 44100 Hz, 22050 Hz, 11025 Hz und 8000 Hz. Audiodaten werden in der Regel nicht als reine Daten“ gespeichert, sondern zu” sammen mit Informationen über das Format und die Samplingrate. Häufig wird hier das WAV-Format verwendet, unter Linux/Unix gelegentlich auch das AUFormat. Beide Formate sind im Anhang beschrieben. 6.4 Video for Linux“ ” Inzwischen ist die Multimedia-Welle auch auf Linux übergeschwappt. Es gibt mittlerweile Kernel-Module, die Video-Quellen wie z. B. eine TV-Karte oder eine WebCam unterstützen. Diese Treiber sind unter dem Begriff Video for Linux“ ” – oder kurz Video4Linux“ – zusammengefasst. In diesem Abschnitt sollen das ” zugehörige Device vorgestellt und die Aufnahme von Bildern mit diesem Device erläutert werden. Die Beispielprogramme sind ausgelegt für eine WebCam, lassen sich aber auch auf eine TV-Karte anwenden. Von Haus aus unterstützen die meisten Linux-Distributionen leider nur sehr wenige USB-Kameras. Es gibt aber ein Open-Source-Projekt zu dem Kernel-Modul spca5xx, das mittlerweile mehr als 160 WebCam-Typen unterstützt. Dieses Modul ist als Quelltext für 2.4er und 2.6er Kernels verfügbar, weitere Informationen finden Sie unter http://mxhaard.free.fr/spca5xx.html. 6.4.1 Eigenschaften des Devices Video-Hardware wie WebCams und TV-Karten lassen sich über den Dateipfad /dev/video“ ansprechen. Dabei handelt es sich um einen symbolischen Link ” auf das Device, im Allgemeinen ist das /dev/video0“. Ist mehr als eine Vi” deoquelle – z. B. TV-Karte und WebCam – vorhanden, gibt es entsprechend viele Device-Dateien /dev/video0, /dev/video1 usw. Alle für die Programmierung relevanten Definitionen und Deklarationen finden Sie in der Include-Datei /usr/include/linux/videodev.h“. ” Die Bandbreite der unterstützten Videoquellen des Devices ist groß, dementsprechend unterschiedlich sind auch Funktionen und Möglichkeiten. Daher bietet das Device das ioctl()-Kommando VIDIOCGCAP (VIDeo IO-Control Get CAPabilities)
6.4 Video for Linux“ ” 131 an, mit dem die Fähigkeiten und Eigenschaften abgefragt werden können. Dieses Kommando erwartet als dritten Parameter des ioctl()-Aufrufs den Zeiger auf eine Variable vom Typ struct video capability: struct video_capability { char name[32]; /* Produktbezeichnung */ int type; /* Bit-Maske aus Eigenschaften */ int channels; /* Anzahl der Kanaele */ int audios; /* Anzahl der Audio-Devices */ int maxwidth; /* maximale Bildbreite */ int maxheight; /* maximale Bildhoehe */ int minwidth; /* minimale Bildbreite */ int minheight; /* minimale Bildhoehe */ }; Als type“ erhält man eine logische Oder-Verknüpfung aus den Eigenschaf” ten des angeschlossenen Gerätes – z. B. VID TYPE MONOCHROME, wenn nur Schwarz/Weiß unterstützt wird, oder VID TYPE TUNER, falls das Gerät einen Empfänger besitzt, dessen Frequenz via Software eingestellt werden kann. Die vollständige Liste der möglichen Eigenschaften (mit Beschreibung) erhält man mit grep VID_TYPE_ /usr/include/linux/videodev.h Neben den möglichen Einstellungen für das Bildformat lassen sich auch die aktuell gewählten Einstellungen abfragen. Dazu dienen unter anderem die Kommandos VIDIOCGWIN (VIDeo IO-Control Get WINdow) und VIDIOCGPICT (VIDeo IOControl Get PICTure). VIDIOCGWIN erwartet als Parameter den Zeiger auf eine Variable vom Typ struct video window: struct video_window { __u32 x,y; /* Position des Bildausschnitts */ __u32 width,height; /* und Groesse */ __u32 chromakey; __u32 flags; struct video_clip *clips; /* nur schreiben */ int clipcount; }; Das Kommando VIDIOCGPICT erwartet als Parameter den Zeiger auf eine Variable vom Typ struct video picture:
132 6 Devices – das Tor zur Hardware struct video_picture { __u16 brightness; __u16 hue; __u16 colour; __u16 contrast; __u16 whiteness; __u16 depth; __u16 palette; }; /* /* /* /* /* /* /* /* Helligkeit */ Farbwert */ Farbsaettigung */ Kontrast */ nur bei schwarz/weiss */ Bits pro Pixel */ Palette z.B. */ VIDEO_PALETTE_RGB24 */ Das folgende Programm nutzt die o. g. ioctl()-Kommandos VIDIOCGCAP, VIDIOCGWIN und VIDIOCGPICT, um Informationen über das angeschlossene Gerät auszugeben: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* videoinfo.c - Informationen ueber /dev/video holen */ # # # # # include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/videodev.h> int main() { int fd; struct video_capability video_cap; struct video_window video_win; struct video_picture video_pict; if ((fd = open("/dev/video", O_RDONLY)) == -1) { perror("videoinfo: Can’t open device"); return(1); } if (ioctl(fd, VIDIOCGCAP, &video_cap) == -1) perror("videoinfo: Can’t get capabilities"); else { printf("Name:\t\t’%s’\n", video_cap.name); printf("Minimum size:\t%d x %d\n", video_cap.minwidth, video_cap.minheight);
6.4 Video for Linux“ ” 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 133 printf("Maximum size:\t%d x %d\n", video_cap.maxwidth, video_cap.maxheight); } if (ioctl(fd, VIDIOCGWIN, &video_win) == -1) perror("videoinfo: Can’t get window information"); else printf("Current size:\t%d x %d\n", video_win.width, video_win.height); if (ioctl(fd, VIDIOCGPICT, &video_pict) == -1) perror("videoinfo: Can’t get picture information"); else printf("Current depth:\t%d\n", video_pict.depth); close(fd); return(0); } Für eine Philips USB-WebCam sieht das z. B. so aus: > gcc videoinfo.c -o videoinfo > videoinfo Name: ’Philips 680 webcam’ Minimum size: 128 x 96 Maximum size: 640 x 480 Current size: 352 x 288 Current depth: 24 6.4.2 Bilder aufzeichnen Eine sehr einfache Möglichkeit der Aufzeichnung eines Bildes von der Videoquelle besteht im Auslesen des Devices mit der Funktion read(). Das folgende Programm liest ein Bild mit der voreingestellten Größe ein und schreibt es als PPMDatei (Portable PixMap) nach stdout: 1 2 3 4 5 6 7 8 9 10 /* read_image.c - Bild von WebCam oder TV-Karte lesen */ # # # # # include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/videodev.h>
134 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 6 Devices – das Tor zur Hardware # define MAX_BYTES (640*480*3) /* Bildspeicher */ # define SWAP_RGB_BGR 0 /* 1 = Farbreihenfolge drehen */ int main() { int fd; long length; struct video_window video_win; static unsigned char image[MAX_BYTES]; if ((fd = open("/dev/video", O_RDONLY)) == -1) { perror("read_image: Can’t open device"); return(1); } if (ioctl(fd, VIDIOCGWIN, &video_win) == -1) { perror("read_image: Can’t get video window"); return(1); } length = video_win.width * video_win.height * 3; if ((length < 1) || (length > MAX_BYTES)) { fprintf(stderr, "read_image: Bad image size. " "Using default values.\n"); video_win.width = 320; video_win.height = 240; length = 320 * 240 * 3; if (ioctl(fd, VIDIOCSWIN, &video_win) == -1) { perror("read_image: Can’t set video window"); return(1); } } if (read(fd, image, length) == -1) { perror("read_image: Error while reading"); return(1); } close(fd);
6.4 Video for Linux“ ” 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 if (SWAP_RGB_BGR) { int i, tmp; 135 /* Rot und Blau tauschen? */ for (i=0; i<video_win.width*video_win.height*3; i+=3) { tmp = image[i]; image[i] = image[i+2]; image[i+2] = tmp; } } printf("P6\n%d %d\n255\n", video_win.width, video_win.height); fwrite(image, 3, video_win.width*video_win.height, stdout); return(0); } Zum Aufzeichnen und Darstellen eines Bildes mit diesem Beispielprogramm können Sie wie folgt vorgehen: > gcc read image.c -o read image > ./read image > aufnahme.ppm > xview aufnahme.ppm Alternativ kann die Aufnahme auch mit den Tools gimp“, xv“ oder display“ ” ” ” (aus dem Programm-Paket ImageMagick) dargestellt werden. Üblicherweise liefern WebCams voreingestellt 24-Bit-Farbbilder in RGB-Format1 , je Pixel werden also drei Bytes für die drei Farbanteile geliefert. Verschiedene WebCams senden die Bytes jedoch in umgekehrter Reihenfolge – wahrscheinlich, weil sie nicht als einzelne Bytes, sondern als 3-Byte-Werte, beginnend mit dem niedrigstwertigen, übertragen werden (little endian Codierung). Falls Rot und Blau vertauscht erscheinen, können Sie in Zeile 12 über die define-Anweisung dafür sorgen, dass die Farben zurückgetauscht werden. (Bei dem o. g. Kernel-Modul spca5xx lässt sich über die Option2 force rgb=1“ auch die Reihenfolge der ” Farbwerte korrigieren.) Nach dem Öffnen des Devices (Zeile 21) fragt das Programm zunächst das eingestellte Bildformat ab (Zeile 27). Sollte das Format zu groß oder nicht initialisiert (Werte = 0) sein, wird es ab Zeile 39 auf Default-Werte eingestellt. Dies geschieht 1 2 RGB steht für Red-Green-Blue bzw. Rot-Grün-Blau. insmod spca5xx.ko force rgb=1“ ”
136 6 Devices – das Tor zur Hardware mit Hilfe des Kommandos VIDIOCSWIN (VIDeo IO-Control Set WINdow), das die zuvor ausgelesene und modifizierte Struktur video win wieder zurückschreibt. Das eigentliche Aufzeichnen des Bildes erfolgt mit der read()-Anweisung in Zeile 49. Vor dem Abspeichern als PPM-Datei erfolgt in den Zeilen 56 bis 66 ggf. das Vertauschen der Farbanteile Rot und Blau (siehe oben). Es besteht natürlich auch die Möglichkeit, vor der Bildaufzeichnung die Einstellungen für Helligkeit, Kontrast, Farbsättigung, Farbtiefe (Bits pro Pixel) und Farbschema (Palette) explizit einzustellen. Dies geschieht mit Hilfe des Kommandos VIDIOCSPICT (VIDeo IO-Control Set PICTure). In dem obigen Beispielprogramm könnten Sie dazu ab Zeile 48 den folgenden Quelltext einfügen: { struct video_picture video_pict; if (ioctl(fd, VIDIOCGPICT, &video_pict) == -1) { perror("Can’t get video picture information"); return(1); } video_pict.brightness = 39321; /* 60% Helligkeit */ video_pict.contrast = 26214; /* 40% Kontrast */ video_pict.depth = 24; video_pict.palette = VIDEO_PALETTE_RGB24; if (ioctl(fd, VIDIOCSPICT, &video_pict) == -1) { perror("Can’t set video picture information"); return(1); } } Bilder mit CAPTURE und mmap() aufzeichnen Die elegantere“, aber etwas aufwändigere Methode für die Aufzeichnung eines ” Bildes ist mit Hilfe der Funktionen mmap() und munmap() gegeben: void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *mem, size_t length); Beschreibung der Parameter: start – length – gewünschte Zieladresse oder NULL (→ keine Vorgabe) Größe des Speicherbereichs
6.4 Video for Linux“ ” prot – flags – fd – offset – 137 Zugriffsrechte, Oder-Verknüpfung aus den Bits PROT READ (Lesen) PROT WRITE (Schreiben) und PROT EXEC (Ausführen) Eigenschaften des Speichersegments, z. B. MAP PRIVATE oder MAP SHARED (Zugriff auch für andere Prozesse) Datei-Deskriptor des Devices zu überspringende Bytes des Devices Als Rückgabewert liefert mmap() die Speicheradresse, wohin die Daten geschrieben werden, oder MAP FAILED im Fehlerfall. Die Adresse und die Größe müssen der Funktion munmap() als Parameter übergeben werden, um das Speichersegment wieder freizugeben. Mit mmap() lässt sich der Bildspeicher (Frame Buffer) des Video-Devices in ein Speichersegment abbilden. Voraussetzung ist allerdings, dass das Device capture-fähig ist. Dies kann mit dem ioctl()-Kommando VIDIOCGCAP (siehe Abschnitt 6.4.1) überprüft werden: hier muss in der Bit-Maske type das Bit VID TYPE CAPTURE gesetzt sein. Das folgende Programm verwendet mmap(), um ein Bild der Größe 320 × 240 mit 24 Bit pro Pixel einzulesen, und schreibt dieses als PPM-Datei in den Standardausgabekanal: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* map_image.c - Ein Bild mit Hilfe von ’mmap()’ lesen */ # # # # # # include include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <sys/mman.h> <linux/videodev.h> # define WIDTH 320 # define HEIGHT 240 # define SWAP_RGB_BGR 0 /* 1 = Farbreihenfolge drehen */ int main() { int fd, frame, i, tmp; unsigned char *image; struct video_mmap vid_mmap; if ((fd = open("/dev/video", O_RDONLY)) == -1) { perror("map_image: Can’t open device");
138 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 6 Devices – das Tor zur Hardware return(1); } if ((image = mmap(NULL, WIDTH*HEIGHT*3, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) { perror("map_image: mmap() failed"); return(1); } vid_mmap.frame = 0; vid_mmap.width = WIDTH; vid_mmap.height = HEIGHT; vid_mmap.format = VIDEO_PALETTE_RGB24; if (ioctl(fd, VIDIOCMCAPTURE, &vid_mmap) == -1) { perror("map_image: Can’t capture to memory"); return(1); } frame = 0; /* muss gleich vid_mmap.frame sein */ if (ioctl(fd, VIDIOCSYNC, &frame) == -1) { perror("map_image: Can’t grab frame"); return(1); } close(fd); if (SWAP_RGB_BGR) /* Rot und Blau tauschen? */ { for (i=0; i<WIDTH*HEIGHT*3; i+=3) { tmp = image[i]; image[i] = image[i+2]; image[i+2] = tmp; } } printf("P6\n%d %d\n255\n", WIDTH, HEIGHT); fwrite(image, 3, WIDTH*HEIGHT, stdout); munmap(image, WIDTH*HEIGHT*3); return(0); }
6.4 Video for Linux“ ” 139 Nach dem Öffnen des Devices (Zeile 22) und dem Abbilden in ein Speichersegment (Zeile 28) wird die Übertragung eines Bildes zunächst in den Zeilen 35 bis 43 mit dem Kommando VIDIOCMCAPTURE vorbereitet und anschließend in den Zeilen 45 bis 50 mit dem Kommando VIDIOCSYNC gestartet. Als frame ist in beiden Fällen 0 angegeben. Besitzt das Device einen Frame-Buffer, in dem N Bilder abgelegt werden können, kann hier eine Zahl zwischen 0 und N − 1 angegeben werden. Damit die richtigen Daten in den Speicher übertragen werden, müsste man in diesem Fall jedoch einen entsprechenden Offset bei mmap() angeben. Wie schon im vorigen Beispielprogramm folgt auch hier optional das Vertauschen der Farb-Bytes für Rot und Blau (ab Zeile 53). Danach wird das Bild wiederum als PPM-Datei gespeichert. Soll eine Sequenz von Bildern aufgezeichnet werden, so müssen für jedes Bild erneut die ioctl()-Kommandos VIDIOCMCAPTURE und VIDIOCSYNC aufgerufen werden, um den Inhalt des Speichersegmentes zu aktualisieren. Weitere Einstellmöglichkeiten Für Geräte, die eine Empfangseinheit (Tuner) besitzen (wie z. B. TV-Karten) oder über mehrere Videoeingänge verfügen, gibt es ioctl()-Kommandos, mit denen die Videoquelle gewählt, das Videoformat (z. B. NTSC oder PAL) eingestellt und die Empfangsfrequenz vorgegeben werden können. Das Kommando VIDIOCSCHAN benötigt den Zeiger auf eine Struktur vom Typ video channel, das Kommando VIDIOCSFREQ erwartet den Zeiger auf eine Variable vom Typ long. Es gibt eine Reihe weiterer Kommandos und Funktionen des Video-Devices – z. B. zur Nutzung der Audio-Fähigkeiten –, die hier jedoch nicht alle im Einzelnen erläutert werden können. An dieser Stelle möchten wir noch einmal auf die Include-Datei /usr/include/linux/videodev.h verweisen, der Sie die entsprechenden ioctl()-Kommandos und die zugehörigen Strukturen entnehmen können. JPEG-Bilder speichern mit der libjpeg Bei den bisherigen Beispielprogrammen wurden die aufgezeichneten Bilder als PPM-Datei gespeichert, weil dieses Format sehr einfach zu erzeugen ist. Üblicherweise sind Bilder jedoch im JPEG-Format, das die Aufnahmen je nach Kompressionseinstellung auf weniger als 10% der Speichergröße reduziert. Die Umwandlung von Bilddaten in das JPEG-Format lässt sich relativ einfach mit der Funktionsbibliothek libjpeg“ realisieren. Das folgende Beispielprogramm zeigt die Er” weiterung des Programms read image“ um die Verwendung der libjpeg (ab Zei” le 77):
140 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 6 Devices – das Tor zur Hardware /* read_image_jpeg.c - WebCam-Bild als JPEG speichern */ # # # # # # include include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/videodev.h> <jpeglib.h> # define MAX_BYTES (640*480*3) /* Bildspeicher */ # define DEF_WIDTH 320 /* Default-Werte */ # define DEF_HEIGHT 240 # define JPEG_QUALITY 75 # define SWAP_RGB_BGR 0 /* 1 = Farbreihenfolge drehen */ int main() { int fd; long length; struct video_window video_win; static unsigned char image[MAX_BYTES]; JSAMPROW row_pointer; static struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; if ((fd = open("/dev/video", O_RDONLY)) == -1) { perror("read_image: Can’t open device"); return(1); } if (ioctl(fd, VIDIOCGWIN, &video_win) == -1) { perror("read_image: Can’t get video window"); return(1); } length = video_win.width * video_win.height * 3; if ((length < 1) || (length > MAX_BYTES))
6.4 Video for Linux“ ” 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 141 { fprintf(stderr, "read_image: Bad image size. " "Using default values.\n"); video_win.width = DEF_WIDTH; video_win.height = DEF_HEIGHT; length = DEF_WIDTH * DEF_HEIGHT * 3; if (ioctl(fd, VIDIOCSWIN, &video_win) == -1) { perror("read_image: Can’t set video window"); return(1); } } if (read(fd, image, length) == -1) { perror("read_image: Error while reading"); return(1); } close(fd); if (SWAP_RGB_BGR) { int i, tmp; /* Rot und Blau tauschen? */ for (i=0; i<video_win.width*video_win.height*3; i+=3) { tmp = image[i]; image[i] = image[i+2]; image[i+2] = tmp; } } /* ----- JPEG-Umwandlung vorbereiten -----*/ cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); jpeg_stdio_dest(&cinfo, stdout); cinfo.image_width = video_win.width; cinfo.image_height = video_win.height; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB;
142 6 Devices – das Tor zur Hardware 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, JPEG_QUALITY, TRUE); /* ----- JPEG-Umwandlung starten -----*/ jpeg_start_compress(&cinfo, TRUE); while (cinfo.next_scanline < cinfo.image_height) { row_pointer = &(image[cinfo.next_scanline*cinfo.image_width*3]); jpeg_write_scanlines(&cinfo, &row_pointer, 1); } jpeg_finish_compress(&cinfo); /* Bild speichern */ jpeg_destroy_compress(&cinfo); /* aufräumen */ return(0); } Zum Übersetzen des Programms muss die JPEG-Bibliothek eingebunden werden: gcc read_image_jpeg.c -ljpeg -o read_image_jpeg 6.5 Die serielle Schnittstelle Obwohl die als RS 232“ oder V 24“ bezeichnete serielle Schnittstelle immer mehr ” ” durch den Universal Serial Bus (USB) verdrängt wird, ist sie noch bei vielen Computern vorhanden oder wird mit Hilfe eines USB/RS232-Umsetzers emuliert. Es handelt sich dabei um einen weit verbreiteten Standard, der nicht nur bei Computer-Peripherie zum Einsatz kommt, sondern beispielsweise auch bei verschiedenen Messgeräten oder Relaiskarten (siehe auch [1]). Unter Linux sind die seriellen Ports1 als Terminal-Devices /dev/ttySn (früher /dev/cuan) in das Dateisystem eingebunden. Als solche kann man sie mit Hilfe von Funktionen zur Einstellung der Terminal-Parameter steuern. Von der Shell aus lässt sich z. B. mit stty ispeed=38400 < /dev/ttyS0 die Baudrate der ersten seriellen Schnittstelle (COM1) auf 38,4 kBit/s stellen. Die Devices /dev/ttySn sind in der Regel der Gruppe uucp zugeordnet. Um darauf 1 Das gilt auch für USB-Modems, bei denen der USB quasi nur als Brücke“ für das serielle Protokoll ” dient.
6.5 Die serielle Schnittstelle 143 zugreifen zu können, müssen Sie sich in der Datei /etc/group“ ebenfalls in ” diese Gruppe eintragen: uucp:x:14:uucp,fax,root,fnet,Ihr User-Name 6.5.1 Terminal-Parameter einstellen Es gibt eine Reihe von Funktionen, mit denen man auf die Einstellungen eines Terminal-Devices zugreifen kann. Die wichtigsten davon sind tcgetattr() zum Auslesen und tcsetattr() zum Modifizieren der Parameter: int tcgetattr(int fd, struct termios *termios_p); int tcsetattr(int fd, int actions, struct termios *termios_p); Beiden Funktionen wird neben dem Dateideskriptor des Terminal-Devices der Zeiger auf eine Variable vom Typ struct termios übergeben. tcsetattr() erwartet zusätzlich den Parameter actions, mit dem man vorgeben kann, wann die neuen Einstellungen übernommen werden. Mögliche Werte sind hier z. B. TCSANOW (sofort) und TCSAFLUSH (Änderung erst, wenn alle bis dahin nach fd geschriebenen Zeichen übertragen wurden). Die in /usr/include/bits/termios.h“ definierte Struktur struct termios ” enthält unter anderem die folgenden Einträge: tcflag_t tcflag_t tcflag_t tcflag_t c_iflag; c_oflag; c_cflag; c_lflag; /* /* /* /* input mode flags */ output mode flags */ control mode flags */ local mode flags */ Jeder dieser Parameter setzt sich aus einer ODER-Verknüpfung verschiedener Konstanten zusammen; eine Auswahl zeigt die Tabelle 6.2. Die vollständige Liste erhält man mit man tcsetattr“. ” Um die Einstellungen eines Terminal-Devices zu verändern, werden in der Regel die aktuellen Einstellungen mit tcgetattr() ausgelesen, dann die gewünschten Änderungen vorgenommen und anschließend alle Einstellungen mit tcsetattr() wieder geschrieben; Beispiel: int fd; struct termios term_attr; if (tcgetattr(fd, &term_attr) == 0) { term_attr.c_lflag &= ˜ECHO; /* kein lokales Echo */ tcsetattr(fd, TCSAFLUSH, &term_attr); }
144 6 Devices – das Tor zur Hardware Tabelle 6.2: Einige Konstanten für die Terminal-Parameter Parameter c iflag c oflag c cflag c lflag Flag Bedeutung INLCR IGNCR ICRNL IUCLC IMAXBEL empfangenes NL (new line) in CR (carriage return) wandeln empfangenes CR ignorieren empfangenes CR in NL wandeln empfangene Groß- in Kleinbuchstaben wandeln Piepton bei Zeilenende OPOST ONLCR OCRNL ONLRET OLCUC Nachbearbeitung der auszugebenden Zeichen einschalten NL beim Senden in CR + NL wandeln CR beim Senden in NL wandeln kein CR senden beim Senden Klein- in Großbuchstaben wandeln Bbaud CSn CSTOPB PARENB PARODD CLOCAL CRTSCTS CREAD Geschwindigkeit auf baud Bit/Sek. stellen Bitbreite auf n stellen (5, 6, 7 oder 8) zwei Stop-Bits statt eines verwenden Parity-Bit erzeugen ungerades Parity-Bit statt gerades Modem-Steuerleitung (CD) ignorieren Hardware-Handshake aktivieren Empfang von Daten aktivieren ISIG ICANON ECHO bestimmte Zeichen lösen ein Signal aus Steuerzeichen zulassen empfangene Zeichen wieder senden Das folgende Programm realisiert eine typische Kennworteingabe, bei der man die Eingabe selbst nicht sehen kann, um das Kennwort vor den Augen Dritter zu schützen: 1 2 3 4 5 6 7 8 9 10 11 12 13 /* password.c - Passwortabfrage ohne Echo */ # # # # # include include include include include <stdio.h> <unistd.h> <string.h> <fcntl.h> <termios.h> int main() { int old_flags;
6.5 Die serielle Schnittstelle 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 145 char password[16]; struct termios term_attr; if (tcgetattr(STDIN_FILENO, &term_attr) != 0) { perror("password: tcgetattr() failed"); return(1); } /* alte Einst. sichern */ old_flags = term_attr.c_lflag; term_attr.c_lflag &= ˜ECHO; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0) perror("password: tcsetattr() failed"); printf("password: "); scanf("%15s", password); /* Std.-Eingabe wie vorher */ term_attr.c_lflag = old_flags; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0) perror("password: tcsetattr() failed"); if (strcmp(password, "secret") == 0) printf("\npassword accepted.\n"); else printf("\nwrong password.\n"); return(0); } 6.5.2 Ein kleines Terminalprogramm Soll über die serielle Schnittstelle eine Verbindung mit einem externen Gerät – z. B. einem Telefon-Modem – aufgebaut werden, so müssen zunächst die Geschwindigkeit (Bit pro Sekunde), die Anzahl der zu übertragenden Bits pro Zeichen und die Anzahl der Stop-Bits korrekt eingestellt werden. Ferner sollte auch das Paritäts-Bit (eine Art Prüfsumme) richtig eingestellt werden. All diese Einstellungen – ebenso wie die Aktivierung des Hardware-Handshakes CTS/RTS – finden sich in dem Parameter c cflag der Struktur termios. Das folgende Programm stellt die Parameter der seriellen Schnittstelle ein (19200 baud, 8N1, Hardware-Handshake) und schaltet das lokale Echo sowie die Steuerzeichenerkennung (ICANON) des Standard-Eingabekanals aus. Alle Eingaben werden dann auf die serielle Schnittstelle gesendet und alle von der Schnittstelle empfangenen Daten im Shell-Fenster ausgegeben, bis das Programm mit ESC beendet wird. Damit verhält sich das Programm wie ein einfaches Terminal-
146 6 Devices – das Tor zur Hardware Programm – echte“ Terminal-Programme wie minicom können natürlich viel ” mehr. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 /* terminal.c - Ein- und Ausgabe ueber die serielle Schnittstelle */ # # # # include include include include <stdio.h> <unistd.h> <fcntl.h> <termios.h> # define TERM_DEVICE "/dev/ttyS0" # define TERM_SPEED B19200 /* = COM1 */ /* Bit/Sek */ int main() { int fd, old_flags; ssize_t length; char buffer[16]; struct termios term_attr; fd_set input_fdset; if ((fd = open(TERM_DEVICE, O_RDWR)) == -1) { perror("terminal: Can’t open device " TERM_DEVICE); return(1); } /* RS232 konfigurieren */ if (tcgetattr(fd, &term_attr) != 0) { perror("terminal: tcgetattr() failed"); return(1); } term_attr.c_cflag = TERM_SPEED | CS8 | CRTSCTS | CLOCAL | CREAD; term_attr.c_iflag = 0; term_attr.c_oflag = OPOST | ONLCR; term_attr.c_lflag = 0; if (tcsetattr(fd, TCSAFLUSH, &term_attr) != 0) perror("terminal: tcsetattr() failed"); /* Std.-Eingabe anpassen */ if (tcgetattr(STDIN_FILENO, &term_attr) != 0)
6.5 Die serielle Schnittstelle 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 147 { perror("terminal: tcgetattr() failed"); return(1); } /* alte Einst. sichern */ old_flags = term_attr.c_lflag; term_attr.c_lflag &= ˜(ICANON | ECHO); if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0) perror("terminal: tcsetattr() failed"); while (1) { FD_ZERO(&input_fdset); FD_SET(STDIN_FILENO, &input_fdset); FD_SET(fd, &input_fdset); if (select(fd+1, &input_fdset, NULL, NULL, NULL) == -1) perror("terminal: select() failed"); if (FD_ISSET(STDIN_FILENO, &input_fdset)) { if ((length = read(STDIN_FILENO, buffer, 16)) == -1) perror("terminal: read() failed"); else if (buffer[0] == ’\33’) /* Abbruch mit ESC */ break; else write(fd, buffer, length); } if (FD_ISSET(fd, &input_fdset)) { if ((length = read(fd, buffer, 16)) == -1) perror("terminal: read() failed"); else write(STDOUT_FILENO, buffer, length); } } /* Std.-Eingabe wie vorher */ term_attr.c_lflag = old_flags; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attr) != 0) perror("terminal: tcsetattr() failed"); printf("Aborted.\n"); close(fd); return(0); }
148 6 Devices – das Tor zur Hardware Nach dem Öffnen des Terminal-Devices (Zeile 22) wird in den Zeilen 28 bis 39 die serielle Schnittstelle konfiguriert. In Zeile 42 bis 51 erfolgt die Einstellung des Standard-Eingabekanals, wobei die alten Einstellungen zuvor in der Variablen old flags gesichert werden. Die while()-Schleife in den Zeilen 53 bis 77 bildet den Kern des Programms. Hier wird zunächst mit der Funktion select() gewartet, bis an einem der beiden Dateideskriptoren – Standard-Eingabekanal oder serielle Schnittstelle – Daten eintreffen“. Entsprechend werden entweder Zeichen ” von der Standardeingabe eingelesen und über die Schnittstelle ausgegeben (Zeile 62 bis 68) oder umgekehrt (Zeile 72 bis 75). Eine Betätigung der Taste ESC beendet die while()-Schleife durch break (Zeile 66) und stellt die alten Einstellungen für den Standard-Eingabekanal wieder her (Zeile 78 bis 81). Die Funktion select() und die dazugehörigen Makros FD ZERO(), FD SET(), FD CLR() und FD ISSET() bedürfen einiger Erklärungen. Diese Funktionen operieren mit so genannten File Descriptor Sets, das sind Variablen vom Typ fd set mit einer Länge von 1024 Bit (128 Bytes). Jedes Bit steht dabei für einen Dateideskriptor, z. B. Bit Nr. 2 für den Deskriptor mit dem Wert 2, also den StandardFehlerkanal. Das Makro FD ZERO() löscht alle Bits des angegebenen File Descriptor Sets. Mit FD SET() kann ein Dateideskriptor zu dem Set hinzugefügt und mit FD CLR() entfernt werden. Mit Hilfe von FD ISSET() lässt sich prüfen, ob ein Deskriptor in einem Set enthalten ist, d.h. das entsprechende Bit gesetzt ist. Die Funktion select() erwartet als zweiten, dritten und vierten Parameter jeweils den Zeiger auf ein File Descriptor Set. Sie testet, ob einer der in dem ersten Set enthaltenen Dateideskriptoren bereit zum Lesen oder einer der in dem zweiten Set enthaltenen Dateideskriptoren bereit zum Schreiben ist. Die Deskriptoren des dritten Sets werden auf Exceptions überprüft. Als ersten Parameter erwartet select() die Nummer des höchsten verwendeten Dateideskriptors plus 1. Als letzter Parameter kann mit Hilfe der Struktur struct timeval (siehe /usr/include/sys/time.h) eine maximale Wartezeit eingestellt werden. Die Funktion select() ermöglicht es somit, auf mehrere dateibezogene Ereignisse gleichzeitig zu warten – im Falle des Terminal-Programms das Drücken einer Taste oder das Empfangen eines Zeichens über die Schnittstelle. Noch ein Hinweis zur Einstellung der Schnittstellenparameter: Wenn die Geschwindigkeit oder die Bitbreite eingestellt werden soll, ohne die anderen Einstellungen zu verändern, müssen die relevanten Bits zunächst mit CBAUD bzw. CSIZE gelöscht werden. Beispiel: int fd; struct termios term_attr; if (tcgetattr(fd, &term_attr) == 0) { /* Bits loeschen */ term_attr.c_cflag &= ˜(CBAUD | CSIZE);
6.6 Druckerausgaben 149 /* und neu einstellen */ term_attr.c_cflag |= B19200 | CS8; tcsetattr(fd, TCSAFLUSH, &term_attr); } Die Übertragungsgeschwindigkeit für das Senden und Empfangen von Zeichen lässt sich übrigens auch separat mit den Funktionen int cfsetispeed(struct termios *termios_p, int speed); int cfsetospeed(struct termios *termios_p, int speed); einstellen.1 Diese Funktionen modifizieren die Geschwindigkeitseinstellungen in der Struktur termios. Anschließend müssen diese Einstellungen wiederum mit einem Aufruf der Funktion tcsetattr() aktiviert werden. Weitere Informationen zur Ansteuerung der seriellen Schnittstelle finden Sie in Abschnitt 9.3. 6.6 Druckerausgaben Die Ansteuerung eines Druckers fällt etwas aus der Reihe, weil sie nicht wie die anderen bisher angesprochenen Geräte über ioctl()-Kommandos funktioniert. Die Kommunikation mit dem Drucker übernimmt bei Linux ein Hintergrundprozess. Ursprünglich war dies der lpd“ (line printer daemon), der auf aktuel” len Systemen durch CUPS“ (Common UNIX Printing System) ersetzt wurde. Bei” de Systeme sind in der Lage, unterschiedliche Dateiformate zu verarbeiten; die gängigsten Formate sind ASCII (reine Textdateien) und PostScript. Während reine ASCII-Texte bei der Druckerausgabe etwas an Schreibmaschinenseiten erinnern, bietet PostScript nahezu unbegrenzte Möglichkeiten, dem Drucker qualitativ hochwertige Text- und Grafikdarstellungen zu entlocken. Daher hat sich dieses Format – oder besser gesagt: diese Sprache – als Standard auf UNIX- und LinuxSystemen durchgesetzt. Um aus der Shell heraus Daten an einen Drucker zu schicken, können Sie das Programm lpr“ verwenden. Stehen mehrere Drucker zur Verfügung, können Sie ” mit der Option -PDrucker“ das gewünschte Ausgabegerät angeben: ” lpr -Pprinter mein Text.txt Achtung: Zwischen der Option -P“ und der Bezeichnung des Druckers darf kein ” Leerzeichen stehen! CUPS bietet ein Web-Interface zur Verwaltung von Druckern und Druckjobs. Dort können Sie auch sehen, welche Drucker eingerichtet sind: 1 Auf manchen Systemen, beispielsweise Cygwin, muss die Geschwindigkeit mit Hilfe der Funktionen cfsetispeed() und cfsetospeed() eingestellt werden; das Einstellen der Übertragungsgeschwindigkeit über c cflag funktioniert dort nicht.
150 6 Devices – das Tor zur Hardware firefox http://localhost:631/printers Doch wie erzeugen Sie mit eigenen Programmen eine Druckerausgabe? Dazu bedienen wir uns der Funktion popen() aus Abschnitt 5.5.1 und rufen so das Programm lpr“ auf. Das folgende Beispiel gibt links oben auf der Seite den Text ” Hallo Welt!“ in großer Schrift (24 Punkte) aus und zeichnet einen Kreis um die” sen Text. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /* printer-demo.c - Druckerausgabe mit popen() */ # include <stdio.h> # include <string.h> int main() { int status; FILE *stream; if ((stream = popen("lpr", "w")) == NULL) { perror("printer-demo: popen() failed"); return(1); } fprintf(stream, "%%!PS\n%%%%BoundingBox: 30 30 565 810\n" "%%%%Orientation: Portrait\n%%%%EndProlog\n" "100 700 moveto\n" "/Times-Roman 24 selectfont\n" "(Hallo Welt!) show\n" "currentpoint pop 100 add 2 div\n" "newpath 708 65 0 360 arc stroke\n" "showpage\n"); status = pclose(stream); printf("printder-demo: lpr returned %d.\n", status); return(0); } Wenn Sie die Ausgabe an einen anderen als den Standarddrucker schicken wollen, müssen Sie in Zeile 13 das Kommando lpr“ um die Option -P“ (siehe oben) ” ” ergänzen. In den Zeilen 19 bis 26 wird eine PostScript-Sequenz an den Standardeingabekanal von lpr“ gesendet. ”
6.6 Druckerausgaben 151 An dieser Stelle möchten wir Ihnen die Sprache“ PostScript etwas näher bringen. ” Dazu betrachten Sie bitte die folgende PostScript-Datei: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 %!PS-Adobe-2.0 %%Title: PostScript-Beispiel1 %%Creator: Martin Gräfe %%BoundingBox: 28 28 567 813 %%Orientation: Portrait %%Pages: 1 %%EndProlog %%Page: 1 28.35 28.35 scale % ab hier in Zentimeter umskalieren 7 25 moveto % Cursor an Position x=7cm, y=25cm % Schriftart auf "Helvetica" mit 1cm Höhe: /Helvetica 1 selectfont % Text darstellen: (Dies ist ein Test.) show 0.1 setlinewidth % Liniendicke auf 0,1cm 0 0 1 setrgbcolor % Farbe Blau % Linie unter dem Text zeichnen: 0 -0.2 rmoveto % 0,2cm nach unten 7 24.8 lineto % Linie bis zum Textanfang stroke % Linie zeichnen showpage % gesamte Seite darstellen Mit dem Programm ghostview“ oder kghostview“ können Sie diese Datei auf ” ” dem Bildschirm darstellen. Es erscheint oben auf der Seite der Text Dies ist ein ” Test.“, der mit einer blauen Linie unterstrichen ist. Betrachten wir nun den Aufbau der PostScript-Datei. Die erste Zeile dient zur Erkennung des PostScriptFormats. Danach folgen einige Zeilen als Prolog“, die Aufschluss über Seiten” format usw. geben. Die BoundingBox“ gibt an, in welchen Bereich auf der Seite ” sich die zu zeichnenden Elemente befinden. Die Grundmaßeinheit von PostScript ist dabei 1 /72 Zoll (ca. 0,35 mm), und der Seitenursprung (0;0) liegt in der linken unteren Ecke des Blatts. Das %-Zeichen definiert in PostScript den Rest der Zeile als Kommentar und ist vergleichbar mit //“ in C. Eine Ausnahme bilden zwei %-Zeichen am Zeilen”
152 6 Devices – das Tor zur Hardware anfang. Diese signalisieren Kontrollinformationen für den PostScript-Interpreter. Der Prolog am Anfang der Datei darf übrigens weder Leer- noch Kommentarzeilen enthalten, damit er von PostScript-Interpretern wie ghostview“ verarbeitet ” wird. Zeile 9 des PostScript-Beispiels signalisiert dem Interpreter, dass hier die Seite mit der Nummer 1 folgt. Wenn Sie hier beispielsweise eine 5 eingeben, zeigt ghostview in der Seitenübersicht die Seitennummer 5 an, obwohl es nur eine Seite ist. In Zeile 11 folgt mit scale“ die erste richtige PostScript-Anweisung. Hier wird ” alles Folgende von der ursprünglichen Maßeinheit auf Zentimeter umskaliert. Bitte beachten Sie, dass PostScript grundsätzlich in der so genannten umgekehrten polnischen Notation (UPN) arbeitet, d. h. die Parameter eines Kommandos oder eines Operators stehen immer vor dem Kommando bzw. Operator. Die Rechnung 20 − 10“ heißt in PostScript 20 10 sub“. So stehen die X- und Y-Position auch ” ” vor dem moveto-Kommando in Zeile 13. Die weiteren Zeilen des Beispiels sind selbsterklärend. Es sei aber noch darauf hingewiesen, dass Zeichen wie ( ) %“ als ” Text ausgegeben werden können, wenn ihnen ein \“ vorangestellt wird. ” Dass PostScript nicht einfach nur ein Text-Dateiformat, sondern eine komplexe Programmiersprache mit Funktionen und Schleifenstrukturen ist, soll dieses zweite Beispiel zeigen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 %!PS-Adobe-2.0 %%Title: PostScript-Beispiel2 %%Creator: Martin Gräfe %%BoundingBox: 28 28 567 813 %%Orientation: Portrait %%Pages: 1 %%EndProlog %%Page: 1 % Schriftart auf "Times-Roman" mit 18pt Höhe: /Times-Roman 18 selectfont 230 320 moveto 8 { % das Folgende 8x wiederholen ( Dies ist ein Test. ) show 45 rotate % um 45◦ nach links drehen } repeat gsave % Position/Skalierung merken newpath 0.5 1 scale 0.8 setgray % Position löschen % alles Folgende halbe Breite % Grau mit 80% Helligkeit
6.6 Druckerausgaben 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 153 582 475 70 0 360 arc fill % Kreis zeichnen % und ausfüllen grestore % alte Position/Skalierung /buffer 2 string def % entspricht: char buffer[2]; 0 1 5 { % for (i=0; i<=5; i+=1) dup % Zähler vom Stack kopieren buffer exch 49 add 0 exch put % buffer[0] = 49+i; -15 mul 510 add 270 exch moveto % x=270, y=510-15*i (Test ) show % printf("Test "); buffer show % printf(buffer); } for showpage In den Zeilen 15 bis 18 wird eine Anweisungsfolge mit Hilfe einer repeat-Schleife acht mal wiederholt. In den Zeilen 32 bis 38 kommt eine for-Schleife zum Einsatz, deren Zähler von 0 bis 5 läuft und (um 1 erhöht) als Text dargestellt wird. Abbildung 6.1 zeigt das Ergebnis. st. Dies ist ein Test. D Te ie ei n si s ist te in t. Dies ist ein Test. D ie s Te s Dies ist ein Test. D ie D st. Te si n st ei ei st n si Te ie st. Test 1 Test 2 Test 3 Test 4 Test 5 Test 6 Dies ist ein Test. Abbildung 6.1: So kommt das zweite PostScript-Beispiel aus dem Drucker. Für weitere Informationen und eine umfassendere Beschreibung der Sprache PostScript sei hier auf entsprechende Tutorials im Internet verwiesen, siehe auch [9], [10] und [11].
154 6 Devices – das Tor zur Hardware 6.7 Der Universal Serial Bus (USB) Zwar haben wir in Abschnitt 6.4 schon gezeigt, wie eine USB-Kamera angesteuert werden kann, wenn ein entsprechender Treiber installiert wurde. Im Folgenden soll aber gezeigt werden, wie man auf USB-Geräte auch ohne Treiber in Form eines Kernel-Moduls zugreift – und zwar mit Hilfe der libusb“. ” Zum Einstieg in dieses Thema zeigt das folgende Programm die Auflistung aller Busse und der dort angeschlossenen Geräte:1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 1 /* find_usb.c - USB-Geräte auflisten */ # include <stdio.h> # include <string.h> # include <usb.h> int main() { int i; struct usb_bus *bus; struct usb_device *udev; usb_dev_handle *udevhd; static char buffer[256]; usb_init(); // libusb initialisieren usb_find_busses(); usb_find_devices(); // alle USBs suchen // alle Geräte an den USBs suchen bus = usb_get_busses(); // Zeiger auf ersten USB i = 1; while (bus != NULL) { printf("%d. Universal Serial Bus:\n", i++); udev = bus->devices; // 1. Gerät while (udev != NULL) { udevhd = usb_open(udev); if (udevhd != NULL) { if (udev->descriptor.iProduct) Das Gleiche leistet übrigens das Programm /usr/sbin/lsusb“. ”
6.7 Der Universal Serial Bus (USB) 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 155 usb_get_string_simple(udevhd, udev->descriptor.iProduct, buffer, 256); else buffer[0] = ’\0’; printf(" %04X : %04X ’%s’\n", udev->descriptor.idVendor, udev->descriptor.idProduct, buffer); usb_close(udevhd); } udev = udev->next; // Zeiger auf nächstes Gerät } bus = bus->next; // Zeiger auf nächsten USB } if (i == 1) printf("No USB found.\n"); return(0); } Das Übersetzen dieses Programms erfordert das Einbinden der libusb mit Hilfe der Option -l“: ” > gcc find usb.c -lusb -o find usb > ./find usb 1. Universal Serial Bus: 1307 : 0165 ’USB Mass Storage Device’ 0AEC : 3260 ’USB Storage Device’ 0000 : 0000 ’EHCI Host Controller’ 2. Universal Serial Bus: 03F0 : 4811 ’PSC 1600 series’ 046D : C001 ’USB Mouse’ 0000 : 0000 ’OHCI Host Controller’ Sehen wir uns das Programm im Detail an: In Zeile 17 wird zunächst die libusb initialisiert. Danach werden die USB-Busse und -Geräte von der Bibliothek analysiert (Zeile 19 und 20) und die Informationen dazu in den Strukturen usb bus und usb device abgelegt. Beide Strukturen enthalten jeweils einen Zeiger auf den nächsten Bus bzw. das nächste Gerät. In zwei verschachtelten while-Schleifen (Zeile 25 und 29) werden alle gefundenen Busse und Geräte der Reihe nach abgefragt und deren Herstellerkennung (idVendor) und Produkt-Code (idProduct) ausgegeben. Außerdem wird der Beschreibungstext aus jedem Gerät ausgelesen (Zeile 35). Dazu muss das Device zunächst geöffnet und später wieder geschlossen werden (Zeile 31 und 43).
156 6 Devices – das Tor zur Hardware Neben den im Beispielprogramm verwendeten Elementen iProduct, idProduct und idVendor enthält die Struktur usb device descriptor einige weitere Einträge mit Informationen über das angeschlossene Gerät, wie z. B. die Seriennummer: struct usb_device_descriptor { u_int8_t bLength; u_int8_t bDescriptorType; u_int16_t bcdUSB; u_int8_t bDeviceClass; u_int8_t bDeviceSubClass; u_int8_t bDeviceProtocol; u_int8_t bMaxPacketSize0; u_int16_t idVendor; u_int16_t idProduct; u_int16_t bcdDevice; u_int8_t iManufacturer; u_int8_t iProduct; u_int8_t iSerialNumber; u_int8_t bNumConfigurations; }; 6.7.1 Ansteuerung von USB-Geräten anhand eines Beispiels Als Beispiel für die Kommunikation mit USB-Geräten zeigen wir in diesem Abschnitt die Ansteuerung eines USB- Raketenwerfers“, der über verschiedene An” bieter im Internet für knapp 30,– Euro erworben werden kann (siehe Abbildung 6.2). Das Gerät hat die Herstellerkennung 1130hex (Tenx Technology Inc.) und die Produkt-ID 0202hex. Das folgende Programm sucht zunächst in der Liste der USB-Devices das richtige Gerät anhand der Hersteller- und Produkt-ID. Danach lässt es den Raketenwerfer nach oben schwenken, eine Rakete abfeuern und anschließend wieder nach unten schwenken. 1 2 3 4 5 6 7 8 9 10 11 /* missile.c - USB-Raketenwerfer ansteuern */ # # # # include include include include <stdio.h> <string.h> <unistd.h> <usb.h> # define ID_PRODUCT 0x0202 # define ID_VENDOR 0x1130 // Kennung des // Raketenwerfers
6.7 Der Universal Serial Bus (USB) 157 Abbildung 6.2: Der USB-Raketenwerfer für den Schreibtisch 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # define TIMEOUT 1000 // Millisekunden char init1[8] = "USBC\0\0\4\0", init2[8] = "USBC\0\x40\2\0"; /*----- Funktion zum Auffinden des Gerätes -----*/ struct usb_device *find_missile(void) { struct usb_bus *bus; struct usb_device *udev; bus = usb_get_busses(); // Zeiger auf ersten USB while (bus != NULL) { udev = bus->devices; // 1. Gerät while (udev != NULL) { if ((udev->descriptor.idVendor == ID_VENDOR) && (udev->descriptor.idProduct == ID_PRODUCT)) return(udev);
158 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 6 Devices – das Tor zur Hardware udev = udev->next; } bus = bus->next; } return(NULL); // Zeiger auf nächstes Gerät // Zeiger auf nächsten USB } /*----- Funktion zum Beanspruchen der Interfaces -----*/ int claim_missile(usb_dev_handle *hd) { usb_detach_kernel_driver_np(hd, 0); // ggf. Kernelusb_detach_kernel_driver_np(hd, 1); // Treiber abkoppeln if (usb_set_configuration(hd, 1)) return(1); if (usb_claim_interface(hd, 1)) return(1); return(0); } /*----- Funktion zum Senden von Kommandos -----*/ int missile_do(usb_dev_handle *hd, int left, int right, int up, int down, int fire) { int n; static char buffer[64]; n = usb_control_msg(hd, USB_TYPE_CLASS | USB_RECIP_INTERFACE, USB_REQ_SET_CONFIGURATION, 0, 1, init1, sizeof(init1), TIMEOUT); if (n != sizeof(init1)) return(1); n = usb_control_msg(hd, USB_TYPE_CLASS | USB_RECIP_INTERFACE, USB_REQ_SET_CONFIGURATION, 0, 1, init2, sizeof(init2), TIMEOUT); if (n != sizeof(init2)) return(1);
6.7 Der Universal Serial Bus (USB) 159 79 buffer[0] = 0; // Kommandos in Puffer schreiben 80 buffer[1] = left; 81 buffer[2] = right; 82 buffer[3] = up; 83 buffer[4] = down; 84 buffer[5] = fire; 85 buffer[6] = buffer[7] = 8; 86 for (n=8; n<64; n++) // Rest mit Nullen auffüllen 87 buffer[n] = 0; 88 89 n = usb_control_msg(hd, 90 USB_TYPE_CLASS | USB_RECIP_INTERFACE, 91 USB_REQ_SET_CONFIGURATION, 0, 1, 92 buffer, sizeof(buffer), TIMEOUT); 93 if (n != sizeof(buffer)) 94 return(1); 95 96 return(0); 97 } 98 99 /*---------- Hauptprogramm ----------*/ 100 101 int main() 102 { 103 struct usb_device *missile; 104 usb_dev_handle *missile_hd; 105 usb_init(); // libusb initialisieren 106 107 108 usb_find_busses(); // alle USBs suchen 109 usb_find_devices(); // alle Geräte an den USBs suchen 110 111 if ((missile = find_missile()) == NULL) 112 { 113 fprintf(stderr, "missile: Device not found!\n"); 114 return(1); 115 } 116 missile_hd = usb_open(missile); // Devide öffnen 117 118 if (missile_hd == NULL) 119 { 120 perror("missile: Can’t open device"); 121 return(1); 122 }
160 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 6 Devices – das Tor zur Hardware if (claim_missile(missile_hd)) // Interface belegen { perror("missile: Can’t claim interface"); usb_close(missile_hd); return(1); } // < > ˆ v F if (missile_do(missile_hd, 0, 0, 1, 0, 1)) // hoch + perror("missile: Error sending message"); // Feuer usleep(1500000L); if (missile_do(missile_hd, 0, 0, 0, 0, 0)) perror("missile: Error sending message"); // stopp usleep(1500000L); if (missile_do(missile_hd, 0, 0, 0, 1, 0)) perror("missile: Error sending message"); // runter usleep(1500000L); if (missile_do(missile_hd, 0, 0, 0, 0, 0)) perror("missile: Error sending message"); // stopp usb_close(missile_hd); return(0); } Zum Übersetzen des Programms muss auch hier wieder die libusb eingebunden werden: gcc missile.c -lusb -o missile Funktionsweise des Programms In den Zeilen 18 bis 97 werden drei Hilfsfunktionen definiert. Die Funktion find missile() (Zeile 20 bis 40) durchsucht die Liste der angeschlossenen USBGeräte nach der richtigen Hersteller- und Produkt-ID und gibt den Zeiger auf das entsprechende USB-Device zurück – bzw. 0, wenn der Raketenwerfer nicht gefunden wurde. Bevor das Programm mit dem Raketenwerfer kommunizieren kann, muss es das Interface des Geräts für sich beanspruchen. Dies geschieht in der Funktion claim missile() (Zeile 44 bis 55). Weil der Raketenwerfer ggf. vom
6.7 Der Universal Serial Bus (USB) 161 Kernel als Human Interface Device (HID) erkannt und daher von einem KernelTreiber belegt“ wird, muss in den Zeilen 46 und 47 zunächst der Kernel-Treiber ” abgekoppelt“ werden, weil das Programm sonst nicht auf das Interface des Rake” tenwerfers zugreifen kann.1 Der Raketenwerfer stellt zwei Interfaces bereit (0 und 1), die beide vom Kernel-Treiber getrennt werden müssen. Anschließend wird die aktive Konfiguration gewählt und das Interface 1 reserviert, das wir für die Kommunikation benutzen wollen. Die dritte Hilfsfunktion missile do() in den Zeilen 59 bis 97 sendet schließlich die Kommandos an den Raketenwerfer. Als Parameter erwartet die Funktion neben dem Zeiger auf den Device-Handle je einen Wert für die vier Bewegungsrichtungen und den Auslöser des Raketenwerfers. Für die auszuführenden Bewegungen muss der entsprechende Parameter auf 1 gesetzt werden, die anderen Parameter müssen auf 0 bleiben. Bevor man das Kommando zur Bewegung des Raketenwerfers senden kann, müssen zwei Initialisierungssequenzen an das Gerät geschickt werden, die in den Zeilen 15 und 16 definiert sind. Das Senden von Steuersequenzen an das USBGerät übernimmt die Funktion usb control msg() in den Zeilen 65, 72 und 89: int usb_control_msg( usb_dev_handle *dev, int requesttype, int request, int value, int index, char *bytes, int size, int timeout ); Der Parameter requesttype ist eine Bitmaske, die einerseits die Art der Anforderung (Request) festlegt (hier der Typ CLASS) und andererseits den Empfänger spezifiziert, in diesem Fall also ein Interface des Gerätes. Der Parameter index gibt die Nummer des adressierten Interfaces an, hier also 1. Als request wird das Setzen der Konfiguration mit den Daten angefordert, auf die der Parameter bytes zeigt. Während size die Anzahl der Bytes angibt, die übertragen werden sollen, hat value hier keine Bedeutung. Weitere Informationen zur libusb finden Sie auf den Internetseiten [13] von Sourceforge.net. Für Programme und Infos zur Ansteuerung des Raketenwerfers unter Linux verweisen wir auf die Internetseiten von Luke Cole [12]. 1 Wurde der Kernel-Treiber bereits vom Interface getrennt, liefern die detach-Funktionen einen Fehler als Rückgabewert, was wir hier jedoch einfach ignorieren.

Kapitel 7 Netzwerkprogrammierung Seit den Anfängen von Linux vor mehr als 10 Jahren ist die Netzwerkkommunikation1 fester Bestandteil des Betriebssystems. Während zu dieser Zeit auf WindowsTM 3.11-Rechner diverse proprietäre Netzwerkprotokolle aufgesetzt wurden, war Linux von Haus aus in der Lage, über den Standard TCP/IP mit Workstations, Großrechnern und Servern in Rechenzentren zu kommunizieren. Im Bereich der PCs und Workstations wurden die proprietären Lösungen fast vollständig von TCP/IP verdrängt, sodass inzwischen die unterschiedlichen Betriebssysteme in einem Netzwerk die gleiche Sprache“ sprechen. ” Möglicherweise fragen Sie sich an dieser Stelle, ob Sie sich mit der komplexen Thematik Netzwerkprogrammierung auseinandersetzen sollen, weil Sie vielleicht gar nicht beabsichtigen, mehrere Computer mit eigenen Programmen zu vernetzen“. ” Bei Linux ist jedoch die Netzwerkkommunikation mehr als ein Hilfmittel zum Datenaustausch zwischen zwei Computern; sie ist die logische Fortsetzung der Interprozesskommunikation, und viele Teile des Systems bauen darauf auf. Wenn Sie auf dem Desktop Ihres Linux-PCs ein Fenster schließen oder eine Schaltfläche anklicken, wird dies dem betreffenden Programm von der grafischen Oberfläche X11 über Funktionen zur Netzwerkkommunikation mitgeteilt. Aus diesem Grund ist es sinnvoll, dieses Thema selbst bei Stand-Alone-Systemen“ ohne einen Netz” werkanschluss zu betrachten. Die Thematik der Netzwerkprogrammierung ist deutlich umfangreicher als beispielsweise die Interprozesskommunikation. Eine vollständige und detaillierte Beschreibung umfasst leicht mehrere hundert Seiten. Deshalb konzentriert sich dieses Kapitel auf die geläufigsten Funktionen und Methoden, die Sie immerhin in die Lage versetzen werden, einen eigenen Webserver zu programmieren! 1 Netzwerkkommunikation wäre die passendere Bezeichnung für dieses Kapitel. In der englischsprachigen Literatur hat sich jedoch der Begriff Network Programming“ etabliert und wurde später ” wörtlich ins Deutsche übersetzt.
164 7 Netzwerkprogrammierung 7.1 Einführung Ähnlich wie bei den vorangegangenen Kapiteln soll auch hier der Einstieg in die Thematik anhand kleiner Beispielprogramme erleichtert werden. Um die einzelnen Schritte in den Programmen nachvollziehen zu können, sind jedoch ein gewisses Grundlagenwissen und etwas Theorie unerlässlich. Insbesondere sollen die im Umfeld der Netzwerkkommunikation auftauchenden Begriffe sowie das Prinzip der Kommunikation über eine Netzwerkverbindung erläutert werden. 7.1.1 Begriffe Ethernet Der Begriff Ethernet“ wird häufig als Synonym für eine Netzwerkverbindung ” verwendet. Tatsächlich beschreibt Ethernet ein Verfahren, wie mehrere Teilnehmer auf eine gemeinsame Netzwerkleitung zugreifen können – es handelt sich dabei also um ein Zugriffsverfahren. Ursprünglich bestand diese Netzwerkleitung aus einem Koaxialkabel (ähnlich einem Antennenkabel), an das alle Computer eines Netzwerksegments über je ein T-Stück angeschlossen waren. Damit waren Datenraten bis 10 MBit/s möglich. Heute findet man eine solche Verkabelung nur noch selten. Das Koaxialkabel wurde weitestgehend von der bekannten 8-adrigen CAT 5“-Leitung verdrängt, und ” die Netzwerkstruktur ist heutzutage in der Regel sternförmig, mit Hubs und Switches in den Sternpunkten. Ethernet definiert also weder die Art der Leitung noch die des Protokolls (s. u.), das über die Leitung übertragen wird. Protokoll Damit sich die Teilnehmer eines Netzwerkes untereinander verstehen“, muss in ” einem Protokoll festgelegt sein, wie ein Datenpaket“ auf der Netzwerkverbindung aussieht; ” wie der Empfänger des Datenpaketes adressiert“ wird; ” ob und wie auf Übertragungsfehler reagiert wird; ... Einige solcher Protokolle sind: IP – Internet Protocol (regelt die Adressierung der Netzwerkteilnehmer); TCP – Transmission Control Protocol (definiert die gesicherte1 Datenübertragung zwischen zwei Netzwerkteilnehmern); 1 gesichert“ bedeutet hier: Es können keine Daten verloren gehen. ”
7.1 Einführung 165 UDP – User Datagram Protocol (regelt die Datenübertragung ähnlich TCP, jedoch nicht gesichert); FTP – File Transfer Protocol (definiert die Übertragung von Dateien); telnet – Protokoll zum Login eines Benutzers auf einem entfernten Rechner; HTTP – Hyper-Text Transfer Protocol (beschreibt den Zugriff auf Web-Seiten); SMTP – Simple Mail Transfer Protocol (regelt die E-Mail-Übertragung); PPP – Point to Point Protocol (definiert die Netzwerkverbindung zwischen zwei Teilnehmern über eine Punkt-zu-Punkt-Verbindung inkl. Authentifizierung und Zuweisung einer IP-Adresse). In der Regel trifft man auf eine Verschachtelung mehrerer Protokolle. So laufen“ ” über ein serielles Kabel zwischen PC und analogem Modem beim Aufrufen einer Internet-Seite gleichzeitig die Protokolle HTTP, TCP, IP und PPP. Bei den meisten PC-Netzwerken wie auch im Internet kommt die Kombination TCP und IP zum Einsatz, kurz als TCP/IP bezeichnet. Das IP sorgt dafür, dass die Daten zum richtigen Teilnehmer gelangen, das TCP stellt sicher, dass alle Datenpakete fehlerfrei und in der richtigen Reihenfolge ankommen. Die Beispiele in diesem Kapitel beziehen sich alle auf TCP/IP-Verbindungen. In Abschnitt 7.3.3 werden die Grundlagen des HTTP beschrieben. Port In TCP/IP-Netzwerken werden die Netzwerkteilnehmer über ihre IP-Adresse adressiert. Doch wie funktioniert es, dass die Daten des Web-Servers (also die HTML-Seiten) im Browser landen, die E-Mails im E-Mail-Client ankommen, die Dateien vom FTP-Server zum FTP-Client (z. B. xftp) übertragen werden und Benutzername und Passwort bei einem remote login“ (rlogin oder telnet) an der ” richtigen Stelle ankommen? Dies ist in ähnlicher Weise gelöst, wie auch die richtigen Daten an Drucker, Modem, Monitor, Tastatur, Maus und Scanner ankommen: Die Geräte hängen an unterschiedlichen Anschlüssen oder auch Ports des Computers. Ebenso enthalten die TCP/IP-Datenpakete neben der IP-Adresse auch eine Port-Nummer, also eine Information, für welchen Anschluss“ des Computers die ” Daten bestimmt sind (Abbildung 7.1). Es gibt eine ganze Reihe von Port-Nummern, die fest für bestimmte Dienste vergeben sind, beispielsweise:
166 7 Netzwerkprogrammierung TCP/IP - Port 1 Port 2 Port 3 Port 4 .. . - Programm a - Programm b - Programm c - Programm d IP-Adr.: xxx.xxx.xxx.xxx Abbildung 7.1: Aufteilung der IP-Verbindungen auf verschiedene Ports Port Dienst 21 23 25 53 79 80 6000 FTP-Server telnet-Server SMTP-Server (E-Mail-Dienst) Domain Name Server Finger“-Server (Infos über Benutzer) ” Web-Server (HTTP-Server) X11-Server Eine umfangreiche Liste der (möglichen) Dienste und deren Port-Nummern finden Sie in der Datei /etc/services“. ” Verbindung (Connection) Unter Verbindung“ versteht man in der Netzwerkkommunikation nicht die phy” sikalische Leitung zwischen zwei Computern (also das Kabel), sondern den Datenpfad, der beispielsweise zwischen einem Browser und einem Webserver aufgebaut wird. Neben der verbindungsorientierten Kommunikation, wie sie in diesem Kapitel behandelt wird, gibt es auch verbindungslosen Datentransfer im Netzwerk. Ist z. B. einem Netzwerkteilnehmer die Adresse des Domain Name Servers (DNS) noch nicht bekannt, fordert er diese mit Hilfe einer Broadcast-Nachricht an, ohne zuvor eine Verbindung aufzubauen. Verbindungslose Kommunikation hat also nichts mit Wireless LAN zu tun. 7.1.2 Vorbereitung Die Firewall Auf aktuellen Linux-Systemen ist in der Regel eine Firewall installiert und auch aktiviert. Diese blockiert aus Sicherheitsgründen verschiedene Netzwerkaktivitäten, ohne dass Warn- oder Fehlermeldungen ausgegeben werden. Dadurch laufen einige der folgenden Beispielprogramme bei aktivierter Firewall nicht oder nicht korrekt. Aus diesem Grund sollten Sie zunächst prüfen, ob eine Firewall aktiv ist
7.1 Einführung 167 und diese dann ggf. deaktivieren.1 Bei SuSE-Linux geht das mit Hilfe des Konfigurationstools YAST. Wählen Sie dazu in der Rubrik Sicherheit und Benutzer“ ” den Punkt Firewall“ (Abbildung 7.2). ” Abbildung 7.2: Konfiguration der Firewall mit YAST unter SuSE Abbildung 7.3: Abschalten der Firewall 1 Aus Sicherheitsgründen sollten Sie bei Versuchen mit eigenen Netzwerkprogrammen und abgeschalteter Firewall keine Verbindung mit dem Internet haben!
168 7 Netzwerkprogrammierung Es erscheint dann das Fenster zur Konfiguration, in dem Sie die Schaltfläche Fire” wall nun stoppen“ wählen (Abbildung 7.3). Danach können Sie die Konfiguration mit der entsprechenden Schaltfläche abbrechen. Wenn Sie die Einstellung Ser” vice starten: Bei Systemstart“ aktiviert lassen, wird die Firewall automatisch beim nächsten Neustart des Rechners wieder aktiv. Netzwerkdienste aktivieren Linux stellt bereits einige Netzwerkdienste zur Verfügung, von denen einige in dem xinetd“ zusammengefasst sind. Dieser Dämon ist in der Grundkonfigurati” on häufig deaktiviert. Da die im xinetd enthaltenen Dienste aber gerade für erste Versuche mit Netzwerkprotokollen hilfreich sind, sollten sie den Dämon aktivieren (siehe Abbildung 7.4, Netzwerkdienste (xinetd)“). Abbildung 7.5 zeigt einen ” Teil der Dienste, die vom xinetd bereitgestellt werden. Hier sollten Sie finger“, ” echo“ und ftp“ für die Beispiele in den folgenden Abschnitten aktivieren. ” ” Abbildung 7.4: Konfiguration der Netzwerkdienste mit YaST
7.1 Einführung 169 Abbildung 7.5: Aktivierung der xindet“-Dienste ” 7.1.3 Das Client-Server-Prinzip Bei der verbindungsorientierten Netzwerkkommunikation, wie sie in den folgenden Abschnitten beschrieben wird, tritt jeweils ein Teilnehmer als Server und der andere als Client auf. Die Abläufe (Funktionsaufrufe) zum Verbindungsaufbau sind bei Client und Server ganz unterschiedlich: Der Server meldet seinen Dienst in der Regel mit einer festen Port-Nummer an (z. B. belegt ein Webserver den Port Nr. 80). Der Client (Browser) baut eine Verbindung zum Server auf, indem er diesen mit der IP-Adresse und der Port-Nummer adressiert. Sobald die Verbindung aufgebaut ist, wird dem Client automatisch ebenfalls eine Port-Nummer zugewiesen, die in der Regel unabhängig von der Port-Nummer des Servers ist (siehe Abbildung 7.6). Web- - Port 1052  Browser IP-Adr.: w.x.y.z Netzwerk - Port 80 - Web- Server IP-Adr.: a.b.c.d Abbildung 7.6: Verbindungsorientierte Kommunikation zwischen Client und Server
170 7 Netzwerkprogrammierung Damit können beide Applikationen, Client und Server, jeweils eindeutig über ihre IP-Adresse und Port-Nummer adressiert werden. Übrigens: Client und Server können natürlich auch auf dem gleichen Computer laufen, also die gleiche IP-Adresse haben. 7.1.4 Sockets Basis der Netzwerkkommunikation – ob verbindungsorientiert oder verbindungslos – bilden die Sockets (zu Deutsch: Sockel“, Steckdosen“). Sowohl Client ” ” als auch Server müssen zunächst einen Socket öffnen, bevor eine Verbindung aufgebaut werden kann. Zum Öffnen eines Sockets dient die gleichnamige Funktion int socket(int domain, int type, int protocol); Dabei gibt der Parameter domain die Protokollfamilie für die Kommunikation über diesen Socket an. Für TCP/IP-Verbindungen ist das die Konstante PF INET. Der zweite Parameter legt die Art der Kommunikation fest, für verbindungsorientierte Kommunikation ist hier die Konstante SOCK STREAM zu wählen. Der dritte Parameter, der das zu benutzende Protokoll bestimmt, kann in der Regel auf 0 gesetzt werden, wodurch automatisch das zu Protokollfamilie und Kommunikationsart passende Protokoll gewählt wird. Als Rückgabewert liefert socket() einen Dateideskriptor (oder —1 im Fehlerfall) – ein Socket ist also quasi eine Datei. Daher wird er wie eine Datei wieder geschlossen: int close(int sock_fd); Damit sieht das Rahmenprogramm“ für die Netzwerkkommunikation wie folgt ” aus: # include <sys/socket.h> int main(int argc, char *argv[]) { int sock_fd; ... sock_fd = socket(PF_INET, SOCK_STREAM, 0); if (sock_fd == -1) perror("socket() failed"); ... close(sock_fd); } Zum Lesen aus oder Schreiben in einen Socket können Sie – wie bei Devices – die Funktionen read() und write() verwenden. Darüber hinaus gibt es für
7.2 Der TCP/IP-Client 171 die Netzwerkkommunikation spezielle Funktionen, die zusätzliche Möglichkeiten bieten: int send(int sock_fd, void *data, size_t len, int flags); int recv(int sock_fd, void *buffer, size_t len, int flags); Doch bevor wir überhaupt Daten über einen Socket lesen oder schreiben können, muss eine Verbindung zum Socket des Kommunikationspartners hergestellt werden. Weil dies bei Client und Server unterschiedlich ist, wird in den folgenden Abschnitten zunächst ein typischer Client und anschließend ein einfacher Server vorgestellt. 7.2 Der TCP/IP-Client Für die Demonstration von Netzwerkprogrammierung benötigt man sowohl ein Client- wie auch ein Server-Programm. Glücklicherweise ist Linux von Haus aus mit einer Reihe von Server-Programmen auf TCP/IP-Basis ausgestattet, sodass wir uns zunächst auf das Programmieren eines TCP/IP-Clients konzentrieren können. 7.2.1 Aufbau einer Verbindung Als erster Schritt muss, wie bereits erwähnt, ein Socket geöffnet werden. Danach kann das Client-Programm diesen Socket mit einem Socket des ServerProgramms verbinden. Dies geschieht mit der Funktion connect(): int connect(int sock_fd, struct sockaddr *serv_addr, socklen_t addrlen); War der Verindungsaufbau erfolgreich, gibt connect() eine 0 zurück, anderenfalls –1. Als ersten Parameter erwartet connect() den Dateideskriptor des Sockets, der verbunden werden soll. Danach folgen als zweiter und dritter Parameter die (Internet-)Adresse des zu kontaktierenden Servers und die Länge der Adresse. Als Adresse verwenden wir nicht die in der Deklaration von connect() angegebene, allgemein gehaltene Struktur sockaddr, sondern die speziell auf IPVerbindungen abgestimmte Variante sockaddr in: struct sockaddr_in { sa_family_t unsigned short int struct in_addr unsigned char } sin_family; sin_port; sin_addr; __pad[8]; /* /* /* /* Adressfamilie */ Port-Nummer */ Internet-Adr. */ auffuellen */
172 7 Netzwerkprogrammierung Vor dem Aufruf der Funktion connect() müssen in diese Struktur die IP-Adresse und die Port-Nummer des Servers eingetragen werden. Das erste Element gibt dabei die Adressfamilie an; für eine IP-Adresse ist das AF INET. Als zweites Element enthält die Struktur die Port-Nummer des Servers. Diese besteht aus zwei Bytes (unsigned short int), deren Reihenfolge (High Byte, Low Byte) im InternetProtokoll festgelegt ist und von der Architektur des Rechners abweichen kann. Um die Port-Nummer korrekt in die Struktur einzutragen, sollte unbedingt die Funktion htons() verwendet werden. Analog dazu sollte auch die IP-Adresse mit Hilfe der Funktion int inet_aton(char *text, struct in_addr *addr); in die Struktur kopiert werden, wobei die Zeichenkette text die IP-Adresse in der üblichen Schreibweise (z. B. 127.0.0.1“) enthält, die von inet aton() in ” binäre Form umgewandelt wird. Ist die angegebene IP-Adresse fehlerhaft, liefert inet aton() eine 0 als Rückgabewert. Um dieser abstrakten, theoretischen Beschreibung etwas Anschauung zu verleihen, soll folgendes Beispiel den Verbindungsaufbau zu Port 80 der IP-Adresse 127.0.0.1 (localhost) demonstrieren: # # # # include include include include <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> int sock_fd; struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(80); inet_aton("127.0.0.1", &(server_addr.sin_addr)); err = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)); if (err == -1) perror("connect() failed"); Mit dem Rahmenprogramm“ von Seite 170 kombiniert, ergibt sich daraus ein ” vollständiges Programm zum Öffnen eines Sockets und Verbinden des Sockets mit einem Web-Server (falls eingerichtet). Da jedoch keine Daten mit dem Server ausgetauscht werden, ist dieses Programm nicht besonders nützlich – es kann allenfalls feststellen, ob ein entsprechendes Server-Programm eingerichtet und aktiviert wurde.
7.2 Der TCP/IP-Client 173 7.2.2 Ein Universal“-Client ” Das folgende Programm öffnet einen Socket und baut die Verbindung zu einem Server auf. Es erwartet als Kommandozeilenparameter die IP-Adresse und die Port-Nummer des Servers. Nach erfolgreichem Verbindungsaufbau arbeitet das Programm ähnlich einem Terminal-Programm: (Tastatur-)Eingaben werden an den Server geschickt; die Antwort des Servers wird im Shell-Fenster ausgegeben. Mit Ctrl-D kann die Eingabe – und somit auch das Programm – beendet werden. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /* connect.c - einfacher Netzwerk-Client */ # # # # # # # include include include include include include include <stdio.h> <unistd.h> <string.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> int main(int argc, char *argv[]) { static char buffer[256]; int sock_fd, err, length, port; struct sockaddr_in server_addr; fd_set input_fdset; if (argc != 3) { fprintf(stderr, "Usage: connect ip-addr port\n"); return(1); } if (sscanf(argv[2], "%d", &port) != 1) { fprintf(stderr, "connect: bad argument ’%s’\n", argv[2]); return(1); } sock_fd = socket(PF_INET, SOCK_STREAM, 0); if (sock_fd == -1) { perror("connect: Can’t create new socket");
174 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 7 Netzwerkprogrammierung return(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(port); err = inet_aton(argv[1], &(server_addr.sin_addr)); if (err == 0) { fprintf(stderr, "connect: Bad IP-Address ’%s’\n", argv[1]); return(1); } err = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)); if (err == -1) { perror("connect: connect() failed"); return(1); } while (1) { FD_ZERO(&input_fdset); FD_SET(STDIN_FILENO, &input_fdset); FD_SET(sock_fd, &input_fdset); if (select(sock_fd+1, &input_fdset, NULL, NULL, NULL) == -1) perror("connect: select() failed"); if (FD_ISSET(STDIN_FILENO, &input_fdset)) { if (fgets(buffer, 256, stdin) == NULL) { printf("connect: Closing socket.\n"); break; } length = strlen(buffer); send(sock_fd, buffer, length, 0); } else { length = recv(sock_fd, buffer, 256, 0); if (length == 0) {
7.2 Der TCP/IP-Client 81 82 83 84 85 86 87 88 89 90 175 printf("Connection closed by remote host.\n"); break; } write(STDOUT_FILENO, buffer, length); } } close(sock_fd); return(0); } Nach dem Auswerten der Kommandozeilenparameter in den Zeilen 20 bis 31 wird in Zeile 33 ein Socket geöffnet, wiederum mit der Protokollfamilie IP (PF INET) und für verbindungsorientierte Kommunikation (SOCK STREAM). In den Zeilen 40 bis 42 werden IP-Adresse und Port-Nummer des Servers in die Adress-Struktur für den Verbindungsaufbau eingetragen. Die Verbindung zum Server-Programm erfolgt in den Zeilen 50 und 51 mit dem Aufruf der Funktion connect(). In der while()-Schleife (Zeile 58 bis 86) wird mit der Funktion select() auf Daten von stdin (Tastatur) und vom adressierten Server gewartet (siehe auch Seite 110). Die Tastatureingaben werden mit fgets() zeilenweise gelesen und mit send() zum Server-Programm übertragen (Zeile 68 bis 74). Daten, die der Socket empfängt, werden mit recv() eingelesen und mit write() ausgegeben (Zeile 78 bis 84). Zum Test und zur Demonstration soll eine Verbindung zum FTP-Server aufgebaut und dessen Online-Hilfe aufgerufen werden. Die Benutzereingaben sind schräg dargestellt – vorausgesetzt, der FTP-Server ist wie in Abschnitt 7.1.2 beschrieben aktiviert (Benutzereingaben sind schräg dargestellt): > connect 127.0.0.1 21 220 toshi.at-home FTP server (Version 6.2/OpenBSD/Linux-0.11) help 214- The following commands are recognized (* unimplemented). USER PORT STOR MSAM* RNTO NLST PASS PASV APPE MRSQ* ABOR SITE ACCT* TYPE MLFL* MRCP* DELE SYST SMNT* STRU MAIL* ALLO CWD STAT REIN* MODE MSND* REST XCWD HELP QUIT RETR MSOM* RNFR LIST NOOP 214 Direct comments to ftp-bugs@toshi.at-home. quit 221 Goodbye. Connection closed by remote host. MKD XMKD RMD XRMD PWD XPWD CDUP XCUP STOU SIZE MDTM
176 7 Netzwerkprogrammierung Neben dem FTP-Server gibt es noch andere Dienste, mit denen Sie Klartext“ spre” chen können: Versuchen Sie einmal, den SMTP-Dienst (Port 25) oder den UserInformationsdienst (Port 79) zu kontaktieren. Bei Letzterem müssen Sie entweder einen Benutzernamen eingeben oder einfach RETURN drücken. Je nach Übertragungsmedium (Ethernet oder analoges Modem) gibt es unterschiedliche Obergrenzen für die mit einem send()-Aufruf übertragenen Daten. Es kann also sein, dass send() nicht alle angegebenen Daten überträgt. Der Rückgabewert von send() liefert die Anzahl der tatsächlich gesendeten Bytes. Um sicherzugehen, dass alle Bytes zum Server übertragen werden, muss der Rückgabewert mit der zu sendenden Anzahl an Bytes verglichen und die Differenz ggf. mit einem zweiten send()-Aufruf übertragen werden. 7.2.3 Rechnernamen in IP-Adressen umwandeln Bislang haben wir das Ziel immer in Form einer IP-Adresse angegeben, doch diese ist ja nur in seltenen Fällen bekannt. Meistens kennt man den Rechnernamen oder den Domain-Namen z. B. in der Form www.hanser.de“. ” Zur Auflösung des Domain-Namens in eine IP-Adresse – ggf. unter Zuhilfenahme eines Domain Name Servers – dient die Funktion gethostbyname(): struct hostent *gethostbyname(char *name); die als Rückgabewert einen Zeiger auf die Struktur hostent liefert: struct { char char int int char }; hostent *h_name; **h_aliases; h_addrtype; h_length; **h_addr_list; /* /* /* /* /* official name of host */ alias list */ host address type */ length of address */ list of addresses */ Neben den Elementen dieser Struktur ist in den Include-Dateien auch das Element h addr definiert: #define h_addr h_addr_list[0] h addr zeigt somit auf den ersten Eintrag der list of addresses“ h addr list. ” Dieser ist in der Struktur zwar als Zeiger auf char deklariert, zeigt aber im Falle einer IP-Adresse auf eine Struktur vom Typ struct in addr, wie sie als drittes Element in der bereits beschriebenen Struktur sockaddr in enthalten ist.
7.2 Der TCP/IP-Client 177 Das folgende Programm erwartet als Kommandozeilenparameter einen Domainoder Rechnernamen und ermittelt die dazugehörige IP-Adresse. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /* ip-lookup.c - IP-Adresse einer Domain holen */ # # # # include include include include <stdio.h> <string.h> <arpa/inet.h> <netdb.h> int main(int argc, char *argv[]) { struct hostent *host; struct in_addr *host_ip; if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) { fprintf(stderr, "Usage: ip-lookup domain-name\n"); return(1); } host = gethostbyname(argv[1]); if (host == NULL) { herror("connect2: Can’t get IP-address"); return(1); } host_ip = (struct in_addr *) host->h_addr; printf("Hostname:\t%s\n", host->h_name); printf("IP-Address:\t%s\n", inet_ntoa(*host_ip)); return(0); } Das Programm übergibt den Kommandozeilenparameter (also den DomainNamen) an die Funktion gethostbyname(), diese liefert dann die zugehörige hostent-Struktur (Zeile 21). In den Zeilen 29 und 30 wird dann der offizielle“ ” Name der Domain sowie ihre IP-Adresse ausgegeben. Da die Struktur hostent die IP-Adresse nur in binärer Form enthält, wird diese mit Hilfe der Funktion inet ntoa() zuvor in eine Zeichenkette umgewandelt.
178 7 Netzwerkprogrammierung Sollten Sie einen Internet-Zugang haben und gerade online sein, können Sie mit dem Programm ip-lookup die IP-Adressen beliebiger Domains im Internet erfragen:1 > ip-lookup www.hanser.de Hostname: www.hanser.de IP-Address: 213.183.13.138 Bitte beachten Sie, dass die Struktur hostent bei weiteren Aufrufen von gethostbyname() unter Umständen überschrieben wird. Daher sollten die benötigten Daten (beispielsweise die IP-Adresse) für die weitere Verwendung – wie den Verbindungsaufbau mit connect() – in eine lokale Variable kopiert werden: struct hostent *server; struct in_addr *server_ip; struct sockaddr_in server_addr; server = gethostbyname("www.hanser.de"); server_ip = (struct in_addr *) server->h_addr; server_addr.sin_addr.s_addr = server_ip->s_addr; 7.3 Server-Programme Nachdem gezeigt wurde, wie ein TCP/IP-Client programmiert wird, soll in den folgenden Abschnitten die Arbeitsweise eines entsprechenden Server-Programms erläutert werden. 7.3.1 Die Funktionsweise eines Servers Ebenso wie jeder Client benötigt auch ein Server-Programm zunächst einen Socket als Basis für die Netzwerkkommunikation. Die anschließenden Schritte sind jedoch ganz anders als bei einem Client-Programm; Abbildung 7.7 zeigt den prinzipiellen Ablauf mit den zugehörigen Funktionen. Hier ist die Beschreibung der einzelnen Funktionsaufrufe im Detail: int bind(int sock_fd, struct sockaddr *my_addr, socklen_t addrlen); Der bind()-Aufruf ist ähnlich dem connect()-Aufruf eines Client-Programms, mit dem Unterschied, dass bind() als zweiten Parameter die eigene Adresse (IPAdresse + Port-Nummer) erwartet. War der Aufruf erfolgreich, liefert die Funk1 Es gibt natürlich bereits ein Tool unter Linux, das dies (und noch mehr) kann: nslookup.
7.3 Server-Programme 179 socket() Öffnen eines Sockets ? bind() Verknüpfen des Sockets mit einem IP-Port des Rechners ? listen() Auf ein connect() eines Client-Programms warten ? accept() Annehmen der Verbindungsanfrage des Client-Programms ? send(), recv() Datenaustausch Abbildung 7.7: Prinzipieller Ablauf eines TCP/IP-Servers tion eine 0 als Rückgabewert. Im Fehlerfall – beispielsweise wenn der Port bereits belegt ist – gibt bind() eine –1 zurück. Um einen Socket an einen Port zu binden“, muss das Programm (je nach ” Portnummer) ggf. über root-Rechte verfügen! Nach Beendigung des Programms bleibt der Port noch eine Zeit lang belegt“ ” Der nächste erforderliche Funktionsaufruf ist listen(): int listen(int sock_fd, int backlog); Der Parameter backlog gibt an, wie lang die Warteschlange mit connect()Anforderungen an diesem Port maximal werden kann. Auch die Funktion listen() gibt bei einem Fehler –1 zurück, sonst 0. Zur Annahme einer Verbindung von einem Client ist schließlich noch die Funktion accept() erforderlich: int accept(int sock_fd, struct sockaddr *addr, socklen_t *addrlen); Die Funktion accept() wartet, bis die connect()-Anforderung eines ClientProgramms eintrifft. Dann trägt accept() die Adresse des anklopfenden“ Cli”
180 7 Netzwerkprogrammierung ents in die Adress-Struktur ein, die als zweiter Parameter angegeben ist. Die Variable, auf die der dritte Parameter zeigt, muss vor dem accept()-Aufruf mit der Länge der Adress-Struktur initialisiert werden. Nach dem Funktionsaufruf enthält die Variable die tatsächliche Länge der Client-Adressinformation. accept() liefert als Rückgabewert den Dateideskriptor eines neuen Sockets, bzw. –1 im Fehlerfall. Dieser neue Socket stellt den Kommunikationskanal zu dem Client dar, dessen Verbindungsanforderung angenommen wurde. Die Kommunikation mittels send() und recv() findet also nicht über den zunächst mit socket() geöffneten Socket statt, sondern über den von accept() gelieferten Socket. Indem für jede mit accept() angenommene Verbindung ein eigener Socket geöffnet wird, kann der Server mit mehreren Clients parallel kommunizieren – sonst wären Webserver ja undenkbar. Für gewöhnlich wird für jede dieser Verbindungen ein eigener Kind-Prozess gestartet, während der Eltern-Prozess erneut mit accept() in Bereitschaft geht, weitere Verbindungen aufzubauen. 7.3.2 Ein interaktiver TCP/IP-Server Das folgende Server-Programm stellt das Pendant zu dem TCP/IP-Client aus Abschnitt 7.2.2 dar. Als Kommandozeilenparameter muss die Port-Nummer angegeben werden, unter der das Server-Programm seinen Dienst anbietet. Die PortNummer darf natürlich nicht bereits von einem anderen Dienst belegt sein. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* server.c - interaktiver Netzwerk-Server */ # # # # # # # # include include include include include include include include <stdio.h> <string.h> <stdlib.h> <unistd.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> void err_exit(char *message) { perror(message); exit(1); } int main(int argc, char *argv[]) { static char buffer[256];
7.3 Server-Programme 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 int sock_fd, client_fd, port, err, length; socklen_t addr_size; struct sockaddr_in my_addr, client_addr; fd_set input_fdset; if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) { fprintf(stderr, "Usage: server port\n"); return(1); } if (sscanf(argv[1], "%d", &port) != 1) { fprintf(stderr, "server: Bad port number.\n"); return(1); } /*--- socket() ---*/ sock_fd = socket(PF_INET, SOCK_STREAM, 0); if (sock_fd == -1) err_exit("server: Can’t create new socket"); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(port); my_addr.sin_addr.s_addr = INADDR_ANY; /*--- bind() ---*/ err = bind(sock_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)); if (err == -1) err_exit("server: bind() failed"); /*--- listen() ---*/ err = listen(sock_fd, 1); if (err == -1) err_exit("server: listen() failed"); /*--- accept() ---*/ addr_size = sizeof(struct sockaddr_in); client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_size); if (client_fd == -1) err_exit("server: accept() failed"); printf("I’m connected from %s\n", inet_ntoa(client_addr.sin_addr)); 181
182 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 7 Netzwerkprogrammierung while (1) { FD_ZERO(&input_fdset); FD_SET(STDIN_FILENO, &input_fdset); FD_SET(client_fd, &input_fdset); if (select(client_fd+1, &input_fdset, NULL, NULL, NULL) == -1) err_exit("server: select() failed"); if (FD_ISSET(STDIN_FILENO, &input_fdset)) { if (fgets(buffer, 256, stdin) == NULL) { printf("server: Closing socket.\n"); break; } length = strlen(buffer); send(client_fd, buffer, length, 0); } else { length = recv(client_fd, buffer, 256, 0); if (length == 0) { printf("Connection closed by remote host.\n"); break; } write(STDOUT_FILENO, buffer, length); } } close(client_fd); close(sock_fd); return(0); } Nach Auswertung der Kommandozeilenparameter in Zeile 28 bis 38 wird in Zeile 40 zunächst ein Socket geöffnet. In den Zeilen 44 bis 46 wird dann die AdressStruktur initialisiert. Durch die Angabe der Konstanten INADDR ANY für die IPAdresse wird der Socket beim folgenden bind()-Aufruf (Zeile 49) automatisch mit allen IP-Adressen des Computers1 verknüpft. Ist dies nicht erwünscht, muss hier explizit die IP-Adresse angegeben werden, unter der der Dienst eingerichtet werden soll. 1 Ist der Computer mit mehreren Netzwerk-Interfaces ausgestattet – beispielsweise eine Netzwerkkarte und ein WLAN-Interface –, haben diese in der Regel unterschiedliche IP-Adressen.
7.3 Server-Programme 183 In den Zeilen 55 bis 64 folgen die Funktionsaufrufe listen() und accept(), mit denen die Kontaktaufnahme“ durch einen Client vorbereitet wird. Nach einem ” connect() durch ein Client-Programm kehrt die Funktion accept() mit dem Dateideskriptor des neuen Sockets zurück, und die Struktur client addr wurde mit der IP-Adresse und Port-Nummer des Client-Programms gefüllt. Die Funktion inet ntoa() wandelt die binäre IP-Adresse in eine Zeichenkette um (Zeilen 65 und 66). Wie bereits bei dem in Abschnitt 7.2.2 vorgestellten TCP/IP-Client ermöglicht auch hier die while()-Schleife in den Zeilen 68 bis 96 die bidirektionale Kommunikation über den Socket. Das Belegen eines reservierten Ports1 mit einem Dienst erfordert root-Rechte. Damit das Programm trotzdem von einem normalen“ Benutzer ausgeführt werden ” kann, sind neben dem Kompilieren weitere Schritte erforderlich (vgl. auch Abschnitt 9.1.1): > gcc server.c -o server > su Kennwort: # chown root server # chmod a+s server # exit Jetzt kann das Programm mit Angabe einer (freien) Port-Nummer gestartet werden. Zum Test soll hier ein HTTP-Server vorgetäuscht“ werden – Voraussetzung ” ist, dass nicht bereits ein solcher Server (z. B. der Apache Webserver) läuft. Als Client-Programm dient ein Browser, z. B. Firefox: > server 80 Jetzt firefox http://localhost/“ in einem anderen Fenster aufrufen. ” I’m connected from 127.0.0.1 GET / HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (X11; U; Linux i686 (x86 64); en-US; rv:1.7.10) Gecko/20050715 Firefox/1.0.6 SUSE/1.0.6-16 Accept: text/xml,application/xml,application/xhtml+xml, text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive 1 Die Organisation IANA vergibt und registriert Portnummern für bestimmte Dienste wie z. B. Port 80 für HTTP. Unregistrierte Ports wie beispielsweise 30.000 können ohne besondere Benutzerrechte belegt werden.
184 7 Netzwerkprogrammierung An dieser Stelle können Sie das Programm mit Ctrl-D beenden oder eine HTTPAntwort eingeben, was jedoch relativ mühsam ist und daher echten“ Webser” vern überlassen werden sollte. Alternativ kann das Server-Programm natürlich auch mit dem interaktiven Client-Programm aus Abschnitt 7.2.2 kommunizieren. Auf diese Weise lässt sich eine Chat“-Verbindung aufbauen, über die sich zwei ” Benutzer miteinander unterhalten können. 7.3.3 Ein kleiner Webserver Der Nutzen eines interaktiven TCP/IP-Servers, wie er im vorigen Abschnitt dargestellt wurde, ist relativ eingeschränkt. Typische Server-Programme erlauben eine Kommunikation mit mehreren Client-Programmen gleichzeitig. Wie man dies realisiert, soll im Folgenden anhand eines kleinen HTTP-Servers demonstriert werden. HTTP-Grundlagen Eine typische HTTP-Anfrage eines Browsers (Firefox) war ja bereits im vorherigen Abschnitt zu sehen. Viele der Angaben, die der Browser seiner Anfrage mit auf den Weg gibt, werden wir nicht benötigen und auch nicht auswerten. Der grundsätzliche Aufbau einer HTTP-Anfrage und einer HTTP-Antwort sind in Abbildung 7.8 dargestellt. HTTP-Anfrage: HTTP-Antwort: Anforderungszeile Antwortzeile Kopfzeile 1 (optional) Kopfzeile 1 (optional) Kopfzeile 2 (optional) .. . Kopfzeile 2 (optional) .. . Kopfzeile n (optional) Kopfzeile n (optional) <Leerzeile> <Leerzeile> Anfrage-Körper (optional) Antwort-Körper (optional) Abbildung 7.8: Prinzipieller Aufbau von HTTP-Anfragen und -Antworten Eine HTTP-Anforderungszeile besteht dabei aus drei, durch Leerzeichen getrennte Elemente: Methode, Pfad, Protokoll. Beispiel: GET /index.html HTTP/1.0
7.3 Server-Programme 185 Mögliche Methoden sind: GET – Daten (z. B. HTML-Datei) vom Server anfordern. HEAD – Nur die Kopfzeilen vom Server anfordern. Damit kann beispielsweise die Länge der Daten und das Format erfragt werden, ohne die Daten selbst zu übertragen. POST – Die Inhalte eines HTML-Formulars an den Server übertragen und die Antwort anfordern. In der Regel enthält eine HTTP-Anfrage mindestens eine Kopfzeile der Form: Host: www.hanser.de Ab HTTP 1.1 ist diese Kopfzeile Pflicht. Sie ermöglicht es, mehrere WWWAdressen auf einem Server (also mit gleicher IP-Adresse) zu verwalten, da der Server an der Host-Angabe erkennen kann, auf welche WWW-Adresse sich der in der Anforderungszeile angegebene Pfad bezieht. Die Methoden GET und HEAD benötigen keinen Anfrage-Körper (Body), während er bei der Methode POST mit den Inhalten des HTML-Formulars gefüllt ist. In diesem Fall ist mindestens eine weitere Kopfzeile erforderlich, die die Länge des Anfrage-Körpers angibt. Die Antwortzeile des Servers enthält – wie die Anforderungszeile – drei Elemente. Hier sind es: Protokoll, Status, Textmeldung. Beispiel: HTTP/1.0 200 OK oder HTTP/1.0 404 Not Found Die möglichen Statuswerte sind der detaillierten Protokollbeschreibung zu entnehmen, wie sie z. B. im Internet zu finden ist. Mit den beiden hier angegebenen Werten kennen Sie aber bereits die wichtigsten. Ein Server, der etwas auf sich hält, sollte danach mindestens zwei Kopfzeilen ausgeben: die Art des Anwort-Körpers und dessen Länge, z. B.: Content-type: text/html Content-length: 1374 Nach einer Leerzeile (CR + LF!) folgt dann der eigentliche Körper, also beispielsweise die HTML-Datei, die JPEG-Grafik, ... Hier das Ganze (HTTP-Anfrage und -Antwort) an einem Beispiel: Browser: GET /index.html HTTP/1.0 Host: www.kein-inhalt.de Leerzeile
186 7 Netzwerkprogrammierung Server: HTTP/1.0 200 OK Content-type: text/html Content-length: 38 Leerzeile <html><body>Leere Seite.</body></html> Eine ausführlichere Beschreibung des HTTP-Standards finden Sie im Internet z. B. unter [14]. Das Programm Die Beschreibung des HTTP war zwar sehr knapp gehalten und auf das Notwendigste beschränkt, diese Informationen reichen aber aus, um damit den folgenden kleinen HTTP-Server zu programmieren. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* webserver.c - minimalistischer HTTP-Server */ # # # # # # # # # # include include include include include include include include include include # # # # define define define define <stdio.h> <string.h> <unistd.h> <stdlib.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> <sys/stat.h> <signal.h> MY_PORT 80 N_CONNECTIONS 20 HTML_PATH "." DEFAULT_FILE "index.html" void err_exit(char *message) { perror(message); exit(1); } int get_line(int sock_fd, char *buffer, int length) { int i;
7.3 Server-Programme 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 i = 0; while ((i < length-1) && (recv(sock_fd, &(buffer[i]), 1, 0) == 1)) if (buffer[i] == ’\n’) break; else i++; if ((i > 0) && (buffer[i-1] == ’\r’)) i--; buffer[i] = ’\0’; return(i); } int is_html(char *filename) { if (strcmp(&(filename[strlen(filename)-5]), ".html") == 0) return(1); if (strcmp(&(filename[strlen(filename)-4]), ".htm") == 0) return(1); return(0); } size_t file_size(char *filename) { struct stat file_info; if (stat(filename, &file_info) == -1) return(0); return(file_info.st_size); } void http_service(int client_fd) { char buffer[256], cmd[8], url[128], *filename; int length; FILE *stream; if (get_line(client_fd, buffer, 256) == 0) return; if (sscanf(buffer, "%7s %127s", cmd, url) < 2) return; while (get_line(client_fd, buffer, 256) > 0); 187
188 7 Netzwerkprogrammierung 75 if ((strcmp(cmd, "GET") != 0) 76 && (strcmp(cmd, "HEAD") != 0)) 77 return; 78 79 filename = &(url[1]); 80 if (strlen(filename) == 0) 81 filename = DEFAULT_FILE; 82 83 if ((stream = fopen(filename, "r")) == NULL) 84 { 85 send(client_fd, "HTTP/1.0 404 Not Found\r\n" 86 "Content-type: text/html\r\n" 87 "Content-length: 91\r\n\r\n" 88 "<html><head><title>Error</title></head>" 89 "<body><hr><h2>File not found.</h2><hr>" 90 "</body></html>", 162, 0); 91 return; 92 } 93 send(client_fd, "HTTP/1.0 200 OK\r\n", 17, 0); 94 95 if (is_html(filename)) 96 send(client_fd, "Content-type: text/html\r\n", 25, 0); 97 sprintf(buffer, "Content-length: %ld\r\n\r\n", 98 file_size(filename)); 99 send(client_fd, buffer, strlen(buffer), 0); 100 if (strcmp(cmd, "GET") == 0) 101 while (!feof(stream)) 102 { 103 length = fread(buffer, 1, 256, stream); 104 if (length > 0) 105 send(client_fd, buffer, length, 0); 106 } 107 fclose(stream); 108 return; 109 } 110 111 /*--------------- Hauptprogramm ---------------*/ 112 113 int main() 114 { 115 int sock_fd, client_fd, err, pid; 116 struct sockaddr_in my_addr, client_addr; 117 socklen_t addr_size; 118
7.3 Server-Programme 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 189 sock_fd = socket(PF_INET, SOCK_STREAM, 0); if (sock_fd == -1) err_exit("webserver: Can’t create new socket"); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(MY_PORT); my_addr.sin_addr.s_addr = INADDR_ANY; err = bind(sock_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)); if (err == -1) err_exit("webserver: bind() failed"); setuid(getuid()); err = listen(sock_fd, N_CONNECTIONS); if (err == -1) err_exit("webserver: listen() failed"); if (chdir(HTML_PATH) != 0) err_exit("webserver: Can’t set HTML path"); signal(SIGCHLD, SIG_IGN); printf("Type Ctrl-C to stop.\n"); while (1) { addr_size = sizeof(struct sockaddr_in); client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_size); if (client_fd == -1) err_exit("webserver: accept() failed"); if ((pid = fork()) == -1) { fprintf(stderr, "webserver: fork() failed.\n"); return(1); } else if (pid == 0) /* Kind-Prozess */ { close(sock_fd); http_service(client_fd); shutdown(client_fd, SHUT_RDWR);
190 163 164 165 166 167 168 169 170 7 Netzwerkprogrammierung close(client_fd); return(0); } close(client_fd); } return(0); /* wird nie erreicht */ } Um den Webserver als normaler“ Benutzer starten zu können, muss auch hier ” wiederum als Besitzer des Programms root“ eingestellt und das s“-Bit gesetzt ” ” werden (vgl. Abschnitt 9.1.1). Danach kann man das Programm ohne Kommandozeilenparameter aufrufen. Der Server läuft nun so lange, bis der Prozess durch Eingabe von Ctrl-C oder durch einen kill-Befehl beendet wird. Um dem Webserver eine HTML-Seite zu entlocken, sollten Sie entweder eine HTML-Datei in das aktuelle Verzeichnis kopieren oder ein Verzeichnis, das HTML-Dateien enthält, in Zeile 18 als HTML PATH“ eintragen. Als URL geben Sie dann im Browser ” http://localhost/HTML-Datei“ ein. ” Betrachten wir zunächst das Hauptprogramm ab Zeile 113. Zu Beginn wird gemäß dem bereits vorgestellten Schema zunächst ein Socket geöffnet und anschließend mit bind() mit dem Port 80 (Konstante MY PORT) verknüpft. In Zeile 132 erfolgt dann der Aufruf setuid(getuid()). Er bewirkt, dass das Programm nach der Verknüpfung mit dem Port die über das s“-Bit verliehenen root” Rechte wieder abgibt. Dadurch könnte das Programm auch bei einer Fehlfunktion keinen ernsthaften Schaden mehr anrichten (siehe auch Abschnitt 7.5). Erst danach erfolgt der listen()-Aufruf, um den Port in Verbindungsbereitschaft zu bringen. Mit Hilfe der Funktion signal() wird in Zeile 141 eingestellt, dass Kind-Prozesse nach Beendigung sofort aus dem Speicher und aus der Prozessliste entfernt werden, statt abzuwarten, bis der Eltern-Prozess den Exit-Status der Kind-Prozesse abfragt. Da bei unserem Webserver-Programm jede HTTP-Anfrage einen KindProzess startet, würde andernfalls eine Unmenge so genannter Zombie-Prozesse1 erzeugt. In der while()-Schleife (Zeilen 145 bis 167) werden Verbindungsanforderungen von Client-Programmen (Browsern) mit accept() angenommen und jeweils in einem eigenen Kind-Prozess mit dem Unterprogramm http service() abgearbeitet. Danach wird die Verbindung zum Client mit Hilfe der Funktion shutdown() beendet (Zeile 162) und anschließend der Socket geschlossen (Zeile 163). Die Funktion http service() ist in den Zeilen 64 bis 109 definiert. Sie liest zunächst die HTTP-Anforderung ein und prüft auf die Kommandos GET“ und ” 1 Das sind Prozesse, die eigentlich beendet sind, aber noch darauf warten, ihren Status zurückzumelden.
7.4 Das User Datagram Protocol (UDP) 191 HEAD“ (andere werden zur Zeit nicht unterstützt). Schlägt das Öffnen der ange” forderten Datei in Zeile 83 fehl, so wird eine entsprechende HTTP-Fehlermeldung an den Client zurückgeschickt (Zeile 85 bis 91). Wenn die angegebene Datei existiert, wird eine positive Rückmeldung generiert (Zeile 94) und geprüft, ob es sich um eine HTML-Datei handelt (Zeile 95). Dies wird dem Client dann durch die Information Content-type: text/html“ angezeigt. In jedem Fall wird die ” Länge der Datei mit Hilfe der Kopfzeile Content-length:“ übermittelt. Han” delte es sich bei der Anforderung um ein HEAD“, ist die Kommunikation an ” dieser Stelle beendet. Bei einer GET“-Anforderung folgt die Übertragung der ei” gentlichen Datei (Zeile 101 bis 106). 7.4 Das User Datagram Protocol (UDP) In den Abschnitten 7.2 bis 7.3.3 haben wir verbindungsorientierte Sockets auf Basis des Protokolls TCP verwendet. Der Vorteil dabei ist, dass eine gesicherte Verbindung zwischen zwei Netzwerk-Ports hergestellt wird, bei der keine Daten ver” loren gehen“ oder in falscher Reihenfolge ankommen können. Im Gegensatz dazu besteht beim Versenden von UDP-Paketen keine Garantie, dass diese wirklich ankommen. Dennoch bietet UDP-basierte Netzwerkkommunikation zwei Eigenschaften, die sehr nützlich oder sogar notwendig sein können: Der Datenstrom“ wird nicht blockiert, falls ein Paket nicht oder nur fehlerhaft ” angekommen ist. Dies ist für zeitkritische Anwendungen wie Internettelefonie wichtig. UDP erlaubt es, Nachrichten an mehrere (Multicast) oder an alle (Broadcast) Netzwerkteilnehmer zu schicken. Das ist mit TCP nicht möglich! Aus diesem Grund zeigen wir in den folgenden Abschnitten, wie die Kommunikation über UDP funktioniert und wie Broadcast- und Multicast-Nachrichten versendet werden. 7.4.1 UDP-Nachrichten senden Bereits beim Öffnen des Sockets muss festgelegt werden, ob das Protokoll TCP oder UDP verwendet werden soll (vgl. Abschnitt 7.1.4). Für UDP muss man als Typ SOCK DGRAM“ angeben: ” int sock fd = socket(PF INET, SOCK DGRAM, 0); Bei dem Protokoll UDP werden einzelne Pakete versendet, ohne dass zuvor eine Verbindung“ zwischen Client und Server aufgebaut wird. Daher entfällt bei der ” UDP-Kommunikation der Funktionsaufruf connect(). Um das Ziel (IP-Adresse und Port) beim Versenden eines Pakets angeben zu können, müssen Sie bei UDP die Funktion
192 7 Netzwerkprogrammierung ssize_t sendto(int sockfd, void *buff, size_t n, int flags, struct sockaddr *addr, socklen_t addr_len); verwenden, die im Gegensatz zu send() den Parameter addr für die Zieladresse enthält. Analog dazu gibt es auch eine Funktion zum Empfangen von UDPPaketen: ssize_t recvfrom(int sockfd, void *buff, size_t n, int flags, struct sockaddr *addr, socklen_t addr_len); Hier dient der Parameter addr als Zeiger auf einen Platzhalter“, der mit den ” Adressinformationen des Absenders der Nachricht gefüllt wird. Das folgende Beispielprogramm udp-client“ verwendet beide Funktionen, um eine Nachricht an ” einen UDP-Server zu schicken und eine Antwort vom Server zu empfangen. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* udp-client.c - Text an UDP-Server schicken */ # # # # # # # # # include include include include include include include include include <stdio.h> <unistd.h> <string.h> <sys/poll.h> <sys/types.h> <sys/socket.h> <sys/time.h> <netinet/in.h> <arpa/inet.h> # define BUF_SIZE 1000 # define TIMEOUT 1000 /* Millisekunden */ int main(int argc, char *argv[]) { int sock_fd, port, length, err; struct sockaddr_in server_addr, from_addr; socklen_t addr_size; struct pollfd pollfd; static char buffer[BUF_SIZE]; if (argc != 4) { fprintf(stderr, "Usage: udp-client ip-addr port message\n"); return(1);
7.4 Das User Datagram Protocol (UDP) 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 193 } if (sscanf(argv[2], "%d", &port) != 1) { fprintf(stderr, "udp-client: bad port number ’%s’\n", argv[2]); return(1); } /*--- Socket öffnen ---*/ sock_fd = socket(PF_INET, SOCK_DGRAM, 0); if (sock_fd == -1) { perror("udp-client: Can’t create new socket"); return(1); } /*--- Zieladresse ---*/ server_addr.sin_family = AF_INET; server_addr.sin_port = htons(port); err = inet_aton(argv[1], &(server_addr.sin_addr)); if (err == 0) { fprintf(stderr, "udp-client: Bad IP-Address ’%s’\n", argv[1]); return(1); } /*--- Nachricht senden ---*/ length = sendto(sock_fd, argv[3], strlen(argv[3]), 0, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)); if (length != strlen(argv[3])) perror("udp-client: sendto() failed"); /*--- auf Antwort warten ---*/ pollfd.fd = sock_fd; /* Polling vorbereiten */ pollfd.events = POLLIN | POLLPRI; err = poll(&pollfd, 1, TIMEOUT); if (err < 0) perror("udp-client: poll() failed"); else if (err == 0) printf("<No answer received.>\n"); else { addr_size = sizeof(struct sockaddr_in); length = recvfrom(sock_fd, buffer, BUF_SIZE-1, 0,
194 75 76 77 78 79 80 81 82 83 84 85 86 87 88 7 Netzwerkprogrammierung (struct sockaddr *)&from_addr, &addr_size); if (length == -1) perror("udp-client: recvfrom() failed"); else { buffer[length] = ’\0’; printf("Response from %s: %s\n", inet_ntoa(from_addr.sin_addr), buffer); } } close(sock_fd); return(0); } Nach dem Öffnen des Sockets (Zeile 40) trägt das Programm die als Kommandozeilenparameter angegebene IP-Adresse und Portnummer in die Struktur server addr ein (Zeile 47 bis 49). An diese Adresse wird dann der als dritter Kommandozeilenparameter übergebene Text gesendet (Zeile 57 bis 59). Anschließend wartet das Programm maximal 1 Sekunde auf eine Antwort vom Server. Dies geschieht mit Hilfe der Funktion poll() in den Zeilen 64 bis 66. poll() arbeitet ähnlich wie die Funktion select(), die unter anderem in unseren TCPBeispielen verwendet und auf Seite 110 beschrieben wurde. Um das Programm zu testen, können Sie den Netzwerkdienst Echo“ (UDP” Port 7) verwenden. Dieser Dienst ist im Dämon xinetd“ enthalten und lässt sich, ” wie in Abbildung 7.9 gezeigt, aktivieren (siehe auch Seite 168). Der Echo“-Dienst sendet alle empfangenen Pakete an den jeweiligen Absender ” zurück. In Verbindung mit unserem Beispielprogramm sieht das dann so aus: > gcc udp-client.c -o udp-client > ./udp-client 127.0.0.1 7 "Dies ist ein Test." Response from 127.0.0.1: Dies ist ein Test. 7.4.2 Der UDP-Server Im Vergleich zu dem in Abbildung 7.7 dargestellten Ablauf von Funktionsaufrufen eines TCP-Servers entfallen beim UDP-Server die Funktionen listen() und accept(), die man nur für verbindungsorientierte Kommunikation benötigt. Außerdem werden zum Senden und Empfangen von Paketen die oben beschriebenen Funktionen sendto() und recvfrom() verwendet. Das folgende Programm realisiert einen UDP-Server, der alle empfangenen Pakete als Text ausgibt und einen Bestätigungstext an den Absender schickt. Empfängt der Server ein UDPPaket mit dem Inhalt quit“, wird das Programm beendet. ”
7.4 Das User Datagram Protocol (UDP) Abbildung 7.9: Aktivierung des Echo“-Dienstes (UDP) ” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /* udp-server.c - Server für UDP-Datagramme */ # # # # # # # include include include include include include include <stdio.h> <string.h> <unistd.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> # define BUF_SIZE 1000 int main(int argc, char *argv[]) { int sock_fd, client_fd, port, err, length, stop; struct sockaddr_in my_addr, client_addr; socklen_t addr_size; static char buffer[BUF_SIZE]; if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) { 195
196 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 7 Netzwerkprogrammierung fprintf(stderr, "Usage: udp-server port\n"); return(1); } if (sscanf(argv[1], "%d", &port) != 1) { fprintf(stderr, "udp-server: Bad port number ’%s’.\n", argv[1]); return(1); } /*--- Socket öffnen ---*/ sock_fd = socket(PF_INET, SOCK_DGRAM, 0); if (sock_fd == -1) { perror("udp-server: Can’t create new socket"); return(1); } /*--- Socket an Port binden ---*/ my_addr.sin_family = AF_INET; my_addr.sin_port = htons(port); my_addr.sin_addr.s_addr = INADDR_ANY; err = bind(sock_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)); if (err == -1) { perror("udp-server: bind() failed"); return(1); } stop = 0; while (!stop) /*--- so lange, bis ’quit’ empf. */ { /*--- Paket empfangen ---*/ addr_size = sizeof(struct sockaddr_in); length = recvfrom(sock_fd, buffer, BUF_SIZE-1, 0, (struct sockaddr *)&client_addr, &addr_size); if (length == -1) perror("udp-server: recvfrom() failed"); else { /*--- Paket ausgeben ---*/ buffer[length] = ’\0’; printf("Datagram from %s:\n%s\n", inet_ntoa(client_addr.sin_addr), buffer); if (strcmp(buffer, "quit") == 0)
7.4 Das User Datagram Protocol (UDP) 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 197 { strcpy(buffer, "Server stopped."); stop = 1; } else strcpy(buffer, "Message received."); /*--- Antwort senden ---*/ length = sendto(sock_fd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)); if (length < strlen(buffer)) perror("udp-server: sendto() failed"); } } close(client_fd); close(sock_fd); return(0); } Das Programm erwartet als Kommandozeilenparameter die Nummer des Ports, an dem der Server Pakete empfangen soll. Wenn Sie hier eine unregistrierte Portnummer (z. B. 30000) angeben, sollte das Programm auch ohne root-Rechte laufen: > gcc udp-server.c -o udp-server > ./udp-server 30000 Wenn Sie jetzt in einem anderen Terminal-Fenster das UDP-Client-Programm auf den Port 30000 anwenden, erhalten Sie die entsprechenden Empfangsbestätigungen: > ./udp-client 127.0.0.1 Response from 127.0.0.1: > ./udp-client 127.0.0.1 Response from 127.0.0.1: 30000 Test Message received. 30000 quit Server stopped. 7.4.3 Pakete an alle Teilnehmer senden: Broadcast Wie bereits oben beschrieben, ist es mit UDP möglich, eine Nachricht an alle Teilnehmer eines lokalen Netzwerks zu schicken.1 Dieses Verfahren wird beispielsweise für die Suche von Diensten im Netzwerk verwendet, wenn die IP-Adresse des Servers noch nicht bekannt ist. Das Senden und Empfangen von Broadcast-Paketen geschieht mit den gleichen Funktionen wie das Übertragen von normalen“ Datagrammen, jedoch mit einer ” 1 Damit solche Nachrichten nicht über das Internet an jeden Computer gehen, der gerade online ist, werden Broadcast-Pakete von Netzwerkroutern nicht weitergeleitet und bleiben daher auf das Local Area Network (LAN) beschränkt.
198 7 Netzwerkprogrammierung speziellen Zieladresse. Jeder Netzwerkschnittstelle, die eine gültige IP-Adresse besitzt, ist automatisch eine entsprechende Broadcast-Adresse zugeordnet. Die Schnittstelle empfängt UDP-Pakete, die an diese Adresse geschickt werden, so als ob sie an die eigentliche IP-Adresse geschickt wurden. Eine Broadcast-Adresse ist dadurch gekennzeichnet, dass alle Bits des Host-Teils der IP-Adresse – also der Teil, der bei allen Teilnehmern eines Netzwerks unterschiedlich ist – auf 1 gesetzt sind. Beispiel: Haben die Teilnehmer eines lokalen Netzwerks Adressen der Form 192.168.0.xxx, so lautet die zugehörige BroadcastAdresse 192.168.0.255. Mit dem Programm ifconfig“ können Sie die IP-Adresse ” und die zugehörige Broadcast-Adresse aller Netzwerkschnittstellen anzeigen lassen: > /sbin/ifconfig eth0 Protokoll:Ethernet Hardware Adresse 00:13:D4:85:4C:89 inet Adresse:192.168.0.1 Bcast:192.168.0.255 Maske:255.255.255.0 inet6 Adresse: fe80::213:d4ff:fe85:4c89/64 Gültigkeitsbereich:Verbindung UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:19 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 Sendewarteschlangenlänge:1000 RX bytes:0 (0.0 b) TX bytes:1575 (1.5 Kb) Interrupt:225 Basisadresse:0x2000 Darüber hinaus gibt es eine besondere Broadcast-Adresse, die von der IP-Adresse der Schnittstelle unabhängig ist: 255.255.255.255. Auch wenn das Senden von Broadcast-Paketen mit den bereits beschriebenen Funktionen für normale“ Pakete geschieht, ist bei Linux eine zusätzliche Siche” rung gegen das unbeabsichtigte Verschicken eines Broadcasts eingebaut: Zuerst muss der Socket für Broadcast-Adressen freigeschaltet“ werden. Dies geschieht ” mit der Funktion setsockopt() und der Option SO BROADCAST. Um das Programm aus Abschnitt 7.4.1 in die Lage zu versetzen, Broadcast-Pakete zu verschicken, müssen Sie ab Zeile 47 folgenden Quelltext einfügen: int i = 1; if (setsockopt(sock_fd, SOL_SOCKET, SO_BROADCAST, &i, sizeof(i)) < 0) perror("udp-client: Can’t set BROADCAST option."); Wenn Ihr Rechner in einem lokalen Netzwerk z. B. die Adresse 192.168.0.1 hat, können Sie mit dem folgenden Aufruf den Echo-Dienst aller Teilnehmer des lokalen Netzwerks adressieren:
7.4 Das User Datagram Protocol (UDP) 199 > udp-client 192.168.0.255 7 Hallo Response from 192.168.0.1: Hallo Sollen die Antworten aller Rechner im Netzwerk angezeigt werden, müssen der poll()- und der recvfrom()-Aufruf so lange wiederholt werden, bis keine Antwort mehr eintrifft, so dass die poll()-Funktion einen TIMEOUT liefert. Auf diese Weise erfahren Sie dann automatisch die IP-Adressen aller Rechner im lokalen Netzwerk, bei denen der Echo-Dienst aktiviert ist! 7.4.4 Multicast-Sockets Bei den bisher vorgestellten Adressierungsarten des Internet Protokolls (IP) gab es entweder die Möglichkeit, eine Nachricht gezielt an einen Teilnehmer zu senden (Unicast), oder ein Paket an alle Netzwerkteilnehmer des lokalen Netzwerks zu schicken (Broadcast). Der aktuelle IP-Standard sieht eine weitere Möglichkeit der Adressierung vor, bei dem eine Nachricht an mehrere, aber nicht alle Teilnehmer gesendet wird: Multicast. Multicast-Adressen Multicast-Adressen liegen – anders als die Broadcast-Adressen – in einem speziellen IP-Adressbereich, der unabhängig von der Adresse der Netzwerkschnittstelle ist: 224.0.0.0 bis 239.255.255.255. Dies bedeutet, dass jeder Empfänger von Multicast-Paketen so eingerichtet sein muss, dass er neben seiner eigentlichen IPAdresse auch Pakete annimmt, die an eine oder mehrere Multicast-Adressen gerichtet sind! Wie Sockets für den Empfang von Multicast-Paketen eingerichtet werden, zeigt der nächste Abschnitt. Aber zunächst zum Senden von Multicast-Nachrichten: Für ausgehende Pakete muss in der Routing-Tabelle eingetragen sein, über welche Schnittstelle Multicast-Pakete gesendet werden, z. B. eth0: > su Kennwort: # route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0 Weitere Vorbereitungen sind für das Senden von Nachrichten an MulticastAdressen nicht notwendig. Mit dem oben beschriebenen Programm udp-client“ ” lassen sich nun Multicast-Pakete an das lokale Netzwerk verschicken. Multicast-Sockets einrichten Es sei zuerst erwähnt, dass der Linux-Kernel Multicast unterstützen muss, um einen Multicast-Socket einrichten zu können. Dies lässt sich u. a. wie folgt überprüfen:
200 7 Netzwerkprogrammierung > cat /proc/config.gz | gunzip | grep MULTICAST CONFIG IP MULTICAST=y Um ein UDP-Server-Programm Multicast-fähig zu machen, muss die gewünschte Multicast-Adresse mit Hilfe der Funktion setsockopt() mit dem Socket verknüpft werden, oder, anders ausgedrückt: Der Socket muss der Multicast-Gruppe hinzugefügt werden: # define MCAST_ADDR 224.0.0.1 struct ip_mreq mreq; mreq.imr_multiaddr.s_addr = inet_addr(MCAST_ADDR); mreq.imr_interface.s_addr = htonl(INADDR_ANY); if (setsockopt(sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { perror("setsockopt() failed"); return(1); } 7.4.5 UPnP – Universal Plug And Play Es gibt ein internationales Gremium, das daran arbeitet, Netzwerke Hot-Plug” And-Play“-fähig zu machen. So wie beim Anschließen einer USB-Maus an einen freien USB-Port automatisch die entsprechenden Treiber geladen werden und die Maus als Eingabegerät eingerichtet wird, soll das Gleiche mit Diensten in einem Netzwerk möglich sein. Der zu diesem Zweck entwickelte Standard heißt Universal Plug And Play (kurz UPnP) und basiert auf UDP-Multicast, HTTP und XML. Da auch MicrosoftTM in diesem Gremium vertreten ist, bieten WindowsTM -Rechner bereits einige Dienste über UPnP an. Auch IP-Kameras verschiedener Hersteller (z. B. Axis oder Pelco) haben UPnP implementiert, um die Kameras und deren Funktionsumfang automatisch über das Netzwerk ermitteln zu können. Um nach UPnP-Diensten im lokalen Netzwerk zu suchen, können Sie die folgende Sequenz an die Multicast-Adresse 239.255.255.250 und Port 1900 schicken: M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 2 ST: ssdp:all <Leerzeile> Sie sehen, dass es sich dabei um eine HTTP-Anfrage handelt, doch ist als Methode nicht GET, sondern M-SEARCH angegeben. Die Kopfzeile MX:“ gibt die ”
7.4 Das User Datagram Protocol (UDP) 201 Anzahl der Sekunden an, die der UPnP-Server maximal warten soll, bis er eine Antwort sendet. Damit soll verhindert werden, dass viele Server gleichzeitig antworten. Mit der Kopfzeile ST:“ wird angegeben, nach welchem Dienst gesucht ” wird (Search Target). Der Wert ssdp:all“ fordert eine Rückmeldung aller Dien” ste an, während upnp:rootdevice“ nur eine Antwort pro UPnP-Host liefert. ” Die im lokalen Netzwerk verfügbaren UPnP-Dienste reagieren auf diese Anforderung mit einer HTTP-Antwort entsprechend Abbildung 7.8, wobei die Antworten in der Regel nur aus den Kopfzeilen ohne Body bestehen. Die wichtigste Kopfzeile beginnt mit dem Schlüsselwort Location:“, gefolgt von einem URL (Uniform Re” source Locator) der Form http://IP-Adresse:Port/Pfad“. Über diesen URL können ” Sie eine XML-Datei mit einer detaillierten Beschreibung des Dienstes abrufen. Das folgende Programm fragt auf diese Weise die aktiven UPnP-Dienste ab und gibt die Antworten aus. Es benötigt keine Kommandozeilenparameter. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* upnp-search.c - UPnP-Dienste abfragen */ # # # # # # # # # include include include include include include include include include <stdio.h> <unistd.h> <string.h> <sys/poll.h> <sys/types.h> <sys/socket.h> <sys/time.h> <netinet/in.h> <arpa/inet.h> # define BUF_SIZE 20000 # define TIMEOUT 3000 /* Millisekunden */ # define UPNP_ADDR "239.255.255.250" # define UPNP_PORT 1900 int main() { int sock_fd, length, i, err; struct sockaddr_in server_addr, from_addr; socklen_t addr_size; struct pollfd pollfd; static char buffer[BUF_SIZE]; /*--- Socket öffnen ---*/ sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
202 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 7 Netzwerkprogrammierung if (sock_fd == -1) { perror("upnp-search: Can’t create new socket"); return(1); } /*--- Zieladresse ---*/ server_addr.sin_family = AF_INET; server_addr.sin_port = htons(UPNP_PORT); inet_aton(UPNP_ADDR, &(server_addr.sin_addr)); /*--- Anfrage senden ---*/ strcpy(buffer, "M-SEARCH * HTTP/1.1\r\n" "HOST: 239.255.255.250:1900\r\n" "MAN: \"ssdp:discover\"\r\n" "MX: 2\r\n" "ST: ssdp:all\r\n" "\r\n"); length = sendto(sock_fd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)); if (length != strlen(buffer)) { perror("upnp-search: sendto() failed"); return(1); } /*--- auf Antwort warten ---*/ pollfd.fd = sock_fd; /* Polling vorbereiten */ pollfd.events = POLLIN | POLLPRI; i = 0; while (1) { err = poll(&pollfd, 1, TIMEOUT); if (err < 0) { perror("upnp-search: poll() failed"); break; } else if (err == 0) /* Timeout erreicht */ { if (i == 0) printf("<No response received.>\n"); break; }
7.4 Das User Datagram Protocol (UDP) 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 203 else { addr_size = sizeof(struct sockaddr_in); length = recvfrom(sock_fd, buffer, BUF_SIZE-1, 0, (struct sockaddr *)&from_addr, &addr_size); if (length == -1) perror("upnp-search: recvfrom() failed"); else { buffer[length] = ’\0’; printf("\33[1m---- Response from %s:\33[0m\n%s\n", inet_ntoa(from_addr.sin_addr), buffer); } i = 1; } } close(sock_fd); return(0); } Wenn sich z. B. ein WindowsTM -Rechner als Internet-Gateway im lokalen Netzwerk befindet, werden Sie mehrere UPnP-Antworten ähnlich dem folgenden Beispiel erhalten: > ./upnp-search ---- Response from 192.168.0.1: HTTP/1.1 200 OK ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1 USN:uuid:e21500e7-3d2f-45a2-849c-d64707cb66b3::urn:schemas←֓ -upnp-org:device:InternetGatewayDevice:1 Location:http://192.168.0.1:2869/upnphost/udhisapi.dll?con←֓ tent=uuid:e21500e7-3d2f-45a2-849c-d64707cb66b3 Cache-Control:max-age=1800 Server:Microsoft-Windows-NT/5.1 UPnP/1.0 UPnP-Device-Host/1.0 Ext: Aus der mit ST:“ beginnenden Zeile können Sie entnehmen, dass es sich um ” ein InternetGatewayDevice“ handelt. Den hinter Location:“ angegebenen URL ” ” können Sie beispielsweise mit Firefox öffnen, um die XML-Beschreibungsdatei des Dienstes darzustellen. Für weitere Informationen zu UPnP möchten wir auf die entsprechenden Spezifikationen [15] und [16] verweisen.
204 7 Netzwerkprogrammierung 7.5 Noch ein Wort zur Sicherheit Die beschriebenen Techniken und Programmbeispiele versetzen Sie in die Lage, über das Internet mit der großen, weiten Welt“ zu kommunizieren. Unglück” licherweise sind es – neben anderen – genau diese Techniken, die Hackern, Viren und Würmern dabei helfen, ihre weniger harmlosen Absichten zu verfolgen. Sobald Sie einen Dienst über einen TCP/IP-Port einrichten, könnten ungebetene Gäste“ versuchen, diesen als Hintertür zu Ihrem System zu missbrauchen. Dies ” gilt vor allem dann, wenn Sie ohne Firewall eine Verbindung zum Internet herstellen. Am sichersten ist es daher, wenn Sie Client-Server-Programme zunächst auf Systemen entwickeln, die keine Verbindung nach draußen“ haben. Außerdem ” sollten Sie beim Erstellen solcher Programme immer berücksichtigen, dass Ihr Gegenüber“ möglicherweise andere Anfragen oder Antworten schickt als die ” von Ihrem Programm erwarteten. Ein beliebter Fehler ist beispielsweise ein nicht abgefangener Pufferüberlauf: Sendet der entfernte Rechner mehr Zeichen, als in den Empfangspuffer passen, können dadurch andere Bereiche des Programms mit Daten des fremden Rechners überschrieben werden. Wenn Sie Programme zur Netzwerkkommunikation auf einem eigenen kleinen Rechnernetzwerk testen wollen, sollten Sie – falls nicht ohnehin schon geschehen – IP-Adressen verwenden, die speziell für private Netzwerke reserviert sind und in der Regel von einem Router nicht weitergeleitet werden. Dies sind die Adressen 192.168.0.1 bis 192.168.0.254. Weitere Informationen zu den verschiedenen IPAdressbereichen finden Sie z. B. unter [17].
Kapitel 8 Grafische Benutzeroberflächen Unter Linux gibt es eine ganze Reihe von Anwendungen, die ohne grafische Benutzerschnittstelle ( GUI“ für Graphical User Interface) auskommen. Im Ab” schnitt 3.4.2 wurde gezeigt, wie eine komfortable Ausgabesteuerung auch im Terminalfenster möglich ist. Die Bedienung eines Programms lässt sich jedoch oft durch eine grafische Benutzerschnittstelle deutlich vereinfachen – für manche Anwendungen wie z.B. Grafikprogramme ist sie sogar unabdingbar. Es stehen unter Linux zahlreiche Funktionsbibliotheken für die Programmierung von grafischen Oberflächen zur Verfügung; die meisten davon setzen auf das X11System (siehe unten) auf. In den folgenden Abschnitten wird eine dieser Bibliotheken vorgestellt. Als Alternative zur Grafikprogrammierung unter X11 – z.B. für Systeme mit knappen Ressourcen – wird in Abschnitt 8.3 die Verwendung der libvga für grafische Anwendungen ohne das X11-System demonstriert. 8.1 Die grafische Oberfläche X11 Das X11-System wurde am Massachusetts Institute of Technology (kurz MIT) in Zusammenarbeit mit der Firma DEC1 als Hardware-unabhängige, grafische Schnittstelle entwickelt. 1988 wurde das MIT X Consortium gegründet, das diese Arbeit fortführte. Nach Auflösung dieses Konsortiums übernahm zunächst The Open ” Group“ die Weiterentwicklung, bevor sie an die X.Org“ übertragen wurde. ” X11 ist als Client-Server-System realisiert – der Server bildet die Schnittstelle zur Hardware, während der Client mit dem Server kommuniziert, um beispielsweise Grafikobjekte darzustellen oder zu manipulieren. Das Programm XF86 SVGA2 ist z.B. ein X11-Server (für SVGA-Grafikkarten), der Window-Manager KDE hinge1 Digital Equipment Corporation 2 Mit Einführung der Version 4 wurden die verschiedenen Programme wie XF86 SVGA durch den einheitlichen Server XFree86 abgelöst.
206 8 Grafische Benutzeroberflächen gen ein X11-Client. Das X11-System zeichnet aus, dass der Client auf einem anderen Rechner laufen kann als der Server, sofern beide über ein TCP/IP-Netzwerk verbunden sind. So ist es z.B. möglich, dass ein Linux-PC über das Internet mit einer Unix-Workstation verbunden ist und auf der Workstation ein Grafikprogramm läuft, das am PC bedient wird. Unter Linux kommt die freie X11-Variante XFree86 zum Einsatz, die ursprünglich für 80x86-Prozessoren entwickelt wurde und als offener Quelltext verfügbar ist. Die in dem Paket enthaltene Funktionsbibliothek libX11 bietet eine Vielzahl von Funktionen zur Manipulation von Grafikobjekten. Dennoch eignet sich diese Bibliothek allein nicht zum Erstellen von Programmen mit grafischer Benutzerschnittstelle. Es gibt eine Reihe weiterer Bibliotheken, die Funktionen für komplexe Bedienelemente wie beispielsweise Menüs beinhalten. Einige dieser Bibliotheken – auch als Toolkits bezeichnet – sind: Tk GTK+ (The Gimp Toolkit) XView / OpenLook (Nachfolger von SunView) Qt Motif (bzw. LessTif, ein Open Source Motif-Ersatz) Athena Widget Das Look & Feel“ dieser Bibliotheken ist sehr unterschiedlich, was sich auf das ” Erscheinungsbild der verschiedenen Programme unter Linux auswirkt. 8.2 Das Toolkit GTK+ Ursprünglich wurde GTK als grafische Benutzerschnittstelle für das GNU Image ” Manipulation Program“ (GIMP) entwickelt. Daraus entstand schließlich ein eigenständiges Projekt und sehr nützliches Toolkit für Bedienerobflächen. GTK ist objektorientiert: Fenster, Schaltflächen, Menüs usw. sind Objekte (GTKWidgets), deren interner Aufbau für den Programmierer verborgen bleibt. Um die Eigenschaften eines solchen Objektes – beispielsweise den Titeltext eines Fensters – zu verändern, werden spezielle Funktionen der GTK-Bibliotheken auf die Objekte angewendet. 8.2.1 GTK 1.2 versus GTK 2.0 Auf aktuellen Linux-Systemen sind in der Regel zwei Versionen des GTK-Toolkits installiert: 1.2 und 2.0. Eine der wichtigsten Erweiterungen der Version 2.0 ist die Unterstützung von Unicode bzw. UTF-8. Damit können Texte mit Zeichen aus den verschiedensten Sprachen dargestellt werden, während GTK 1.2 bereits mit deutschen Umlauten Probleme hatte.
8.2 Das Toolkit GTK+ 207 8.2.2 GTK-Programme übersetzen Für das Übersetzen von GTK-Programmen müssen mehrere Bibliotheken eingebunden werden. Darüber hinaus müssen dem Compiler zusätzliche Pfade für die Include-Dateien bekannt gemacht werden. Zu diesem Zweck enthält das GTK 2.0Paket ein nützliches Tool: pkg-config. Mit der Option --libs“ liefert das Pro” gramm die erforderlichen Compiler-Optionen für das Einbinden der Funktionsbibliotheken; die Option --cflags“ bewirkt, dass die Compiler-Optionen für die ” zusätzlichen Include-Pfade ausgegeben werden. Außerdem muss man das Paket angeben, dessen Bibliotheken eingebunden werden sollen, also gtk+-2.0“ für ” GTK 2.0 und gtk+“ für GTK 1.2. (Die meisten Beispielprogramme in diesem Ka” pitel lassen sich unter beiden GTK-Versionen übersetzen.) Natürlich können Sie beide Optionen auch gleichzeitig angeben, um mit einem Aufruf von pkg-config die für GTK-Applikationen notwendigen CompilerParameter zu erhalten. Durch die Verwendung von Hochkommata (‘) werden die Ausgaben von pkg-config als Kommandozeilenparameter an den C-Compiler übergeben: gcc mein_prog.c -o mein_prog \ ‘pkg-config --libs --cflags gtk+-2.0‘ Für das Übersetzen des Quelltextes in ein GTK 1.2-Programm kann alternativ das Programm gtk-config verwendet werden: gcc mein_prog.c ‘gtk-config --libs --cflags‘ -o mein_prog Wenn Sie sich bei der Übersetzung der Beispielprogramme eine Menge Tipparbeit sparen wollen, geben Sie einfach folgendes Universal-Makefile“ ein: ” 1 # 2 # Universal-Makefile für GTK+2.0-Programme 3 # 4 5 default: 6 @echo "Usage: make filename" 7 @echo "Please use name of target without ’.c’!" 8 9 %: %.c 10 gcc -Wall $< -o $@ \ 11 ‘pkg-config --libs --cflags gtk+-2.0‘ Anschließend können Sie die Beispielprogramme einfach mit make Dateiname“ ” übersetzen.
208 8 Grafische Benutzeroberflächen 8.2.3 Ein erstes Beispiel Als Einstieg in die Programmierung von GTK-Applikationen dient das folgende Programm, das ein einfaches Fenster voreingestellter Größe und ohne weitere Elemente öffnet: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /* fenster.c - Ein X11-Fenster öffnen */ # include <gtk/gtk.h> int main(int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); /* Optionen auswerten */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_show(window); gtk_main(); /* Verbindung zu X-Server und Callback-Mechanismus starten */ return(0); } Zum Übersetzen des Quelltextes verwenden wir das oben beschriebene Makefile: make fenster Nach dem Starten des Programms erscheint – wie erwartet – ein leeres Fenster. Zum Beenden des Programms müssen Sie in der Shell, aus der Sie es gestartet haben, Ctrl-C1 eingeben, denn eine andere Möglichkeit haben wir ja bislang noch nicht in das Programm eingebaut. Sehen wir uns das Programm einmal genauer an: In Zeile 9 wird das einzige in diesem Beispiel verwendete GTK-Objekt definiert. Wie alle anderen GTKObjekte ist auch window ein Zeiger auf GtkWidget. Die Initialisierung der GTKFunktionen erfolgt durch den Aufruf von gtk init() in Zeile 11. Diese Funktion wertet auch die Kommandozeilenparameter aus und filtert die GKT-Optionen heraus. Beispielsweise können Sie mit der Option --name“ den Applikationsna” men ändern, der im Fenstertitel angezeigt wird (siehe Abbildung 8.1): ./fenster --name "Mein Fenster" 1 bzw. Strg-C, je nach Tastatur.
8.2 Das Toolkit GTK+ 209 In Zeile 13 wird das Fenster-Objekt erzeugt und in der nächsten Zeile mit der Funktion gtk widget show() auf sichtbar“ eingestellt. Durch diesen Funktions” aufruf wird das Fenster noch nicht dargestellt! Dies geschieht erst mit dem Aufruf von gtk main() in Zeile 16. Erst damit nimmt das Programm Kontakt mit dem X11-Server auf und startet den Callback-Mechanismus, den wir in Abschnitt 8.2.4 näher erläutern. Abbildung 8.1: Änderung des Fenstertitels mit der Option --name“ ” Fenstereigenschaften ändern Selbstverständlich können Sie den Fenstertitel nicht nur mit Hilfe der Kommandozeilenoption --name“, sondern auch aus dem Programm heraus einstellen. ” Dazu dient die folgende Funktion: void gtk_window_set_title(GtkWindow *window, const gchar *title); Beispiel:1 gtk_window_set_title(GTK_WINDOW(window), "Fenstertitel"); Analog lässt sich auch die Größe des Fensters mit Hilfe der entsprechenden Funktion einstellen: void gtk_window_set_default_size(GtkWindow *window, gint width, gint height); 1 Bitte beachten Sie das Makro GTK WINDOW! Da alle GTK-Widgets vom Typ GtkWidget sind, sich dahinter aber unterschiedliche Objekte mit unterschiedlichen Strukturen verbergen, werden solche Makros eingesetzt, um zu prüfen, ob die Funktion auf das Widget anwendbar ist. Wenn ja, wird eine Typumwandlung der Art (GtkWindow *)window vorgenommen.
210 8 Grafische Benutzeroberflächen Wenn Sie möchten, dass die Anwender Ihres Programms das Fenster mit der Maus nicht größer oder nicht kleiner als die Objekte in dem Fenster ziehen können, so lässt sich dies ebenfalls einstellen: void gtk_window_set_policy(GtkWindow *window, gint allow_shrink, gint allow_grow, gint auto_shrink); Für eine feste Fenstergröße, die optimal auf den Fensterhinhalt angepasst ist, sieht der Aufruf dieser Funktion so aus: gtk_window_set_policy(GTK_WINDOW(window), FALSE, FALSE, TRUE); Bei unserem ersten Beispielprogramm funktioniert dies allerdings noch nicht, weil das Fenster noch keine Elemente enthält und dadurch die Größe auf wenige Pixel zusammenschrumpft! Voreingestellt ist übrigens, dass die Fenster zwar größer, aber nicht kleiner als die enthaltenen Elemente gezogen werden können (Parameter: FALSE, TRUE, TRUE). Dadurch wird sichergestellt, dass alle Elemente vollständig angezeigt werden, der Benutzer das Fenster aber nach Belieben vergrößern kann. Schließlich haben Sie noch die Möglichkeit einzustellen, an welcher Stelle das Fenster beim Öffnen erscheinen soll. Als mögliche Positionen können hier unter anderem die Werte GTK WIN POS MOUSE (mittig unter dem Mauszeiger) oder GTK WIN POS CENTER (mittig auf dem Bildschirm) gewählt werden. void gtk_window_set_position(GtkWindow *window, GtkWindowPosition position); Selbstverständlich können Sie die Position des Fensters auch konkret (in Pixel) vorgeben. Dazu dient die Funktion void gtk_widget_set_uposition(GtkWidget *widget, gint x, gint y); Für das Beispielprogramm könnte der Funktionsaufruf wie folgt lauten: gtk_widget_set_uposition(window, 50, 30); 8.2.4 Das Callback-Prinzip Bevor wir Bedienelemente in das Fenster einbauen, kommen wir zu dem Mechanismus, mit dem Aktionen ausgeführt werden, wenn Elemente des Fens-
8.2 Das Toolkit GTK+ 211 ters angeklickt werden. Wird ein Eingabeelement – z. B. eine Schaltfläche – bedient, löst dies ein Signal aus, und alle diesem Signal zugeordneten CallbackFunktionen werden aufgerufen. Die Funktion gtk main() startet diesen Mechanismus und wird erst beendet, wenn eine der Callback-Funktionen die Funktion gtk main quit() aufruft. Daher mussten Sie das Beispielprogramm fenster.c“, ” in dem noch keine Callback-Funktionen implementiert waren, mit Ctrl-C abbrechen. Im nächsten Schritt erweitern wir nun das Programm um die Verwendung einer Callback-Funktion zum Schließen des Fensters: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* callback.c - Funktionen beim Schließen des Fensters einstellen */ # include <gtk/gtk.h> int main(int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); /* Optionen auswerten */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_widget_show(window); gtk_main(); /* Verbindung zu X-Server und Callback-Mechanismus starten */ return(0); } Mit Hilfe der Funktion gtk signal connect() in Zeile 16 verknüpfen wir das GTK-Objekt des Fensters mit der Funktion gtk main quit(). Beachten Sie, dass hier eine Funktion als Parameter übergeben wird! Mit dem zweiten Parameter ("delete_event") teilen wir der Funktion gtk signal connect() mit, welches Ereignis (Event) die Callback-Funktion auslösen soll – in diesem Fall das Löschen“ (Schließen) des Fensters. ” Wenn Sie den Typ des Events in Zeile 16 in key press event“ ändern, wird das ” Fenster bei einem Tastendruck geschlossen. Der Event-Typ focus out event“ ” führt dagegen zum Schließen des Fensters, sobald es inaktiv wird.
212 8 Grafische Benutzeroberflächen Der letzte Parameter der Funktion gtk signal connect() ermöglicht es, zusätzliche beliebige Daten an die Callback-Funktion zu übergeben: gint gtk_signal_connect(GtkObject *object, gchar *name, GtkSignalFunc func, gpointer func_data); Als Rückgabewert liefert die Funktion eine ID, eine Art Merker, mit dem sich die Verknüpfung zwischen dem Objekt und der Callback-Funktion eindeutig identifizieren lässt. Dieser Wert wird benötigt, wenn Sie diese Verknüpfung später wieder aufheben wollen: void gtk_signal_disconnect(GtkObject *object, gint id); Nachdem wir die GTK-Funktion gtk main quit() als Callback verwendet haben, definieren wir nun eine eigene Funktion, wie es später auch für Bedienelemente erforderlich ist: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /* callback2.c - Funktionen beim Schließen des Fensters einstellen */ # include <stdio.h> # include <gtk/gtk.h> /*----- Callback-Funktion -----*/ gint close_win(GtkWidget *widget, GdkEvent *event, gpointer data) { printf("’delete’-Event ausgelöst.\n"); return(FALSE); /* destroy-Event auslösen */ } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); /* Optionen auswerten */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
8.2 Das Toolkit GTK+ 27 28 29 30 31 32 33 34 35 36 37 38 39 213 gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(close_win), NULL); gtk_signal_connect(GTK_OBJECT(window), "destroy", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_widget_show(window); gtk_main(); return(0); } Unsere Callback-Funktion close win() in Zeile 11 bis 16 erhält als ersten Parameter das Widget, das die Funktion aufgerufen hat – in diesem Fall also das Fenster-Widget. Der zweite Parameter enthält das Ereignis (Event), das den Aufruf der Funktion ausgelöst hat, hier das Schließen des Fensters. Der dritte und letzte Parameter enthält die optionalen Daten entsprechend dem vierten Parameter der Funktion gtk signal connect() (siehe oben). Davon machen wir aber zunächst noch keinen Gebrauch. Beendet wird die Callback-Funktion mit dem Rückgabewert FALSE. Dadurch wird erreicht, dass das Signal nicht gelöscht und somit automatisch im Anschluss an das delete“-Event ein destroy“-Event aus” ” gelöst wird. Mit dem Wert TRUE würde man das Signal löschen und kein weiteres Event auslösen. Sie können also über den Rückgabewert der Callback-Funktion entscheiden, ob das Fenster tatsächlich geschlossen wird! Diese Möglichkeit wenden wir in Abschnitt 8.2.9 an. Im Hauptprogramm wird unsere Funktion close win() mit dem delete“-Event ” des Fensters verknüpft (Zeile 28 und 29). Zusätzlich wird gtk main quit() als Callback-Funktion für das destroy“-Event eingerichtet (Zeile 31 und 32). ” Selbstverständlich ließe sich auch für destroy“ wiederum eine eigene Callback” Funktion einrichten, falls vor dem Schließen sämtlicher Fenster noch Aktionen erforderlich sind. 8.2.5 Schaltflächen (Buttons) Viele GTK-Objekte – z. B. die Fenster – enthalten einen so genannten Container, in den ein weiteres Objekt – z. B. eine Schaltfläche – eingebaut werden kann. Auf diese Weise werden die GTK-Objekte verschachtelt oder gestapelt. Im folgenden Beispielprogramm haben wir eine Schaltfläche zum Beenden des Fensters eingebaut. 1 2 3 /* button.c - Schaltfläche zum Beenden */
214 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 8 Grafische Benutzeroberflächen # include <stdio.h> # include <gtk/gtk.h> /*----- Callback-Funktionen -----*/ gint close_win(GtkWidget *widget, GdkEvent *event, gpointer data) { printf("Bitte klicken Sie auf ’Beenden’.\n"); return(TRUE); /* Signal löschen -> kein ’destroy’ */ } void quit_proc(GtkWidget *widget, gpointer data) { gtk_main_quit(); /* gtk_main() beenden */ return; } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *button; gtk_init(&argc, &argv); /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(close_win), NULL); gtk_container_set_border_width(GTK_CONTAINER(window), 30); /* Schaltfläche erzeugen */ button = gtk_button_new_with_label(" Beenden "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(quit_proc), NULL);
8.2 Das Toolkit GTK+ 48 49 50 51 52 53 54 55 56 57 58 215 gtk_container_add(GTK_CONTAINER(window), button); /* Objekte darstellen */ gtk_widget_show(button); gtk_widget_show(window); gtk_main(); return(0); } Abbildung 8.2 zeigt das Fenster mit der Schaltfläche. Wenn Sie bei diesem Beispielprogramm versuchen, das Fenster zu schließen, erhalten Sie im Shell-Fenster die Ausgabe Bitte klicken Sie auf ’Beenden’.“ ” Abbildung 8.2: Das Ergebnis des Programms button.c“ ” In den Zeilen 10 bis 21 werden zunächst wieder zwei Callback-Funktionen definiert: eine für das Schließen“-Symbol des Fensters und die zweite für unse” re Schaltfläche. Beachten Sie bitte, dass die Parameter beider Funktionen unterschiedlich sind! Das liegt an den unterschiedlichen Signalen, die die CallbackFunktionen auslösen. Durch den Rückgabewert TRUE“ in Zeile 14 erreichen ” wir bei diesem Beispielprogramm, dass unser Programm durch das Schließen“” Symbol des Fensters nicht beendet wird, sondern explizit die Schaltfläche Been” den“ verwendet werden muss. In dem Hauptprogramm ab Zeile 25 wird nach der Initialisierung von GTK (Zeile 29) das Fenster erzeugt (Zeile 33) und die Callback-Funktion für das Schließen des Fensters eingestellt (Zeile 35 und 36). In Zeile 38 verändern wir die Eigenschaften des Containers (siehe oben) des Fensters. In diesem Fall vergrößern wir den Rand innerhalb des Fensters auf 30 Pixel. Mit der Funktion gtk button new with label() erzeugen wir in Zeile 43 eine Schaltfläche (Button) und verknüpfen sie anschließend mit der Callback-Funktion quit proc() (Zeile 45 und 46). Jetzt muss die Schaltfläche noch in den Container des Fensters eingebaut werden. Dies geschieht in Zeile 48. Als letzten Schritt müssen wir noch beide GTK-Widgets – das Fenster und die Schaltfläche – mit gtk widget show() in den Zeilen 52 und 53 sichtbar machen.
216 8 Grafische Benutzeroberflächen Wenn – wie in diesem Fall – alle Widgets sofort sichtbar sein sollen, erreichen Sie dies mit folgendem Aufruf: gtk widget show all(window); 8.2.6 Hinweistexte (Tipps) Um dem Anwender die Benutzung eines Programms zu erleichtern, haben Sie die Möglichkeit, GTK-Objekte mit Hinweistexten oder Tipps zur Benutzung zu versehen. Diese so genannten Tooltips werden automatisch angezeigt, wenn der Anwender mit dem Mauszeiger über dem Objekt verharrt (siehe Abbildung 8.3). Wenn Sie in das Programm button.c“ aus dem vorigen Abschnitt folgende Zeilen ” einfügen (ab Zeile 49), wird die Schaltfläche mit einem entsprechenden Hinweistext versehen: GtkTooltips* tooltips = gtk_tooltips_new(); gtk_tooltips_set_tip(tooltips, button, "Zum Beenden hier klicken.", NULL); Der vierte Parameter der Funktion gtk tooltips set tip() kann für kontextsensitive Hilfefunktionen verwendet oder – falls nicht benötigt – auf NULL gesetzt werden. Abbildung 8.3: Schaltfläche mit Hinweistext (Tooltips) 8.2.7 Widgets anordnen Im vorigen Abschnitt haben wir in den Container eines Fensters eine Schaltfläche eingebaut. Damit ist dieser Container belegt – weitere Elemente lassen sich nicht hinzufügen! Doch wie bekommt man mehr als ein Objekt in das Fenster, z. B. mehrere Schaltflächen? Dazu müssen wir die Objekte mit Hilfe spezieller GTK-Widgets in Zeilen, Spalten oder einer Matrix (Tabelle) anordnen. Die folgenden beiden Funktionen erzeugen eine Zeile bzw. eine Spalte, in die beliebig viele GTK-Widgets eingefügt werden können: GtkWidget *gtk_hbox_new(gint homogeneous, gint spacing); GtkWidget *gtk_vbox_new(gint homogeneous, gint spacing);
8.2 Das Toolkit GTK+ 217 Beide Funktionen liefern als Rückgabewert ein GTK-Widget vom Typ GtkBox. Der Parameter homogeneous kann TRUE oder FALSE sein und legt fest, ob alle Elemente der Zeile oder Spalte die gleiche Breite bzw. Höhe haben sollen. Mit dem Parameter spacing können Sie einen Abstand (in Pixel) zwischen den Objekten einstellen. Nachdem Sie eine solche Box erzeugt haben, können Sie darin Schaltflächen oder andere Widgets einfügen, wahlweise von links nach rechts (bzw. oben nach unten) oder von rechts nach links (bzw. unten nach oben) – je nachdem, welche der beiden folgenden Funktionen Sie verwenden: void gtk_box_pack_start(GtkBox *box, GtkWidget *child, gint expand, gint fill, gint padding); void gtk_box_pack_end(GtkBox *box, GtkWidget *child, gint expand, gint fill, gint padding); Als ersten Parameter erwarten beide Funktionen die Box, in die ein Element eingefügt werden soll, gefolgt von dem einzufügenden Element. Der Parameter expand kann TRUE oder FALSE sein und bestimmt, ob die Box automatisch auf die Fenstergröße ausgedehnt wird. Mit dem vierten Parameter (fill) können Sie einstellen, ob die Größe des einzufügenden Objekts mit der Größe der Box verändert werden kann. Auch dieser Parameter kann TRUE oder FALSE sein. Der letzte Parameter (padding) erlaubt es, einen Abstand zwischen den Objekten einzubauen. Der Wert gibt die Distanz in Pixel an. Um einen Eindruck von den Möglichkeiten zu bekommen, empfiehlt es sich, einfach mal verschiedene Kombinationen für die Parameter expand, fill und padding auszuprobieren und dabei auch den Wert für homogeneous beim Erzeugen der Box zu modifizieren. Bitte beachten Sie, dass Sie auch eine Box als Element in eine andere Box einfügen können, so dass beliebige Kombinationen von Zeilen und Spalten möglich sind! Bevor wir die Verwendung dieser Funktionen anhand eines Beispiels erläutern, möchten wir das GTK-Widget frame vorstellen, das die Gestaltung und Strukturierung von Elementen in einem Fenster erlaubt. GtkWidget* gtk_frame_new(const gchar *label); Dieses Widget enthält einen Container, in den ein anderes Widget – beispielsweise eine Box – eingefügt werden kann, und zeichnet einen Rahmen darum. Dieser Rahmen wird mit dem als Argument label angegebenen Text beschriftet.
218 8 Grafische Benutzeroberflächen Das folgende Beispiel öffnet ein Fenster, das eine vertikale Box enthält, die wiederum eine horizontale Box und eine einzelne Schaltfläche beinhaltet. Die horizontale Box umfasst vier Schaltflächen (siehe Abbildung 8.4). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 /* boxes.c - Schaltflächen mit vbox und hbox anordnen */ # include <stdio.h> # include <gtk/gtk.h> /*----- Callback-Funktionen -----*/ void quit_proc(GtkWidget *widget, gpointer data) { gtk_main_quit(); /* gtk_main() beenden */ return; } void button_proc(GtkWidget *widget, gpointer data) { printf("%s wurde betätigt.\n", (gchar *)data); return; } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *frame1, *frame2, *box1, *box2, *button; int i; gchar buffer[4][16]; gtk_init(&argc, &argv); /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_container_set_border_width(GTK_CONTAINER(window), 10);
8.2 Das Toolkit GTK+ 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 219 /* Frame für die 1. Box erzeugen */ frame1 = gtk_frame_new("box1 (vbox)"); gtk_container_add(GTK_CONTAINER(window), frame1); /* vertikale Box erzeugen */ box1 = gtk_vbox_new(FALSE, 10); gtk_container_set_border_width(GTK_CONTAINER(box1), 10); gtk_container_add(GTK_CONTAINER(frame1), box1); /* Frame für die 2. Box erzeugen */ frame2 = gtk_frame_new("box2 (hbox)"); gtk_box_pack_start(GTK_BOX(box1), frame2, FALSE, FALSE, 0); /* horizontale Box erzeugen */ box2 = gtk_hbox_new(TRUE, 10); gtk_container_set_border_width(GTK_CONTAINER(box2), 10); gtk_container_add(GTK_CONTAINER(frame2), box2); /* Schaltflächen erzeugen */ for (i=0; i<4; i++) { sprintf(buffer[i], " Button %d ", i+1); button = gtk_button_new_with_label(buffer[i]); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(button_proc), buffer[i]); gtk_box_pack_start(GTK_BOX(box2), button, TRUE, TRUE, 0); } button = gtk_button_new_with_label(" Beenden "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(quit_proc), NULL); gtk_box_pack_start(GTK_BOX(box1), button, FALSE, FALSE, 0); /* Objekte darstellen */
220 86 87 88 89 90 91 8 Grafische Benutzeroberflächen gtk_widget_show_all(window); gtk_main(); return(0); } In diesem Beispiel verwenden wir eine Callback-Funktion (button proc(), Zeile 17 bis 21) für mehrere Schaltflächen. Mit Hilfe des Parameters data wird der Callback-Funktion je nach Schaltfläche eine andere Information übergeben – in diesem Fall der Text der Schaltfläche. Abbildung 8.4: Anordnung der Schaltflächen mittels vbox und hbox 8.2.8 Text-Labels In den vorherigen Beispielen haben wir Schaltflächen mit einem Label (also einem Text) verwendet. Das Label selbst ist ein eigenständiges GTK-Widget, das auch außerhalb von Schaltflächen platziert werden kann, beispielsweise um zusätzliche Informationen zu den Bedienelementen eines Programms zu geben – ähnlich den oben beschriebenen Frames, jedoch ohne zusätzlichen Rahmen. Erzeugt wird ein Label mit der Funktion: GtkWidget *gtk_label_new(char *text); Anschließend kann das Label in der gleichen Weise wie andere Widgets im Fenster oder einer Box platziert werden. Während der Ausführung des Programms lässt sich der Text des Labels mit der Funktion void gtk_label_set_text(GtkLabel *label, char *text); nachträglich verändern.
8.2 Das Toolkit GTK+ 221 8.2.9 Dialogfenster Häufig benötigt man neben dem Hauptfenster weitere Dialogfenster. GTK bietet dafür eine Funktion, die das neue Fenster erzeugt und bereits mit einer vertikalen Box für Texte etc. und einer Action-Area“ für Bedienelemente ausstattet: ” GtkWidget *gtk_dialog_new(); Das nachfolgende Beispielprogramm verwendet Dialogfenster für eine Sicherheitsabfrage beim Schließen des Hauptfensters (Abbildung 8.5). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /* dialog.c - Dialogfenster beim Beenden öffnen */ # include <gtk/gtk.h> /*----- Callback-Funktion -----*/ gint close_win(GtkWidget *widget, GdkEvent *event, gpointer dialog) { gtk_widget_show(dialog); return(TRUE); /* destroy-Event NICHT auslösen */ } gint close_sub(GtkWidget *widget, GdkEvent *event, gpointer data) { gtk_widget_hide(widget); return(TRUE); /* destroy-Event NICHT auslösen */ } void cont_proc(GtkWidget *widget, gpointer subwin) { gtk_widget_hide(subwin); /* Dialog schließen */ return; } void quit_proc(GtkWidget *widget, gpointer subwin) { gtk_widget_show(subwin); /* Dialog öffnen */ return; }
222 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 8 Grafische Benutzeroberflächen /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *subwin, *label, *button; gtk_init(&argc, &argv); /* Optionen auswerten */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_container_set_border_width(GTK_CONTAINER(window), 30); /* Dialogfenster erzeugen */ subwin = gtk_dialog_new(); gtk_signal_connect(GTK_OBJECT(subwin), "delete_event", GTK_SIGNAL_FUNC(close_sub), NULL); gtk_window_set_title(GTK_WINDOW(subwin), "Beenden?"); /* Callback für Hauptfenster einstellen */ gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(close_win), subwin); gtk_signal_connect(GTK_OBJECT(window), "destroy", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); /* Text in das Dialogfenster schreiben */ label = gtk_label_new("Wollen Sie das Programm\n" "wirklich beenden?"); gtk_box_pack_start(GTK_BOX(GTK_DIALOG(subwin)->vbox), label, TRUE, TRUE, 20); gtk_widget_show(label); /* Schaltflächen für Dialog erzeugen */ button = gtk_button_new_with_label(" Beenden "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_box_pack_start( GTK_BOX(GTK_DIALOG(subwin)->action_area), button, TRUE, FALSE, 0); gtk_widget_show(button);
8.2 Das Toolkit GTK+ 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 223 button = gtk_button_new_with_label(" Abbrechen "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(cont_proc), subwin); gtk_box_pack_start( GTK_BOX(GTK_DIALOG(subwin)->action_area), button, TRUE, FALSE, 0); gtk_widget_show(button); /* Schaltfläche für Hauptfenster erzeugen */ button = gtk_button_new_with_label(" Beenden "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(quit_proc), subwin); gtk_container_add(GTK_CONTAINER(window), button); /* Fenster darstellen und Callback starten */ gtk_widget_show_all(window); gtk_main(); return(0); } Am Anfang des Quelltextes werden wiederum die Callback-Funktionen für die verschiedenen Ereignisse/Signale definiert: close win() wird beim Schließen des Hauptfensters aufgerufen. close sub() wird beim Schließen des Dialogfensters aufgerufen. cont proc() bei Betätigung der Schaltfläche Abbrechen“ im Dialogfenster. ” quit proc() bei Betätigung der Schaltfläche Beenden“ im Hauptfenster. ” Abbildung 8.5: Sicherheitsabfrage mit Hilfe eines Dialogfensters
224 8 Grafische Benutzeroberflächen Im Hauptprogramm wird nach dem Hauptfenster auch gleich das Dialogfenster erzeugt (Zeile 49), obwohl dieses ja erst beim Schließen des Hauptfensters geöffnet werden soll. Der Trick besteht darin, dass der Dialog bereits vollständig mit Text und Schaltflächen erzeugt aber noch nicht mit der Funktion gtk widgt show() sichtbar“ gemacht wird. Dies geschieht erst beim Versuch, das Hauptfenster zu ” schließen (Zeile 12 und 31). 8.2.10 Auswahlfelder Bislang haben wir als Eingabeelemente ausschließlich einfache Schaltflächen verwendet. GTK kennt darüber hinaus spezielle Schaltflächen, die der Benutzer einund ausschalten kann. Das folgende Programm zeigt einige dieser Auswahlfelder (Abbildung 8.6): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /* selection.c - verschiedene Auswahlelemente */ # include <stdio.h> # include <string.h> # include <gtk/gtk.h> /*----- Callback-Funktionen -----*/ void quit_proc(GtkWidget *widget, gpointer data) { gtk_main_quit(); /* gtk_main() beenden */ return; } void button_proc(GtkWidget *widget, gpointer data) { if (GTK_TOGGLE_BUTTON(widget)->active) printf("%s-Button: An\n", (char *)data); else printf("%s-Button: Aus\n", (char *)data); return; } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *vbox, *button, *separator; GSList *group;
8.2 Das Toolkit GTK+ 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 225 gtk_init(&argc, &argv); /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_container_set_border_width(GTK_CONTAINER(window), 10); /* vertikale Box erzeugen */ vbox = gtk_vbox_new(FALSE, 10); gtk_container_add(GTK_CONTAINER(window), vbox); /* Toggle- und Check-Schaltflächen erzeugen */ button = gtk_toggle_button_new_with_label("An/Aus"); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(button_proc), "Toggle"); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); button = gtk_check_button_new_with_label("An/Aus"); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(button_proc), "Check"); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); /* Radio-Buttons erzeugen */ button = gtk_radio_button_new_with_label(NULL, "Auswahl 1"); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(button_proc), "Radio1"); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); group = gtk_radio_button_group(GTK_RADIO_BUTTON(button));
226 8 Grafische Benutzeroberflächen 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 button = gtk_radio_button_new_with_label(group, "Auswahl 2"); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(button_proc), "Radio2"); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); /* Trennlinie einfügen */ separator = gtk_hseparator_new(); gtk_box_pack_start(GTK_BOX(vbox), separator, FALSE, FALSE, 0); /* Schaltfläche zum Beenden */ button = gtk_button_new_with_label(" Beenden "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(quit_proc), NULL); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); /* Objekte darstellen */ gtk_widget_show_all(window); gtk_main(); return(0); } In diesem Beispiel verwenden wir für alle Auswahlfelder die gleiche CallbackFunktion button proc() in Zeile 17 bis 24. Anhand des Parameters data können wir unterscheiden, welches Element der Benutzer angewählt hat. Dazu haben wir bei der Verknüpfung der Objekte mit der Callback-Funktion jeweils einen anderen Text als Kennung an die Funktion gtk signal connect() übergeben (Zeile 53, 60, 70 und 80). Die Auswahlfelder vom Typ Toggle-Button und Check-Button sind den bereits vorgestellten Schaltflächen ähnlich, bleiben jedoch beim Anklicken so lange aktiv“, ” bis sie erneut angeklickt werden. In der Callback-Funktion können wir anhand der Variablen GTK TOGGLE BUTTON(widget)->active
8.2 Das Toolkit GTK+ 227 Abbildung 8.6: Verschiedene Auswahlelemente abfragen, ob das Auswahlfeld aktiviert oder deaktiviert wurde. Die Toggle- und Check-Buttons unterscheiden sich lediglich in der grafischen Gestaltung (siehe Abbildung 8.6). Etwas anders verhält es sich mit den so genannten Radio-Buttons. Diese Widgets werden zu einer Gruppe (GSList *group) verküpft, so dass der Anwender nur genau ein Element dieser Gruppe aktivieren kann, wie bei den Stationstasten eines Radios – daher der Name Radio-Button. In unserem Beispiel haben wir zwei solcher Auswahlfelder mit der Funktion GtkWidget *gtk_radio_button_new_with_label(GSList *group, gchar *label); erzeugt. Beim ersten Button geben wir als Zeiger auf die Gruppe NULL an, da die Gruppe noch nicht existiert, zu der dieses Widget hinzugefügt werden soll (Zeile 66 und 67). In Zeile 74 erzeugen wir dann die neue Gruppe und fügen den ersten Radio-Button hinzu. Für den zweiten Radio-Button geben wir anschließend direkt die Gruppe an, zu der er hinzugefügt werden soll (Zeile 76 und 77). Bei Betätigung eines der Radio-Buttons wird unsere Callback-Funktion zweimal aufgerufen: einmal zum Deaktivieren des zuvor aktiven Buttons und anschließend zum Aktivieren des angeklickten Buttons. In unserem Beispielprogramm sind die Toggle- und Check-Buttons beim Start des Programms inaktiv und der erste Radio-Button aktiv. Natürlich können Sie die Voreinstellung beim Start des Programms auch selbst vorgeben, und zwar mit der Funktion gtk_toggle_button_set_active(GtkToggleButton *button, gint state);
228 8 Grafische Benutzeroberflächen 8.2.11 Eingabefelder für Text und Zahlen Neben den Auswahl-Buttons, die nur ein- oder ausgeschaltet werden können, bietet GTK komplexere Eingabefelder bis hin zu mehrzeiligen Textfeldern mit automatischem Scrollen und Cut-and-paste-Funktion. Das folgende Beispiel erzeugt zwei einzeilige Text-, ein Zahlen- und ein mehrzeiliges Textfeld.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 1 /* input.c - verschiedene Text-Eingabefelder */ # include <stdio.h> # include <gtk/gtk.h> /*----- Callback-Funktionen -----*/ void quit_proc(GtkWidget *widget, gpointer data) { gtk_main_quit(); /* gtk_main() beenden */ return; } void entry_proc(GtkWidget *widget, gpointer data) { printf("Eingabe in Textfeld %s: %s\n", (char *)data, gtk_entry_get_text(GTK_ENTRY(widget))); return; } void spin_proc(GtkWidget *widget, gpointer data) { printf("Eingabe in Zahlenfeld: %d\n", gtk_spin_button_get_value_as_int(data)); return; } gboolean text_proc(GtkWidget *view, GdkEvent *event, gpointer data) { GtkTextBuffer *buffer; GtkTextIter start, end; char *text; Dieses Programm lässt sich nur mit GTK 2.0 übersetzen. Für GTK 1.2 muss das Widget GtkTextView durch die ältere Version GtkText ersetzt werden.
8.2 Das Toolkit GTK+ 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 229 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)); gtk_text_buffer_get_start_iter(buffer, &start); gtk_text_buffer_get_end_iter(buffer, &end); text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); printf("Textbox:\n%s\n", text); return(FALSE); /* Signal nicht löschen! */ } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *vbox, *button, *entry, *spin, *view; GtkTextBuffer *buffer; GtkAdjustment *adj; gtk_init(&argc, &argv); /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_container_set_border_width(GTK_CONTAINER(window), 10); /* vertikale Box erzeugen */ vbox = gtk_vbox_new(FALSE, 10); gtk_container_add(GTK_CONTAINER(window), vbox); /* Textfelder erzeugen */ entry = gtk_entry_new(); gtk_signal_connect(GTK_OBJECT(entry), "changed", GTK_SIGNAL_FUNC(entry_proc), "1"); gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE, 0); gtk_entry_set_text(GTK_ENTRY(entry), "Textfeld 1"); entry = gtk_entry_new_with_max_length(20); gtk_signal_connect(GTK_OBJECT(entry), "changed",
230 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 8 Grafische Benutzeroberflächen GTK_SIGNAL_FUNC(entry_proc), "2"); gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE, 0); gtk_entry_set_text(GTK_ENTRY(entry), "max. 20 Zeichen"); /* nummerisches Feld erzeugen */ adj = (GtkAdjustment *)gtk_adjustment_new(20, 0, 100, 1, 10, 0); spin = gtk_spin_button_new(adj, 0.0, 0); gtk_signal_connect(GTK_OBJECT(adj), "value_changed", GTK_SIGNAL_FUNC(spin_proc), spin); gtk_box_pack_start(GTK_BOX(vbox), spin, FALSE, FALSE, 0); /* Textbox erzeugen */ view = gtk_text_view_new(); gtk_signal_connect(GTK_OBJECT(view), "focus_out_event", GTK_SIGNAL_FUNC(text_proc), NULL); gtk_widget_set_usize(view, 200, 120); gtk_text_view_set_editable(GTK_TEXT_VIEW(view), TRUE); gtk_box_pack_start(GTK_BOX(vbox), view, TRUE, TRUE, 0); buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)); gtk_text_buffer_set_text(buffer, "mehrzeiliger Text\n", -1); /* Schaltfläche zum Beenden */ button = gtk_button_new_with_label(" Beenden "); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(quit_proc), NULL); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); /* Objekte darstellen */ gtk_widget_show_all(window); gtk_main(); return(0); }
8.2 Das Toolkit GTK+ 231 Alle Textfelder werden im Programm mit unterschiedlichen Texten vorbelegt, die direkt beim Öffnen des Fensters zu sehen sind (siehe Abbildung 8.7). Abbildung 8.7: Eingabefelder für Text und Zahlen Häufig werden Texteingabefelder erst ausgewertet, wenn eine Schaltfläche betätigt wird, z. B. bei einem Feld für einen Dateinamen kombiniert mit einer Schaltfläche Speichern unter“. In unserem Beispiel haben wir aber auch die Eingabe” felder mit Callback-Funktionen verknüpft, so dass bereits bei der Eingabe eines Textes Aktionen erfolgen. Die Funktion entry proc() (Zeile 16 bis 22) wird aufgerufen, sobald der Text in einem der einzeiligen Felder geändert wird. Dies erreichen wir durch die Verknüpfung mit dem Signal changed“ in Zeile 73 und 80. Mit Hilfe der Funktion ” gtk entry get text() geben wir bei jeder Änderung den aktuellen Inhalt des Feldes aus (Zeile 18 bis 20). Bitte beachten Sie die unterschiedlichen Funktionsaufrufe zum Erzeugen der einzeiligen Textfelder in den Zeilen 72 und 79. Damit erreichen wir, dass die Anzahl der Zeichen beim zweiten Textfeld auf 20 begrenzt wird. In den Zeilen 24 bis 29 ist die Funktion spin proc() definiert, die als CallbackFunktion für das Zahlenfeld (in GTK als Spin-Button bezeichnet) dient. Den Parameter data verwenden wir, um der Funktion einen Zeiger auf den Spin-Button zu übergeben. Das Zahlenfeld selbst wird in den Zeilen 88 bis 94 erzeugt und eingerichtet. Für einen Spin-Button benötigen wir ein so genanntes Adjustment, das in der Zeile 88 mit der Funktion
232 8 Grafische Benutzeroberflächen GtkObject *gtk_adjustment_new(gfloat gfloat gfloat gfloat gfloat gfloat value, lower, upper, step_increment, page_increment, page_size); neu angelegt wird. Die ersten drei Parameter geben den Startwert (beim Öffnen des Fensters), den minimalen und den maximalen Wert des Feldes an. Beachten Sie bitte, dass es sich um Fließkommazahlen handelt; Sie können dieses GTKElement also nicht nur für ganzzahlige Eingabefelder verwenden. Der vierte Parameter gibt an, um wie viel der Wert erhöht/verringert wird, wenn der Benutzer mit der linken Maustaste auf die Pfeile rechts neben dem Feld klickt (siehe Abbildung 8.7). Beim Klick mit der mittleren Maustaste (oder dem Scrollrad) auf die Pfeile wird der Wert um den als fünften Parameter angegebenen Betrag vergrößert bzw. verkleinert. Der letzte Parameter wird zur Zeit nicht genutzt. In Zeile 90 wird der eigentliche Spin-Button erzeugt und direkt mit dem zuvor angelegten Adjustment adj verknüpft. Der zweite Parameter der Funktion gkt spin button new() gibt an, wie stark das Hoch- und Runterzählen des Feldes beschleunigt wird, wenn der Benutzer auf die Pfeiltasten klickt und die Maustaste gedrückt hält. Der Wertebereich für diesen Parameter bewegt sich von 0 bis 1,0. Mit dem dritten Parameter geben Sie schließlich an, wie viele Nachkommastellen das Zahlenfeld anzeigen soll. In unserem Beispiel ist der Wert 0 – es werden also nur ganze Zahlen angezeigt. Bitte beachten Sie, dass unsere CallbackFunktion nicht mit dem Spin-Button, sondern dem Adjustment verknüpft wird (Zeile 91)! Für das mehrzeilige Textfeld erzeugen wir ein TextView-Widget (Zeile 98) und verknüpfen es mit dem focus out event“ (Zeile 99 und 100). Dadurch wird die ” Callback-Funktion text proc() (Zeile 31 bis 45) immer dann aufgerufen, wenn der Cursor in dem mehrzeiligen Textfeld ist und ein anderes Feld (oder Fenster) angeklickt – die Eingabe also abgeschlossen wird. Da ein TextView keine sinnvoll voreingestellte Größe hat – ähnlich dem leeren Fenster in unserem ersten GTKBeispiel –, verwenden wir die Funktion void gtk_widget_set_usize(GtkWidget *widget, gint width, gint height); zum Einstellen einer Startbreite und -höhe (Zeile 101). Diese Funktion lässt sich auch auf andere Widgets anwenden – z. B. auf Schaltflächen. Wenn Sie das mehrzeilige Textfeld nur für Ausgaben (z. B. Statusinformationen) nutzen wollen, müssen Sie die Editierbarkeit“ abschalten (Zeile 102): ” gtk text view set editable(GTK TEXT VIEW(view), FALSE);
8.2 Das Toolkit GTK+ 233 Beim Erzeugen des Widgets TextView wird automatisch ein Puffer für die Eingaben angelegt. Bevor wir Text in das Feld schreiben können, müssen wir diesen Puffer abfragen (Zeile 105). Danach kann mit der Funktion void gtk_text_buffer_set_text(GtkTextBuffer *buffer, const gchar *text, gint len); der gesamte Inhalt des Puffers gelöscht und durch den als zweiten Parameter angegebenen Text ersetzt werden (Zeile 106). Der dritte Parameter gibt die Länge des neuen Textes an. Ist sie –1, wird sie automatisch ermittelt. Soll der bereits vorhandene Text im Puffer erhalten bleiben und nur etwas hinzugefügt werden, so können Sie dazu die Funktion void gtk_text_buffer_insert_at_cursor(GtkTextBuffer *buffer, const gchar *text, gint len); verwenden. Das Auslesen des Pufferinhalts (nach Benutzereingaben) kann unter anderem mit der Funktion gchar *gtk_text_buffer_get_text(GtkTextBuffer *buffer, const GtkTextIter *start, const GtkTextIter *end, gboolean include_hidden_chars); erfolgen (Zeile 41). Weil das Textfeld neben den sichtbaren“ Zeichen auch In” formationen über die Schriftart und -farbe und sogar Grafikelemente enthalten kann, wird die Position im Puffer nicht einfach als Zahl angegeben, sondern als GtkTextIter. Daher fragen wir die Anfangs- und Endposition des aktuellen Textes in der Callback-Funktion mit Hilfe von void gtk_text_buffer_get_start_iter(GtkTextBuffer *buffer, GtkTextIter *start); void gtk_text_buffer_get_end_iter(GtkTextBuffer *buffer, GtkTextIter *end); ab (Zeile 39 und 40). 8.2.12 Menüs Zu den wichtigsten Elementen einer grafischen Benutzeroberfläche gehören Pulldown-Menüs. Ein typisches Menü umfasst bei GTK die in Abbildung 8.8 dargestellten Elemente. Beachten Sie, dass die Elemente in der Menüleiste vom gleichen Typ (MenuItem) sind wie die einzelnen Unterpunkte eines Menüs.
234 8 Grafische Benutzeroberflächen MenuBar MenuItem Menu Abbildung 8.8: Die Elemente eines GTK-Menüs Im folgenden Programm realisieren wir eine Menüleiste (MenuBar) mit einem Menü, bestehend aus drei MenuItems. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* menu.c - Fenster mit Menüleiste */ # include <stdio.h> # include <string.h> # include <gtk/gtk.h> GtkWidget *label; /* für Callback sichtbar! */ /*----- Callback-Funktion -----*/ void menu_proc(GtkWidget *widget, gpointer data) { static char buffer[40]; sprintf(buffer, "Menüpunkt ’%s’ gewählt.", (char *)data); gtk_label_set_text(GTK_LABEL(label), buffer); return; } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *vbox, *menubar, *menu, *menuitem; gtk_init(&argc, &argv);
8.2 Das Toolkit GTK+ 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 235 /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_window_set_default_size(GTK_WINDOW(window), 250, 150); /* vertikale Box erzeugen */ vbox = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(window), vbox); /* Menüs erzeugen und einrichten */ menubar = gtk_menu_bar_new(); gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 2); menu = gtk_menu_new(); menuitem = gtk_menu_item_new_with_label("Laden"); gtk_signal_connect(GTK_OBJECT(menuitem), "activate", GTK_SIGNAL_FUNC(menu_proc), "Laden"); gtk_menu_append(GTK_MENU(menu), menuitem); menuitem = gtk_menu_item_new_with_label("Speichern"); gtk_signal_connect(GTK_OBJECT(menuitem), "activate", GTK_SIGNAL_FUNC(menu_proc), "Speichern"); gtk_menu_append(GTK_MENU(menu), menuitem); menuitem = gtk_menu_item_new_with_label("Beenden"); gtk_signal_connect(GTK_OBJECT(menuitem), "activate", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_menu_append(GTK_MENU(menu), menuitem); menuitem = gtk_menu_item_new_with_label("Datei"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu); gtk_menu_bar_append(GTK_MENU_BAR(menubar), menuitem); /* Label als Leerraum erzeugen */
236 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 8 Grafische Benutzeroberflächen label = gtk_label_new(""); gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0); /* Label zur Textanzeige erzeugen */ label = gtk_label_new("Kein Menüpunkt gewählt."); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); /* Objekte darstellen */ gtk_widget_show_all(window); gtk_main(); return(0); } In den Zeilen 13 bis 21 ist die Callback-Funktion für die Menüpunkte definiert. In dieser Funktion wird der aufgerufene Menüpunkt mit Hilfe eines Label-Widgets als Text unten im Fenster ausgegeben (siehe Abbildung 8.9). Um dies zu vereinfachen, haben wir die Variable mit dem GTK-Label als globale Variable definiert (Zeile 9), so dass auch die Callback-Funktion darauf zugreifen kann. Abbildung 8.9: Fenster mit Menüzeile und einem Menü Zur Erstellung des Menüs erzeugen wir in dem Hauptprogramm zuerst die Menüleiste (Zeile 46) und dann das Menü (Zeile 50). In Zeile 52 bis 67 werden die drei Menüpunkte mit gtk menu item new() angelegt und mit der Funktion gtk menu append() dem Pulldown-Menü hinzugefügt. In Zeile 69 erzeugen wir schließlich das Menü-Item mit dem Text Datei“ als Überschrift und verknüpfen ” es in Zeile 70 und 71 mit dem Pulldown-Menü. Als letzten Schritt fügen wir dieses Menü-Item dann in die Menüleiste ein (Zeile 72).
8.2 Das Toolkit GTK+ 237 Trennlinien zwischen Menüpunkten Manchmal sind Menüs für den Anwender übersichtlicher, wenn die Menüpunkte thematisch gruppiert und die Gruppen durch Trennlinien abgegrenzt sind. Ab Version 2.0 bietet GTK dafür ein spezielles Widget: GtkSeparatorMenuItem. Dieses Widget wird in gleicher Weise wie ein Menüpunkt (MenuItem) in das Pulldown-Menü eingefügt. Um dies am vorigen Programmbeispiel zu demonstrieren, fügen wir ab Zeile 63 folgende Quelltextzeilen ein: menuitem = gtk_separator_menu_item_new(); gtk_menu_append(GTK_MENU(menu), menuitem); Dadurch wird der letzte Menüpunkt ( Beenden“) von den anderen beiden durch ” eine Trennline abgegrenzt (siehe Abbildung 8.10). Abbildung 8.10: Menü mit Trennlinie (Separator) Menüpunkte und Schaltflächen deaktivieren Unter Umständen ist es erforderlich, dass bestimmte Menüpunkte oder Schaltflächen für den Anwender gesperrt sind. GTK stellt auch für diesen Zweck eine spezielle Funktion bereit: void gtk_widget_set_sensitive(GtkWidget *button, gboolean sensitive); Das Widget wird dann grau dargestellt, so dass der Benutzer erkennt, dass es nicht angewählt werden kann. Um beispielsweise die Schaltfläche button zu deaktivieren, lautet der Funktionsaufruf: gtk_widget_set_sensitive(button, FALSE); Mit der gleichen Funktion kann das Widget auch wieder aktiviert werden, wenn Sie als zweiten Parameter TRUE“ angeben. ”
238 8 Grafische Benutzeroberflächen 8.2.13 Pixmap-Grafiken darstellen Das Toolkit GTK bietet spezielle Funktionen, um Bitmap-Grafiken zu verarbeiten und darzustellen. Der X11-Server arbeitet intern mit so genannten PixmapStrukturen für die Verarbeitung von farbigen Bildern wie beispielsweise Icons. Diese Strukturen eignen sich aber nicht für Grafikdateien, daher stellt GTK Funktionen bereit, mit denen sich Bilder im XPM-Format in eine Pixmap umwandeln lassen: GdkPixmap* gdk_pixmap_create_from_xpm(GdkWindow *window, GdkBitmap **mask, GdkColor *transparent_color, const gchar *filename); GdkPixmap* gdk_pixmap_create_from_xpm_d(GdkWindow *window, GdkBitmap **mask, GdkColor *transparent_color, gchar **data); Der Unterschied zwischen diesen Funktionen besteht in der Quelle für die Grafikdaten: Während die erste Funktion sie aus der XPM-Datei mit dem angegebenen Dateinamen liest, erwartet die zweite als letzten Parameter einen Zeiger auf die Daten im Speicher. Dadurch haben Sie die Möglichkeit, kleinere Bilder wie etwa Icons direkt in den Quelltext einzubauen. Bitte beachten Sie, dass sowohl die Funktionsnamen als auch die Variablentypen mit gdk (statt gtk) beginnen! Es handelt sich dabei um Funktionen aus der libgdk, die auch zum GTK-Paket gehört. Beim Erstellen der Pixmap berücksichtigen beide Funktionen die Eigenschaften des X11-Displays bezüglich der Farbtiefe (Bits pro Pixel) und der Art der Farbverwaltung (feste Farbtabelle oder direkter Farbwert für jeden Pixel). Daher benötigen die Funktionen als ersten Parameter das Grafikelement (Typ GdkWindow), in dem die Pixmap später dargestellt werden soll. Ein solches GdkWindow ist Bestandteil der Struktur GtkWindow. Das XPM-Grafikformat unterstützt Transparenz, d. h. ein Farbwert kann als durchsichtig“ definiert werden. Damit ist es möglich, dass Grafikelemente eine ” beliebige Kontur haben, wie dies bei Desktop-Icons üblich ist. Die Struktur Pixmap bietet jedoch nicht die Möglichkeit einer transparenten Farbe. Daher legen beide Funktionen zusätzlich eine Bitmap (1 Bit pro Pixel) mit der Maske an und schreiben das Bitmap-Objekt in die Variable, auf die der Parameter mask zeigt. In dieser Maske haben alle Pixel den Wert 0, wo die XPM-Grafik transparent ist. Die anderen Pixel in der Maske haben den Wert 1. Da in XPM-Dateien für die transparenten Bereiche kein Farbwert“ angegeben ist, die Pixmap in diesen Bereichen ” jedoch irgendeine Farbe haben muss, verlangen beide Funktionen als dritten Parameter einen Farbwert für diese Bereiche.
8.2 Das Toolkit GTK+ 239 Hier ein Beispiel für den Funktionsaufruf zum Laden der XPM-Grafikdatei tux.xpm“ und Ersetzen der transparenten Farbe durch Weiß: ” GtkWidget *win; GdkPixmap *pixmap; GdkBitmap *mask; pixmap = gdk_pixmap_create_from_xpm(win->window, &mask, &window->style->white, "tux.xpm"); Um eine so erzeugte GdkPixmap in einem Fenster oder einem anderen Widget darzustellen, muss daraus ein GTK-Objekt – genauer gesagt ein Objekt vom Typ GtkPixmap – erzeugt werden. Dazu dient die Funktion GtkPixmap *gtk_pixmap_new(GdkPixmap *pixmap, GdkBitmap *mask); Eine GtkPixmap unterstützt Transparenz, daher verwendet diese Funktion die zuvor erzeugte Bitmap-Maske, um die transparenten Bereiche auszublenden. Bei Verwendung dieser Funktion spielt es im Grunde keine Rolle mehr, welche Farbe Sie für die transparenten Pixel beim Erzeugen der Pixmap gewählt haben, weil diese Pixel ohnehin ausgeblendet werden. Das folgende Programm zeigt diese Schritte im Zusammenhang: es erzeugt ein Fenster, lädt die als Kommandozeilenparameter angegebene XPM-Datei und stellt die Grafik in dem Fenster dar (siehe Abbildung 8.11). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /* view_xpm.c - XPM-Grafikdatei anzeigen */ # include <stdio.h> # include <string.h> # include <gtk/gtk.h> int main(int argc, char *argv[]) { GtkWidget *window, *gtk_pm; GdkPixmap *pixmap; GdkBitmap *mask; int width, height; /* Kommandozeile auswerten */
240 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 8 Grafische Benutzeroberflächen gtk_init(&argc, &argv); if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) { printf("Usage: view_xpm XPM-file\n"); return(1); } /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_window_set_default_size(GTK_WINDOW(window), 80, 40); gtk_window_set_title(GTK_WINDOW(window), argv[1]); gtk_widget_show(window); /* VOR gdk_pixmap_create_... */ /* Pixmap aus XPM-Datei laden */ pixmap = gdk_pixmap_create_from_xpm(window->window, &mask, &window->style->white, argv[1]); if (pixmap == NULL) { perror("view_xpm: Can’t load Pixmap"); return(1); } gdk_window_get_size(pixmap, &width, &height); printf("Image size: %d x %d\n", width, height); gtk_pm = gtk_pixmap_new(pixmap, mask); gtk_container_add(GTK_CONTAINER(window), gtk_pm); /* Fenster darstellen */ gtk_widget_show_all(window); gtk_main(); return(0); } Beachten Sie bitte, dass wir in Zeile 33 das Fenster bereits mit der Funktion gtk widget show() auf sichtbar“ einstellen. Dies ist erforderlich, weil zum Er” zeugen der Pixmap in Zeile 37 die Informationen über die Bit-Tiefe und Farben-
8.2 Das Toolkit GTK+ 241 zahl des X11-Displays erforderlich sind. Diese erhält das Fenster-Widget aber erst mit Aufruf der Funktion gtk widget show(). Abbildung 8.11: XPM-Bilddateien anzeigen mit view xpm Schaltflächen und Menüs mit Grafik Nachdem wir anhand des vorigen Beispiels gesehen haben, wie man farbige Pixmap-Grafiken aus XPM-Dateien erzeugt, zeigen wir Ihnen, wie man auch Schaltflächen und Menüpunkte mit einer Grafik statt mit einem Textlabel einrichtet. Zunächst müssen wir zum Erzeugen der Schaltfläche oder des Menüpunkts jeweils eine andere Funktion wählen als bisher: GtkWidget *gtk_button_new(); GtkWidget *gtk_menu_item_new(); Wie Sie sehen, fehlt bei diesen Funktionsnamen die Endung with label“ – ” in diesem Fall verwenden wir ja kein Textlabel. Danach benötigen wir eine GtkPixmap, die wir analog zum vorigen Beispiel aus einer XPM-Datei erzeugen können. Diese GtkPixmap fügen wir dann mit der bereits bekannten Funktion gtk container add() zum Container der Schaltfläche oder des Menüpunkts hinzu: GtkWidget button, menuitem, gtk_pixmap; button = gtk_button_new(); gtk_container_add(GTK_CONTAINER(button), gtk_pixmap); menuitem = gtk_menu_item_new(); gtk_container_add(GTK_CONTAINER(menuitem), gtk_pixmap); Wenn Sie eine Schaltfläche mit Text und Grafik versehen wollen, so müssen Sie in den Container der Schaltfläche zuerst eine vertikale oder horizontale Box (siehe Abschnitt 8.2.7) einfügen, die Sie dann mit der Grafik und einem Label füllen. Das Gleiche gilt natürlich auch für Menüpunkte.
242 8 Grafische Benutzeroberflächen Das folgende Programmbeispiel versieht eine Schaltfläche mit einer Grafik und einem Textlabel (siehe Abbildung 8.12). Abbildung 8.12: Schaltfläche mit Grafik und Text in einer hbox 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /* button-grafik.c - Schaltfläche mit Grafik und Text */ # include <stdio.h> # include <gtk/gtk.h> static const char *xpm_data[] = { "16 14 3 1", " c None", ". c #000000000000", "X c #FFFFFFFFFFFF", " ...... ", " .XXX.X. ", " .XXX.XX. ", " .XXX.XXX. ", " .XXX..... ", " .XXXXXXX. ", " .XXXXXXX. ", " .XXXXXXX. ", " .XXXXXXX. ", " .XXXXXXX. ", " .XXXXXXX. ", " .XXXXXXX. ", " ......... ", " "}; /*----- Callback-Funktion -----*/ void button_proc(GtkWidget *widget, gpointer data) { printf("Schaltfläche ’neue Datei’ betätigt.\n");
8.2 Das Toolkit GTK+ 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 243 return; } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *button, *hbox, *label, *gtk_pm; GdkPixmap *pixmap; GdkBitmap *mask; gtk_init(&argc, &argv); /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_container_set_border_width(GTK_CONTAINER(window), 30); gtk_widget_show(window); /* Schaltfläche erzeugen */ button = gtk_button_new(); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(button_proc), NULL); gtk_container_add(GTK_CONTAINER(window), button); /* hbox erzeugen */ hbox = gtk_hbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(button), hbox); /* Pixmap erzeugen */ pixmap = gdk_pixmap_create_from_xpm_d(window->window, &mask, &window->style->white, (gchar **)xpm_data); gtk_pm = gtk_pixmap_new(pixmap, mask); gtk_box_pack_start(GTK_BOX(hbox), gtk_pm, FALSE, FALSE, 0); /* label erzeugen und in hbox stellen */
244 77 78 79 80 81 82 83 84 85 86 87 88 89 8 Grafische Benutzeroberflächen label = gtk_label_new("neue Datei"); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 4); /* Objekte darstellen */ gtk_widget_show_all(window); gtk_main(); return(0); } 8.2.14 Zeichenflächen Nachdem wir verschiedene Bedienelemente verwendet und Bilder/Icons aus Grafikdateien dargestellt haben, wenden wir uns der echten“ Grafikausgabe zu – ” also dem Zeichnen von Grafikobjekten wie Linien und Kreisen. Für diesen Zweck stellt GTK ein Widget als Zeichenfläche“ zur Verfügung: GtkDrawingArea: ” GtkWidget *drawing; drawing = gtk_drawing_area_new(); gtk_drawing_area_size(GTK_DRAWING_AREA(drawing), 300, 200); Dabei handelt es sich um ein einfaches X11-Window, das in unsere GTKApplikation eingebettet wird. GTK selbst übernimmt bei diesen Elementen nicht das Wiederherstellen des Inhalts, wenn ein Teil dieser Zeichenfläche von einem anderen Fenster überdeckt war. Das muss unser Programm übernehmen. GTK stellt jedoch entsprechende Signale und Events zur Verfügung, die unser Programm informieren, wann welcher Bereich neu gezeichnet werden muss. Um diesen Mechanismus zu beschleunigen, legen wir eine Pixmap an, in der gezeichnet wird, und übertragen nur die Bereiche der Pixmap auf die Zeichenfläche, die es zu aktualisieren gilt. Auf diese Weise vermeiden wir, dass alle Zeichenfunktionen neu ausgeführt werden müssen, wenn eine kleine Ecke der Zeichenfläche durch ein anderes Fenster verdeckt war. Die Ereignisse, die wir mit einer Callback-Funktion verknüpfen müssen, sind das configure event“ und das ex” ” pose event“. Das configure event wird ausgelöst, wenn das Fenster zum ersten Mal geöffnet oder die Größe verändert wird. Wurde ein Teil des Fensters verdeckt und dann wieder sichtbar, wird das expose event ausgelöst. Beiden CallbackFunktionen übergeben wir einen Zeiger auf die Pixmap zum Zwischenspeichern der Grafik:
8.2 Das Toolkit GTK+ 245 GdkPixmap *pixmap; gtk_signal_connect(GTK_OBJECT(drawing), "expose_event", GTK_SIGNAL_FUNC(redraw), &pixmap); gtk_signal_connect(GTK_OBJECT(drawing), "configure_event", GTK_SIGNAL_FUNC(configure), &pixmap); Die Callback-Funktionen sehen wir uns am besten in dem Beispielprogramm an, das die in Abbildung 8.13 gezeigte Grafik erzeugt. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /* draw.c - Grafikausgabe mit GTK */ # include <gtk/gtk.h> /*----- Callback-Funktionen -----*/ gint configure(GtkWidget *widget, GdkEventConfigure *event, gpointer *pixmap) { gint width, height, depth; GdkGC *gc; GdkColor color; GdkColormap *colormap; GdkFont *font; if (*pixmap != NULL) gdk_pixmap_unref(*pixmap); width = widget->allocation.width; height = widget->allocation.height; depth = -1; /* Farbtiefe auf default */ *pixmap = gdk_pixmap_new(widget->window, width, height, depth); gdk_draw_rectangle(*pixmap, widget->style->white_gc, TRUE, 0, 0, width, height); gdk_draw_rectangle(*pixmap, widget->style->black_gc, FALSE, 0, 0, width-1, height-1); /* neuer GraphicContext (GC) für Farben */ gc = gdk_gc_new(widget->window); colormap = gdk_window_get_colormap(widget->window); /* Vordergrundfarbe orange */
246 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 8 Grafische Benutzeroberflächen color.red = 0xf000; color.green = 0xb000; color.blue = 0x0000; gdk_color_alloc(colormap, &color); gdk_gc_set_foreground(gc, &color); /* 3/4-Kreis zeichnen */ gdk_draw_arc(*pixmap, gc, TRUE, 20, 20, 100, 80, 0*64, 270*64); /* Vordergrundfarbe blaugrau */ color.red = 0x6000; color.green = 0x9000; color.blue = 0xb000; gdk_color_alloc(colormap, &color); gdk_gc_set_foreground(gc, &color); /* Text ausgeben */ font = gdk_font_load( "-*-times-bold-i-*-*-24-*-*-*-*-*-*-*"); gdk_draw_text(*pixmap, font, gc, 80, 85, "Dies ist ein Test.", 18); /* Vordergrundfarbe dunkelgrün */ color.red = 0x0000; color.green = 0xa000; color.blue = 0x0000; gdk_color_alloc(colormap, &color); gdk_gc_set_foreground(gc, &color); /* Dreieck zeichnen */ gdk_gc_set_line_attributes(gc, 7, GDK_LINE_SOLID, GDK_CAP_ROUND, GDK_JOIN_ROUND); gdk_draw_line(*pixmap, gc, 20, height-20, width-20, height-20); gdk_draw_line(*pixmap, gc, 20, height-20, width/2, height/2); gdk_draw_line(*pixmap, gc, width/2, height/2,
8.2 Das Toolkit GTK+ 247 80 width-20, height-20); 81 82 gdk_gc_destroy(gc); 83 return(TRUE); 84 } 85 86 gint redraw(GtkWidget *widget, GdkEventExpose *event, 87 gpointer *pixmap) 88 { 89 gdk_draw_pixmap(widget->window, 90 widget->style->white_gc, 91 *pixmap, 92 event->area.x, event->area.y, 93 event->area.x, event->area.y, 94 event->area.width, event->area.height); 95 return(FALSE); 96 } 97 98 /*----- Hauptprogramm -----*/ 99 100 int main(int argc, char *argv[]) 101 { 102 GtkWidget *window, *drawing; 103 GdkPixmap *pixmap; 104 gtk_init(&argc, &argv); 105 106 /* Fenster erzeugen */ 107 108 109 window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 110 gtk_signal_connect(GTK_OBJECT(window), "delete_event", 111 GTK_SIGNAL_FUNC(gtk_main_quit), NULL); 112 pixmap = NULL; 113 114 /* Zeichenfläche erzeugen */ 115 116 117 drawing = gtk_drawing_area_new(); 118 gtk_signal_connect(GTK_OBJECT(drawing), "expose_event", 119 GTK_SIGNAL_FUNC(redraw), &pixmap); 120 gtk_signal_connect(GTK_OBJECT(drawing), 121 "configure_event", 122 GTK_SIGNAL_FUNC(configure), &pixmap); 123 gtk_drawing_area_size(GTK_DRAWING_AREA(drawing),
248 124 125 126 127 128 129 130 131 132 133 134 8 Grafische Benutzeroberflächen 300, 200); gtk_container_add(GTK_CONTAINER(window), drawing); /* Objekte darstellen */ gtk_widget_show_all(window); gtk_main(); return(0); } Abbildung 8.13: Eine Zeichenfläche mit verschiedenen Grafikelementen Betrachten wir zunächst die Callback-Funktion redraw() in Zeile 86 bis 96. Diese Funktion wird aufgerufen, sobald ein Teil des Fensters neu gezeichnet werden muss. Als Parameter erhält die Funktion das Widget der Zeichenfläche, das Event, das die Funktion ausgelöst hat, und unsere Pixmap mit der zwischengespeicherten Grafik. In der Callback-Funktion wird mit gdk draw pixmap() der im Event angegebene Bereich aus unserer Pixmap in die Zeichenfläche kopiert. Die eigentlichen Zeichenfunktionen werden in der Funktion configure() in Zeile 9 bis 84 aufgerufen. Diese Callback-Funktion wird beim Starten des Programms und bei jedem Ändern der Fenstergröße aufgerufen. In Zeile 18 überprüfen wir zunächst, ob schon eine Pixmap als Zwischenspeicher angelegt wurde. Wenn ja, wird diese wieder entfernt. Danach fragen wir die aktuelle Größe der Zeichenfläche ab und erzeugen eine neue Pixmap mit dieser Größe (Zeile 23). In Zeile 25 und 26 zeichnen wir ein weiß ausgefülltes Rechteck über die gesamte Zeichenfläche und danach ein schwarzes Rechteck (nicht ausgefüllt) als Umrandung der Zeichenfläche (Zeile 27 und 28). Sehen wir uns diese Zeichenfunktion etwas näher an:
8.2 Das Toolkit GTK+ 249 void gdk_draw_rectangle(GdkDrawable *drawable, GdkGC *gc, gint filled, //TRUE or FALSE gint x, gint y, gint width, gint height); Als zweiten Parameter verlangt diese (wie auch fast alle anderen Zeichenfunktionen) einen so genannten Graphics Context (GC). Dieser enthält alle Informationen, die für das Zeichnen erforderlich sind: Farbe, Strichstärke, Schriftart für Text, Füllmuster usw. Beim Erzeugen der Zeichenfläche werden automatisch mehrere GCs angelegt und im Objekt hinterlegt – so auch white gc und blackgc. Für die weiteren Zeichenfunktionen erzeugen wir in Zeile 32 einen neuen Graphics Context, um andere Farben und Strichstärken einstellen zu können. In Zeile 58 und 59 laden wir eine Schriftart für die Textausgabe in Zeile 60 und 61. Die Zeichenkette zur Spezifikation der Schriftart sieht etwas kryptisch aus, aber mit Hilfe des Tools xfontsel“ (siehe Abbildung 8.14) können Sie eine Schriftart ” wählen, deren Beschreibungs-String dann angezeigt wird. Abbildung 8.14: Übersicht der Schriftarten unter X11 mit xfontsel“ ” Neben den in unserem Programm verwendeten Zeichenfunktionen stellt GTK eine Reihe weiterer Funktionen zur Verfügung. Hier eine Übersicht: gdk_draw_rectangle() gdk_draw_arc() gdk_draw_polygon() gdk_draw_string() gdk_draw_text() gdk_draw_pixmap() gdk_draw_bitmap() gdk_draw_image() gdk_draw_points() gdk_draw_segments() Die Parameter dieser Funktionen können Sie der Include-Datei <gdk/gdk.h>“ ” entnehmen.
250 8 Grafische Benutzeroberflächen 8.2.15 Zeichenfläche mit Rollbalken Für große Zeichenflächen empfiehlt es sich, dass der Anwender das Fenster kleiner ziehen und mit Rollbalken (Scrollbars) am Rand der Zeichenfläche den gewünschten Bereich anscrollen kann. Bei GTK erreichen Sie das mit Hilfe eines Scrolled Window, in das die Zeichenfläche eingebettet wird. Das folgende Programm realisiert eine solche Zeichenfläche mit fester Größe und Rollbalken am Rand (siehe Abbildung 8.15): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /* draw2.c - Zeichenfläche mit Rollbalken */ # include <gtk/gtk.h> # define WIDTH 500 # define HEIGHT 500 /* Größe der Zeichenfläche */ /*----- Callback-Funktionen -----*/ gint configure(GtkWidget *widget, GdkEventConfigure *event, gpointer *pixmap) { gint depth, i; if (*pixmap != NULL) gdk_pixmap_unref(*pixmap); depth = -1; /* Farbtiefe auf default */ *pixmap = gdk_pixmap_new(widget->window, WIDTH, HEIGHT, depth); gdk_draw_rectangle(*pixmap, widget->style->white_gc, TRUE, 0, 0, WIDTH, HEIGHT); for (i=0; i<=50; i++) gdk_draw_line(*pixmap, widget->style->black_gc, 10*i, 0, WIDTH-1-10*i, HEIGHT-1); return(TRUE); } gint redraw(GtkWidget *widget, GdkEventExpose *event, gpointer *pixmap) { gdk_draw_pixmap(widget->window, widget->style->white_gc, *pixmap, event->area.x, event->area.y,
8.2 Das Toolkit GTK+ 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 251 event->area.x, event->area.y, event->area.width, event->area.height); return(FALSE); } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *scr_win, *drawing; GdkPixmap *pixmap; gtk_init(&argc, &argv); /* Fenster erzeugen */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); /* Fenster mit Rollbalken */ scr_win = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(window), scr_win); /* Zeichenfläche erzeugen */ pixmap = NULL; drawing = gtk_drawing_area_new(); gtk_signal_connect(GTK_OBJECT(drawing), "expose_event", GTK_SIGNAL_FUNC(redraw), &pixmap); gtk_signal_connect(GTK_OBJECT(drawing), "configure_event", GTK_SIGNAL_FUNC(configure), &pixmap); gtk_drawing_area_size(GTK_DRAWING_AREA(drawing), 500, 500); gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW(scr_win), drawing); /* Objekte darstellen */
252 81 82 83 84 85 86 87 8 Grafische Benutzeroberflächen gtk_widget_show_all(window); gtk_main(); return(0); } Abbildung 8.15: Zeichenfläche mit fester Größe und Rollbalken 8.2.16 Dateiauswahlfenster Als letztes GTK-Objekt möchten wir ein Dateiauswahlfenster vorstellen. Dieses Widget wird mit GtkWidget *gtk_file_selection_new(const gchar *title); erzeugt und enthält verschiedene Schaltflächen zum Löschen oder Umbenennen von Dateien sowie das Eingabefeld für den Dateinamen und zwei Listen für die Verzeichnisse und die Dateien (siehe Abbildung 8.16). Nach dem Erzeugen des Dateiauswahlfensters mit GtkWidget* filesel = gtk_file_selection_new("Datei laden"); können Sie auf die Schaltflächen für OK“ und für Abbrechen“ als Elemente der ” ” GtkFileSelection-Struktur zugreifen: GTK_FILE_SELECTION(filesel)->ok_button GTK_FILE_SELECTION(filesel)->cancel_button Beide Schaltflächen müssen mit einer entsprechenden Callback-Funktion verknüpft werden. Wenn der Benutzer den OK-Button betätigt hat, können Sie den gewählten Dateinamen mit der Funktion
8.2 Das Toolkit GTK+ 253 Abbildung 8.16: Das GTK-Dateiauswahlfenster gchar *gtk_file_selection_get_filename( GtkFileSelection *filesel); auswerten. Das folgende Programm erzeugt ein kleines Fenster mit einer Datei ” laden“-Schaltfläche. Durch Anklicken dieser Schaltfläche öffnet sich das Dateiauswahlfenster. Wählt der Benutzer eine Datei aus, gibt das Programm den Dateinamen im Shell-Fenster aus. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* file_select.c - Dateiauswahlfenster */ # include <stdio.h> # include <gtk/gtk.h> /*----- Callback-Funktion -----*/ void file_ok_proc(GtkWidget *widget, gpointer data) { printf("Datei laden: ’%s’\n", gtk_file_selection_get_filename( GTK_FILE_SELECTION(data))); gtk_widget_hide(data);
254 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 8 Grafische Benutzeroberflächen return; } /*----- Hauptprogramm -----*/ int main(int argc, char *argv[]) { GtkWidget *window, *filesel, *button; gtk_init(&argc, &argv); /* Fenster einrichten */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(window), "delete_event", GTK_SIGNAL_FUNC(gtk_main_quit), NULL); gtk_container_set_border_width(GTK_CONTAINER(window), 30); /* Dateiauswahlfenster erzeugen */ filesel = gtk_file_selection_new ("Datei laden"); gtk_signal_connect(GTK_OBJECT(filesel), "delete_event", GTK_SIGNAL_FUNC(gtk_widget_hide), &filesel); gtk_signal_connect( GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked", GTK_SIGNAL_FUNC(file_ok_proc), filesel); gtk_signal_connect_object( GTK_OBJECT(GTK_FILE_SELECTION(filesel)->cancel_button), "clicked", GTK_SIGNAL_FUNC(gtk_widget_hide), GTK_OBJECT(filesel)); /* Schaltfläche erzeugen */ button = gtk_button_new_with_label(" Datei laden "); gtk_signal_connect_object(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(gtk_widget_show), GTK_OBJECT(filesel)); gtk_container_add(GTK_CONTAINER(window), button); gtk_widget_show(button); /* Hauptfenster darstellen */
8.2 Das Toolkit GTK+ 60 61 62 63 64 65 255 gtk_widget_show(window); gtk_main(); return(0); } In den Zeilen 44 und 52 verwenden wir eine andere Funktion als bisher zur Verknüpfung eines GTK-Elements mit einer Callback-Funktion: guint gtk_signal_connect_object(GtkObject *object, const gchar *name, GtkSignalFunc func, GtkObject *slot_object); Im Unterschied zu gtk signal connect() (vgl. Seite 212) wird der CallbackFunktion nur ein Parameter übergeben: das slot object. Auf diese Weise können wir die Funktionen gtk widget show() und gtk widget hide() direkt als Callback-Funktionen einsetzen (Zeile 46 und 53), um das angegebene Widget darzustellen oder auszublenden. Insofern benötigen wir nur eine einzige Callback-Funktion (Zeile 10 bis 17) für das Programm. Wie bereits im Beispiel aus Abschnitt 8.2.9 werden auch hier alle GTK-Objekte schon bei Programmstart erzeugt; das Dateiauswahlfenster wird zunächst jedoch nicht mit gtk widget show() sichtbar“ gemacht. Dies geschieht erst bei Betätigung der ” Schaltfläche Datei laden“. ” 8.2.17 Umlaute und Sonderzeichen Wie bereits in Abschnitt 8.2.1 erwähnt, unterstützt das Toolkit GTK+ in der Version 2.0 UTF-8, um Sonderzeichen wie deutsche Umlaute darzustellen. Daher müssen Sie beim Schreiben von Programmen darauf achten, dass diese im UTF8-Format abgespeichert werden. Dies bedeutet aber gleichzeitig, dass Buchstaben und Zeichen mehrere Bytes lang sein können! So besteht das Wort für“ beispiels” weise im UTF-8-Format aus vier Zeichen: f“ 0xC3 0xBC r“. ” ” 8.2.18 Wie geht es weiter? Im Rahmen dieses Kapitels konnten wir nicht alle Funktionen und Objekte von GTK vorstellen und erläutern, sondern lediglich einen Einstieg in das Thema geben. Eine vollständige Beschreibung der vielen weiteren Möglichkeiten des Toolkits finden Sie in den Tutorials der GTK-Homepage [19] und in weiterführender Literatur, z. B. [18].
256 8 Grafische Benutzeroberflächen 8.3 Grafik ohne X11 mit der SVGALIB Für Grafikausgaben unter Linux ist das X11-System der Standard. Nicht zuletzt aufgrund der Client-Server-Struktur und der Unterstützung für viele verschiedene Grafikkarten ist das System jedoch sehr mächtig – für sehr kleine LinuxRechner wie Embedded Computer unter Umständen zu mächtig. Mit Hilfe der Funktionsbibliothek libvga“ (das Paket wird auch als SVGALIB“ bezeichnet) ist ” ” eine einfache Grafikausgabe auch ohne das X11-System möglich. Allerdings sei darauf hingewiesen, dass die libvga natürlich kein gleichwertiger Ersatz für X11 sein kann. Sie bietet nur einen begrenzten Funktionsumfang, beispielsweise für das Zeichnen von Linien und das Einstellen der Farben. Es gibt aber mittlerweile sogar ein 3D-Toolkit für die Darstellung komplexer 3D-Elemente mit der libvga. (Zum Download der SVGALIB siehe [20].) 8.3.1 Besonderheiten beim Arbeiten mit der libvga Um die Grafikmodi einstellen und auf den Grafikspeicher zugreifen zu können, muss die libvga entsprechende Hardware-Zugriffe ausführen. Diese sind nor” malen“ Benutzern nicht gestattet, sodass Programme, die die libvga benutzen, zum Teil root-Privilegien benötigen (siehe auch Kapitel 9). Dies birgt naturgemäß das Risiko eines Systemabsturzes bei fehlerhaften Programmen. Seit der Version 1.9.0 wurde der Teil der libvga, der auf die Hardware zugreift, als KernelModul ( svgalib helper“) ausgelagert. Die Programme kommunizieren mit die” sem Kernel-Modul über das Device /dev/svga und benötigen dementsprechend auch nur die Zugriffsrechte für dieses Device. Des Weiteren ist zu beachten, dass bei Verwendung hochauflösender Grafikmodi auf nicht unterstützten Grafikkarten der ursprüngliche Textmodus möglicherweise nicht wiederhergestellt werden kann und so ein Neustart des Rechners erforderlich wird. Aus diesem Grund sollte man Programme für die libvga auf Rechnern mit laufendem X11-System entwickeln, auch wenn dies zunächst widersinnig erscheint. In der Regel ist der X11-Server jedoch in der Lage, die Grafikeinstellungen für die X11-Oberfläche wiederherzustellen. Sollte ein auf die libvga aufsetzendes Programm nicht automatisch zur X11-Oberfläche zurückkehren, kann man mit der Tastenkombination Strg-Alt-F7 nachhelfen“. ” Wenn libvga-Programme ohne einen X11-Server im Hintergrund entwickelt werden, sollte man zumindest mit dem Shell-Kommando savetextmode zuvor die Einstellungen des Konsole-Textmodus sichern. Werden die Einstellungen von einem fehlerhaften libvga-Programm verstellt, sodass das Programm nach Beendigung den Textmodus nicht mehr korrekt einstellt, können mit dem Shell-Kommando
8.3 Grafik ohne X11 mit der SVGALIB 257 textmode die zuvor mit savetextmode gesicherten Einstellungen wiederhergestellt werden. Der Befehl muss jedoch unter Umständen blind“ eingegeben werden, wenn ” der Textmodus zerstört wurde. Sicherer ist auf jeden Fall die Verwendung eines X11-Servers im Hintergrund. Noch ein Hinweis zur Verwendung der libvga: Die Funktionsbibliothek benutzt die Signale SIGUSR1 und SIGUSR2. Programme, die auf die libvga aufsetzen, dürfen diese Signale daher nicht verwenden. 8.3.2 Ein erstes Beispiel Nach der langen Vorrede soll jetzt ein einfaches Beispiel die Verwendung der libvga demonstrieren: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /* lines.c - Linien mit der SVGALIB darstellen */ # include <stdio.h> # include <vga.h> int main() { int i; if (vga_init()) return(1); if (vga_setmode(G640x480x16)) return(1); vga_clear(); for (i=0; i<64; i++) { vga_setcolor(i % 16); vga_drawline(i*10, 0, 639-i*10, 479); } vga_getch(); return(0); } /* auf Taste warten */
258 8 Grafische Benutzeroberflächen Um aus diesem Quelltext ein lauffähiges Programm zu generieren, muss zum einen die libvga beim Übersetzen mit eingebunden werden. Zum anderen muss man dem Programm ermöglichen, auf die Register der Grafik-Hardware zuzugreifen (vgl. Abschnitt 9.1.1): > gcc lines.c -lvga -o lines > su Kennwort: # chown root lines # chmod a+s lines # exit Danach kann das Programm mit lines“ gestartet werden und sollte das in Ab” bildung 8.17 dargestellte Bild erzeugen.1 Durch Drücken einer beliebigen Taste wird es beendet. Abbildung 8.17: Linien auf der Konsole zeichnen mit der libvga Bei diesem Beispiel wird als Erstes die Funktion vga init() aufgerufen (Zeile 12), mit der die Benutzung der libvga initialisiert wird. Kehrt diese Funktion mit einem Wert 6= 0 zurück, ist der Initialisierungsvorgang gescheitert. In Zeile 15 wird die Grafikauflösung eingestellt, in diesem Fall 640 × 480 Punkte mit 16 Farben. Einige der möglichen anderen Modi sind: 1 Auf einer der getesteten Linux-Installationen ließen sich die libvga-Programme trotz dieser Einstellungen nur von dem Benutzer root“ ausführen. ”
8.3 Grafik ohne X11 mit der SVGALIB Modus 0 1 4 5 6 9 10 11 12 13 18 19 21 22 259 Alias Auflösung Farben TEXT G320x200x16 G640x480x16 G320x200x256 G320x240x256 G640x480x2 G640x480x256 G800x600x256 G1024x768x256 G1280x1024x256 G640x480x64K G640x480x16M G800x600x64K G800x600x16M – 320 × 200 640 × 480 320 × 200 320 × 240 640 × 480 640 × 480 800 × 600 1024 × 768 1280 × 1024 640 × 480 640 × 480 800 × 600 800 × 600 – 16 16 256 256 2 256 256 256 256 65.536 16.777.216 65.536 16.777.216 Doch Vorsicht! Nur wenn Ihre Grafikkarte von der libvga unterstützt wird und den angegebenen Modus auch ermöglicht, sollten höhere Auflösungen verwendet werden. Unproblematisch sind die Modi 1, 4, 5, 6 und 9. Diese sollten auf allen VGA-Karten laufen. Mit dem Modus TEXT“ kann auf den Konsole-Textmodus ” umgeschaltet werden, um beispielsweise (Fehler-)Meldungen auszugeben. Statt den Grafikmodus im Programm fest vorzugeben, kann auch mit der Funktion int vga getdefaultmode(void); die Voreinstellung abgefragt werden. Diese lässt sich vom Anwender mit Hilfe der Umgebungsvariablen SVGALIB DEFAULT MODE vorgeben, z.B.: export SVGALIB DEFAULT MODE=G640x480x16 Die Geometrie und Farbenzahl des eingestellten Modus lässt sich über die Funktionen vga getxdim() und vga getydim() feststellen: int mode, width, height, colors; mode = vga_getdefaultmode(); if (vga_setmode(mode)) return(1); width = vga_getxdim(); height = vga_getydim(); colors = vga_getcolors(); Doch zurück zum Beispielprogramm: Nach dem Einstellen des Grafikformates wird in Zeile 18 zunächst der Grafikspeicher gelöscht. Danach werden mit vga drawline() Linien in der jeweils mit vga setcolor() eingestellten Farbe
260 8 Grafische Benutzeroberflächen gezeichnet. Bei einer Farbenzahl von bis zu 256 ist als Parameter der Farbindex einzusetzen – z.B. 0 für die Hintergrundfarbe. Bei True-Color-Modi (65536 Farben und mehr) muss stattdessen die Funktion vga setrgbcolor() verwendet werden: void vga_setrgbcolor(int red, int green, int blue); Damit werden die Farbanteile für Rot, Grün und Blau direkt angegeben, wobei sich die Werte zwischen 0 und 63 bewegen müssen. Im 16-Farben-Modus sind die voreingestellten Farben: Schwarz, Dunkelblau, Dunkelgrün, Türkis, Dunkelrot, Violett, Braun, Hellgrau, Dunkelgrau, Blau, Grün, Helltürkis, Rot, Hellviolett, Orange und Weiß. In Zeile 26 wird mit der Funktion vga getch() auf eine Tastatureingabe gewartet, bevor das Programm beendet wird. 8.3.3 Mit Perspektive: 3D-Funktionen zeichnen Als Anwendungsbeispiel für die libvga soll im Folgenden ein Programm zum Zeichnen von dreidimensionalen Funktionen vorgestellt werden: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* plot3d.c - 3D-Funktion zeichnen */ # include <stdio.h> # include <vga.h> # define f(x, y) (0.1/(0.1+(x)*(x)+(y)*(y))) int main() { int i, j, xpos, ypos, xpos_old, ypos_old; double x, y, z; if (vga_init()) return(1); if (vga_setmode(G640x480x16)) return(1); vga_setpalette(0, 16, 44, 63); vga_setpalette(1, 0, 0, 0); vga_clear(); vga_setcolor(1); /* hellblau */ /* schwarz */
8.3 Grafik ohne X11 mit der SVGALIB 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 261 for (i=0; i<=40; i++) /* for (j=0; j<=200; j++) { x = 0.01*j-1.0; y = 1.0-0.05*i; z = f(x, y)-2; xpos = 320+900*x/(y+4); ypos = -600*z/(y+4); if (j > 0) vga_drawline(xpos_old, xpos_old = xpos; ypos_old = ypos; } for (j=0; j<=40; j++) /* for (i=0; i<=200; i++) { x = 0.05*j-1.0; y = 1.0-0.01*i; z = f(x, y)-2; xpos = 320+900*x/(y+4); ypos = -600*z/(y+4); if (i > 0) vga_drawline(xpos_old, xpos_old = xpos; ypos_old = ypos; } vga_getch(); return(0); } horizontale Linien */ ypos_old, xpos, ypos); vertikale Linien */ ypos_old, xpos, ypos); /* auf Taste warten */ Wird dieses Programm analog zu Abschnitt 8.3.2 übersetzt und gestartet, sollte die in Abbildung 8.18 dargestellte Grafik erscheinen. Da hier die Programmierung mit der libvga im Vordergrund steht, soll die für die 3D-Darstellung erforderliche Mathematik nur im Ansatz beschrieben werden. Die zu zeichnende Funktion f ( x, y) ist in Zeile 8 definiert – hier der Einfachheit halber mit Hilfe einer define-Anweisung. Das Programm ist so ausgelegt, dass sich die Werte für x und y zwischen –1 und 1 bewegen. Der Funktionswert liegt im Intervall von 0 bis 1. Bei der hier gewählten 3D-Darstellung verläuft die xAchse von links nach rechts und die y-Achse von vorn nach hinten (senkrecht zur Ebene des Monitors). Der Funktionswert entspricht der z-Koordinate, die bei der 3D-Darstellung von unten nach oben läuft.1 1 Bei den meisten 3D-Bibliotheken verläuft die z-Achse senkrecht zur Monitor-Ebene, bei 3DFunktionen wird der Funktionswert jedoch im Allgemeinen auf der z-Achse abgetragen.
262 8 Grafische Benutzeroberflächen Abbildung 8.18: 3D-Funktion plotten mit der libvga In den Zeilen 25 bis 51 werden die Gitternetzlinien gezeichnet, zuerst die horizontalen, dann die vertikalen Linien. In den Zeilen 32 und 33 bzw. 45 und 46 werden die 3D-Koordinaten ( x, y, z) in Bildschirmkoordinaten xpos und ypos umgerechnet. Dabei beruht die Perspektive im Prinzip auf der Anwendung des Strahlensatzes. 8.3.4 Ein kleines Malprogramm Bislang haben wir die libvga nur zur Grafikausgabe verwendet – abgesehen von der Tastaturabfrage am Programmende. Natürlich unterstützt auch die libvga die Maus als Eingabegerät. Dazu muss der Bibliothek jedoch mitgeteilt werden, welche Art Maus an welchem Anschluss verwendet wird, z.B. eine Microsoft-kompatible Maus an COM1 oder eine MouseSystems 3-TastenMaus am PS/2-Anschluss. Die Konfiguration der Maus sollte in der Datei /etc/vga/libvga.config“ vom Benutzer eingestellt sein und in der Regel ” vom Programm beibehalten werden. Die Mausunterstützung wird in diesem Fall aktiviert mit der Funktion vga setmousesupport(1); Alternativ können Maustyp und Schnittstelle mit der Funktion mouse init() auch explizit vorgegeben werden: int mouse_init(char *dev, int type, int samplerate);
8.3 Grafik ohne X11 mit der SVGALIB 263 Beispiel: mouse_init("/dev/mouse", MOUSE_MICROSOFT, 60); Dabei sollte entweder vga setmousesupport() oder mouse init() verwendet werden, nicht beide Funktionen. Ein typischer Wert für die samplerate ist 60, alternativ kann auch MOUSE DEFAULTSAMPLERATE eingesetzt werden. Nach Einschalten der Mausunterstützung erscheint kein Mauszeiger! Diesen muss das Programm selbst realisieren. Die libvga stellt lediglich die Funktionen mouse getx() und mouse gety() zur Abfrage der Mausposition sowie mouse getbutton() zum Auswerten der Maustasten zur Verfügung. Die Mausunterstützung funktioniert nur, wenn der Benutzer Lese- und Schreibzugriff auf das entsprechende Device hat (z.B. /dev/psaux“ für eine Maus ” am PS/2-Anschluss). Ferner muss der Aufruf von vga setmousesupport() bzw. mouse init() vor vga setmode() erfolgen. Das folgende kleine Malprogramm soll die Benutzung der Mausfunktionen der libvga demonstrieren: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* vga_draw.c - SVGALIB-Malprogramm */ # include <stdio.h> # include <vga.h> # include <vgamouse.h> int main() { int x, y, x_old, y_old, i, color; static unsigned char background[21][21]; if (vga_getmousetype() == MOUSE_NONE) { fprintf(stderr, "No mouse configured.\n"); return(1); } if (vga_init()) return(1); vga_setmousesupport(1); /* Maus vorbereiten */
264 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 8 Grafische Benutzeroberflächen if (vga_setmode(G640x480x16)) return(1); vga_setpalette(0, 24, 48, 60); vga_setpalette(1, 0, 0, 0); /* Hellblau */ /* Schwarz */ vga_clear(); for (i=0; i<448; i++) /* Palette zeichnen */ { vga_setcolor(1+i/32); vga_drawline(0, i, 32, i); } vga_setcolor(1); /* Box mit aktueller Farbe */ vga_drawline(1, 478, 1, 449); vga_drawline(1, 478, 31, 478); vga_drawline(31, 478, 31, 449); vga_drawline(1, 449, 31, 449); color = 5; /* aktuelle Farbe */ vga_setcolor(color); for (i=451; i<477; i++) vga_drawline(3, i, 29, i); x = x_old = 320; y = y_old = 240; /* Startkoordinaten */ mouse_setposition(x, y); mouse_setxrange(10, 629); mouse_setyrange(10, 469); mouse_setscale(32); /* Maus einstellen */ for (i=0; i<21; i++) vga_getscansegment(background[i], x-10, y-10+i, 21); vga_setcolor(1); vga_drawline(x-10, y, x+10, y); vga_drawline(x, y-10, x, y+10); do { mouse_waitforupdate(); for (i=0; i<21; i++) vga_drawscansegment(background[i], x-10, y-10+i, 21);
8.3 Grafik ohne X11 mit der SVGALIB 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 265 x = mouse_getx(); y = mouse_gety(); if (mouse_getbutton() == MOUSE_LEFTBUTTON) { if (x > 32) /* Zeichenflaeche */ { vga_setcolor(color); vga_drawline(x_old, y_old, x, y); x_old = x; y_old = y; } else /* Farbauswahl */ { color = y/32+1; vga_setcolor(color); for (i=451; i<477; i++) vga_drawline(3, i, 29, i); } } else { x_old = x; y_old = y; } for (i=0; i<21; i++) vga_getscansegment(background[i], x-10, y-10+i, 21); vga_setcolor(1); vga_drawline(x-10, y, x+10, y); vga_drawline(x, y-10, x, y+10); } while (mouse_getbutton() != MOUSE_RIGHTBUTTON); return(0); } In Zeile 12 wird ein Feld von 21 × 21 Farbwerten angelegt. Dieses wird zum Sichern des Hintergrundes unter dem Mauszeiger“ verwendet. Die Mausun” terstützung wird in Zeile 23 aktiviert. Danach erfolgt das Zeichnen der Farbpalette am linken Bildschirmrand. Mit mouse setposition() wird in Zeile 51 die Anfangsposition des Mauszeigers initialisiert. In den beiden folgenden Zeilen wird der Bereich, in dem sich die Mauskoordinaten bewegen können, eingeschränkt. Das ist normalerweise nicht erforderlich, dann muss man sich jedoch um das Clipping“ (Abschneiden) des Mauszeigers an den Bildschirmrändern kümmern. ”
266 8 Grafische Benutzeroberflächen Als letzte Vorbereitung wird in Zeile 54 die Geschwindigkeit“ der Maus einge” stellt: Je kleiner der Parameter von mouse setscale() ist, desto schneller bewegt sich anschließend der Mauszeiger. Mit der for-Schleife (Zeile 56 bis 58) wird die Grafik in dem Bereich gesichert, der durch das Zeichnen des Mauszeigers verändert wird. Dazu wird die Funktion vga scansegment() verwendet, mit der die Farbwerte von mehreren aufeinanderfolgenden Bildpunkten ausgelesen werden können: void vga_scansegment(unsigned char *colors, int x, int y, int width); Der Mauszeiger selbst – hier als Fadenkreuz ausgeführt – wird in den Zeilen 60 und 61 gezeichnet. Danach folgt der Kern des Programms: die do-whileSchleife, in der die Mausposition abgefragt und auf das Drücken der Maustasten reagiert wird. Das Programm kann durch Betätigung der rechten Maustaste beendet werden (siehe Zeile 100). Am Anfang des Schleifenrumpfes hält die Funktion mouse waitforupdate() den Prozess so lange an, bis eine Mausbewegung (bzw. das Drücken einer Maustaste) erfolgt. Mit der for()-Schleife in den Zeilen 66 bis 68 wird dann zunächst die ursprüngliche Grafik unter dem Mauszeiger wiederhergestellt. Dazu bedient sich das Programm der Funktion vga drawscansegment(), die als Pendant zu vga getscansegment() eine Folge von Farbwerten auf eine Reihe von Bildschirmpunkten überträgt. Danach folgt die Auswertung der Mausposition und -tasten (Zeile 69 bis 92), das Sichern des Hintergrundes an der neuen Mausposition (Zeile 93 bis 95) und zuletzt das Neuzeichnen des Mauszeigers (Zeile 96 bis 98). 8.3.5 Erweiterte Funktionen mit der libvgagl Wie Sie vermutlich bereits bemerkt haben, ist der Funktionsumfang der libvga relativ gering; sie erlaubt das Zeichnen von Punkten und Linien – Funktionen zur Darstellung von Kreisen oder Text enthält sie nicht. Werden komplexere Grafikfunktionen benötigt, so kann auf eine Erweiterung der libvga zurückgegriffen werden: die libvgagl. Diese Funktionsbibliothek setzt auf die libvga auf und bietet eine Vielzahl weiterer Grafikfunktionen. Sie ermöglicht auch die Verwendung von Grafikbeschleunigern (Blitter oder Accelerator). Leider setzt die libvgagl eine Grafikauflösung mit 8, 16, 24 oder 32 Bit pro Pixel voraus, und auch hier werden einige Grafikmodi nicht voll unterstützt. Es gibt jedoch einen Trick, mit dem man Funktionen der libvgagl auf alle von der libvga zur Verfügung gestellten Grafikmodi anwenden kann. Dazu muss zunächst in einem virtuellen Grafikspeicher gezeichnet und dann mit der Funktion gl copyscreen() die Grafik in den Bildspeicher kopiert werden. Das folgende Programm nutzt dieses Verfahren, um verschiedene Funktionen der libvgagl bei einem Grafikmodus von 640 × 480 Punkten und 16 Farben anzuwenden:
8.3 Grafik ohne X11 mit der SVGALIB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 267 /* vgagl_demo.c - komplexere Grafik mit der LIBVGAGL */ # include <stdio.h> # include <vga.h> # include <vgagl.h> int main() { int mode; GraphicsContext gc; if (vga_init()) return(1); mode = G640x480x16; if (vga_setmode(mode)) return(1); gl_setcontextvga(mode); gl_getcontext(&gc); /* GraphicsContext holen */ vga_setpalette(0, 58, 58, 58); vga_clear(); gl_setcontextvgavirtual(mode); gl_setfont(8, 8, gl_font8x8); /* Text vorbereiten */ gl_setwritemode(FONT_COMPRESSED | WRITEMODE_OVERWRITE); gl_setfontcolors(15, 1); gl_write(10, 10, "Dies ist ein Test."); gl_printf(10, 20, "Breite=%d", vga_getxdim()); gl_circle(320, 240, 100, 1); gl_fillbox(10, 400, 100, 70, 5); gl_copyscreen(&gc); /* Grafik ins Video-RAM */ vga_getch(); /* auf Taste warten */ return(0); }
268 8 Grafische Benutzeroberflächen Beim Übersetzen des Programms muss auch die libvgagl eingebunden werden. Dies sollte vor dem Einbinden der libvga geschehen: gcc vgagl demo.c -lvgagl -lvga -o vgagl demo Bevor das Programm gestartet werden kann, müssen auch für dieses Beispiel zunächst die Schritte aus Abschnitt 8.3.2 durchgeführt werden, um den Zugriff auf die Hardware zu ermöglichen. In Zeile 12 des Quelltextes wird eine Variable vom Typ GraphicsContext (gc) definiert. Anders als bei X11 und GTK beinhaltet diese Struktur keine Informationen über den Zeichenmodus, sondern beschreibt die Eigenschaften des eingestellten Grafikmodus (Breite, Höhe, Anzahl der Farben, . . . ). Nachdem die Grafik wie bei den vorherigen Beispielprogrammen initialisiert wurde (Zeile 14 bis 19), wird in Zeile 20 und 21 der gleiche Grafikmodus auch für die Funktionen der libvgagl eingestellt und der zugehörige Graphics Context gesichert. Vor Benutzung der libvgagl-Funktionen wird in Zeile 26 dann auf einen virtuellen Bildspeicher der gleichen Größe und Farbtiefe umgeschaltet. Erst in Zeile 38 wird schließlich der Inhalt des virtuellen Bildspeichers in den tatsächlichen Grafikspeicher kopiert. Hier benötigt man als Ziel“ für die Funktion gl copyscreen() den zuvor gesi” cherten Graphics Context. Wie schon an diesem kleinen Beispielprogramm erkennbar ist, stellt die libvgagl deutlich komfortablere Funktionen als die libvga zur Verfügung. So kann beispielsweise mit gl printf() ein formatierter Text in die Grafik geschrieben werden. Auch das Zeichnen von Kreisen und ausgefüllten Rechtecken ist mit jeweils einem Befehl möglich. 8.3.6 Weitere Informationsquellen Die Nutzung der libvga, insbesondere der libvgagl, konnte im Rahmen dieses Buches nur angerissen werden. Weitere, ausführlichere Informationen bieten zum einen die man“-Hilfeseiten ” man svgalib man vgagl man libvga.config Zum anderen stellt die Organisation, die die libvga pflegt und weiterentwickelt, eine Internet-Seite (siehe [20]) mit Beispielen und einem Tutorial zur Verfügung. Die Version 1.9.25 enthält zusätzlich die Funktionsibliothek lib3dkit“ zum Zeich” nen von 3D-Objekten mit Schattierung und Lichteffekten. Abbildung 8.19 zeigt den Screenshot des Demo-Programms plane“, das ebenfalls in dem Quelltextpa” ket der Version 1.9.25 enthalten ist.
8.3 Grafik ohne X11 mit der SVGALIB 269 Abbildung 8.19: Die lib3dkit“ erlaubt das Zeichnen komplexer 3D-Objekte – hier am Bei” spiel des Demo-Programms plane“. ”

Kapitel 9 Hardware-Programmierung In der Anfangszeit der Home-Computer“ – wie etwa eines C 64“ – war es durch” ” aus üblich, durch direkte Zugriffe auf die Hardware des Computers (unter Umgehung des Betriebssystems) die maximale Performance der Programme zu erreichen. Bei modernen, komplexen Betriebssystemen auf heutigen Computern ist solche Hardware-nahe Programmierung eigentlich verpönt – das Betriebssystem verweigert sie dem normalen“ Benutzer sogar: Zugriffe auf Adressen außerhalb ” des zugewiesenen Speichers oder gar auf I/O-Ports werden strikt mit der Meldung Segmentation fault“ (bzw. Speicherzugriffsfehler“) abgelehnt. ” ” Möchte man jedoch auf Hardware zugreifen, für die es (noch) keinen Treiber gibt, oder will man einfach mal ausprobieren, was bestimmte Hardware-Komponenten alles hergeben“ können, so sind direkte Hardware-Zugriffe eine mögliche Alter” native. Aber Vorsicht: Direkte (Schreib-)Zugriffe auf die Hardware des Computers können zu Systemabstürzen und damit unter Umständen auch zu Datenverlust führen! Generell sollte daher für das Schreiben solcher Programme gelten: Be careful!“ ” 9.1 Hardware-nahe Programme schreiben Wie bereits oben erwähnt, können Programme nicht ohne Weiteres direkt auf Hardware-Komponenten zugreifen, aus Sicherheitsgründen wird dies vom Betriebssystem verhindert. Um dennoch in eigenen Programmen solche Zugriffe zu realisieren, muss man bestimmte Bedingungen erfüllen – bis hin zu einer speziellen Option für den C-Compiler. In den folgenden Abschnitten werden die für die Hardware-nahe Programmierung erforderlichen Schritte erläutert.
272 9 Hardware-Programmierung 9.1.1 Eigene Programme mit root-Rechten ausstatten Grundsätzlich ist es nur dem Betriebssystem, also dem Kernel, und dem Benutzer root“ gestattet, auf die Hardware zuzugreifen. Doch wie ist es dann möglich, ” dass jeder normale“ Benutzer die grafische Oberfläche X11 starten kann, die ja ” massiv auf die Grafik-Chips des Computers zugreift?1 Unter Linux gibt es neben den Zugriffsrechten Lesen (R), Schreiben (W) und Ausführen (X) auch das Recht set user ID“ (S). Ist dieses Bit bei einer ausführbaren ” Datei gesetzt, so erhält der ausführende Prozess für die Dauer der Ausführung die Benutzer-ID dieser Datei. Gehört also ein Programm z. B. dem Benutzer root“, ” werden dem ausführenden Prozess root-Privilegien verliehen. Die Vergabe der Zugriffsrechte erfolgt mit dem Programm chmod“, der Besitzer einer Datei kann ” mit chown“ geändert werden. Die einzelnen Schritte, mit denen ein eigenes Pro” gramm mit root-Rechten ausgestattet wird, sehen dann folgendermaßen aus: > gcc mein prog.c -o mein prog > su Kennwort: # chown root:root mein prog # chmod a+s mein prog # exit Wenn Sie das Programm mein prog“ jetzt als normaler Benutzer starten, läuft ” es dennoch mit root-Privilegien – und kann entsprechenden Schaden anrichten! Aus diesem Grund sollte ein solches Programm diese Privilegien mit Hilfe des Funktionsaufrufs setuid(getuid());“ wieder abgeben, sobald sie nicht mehr ” benötigt werden. 9.1.2 Zugriff auf I/O-Ports freischalten Als zweite Sicherung gegen (unbeabsichtigte) Zugriffe auf Komponenten des Computers wird auch dem Benutzer root zunächst der Zugriff auf die Hardware verweigert (→ Speicherzugriffsfehler). Zugriffe auf I/O-Ports müssen zuvor explizit freigeschaltet werden. Dafür stehen zwei Funktionen zur Verfügung: int iopl(int level); int ioperm(unsigned long from, unsigned long num, int turn_on); Mit der Funktion iopl() kann der Level der Zugriffsmöglichkeiten auf I/OPorts von 0 (keine Erlaubnis) bis 3 (voller Zugriff) eingestellt werden. Eine Beschränkung des Adressbereiches ist mit iopl() nicht möglich. Die Funktion 1 Der von den X-Server-Programmen benutzte Mechanismus wurde mittlerweile durch ein komplexeres Verfahren ersetzt. Ältere Versionen nutzten jedoch das hier vorgestellte Prinzip.
9.1 Hardware-nahe Programme schreiben 273 ioperm() gibt dagegen einen bestimmten I/O-Adressbereich für Zugriffe frei, und zwar num Bytes ab der Adresse from. Der Parameter turn on kann hier 1 (Zugriff freischalten) oder 0 (Zugriff sperren) sein. 9.1.3 Zugriff auf die I/O-Ports Die Hardware-Komponenten des Computers sind nicht wie das RAM über normale Speicherzugriffe zugänglich, sondern nur mit Hilfe spezieller (Assembler-) Befehle erreichbar. Für C-Programme werden eine Reihe von Makros zur Verfügung gestellt, die diese Befehle auf C-Funktionen abbilden. Tabelle 9.1 gibt eine Übersicht dieser Funktionen. Tabelle 9.1: Makros für den Zugriff auf I/O-Ports Makro Beschreibung inb(Adresse) inw(Adresse) inl(Adresse) insb(Adresse) insw(Adresse) insl(Adresse) Der Inhalt des I/O-Bereichs an der Adresse Adresse wird ausgelesen. Dabei wird je nach Endung des Makros ein Byte (b), Word (w) oder Long-Word (l) zurückgegeben, wahlweise auch mit Vorzeichen (s). outb(Wert,Adresse) outw(Wert,Adresse) outl(Wert,Adresse) outsb(Wert,Adresse) outsw(Wert,Adresse) outsl(Wert,Adresse) Der Wert Wert wird in den I/O-Bereich an die Adresse Adresse geschrieben. Dabei wird Wert je nach Endung des Makros als Byte (b), Word (w) oder Long-Word (l) interpretiert, wahlweise auch mit Vorzeichen (s). inb p(Adresse) inw p(Adresse) inl p(Adresse) outb p(Wert,Adresse) outw p(Wert,Adresse) outl p(Wert,Adresse) Im Unterschied zu den Makros ohne p“ wird ” bei diesen Funktionen die Ausführung etwas verzögert, um langsamer“ Hardware ” Rechnung zu tragen. Es handelt sich bei diesen Funktionen um Inline Makros, die nur verfügbar sind, wenn das Programm mit der Option -O oder -O2 übersetzt wird. Auf aktuellen Linux-Systemen sind diese Makros in der Include-Datei <sys/io.h>“ de” finiert, auf älteren Linux-Distributionen werden stattdessen die beiden Dateien <asm/io.h>“ und <unistd.h>“ benötigt. ” ”
274 9 Hardware-Programmierung 9.2 Ansteuerung des Parallelports Als Beispiel für (relativ risikolose) Hardware-Zugriffe soll in den folgenden Abschnitten der Parallelport (also die Druckerschnittstelle) angesteuert werden. Dieser Anschluss bietet mehrere digitale Ein- und Ausgänge und eignet sich daher gut zur Ansteuerung externer, digitaler Hardware. 9.2.1 Beschreibung des Parallelports Die Parallel- oder Druckerschnittstelle verfügt über acht Datenausgänge, vier Steuerausgänge und fünf Statusleitungen (Eingänge). Die Eingänge sind über interne Widerstände (2 kΩ. . . 5 kΩ) auf 5 V gelegt, um auch bei offenen Anschlüssen einen definierten Pegel zu erreichen. Abbildung 9.1 zeigt die Anschlussbelegung der Schnittstelle, wie sie sich darstellt, wenn man von außen auf die Buchse am PC schaut. 13 12 11 10 9 8 7 6 5 4 3 2 1 25 24 23 22 21 20 19 18 17 16 15 14 Pin Nr. Typ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 – 25 Ausgang Ausgang Ausgang Ausgang Ausgang Ausgang Ausgang Ausgang Ausgang Eingang Eingang Eingang Eingang Ausgang Eingang Ausgang Ausgang — — Funktion Steuerleitung 0 (invertiert) Datenleitung 0 Datenleitung 1 Datenleitung 2 Datenleitung 3 Datenleitung 4 Datenleitung 5 Datenleitung 6 Datenleitung 7 Statusleitung 6 Statusleitung 7 (invertiert) Statusleitung 5 Statusleitung 4 Steuerleitung 1 (invertiert) Statusleitung 3 Steuerleitung 2 Steuerleitung 3 (invertiert) Masse (0 V) in der Regel auf Masse Abbildung 9.1: Anschlussbelegung des Parallelports
9.2 Ansteuerung des Parallelports 275 Die I/O-Basisadresse, an der der Parallelport angesprochen werden kann, ist entweder 3BChex , 378hex oder 278hex. An dieser Adresse liegt das Datenregister; ein Byte, das in dieses Register geschrieben wird, liegt dann an den Datenleitungen 0 bis 7 an. An der nächsthöheren Adresse (Basis-Adresse + 1) liegt das Statusregister, das den Zustand der Statusleitungen enthält. Die Steuerleitungen können über das Steuerregister (Basisadresse + 2) verändert werden. 9.2.2 Die Adresse des Parallelports suchen Beim Booten des Linux-Systems sucht“ der Kernel an den oben genannten ” Adressen nach Parallelports und merkt sich diese für die Devices /dev/lpn. Leider gibt der Kernel diese Adressen für normale“ Programme nicht preis, sodass ” wir den Parallelport selbst ausfindig machen müssen. Dieser Vorgang ist relativ einfach: Man schreibt an die möglichen Basisadressen eine 0 und schaut anschließend nach, ob dieser Inhalt erhalten bleibt. Liegt an dieser Stelle ein Parallelport – genauer gesagt: das Datenregister eines Parallelports –, wird der Wert über die Datenleitungen ausgegeben und kann auch wieder gelesen werden. Befindet sich an der zu testenden Basisadresse keine Hardware, erhält man beim Auslesen in der Regel den Wert 255. Das folgende Programm testet die möglichen Adressen und gibt diejenige aus, an der ein Parallelport gefunden wurde: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* find_port.c */ # include <stdio.h> # if defined __GLIBC__ # include <sys/io.h> # else # include <unistd.h> # include <asm/io.h> # endif int main() { int i, port = 0; const int base_adr[3] = {0x3bc, 0x378, 0x278}; if (iopl(3) != 0) { perror("find_port: Can’t set I/O permissions"); return(1);
276 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 9 Hardware-Programmierung } for (i=0; i<3; i++) { outb_p(0, base_adr[i]); if (inb_p(base_adr[i]) == 0) port = base_adr[i]; } if (port == 0) { fprintf(stderr, "find_port: No parallel port found.\n"); return(1); } printf("Parallel port found at 0x%x.\n", port); return(0); } Beim Übersetzen des Programms ist die Compiler-Option -O“ (oder -O2“) zu ” ” verwenden: gcc -O find port.c -o find port Bevor das Programm gestartet werden kann, muss noch das Zugriffsrecht set user ID eingestellt werden (siehe Abschnitt 9.1.1). 9.2.3 Ein Beispiel: LED-Lauflicht“ ” Als kleines Anwendungsbeispiel für die Ansteuerung des Parallelports soll hier ein Lauflicht mit Leuchtdioden (LEDs) realisiert werden. Abbildung 9.2 zeigt das Schaltbild; die Widerstände sollten je nach Leuchtdiodentyp zwischen 680 Ω und 1 kΩ ausgelegt sein. Dass man diese Schaltung auch ganz ohne Platine realisieren kann, zeigt Abbildung 9.3. Das folgende Programm lässt einen Leuchtpunkt“ zwischen den beiden Enden ” der Leuchtdiodenreihe hin- und herwandern. Synchron dazu wird die Position auch im Terminalfenster dargestellt: 1 2 3 4 5 6 /* LED-line.c */ # include <stdio.h> # include <unistd.h>
9.2 Ansteuerung des Parallelports 277 Parallelport 2 3 4 5 6 7 8 9 18 Abbildung 9.2: Schaltbild für das LED-Lauflicht 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # if defined __GLIBC__ # include <sys/io.h> # else # include <asm/io.h> # endif int get_lp_base(void) { int i, base_adr[3] = {0x3bc, 0x378, 0x278}; for (i=0; i<3; i++) { outb_p(0, base_adr[i]); if (inb_p(base_adr[i]) == 0) return(base_adr[i]); } return(0); }
278 9 Hardware-Programmierung Abbildung 9.3: Das LED-Lauflicht – ganz ohne Platine. Zusätzlich sind zwei Taster an den Statusleitungen 5 und 6 des Parallelports angeschlossen. 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 int main() { int i, j, port_adr; char light[12] = " 0 \r"; if (iopl(3) != 0) { perror("LED-line: Can’t set I/O permissions"); return(1); } if ((port_adr = get_lp_base()) == 0) { fprintf(stderr, "LED-line: No parallel port found.\n"); return(1); } printf("Parallel port found at 0x%x.\n", port_adr); i = 0; j = 1; while (1)
9.3 Modem-Steuerleitungen abfragen 49 50 51 52 53 54 55 56 57 58 59 279 { outb(1<<i, port_adr); write(1, &(light[i]), 11-i); usleep(100000L); /* 100000 Mikrosek. warten */ i += j; if ((i == 0) || (i == 7)) j = -j; } return(0); } Zum Übersetzen des Programms ist auch hier wieder die Option -O“ erforder” lich. Vor dem Starten des Programms als normaler“ Benutzer müssen ferner die ” Zugriffsrechte gemäß Abschnitt 9.1.1 eingestellt werden. 9.3 Modem-Steuerleitungen abfragen Ähnlich wie der Parallelport lässt sich auch der COM-Port (die serielle Schnittstelle RS 232) des Computers ansteuern. Beim Anschluss eines externen Gerätes an dieser Schnittstelle kann es notwendig sein, den Pegel auf den Steuerleitungen abzufragen, z.B. um festzustellen, ob das angeschlossene Gerät bereit ist, Daten anzunehmen. Tabelle 9.2 beschreibt die Funktion dieser Steuerleitungen. Tabelle 9.2: Bedeutung der (Modem-)Steuerleitungen Leitung Funktion CTS Clear To Send – das angeschlossene Gerät ist bereit, Daten anzunehmen, d.h. es erteilt Sendeerlaubnis. DSR Data Set Ready – das angeschlossene Gerät ist bereit, dem Computer Daten zu senden. RI Ring Indicator – eingehender Anruf (nur Modem). CD Carrier Detect – Modem-Trägersignal erkannt, Verbindung zum Modem der Gegenstelle besteht (nur Modem). Der Status dieser Steuerleitungen wird in dem Modem Status Register (MSR) des UART1 abgebildet. Die Registeradressen und Bit-Nummern der Steuerleitungen sind in der Include-Datei <linux/serial reg.h>“ definiert. Die Basisadres” se des UART lässt sich mit Hilfe des ioctl()-Kommandos TIOCGSERIAL abfra1 Universal Asynchronous Receiver Transmitter – ein Chip zur Ansteuerung der seriellen Schnittstelle
280 9 Hardware-Programmierung gen (siehe auch Kapitel 6). Der entsprechende ioctl()-Aufruf benötigt dazu den Zeiger auf eine Variable vom Typ struct serial struct. In diese Variable trägt das Device unter anderem die Basisadresse des UART und die Interrupt-Nummer (IRQ) ein. Das folgende Programm erwartet als Kommandozeilenparameter den Pfadnamen für das Device der seriellen Schnittstelle, z.B. /dev/ttyS0“. Es öffnet das ange” gebene Device, holt mit ioctl() die Basisadresse des zugehörigen UART und liest dann in einer Endlosschleife das Modem-Status-Register aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /* serialinfo.c - Informationen ueber die serielle Schnittstelle holen */ # # # # # # # # # # # # include <stdio.h> include <unistd.h> include <string.h> include <fcntl.h> include <sys/ioctl.h> include <linux/serial.h> include <linux/serial_reg.h> if defined __GLIBC__ include <sys/io.h> else include <asm/io.h> endif int main(int argc, char *argv[]) { int fd, base_adr, msr; struct serial_struct serial_port; if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) { printf("Usage: serialinfo device\n"); return(0); } if ((fd = open(argv[1], O_RDWR)) == -1) { perror("serialinfo: Can’t open device"); return(1); }
9.3 Modem-Steuerleitungen abfragen 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 281 if (ioctl(fd, TIOCGSERIAL, &serial_port) == -1) { perror("serialinfo: ioctl() failed"); return(1); } base_adr = serial_port.port; printf("Device:\t%s (COM%d)\nPort:\t0x%x\n" "IRQ:\t%d\n", argv[1], serial_port.line+1, serial_port.port, serial_port.irq); ioperm(base_adr+UART_MSR, 1, 1); printf("Line status:\nCD RI DSR CTS\n"); while(1) { msr = inb(base_adr+UART_MSR); printf("\r %d %d %d %d", (msr & UART_MSR_DCD)? 1 : 0, (msr & UART_MSR_RI)? 1 : 0, (msr & UART_MSR_DSR)? 1 : 0, (msr & UART_MSR_CTS)? 1 : 0); fflush(stdout); usleep(100000L); } ioperm(base_adr+UART_MSR, 1, 0); close(fd); return(0); } Nach Öffnen des Devices, dessen Pfadname als Kommandozeilenparameter angegeben werden muss, erfolgt in Zeile 36 der ioctl()-Aufruf, mit dem die Variable serial port initialisiert wird. Diese Struktur enthält dann unter anderem die Basisadresse der Schnittstelle (Zeile 41). Im Gegensatz zu den vorherigen Beispielprogrammen muss dieses Programm nur auf eine I/O-Adresse zugreifen, sodass sich hier zum Freischalten die Funktion ioperm() anbietet (Zeile 46). Wie die Beispiele zuvor muss auch dieses Programm mit der Option -O“ übersetzt und mit root-Rechten ausgestattet werden ” (siehe Abschnitt 9.1.1). Danach kann es z.B. mit serialinfo /dev/ttyS0 für die Abfrage der ersten seriellen Schnittstelle (COM1) gestartet werden.

Kapitel 10 Beispielprojekte In den Kapiteln 3 bis 9 haben wir gezeigt, wie man auf Devices zugreift, wie Interprozess- und Netzwerkkommunikation funktionieren und wie man eine grafische Oberfläche erstellt. Echte“ Software-Projekte beinhalten in der Regel eine ” Kombination aus mehreren dieser Themen. Aus diesem Grund wird in den folgenden Abschnitten anhand von zwei Beispielprojekten gezeigt, wie man solche Projekte aufbaut und strukturiert und wie das Zusammenspiel der in den vorangegangenen Kapiteln vorgestellten Themen funktioniert. 10.1 WebCam: Video-Übertragung per HTTP Als erstes Beispielprojekt erstellen wir eine Applikation, die aus einer USBKamera (oder anderen Video-Quelle) eine echte Webcam“ macht. Das Videobild ” soll also über das Netzwerk mit einem Webbrowser wie Firefox dargestellt werden. Dieses Beispielprojekt beinhaltet folgende Themen: Auswertung der Kommandozeilenparameter mit getopt() Dateizugriffe inkl. stat() und remove() Prozesse und Signale (fork(), signal()) Ansteuerung des Video4Linux-Devices Verwendung der libjpeg zur JPEG-Kompression Netzwerkkommunikation mit TCP und HTTP inkl. Authentifizierung Aufteilung des Quelltextes und Erstellen eines Makefiles
284 10 Beispielprojekte 10.1.1 Wie die Bilder laufen lernen In Abschnitt 7.3.3 hatten wir bereits ein kleines Webserver-Programm vorgestellt, das es ermöglicht, HTML-Seiten und Grafiken über Webbrowser abzurufen. Doch wie kann das Live-Videobild einer Kamera als bewegtes Bild“ dargestellt wer” den? Für diesen Zweck wurde das so genannte Server-push-Prinzip entwickelt. Fordert der Browser die Grafikdatei eines solchen Live-Videobilds an, sendet der Webserver eine Multipart-Antwort und signalisiert dem Browser damit, dass die angeforderte Grafik nicht aus einer einzelnen Datei, sondern einer Folge von Bildern besteht. Danach folgen die einzelnen Bilder, die jeweils durch eine Leerzeile und das mit boundary=. . .“ definierte Schlüsselwort getrennt werden: ” HTTP/1.1 200 OK Content-type: multipart/x-mixed-replace;boundary=Schlüsselwort Leerzeile --Schlüsselwort Content-type: image/jpeg Content-length: Anzahl Bytes des 1. Bildes Leerzeile Bilddaten (JPEG): 1. Bild Leerzeile --Schlüsselwort Content-type: image/jpeg Content-length: Anzahl Bytes des 2. Bildes Leerzeile Bilddaten (JPEG): 2. Bild Leerzeile usw. 6 Bild 1 ? 6 Bild 2 ? Mit Ausnahme des Internet-ExplorersTM von MicrosoftTM unterstützen alle aktuellen Browser (Firefox, Mozilla, Netscape) diese Technik, um Video-Sequenzen darzustellen. 10.1.2 Strukturierung der Quelltexte Die verschiedenen Funktionen unseres Webcam-Projektes lassen sich gut auf mehrere Quelltexte aufteilen, um die einzelnen Dateien übersichtlicher zu halten: webcam.c – Hauptprogramm mit Auswertung der Kommandozeilenparameter und Initialisierung der libusb und des HTTP-Netzwerkports http service.c – Abarbeitung der HTTP-Anfragen inkl. Erzeugen der MultipartAntwort für das Live-Video usbcam.c – Ansteuerung der USB-Kamera mit Speicherung der Einzelbilder als JPEG-Datei
10.1 WebCam: Video-Übertragung per HTTP 285 Zu den letzten beiden Quelltexten gehört auch je eine Header-Datei ( .h“), in der ” die Funktionsaufrufe deklariert sind. Werfen wir zunächst einen Blick auf das Hauptprogramm webcam.c“: ” 1 /* 2 webcam.c - Webserver für USB-Kamera 3 - Hauptteil 4 / * 5 6 # include <stdio.h> 7 # include <string.h> 8 # include <stdlib.h> 9 # include <unistd.h> 10 # include <sys/types.h> 11 # include <sys/socket.h> 12 # include <netinet/in.h> 13 # include <arpa/inet.h> 14 # include <sys/stat.h> 15 # include <signal.h> 16 # include "http_service.h" 17 # include "usbcam.h" 18 19 # define IP_PORT 80 20 # define N_CONNECTIONS 10 21 22 void err_exit(char *message) 23 { 24 perror(message); 25 exit(1); 26 } 27 28 /*--------------- Hauptprogramm ---------------*/ 29 30 int main(int argc, char *argv[]) 31 { 32 int option, sock_fd, client_fd, cam_fd, err, pid, 33 delay, swap_RGB; 34 char *dev_name, *auth; 35 socklen_t addr_size; 36 struct sockaddr_in my_addr, client_addr; 37 38 /*---- Defaultwerte für die Einstellungen ----*/ 39 swap_RGB = 0; /* Rot/Blau tauschen? */ 40 41 dev_name = "/dev/video"; /* Video4Linux device */
286 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 10 Beispielprojekte auth = ""; delay = 100; /* Username:Passwort, base64-codiert */ /* Wartezeit zwischen den Bildern */ /*---- Kommandozeilenparameter auswerten ----*/ while ((option = getopt(argc, argv, "hsa:d:")) >= 0) switch (option) { case ’h’: printf("Usage: %s [-s] [-d #] [-a str] [device]\n" "-s : swap colours from BGR to RGB\n" "-a str : set HTTP authentication code\n" "-d # : set delay between images\n", argv[0]); return(0); case ’s’: swap_RGB = 1; break; case ’a’: auth = optarg; break; case ’d’: sscanf(optarg, "%d", &delay); break; case ’?’: return(1); /* unbekannte Option */ } if (argc-optind > 1) { fprintf(stderr, "webcam: Bad arguments.\n"); return(1); } if (argc-optind == 1) dev_name = argv[optind]; /*---- Socket öffnen und an Port binden ----*/ sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd == -1) err_exit("webcam: Can’t create new socket"); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(IP_PORT); my_addr.sin_addr.s_addr = INADDR_ANY; err = bind(sock_fd, (struct sockaddr *)&my_addr,
10.1 WebCam: Video-Übertragung per HTTP 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 287 sizeof(struct sockaddr_in)); if (err == -1) err_exit("webcam: bind() failed"); setuid(getuid()); /* root-Rechte abgeben */ err = listen(sock_fd, N_CONNECTIONS); if (err == -1) err_exit("webcam: listen() failed"); /*---- Kamera öffnen und initialisieren ----*/ if ((cam_fd = init_cam(dev_name)) < 0) return(1); signal(SIGCHLD, SIG_IGN); /* keine Zombie-Prozesse */ signal(SIGPIPE, SIG_IGN); /* wenn Browser beendet */ printf("HTTP service started.\n" "Press Ctrl-C to stop.\n"); while (1) /*---- Endlosschleife ----*/ { addr_size = sizeof(struct sockaddr_in); client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_size); if (client_fd == -1) err_exit("webcam: accept() failed"); if ((pid = fork()) == -1) { fprintf(stderr, "webcam: fork() failed.\n"); return(1); } else if (pid == 0) /* Kind-Prozess */ { while (http_service(client_fd, cam_fd, delay, swap_RGB, auth)); shutdown(client_fd, SHUT_RDWR); close(client_fd); return(0); } close(client_fd); /* Eltern-Prozess */
288 130 131 132 133 10 Beispielprojekte } return(0); /* wird nie erreicht */ } In den Zeilen 16 und 17 werden die Header-Dateien zu den beiden anderen Quelltexten eingebunden, um die darin deklarierten Funktionen dem Hauptprogramm bekannt zu machen. Nachdem in Zeile 40 bis 43 zunächst die Einstellungen des Programms mit Default-Werten belegt werden, folgt die Auswertung der Kommandozeilenparameter mit Hilfe der Funktion getopt() gemäß Abschnitt 3.1.3. Das Einrichten des Webserver-Ports 80 in den Zeilen 77 bis 94 ist bereits aus dem Programmbeispiel in Abschnitt 7.3.3 bekannt. In Zeile 98 folgt das Öffnen und Initialisieren des Video4Linux-Devices (USBKamera oder TV-Karte) mit Hilfe der Funktion init cam(), die im Quelltext usbcam.c“ definiert ist (siehe unten). Den ersten der beiden anschließenden ” signal()-Aufrufe kennen wir vom Webserver-Beispiel aus Abschnitt 7.3.3. Mit dem zweiten Aufruf (Zeile 103) wird zusätzlich verhindert, dass beim Beenden der Netzwerkverbindung durch den Browser – z. B. durch Schließen des Browsers – der Kind-Prozess des Webservers ebenfalls sofort beendet wird. Dies kommt bei der Übertragung der Videodaten zum Tragen, die ja so lange läuft, bis der Browser beendet oder eine andere Webseite aufgerufen wird. Im Vergleich zu dem in Abschnitt 7.3.3 beschriebenen Webserver machen wir in diesem Programm Gebrauch von den erweiterten Funktionen des Standards HTTP 1.1. Darin ist definiert, dass eine Verbindung zwischen Browser und Webserver nicht nach der HTTP-Antwort beendet werden muss, sondern weitere HTTP-Anfragen und -Antworten über die gleiche Verbindung geschickt werden können.1 Aus diesem Grund werden hier in jedem Kind-Prozess die HTTPAnfragen in einer while()-Schleife (Zeile 123) abgearbeitet, bis der Browser die Verbindung abbricht oder ein Fehler auftritt. Die Funktion http service(), die in dem gleichnamigen Quelltext definiert ist, liefert einen Rückgabewert von 1, wenn die Verbindung bestehen bleiben soll, und eine 0, wenn die Verbindung abgebrochen wurde. Sehen wir uns als Nächstes den Quelltext http service.c“ an: ” 1 /* 2 http_service.c - HTTP-Server für WebCam / 3 * 4 5 # include <stdio.h> 6 # include <string.h> 7 # include <unistd.h> 8 # include <sys/types.h> 9 # include <sys/socket.h> 1 Der Browser signalisiert dies durch die Kopfzeile Connection: keep-alive“, siehe Seite 183. ”
10.1 WebCam: Video-Übertragung per HTTP 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 289 # include <sys/stat.h> # include <signal.h> # include "usbcam.h" # define START_FILE "start.html" # define TMP_PATH "./tmp_" # define BUFSIZE 2000 /*--------------- get_line() ---------------*/ int get_line(int sock_fd, char *buffer, int length) { int i; i = 0; while ((i < length-1) && (recv(sock_fd, &(buffer[i]), 1, 0) == 1)) if (buffer[i] == ’\n’) break; else i++; if ((i > 0) && (buffer[i-1] == ’\r’)) i--; buffer[i] = ’\0’; return(i); } /*--------------- file_size() ---------------*/ size_t file_size(char *filename) { struct stat file_info; if (stat(filename, &file_info) == -1) return(0); return(file_info.st_size); } /*-------------- send_video() --------------*/ void send_video(int client_fd, int cam_fd, char *buffer, int bufsize, char *cmd, int swap_RGB, int delay) { int length;
290 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 10 Beispielprojekte FILE *stream; static char tmp_name[32]; send(client_fd, "HTTP/1.1 200 OK\r\n" "Content-type: multipart/x-mixed-replace;" "boundary=next-jpeg-image-data\r\n\r\n", 90, 0); if (strcmp(cmd, "HEAD") == 0) return; snprintf(tmp_name, 31, "%s%d.jpg", TMP_PATH, getpid()); while (1) /* Schleife, bis Verbindung abbricht */ { if (get_image(cam_fd, tmp_name, swap_RGB)) break; if ((stream = fopen(tmp_name, "r")) == NULL) break; sprintf(buffer, "--next-jpeg-image-data\r\n" "Content-type: image/jpeg\r\n" "Content-length: %ld\r\n\r\n", file_size(tmp_name)); if (send(client_fd, buffer, strlen(buffer), 0) < 0) { fclose(stream); break; } while (!feof(stream)) { length = fread(buffer, 1, bufsize, stream); if (length > 0) if (send(client_fd, buffer, length, 0) <= 0) { fclose(stream); remove(tmp_name); return; } } fclose(stream); send(client_fd, "\r\n", 2, 0); /* ’Part’ Ende */ usleep(delay*1000L); } remove(tmp_name);
10.1 WebCam: Video-Übertragung per HTTP 291 98 return; 99 } 100 101 /*-------------- http_service() --------------*/ 102 103 int http_service(int client_fd, int cam_fd, int delay, 104 int swap_RGB, char *auth) 105 { 106 static char buffer[BUFSIZE], cmd[8], url[256]; 107 char *filename; 108 int length, auth_ok; 109 FILE *stream; 110 111 auth_ok = (auth[0] == ’\0’); /* mit Passwort? */ 112 113 if (get_line(client_fd, buffer, BUFSIZE) == 0) 114 return(0); 115 116 if (sscanf(buffer, "%7s %255s", cmd, url) < 2) 117 return(0); 118 119 /*---- Kopfzeilen auswerten -----*/ 120 121 while (get_line(client_fd, buffer, BUFSIZE) > 0) 122 if (strncasecmp(buffer, "Authorization:", 14) == 0) 123 if ((!auth_ok) && (strncmp(&(buffer[21]), auth, 124 strlen(auth)) == 0)) 125 auth_ok = 1; 126 127 if ((strcmp(cmd, "GET") != 0) 128 && (strcmp(cmd, "HEAD") != 0)) 129 return(0); 130 /*---- Authentifizierung -----*/ 131 132 if (!auth_ok) /* Zugangsdaten OK? */ 133 134 { 135 strcpy(buffer, 136 "HTTP/1.1 401 Authorization Required\r\n" 137 "WWW-Authenticate: Basic realm=\"WebCam\"\r\n" 138 "Content-Type: text/html\r\n" 139 "Content-Length: 42\r\n\r\n" 140 "<html><body>Falsche Kennung!</body></html>"); 141 send(client_fd, buffer, strlen(buffer), 0);
292 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 10 Beispielprojekte return(1); } filename = &(url[1]); /* ’/’ am Anfang entfernen */ if (strcasecmp(filename, "video.jpg") == 0) { send_video(client_fd, cam_fd, buffer, BUFSIZE, cmd, swap_RGB, delay); return(0); } /*---- einfache Datei übertragen -----*/ if (strlen(filename) == 0) filename = START_FILE; if ((stream = fopen(filename, "r")) == NULL) { send(client_fd, "HTTP/1.1 404 Not Found\r\n" "Content-type: text/html\r\n" "Content-length: 91\r\n\r\n" "<html><head><title>Error</title></head>" "<body><hr><h2>File not found.</h2><hr>" "</body></html>/r/n", 164, 0); return(1); } sprintf(buffer, "HTTP/1.1 200 OK\r\n" "Content-length: %ld\r\n\r\n", file_size(filename)); if (send(client_fd, buffer, strlen(buffer), 0) <= 0) return(0); if (strcmp(cmd, "GET") == 0) while (!feof(stream)) { length = fread(buffer, 1, BUFSIZE, stream); if (length > 0) if (send(client_fd, buffer, length, 0) <= 0) return(0); } fclose(stream); return(1); }
10.1 WebCam: Video-Übertragung per HTTP 293 Die Funktionen get line() (Zeile 20 bis 35) und file size() (Zeile 39 bis 46) sind ja bereits aus dem Webserver-Beispiel (Abschnitt 7.3.3) bekannt. Neu hinzugekommen ist die Funktion send video(). Sie generiert die Multipart-Antwort gemäß Abschnitt 10.1.1 und sendet zyklisch die Videobilder als JPEG-Daten an den Browser. Dazu wird in Zeile 69 mit der im Quelltext usbcam.c“ definier” ten Funktion get image() ein einzelnes Bild von der Kamera/TV-Karte abgerufen und als JPEG-Datei gespeichert. Der Dateiname für die temporäre Bilddatei wird in Zeile 64 aus der Prozess-ID des Kind-Prozesses generiert. Dadurch wird erreicht, dass parallel laufende Video-Übertragungen (Zugriff von mehreren Browsern auf das Video) jeweils unabhängige temporäre Dateien benutzen. Nach Abbruch der Verbindung zum Browser wird diese Datei mit remove() wieder gelöscht (Zeile 89 und 97). Ab Zeile 103 beginnt die Funktion http service(), die die HTTP-Anfragen abarbeitet. Auch diese Funktion ist gegenüber unserem Webserver-Beispiel aus Abschnitt 7.3.3 erweitert: In Zeile 121 bis 125 werden die Kopfzeilen ausgewertet, wobei nach einer HTTP-Authentifizierung gesucht wird. Wie diese Art der Zugriffskontrolle funktioniert, beschreiben wir in Abschnitt 10.1.3. Außerdem wird in Zeile 147 bis 152 geprüft, ob als URL video.jpg“ angegeben wurde. Wenn ja, ” wird anstelle einer HTML- oder Bild-Datei das Live-Videobild mit Hilfe der Funktion send video() gesendet. In der zum Quelltext http service.c“ gehörenden Header-Datei ist lediglich die ” Funktion http service() deklariert, da nur diese aus den anderen Quelltexten heraus aufgerufen wird: 1 2 3 4 5 6 /* http_service.h - HTTP-Server für WebCam */ int http_service(int client_fd, int cam_fd, int delay, int swapRGB, char *pass); In dem dritten Quelltext usbcam.c“ sind die Funktionen zusammengefasst, die ” das Video4Linux-Device ansteuern: 1 2 3 4 5 6 7 8 9 10 11 12 /* usbcam.c - Kamera-Ansteuerung für WebCam */ # # # # # # include include include include include include <stdio.h> <unistd.h> <fcntl.h> <sys/ioctl.h> <linux/videodev.h> <jpeglib.h> # define WIDTH 320 /* Bildgröße QVGA */
294 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 10 Beispielprojekte # define HEIGHT 240 # define IMGSIZE (WIDTH*HEIGHT*3L) # define JPEG_QUALITY 70 /* Bildspeicher */ /*--------------- init_cam() ---------------*/ int init_cam(char *device_name) { int fd; struct video_window video_win; if ((fd = open(device_name, O_RDONLY)) == -1) { perror("webcam: Can’t open video device"); return(-1); } if (ioctl(fd, VIDIOCGWIN, &video_win) == -1) { perror("webcam: Can’t get video window"); return(-1); } video_win.width = WIDTH; video_win.height = HEIGHT; if (ioctl(fd, VIDIOCSWIN, &video_win) == -1) { perror("webcam: Can’t set video window"); return(-1); } return(fd); } /*--------------- get_image() ---------------*/ int get_image(int cam_fd, char *tmpfile, int swap_RGB) { int i, tmp; long len; static unsigned char image[IMGSIZE]; FILE *stream; JSAMPROW row_pointer;
10.1 WebCam: Video-Übertragung per HTTP 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 static struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; len = read(cam_fd, image, IMGSIZE); if (len < IMGSIZE) { perror("webcam: Error while reading"); if (len < 0) return(1); } if (swap_RGB) /* Rot und Blau tauschen? */ for (i=0; i<IMGSIZE; i+=3) { tmp = image[i]; image[i] = image[i+2]; image[i+2] = tmp; } /* ----- JPEG-Umwandlung vorbereiten -----*/ cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); if ((stream = fopen(tmpfile, "w")) == NULL) { perror("webcam: Can’t open temporary file."); return(1); } jpeg_stdio_dest(&cinfo, stream); cinfo.image_width = WIDTH; cinfo.image_height = HEIGHT; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, JPEG_QUALITY, TRUE); /* ----- JPEG-Umwandlung starten -----*/ jpeg_start_compress(&cinfo, TRUE); 295
296 101 102 103 104 105 106 107 108 109 110 111 112 113 10 Beispielprojekte while (cinfo.next_scanline < HEIGHT) { row_pointer = &(image[cinfo.next_scanline*WIDTH*3]); jpeg_write_scanlines(&cinfo, &row_pointer, 1); } jpeg_finish_compress(&cinfo); /* Bild speichern */ jpeg_destroy_compress(&cinfo); /* aufräumen */ fclose(stream); return(0); } Bildgröße und JPEG-Kompressionsgrad sind mit define-Anweisungen fest eingestellt (Zeile 12 bis 15). In der Funktion init cam() wird das Device geöffnet und das Bildformat eingestellt (Zeile 19 bis 46). Die Funktion get image() ab Zeile 50 liest ein Bild aus der Videoquelle und wandelt es in eine JPEG-Datei um (siehe auch Seite 139). Beide in diesem Quelltext definierte Funktionen werden auch in der zugehörigen Header-Datei deklariert und somit den anderen Programmteilen bekannt gemacht: 1 2 3 4 5 6 7 /* usbcam.h - Kamera-Ansteuerung für WebCam */ int init_cam(char *device_name); int get_image(int cam_fd, char *tmpfile, int swap_RGB); Um die verschiedenen Quelltexte zu einem Programm zu übersetzen, müssen insgesamt vier gcc-Aufrufe ausgeführt werden. Daher bietet sich hier die Verwendung des Tools make“ an (vgl. Abschnitt 1.4.5). Ein passendes Makefile“ sieht ” ” dann so aus: 1 2 3 4 5 6 7 8 9 10 # # Makefile für WebCam # OBJ = webcam.o http_service.o usbcam.o HDR = http_service.h usbcam.h LIB = -ljpeg webcam: $(OBJ) gcc $ˆ $(LIB) -o $@
10.1 WebCam: Video-Übertragung per HTTP 11 12 13 14 15 16 %.o: 297 %.c $(HDR) gcc -Wall -c $< clean: rm -f $(OBJ) Zum Übersetzen des gesamten Projektes müssen Sie nun einfach nur make“ ein” geben. Bevor Sie das Programm starten, sollten Sie noch eine HTML-Datei mit dem Namen start.html“ anlegen, die der Webserver dann als Startseite anzeigt, wenn ” man im Browser den URL http://localhost“ eingibt. Um das Video-Bild auf ” dieser Web-Seite darzustellen, müssen Sie in die HTML-Datei ein IMG-Tag der Form <img src="video.jpg"> einfügen. Optional ist die Angabe der Bildgröße möglich, damit der Browser schon vor dem Empfang des Video-Streams die Position und Größe des Bildes korrekt darstellen kann. Eine entsprechende HTML-Datei sieht dann beispielsweise so aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <html> <head> <title>WebCam</title> </head> <body bgcolor="#e0deda"> <p> <h1 align=center>WebCam</h1> <h3 align=center>aus "C und Linux", Hanser Verlag</h3> <hr> <table align=center border=2> <tr><td> <img src="video.jpg" width=320 height=240> </td></tr> </table> </body> </html> Da unser WebCam-Programm den für HTTP-Server registrierten Port 80 verwendet, muss es über root-Rechte verfügen. Dazu können Sie das Programm entweder als Benutzer root“ oder mit dem Tool sudo“ starten – oder Sie setzen das S“-Bit ” ” ” gemäß Abschnitt 9.1.1. Sobald das Programm läuft, können Sie von jedem Rechner im Netzwerk mit einem Browser auf das Videobild der Kamera/TV-Karte zugreifen. Abbildung 10.1 zeigt das Ergebnis am Beispiel des Browsers Firefox und einer TV-Karte als Videoquelle.
298 10 Beispielprojekte Abbildung 10.1: TV-Karte als Videoquelle für das Programm webcam“ ” 10.1.3 Die HTTP-Authentifizierung Da Sie vielleicht nicht wünschen, dass jeder Benutzer im lokalen Netzwerk (heimlich) das Videobild Ihrer USB-Kamera abrufen kann, haben wir eine Besonderheit in das WebCam-Programm eingebaut: eine Authentifizierung mit Benutzername und Passwort. Dazu verwenden wir den ab Version 1.1 des HTTP-Standards definierten Mechanismus, den alle Browser unterstützen. Eine ankommende HTTPAnfrage wird dabei zunächst mit folgender Antwort abgewiesen: HTTP/1.1 401 Authorization Required WWW-Authenticate: Basic realm="Name" Daraufhin öffnet der Browser ein kleines Fenster zur Eingabe von Benutzername und Passwort. Dabei wird der in der HTTP-Antwort als Name angegebene Text in dem Fenster angezeigt, damit der Benutzer weiß, welche Webseite hier eine Zugangskennung verlangt. Nach Eingabe der Zugangsdaten sendet der Browser seine HTTP-Anfrage erneut an den Webserver, diesmal jedoch mit einer zusätzlichen Kopfzeile der Form: Authorization: Basic Zugangscode Der hinter dem Schlüsselwort Basic“ angegebene Zugangscode entspricht den ” durch einen Doppelpunkt getrennten Benutzernamen und Passwort, jedoch nicht
10.1 WebCam: Video-Übertragung per HTTP 299 im Klartext sondern Base64-codiert. Stimmt der Code mit dem im Webserver hinterlegten überein, wird die Webseite an den Browser geschickt. Andernfalls wiederholt der Webserver die Authentifizierungsanforderung. In den Zeilen 121 bis 125 und 133 bis 143 des Quelltextes http service.c“ ist diese Authentifizierung ” realisiert. Bei unserem WebCam-Programm können Sie mit der Option -a“ einen Zugangs” code angeben, der für das Abrufen von Inhalten erforderlich sein soll. Mit Hilfe des Programms uuencode“ aus dem Paket sharutils“ können Sie Benutzername ” ” und Passwort in die Base64-Zeichenfolge für die HTTP-Authentifizierung umcodieren. Zwischen dem Benutzernamen und dem Passwort muss dabei ein Doppelpunkt stehen: > echo -n gast:pass | uuencode -m foo begin-base64 644 foo Z2FzdDpwYXNz ==== > su Kennwort: s # ./webcam -a Z2FzdDpwYXNz HTTP service started. Press Ctrl-C to stop. Auf diese Weise gestartet, verlangt der Webserver die Zugangskennung gast“ ” und pass“, bevor Daten übertragen werden. ” Noch ein Hinweis zur Sicherheit: Die vorgestellte HTTP-Authentifizierung ist kein sehr sicheres Verfahren, da die in Base64-codierten Zugangsdaten problemlos wieder in Klartext zurückgewandelt werden können ( uudecode“). Wird der Datenverkehr zwischen Browser und ” Webserver von irgendjemandem angezapft“ und mitgeschnitten – auch als Man” In-The-Middle-Attacke bezeichnet –, kann derjenige relativ leicht die Zugangskennung entschlüsseln. Daher verwenden professionelle Internetanbieter für die Übertragung von Kennwörtern immer eine verschlüsselte Verbindung ( https“), ” bei der sich allein aus den über das Netzwerk übertragenen Daten der Benutzername und das Passwort nicht rekonstruieren lassen.
300 10 Beispielprojekte 10.2 Telefonbuch mit automatischer Anwahl Als zweites Software-Projekt soll das Programm telefonbuch“ vorgestellt wer” den. Es zeigt die Verwendung der ncurses-Bibliothek zur Ein- und Ausgabesteuerung im Shell-Fenster sowie die Ansteuerung eines Telefon-Modems. Die Einbindung von Shell-Programmen über einen zweiten Prozess und die zugehörige Interprozesskommunikation sind ebenfalls Bestandteil dieses Projektes. Auch wenn Sie einen DSL-Internetzugang haben, besitzen Sie vielleicht noch ein serielles oder USB-Modem, das Sie mit Hilfe dieses Programms als Wählhilfe“ ” reaktivieren können. 10.2.1 Ziel des Projektes Das Shell-Programm soll eine (unsortierte) Textdatei mit Namen und Telefonnummern einlesen und sortiert im Terminal-Fenster darstellen. Die Textdatei soll pro Zeile einen Namen und – mit einem oder mehreren Tabulatoren getrennt – die zugehörige Telefonnummer enthalten. Ähnlich wie bei einem Mobiltelefon soll hier über die Eingabe des Anfangsbuchstabens automatisch der erste passende Eintrag angesprungen“ werden; mit den Cursor-Tasten kann man sich in der ” Liste vor und zurück bewegen. Durch Drücken der Enter-Taste wird die Telefonnummer des markierten Eintrags mit einem analogen Telefonmodem gewählt. Während des Wählvorgangs, der bei den meisten Modems akustisch ausgegeben wird, kann man den Hörer eines nachgeschalteten“ Telefons (siehe Abbil” dung 10.2) abnehmen. Das Modem legt nach Anwahl der Nummer automatisch auf und übergibt so die aufgebaute Verbindung an das Telefon.1 N F N Abbildung 10.2: Anschluss von Analog-Modem und Telefon. 1 Voraussetzung dafür ist, dass das Modem über eine 4-adrige Leitung angeschlossen ist.
10.2 Telefonbuch mit automatischer Anwahl 301 10.2.2 Strukturierung des Projektes Auch dieses Beispielprojekt lässt sich in drei Teile zerlegen: das Hauptprogramm, die Funktionen zur Ansteuerung des Modems und die Benutzerschnittstelle – hier mit der ncurses-Bibliothek realisiert. Jedes dieser Module besteht aus einer Include-Datei mit Deklarationen und einer .c“-Datei mit den Funktionsdefinitio” nen. 10.2.3 Das Hauptprogramm Die Datei telefonbuch.c“ beinhaltet neben der Definition von main() die Funk” tion read phonebook() zum Einlesen der Telefonliste sowie die globale Variable phonebook, in der das Telefonbuch abgelegt wird: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /* telefonbuch.c - automatische Anwahl mit Modem */ # # # # # # include include include include include include <stdio.h> <string.h> <stdlib.h> "telefonbuch.h" "modem.h" "eingabe.h" char phonebook[MAX_ENTRIES][2][MAX_CHARS]; int get_entry(char *buffer, char *dest) { char c; int i; i = 0; while (((c = buffer[i]) != ’\0’) && (c != ’\t’) && (c != ’\n’)) { if (i < MAX_CHARS-1) dest[i] = c; i++; } if (i < MAX_CHARS) dest[i] = ’\0’; else dest[MAX_CHARS-1] = ’\0’; return(i);
302 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 10 Beispielprojekte } int read_phonebook(char *name) { int i, k; char buffer[80]; FILE *stream; strcpy(buffer, "sort "); strcat(buffer, name); if ((stream = popen(buffer, "r")) == NULL) { perror("telefonbuch"); return(0); } i = 0; while (fgets(buffer, 80, stream) != NULL) if (strlen(buffer) > 1) { /* Namen einlesen */ k = get_entry(buffer, phonebook[i][0]); if (buffer[k] != ’\t’) { fprintf(stderr, "fehlerhafter Eintrag:" " %s\n", buffer); pclose(stream); return(0); } while (buffer[++k] == ’\t’); /* Nummer einlesen */ get_entry(&(buffer[k]), phonebook[i][1]); if (++i == MAX_ENTRIES) { fprintf(stderr, "Telefonbuch zu lang.\n"); pclose(stream); return(i); } } pclose(stream); if (i == 0) fprintf(stderr, "kein Eintrag gefunden.\n"); return(i);
10.2 Telefonbuch mit automatischer Anwahl 303 76 } 77 78 79 int main(int argc, char *argv[]) 80 { 81 int n, error; 82 83 if ((argc != 2) || (strcmp(argv[1], "-h") == 0)) 84 { 85 printf("telefonbuch - automatische Anwahl mit" 86 " einem analogen Modem.\n"); 87 printf("Aufruf: telefonbuch Datei\n"); 88 return(1); 89 } 90 91 if ((n = read_phonebook(argv[1])) < 1) 92 return(1); 93 94 if (error = open_modem()) 95 { 96 fprintf(stderr, "telefonbuch: open_modem(): %s\n", 97 strerror(error)); 98 exit(1); 99 } 100 if (error = reset_modem()) 101 102 { 103 fprintf(stderr, "telefonbuch: reset_modem(): %s\n", 104 strerror(error)); 105 exit(1); 106 } 107 108 error = eingabe(n); 109 110 close_modem(); 111 return(error); 112 } In Zeile 12 wird das Telefonbuch als Feld mit MAX ENTRIES Einträgen und je zwei Zeichenketten mit MAX CHARS Zeichen pro Eintrag definiert. Beide Konstanten sind in telefonbuch.h festgelegt. Die erste Zeichenkette jedes Eintrags enthält den Namen, die zweite Zeichenkette die Telefonnummer (ohne Sonderzeichen wie /“ oder -“). Die Hilfsfunktion get entry() in den Zeilen 14 bis 32 wird ” ” von der Funktion read phonebook() zum Auswerten der Telefonliste verwen-
304 10 Beispielprojekte det. Sie kopiert Zeichen aus der Zeichenkette buffer nach dest, bis sie auf ein Tabulator- oder Zeilenvorschubzeichen stößt. Die Funktion read phonebook() (Zeile 34 bis 76) ruft zunächst mit der Funktion popen() das Shell-Programm sort“ auf; als Kommandozeilenparameter wird ” der Dateiname der Telefonliste übergeben. Als Rückgabewert liefert popen() den Stream mit den Ausgaben von sort, also die alphabetisch sortierte Telefonliste. In der while()-Schleife (Zeile 49 bis 69) werden die sortierten Einträge der Telefonliste in das Feld phonebook eingelesen. Die Funktion read phonebook() wird danach beendet und gibt die Anzahl der eingelesenen Einträge zurück. In der Funktion main() (ab Zeile 79) werden zunächst die Kommandozeilenparameter geprüft und anschließend die Telefonliste eingelesen. Danach wird die Schnittstelle zum Modem geöffnet und das Modem (sicherheitshalber) zurückgesetzt (Zeile 101). Dazu dienen zwei in modem.h deklarierte und in modem.c definierte Funktionen. In Zeile 108 wird dann die Benutzerschnittstelle gestartet, die in eingabe.c und eingabe.h definiert bzw. deklariert ist. Wie bereits erwähnt, wird neben telefonbuch.c“ auch eine Include-Datei te” ” lefonbuch.h“ benötigt, in der die globalen Konstanten definiert und das Feld phonebook deklariert sind: 1 2 3 4 5 6 7 8 /* telefonbuch.h */ # define MAX_ENTRIES 500 # define MAX_CHARS 40 extern char phonebook[MAX_ENTRIES][2][MAX_CHARS]; 10.2.4 Funktionen zur Ansteuerung des Modems Der Zugriff auf die serielle Schnittstelle sowie die Ansteuerung des Modems über diese Schnittstelle sind in dem Modul modem.c“ realisiert:1 ” 1 /* 2 modem.c - Funktionen zur Ansteuerung des Modems 3 */ 4 1 In diesem Teil wird der so genannte AT-Befehlssatz“ verwendet und auf Modem-Register zugegrif” fen. Die AT-Befehle wurden von der Firma Hayes eingeführt und haben sich als De-facto-Standard etabliert – man spricht auch vom Hayes-Standard“ und von Hayes-kompatibel“. Tatsächlich gibt ” ” es Unterschiede zwischen den Modems der verschiedenen Hersteller. Die hier verwendeten Befehle sollte dennoch jedes Modem verstehen. Für weitere Informationen schauen Sie in das Handbuch Ihres Modems oder auf die im Anhang angegebene Internet-Seite. Sie können auch mit dem Programm terminal.c aus Kapitel 6 einmal direkt AT-Befehle an das Modem senden und sich die Antwort“ ” des Modems ansehen.
10.2 Telefonbuch mit automatischer Anwahl 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # # # # # # # include include include include include include include <errno.h> <string.h> <unistd.h> <fcntl.h> <termios.h> <sys/select.h> <sys/time.h> # define MODEM_DEV "/dev/modem" # define SPEED B19200 int modem_fd = -1; /* Datei-Deskriptor d. Modems */ int open_modem() { if ((modem_fd = open(MODEM_DEV, O_RDWR)) == -1) return(errno); else return(0); } void close_modem() { if (modem_fd != -1) { close(modem_fd); modem_fd = -1; } return; } int reset_modem() { struct termios term_attr; fd_set fdset; struct timeval timeout; /* RS232 konfigurieren */ if (tcgetattr(modem_fd, &term_attr) != 0) return(errno); term_attr.c_cflag = CS8 | CLOCAL | CREAD; term_attr.c_iflag = 0; term_attr.c_oflag = 0; term_attr.c_lflag = 0; 305
306 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 10 Beispielprojekte cfsetospeed(&term_attr, SPEED); cfsetispeed(&term_attr, SPEED); if (tcsetattr(modem_fd, TCSAFLUSH, &term_attr) != 0) return(errno); /* Modem testen */ if (write(modem_fd, "\r\n", 2) != 2) return(errno); usleep(200000); if (write(modem_fd, "ATZ\r\n", 5) != 5) return(errno); usleep(500000); FD_ZERO(&fdset); FD_SET(modem_fd, &fdset); timeout.tv_sec = 2; timeout.tv_usec = 0; /* 2 Sek. Timeout */ /* Antwort vom Modem? */ if (select(modem_fd+1, &fdset, NULL, NULL, &timeout) <= 0) return(EBUSY); term_attr.c_cflag |= CRTSCTS; if (tcsetattr(modem_fd, TCSAFLUSH, &term_attr) != 0) return(errno); if (write(modem_fd, "ATS7=2\r\n", 8) != 8) return(errno); usleep(200000); return(0); } int dial(char *number) { int l; if (write(modem_fd, "ATDT ", 5) != 5) return(errno); l = strlen(number); if (write(modem_fd, number, l) != l) return(errno);
10.2 Telefonbuch mit automatischer Anwahl 93 94 95 96 307 if (write(modem_fd, "\r\n", 2) != 2) return(errno); return(0); } Die Funktionen open modem() und close modem() (Zeile 18 bis 34) dienen zum Öffnen und Schließen des Devices. In der Funktion reset modem() werden zunächst die Parameter der zuvor geöffneten Schnittstelle eingestellt. Danach wird in Zeile 54 eine Zeilenende-Sequenz (CR-LF) an das Modem geschickt. Dies ist erforderlich, damit sich das Modem auf die RS232-Parameter (Baudrate, Bits/Zeichen, Parity usw.) einstellen kann. Im Anschluss wird das Modem zurückgesetzt (Zeile 57). Dieses Zurücksetzen quittiert das Modem im Allgemeinen mit OK“. In den Zeilen 61 bis 70 wird maximal zwei Sekunden auf diese Quittung ” gewartet. Kommt vom Modem keine Antwort, bricht die Funktion mit dem Fehler EBUSY ( device or ressource busy“) ab (Zeile 70). Erst danach wird an der ” Schnittstelle das Hardware-Handshake RTS/CTS eingeschaltet (Zeile 72 bis 75). Wäre dies bereits vor dem Senden der ersten Zeichen an das Modem geschehen und wäre das Modem nicht bereit, Zeichen zu empfangen (z. B. weil es ausgeschaltet ist), würde der Prozess spätestens beim Aufruf von close() hängen, und das Programm ließe sich nur noch mittels Ctrl-C abbrechen. In Zeile 77 wird das Register S7 des Modems auf den Wert 2 gestellt. Damit erreicht man, dass das Modem zwei Sekunden nach dem Wählen der Telefonnummer auflegt und die Verbindung an das Telefon übergibt.1 Die Funktion dial() veranlasst das Modem, die als Zeichenkette übergebene Telefonnummer anzuwählen. Alle vier Funktionen aus modem.c“ sind auch ” in modem.h“ deklariert und werden so für die anderen Programm-Module ” verfügbar gemacht: 1 2 3 4 5 6 7 8 /* modem.h - Funktionen zur Ansteuerung des Modems */ int open_modem(void); void close_modem(void); int reset_modem(void); int dial(char *number); 10.2.5 Die Benutzerschnittstelle Die Funktionen zur Steuerung der Ein- und Ausgaben des Programms sind in dem Modul eingabe.c“ zusammengefasst: ” 1 Register S7 gibt die Zeit an, die das Modem maximal auf einen Trägerton des angerufenen Modems wartet.
308 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 10 Beispielprojekte /* eingabe.c - curses-Benutzerschnittstelle */ # # # # # # include include include include include include <string.h> <unistd.h> <ncurses.h> <ctype.h> "telefonbuch.h" "modem.h" /*---- Hilfsfunktionen ----*/ void show_list(int n, int pos) { int i, start; start = (pos < LINES/2)? 0 : pos - LINES/2; erase(); move(0, 0); attrset(A_NORMAL); for (i=start; i<pos; i++) printw("%-40s %s\n", phonebook[i][0], phonebook[i][1]); attrset(A_REVERSE); printw("%-40s %-38s\n", phonebook[pos][0], phonebook[pos][1]); i++; attrset(A_NORMAL); while ((i < n) && (i < LINES+start)) { printw("%-40s %s\n", phonebook[i][0], phonebook[i][1]); i++; } refresh(); return; } int search(int n, char letter) { int i; letter = toupper(letter);
10.2 Telefonbuch mit automatischer Anwahl 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 for (i=0; i<n; i++) if (toupper(phonebook[i][0][0]) >= letter) return(i); return(n-1); } void show_message(char *message) { int width; WINDOW *win; width = strlen(message)+4; win = newwin(5, width, LINES/2-6, (COLS-width)/2); box(win, 0, 0); mvwprintw(win, 2, 2, "%s", message); wrefresh(win); sleep(3); delwin(win); touchline(stdscr, LINES/2-6, 5); refresh(); return; } /*---- Hauptfunktion ----*/ int eingabe(int n) { int c, pos; WINDOW *win; if ((win = initscr()) == NULL) { fprintf(stderr, "initscr() fehlgeschlagen.\n"); return(1); } cbreak(); noecho(); curs_set(0); keypad(win, TRUE); pos = 0; show_list(n, pos); /* Cursor unsichtbar */ /* Sondertasten auswerten */ 309
310 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 10 Beispielprojekte while ((c = getch()) != ’\33’) /* ESC = Abbruch */ if ((c >= ’A’) && (c <= ’z’)) { pos = search(n, c); show_list(n, pos); } else if (c == ’\n’) /* Auswahl */ { dial(phonebook[pos][1]); show_message("Bitte Hörer abnehmen!"); } else if (c == KEY_UP) /* Zeile hoch */ { if (pos > 0) show_list(n, --pos); } else if (c == KEY_DOWN) /* Zeile runter */ { if (pos < n-1) show_list(n, ++pos); } else if (c == KEY_PPAGE) /* Seite hoch */ { pos -= LINES-1; if (pos < 0) pos = 0; show_list(n, pos); } else if (c == KEY_NPAGE) /* Seite runter */ { pos += LINES-1; if (pos >= n) pos = n-1; show_list(n, pos); } erase(); refresh(); endwin(); return(0); } In den Zeilen 14 bis 38 wird eine Hilfsfunktion definiert, die den Inhalt des Fensters neu schreibt und dabei den Eintrag mit dem Index pos durch invertierte Darstellung hervorhebt (siehe auch Abbildung 10.3 auf Seite 313). Der Parameter
10.2 Telefonbuch mit automatischer Anwahl 311 n gibt die Anzahl der Einträge im Telefonbuch an. Es ist zu beachten, dass die Funktionen erase() (Zeile 19) und printw() der libncurses zunächst nur die Datenstrukturen des Fensters modifizieren – ohne sichtbaren Effekt. Erst mit Aufruf der Funktion refresh() in Zeile 36 wird der Inhalt der Datenstrukturen auf das Fenster übertragen. Das Einlesen von Tastatureingaben – beispielsweise mit getch() – bewirkt ebenfalls die Aktualisierung des Fensterinhaltes. Die in den Zeilen 40 bis 49 definierte Funktion search() sucht den ersten Eintrag im Telefonbuch, der mit dem Zeichen letter beginnt. Diese Hilfsfunktion wird verwendet, um durch Eintippen des Anfangsbuchstabens auf den ersten dazu passenden Eintrag zu springen. Die dritte Hilfsfunktion show message() stellt für drei Sekunden den in message angegebenen Text in einem eingerahmten Fenster dar. Nach dem Löschen dieses Fensters mit delwin() in Zeile 62 muss dem darunterliegenden Hauptfenster signalisiert werden, welche Zeilen durch das zweite Fenster überschrieben wurden. Dies geschieht hier mit der Funktion touchline(). Alternativ kann die Funktion touchwin() verwendet werden, die das gesamte Fenster zum Neuzeichnen markiert.1 Kern der Benutzerschnittstelle ist die ab Zeile 70 definierte Funktion eingabe(). Sie initialisiert zunächst das Hauptfenster und stellt dessen Eigenschaften ein (Zeile 75 bis 84). Danach wird die Telefonliste mit der oben definierten Funktion show list() dargestellt, wobei der erste Eintrag selektiert ist. Ab Zeile 89 beginnt die Hauptschleife, in der Tastatureingaben eingelesen und ausgewertet werden. Folgende Eingaben sind möglich: Eingabe Funktion ’A’–’Z’ ↓, ↑ PgDn, PgUp Enter Esc ersten Eintrag mit diesem Anfangsbuchstaben selektieren nächsten bzw. vorhergehenden Eintrag selektieren eine Seite vor- bzw. zurückblättern selektierte Telefonnummer anwählen Programm beenden Von den in eingabe.c“ definierten Funktionen wird lediglich die Hauptfunkti” on, also eingabe(), im Hauptprogramm verwendet. Dementsprechend fällt die Include-Datei eingabe.h“ relativ kurz aus: ” 1 /* 2 eingabe.h - curses-Benutzerschnittstelle 3 */ 4 5 int eingabe(int n); 1 touchline() und touchwin() bewirken selbst keine Aktualisierung des Fensterinhaltes. Sie mar- kieren die Datenstrukturen des Fenster so, dass die betroffenen Zeilen bzw. das gesamte Fenster beim nächsten Aufruf von refresh() neu geschrieben werden.
312 10 Beispielprojekte Als Letztes fehlt nur noch das Makefile“, mit dessen Hilfe sich das Projekt kom” pilieren lässt: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # # Makefile für ’telefonbuch’ # OBJ = telefonbuch.o modem.o eingabe.o HDR = telefonbuch.h modem.h eingabe.h LIB = -lncurses telefonbuch: %.o: $(OBJ) gcc $ˆ $(LIB) -o $@ %.c $(HDR) gcc -c $< clean: rm -f $(OBJ) Nach erfolgreichem Übersetzen des Quelltextes kann das Programm wie folgt aufgerufen werden: telefonbuch Dateiname wobei Dateiname der Name der Telefonliste ist. Abbildung 10.3 zeigt das Erscheinungsbild des Programms. 10.2.6 To Do Dieses Beispielprogramm ist sicherlich ausbaufähig. Die folgenden interessanten Ergänzungen könnte man sich für das Programm vorstellen: Unterstützung einer Konfigurationsdatei, in der z. B. die Modem-Schnittstelle, Baud-Rate usw. eingetragen und damit ohne erneutes Kompilieren verändert werden können. Umstellung auf eine dynamische Speicherreservierung für das Telefonbuch anstelle einer festen Obergrenze für die Anzahl der Einträge. Eine Online-Hilfe für das Programm Automatisches Anpassen der Darstellung an die Konstante COLS, sodass auch weniger als 80 Zeichen pro Zeile möglich sind.
10.2 Telefonbuch mit automatischer Anwahl Abbildung 10.3: Das Programm telefonbuch“ in Aktion. ” 313

Anhang A1 – Daten zum Buch im Internet Um Ihnen beim Ausprobieren der zahlreichen Beispielprogramme aus diesem Buch Tipparbeit zu ersparen, sind die Quelltexte aller Beispiele1 sowie weitere nützliche Dateien als Download im Internet verfügbar. Unter der Adresse http://downloads.hanser.de gelangen Sie zu einem Eingabeformular, in das Sie die ISBN-Nummer oder den Titel dieses Buches eintragen können. Im Download-Bereich sind folgende Daten verfügbar: – spca5xx.tar.gz – svgalib-1.9.25.tar.gz – 3.Auflage.tar.gz – Quelltexte.tar.gz alle Beispielquelltexte nach Kapiteln geordnet Treiber für USB-Kameras (Quelltext) Quelltext der SVGALIB inkl. Demos Abschnitte aus der 3. Auflage, die nicht in die 4. Auflage übernommen wurden A2 – Das X11-Toolkit XView Das Toolkit XView/OpenLook“ als Nachfolger von SunView“ wird leider nicht ” ” weiterentwickelt oder gepflegt. Daher ist es auf (fast) keiner aktuellen LinuxDistribution mehr enthalten. Aus diesem Grund wurden die Kapitel und Beispielprogramme der 3. Auflage dieses Buchs, die sich mit XView befassten, nicht mehr in die aktuelle Auflage übernommen. Im Download-Bereich (siehe Anhang A1) sind jedoch sowohl die entsprechenden Seiten der 3. Auflage im PDF- und PostScript-Format als auch die Beispielprogramme und ein Installationspaket für die XView-Dateien enthalten. 1 Als Beispiele“ bezeichnen wir die vollständigen Programme, die im Buch mit Zeilennummern am ” linken Rand versehen sind.
316 Anhang A3 – Aufbau einer WAV-Audiodatei Die meisten Audio-Samples sind im WAV(E)-Format gespeichert, einem Standard von Microsoft und IBM, der erstmals unter Windows 3.1 implementiert wurde. Wie viele andere Audio- oder Grafikformate ist auch das WAVE-Format in so genannte Chunks aufgeteilt – Abschnitte, die durch ein Schlüsselwort am Anfang gekennzeichnet sind, gefolgt von der Länge dieses Abschnittes. Der Vorteil solcher Formate liegt in der Erweiterbarkeit ohne Kompatibilitätsverlust. Enthält die Datei einen Chunk, der dem Programm nicht bekannt ist – beispielsweise mit Informationen über Autor und Erstelldatum –, kann dieser Abschnitt mit Hilfe der Längeninformation einfach überlesen“ und somit ignoriert werden. Das ” Schlüsselwort jedes Chunks umfasst immer vier Zeichen. Die Längeninformation besteht ebenfalls aus vier Bytes. Eine WAV-Datei beginnt immer mit den vier Zeichen RIFF“, gefolgt von vier By” tes für die Länge der gesamten Datei, abzüglich der acht Bytes für das RIFF“ ” und die Längeninformation. Alle Zahlenwerte wie auch die Längenangaben beginnen immer mit dem niederwertigsten Byte (little endian encoding). Es folgt die Zeichenfolge WAVE“; damit umfasst dieser Dateikopf insgesamt 12 Bytes. ” Neben dieser Kopfinformation enthält eine WAV-Datei mindestens zwei Chunks: die Audioparameter und die Audiodaten selbst. Die Audioparameter folgen in der Regel unmittelbar auf die Kopfinformation (also ab dem 13. Byte) und sind durch die Zeichenfolge fmt⊔“ gekennzeichnet (Achtung: Das letzte Zeichen ist ” ein Leerzeichen!). Nach diesen acht Bytes folgen vier Bytes für die Länge dieses Chunks. Sie beträgt normalerweise 16 Bytes und setzt sich wie folgt zusammen: Parameter Länge Bemerkung Codierungsart Anzahl Kanäle Samplingrate Bytes / Sekunde Bytes / Sample Bits / Sample 2 Bytes 2 Bytes 4 Bytes 4 Bytes 2 Bytes 2 Bytes 1=linear, 2=ADPCM, 6=A-law, 7=µ -law 1=mono, 2=stereo Samples pro Kanal / Sekunde Speicherbedarf / Sekunde =(Bytes / Sekunde) / Samplingrate 8 (unsigned char) oder 16 (short) Ist der Chunk länger als 16 Bytes, enthalten die zusätzlichen Bytes weitere Informationen, die aber ignoriert werden können. Der letzte Abschnitt einer WAV-Datei ist immer der Daten-Chunk. Dieser wird durch das Schlüsselwort data“ eingeleitet, wiederum gefolgt von der Länge ” dieses Chunks, die der Restlänge der Datei entsprechen sollte. Bei StereoAufzeichnungen folgen die Samples für den linken und rechten Kanal im Wechsel, angefangen mit dem linken Kanal. Bei 16 Bit pro Sample kommt auch hier wieder das niederwertige Byte zuerst. Das folgende Beispiel zeigt eine WAV-Datei mit 20 Samples bei 22050 Hz, mono und 8 Bit pro Sample (linear codiert):
Anhang 317 ’R’ ’I’ ’F’ ’F’ 0x38 0x00 0x00 0x00 ’W’ ’A’ ’V’ ’E’ ’f’ ’m’ ’t’ ’ ’ 0x10 0x00 0x00 0x00 0x01 0x00 /* Codierung: linear */ 0x01 0x00 /* 1 Kanal (mono) */ 0x22 0x56 0x00 0x00 /* 22050 Samples/s */ 0x22 0x56 0x00 0x00 /* 22050 Bytes/s */ 0x01 0x00 /* 1 Byte pro Sample */ 0x08 0x00 /* 8 Bit pro Sample */ ’d’ ’a’ ’t’ ’a’ 0x14 0x00 0x00 0x00 0x80 0xCA 0xF9 0xF9 0xCA 0x7F 0x35 0x06 0x06 0x35 0x7F 0xCA 0xF9 0xF9 0xCA 0x7F 0x35 0x06 0x06 0x35 A4 – Aufbau einer AU-Audiodatei Etwas seltener als das WAVE-Format wird für Audio-Daten das AU-Format verwendet, welches eher aus der Unix-Welt stammt (Sun, NeXT, DEC). Bei diesem Format ist den Sampling-Daten ein Header vorangestellt, der Auskunft über die Audio-Parameter gibt. Dieser Kopf ist wie folgt aufgebaut: Pos. 0 4 8 12 16 20 24 Parameter Bemerkung .snd“ ” Offset Länge Format Samplingrate Kanäle Info Schlüsselwort zur Kennzeichnung Position der Samplingdaten in der Datei Anzahl der Daten-Bytes 1=8-Bit µ -law, 2=8-Bit linear, 3=16-Bit linear Abtastfrequenz in Hz 1 (mono) oder 2 (stereo) Beschreibung (Zeichenkette) Im Gegensatz zum WAVE-Format werden beim AU-Format alle Zahlen in umgekehrter Reihenfolge gespeichert, also beginnend mit dem höchstwertigen Byte. Auch das Format für 8-Bit-Samples ist anders: die Samples werden als char gespeichert, also mit Vorzeichen! Der Wertebereich für 8-Bit-Signale geht somit von –128 bis +127. A5 – Linux-Programmierung unter Windows: Cygwin Vielleicht ist Ihnen einmal der Gedanke gekommen, dass Sie ein unter Linux erstelltes Programm gern auch unter WindowsTM nutzen würden. Insbesondere wenn Sie sich mit Linux-Spezifischem wie Device-Programmierung auseinandergesetzt haben, möchten Sie das Gleiche nicht für das Erstellen eines vergleichbaren Windows-Programms neu erlernen, sondern möglichst dieselben Verfahren oder sogar denselben Quelltext verwenden.
318 Anhang Genau zu diesem Zweck wurde vor einiger Zeit das Projekt Cygwin“ ins Leben ” gerufen. Dabei handelt es sich nicht um einen Emulator, sondern um eine Linuxkompatible Umgebung unter Windows, inklusive der Linux-Shell bash und dem Compiler gcc. Das Herzstück von Cygwin sind DLLs (Dynamic Link Libraries), die die Linux-Systemfunktionen unter Windows realisieren. Damit können LinuxProgramme, die unter Cygwin kompiliert wurden, direkt unter Windows (in der Eingabeaufforderung) gestartet werden! Lediglich die cygwin.dll muss im Windows-Suchpfad für DLLs vorhanden sein oder im gleichen Verzeichnis wie das Linux-Programm stehen. Verwendet ein Programm zusätzliche LinuxBibliotheken wie beispielsweise die libm oder die libncurses, müssen auch die entsprechenden Cygwin-DLLs beim Starten des Programms verfügbar sein. Um Linux-Quelltexte unter Cygwin zu kompilieren, müssen Sie die vollständige Cygwin-Umgebung installiert haben. Diese können Sie kostenlos von der Homepage des Cygwin-Projektes http://www.cygwin.com laden und installieren. Ein Großteil der in diesem Buch beschriebenen Beispielprogramme lässt sich problemlos unter Cygwin kompilieren und steht dann auch als Windows-Programm zur Verfügung. Beispiele für die Leistungsfähigkeit von Cygwin sind die Linux-Programme mkisofs“ und cdrecord“, die – unter Cygwin kompiliert – auch unter Win” ” dows einwandfrei funktionieren und mit einer Größe von nur wenigen 100 kByte das Brennen von Daten-CDs ermöglichen. Es wurden inzwischen auch ein X11-Server, verschiedene X11-Toolkits (GTK+, XView) und auch die libusb nach Cygwin portiert. Auch die in Kapitel 7 beschriebenen Beispielprogramme zur Netzwerkkommunikation sind alle unter Cygwin lauffähig. Einschränkungen gibt es hingegen bei einigen Devices – beispielsweise /dev/dsp“ hat nur eingeschränkte Funktionalität, /dev/mixer“ ” ” und /dev/video“ sind gar nicht verfügbar – und bei der Interprozesskommu” nikation mittels Shared Memory (vgl. Abschnitt 5.4.3).
Literaturverzeichnis [1] Jürgen Plate: Linux Hardware Hackz“, Hanser Verlag, 2007 ” [2] Jürgen Plate: Linux Server für Intranet und Internet“, Hanser Verlag, 2000 ” [3] Homepage der Eclipse Entwicklungsumgebung: http://www.eclipse.org/ [4] Homepage des C Development Tooling“ für Eclipse: ” http://www.eclipse.org/cdt [5] Tutorial für die Eclipse Entwicklungsumgebung mit CDT: http://ittk.falb.at/pt/unterlagen/Eclipse_CDT_Tutorial.pdf [6] Linux Man-Pages online lesen: http://linux.die.net/man/ http://linuxmanpages.com/ http://man-wiki.net/index.php/Main_Page http://wwwcip.informatik.uni-erlangen.de/man Deutsche Man-Pages zum Download: http://www.infodrom.org/projects/manpages-de/changes-0.4.←֓ php3 [7] Tutorials zur libpthread im Internet: http://www.laptev.org/doc/pthreads.html http://users.actcom.co.il/˜choo/lupg/tutorials/multi-thre←֓ ad/multi-thread.html [8] Edward A. Falk: Summary of CDROM ioctl calls“, ” http://www.kernel.org/doc/Documentation/ioctl/cdrom.txt [9] Peter Vollenweider: Die PostScript-Sprache“ ” http://www.id.uzh.ch/cl/zinfo/pdf/Einfuehrung.pdf [10] Claudio Marxer: Postscript“ ” http://informatik.unibas.ch/lehre/fs09/cs506/_Downloads/c←֓ s506-20090423.pdf
320 Literaturverzeichnis [11] Oliver Thilmann: PostScript“ ” http://linuxfocus.org/Deutsch/May1998/article43.html [12] Linux-Treiber für USB Raketenwerfer: http://lukecole.name/usb_missile_launcher.php [13] Dokumentation der libusb“: ” http://libusb.sourceforge.net/api-1.0/index.html [14] James Marshall: HTTP Made Really Easy“: ” http://www.jmarshall.com/easy/http/ [15] UPnP-Dokumentationen: http://www.upnp.org/resources/documents.asp [16] UPnP Device Architecture“: ” http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitectu←֓ re-v1.0.pdf [17] Wikipedia.de, Stichwort IP-Adressen“: ” http://de.wikipedia.org/wiki/IP-Adresse#Besondere_IP-Adre←֓ ssen [18] Matthias Warkus: Das GTK+/GNOME-Entwicklerhandbuch“, dpunkt ” Verlag, 2008 [19] GKT-Tutorials im Internet (englisch): GTK 1.2: http://www.gtk.org/tutorial1.2/ GTK 2.0: http://library.gnome.org/devel/gtk-tutorial/2.17/ GKT-Referenz: http://www.gtk.org/documentation.html [20] Homepage der SVGALIB: http://www.svgalib.org Dort kann unter Development versions“ die Version 1.9.25 als Quelltext ” bezogen werden (kostenlos).
Stichwortverzeichnis 3D 260, 268 A-law 126, 316 a.out 8 Abtastfrequenz 128 Action-Area 221 Adjustment 231 alarm() 87 Anjuta 14, 31 apt 4 argc 43, 44 argv 43, 44 AT-Befehle 304 AU-Datei 317 AU-Format 130 Audioformat 126 Aufnahmequelle 125 Auswahlfelder 224 Authentifizierung 298 Base64 299 bindtextdomain() 58 Body 185 Box 217 Breakpoint 11, 18 Broadcast 166, 197 Browser 183 Buttons 213 C-Compiler 8 Callback 210 capture 137 cbreak() 63 CDT 14, 39 cfsetispeed 149 cfsetospeed 149 Check-Button 226 Client-Server-Prinzip 169 Client-Server-System 205 clone() 101 close() 108 Connection 166 Container 213 CUPS 149 curses 61 Cygwin 1, 317 Datei-Deskriptor 68 dbkg 4 ddd 10 Debugger 10 Device 107 dgettext() 55 Dialog 221 Dienst 165 DrawingArea 244 Drucker 149 dup2() 94 Echo 195 Eclipse 14, 39 Editor 6 Eigenschaften 75 Eingabefelder 228 endwin() 64 Entwicklungsumgebung 11, 31
322 Stichwortverzeichnis EOF 71 errno 51 Escape-Sequenz 60 Ethernet 164 Event 211, 213 exec-Familie 81 exit() 51 Grafikbeschleuniger 266 Graphics Context 249, 268 grep 29 GTK+ 206 gtk-config 207 GUI 21 gzip 51 fclose() 70 Fehlermeldungen 50 Fehlersuche 16 feof() 71 ferror() 71 fflush() 73, 85 fgetc() 71 fgets() 73 FIFO 95 FILE 28 fileno() 68, 110 find 29 Firefox 183 Firewall 166, 204 fopen() 69 fork() 82 fprintf() 73 fputc() 71 fputs() 73 frame 217 fread() 72 fscanf() 73 fseek() 73 Funktionsbibliothek 19 fwrite() 72 Handshake 307 Hardware 271 HTTP 184, 283 HTTP-Server 184 GC 249 gcc 2, 8, 16 gdb 10, 17 gepufferte Ein-/Ausgabe getch() 64 getopt() 46 getppid() 79 gettext() 55 getyx() 64 ghostview 151 Grafikauflösung 258 Label 220 LANG 54 Langtextoption 45 Lautstärke 120 LBA 116 libjpeg 139 libm 20 libncurses 2 libpthread 103 Library 19 libvga 256 I/O-Ports 271, 273 IDE 11 Include-Datei 28 initscr() 63 Inode 107 ioctl() 110 ioperm() 272 iopl() 272 IP-Adresse 165 JPEG 139 Kanal 121, 122 kate 7 kdbg 10, 19 KDevelop 14, 36 keypad() 64 Keyword 26 kill() 90 Kommandozeilenparameter 67 44, 45
Stichwortverzeichnis 323 libvgagl 266 little endian 316 locale 53 lpd 149 lpr 149 main() 43 make 2, 8, 23 Makefile 9, 24 man 26, 51 MANPATH 51 Maus 262 Maus-Typ 262 Menü 233 Message Object 57 Methode 185 MIDI 107 Mixer 122 mkfifo() 95 mmap() 73, 136 Modem 300, 304 Modem-Steuerleitungen 279 MSF 116 msgfmt 59 Multicast 199 Multipart 284 Multi-Tasking 79 munmap() 73, 136 mvprintw() 64 ncurses 61, 300 nedit 7 Netzwerkkommunkation noecho() 63 Objekt 206 Objektdatei 8, 23 objektorientiert 206 open() 108 opendir() 77 Option -g 11 Parallelport 274 pclose() 100 perror() 50, 55 163 Perspektive 260 Pipe 73, 91 pipe() 91 Pixmap 238 pkg-config 207 Platzhalter 47 poll() 194 popen() 100, 304 Port 165 Portable Message 58 Portable Pixmap 133 POSIX 103 PostScript 149 PPM 133 pread() 109 Primzahlen 14 Protokoll 164 Protokollfamilie 170 Prozess-ID 79 PS/2 262 Puffer 67 Pulldown-Menüs 233 pwrite() 109 Rückgabewert 44 Radio-Button 227 Raketenwerfer 156 read() 92, 109 readdir() 77 recvfrom() 192 RGB-Format 135 Rollbalken 250 RS232 142 Samplingrate 128 Schaltflächen 213 sched yield() 86 Scrolled Window 250 Scrollen 61 Sektion 26 select() 110, 148 sendto() 192 Server-Programm 178 Server-push-Prinzip 284 setlocale() 54
324 Shared Memory 97 Shell-Programm 43 shmat() 97 shmctl() 97 shmdt() 97 shmget() 97 Sicherheit 204 SIG DFL 88 SIG IGN 88 Signal 86, 211 signal() 88 Signal-Handler 88 sleep() 85 Socket 170 Soundkarte 121 spca5xx 130 Spin-Button 231 stat() 75 stderr 50 Steuersequenzen 60 Stream 68 strerror() 51 strip 11 SuSE 2 svgalib 256 system() 80 Task 79 tee 69 Terminal-Device 142 Textdomain 56 textdomain() 58 Thread 79, 103 Timeout 87 tkman 26 Toggle-Button 226 Toolkit 206 Tooltips 216 Transparenz 238 TV-Karte 130 u-law 126, 316 UART 279 Ubuntu 4 udev 108 Stichwortverzeichnis UDP 191 Umlaute 255 ungetc() 71 Unicode 206 UPnP 200 USB 154 User ID 76 usleep() 85 UTF-8 206, 255 uuencode 299 V24 142 Verbindung 166 verbindungsorientiert Verzeichnis 77 Video 130 wait() 89 waitpid() 89 Warnmeldung 16 Warnung 16 Warteschleifen 85 WAV-Datei 316 WAV-Format 130 WebCam 130, 283 Webserver 184 WEXITSTATUS() 90 Widget 206 WNOHANG 90 write() 109 X11 205 X11-Display 238 xgettext 58 xinetd 195 xman 26 XPM-Format 238 XView 315 xwpe 14 xxgdb 10 YaST 2, 3, 167 Zahlenformat 55 Zeichenfläche 244 Zugriffsverfahren 164 166
C UND LINUX = DREAM TEAM // ■ Erfahren Sie, wie Sie mit eigenen C-Programmen die Möglichkeiten von Linux ausnutzen können. ■ Nutzen Sie die praktischen Lösungen für häufig auftauchende Aufgabenstellungen. ■ Viele kleine Beispiele und zwei umfangreiche Beispielprojekte geben Ihnen Anregungen für eigene Programme. ■ Im Internet: Die Quelltexte zum Buch, Tools und weiterführende Informationen C UND LINUX // Wer Erfahrungen in der C-Programmierung hat, kann die vielen Möglichkeiten von Linux kennen lernen und ausschöpfen. Dieses Buch zeigt wie und enthält praktische Lösungen für häufig auftauchende Aufgabenstellungen. Als Grundlage wird zunächst der Umgang mit wichtigen Werkzeugen erklärt, die in jeder Linux-Distribution enthalten sind: Editor, Compiler, Debugger, Make usw. Danach führt der Autor Sie anhand vieler Beispiele in diese Themen ein: • Zugriff auf Dateien und Verzeichnisse mit C • Programme mit parallel laufenden Prozessen schreiben • USB-Geräte, CD-Laufwerk, Soundkarte und WebCams ansteuern • Programme mit grafischer Benutzeroberfläche erstellen • Direkte Hardware-Zugriffe programmieren • Eigene Hilfe-Seiten erstellen • Die automatische Anpassung auf die eingestellte Landessprache Wie diese Themen in »echten« Software-Projekten zusammenspielen, können Sie an zwei umfangreichen Beispielprojekten nachverfolgen. Die 4. Auflage wurde vollständig überarbeitet. Neu aufgenommen wurden Abschnitte zu den Entwicklungsumgebungen KDevelop und Eclipse, zur Kommunikation mit USB-Geräten, zu Broadcast und Multicast und zu Universal Plug And Play (UPnP). //… enthält eine Vielzahl an wichtigen Informationen zur Systemprogrammierung unter Linux. [...] Schöne Beispiele, die Funktionsaufrufe gut kommentiert und eine interessante Themensammlung. // c-plusplus.de Martin GRÄFE promovierte im Bereich Elektrotechnik/Mikroelektronik. Im Rahmen seiner Ingenieurstätigkeit entwickelt er Software für Unix-Workstations und seit 1995 auch für Linux-Systeme. www.hanser.de/computer ISBN 978-3-446-42176-9 Linux-User C-Programmierer 9 783446 421769