Aus Linux-Magazin 12/2012

Kernel- und Treiberprogrammierung mit dem Linux-Kernel – Folge 65

© psdesign1, Fotolia

Wer mit Linux zu tun hat, sieht sich realen, virtuellen oder Boot-Konsolen gegenüber, begegnet klassischen, Pseudo- oder auch Controlling-Terminals. Die Kern-Technik bringt Ordnung in die babylonische Sprachverwirrung und zeigt, wie man einen eigenen Terminaltreiber schreibt.

Als Unix-Rechner noch Schrankgröße hatten, liefen die Ein- und Ausgaben über Terminals. Ein Terminal, etwa das VT100 von Digital Equipment (DEC), bestand aus einer Tastatur und einem Monitor, der nur Text in grüner Schrift auf dunklem Hintergrund darstellte. Es war per serieller Schnittstelle mit dem eigentlichen Rechner verbunden. Als Multiuser-Systeme konnten Unix-Rechner meist mehrere Terminals bedienen, wobei jenes, das direkt beim Rechner stand und Boot- und sonstige Systemnachrichten anzeigte, als Konsole bezeichnet wurde [1].

Moderne Linux-Systeme passen längst in unsere Hosentaschen und eine serielle Schnittstelle zum Anschluss eines VT100 sucht man am Gehäuse vergebens. Auch wenn das klassische Terminal also ausgedient hat, starten Profis direkt nach dem Hochfahren von X-Window und dem Login etwas, das sie ebenfalls Terminal nennen, um textbasiert schnell mit dem System zu kommunizieren.

Allerdings handelt es sich bei diesem Terminal nicht mehr um Hardware, sondern um Software. Unabhängig von der Realisierungsform ist das Konzept des Terminals aber auch heute noch zentraler und zeitgemäßer Bestandteil eines Unix- beziehungsweise Linux-Systems. Dabei findet sich Linux auch in vielen Embedded-Geräten, die weder einen Tastaturanschluss noch eine Grafikkarte aufweisen.

Terminal per VGA

Jedes Linux-System besitzt eine Gerätedatei namens »/dev/console« , und jede Applikation hat die Möglichkeit und das Recht, »/dev/tty« zu öffnen. Das Gerät »/dev/console« repräsentiert die klassische Konsole, manchmal auch als Systemkonsole bezeichnet. Sie dient dazu, Textmeldungen des Betriebssystems entgegenzunehmen, beispielsweise die Bootmessages.

Ubuntu Linux etwa leitet die Meldungen standardmäßig auf das erste virtuelle Terminal um. “Virtuell” deutet schon an: Das Terminal wird emuliert. Insbesondere die (textbasierten) Ausgaben werden per Treibersoftware über die eingebaute Grafikkarte (VGA) auf den Monitor gezaubert. Auf diese Art stellt Linux sogar mehrere virtuelle Terminals zur Verfügung, repräsentiert als Gerätedateien »/dev/ttyn« . Die Zahl n steht für die Nummer des Terminals, typischerweise zwischen 1 und 6.

Die virtuellen Terminals lassen sich über das gleichzeitige Drücken der Tasten [Strg]+[Alt]+[Fn] aktivieren. Daraufhin erscheinen die zugehörigen Ausgaben auf dem Bildschirm und die Tastatureingaben bekommen jene Applikationen weitergereicht, die lesend auf das Terminal zugreifen. In den meisten Fällen ist dies der Prozess »getty« , der den Benutzer auch nach seinem Login-Namen und dem Passwort fragt und in dessen Folge die Login-Shell startet.

Eine noch weiter gehende Entkopplung zwischen Applikation und Hardware realisieren die Pseudoterminals (»pty« ). Sie ermöglichen es einer Applikation wie etwa Telnet oder SSH, Texte gewohnt einfach sogar über Rechnergrenzen hinweg auszugeben. Aber auch die gängigen Terminalprogramme für X11 wie Gnome-Terminal oder das gute alte Xterm basieren auf den aus Master und Slave bestehenden Pseudoterminals.

Ein Programm wie Xterm öffnet dabei zunächst die Gerätedatei »/dev/pts/ptmx« . Der Kernel erstellt daraufhin ein neues Slave-Device »/dev/pts/n« , wobei n eine laufende Nummerierung darstellt. Nach entsprechender Initialisierung repräsentiert das neue Slave-Device eine Terminalschnittstelle, die die nun gestartete Shell mitgegeben bekommt (siehe Abbildung 1).

Abbildung 1: Pseudoterminals ermöglichen es, Applikation und Terminalhardware zu entkoppeln.

Abbildung 1: Pseudoterminals ermöglichen es, Applikation und Terminalhardware zu entkoppeln.

Eingaben von der Tastatur reicht der X-Server bei dieser Architektur per X11-Protokoll an Xterm weiter, das die Daten auf das Master-Interface schreibt. Der Kernel übergibt die Daten dann inklusive der Zwischenverarbeitung dem Slave-Device und damit der Shell. Der Rückweg, also die Ausgabe, vollzieht sich analog.

Zeilen-Disziplin

Bisher blieb unerwähnt, dass die Zwischenverarbeitung ein wesentliches Feature der Terminalschnittstelle bereitstellt. Das Besondere an einem Terminal ist nämlich, dass es die Eingaben nicht einfach so an den lesend zugreifenden Prozess weiterleitet. Vielmehr findet eine Interpretation der Daten statt. Tauchen im Datenstrom etwa die Zeichen für [Strg]+[C] auf, bekommt der lesende Prozess das Signal »SIGINT« übermittelt, woraufhin die meisten Tasks abbrechen. Einen Linefeed (»\n« , »0x0a« ) ersetzt das Terminal durch Carriage Return plus Linefeed (»\r\n« , »0x0d 0x0a« ).

Bei einem Terminal – gleichgültig ob real, virtuell oder pseudo – greift eine Applikation also immer über diese Zwischenschicht zu, die sich Line Discipline nennt. Durch Austauschen kann sie sogar je nach Anwendungsfall neben der normalen Terminalfunktionalität auch noch Übertragungsprotokolle wie SLIP oder PPP realisieren.

Diese Zwischenschicht ist darüber hinaus über die so genannten Terminalsettings (Termios) noch in weiten Bereichen konfigurierbar. Gibt der Anwender auf der Kommandozeile etwa »stty« ein, bekommt er die gerade aktuellen Terminalsettings angezeigt. Mit dem gleichen Kommando lassen sich diese auch verändern, um damit beispielsweise das Echo der Eingaben ein- oder auszuschalten oder die Übertragungsgeschwindigkeit einer seriellen Schnittstelle zu ändern.

"<a

Die immer wieder hilfreiche und oft blind eingetippte Variante »stty sane« überführt übrigens ein “verwirrtes” Terminal, bei dem alle Zeichen durcheinandergeraten sind, wieder in seinen normalen “gesunden” Zustand.

Damit ergibt sich im Kernel der in Abbildung 2 dargestellte dreischichtige Aufbau. Oben sitzt das TTY-Core-System, über das der Anwender beziehungsweise die Anwendung auf die Daten des Keyboards zugreift und Daten auf den Bildschirm ausgibt. Das TTY-Core schleift die Daten durch die Line-Discipline-Schicht (»ldisc« ) und diese wiederum holt die Daten beziehungsweise gibt die Daten weiter über den TTY-Treiber. Dieser Treiber ist für die Ein- und Ausgabe, beispielsweise über eine Hardware, zuständig. Mit Hilfe des Kommandos

Abbildung 2: Aufgaben des TTY-Subsystems sind die Interpretation und Modifikation ausgetauschter Daten.

Abbildung 2: Aufgaben des TTY-Subsystems sind die Interpretation und Modifikation ausgetauschter Daten.

cat /proc/tty/drivers

lässt sich anzeigen, welche TTY-Treiber zurzeit aktiv und über welche Gerätenummern sie zu erreichen sind.

Gerade bei der Portierung von Linux auf eine neue Hardwareplattform kann es notwendig sein, einen eigenen TTY-Treiber zu schreiben. Solange man nicht außergewöhnliche Anforderungen oder eine komplexe Hardware bedienen muss, ist das auch vergleichsweise leicht zu bewerkstelligen.

Aufgabe des TTY-Treibers ist es, Daten zwischen der realen oder virtuellen Terminalhardware (Ein-/Ausgabeport) und der Line Discipline auszutauschen. Daten von der Terminalhardware – einer Tastatur beispielsweise – erhält die Line-Discipline-Schicht durch den TTY-Treiber mit Hilfe eines Speicherbuffers. Zur Ausgabe ruft die Zwischenschicht eine Schreibfunktion des Treibers auf. Zusätzlich sind die Funktionen »tty_open()« und »tty_close()« erforderlich.

Schließen auf Befehl

Im Rahmen der »tty_open()« -Funktion erhält der TTY-Treiber den eigentlichen Ein-/Ausgabeport, repräsentiert durch die Adresse der zugehörigen Datenstruktur »struct tty_struct« . Innerhalb dieser Funktion kann der Code unter anderem eine Hardware-Initialisierung ausführen. Je nach Erfolg gibt die Funktion 0 oder einen Fehlercode zurück. Anders als bei normalen Treibern wird aber die Funktion »tty_close()« in jedem Fall aufgerufen, also auch dann, wenn »tty_open()« negativ quittiert wurde.

Die Funktion »tty_write()« kommt zum Aufruf, sobald die Line-Discipline-Schicht Daten zur Ausgabe parat hat. Parameter der Funktion ist mit der »struct tty_struct« ebenfalls wieder das ausgewählte Ausgabe-Interface. Die Funktion »tty_write()« gibt die Daten an die Hardware weiter. Da manche Terminalhardware langsam ist, kann das durchaus asynchron realisiert sein.

Um sicherzugehen, dass sich sämtliche Daten auch schreiben lassen, erwartet das TTY-Core zusätzlich noch die Methode »tty_write_room()« . Diese gibt jene Anzahl Bytes zurück, die beim nächsten Aufruf von »tty_write()« geschrieben werden können.

Nimmt die Terminalhardware Daten entgegen, bekommt das TTY-Core diese asynchron übergeben. Eine »tty_read()« -Methode, die das TTY-Core aufruft, ist nicht erforderlich. Vielmehr ist der Datentransport von der Terminalhardware hin zum Core über gemeinsame Speicherbereiche, die TTY-Buffer, früher auch Flip-Buffer genannt, realisiert (Abbildung 3). Die Empfangsfunktion nimmt die Daten entgegen und legt sie mit den entsprechenden Funktionen in die TTY-Buffer. Danach informiert sie das TTY-Core durch Aufruf der Funktion »tty_flip_buffer_push()« . Nach Analyse durch die Line Discipline reicht Linux die eventuell modifizierten Daten an eine Applikation weiter.

Abbildung 3: In TTY-Puffern liegen die Daten samt Statusflags.

Abbildung 3: In TTY-Puffern liegen die Daten samt Statusflags.

Als Letztes bleibt noch die Integration des TTY-Treibers in den Kernel übrig. Hierzu muss der Programmierer die Datenstruktur »struct tty_driver« geeignet initialisieren und dem Terminal-Subsystem übergeben. Dabei muss er den Namen des Treibers, die zugehörigen Gerätedateien, den Treibertyp, Terminalsettings und natürlich die Adressen der Treiberfunktionen spezifizieren.

Die Angabe einer Gerätenummer (Major- und Minor-Nummer) ist nicht unbedingt notwendig; fehlt sie, lässt sich das Terminal-Subsystem eine vom Kernel aushändigen. Ist die Datenstruktur initialisiert, übergibt die Funktion »tty_register_driver()« sie dem Kernel.

Platz im Puffer

Listing 1 zeigt einen einfachen TTY-Treiber. In Ermangelung realer Terminalhardware gibt der Treiber die zu schreibenden Daten im Syslog als Hexwerte aus. Daher braucht er auch keine Daten zwischenzuspeichern, sodass die Funktion »tty_write_room()« einen festen Wert für den verbleibenden Speicherplatz zurückgeben kann. Ein High-Resolution-Timer [2] emuliert die Tastatur, indem er alle 2 Sekunden so tut, als hätte jemand den Text »Hi« eingetippt. Um bei parallelen Zugriffen auf den Treiber Inkonsistenzen zu vermeiden, erlaubt er über die Atomic-Variable »access_count« nur jeweils einer Instanz den Zugriff.

Listing 1

Basisfunktionen eines TTY-Treibers (tty_sample.c)

001 #include <linux/module.h>
002 #include <linux/ctype.h>
003 #include <linux/tty.h>
004 #include <linux/tty_flip.h>
005 #include <linux/interrupt.h>
006
007 static struct tty_driver *tty_sample_driver;
008 static struct tty_struct *tty_sample;
009 static ktime_t fireup_time;
010 static struct hrtimer emu_kbd_desc;
011 static atomic_t access_count = ATOMIC_INIT(-1);
012
013 static enum hrtimer_restart emu_keyboard( struct hrtimer *hrt )
014 {
015     hrtimer_forward_now( &emu_kbd_desc, ktime_set(1,0) );
016     tty_insert_flip_string( tty_sample, "Hi\n", 3 );
017     tty_flip_buffer_push( tty_sample );
018     return HRTIMER_RESTART;
019 }
020
021 static int tty_sample_open(struct tty_struct *tty,
022     struct file *filp)
023 {
024     if (!atomic_inc_and_test(&access_count))
025         return -EIO;
026     printk("tty_sample_open\n");
027     tty_sample = tty;
028     fireup_time = ktime_set( 2, 0 );
029     hrtimer_start(&emu_kbd_desc, fireup_time, HRTIMER_MODE_REL);
030     return 0;
031 }
032
033 static void tty_sample_close(struct tty_struct *tty,
034     struct file *filp)
035 {
036     printk("tty_sample_close\n");
037     atomic_dec( &access_count );
038     if (atomic_read(&access_count)==(-1) )
039         hrtimer_cancel( &emu_kbd_desc );
040 }
041
042 static int tty_sample_write(struct tty_struct *tty,
043     const unsigned char *buf, int count)
044 {
045     int i;
046
047     for(i=0; i<count; i++ )
048         printk("%02x %c", buf[i], isalpha(buf[i])?buf[i]:' ' );
049     printk("\n");
050     return i;
051 }
052
053 static int tty_sample_write_room(struct tty_struct *tty)
054 {
055     return 256; // always enough space
056 }
057
058 static const struct tty_operations tty_sample_ops = {
059     .open            = tty_sample_open,
060     .close           = tty_sample_close,
061     .write           = tty_sample_write,
062     .write_room      = tty_sample_write_room,
063 };
064
065 static int __init mod_init( void )
066 {
067     int ret=-1;
068
069     tty_sample_driver = alloc_tty_driver(1);
070     if (!tty_sample_driver)
071         return ret;
072     tty_sample_driver->owner        = THIS_MODULE;
073     tty_sample_driver->driver_name  = "tty-sample";
074     tty_sample_driver->name         = "ttySample";
075     tty_sample_driver->type         = TTY_DRIVER_TYPE_SERIAL;
076     tty_sample_driver->subtype      = SERIAL_TYPE_NORMAL;
077     tty_sample_driver->init_termios = tty_std_termios;
078     tty_set_operations(tty_sample_driver, &tty_sample_ops);
079
080     ret = tty_register_driver(tty_sample_driver);
081     if (ret) {
082         put_tty_driver(tty_sample_driver);
083         return ret;
084     }
085     hrtimer_init(&emu_kbd_desc,CLOCK_MONOTONIC,HRTIMER_MODE_REL);
086     emu_kbd_desc.function = emu_keyboard;
087     printk("tty_sample: initialized\n");
088     return 0;
089 }
090
091 static void __exit mod_exit(void)
092 {
093     hrtimer_cancel( &emu_kbd_desc );
094     tty_unregister_driver(tty_sample_driver);
095     put_tty_driver(tty_sample_driver);
096 }
097
098 module_init( mod_init );
099 module_exit( mod_exit );
100 MODULE_LICENSE("GPL");

Bei der Programmierung ist noch zu beachten, dass die Treiberfunktionen sowohl im Prozess- als auch im Interrupt-Kontext aufrufbar sind. Das hat zur Folge, dass der Programmierer nicht alle Funktionen, beispielsweise die zum Schlafenlegen, verwenden darf.

Die Wirkungsweise des Treibers und auch des Terminal-Subsystems lässt sich leicht testen. Dazu kompiliert man den Treiber mit Hilfe eines einfachen Makefiles und lädt ihn anschließend in den Kernel. Beim lesenden Zugriff auf die vom Treiber erzeugte Gerätedatei »/dev/ttySample0« erscheint der Text »Hi« . Das Terminal-Subsystem echot diesen Text auf die Ausgabe, was sich durch die im Syslog auftauchenden Hexziffern nachvollziehen lässt. Schaltet der Benutzer mit dem Kommando »stty -F /dev/ttySample0 -echo« das Echo ab, landet beim nächsten Lesen keine Ausgabe der Hexziffern mehr im Syslog.

Bei realer Hardware gestaltet sich der TTY-Treiber deutlich komplexer als der Beispielcode in diesem Artikel, besonders dann, wenn der Programmierer mit Bitraten, Parity und limitierten Sende- und Empfangspuffern einer klassischen seriellen Schnittstelle hantieren muss. Doch auch bei solchen Anwendungsfällen hilft der Kernel: Mit dem UART-Subsystem haben die Entwickler einen TTY-Treiber zur Verfügung gestellt, der nur noch um die spezifischen Hardwarezugriffe zu ergänzen ist [3].

Eine zweite Spezialform des TTY-Treibers ist die als Treiber für die Konsole, die es als Boot- und als Systemkonsole gibt. Erstere macht es möglich, bereits relativ früh im Bootprozess Ausgaben zu tätigen (»early_printk« ). Nach der Grundinitialisierung lässt sie sich durch eine oder mehrere Systemkonsolen ersetzten. Die damit erreichte Entkopplung der Konsole von einer dedizierten Hardware bringt Vorteile: Startet man beispielsweise unter X-Window ein Xterm mit der Option »-C« , landen alle Systemmeldungen in dem grafischen Terminal.

Sparversion

Wer kein grafisches Terminal benötigt, kann übrigens ausgesprochen kompakte Linux-Systeme bauen: 28 MByte Hauptspeicher und 12 MByte Festplatte reichen im Grunde bereits aus, das versprechen die Macher einer Mini-Distribution. Dabei wird die Bedeutung des Terminal-Subsystems deutlich. Zugunsten der kompakten Größe entfällt die Grafik – und damit tritt das Terminal mit dem antiquierten Namen in den Vordergrund. Wen wundert’s, dass die Entwickler ihre Distribution ausgerechnet TTYlinux [4] genannt haben? (mhu)

Infos

  1. Linus Akesson, “The TTY demystified”: http://www.linusakesson.net/programming/tty/index.php
  2. Quade, Kunst, “Linux-Treiber entwickeln”: Dpunkt-Verlag 2011
  3. Linux-Mips, “Serial Driver and Console”: http://www.linux-mips.org/wiki/Serial_Driver_and_Console
  4. TTYlinux, Projektseite: http://ttylinux.net/index.html.
  5. Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2012/12/kern-technik

Der Autor

Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Ihr Buch “Linux Treiber entwickeln” liegt in dritter Auflage vor.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben