Hallo Welt! Tiefes Eintauchen in Terminals


Ich wurde inspiriert, diesen Artikel durch einen Artikel über die Analyse von Sishny printf zu schreiben. Es wurde jedoch ein Moment verpasst, in welche Richtung die Daten gehen, nachdem sie in das Endgerät eingegeben wurden. In diesem Artikel möchte ich diesen Fehler beheben und den Datenpfad im Terminal analysieren. Wir werden auch verstehen, wie sich Terminal von Shell unterscheidet, was Pseudoterminal ist, wie Terminalemulatoren funktionieren und vieles mehr.


Die Grundlagen


Lassen Sie uns zunächst verstehen, was Terminal, Shell, Konsole ist, wie sich der Terminal-Emulator vom normalen Terminal unterscheidet und warum er so benannt ist. Es wurden bereits viele Informationen darüber geschrieben, so dass Sie hier nichts Neues hören werden. Fast alle Informationen hier stammen aus dem Internet, ich werde am Ende des Artikels Links bereitstellen. Wer bereits weiß, was all diese Dinge bedeuten, kann diesen Abschnitt sicher überspringen.




Terminal


Ein Terminal ist eine Kombination aus einem Display und einer Tastatur, dh einem physischen Gerät. Bevor die Terminals zu dieser speziellen Kombination wurden, waren sie eine Art Gerät namens Fernschreiber (Teletyp, Teletypewriter oder kurz TTY), dh eine Kombination aus einem Drucker und einer Tastatur. In der Regel sind mehrere Terminals an denselben Computer angeschlossen. Somit war es möglich, für mehrere Benutzer am selben Computer zu arbeiten, und jeder hatte seine eigene Sitzung, unabhängig von den anderen. Das Terminal wurde so benannt, weil es sich am Ende des Terminalkabels befand.


Dies ist Teletyp :


Teletype

Und das ist Terminal :


Terminal



Konsole


Konsole (Konsole) - ein Terminal, das direkt mit dem Computer verbunden ist. Tatsache ist, dass die meisten Terminals implizit verbunden waren, aber mindestens eines direkt mit dem Computer verbunden war. Die Konsole durfte einen genau definierten Personenkreis verwenden, da Sie den Computer konfigurieren konnten.




Shell


Wenn es sich bei den beiden vorherigen Geräten um physische Geräte handelt, bezieht sich diese Definition ausschließlich auf Software.


Shell ist ein Befehlszeileninterpreter. Der Hauptzweck besteht darin, andere Programme auszuführen. Es gibt eine große Anzahl verschiedener Muscheln. Am gebräuchlichsten ist Bash (aus der englischen Bourne Again SHell, die, wie Wikipedia vorschlägt, ein Wortspiel für "Born again" Shell ist, dh eine "wiederbelebte" Shell). Andere Beispiele: Dash (eine leichte Shell, verfügbar, wenn Sie die Binärdatei unter / bin / sh ausführen), Zsh.




NatĂĽrlich konnten sowohl Terminals als auch Konsolen in der Neuzeit ihr Spiegelbild finden. Daher werden wir weiter Dinge wie Terminal Emulator und Virtual Console betrachten .


Terminalemulator


Terminal Emulator - ein Emulator des guten alten Terminals. Für Programme, die nicht direkt mit dem X Window System interagieren können, ist ein Terminalemulator erforderlich - Bash, Vim und andere.


Lassen Sie uns zunächst die Verantwortlichkeiten des Terminals festlegen:


  1. Ăśbertragen Sie Benutzereingaben auf einen Computer
  2. Lieferung der Computerausgabe an das Display

Unser Terminal-Emulator macht also genau das Gleiche: Er liefert Benutzereingaben an das laufende Programm und zeigt auch die Ausgabe des Programms auf dem Display an. In jedem Fall bleibt die Bedeutung erhalten - zwischen dem Benutzer und dem laufenden Programm gibt es eine Art Schicht, die fĂĽr die Eingabe / Ausgabe verantwortlich ist. Beispiele fĂĽr Terminal Emulator: gnome-terminal, xterm, konsole.


Bitte verwechseln Sie Shell und Terminal Emulator nicht!
Terminal Emulator ist eine GUI-Anwendung, dh ein Fenster im X Window System. Shell ist ein Befehlszeileninterpreter, dh nur ein BefehlsausfĂĽhrender, der keine grafische Shell hat. Wenn Sie ganz richtig sprechen, starten Sie Bash nicht , sondern fĂĽhren den Terminal Emulator aus, der Bash in sich selbst startet . Terminal Emulator und Bash sind absolut zwei verschiedene Programme. Der erste ist allein fĂĽr die Eingabe / Ausgabe verantwortlich, der zweite fĂĽr die Verarbeitung von Befehlen.


Weiter im Artikel beziehen sich alle Verweise auf das Terminal auf den Terminalemulator.




Virtuelle Konsole (virtuelles Terminal)


Drücken Sie Strg + Alt + FN, wobei N normalerweise Werte von 1 bis 6 hat. Was Sie gerade gesehen haben, heißt Virtuelle Konsole (virtuelle Konsole) oder Virtuelles Terminal (virtuelles Terminal). Erinnerst du dich, was ich früher über Terminals gesagt habe? Viele Terminals waren an einen Computer angeschlossen, und jedes Terminal war eine separate Sitzung, unabhängig von den anderen. Virtual Console wiederholt diese Idee: Es können mehrere unabhängige Sitzungen in Ihrem Computer vorhanden sein (die Computerressourcen werden jedoch offensichtlich immer noch gemeinsam genutzt).


Sie können diese Entität sowohl als virtuelle Konsole als auch als virtuelles Terminal bezeichnen, da eine Konsole per Definition ein Terminal ist, das direkt mit einem Computer verbunden ist, aber alle virtuellen Terminals gewissermaßen direkt mit einem Computer verbunden sind.




TTY-Geräte


Jedem Terminal ist ein eigenes TTY-Gerät (Terminal Device) zugeordnet, das die Konsole bereitstellt. Es ist zwar unwahrscheinlich, dass Sie Teletypen finden, aber die Verringerung der TTY hat bis heute überlebt.


Ein TTY-Gerät besteht aus zwei grundlegenden Komponenten:


  1. Gerätetreiber Er ist verantwortlich für die Übermittlung der Tastatureingabe an das Programm und für die Anzeige der Programmausgabe auf dem Bildschirm.
  2. TTY Line Discipline (Russisch - Liniendisziplin). Leitungsdisziplin ist die Treiberzugriffsschnittstelle, die dem TTY-Gerät jedoch viel Logik verleiht. Wir können sagen, dass Leitungsdisziplin-Proxies den Fahrer anrufen. Welchen Verantwortungsbereich diese Komponente hat, erfahren Sie im Artikel.

TTY-Gerät erstellen:



Es gibt 3 Arten von TTY-Geräten:


  1. Konsolengerät - Ermöglicht den Betrieb der virtuellen Konsole. Die Ein- und Ausgabe dieses Geräts wird vollständig vom Kernel gesteuert.
  2. PTY-Gerät (Pseudo-Terminal) - Bereitstellung des Terminalbetriebs in der Fensterschnittstelle. Die Ein- und Ausgabe dieses Geräts wird von einem Terminalemulator gesteuert, der im Benutzerbereich arbeitet.
  3. Serielles Gerät - kommuniziert direkt mit der Hardware. Es wird normalerweise nicht direkt verwendet, sondern ist die unterste Ebene in der Organisation der Architektur eines Endgeräts.

In diesem Artikel werden wir speziell auf den zweiten Typ von TTY-Geräten eingehen - Pseudo-Terminals.




TTY Line Disziplin


Wir beginnen, die Disziplin der TTY-Gerätelinie zu untersuchen.


Das erste wichtige Merkmal einer Liniendisziplin ist, dass sie für die Verarbeitung von E / A verantwortlich ist. Dies umfasst beispielsweise die Verarbeitung von Steuerzeichen (siehe Steuerzeichen ) und die Formatierung der Ausgabe. Zum Beispiel geben Sie einen beliebigen Text ein, aber plötzlich stellen Sie fest, dass Sie sich beim Schreiben von etwas geirrt haben und es löschen möchten - hier kommt die Liniendisziplin ins Spiel.


Wir werden im Detail analysieren, was genau passiert, wenn wir in Bash arbeiten und im Terminal laufen. Standardmäßig arbeitet ein TTY-Gerät im kanonischen Modus mit aktiviertem Echo . Ein Echo ist eine Anzeige der Zeichen, die Sie auf dem Bildschirm eingegeben haben.


Wenn wir zum Beispiel das Zeichen a eingeben, wird dieses Zeichen an das TTY-Gerät gesendet, aber von der Disziplin der TTY-Zeile des Geräts abgefangen. Sie liest ein Zeichen in ihren internen Puffer ein, sieht, dass der echo aktiviert ist, und zeigt das Zeichen auf dem Bildschirm an. Derzeit ist noch nichts zum Lesen des Programms verfügbar, an das das Endgerät angeschlossen ist. Lassen Sie uns die backspace auf der Tastatur drücken. Symbol ^? erneut von der Zeilendisziplin abgefangen, und letztere erkennt, dass der Benutzer das zuletzt eingegebene Zeichen löschen möchte, entfernt dieses Zeichen aus seinem internen Puffer und löscht dieses Zeichen auch vom Bildschirm. Wenn wir nun die Eingabetaste drücken, sendet die TTY-Leitungsdisziplin schließlich alles, was zuvor in den internen Puffer der Disziplin geschrieben wurde, einschließlich LF, an den Lesepuffer des Endgeräts. Gleichzeitig werden die Zeichen CR und LF auf dem Bildschirm angezeigt, um den Cursor auf eine neue Zeile zu bewegen - dies ist die Formatierung der Ausgabe.


So funktioniert der kanonische Modus: Er überträgt alle eingegebenen Zeichen erst nach Drücken der Enter auf das Gerät, verarbeitet die Steuerzeichen und formatiert die Ausgabe.


TTY-Zeilenbearbeitung


TTY Line Editing ist die Komponente, die fĂĽr die Verarbeitung von Eingaben in der Zeilendisziplin verantwortlich ist. Es sollte gesagt werden, dass die Zeilenbearbeitung ein allgemeines Konzept ist und sich auf die Eingabeverarbeitung bezieht. Zum Beispiel haben Bash und Vim ihre eigene Zeilenbearbeitung.


Mit dem Programm stty können wir die Disziplineinstellungen der Leitung des aktuellen TTY-Geräts steuern . Lass uns ein bisschen experimentieren.


Ă–ffnen Sie Bash oder eine andere Shell und geben Sie Folgendes ein:


 stty icanon -echo 

Versuchen Sie nun, etwas einzugeben, und Sie werden Ihre Eingabe nicht sehen (keine Sorge, Sie können die Eingabe trotzdem an das Programm übergeben). Sie haben gerade das Echo deaktiviert, dh die Anzeige der eingegebenen Zeichen auf dem Bildschirm. Geben Sie nun Folgendes ein:


 stty raw echo 

Versuchen Sie etwas zu tippen. Sie sehen, wie die Schlussfolgerung gebrochen ist. Aber für mehr Effekt gehen wir zu Dash - geben Sie /bin/sh . Versuchen Sie nun, Sonderzeichen einzugeben ( Ctrl + ein beliebiges Zeichen auf der Tastatur) oder drücken Enter einfach die Enter . Sie sind ratlos - was sind diese seltsamen Zeichen auf dem Bildschirm? Tatsache ist, dass wir, nachdem wir die einfachste Shell eingegeben haben, zusätzlich zur Linienbearbeitung der Disziplin selbst auch den Linienbearbeitungs-Bash deaktiviert haben und nun mit Macht und Hauptwirkung den Effekt der Einbeziehung des rohen Disziplinierungsmodus der Linie beobachten können. Dieser Modus verarbeitet Eingaben überhaupt nicht und formatiert keine Ausgaben. Warum wird der Raw-Modus benötigt? Zum Beispiel für Vim : Es öffnet sich in das gesamte Terminalfenster und verarbeitet die Eingabe selbst, zumindest damit sich spezielle Symbole der Liniendisziplin nicht mit speziellen Symbolen von Vim selbst überschneiden.


Schauen wir uns zum noch besseren Verständnis das Anpassen von Steuerzeichen an. Der stty <control-character> <string> hilft uns dabei.
Geben Sie in Bash ein:


 stty erase 0 

Jetzt wird das erase dem Zeichen 0 zugewiesen. Die backspace normalerweise wichtig ^? , aber jetzt wird dieses Sonderzeichen buchstäblich an den PTS-Lesepuffer gesendet - probieren Sie es selbst aus. Jetzt können Sie Zeichen mit der Taste 0 auf der Tastatur löschen, da Sie selbst die Zeilendisziplin gebeten haben, das eingegebene Zeichen als Löschsteuerzeichen zu erkennen. Sie können die Einstellung mit dem Befehl stty erase ^\? oder einfach das Terminal schließen, weil wir nur das aktuelle tty-Gerät betroffen haben.


Weitere Informationen finden Sie in man stty .




Terminal Emulator und Pseudoterminal


Jedes Mal, wenn wir ein neues Terminal im X Window System öffnen, erzeugt der GNOME Terminal Server einen neuen Prozess und startet das Standardprogramm darin. Normalerweise ist dies eine Art Shell (zum Beispiel Bash).


Die Kommunikation mit dem laufenden Programm erfolgt über das sogenannte Pseudoterminal (Pseudo-Terminal, PTY). Das Pseudo-Terminal selbst existiert im Kernel, empfängt jedoch Eingaben aus dem Benutzerbereich - vom Terminal-Emulator.


Das Pseudo-Terminal besteht aus den folgenden zwei virtuellen TTY-Geräten :
1) PTY-Master (PTM) - der fĂĽhrende Teil des Pseudo-Terminals. Wird vom GNOME-Terminalserver verwendet, um Tastatureingaben an ein im Terminal ausgefĂĽhrtes Programm zu ĂĽbertragen sowie Programmausgaben und Anzeigeausgaben zu lesen. Der GNOME-Terminalserver kommuniziert wiederum ĂĽber das X-Protokoll mit dem X Window System.
2) PTY-Slave (PTS) - Slave-Teil des Pseudo-Terminals. Wird von einem Programm verwendet, das im Terminal ausgeführt wird, um Tastatureingaben zu lesen und Ausgaben auf dem Bildschirm anzuzeigen. Zumindest das Programm selbst glaubt das (ich werde etwas weiter erklären, was dies bedeutet).


Alle im PTS-Gerät aufgezeichneten Daten sind der Eingang des PTM-Geräts, dh sie werden auf dem PTM-Gerät lesbar. Und umgekehrt: Alle im PTM-Gerät aufgezeichneten Daten sind der Eingang des PTS-Geräts. Auf diese Weise kommunizieren der GNOME-Terminalserver und das im Terminal ausgeführte Programm. Jedes PTM-Gerät ist einem eigenen PTS-Gerät zugeordnet.


Der Start eines neuen Terminals sieht ungefähr so ​​aus:
1) GNOME Terminal Server erstellt Master- und Slave-Geräte durch Aufrufen der Funktion open () auf einem speziellen Gerät / dev / ptmx . Der Aufruf open () gibt den Dateideskriptor des erstellten PTM-Geräts zurück - master_fd .
2) GNOME Terminal Server erstellt einen neuen Prozess durch Aufrufen von fork() . Dieser Prozess wird das neue Terminal sein.
3) Im PTS-Terminal wird das Gerät in den Dateideskriptoren 0, 1, 2 (stdin, stdout bzw. stderr) geöffnet. Jetzt fließen Standard-Terminal-E / A zu diesem Gerät.
4) Das gewünschte Programm wird im Terminal durch Aufrufen der Funktion exec() gestartet. Einige Shell starten normalerweise (zum Beispiel Bash). Jedes Programm, das anschließend von Bash aus gestartet wird, hat dieselben Dateideskriptoren wie Bash selbst, dh die Programmabläufe werden an das PTS-Gerät geleitet.


Mit dem ls -la /proc/self/fd können Sie selbst sehen, wohin die Standard-Terminal-Ausgangsflüsse geleitet werden:


Das PTS-Gerät befindet sich im Pfad / dev / pts / N , und der Pfad zum PTM-Gerät interessiert uns überhaupt nicht. Tatsache ist, dass der GNOME-Terminalserver bereits über einen Dateideskriptor für das geöffnete PTM-Gerät verfügt und keinen Pfad dazu benötigt. Im untergeordneten Prozess müssen wir das PTS-Gerät jedoch in Standardausgabestreams open() indem wir die Funktion open() aufrufen, für die der Pfad zur Datei erforderlich ist.


Denken Sie daran, ich sagte, dass ein Programm, das ein PTS-Gerät verwendet, nur denkt, dass es direkt mit dem Terminal kommuniziert? Tatsache ist, dass das PTS auch ein Endgerät (TTY-Gerät) ist, aber der Unterschied zwischen dem PTS-Gerät und dem tatsächlichen TTY-Gerät besteht darin, dass das PTS-Gerät Eingaben nicht von der Tastatur, sondern vom Master-Gerät empfängt und die Ausgabe nicht an das Display, sondern an geht Master-Gerät. Deshalb heißt das Pseudo-Terminal so - das Pseudo-Terminal ahmt nur (wieder ??) das Terminal nach. Der Unterschied zwischen dem Terminalemulator und dem Pseudo-Terminal besteht darin, dass der Terminal-Emulator nur ein grafisches Programm ist, mit dem Sie das Terminal direkt in der Fensterschnittstelle ausführen können. Diese Funktion wird jedoch mithilfe des Pseudo-Terminals implementiert.


Die Tatsache, dass das PTS-Gerät ein TTY-Gerät ist , ist sehr wichtig. Deshalb:


  1. Das Programm, an das das Endgerät angeschlossen ist, verfügt über alle Funktionen eines herkömmlichen Terminals. Zum Beispiel: Echo deaktivieren, kanonische Ansicht deaktivieren / aktivieren.
  2. Das Programm kann interaktiv arbeiten und den Benutzer um Eingabe bitten, da es weiß, dass ein Endgerät daran angeschlossen ist (es wird gesagt, dass das Programm über ein Steuerterminal verfügt). Fragen Sie beispielsweise nach einem Benutzernamen und einem Passwort.
  3. Es gibt auch eine TTY-Zeilendisziplin, sodass wir Steuerzeichen verarbeiten können, bevor sie das Programm erreichen, und die Ausgabe des Programms formatieren können.

Das PTM-Gerät ist ebenfalls ein TTY-Gerät, spielt jedoch keine Rolle, da es nicht als Steuerterminal verwendet wird. Darüber hinaus ist die Leitungsdisziplin des PTM-Geräts auf den Rohmodus eingestellt, weshalb beim Übertragen von Daten vom PTS zum PTM-Gerät keine Verarbeitung durchgeführt wird. Aufrufe zum read() und write() aus dem Benutzerbereich werden jedoch immer noch zuerst von der Leitungsdisziplin auf beiden Geräten bedient. Dieser Moment wird eine noch größere Rolle spielen, wie wir später sehen werden.


Der Kommunikationsprozess zwischen dem GNOME-Terminalserver und dem im Terminal ausgefĂĽhrten Programm ist wie folgt:



Es lohnt sich, die Rolle der Liniendisziplin bei der Kommunikation zwischen beiden Teilen eines Pseudo-Terminals genauer zu untersuchen. Hier ist die Leitungsdisziplin für die Verarbeitung von Daten verantwortlich, die vom PTM zum PTS-Gerät übertragen werden , sowie für die Lieferung von Daten von einem Teil des Pseudo-Terminals zu einem anderen. Wenn wir uns im PTS-Gerätetreiber befinden, wenden wir die Leitungsdisziplin des PTM-Geräts an und umgekehrt.




Virtuelle Geräte


Sie hätten wahrscheinlich gedacht, Sie könnten die Datei entlang des Pfads / dev / pts / N öffnen und Daten daraus schreiben oder lesen, wie aus einer normalen Textdatei? Ja, alle Geräte auf Unix-ähnlichen Systemen sind Dateien, dank des Grundprinzips von Unix, das besagt, dass alles eine Datei ist. Keine speziellen Gerätedateien (Englisch - Gerätedatei) sind jedoch Textdateien. Solche Geräte werden als virtuelle Geräte bezeichnet, dh sie befinden sich ausschließlich im Speicher und nicht auf der Festplatte.


Versuchen Sie nicht, diese Dateien als normale Textdateien zu öffnen. Sie können diese Geräte jedoch über write() und read() -Operationen verwenden, deren Aufruf vom Gerätetreiber bedient wird. Lass es uns versuchen.


Öffnen Sie zwei Terminalfenster und geben Sie in jeden Befehl tty ein. Dieser Befehl zeigt an, welches TTY-Gerät das aktuell aktive Terminal bedient. Geben Sie nun das echo "Hello, World!" > /dev/pts/N echo "Hello, World!" > /dev/pts/N im ersten Terminalfenster, wobei N der PTS-Index des zweiten Fenstergeräts ist, wechseln Sie zum zweiten Fenster und Sie sehen Ihre Eingabe aus dem ersten Fenster. Jetzt haben Sie die Daten auf das PTS-Gerät des zweiten Fensters geschrieben, als ob sie von einem Programm ausgeführt würden, das in diesem Terminal ausgeführt wird .





Pseudo-Endgerät


Wir nähern uns immer mehr dem letzten Teil des Artikels, aber vorher werfen wir einen Blick unter die Haube von Linux - betrachten Sie das Gerät des Pseudo-Terminals auf Kernel-Ebene. Es wird viel Code geben, aber ich werde versuchen, jeden gegebenen Codeblock so detailliert wie möglich zu erklären, unwichtige Details zu reduzieren und nacheinander vorzugehen.


Bevor Sie beginnen, stellen wir Ihnen den sogenannten "Komponentenkorb" vor. Während wir uns entlang des Kerns bewegen, werden wir ihm immer mehr Komponenten hinzufügen und eine Verbindung zwischen ihnen finden. Ich hoffe, dies hilft Ihnen dabei, das Pseudo-Endgerät besser zu verstehen. Fangen wir an.


Beim Start von Linux werden die erforderlichen Gerätetreiber geladen. Unser Pseudo-Terminal hat auch einen solchen Treiber. Die Registrierung beginnt mit einem Aufruf dieser Funktion:


 static int __init pty_init(void) { legacy_pty_init(); unix98_pty_init(); // <- ,    return 0; } device_initcall(pty_init); // ,       

FĂĽr alle modernen Systeme wird die Funktion unix98_pty_init() aufgerufen:


 static void __init unix98_pty_init(void) { ptm_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX, TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV | TTY_DRIVER_DEVPTS_MEM | TTY_DRIVER_DYNAMIC_ALLOC); if (IS_ERR(ptm_driver)) panic("Couldn't allocate Unix98 ptm driver"); pts_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX, TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV | TTY_DRIVER_DEVPTS_MEM | TTY_DRIVER_DYNAMIC_ALLOC); if (IS_ERR(pts_driver)) panic("Couldn't allocate Unix98 pts driver"); ptm_driver->driver_name = "pty_master"; ptm_driver->name = "ptm"; ptm_driver->major = UNIX98_PTY_MASTER_MAJOR; ptm_driver->minor_start = 0; ptm_driver->type = TTY_DRIVER_TYPE_PTY; ptm_driver->subtype = PTY_TYPE_MASTER; ptm_driver->init_termios = tty_std_termios; ptm_driver->init_termios.c_iflag = 0; ptm_driver->init_termios.c_oflag = 0; ptm_driver->init_termios.c_cflag = B38400 | CS8 | CREAD; ptm_driver->init_termios.c_lflag = 0; ptm_driver->init_termios.c_ispeed = 38400; ptm_driver->init_termios.c_ospeed = 38400; ptm_driver->other = pts_driver; tty_set_operations(ptm_driver, &ptm_unix98_ops); pts_driver->driver_name = "pty_slave"; pts_driver->name = "pts"; pts_driver->major = UNIX98_PTY_SLAVE_MAJOR; pts_driver->minor_start = 0; pts_driver->type = TTY_DRIVER_TYPE_PTY; pts_driver->subtype = PTY_TYPE_SLAVE; pts_driver->init_termios = tty_std_termios; pts_driver->init_termios.c_cflag = B38400 | CS8 | CREAD; pts_driver->init_termios.c_ispeed = 38400; pts_driver->init_termios.c_ospeed = 38400; pts_driver->other = ptm_driver; tty_set_operations(pts_driver, &pty_unix98_ops); if (tty_register_driver(ptm_driver)) panic("Couldn't register Unix98 ptm driver"); if (tty_register_driver(pts_driver)) panic("Couldn't register Unix98 pts driver"); /* Now create the /dev/ptmx special device */ tty_default_fops(&ptmx_fops); ptmx_fops.open = ptmx_open; cdev_init(&ptmx_cdev, &ptmx_fops); if (cdev_add(&ptmx_cdev, MKDEV(TTYAUX_MAJOR, 2), 1) || register_chrdev_region(MKDEV(TTYAUX_MAJOR, 2), 1, "/dev/ptmx") < 0) panic("Couldn't register /dev/ptmx driver"); device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 2), NULL, "ptmx"); 

Hier interessieren uns 3 Dinge:


  1. tty_set_operatons für den pty-Mastertreiber und die pty-Slave-Geräte auf.
  2. Die Funktion ptmx_open , die beim Öffnen des Spezialgeräts / dev / ptmx für die Erstellung beider Teile des Pseudo-Terminals verantwortlich ist . Wichtig: / dev / ptmx ist kein PTM-Gerät, sondern nur eine Schnittstelle zum Erstellen eines neuen Pseudo-Terminals.
  3. Registrieren Sie PTM- und PTS-Gerätetreiber.

Lass uns in der richtigen Reihenfolge gehen:


1. tty_set_operations


Die Funktion tty_set_operations () erstellt lediglich eine Funktionstabelle fĂĽr den aktuellen Treiber:


 void tty_set_operations(struct tty_driver *driver, const struct tty_operations *op) { driver->ops = op; }; 

Die Struktur tty_operations ist eine Funktionstabelle, mit der auf die TTY-Treiberfunktionen des Geräts zugegriffen wird.


Ich werde das Wichtigste in den Strukturen pty_unix98_ops und ptm_unix98_ops , die die Funktionstabelle fĂĽr die entsprechenden Teile des Pseudo-Terminals sind:


 static const struct tty_operations ptm_unix98_ops = { .install = pty_unix98_install, .remove = pty_unix98_remove, .open = pty_open, .close = pty_close, .write = pty_write, // ... }; static const struct tty_operations pty_unix98_ops = { .install = pty_unix98_install, .remove = pty_unix98_remove, .open = pty_open, .close = pty_close, .write = pty_write, // ... }; 

Hier können Sie die Funktion pty_write beobachten, pty_write bereits aus dem Artikel über Sishny printf bekannt ist - wir werden etwas später darauf zurückkommen.


FĂĽgen wir diese Struktur unserem Komponentenkorb hinzu:


Wie Sie sehen können, unterscheiden sich die Hauptmethoden beider Treiber überhaupt nicht. pty_read() übrigens, dass es keine Funktion für die Operation read () gibt - es gibt nichts pty_read() wie pty_read() . Tatsache ist, dass das Lesen ausschließlich durch Zeilendisziplin bedient wird. So lernen wir das zweite wichtige Merkmal der Leitungsdisziplin kennen - das Lesen von Daten von einem TTY-Gerät.




2. ptmx_open


Fahren wir nun mit ptmx_open () fort :


 static int ptmx_open(struct inode *inode, struct file *filp) { struct tty_struct *tty; //    -   ! fsi = devpts_acquire(filp); //     devpts index = devpts_new_index(fsi); //       /dev/pts // ... tty = tty_init_dev(ptm_driver, index); // ... devpts_pty_new(fsi, index, tty->link); //     /dev/pts retval = ptm_driver->ops->open(tty, filp); //  PTM ,   } 

Wir interessieren uns für die Funktion tty_init_dev() , wobei das erste Argument der PTM-Gerätetreiber und das zweite der Geräteindex ist. Hier verlassen wir die Verantwortungszone des PTY-Treibers und gehen zu der Datei, die nur für die allgemeinen TTY-Geräte verantwortlich ist und nichts über unser Pseudo-Terminal weiß.


 struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx) { struct tty_struct *tty; tty = alloc_tty_struct(driver, idx); retval = tty_driver_install_tty(driver, tty); /* * Structures all installed ... call the ldisc open routines. */ retval = tty_ldisc_setup(tty, tty->link); //  ,       return tty; } 

Zuerst alloc_tty_struct() wir alloc_tty_struct() Funktion alloc_tty_struct() :


 struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx) { struct tty_struct *tty; tty = kzalloc(sizeof(*tty), GFP_KERNEL); //  tty_struct tty_ldisc_init(tty) //      tty_struct tty->driver = driver; //       tty_struct tty->ops = driver->ops; //        tty_struct.     tty->index = idx; //   tty  return tty; } 

Das einzige, was uns hier interessiert, ist die Funktion tty_ldisc_init() :


 int tty_ldisc_init(struct tty_struct *tty) { struct tty_ldisc *ld = tty_ldisc_get(tty, N_TTY); if (IS_ERR(ld)) return PTR_ERR(ld); tty->ldisc = ld; //        tty_struct return 0; } 

Was tty_ldisc_get() :


 static struct tty_ldisc *tty_ldisc_get(struct tty_struct *tty, int disc) { struct tty_ldisc *ld; //    struct tty_ldisc_ops *ldops; //     ldops = get_ldops(disc); //      .   ,       .   - N_TTY ld = kmalloc(sizeof(struct tty_ldisc), GFP_KERNEL | __GFP_NOFAIL); ld->ops = ldops; //       ld->tty = tty; //    tty_struct   .          return ld; } 

Daher haben wir den Aufruf der Funktion alloc_tty_struct() , mit der die Struktur tty_struct zusammen mit der Liniendisziplin - der Struktur tty_ldisc - erstellt wird. Beide Strukturen sind miteinander verbunden. Schauen wir uns diese Strukturen genauer an.


  • tty_struct ist eine Struktur fĂĽr den Zugriff auf den TTY-Gerätetreiber und einige andere Felder. Es sieht so aus:

 struct tty_struct { struct tty_driver *driver; //  TTY  const struct tty_operations *ops; //  .    ,   driver->ops,       int index; //   struct tty_ldisc *ldisc; //     struct tty_struct *link; //     PTY // ... } 

  • tty_ldisc ist die Struktur fĂĽr die Disziplin der TTY-Linie des Geräts. Es besteht nur aus zwei Feldern und sieht wie folgt aus:

 struct tty_ldisc { struct tty_ldisc_ops *ops; //    struct tty_struct *tty; //   tty_struct  .       }; 

Es scheint nichts kompliziertes zu sein? FĂĽgen wir alle bis zu diesem Punkt berĂĽcksichtigten Strukturen unserem Warenkorb hinzu und verknĂĽpfen sie auf dieselbe Weise, wie sie im Code verbunden sind:
Erstellen Sie tty_struct


Wir haben tty_struct jedoch nur für das PTM-Gerät erstellt. Was ist mit dem PTS-Gerät? Dazu kehren wir zur Funktion tty_init_dev() und erinnern uns, dass wir dann die Funktion tty_driver_install_tty() aufrufen tty_driver_install_tty() :


 /** * This method is responsible * for ensuring any need additional structures are allocated and configured. */ static int tty_driver_install_tty(struct tty_driver *driver, struct tty_struct *tty) { return driver->ops->install ? driver->ops->install(driver, tty) : tty_standard_install(driver, tty); } 

Der Kommentar sagt uns, dass diese Methode für die Erstellung verschiedener zusätzlicher Strukturen verantwortlich ist. PTS-Gerät und wird unsere zusätzliche Struktur sein. Ich gebe zu, es war äußerst überraschend für mich, denn es ist verdammt noch mal das ganze Gerät und nicht nur eine Art zusätzliche Struktur! Aber wir alle verstehen, dass alle Geräte nur eine Art Struktur sind, also fahren Sie fort. Ok, was ist Treiber-> Ops-> hier installieren ? Schauen Sie sich dazu noch einmal die Funktionstabelle für den PTM-Treiber an:


 static const struct tty_operations ptm_unix98_ops = { .install = pty_unix98_install, // ... 

Und wir verstehen, dass wir an der Funktion pty_unix98_install() interessiert sind:


 static int pty_unix98_install(struct tty_driver *driver, struct tty_struct *tty) { return pty_common_install(driver, tty, false); } 

Welches ruft die Funktion pty_common_install() :


 static int pty_common_install(struct tty_driver *driver, struct tty_struct *tty, bool legacy) { struct tty_struct *o_tty; // tty_struct    PTY -    PTS  //    ,       install.   ,   PTM     tty_struct,        if (driver->subtype != PTY_TYPE_MASTER) return -EIO; o_tty = alloc_tty_struct(driver->other, idx); tty->link = o_tty; o_tty->link = tty; } 

, PTS tty_struct , PTS . . tty_struct PTS .





, TTY ( - ?).
— , PTM, PTS :


 static const struct file_operations tty_fops = { .llseek = no_llseek, .read = tty_read, .write = tty_write, .poll = tty_poll, .unlocked_ioctl = tty_ioctl, .compat_ioctl = tty_compat_ioctl, .open = tty_open, .release = tty_release, .fasync = tty_fasync, .show_fdinfo = tty_show_fdinfo, }; 

, TTY .




Fertig. , /dev/ptmx . , PTS , , PTM , :





Hallo Welt!


. "Hello, World!", .


 #include <stdio.h> void main() { printf("Hello, World!\n"); } 

, "Hello, World!" . , , , . , . stdout /dev/null — . , Linux.


Unix write() , read() , close() , write() /dev/pts/0 __vfs_write() :


 ssize_t __vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { ssize_t ret; //... ret = file->f_op->write(file, buf, count, pos); //... return ret; } 

write() . , :


 static const struct file_operations tty_fops = { // ... .write = tty_write, // ... 

tty_write() :


 static ssize_t tty_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct tty_struct *tty = file_tty(file); struct tty_ldisc *ld; ssize_t ret; ld = tty_ldisc_ref_wait(tty); ret = do_tty_write(ld->ops->write, tty, file, buf, count); tty_ldisc_deref(ld); return ret; } 

tty_struct TTY , write() . :


 static struct tty_ldisc_ops n_tty_ops = { .write = n_tty_write, // ... }; 

n_tty_write() :


 /** * n_tty_write - write function for tty * @tty: tty device * @file: file object * @buf: userspace buffer pointer * @nr: size of I/O */ static ssize_t n_tty_write(struct tty_struct *tty, struct file *file, const unsigned char *buf, size_t nr) { const unsigned char *b = buf; // b - ,       "Hello, World!".          int c; //    //     PTS ,  write()    0,  ,     while (nr > 0) { c = tty->ops->write(tty, b, nr); //  write()       TTY  if (!c) break; b += c; //     nr -= c; //      :  -  -  -  } } 

, "Hello, World!" write() PTS . :


 static const struct tty_operations pty_unix98_ops = { .write = pty_write, // ... } 

pty_write() :


 static int pty_write(struct tty_struct *tty, const unsigned char *buf, int c) { struct tty_struct *to = tty->link; //      PTY.    -  PTM  if (c > 0) { //    PTM  c = tty_insert_flip_string(to->port, buf, c); //     ,       if (c) { tty_flip_buffer_push(to->port); tty_wakeup(tty); } } return c; } 

:


  __vfs_write() -> // 1- :   tty_write() -> do_tty_write() -> n_tty_write() -> // 2- :   pty_write() // 3- :  

. , PTM . , .


, flip buffer . Flip buffer — , . tty driver , . , . , , . , , . - flip buffer — (, - , flip).


, . tty_insert_flip_string() tty_insert_flip_string_fixed_flag() , PTM :


 int tty_insert_flip_string_fixed_flag(struct tty_port *port, const unsigned char *chars, char flag, size_t size) { int copied = 0; do { int goal = min_t(size_t, size - copied, TTY_BUFFER_PAGE); //      int space = __tty_buffer_request_room(port, goal, flags); //     struct tty_buffer *tb = port->buf.tail; //       if (unlikely(space == 0)) break; memcpy(char_buf_ptr(tb, tb->used), chars, space); //      tb->used += space; copied += space; chars += space; /* There is a small chance that we need to split the data over several buffers. If this is the case we must loop */ } while (unlikely(size > copied)); return copied; } 

, flip buffer , , . , — PTM , .


, "Hello, World!" PTM . GNOME Terminal Server poll() ( I/O) master . , ? Egal wie. , , — .


tty_flip_buffer_push() ( pty_write):


 /** * tty_flip_buffer_push - terminal * @port: tty port to push * * Queue a push of the terminal flip buffers to the line discipline. * Can be called from IRQ/atomic context. * * In the event of the queue being busy for flipping the work will be * held off and retried later. */ void tty_flip_buffer_push(struct tty_port *port) { tty_schedule_flip(port); } 

tty_schedule_flip() , , :


 /** * tty_schedule_flip - push characters to ldisc * @port: tty port to push from * * Takes any pending buffers and transfers their ownership to the * ldisc side of the queue. It then schedules those characters for * processing by the line discipline. */ void tty_schedule_flip(struct tty_port *port) { struct tty_bufhead *buf = &port->buf; /* paired w/ acquire in flush_to_ldisc(); ensures * flush_to_ldisc() sees buffer data. */ smp_store_release(&buf->tail->commit, buf->tail->used); queue_work(system_unbound_wq, &buf->work); } 

, work (, - ) , — , flush_to_ldisc() :


 static void flush_to_ldisc(struct work_struct *work) { struct tty_port *port = container_of(work, struct tty_port, buf.work); //   tty_port PTM . tty_port -       TTY  struct tty_bufhead *buf = &port->buf; struct tty_buffer *head = buf->head; // ... receive_buf(port, head); // ... } 

receive_buf() __receive_buf() , :


 static void __receive_buf(struct tty_struct *tty, const unsigned char *cp, char *fp, int count) { struct n_tty_data *ldata = tty->disc_data; bool preops = I_ISTRIP(tty) || (I_IUCLC(tty) && L_IEXTEN(tty)); if (ldata->real_raw) n_tty_receive_buf_real_raw(tty, cp, fp, count); else if (ldata->raw || (L_EXTPROC(tty) && !preops)) n_tty_receive_buf_raw(tty, cp, fp, count); else if (tty->closing && !L_EXTPROC(tty)) n_tty_receive_buf_closing(tty, cp, fp, count); else { if (ldata->lnext) { char flag = TTY_NORMAL; if (fp) flag = *fp++; n_tty_receive_char_lnext(tty, *cp++, flag); count--; } if (!preops && !I_PARMRK(tty)) n_tty_receive_buf_fast(tty, cp, fp, count); else n_tty_receive_buf_standard(tty, cp, fp, count); } if (read_cnt(ldata)) { kill_fasync(&tty->fasync, SIGIO, POLL_IN); wake_up_interruptible_poll(&tty->read_wait, EPOLLIN); } } 

, n_tty_receive_buf ( , _raw) read_buf , TTY . PTM raw , read_buf. , PTM PTS , .


, :


  ... pty_write() -> // 3- :  PTS  tty_insert_flip_string + tty_flip_buffer_push() -> tty_schedule_flip() -> --- //    PTM  flush_to_ldisc() -> // 2- :   PTM  receive_buf() -> n_tty_receive_buf -> n_tty_receive_buf_common -> __receive_buf() 

, PTM — PTS .


: PTM . GNOME Terminal Server "Hello, World!", read() PTM . read() write() — n_tty_read() . , , — read_buf — . GNOME Terminal Server X Server, .


, "Hello, World!" :


  -> PTY slave -> PTY master -> GNOME-TERMINAl-SERVER -> X Server -> ->  



Fazit


. :


  1. TTY
  2. ,

, ! - — , !


Quellen


Source: https://habr.com/ru/post/de460257/


All Articles