Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Linux-UNIX-Programmierung von Jürgen Wolf
Das umfassende Handbuch – 2., aktualisierte und erweiterte Auflage 2006
Buch: Linux-UNIX-Programmierung

Linux-UNIX-Programmierung
1216 S., mit CD, 49,90 Euro
Rheinwerk Computing
ISBN 3-89842-749-8
gp Kapitel 13 Terminal E/A und Benutzerschnittstellen für die Konsole
  gp 13.1 termios
    gp 13.1.1 Terminalattribute bearbeiten
    gp 13.1.2 Flags setzen und löschen
    gp 13.1.3 Terminalidentifizierung
    gp 13.1.4 Geschwindigkeitskontrolle – Baudrate von Terminals einstellen
  gp 13.2 terminfo
    gp 13.2.1 terminfo verwenden
    gp 13.2.2 terminfo initialisieren – setupterm()
    gp 13.2.3 Eigenschaften eines Terminals (Finden von capnames) – tigetflag(), tigetnum() und tigetstr()
    gp 13.2.4 Mit terminfo-Eigenschaften arbeiten – putp(), tputs(), tparm()
  gp 13.3 ncurses – Halbgrafik
    gp 13.3.1 ncurses initialisieren
    gp 13.3.2 Tastaturmodus und Ein- und Ausgabe
    gp 13.3.3 Eigenschaft der Fenster
    gp 13.3.4 Scrolling
    gp 13.3.5 Attribute und Farben setzen
    gp 13.3.6 Fensterroutinen
    gp 13.3.7 Mausprogrammierung mit ncurses

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.


Rheinwerk Computing

13.1 termios  downtop

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:

gp  Wie kann ich ein Zeichen von der Tastatur lesen?
gp  Wie kann man den Standardzeichensatz von Linux verändern?
gp  Wie kann ich herausfinden, welche Terminals gerade offen sind?

Für ein Terminal stehen Ihnen zwei verschiedene Modi zur Verfügung:

gp  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.
gp  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.



Rheinwerk Computing

13.1.1 Terminalattribute bearbeiten  downtop

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.

gp  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.
gp  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:

gp  TCSANOW – Änderung wird sofort aktiv.
gp  TCSADRAIN – Änderung wird aktiv, wenn alle Zeichen aus dem Puffer ausgegeben wurden.
gp  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.


Rheinwerk Computing

13.1.2 Flags setzen und löschen  downtop

Um einzelne oder mehrere Flags für c_iflag, c_oflag, c_cflag und c_lflag zu setzen bzw. abzufragen, wird wie folgt vorgegangen:

gp  Flag(s) setzen:
// einzelnes Flag setzen c_iflag | <flag>               // mehrere Flags setzen ( hier flag1, flag2 und flag3) c_iflag | <flag1|flag2|flag3>
gp  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.


Rheinwerk Computing

13.1.3 Terminalidentifizierung  downtop

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).


Rheinwerk Computing

13.1.4 Geschwindigkeitskontrolle – Baudrate von Terminals einstellen  toptop

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.

 << zurück
  
  Zum Rheinwerk-Shop
Neuauflage: Linux-UNIX-Programmierung
Neuauflage:
Linux-UNIX-
Programmierung

bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Linux-Server






 Linux-Server


Zum Rheinwerk-Shop: Das Komplettpaket LPIC-1 & LPIC-2






 Das Komplettpaket
 LPIC-1 & LPIC-2


Zum Rheinwerk-Shop: Linux-Hochverfügbarkeit






 Linux-
 Hochverfügbarkeit


Zum Rheinwerk-Shop: Shell-Programmierung






 Shell-
 Programmierung


Zum Rheinwerk-Shop: Linux Handbuch






 Linux Handbuch


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
Info





Copyright © Rheinwerk Verlag GmbH 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern