Kapitel 13 Terminal E/A und Benutzerschnittstellen für die Konsole
Das Thema der Benutzerschnittstelle wird leider in vielen Büchern wenig bis gar nicht behandelt. Aber es ist gerade die Bedienung eines Programms, die zwischen einer brauchbaren und unbrauchbaren Anwendung unterscheidet. Dies gilt vor allem auch für die Kommandozeilenanwendungen – wo keine grafische Oberfläche für die Interaktion zwischen dem Benutzer und PC vorhanden ist.
Eines gleich zu Beginn. Bei diesem Kapitel handelt es sich lediglich um eine Einführung zu den Themen rund um das Terminal und deren Ein- und Ausgabe. Eine umfangreichere Abhandlung zu den Themen termios, terminfo und curses würde ein ganzes Buch füllen. Dennoch sind diese Grundlagen ausreichend, um anschließend mithilfe der entsprechenden Manual Pages sinnvoll damit arbeiten zu können.
13.1 termios
 
In diesem Kapitel erfahren Sie, wie Sie ein Terminal kontrollieren können. Dabei werden Sie auf einer sehr tiefen Ebene, der API termios, die von POSIX.1 standardisiert wurde, zugreifen. Terminals sind über serielle Schnittstellen mit Linux-Rechnern verbunden. Um Sie jetzt nicht zu verwirren, wenn Sie eine Konsole verwenden, arbeiten Sie in gleicher Weise wie mit einer Terminalleitung, auch wenn keine Verbindung über die serielle Schnittstelle vorliegt. Somit ist die Grundlage eines auf dem Bildschirm konfigurierten Terminals gleichwertig zu einem externen angeschlossenen Terminal.
Die Ein-/Ausgabe eines Terminals ist in der Grundeinstellung immer zeilengepuffert. Ein read() wird somit erst beendet, wenn die (ENTER)-Taste gedrückt wurde. Drücken Sie stattdessen die Tastenkombination (STRG)+(D), liefert read() 0 (EOF) zurück.
Hinweis Beim Editor vi bedeutet (STRG)+(D) z. B. eine halbe Seite weiterblättern. Natürlich wird hierbei auch kein zeilengepufferter Terminalmodus mehr verwendet (dazu in Kürze mehr).
|
Zugegeben, in der Zeit der GUIs (Graphical User Interfaces) mag dieses Thema (Terminal E/A) altertümlich erscheinen, aber wenn es um Netzwerkprogrammierung geht, sind Terminals immer noch unschlagbar. Das Kapitel Terminal E/A handelt u. a. von solchen Themen:
|
Wie kann ich ein Zeichen von der Tastatur lesen? |
|
Wie kann man den Standardzeichensatz von Linux verändern? |
|
Wie kann ich herausfinden, welche Terminals gerade offen sind? |
Für ein Terminal stehen Ihnen zwei verschiedene Modi zur Verfügung:
|
Der so genannte kanonische Modus: Hier erfolgt das Lesen und Schreiben zeilenorientiert. Das heißt, eine Eingabe wird erst weitergereicht, wenn ein Zeilenabschluss (Linefeed, NL) oder Carriage Return (CR) übertragen wurde. Für diesen Modus benötigt man die Steuerzeichen des c_cc-Arrays der termios-Struktur. Der Nachteil ist, dass ein Programm beim Lesen in diesem Modus so lange wartet (CPU-Zeit vergeudet), bis tatsächlich eine Zeile übertragen wurde. Wird kein Zeilenabschluss gelesen, so wird für immer gewartet. Die Aufgabe des Zwischenspeicherns übernimmt der Kernel in speziellen Puffern. Es gibt einen Eingabe- und einen Ausgabepuffer. Steuerzeichen regeln, wann welche Puffer entleert werden und somit ihren Inhalt an das Gerät oder das Programm weitergeben. |
|
Beim nicht kanonischen Modus erfolgt das Lesen oder Schreiben entweder nach einer bestimmten Anzahl an Bytes (was auch nur ein Byte und somit ein Zeichen sein kann), oder es kann eine gewisse Zeit dafür angegeben werden. Hierfür können folgende zwei Felder des Arrays c_cc gesetzt werden. In c_cc[VTIME] wird die entsprechende Zeit in Zehntelsekunden und in c_cc[VMIN] das Minimum der zu lesenden Bytes angegeben. |
Hinweis So toll sich das hier anhören mag, Sie sollten immer bedenken, dass hierbei doch ein nicht ganz unbedeutender Rechenaufwand betrieben wird, um etwa ein einzelnes Zeichen einzulesen. Mit Aufwand spreche ich hier die CPU-Zeit an, die für Terminal E/A benötigt wird. Bedenken Sie, dass diese API ein Relikt aus recht alten Zeiten ist, wo man mit dem Begriff »Multitasking« in der Praxis noch nichts anfangen konnte.
|
13.1.1 Terminalattribute bearbeiten
 
Alle Attribute, die Sie bei einem Terminal setzen bzw. abfragen können, sind in der Struktur termios in der Headerdatei <termios.h> definiert.
struct termios {
tcflag_t c_iflag; /*Eingabeflag*/
tcflag_t c_oflag; /*Ausgabeflag*/
tcflag_t c_cflag; /*Kontrollflag*/
tcflag_t c_lflag; /*Lokale Flags*/
cc_t c_cc[NCCS]; /*Steuerzeichen*/
};
Jede dieser Strukturvariablen können Sie mit einem bestimmten Terminalflag besetzen bzw. erfragen, ob ein bestimmtes Flag gesetzt ist. Mit der Strukturvariablen c_iflag können die Abläufe für die Eingabeoptionen gesetzt oder erfragt werden. Dies beinhaltet den ganzen Terminalablauf, wie der Terminaltreiber die Eingabe noch vor dem Senden an das Programm behandeln soll.
Den Ablauf zur Ausgabe behandeln Sie mit dem Flag c_oflag. Damit können Sie einstellen, wann und wie der Terminaltreiber die Ausgabe behandelt, bevor etwas auf dem Bildschirm ausgegeben wird.
Verschiedene Kontrollflags, die vorwiegend die Merkmale der Hardware vom Terminalgerät berücksichtigen, können Sie mit dem Flag c_cflag setzen.
Mit c_lflag können Sie Terminaleigenschaften setzen bzw. abfragen, wie z. B. ob etwas bei der Eingabe von der Tastatur auf dem Bildschirm ausgegeben werden soll.
Im Array c_cc finden Sie spezielle Zeichensequenzen (Steuerzeichen) wie ^\ (Quit).
Im Folgenden sind die einzelnen Flags und deren Bedeutung aufgelistet, die bei den eben aufgezählten Strukturvariablen in der Struktur termios gesetzt bzw. erfragt werden können. Aufgelistet sind nur die Flags, die von POSIX.1 vorgeschrieben sind. In BSD und SVR4 finden Sie hierzu noch einige erweiterte Flags, worauf hier verzichtet wurde – da diese in der Praxis auch nicht allzu häufig anzutreffen sind. Einen Überblick und noch mehr Details aller möglichen Flags finden Sie in der Manual Page zu termios (man termios).
Folgende symbolische Konstanten können für das Eingabeflag tcflag_t c_iflag verwendet werden:
Tabelle 13.1
Angaben für das Eingabeflag c_iflag
Konstante
|
Bedeutung
|
BRKINT
|
Generieren von SIGINT bei Tastendruck auf (BREAK)
|
ICRNL
|
Umwandeln von CR (Carriage Return) in NL (Newline) bei der Eingabe
|
IGNBRK
|
Break ignorieren
|
IGNCR
|
CR (Carriage Return) ignorieren
|
IGNPAR
|
Bytes mit Paritätsprüfung ignorieren
|
INLCR
|
Umwandeln von NL (Newline) in CR (Carriage Return) – bei CR ignorieren der Eingabe
|
INPCK
|
Einschalten der Eingabeparitätsprüfung
|
ISTRIP
|
Abschneiden des 8. Bits bei Eingabezeichen
|
IXOFF
|
Einschalten des START-/STOPP-Eingabeprotokolls
|
IXON
|
Einschalten des START-/STOPP-Ausgabeprotokolls
|
PARMRK
|
Markieren von Paritätsfehlern
|
Außer den beiden Flags IXOFF und IXON sollte Ihnen die Bedeutung der einzelnen Flags recht verständlich sein. Zu IXOFF und IXON folgt hierfür noch eine genauere Erläuterung.
|
IXOFF – Wird IXOFF gesetzt, dann ist das START-/STOPP-Eingabeprotokoll gesetzt. Dies wird wie folgt benötigt. Wenn ein Terminalgerätetreiber feststellt, dass der Eingabepuffer voll ist, wird das Zeichen STOPP an den Terminal ausgegeben. Das sendende Gerät soll dadurch feststellen, dass es seine Übertragung kurz anhalten soll. Ist wieder Platz im Eingabepuffer, wird das START-Zeichen ausgegeben, und der Sender kann mit dem Senden von Daten fortfahren. Dieses Flag wird nur bei seriellen Terminals verwendet, da es hierbei im Gegensatz zu Netzwerkterminals und lokalen Terminals keine eigene Form der Flusskontrolle gibt. |
|
IXON – Hiermit wird, falls gesetzt, dass START-/STOPP-Ausgabeprotokoll gesetzt. Erhält der Terminaltreiber ein STOPP-Zeichen, dann wird die Ausgabe angehalten. Bei einem START-Zeichen wird wieder mit der Ausgabe fortgefahren. |
Jetzt zu den Werten des Ausgabeflags tcflag_t c_oflag:
Tabelle 13.2
Angaben für das Ausgabeflag c_oflag
Konstante
|
Bedeutung
|
OPOST
|
Eine implementierungsdefinierte Ausgabeart einschalten
|
Als Nächstes folgt ein Überblick zu den Kontrollflags tcflag_t c_cflag:
Tabelle 13.3
Angaben für das Kontrollflag c_cflag
Konstante
|
Bedeutung
|
CLOCAL
|
Ausschalten der Modemsteuerung
|
CREAD
|
Aktivieren des Empfängers
|
CSIZE
|
Bitanzahl für ein Zeichen (CS5 bis CS8)
|
CSTOPB
|
Zwei Stopp-Bits anstelle von einem senden
|
HUPCL
|
Verbindungsabbruch bei Beendigung des letzten Prozesses
|
PARENB
|
Einschalten von Paritätsprüfung
|
PARODD
|
Ungerade Parität, sonst gerade
|
Zum Schluss folgt der Überblick zu den lokalen Flags (tcflag_t c_lflag):
Tabelle 13.4
Angaben für das lokale Flag c_lflag
Konstante
|
Bedeutung
|
ECHO
|
Einschalten der ECHO-Funktion
|
ECHOE
|
Gelöschte Zeichen mit Leerzeichen überschreiben
|
ECHOK
|
Zeichen löschen oder zur Neueingabe in neue Zeile positionieren
|
ECHONL
|
Ausgabe von NL
|
ICANON
|
Zeilenorientierter Eingabemodus
|
IEXTEN
|
Einschalten des erweiterten Zeichensatzes für die Eingabe
|
ISIG
|
Sonderbedeutung von Terminal-Sonderzeichen einschalten
|
NOFLSH
|
Abschalten des Leerens von Puffern bei INTR oder QUIT
|
TOSTOP
|
Senden des Signals SIGTTOU bei der Ausgabe durch Hintergrundprozesse
|
Die Funktionen, womit Sie auf die eben aufgelisteten Flags der Struktur termios und ihre Mitglieder zugreifen können, sind:
#include <termios.h>
#include <unistd. h.>
int tcgetattr(int fd, struct termios *terminal_zeiger);
int tcsetattr( int fd, int option,
struct termios *terminal_zeiger);
Wie man aus den Namen der Funktionen herauslesen kann, dient tcgetattr() dazu, die Attribute eines Terminals zu ermitteln, und tcsetattr() dazu, die Attribute zu setzen. Für den Dateideskriptor fd geben Sie den Filedeskriptor an, den Sie verändern bzw. abfragen wollen – 0 bzw. STDIN_FILENO (Standardeingabe) oder 1 bzw. SDTOUT_FILENO (Standardausgabe). Die Funktionen liefern beide bei einem Fehler –1, ansonsten, wenn alles planmäßig verlief, 0 zurück. Mit dem Argument option bei tcsetattr() können Sie angeben, wann die Veränderungen aktiv werden sollen. Folgende Flags können dabei verwendet werden:
|
TCSANOW – Änderung wird sofort aktiv. |
|
TCSADRAIN – Änderung wird aktiv, wenn alle Zeichen aus dem Puffer ausgegeben wurden. |
|
TCSAFLUSH – ... wie TCSADRAIN, nur dass noch alle befindlichen Zeichen im Puffer verworfen werden. |
Bevor Sie zur Praxis schreiten, benötigen Sie noch die Kenntnis über eine Variable in der Struktur termios, die noch nicht zur Sprache kam: das Array c_cc. Es enthält alle Sonderzeichen, die durch das Programm verändert werden können. Hier einige Sonderzeichen dazu. Weitere Zeichen finden Sie unter /usr/include/termbits.h oder /usr/include/asm/termbits.h.
Tabelle 13.5
Einige Angaben für das Struktur-Array c_cc
Konstante
|
Bedeutung
|
VEOF
|
Dateiende
|
VEOL
|
Zeilenende
|
VERASE
|
Letztes Zeichen löschen
|
VINTR
|
Unterbrechungssignal SIGINT
|
VKILL
|
Zeile löschen
|
VQUIT
|
Abbruch ŕ la SIGQUIT
|
VSTOP
|
Ausgabe anhalten
|
VSTART
|
Ausgabe fortsetzen
|
VSTATUS
|
Statusinformationen anfordern
|
VSUSP
|
Einen Prozess suspendieren
|
VWERASE
|
Letztes Wort löschen
|
VMIN
|
Anzahl der Bytes, die gelesen werden müssen, bevor read() zurückkehrt
|
VTIME
|
Es wird eine entsprechende Zeit in 0.1 Sekunden angegeben.
|
Dazu ein einfaches Beispiel, womit Sie das Zeichen für EOF, das mit (STRG)+(C) generiert wird, verändern können. Im Beispiel wird auch EOF ausgelöst, wenn Sie die (ESC)-Taste drücken. Die Tastenkombination (STRG)+(C) bleibt allerdings trotzdem erhalten.
/* my_eof.c */
#include <stdio.h>
#include <unistd. h.>
#include <termios.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main (void) {
struct termios term;
int c;
if ((tcgetattr (STDIN_FILENO, &term)) < 0) {
printf ("Fehler bei tcgetattr (%s) \n",
strerror(errno));
exit (EXIT_FAILURE);
}
// ASCII-Code 27 = ESC == EOF
term.c_cc[VEOF] = 27;
if ((tcsetattr (STDIN_FILENO, TCSAFLUSH, &term)) < 0) {
printf ("Fehler bei tcsetattr (%s) \n",
strerror(errno));
exit (EXIT_FAILURE);
}
printf("Eingabe machen (mit ESC abbrechen)\n");
while ((c = getchar ()) != EOF)
putchar (c);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o my_eof my_eof.c
$ ./my_eof
Eingabe machen (mit ESC abbrechen)
Eine Eingabe
Eine Eingabe
[ESC]
In diesem Beispiel haben Sie das Sonderzeichen EOF verändert. Mit der Angabe von
term.c_cc[VEOF] = 27;
bekommt das Sonderzeichen EOF (VEOF) den ASCII-Code 27 zugewiesen, der dem Escape-Zeichen entspricht. Mit
if((tcsetattr(STDIN_FILENO, TCSAFLUSH, &term)) > 0)
setzen Sie die Attributsänderung in Kraft. Jetzt folgt der Test, ob es auch funktioniert:
while((c=getchar()) != EOF)
putchar(c);
So lange, bis Sie EOF betätigen, könne Sie etwas mit der Tastatur eingeben. In diesem Beispiel wäre das Programm zu Ende, wenn die Escape-((ESC))-Taste gedrückt wird statt, wie gewöhnlich verwendet, die Tastenkombination (STRG)+(D).
Da nicht jeder sämtliche ASCII-Code-Zeichen im Kopf hat – geschweige den Wert der Sonderzeichen –, ist in der Headerdatei <termios.h> noch folgendes Makro definiert:
#ifndef CTRL
#define CTRL(ch) ((ch)&0x1F)
#endif
Falls dies bei Ihnen nicht vorhanden sein sollte, so wissen Sie ja jetzt, wie Sie es selbst definieren können. Damit können Sie die Eingabe eines Sonderzeichens, z. B. die Tastenkombination (STRG)+(Y), folgendermaßen festlegen:
term.c_cc[VEOF] = CTRL('Y');
Hiermit würden Sie mit der Tastenkombination (STRG)+(Y) das EOF-Zeichen generieren.
13.1.2 Flags setzen und löschen
 
Um einzelne oder mehrere Flags für c_iflag, c_oflag, c_cflag und c_lflag zu setzen bzw. abzufragen, wird wie folgt vorgegangen:
|
Flag(s) setzen: |
// einzelnes Flag setzen
c_iflag | <flag>
// mehrere Flags setzen ( hier flag1, flag2 und flag3)
c_iflag | <flag1|flag2|flag3>
|
Flag(s) löschen: |
// einzelnes Flag löschen
c_iflag & ~<flag>
// mehrere Flags löschen (hier flag1, flag2 und flag3)
c_iflag & ~<flag1|flag2|flag3>
Hierzu folgt ein einfaches Beispiel, in dem Sie eine Linux-übliche Passworteingabe machen können. Im Beispiel wird das Flag ECHO abgeschaltet, womit die Eingabe nicht am Bildschirm angezeigt wird. Das Flag ECHONL hingegen wird eingeschaltet, damit nach Eingabe des Passwortes durch Betätigung von (ENTER) dieses Newline-Zeichen ausgegeben wird – und das Programm auch sauber in der nächsten Zeile fortfährt. Am Ende stellen Sie den zuvor gesicherten Terminalzustand wieder her.
/* my_getpass.c */
#include <termios.h>
#include <stdio.h>
#include <stdlib.h>
#define PASS 8
static char *my_getpass (void) {
struct termios origstate, changedstate, teststate;
char passwort[PASS];
char *p = passwort;
/* alten Terminalzustand speichern (von stdin = id 0) */
if (tcgetattr (0, &origstate) != 0)
printf ("tcgetattr failed");
changedstate = origstate;
/* neuen Terminalzustand setzen (von stdin = id 0) */
/* Ausgabe ausschalten */
changedstate.c_lflag &= ~ECHO;
/* Newline beachten (einschalten) */
changedstate.c_lflag |= ECHONL;
if (tcsetattr (0, TCSANOW, &changedstate) != 0)
printf ("tcsetattr failed");
/* Überprüfen, ob der Terminalzustand erfolgreich */
/* verändert wurde */
tcgetattr (0, &teststate);
if (teststate.c_lflag & ECHO)
printf ("ECHO-Flag konnte nicht abgeschaltet werden\n");
if (!teststate.c_lflag & ECHONL)
printf ("ECHONL konnte nicht eingeschaltet werden\n");
fprintf(stdout,"Passwort eingeben : ");
fgets(passwort, PASS, stdin);
/* alten Terminalzustand wiederherstellen */
/* (von stdin = id 0) */
if (tcsetattr (0, TCSANOW, &origstate) != 0)
printf ("tcsetattr failed");
return p;
}
int main (void) {
char *ptr = my_getpass();
printf("Ihre Eingabe war: %s\n",ptr);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o my_getpass my_getpass.c
$./my_getpass
Passwort eingeben :********
Ihre Eingabe war: juergen
Jetzt zu einem weiteren Beispiel, das Ihnen zeigt, wie Sie bestimmte Steuerzeichen von der Tastatur abfragen können. Im Beispiel werden Tastendrücke wie (F1), (F2), die Pfeiltasten oben, unten, rechts und links, (Page_˝), (Page_Ľ)und (Home) überprüft. Sie können dies natürlich noch weiter ausstricken. Dabei werden die lokalen Flags ECHO, ICANON und ISIG abgeschaltet. Sie sollten das Beispiel außerdem in einem echten Terminal ausführen, damit es richtig läuft – sonst wird z. B. (F1) nicht angezeigt. Ein echtes Terminal können Sie durch gleichzeitiges Drücken von (STRG)+(ALT)+(F1) öffnen. Zurück zu Ihrer grafischen Oberfläche kommen Sie wieder mit der Tastenkombination (STRG)+(ALT)+(F7). Das Programm ist recht ordentlich dokumentiert, womit ich mir weitere Erklärungen ersparen kann.
/* keystroke.c */
#include <stdio.h>
#include <unistd. h.>
#include <termios.h>
#include <stdlib.h>
#include <string.h>
/*Altes Terminal wiederherstellen */
static struct termios BACKUP_TTY;
/* Eingabekanal wird so umgeändert, damit die Tasten */
/* einzeln abgefragt werden können */
static int new_tty (int fd) {
struct termios buffer;
/* Wir fragen nach den Attributen des Terminals und */
/* übergeben diese dann an buffer. BACKUP_TTY dient */
/* bei Programmende zur Wiederherstellung der alten */
/* Attribute und bleibt somit unberührt. */
if ((tcgetattr (fd, &BACKUP_TTY)) == -1)
return -1;
buffer = BACKUP_TTY;
/* Lokale Flags werden gelöscht : */
/* ECHO - Zeichenausgabe auf Bildschirm */
/* ICANON - Zeilenorientierter Eingabemodus */
/* ISIG – Terminal-Steuerzeichen (kein STRG+C mehr */
/* möglich) */
buffer.c_lflag = buffer.c_lflag & ~(ECHO | ICANON | ISIG);
/* VMIN=Anzahl der Bytes, die gelesen werden müssen, */
/* bevor read() zurückkehrt */
/* In unserem Beispiel 1 Byte für 1 Zeichen */
buffer.c_cc[VMIN] = 1;
/*Wir setzen jetzt die von uns gewünschten Attribute*/
if ((tcsetattr (fd, TCSAFLUSH, &buffer)) == -1)
return -1;
return 0;
}
/*Ursprüngliches Terminal wiederherstellen*/
static int restore_tty (int fd) {
if ((tcsetattr (fd, TCSAFLUSH, &BACKUP_TTY)) == -1)
return -1;
return 0;
}
int main (int argc, char **argv) {
int rd;
char c, buffer[10];
/*Setzen des neuen Modus*/
if (new_tty (STDIN_FILENO) == -1) {
printf ("Fehler bei der Funktion new_tty()\n");
exit (EXIT_FAILURE);
}
do {
/*Erste Zeichen lesen*/
if (read (STDIN_FILENO, &c, 1) < 1) {
printf ("Fehler bei read\n");
restore_tty (STDIN_FILENO);
exit (EXIT_FAILURE);
}
/*Haben wir ein ESC ('\E') gelesen? */
if (c == 27) {
/* Ja, eine Escape-Sequenz, wir wollen den Rest */
/* der Zeichen auslesen */
rd = read (STDIN_FILENO, buffer, 4);
buffer[rd] = '\0'; /*String terminieren */
/*Folg. Werte haben die Funktionstasten in der Term */
/* F1 = \E[[A */
/* F2 = \E[[B */
/* PFEIL RECHTS= \E[C */
/* PFEIL LINKS = \E[D */
/* PFEIL RUNTER= \E[B */
/* PFEIL HOCH = \E[A */
/* POS 1 = \E[1~ */
/* BILD RUNTER = \E[6~ */
/* BILD HOCH = \E[5~ */
if (strcmp (buffer, "[[A") == 0)
printf ("F1\n");
if (strcmp (buffer, "[[B") == 0)
printf ("F2\n");
if (strcmp (buffer, "[C") == 0)
printf ("->\n");
if (strcmp (buffer, "[D") == 0)
printf ("<-\n");
if (strcmp (buffer, "[B") == 0)
printf ("V\n");
if (strcmp (buffer, "[A") == 0)
printf ("^\n");
if (strcmp (buffer, "[1~") == 0)
printf ("POS 1\n");
if (strcmp (buffer, "[6~") == 0)
printf ("BILD RUNTER\n");
if (strcmp (buffer, "[5~") == 0)
printf ("BILD HOCH\n");
/* Nein, kein ESC ... */
}
else {
if ((c < 32) || (c == 127))
/*Nummerischen Wert ausgeben */
printf ("%d\n", c);
else
/*Zeichen ausgeben */
printf ("%c\n", c);
}
} while (c != 'q');
restore_tty (STDIN_FILENO);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o keystroke keystroke.c
$ ./keystroke
^
->
<-
V
BILD HOCH
BILD RUNTER
F1
F2
POS1
q
Zum Abschluss noch ein Beispiel, wie Sie eine einzelne Taste abfragen bzw. abfangen können. Dazu müssen Sie das Terminal in den Raw-Modus schalten – oder besser wäre wohl abschalten. Denn wie der Name raw (für roh, unbearbeitet) schon andeutet, handelt es sich hier um einen fast nackten Terminalmodus – dabei werden vorwiegend Flags ausgeschaltet. Lediglich das Kontrollflag, wie groß ein Zeichen in Bits ist, wird dabei gesetzt.
/* my_getch.c */
#include <stdio.h>
#include <unistd. h.>
#include <termios.h>
#include <stdlib.h>
static struct termios new_io;
static struct termios old_io;
/* Funktion schaltet das Terminal in den Raw-Modus: */
/* Folgende Eingabeflags werden gelöscht: */
/* BRKINT, ICRNL, INPCK, ISTRIP, IXON */
/* Folgende Kontrollflags werden gelöscht: CSIZE, PERENB */
/* Folgende Ausgabeflags werden gelöscht: OPOST */
/* Folgende lokale Flags werden gelöscht: */
/* ECHO, ICANON, IEXTEN, ISIG */
/* Gesetzt wird das Kontrollflag CS8, was bedeutet, */
/* dass ein Zeichen 8 Bit breit ist */
/* Steuerzeichen: Leseoperation liefert 1 Byte VMIN=1 VTIME=1 */
static int raw (int fd) {
/* Sichern unseres Terminals */
if ((tcgetattr (fd, &old_io)) == -1)
return -1;
new_io = old_io;
/* Wir verändern jetzt die Flags für den Raw-Modus */
new_io.c_iflag =
new_io.c_iflag & ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
new_io.c_oflag = new_io.c_iflag & ~(OPOST);
new_io.c_cflag = new_io.c_cflag & ~(CSIZE | PARENB);
new_io.c_lflag = new_io.c_lflag & ~(ECHO|ICANON|IEXTEN|ISIG);
new_io.c_cflag = new_io.c_cflag | CS8;
new_io.c_cc[VMIN] = 1;
new_io.c_cc[VTIME] = 0;
/*Jetzt setzen wir den Raw-Modus*/
if ((tcsetattr (fd, TCSAFLUSH, &new_io)) == -1)
return -1;
return 0;
}
/*Funktion zur Abfrage einer Taste*/
static int getch () {
int c;
if (raw (STDIN_FILENO) == -1) {
printf ("Fehler bei der Funktion raw()\n");
exit (EXIT_FAILURE);
}
c = getchar ();
/*Alten Terminalmodus wiederherstellen*/
tcsetattr (STDIN_FILENO, TCSANOW, &old_io);
return c;
}
int main (void) {
int zeichen;
printf ("Warte auf'q', um das Programm zu beenden\n");
while ((zeichen = getch ()) != 'q')
printf ("'%c' != 'q'\n", zeichen);
printf ("'%c' == 'q'\n", zeichen);
printf ("ENDE\n");
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o my_getch my_getch.c
$ ./my_getch
Warte auf'q', um das Programm zu beenden
'a' != 'q'
's' != 'q'
'd' != 'q'
'f' != 'q'
'j' != 'q'
'k' != 'q'
'l' != 'q'
'ö' != 'q'
'q' == 'q'
ENDE
Die Funktion raw() schaltet das Terminal in den Raw-Modus, und mit der Funktion getch() können Sie dann nach einer einzelnen oder irgendeiner Taste fragen. Den großen Overhead der Funktion raw() können Sie sich ersparen, da Linux/UNIX dafür eine Funktion für uns bereithält:
#include <termios.h>
int cfmakeraw(struct termios *tty_zeiger);
Diese Funktion ist ebenfalls in der Headerdatei <termios.h> definiert und erleichtert Ihnen den Schritt, das Terminal in den Raw-Modus zu setzen.
13.1.3 Terminalidentifizierung
 
Um den Namen des Terminals zu erfragen, der gerade die Kontrolle hat, können Sie folgende Funktion verwenden:
#include <stdio.h>
char *ctermid(char *s);
Die Funktion ctermid() gibt einen String mit dem Pfadnamen des Kontrollterminals für den aktuellen Prozess zurück. In den meisten Fällen dürfte dies /dev/tty sein. Für s sollten Sie mindestens L_ctermid Bytes Speicherplatz reservieren. Die symbolische Konstante L_ctermid ist in der Headerdatei <stdio.h> definiert. Die Funktion gibt bei Erfolg einen Zeiger auf einen Puffer zurück, der auf den Namen des Kontrollterminals verweist, ansonsten wird bei einem Fehler NULL zurückgegeben. Hierzu ein kurzes Beispiel der Funktion ctermid():
/* my_tty.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd. h.>
#include <termios.h>
static char termname[L_ctermid];
int main (void) {
const char *ptr = ctermid (termname);
if (ptr != NULL)
printf ("Kontrollterminalname = %s\n", ptr);
else
printf("Konnte Terminalname nicht ermitteln\n");
return EXIT_SUCCESS;
}
Wenn Sie die Adresse des Terminalpfadnamens benötigen, können Sie die folgende Funktion verwenden:
char *ttyname(int fd);
Die Funktion liefert einen nullterminierten Pfadnamen (in einem statischen Speicherbereich) des Terminalgerätes zurück, womit der Filedeskriptor fd geöffnet wurde. Bei einem Fehler oder wenn fd mit keinem Terminal verbunden ist, wird NULL zurückgegeben. Auch hierzu ein einfaches Beispiel:
/* sende.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd. h.>
#include <termios.h>
int main(int argc, char **argv) {
printf("Nachricht von Terminal %s\n%s",
ttyname(STDIN_FILENO), *++argv);
return EXIT_SUCCESS;
}
Öffnen Sie nun zwei Pseudo-Konsolen (pts) unter X. Ermitteln Sie zuerst mit diesem Programm den Pfad der Konsole (tty). In meinem Beispiel hat Konsole 1 den Pfad /dev/pts/2 und Konsole 2 den Pfad /dev/pts/3. Führen Sie das Programm folgendermaßen aus (der Programmname sei sende):
---[/dev/pts/2]---
$ ./sende hallo > /dev/pts/3
Auf der Konsole /dev/pts/3 steht jetzt:
---[/dev/pts/3]---
Nachricht von Terminal /dev/pts/2
hallo
Wenn Sie wollen, können Sie jetzt vom Terminal /dev/pts/3, wie eben gemacht, eine Nachricht an /dev/pts/2 senden. Dies funktioniert daher, da Sie mit
ttyname(STDIN_FILENO)
den Terminalnamen der Standardeingabe abfragen. Sie können ja statt der Standardeingabe die Standardausgabe abfragen (STDOUT_FILENO).
13.1.4 Geschwindigkeitskontrolle – Baudrate von Terminals einstellen
 
Jetzt zu einigen Funktionen, womit Sie die Geschwindigkeit der Ein-/Ausgabe eines Terminalgerätes verändern oder erfragen können. Diese Schnittstelle ist schon sehr alt, weshalb hier für die Geschwindigkeit noch Baud verwendet wird. Die korrekte Bezeichnung würde Bits pro Sekunde (bps) lauten. Damit können Sie zum Beispiel testen, wie die Geschwindigkeit zwischen zwei Terminals im Netzwerk ist. Folgende Funktionen stehen Ihnen zum Erfragen der Geschwindigkeit zur Verfügung:
#include <termios.h>
speed_t cfgetispeed(const struct termios *tty_zeiger);
speed_t cfgetospeed(const struct termios *tty_zeiger);
Beide Funktionen sind in der Headerdatei termios.h definiert und geben die momentan gesetzte Baudrate zurück, wobei cfgetispeed() die Geschwindigkeit der Eingabe und cfgetospeed() die Geschwindigkeit der Ausgabe zurückgibt. Folgende Konstanten sind hierfür auf alle Fälle definiert:
B0,B50,B75,B110,B134,B150,B200,B300,B600,
B1200,B1800,B2400,B4800,B9600,B19200,B38400
Wobei B0 für die Beendigung einer Verbindung steht. Weiterhin können (unter Linux auf alle Fälle) vorhanden sein:
B57600,B115200,B230400,B460800
Angewendet werden diese Funktionen folgendermaßen:
baudrate = funktion_erfrage_baudrate(cfgetispeed(&terminal));
...
unsigned long funktion_erfrage_baudrate(speed baudkonstante) {
if(baudkonstante == B0) return(0);
else if(baudkonstante == B50) return(50);
else if(baudkonstante == B75) return(75);
...
...
else if(baudkonstante == B460800) return(460800);
else return -1;
}
Hier das Beispiel, womit Sie die Baudrate eines Terminalgerätes erfragen, das Sie in der Kommandozeile als zweites Argument mit angeben.
/* baud.c */
#include <stdio.h>
#include <unistd. h.>
#include <termios.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
typedef unsigned long Ulong;
typedef struct termios TTY;
static speed_t baud_konstante[] = {
B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200,
B1800, B2400, B4800, B9600, B19200, B38400, B57600,
B115200, B230400/*, B460800 */
};
static Ulong baud_werte[] = {
0, 50, 75, 110, 134, 150, 200, 300, 600, 1200,
1800, 2400, 4800, 9600, 19200, 38400, 57600,
115200, 230400/*, 460800 */
};
static Ulong baudrate_wert (speed_t baud) {
int i;
for (i = 0; i <= 19; i++) {
if (baud == baud_konstante[i])
return (baud_werte[i]);
}
return -1;
}
int main (int argc, char **argv) {
int fd;
speed_t baud_input, baud_output;
TTY terminal;
if (argc != 2) {
printf ("Usage: %s Gerätepfad\n", *argv);
exit (EXIT_FAILURE);
}
if ((fd = open (argv[1], O_RDWR | O_NONBLOCK)) == -1) {
perror ("fopen()");
exit (EXIT_FAILURE);
}
if (isatty (fd) == 0) {
printf ("%s ist keine tty\n", argv[1]);
exit (EXIT_FAILURE);
}
if (tcgetattr (fd, &terminal) == -1) {
printf ("Fehler bei tcgetattr()\n");
exit (EXIT_FAILURE);
}
if((baud_input=baudrate_wert(cfgetispeed(&terminal))) == -1) {
printf ("Fehler bei baud_input ...\n");
exit (EXIT_FAILURE);
}
if((baud_output=baudrate_wert(cfgetospeed(&terminal))) == -1) {
printf ("Fehler bei baud_output ...\n");
exit (EXIT_FAILURE);
}
printf ("Eingabe-Baudrate : %d\n", baud_input);
printf ("Ausgabe-Baudrate : %d\n", baud_output);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o baud baud.c
$ ./baud /dev/tty
Eingabe-Baudrate : 38400
Ausgabe-Baudrate : 38400
Neu in diesem Programm ist die Funktion:
#include <unistd. h.>
int isatty(int fd);
Damit können Sie abfragen, ob ein Filedeskriptor auf das Terminal eingestellt ist. Wichtig an den Funktionen cfget...() und cfset...() ist, wenn Sie mit cfgetispeed() etwas abfragen wollen, führen Sie zuerst mit der Funktion tcgetattr() die Struktur termios für das betreffende Gerät eine Abfrage aus. Dasselbe gilt auch, wenn Sie die beiden folgenden Funktionen einsetzen wollen.
#include <termios.h>
int cfsetispeed( struct termios *termzeiger,
speed_t baudrate );
int cfsetospeed( struct termios *termzeiger,
speed_t baudrate);
Auch hier müssen Sie die Aufrufe erst mit dem tcsetattr()-Aufruf aktivieren. Hier das Beispiel, erweitert um die Möglichkeit, die Baudrate auf 115200 hoch zu setzen.
/* setbaud.c */
#include <stdio.h>
#include <unistd. h.>
#include <termios.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdarg.h>
typedef unsigned long Ulong;
typedef struct termios TTY;
static speed_t baud_konstante[] = {
B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200,
B1800, B2400, B4800, B9600, B19200, B38400, B57600,
B115200, B230400/*, B460800 */
};
static Ulong baud_werte[] = {
0, 50, 75, 110, 134, 150, 200, 300, 600, 1200,
1800, 2400, 4800, 9600, 19200, 38400, 57600,
115200, 230400/*, 460800 */
};
// R. W. Stevens-like Fehlerbehandlung
static void fehler_exit (char *error_msg, ...) {
va_list az;
va_start (az, error_msg);
vprintf (error_msg, az);
va_end (az);
exit (EXIT_FAILURE);
}
static Ulong baudrate_wert (speed_t baud) {
int i;
for (i = 0; i <= 19; i++) {
if (baud == baud_konstante[i])
return (baud_werte[i]);
}
return -1;
}
int main (int argc, char **argv) {
int fd;
speed_t baud_input, baud_output;
TTY terminal;
if (argc != 2)
fehler_exit ("Bitte einen Gerätepfad mit angeben\n");
if ((fd = open (argv[1], O_RDWR | O_NONBLOCK)) == -1)
fehler_exit ("Konnte %s nicht öffnen\n", argv[1]);
if (isatty (fd) == 0)
fehler_exit ("%s ist kein tty\n", argv[1]);
if (tcgetattr (fd, &terminal) == -1)
fehler_exit ("Fehler bei tcgetattr()\n");
if((baud_input=baudrate_wert(cfgetispeed(&terminal))) == -1)
fehler_exit ("Fehler bei baud_input ...\n");
if((baud_output=baudrate_wert(cfgetospeed(&terminal))) == -1)
fehler_exit ("Fehler bei baud_output ...\n");
printf ("Eingabe-Baudrate : %d\n", baud_input);
printf ("Ausgabe_Baudrate : %d\n", baud_output);
if ((cfsetispeed (&terminal, B115200)) == -1)
fehler_exit("Fehler beim Setzen von Eingabe-Baudrate ...\n");
if ((cfsetospeed (&terminal, B115200)) == -1)
fehler_exit("Fehler beim Setzen von Ausgabe-Baudrate ...\n");
if (tcsetattr (fd, TCSAFLUSH, &terminal) == -1)
fehler_exit ("Fehler bei tcsetattr ...\n");
if (tcgetattr (fd, &terminal) == -1)
fehler_exit ("Fehler bei tcgetattr ...\n");
if((baud_input=baudrate_wert(cfgetispeed(&terminal))) == -1)
fehler_exit ("Fehler bei baud_input ...\n");
if((baud_output=baudrate_wert(cfgetospeed(&terminal))) == -1)
fehler_exit ("Fehler bei baud_output ...\n");
printf ("Eingabe-Baudrate-neu : %d\n", baud_input);
printf ("Ausgabe_Baudrate-neu : %d\n", baud_output);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o setbaud setbaud.c
$ ./setbaud /dev/tty
Eingabe-Baudrate : 38400
Ausgabe_Baudrate : 38400
Eingabe-Baudrate-neu : 115200
Ausgabe_Baudrate-neu : 115200
Zum Thema Terminal E/A ließe sich noch eine ganze Menge mehr verfassen. Für den Fall, dass Sie mehr Informationen dazu benötigen, sei die Manual Page (man termios) empfohlen.
|