Bonjour tout le monde! Immersion profonde dans les terminaux


J'ai été inspiré pour écrire cet article par un article sur l'analyse de Sishny printf . Cependant, il y a eu un moment manquant sur la façon dont les données vont après leur entrée dans le terminal. Dans cet article, je souhaite corriger ce défaut et analyser le chemin des données dans le terminal. Nous découvrirons également en quoi Terminal diffère de Shell, ce qu'est le pseudoterminal, comment fonctionnent les émulateurs de terminaux, et bien plus encore.


Les bases


Commençons par comprendre ce qu'est Terminal, Shell, Console, en quoi l'émulateur de terminal diffère du terminal ordinaire et pourquoi il est ainsi nommé. Beaucoup d'informations ont déjà été écrites à ce sujet, vous n'entendrez donc rien de nouveau ici. Presque toutes les informations ici ont été tirées d'Internet, je fournirai des liens à la fin de l'article. Qui sait déjà ce que toutes ces choses signifient, peut sauter cette section en toute sécurité.




Terminal


Un terminal est une combinaison d'un écran et d'un clavier, c'est-à-dire un périphérique physique. Avant que les terminaux ne deviennent cette combinaison particulière, ils étaient une sorte d'appareil appelé téléimprimeur (téléscripteur, téléscripteur ou ATS pour faire court), c'est-à-dire une combinaison d'une imprimante et d'un clavier. En règle générale, plusieurs terminaux sont connectés au même ordinateur. Ainsi, il était possible de travailler pour plusieurs utilisateurs sur le même ordinateur, et chacun avait sa propre session, indépendante des autres. Le terminal a été nommé ainsi car il était situé à l'extrémité du câble du terminal.


Voici le télétype :


Teletype

Et voici Terminal :


Terminal



La console


Console (console) - un terminal qui est connecté directement à l'ordinateur. Le fait est que la plupart des terminaux étaient connectés implicitement, mais au moins un était connecté directement à l'ordinateur. La console était autorisée à utiliser un cercle de personnes strictement défini, car elle vous permettait de configurer l'ordinateur.




Coquille


Si les deux précédents sont des périphériques physiques, cette définition se réfère exclusivement aux logiciels.


Shell est un interpréteur de ligne de commande. Le but principal est d'exécuter d'autres programmes. Il existe un grand nombre de coquilles différentes. Le plus commun est Bash (de l'anglais Bourne Again SHell, qui, comme le suggère Wikipedia , est un jeu de mots pour Shell «Born again», c'est-à-dire un shell «revived»). Autres exemples: Dash (un Shell léger, disponible si vous exécutez le binaire dans / bin / sh), Zsh.




Bien sûr, les terminaux et les consoles ne pouvaient que trouver leur reflet dans les temps modernes. Par conséquent, nous examinerons plus loin des choses telles que l' émulateur de terminal et la console virtuelle .


Émulateur de terminal


Terminal Emulator - un émulateur du bon vieux terminal. Un émulateur de terminal est requis pour les programmes qui ne peuvent pas interagir directement avec le système X Window - Bash, Vim et autres.


Établissons d'abord les responsabilités du terminal:


  1. Transférer les entrées utilisateur sur un ordinateur
  2. Livraison de la sortie de l'ordinateur à l'écran

Notre émulateur de terminal fait donc exactement la même chose: il fournit une entrée utilisateur au programme en cours d'exécution et affiche également la sortie du programme sur l'écran. Dans tous les cas, la signification demeure - entre l'utilisateur et le programme en cours d'exécution, il existe une sorte de couche responsable des entrées / sorties. Exemples d'émulateur de terminal: gnome-terminal, xterm, konsole.


Veuillez ne pas confondre Shell et l'émulateur de terminal!
Terminal Emulator est une application GUI, c'est-à-dire une fenêtre du système X Window. Shell est un interpréteur de ligne de commande, c'est-à-dire juste un exécuteur de commandes, il n'a pas de shell graphique. En parlant assez correctement, vous ne lancez pas Bash , vous exécutez Terminal Emulator, qui lance Bash en lui-même . Terminal Emulator et Bash sont absolument 2 programmes différents. Le premier est seul responsable des entrées / sorties, le second - du traitement des commandes.


Plus loin dans l'article, toutes les références au terminal feront référence à l'émulateur de terminal.




Console virtuelle (terminal virtuel)


Appuyez sur Ctrl + Alt + FN, où N a généralement des valeurs de 1 à 6. Ce que vous venez de voir s'appelle Virtual Console (console virtuelle) ou Virtual Terminal (terminal virtuel). Rappelez-vous ce que j'ai dit plus tôt à propos des terminaux? De nombreux terminaux étaient connectés à un ordinateur et chaque terminal était une session distincte, indépendante des autres. La console virtuelle reprend cette idée: il peut y avoir plusieurs sessions indépendantes à l'intérieur de votre ordinateur (cependant, les ressources informatiques sont toujours évidemment partagées).


Vous pouvez nommer cette entité à la fois console virtuelle et terminal virtuel, car par définition, une console est un terminal connecté directement à un ordinateur, mais tous les terminaux virtuels sont, en un sens, connectés directement à un ordinateur.




Appareils ATS


Chaque terminal se voit attribuer son propre appareil TTY (appareil terminal), qui fournit la console. Il est peu probable que vous trouviez des télétypes, mais la réduction des ATS a survécu à ce jour.


Un appareil TTY se compose de deux composants fondamentaux:


  1. Pilote de périphérique Il est chargé de fournir une entrée clavier au programme et d'afficher la sortie du programme sur l'écran.
  2. TTY Line Discipline (russe - discipline de ligne). La discipline de ligne est l'interface d'accès au pilote, qui, cependant, apporte beaucoup de logique au dispositif TTY. Nous pouvons dire que la discipline de ligne fait appel au conducteur. Quel est le domaine de responsabilité de cette composante, nous le découvrirons dans l'article.

Construire un appareil TTY:



Il existe 3 types d'appareils TTY:


  1. Périphérique de console - fournit le fonctionnement de la console virtuelle. L'entrée et la sortie de ce périphérique sont entièrement contrôlées par le noyau.
  2. Dispositif PTY (pseudo-terminal) - assure le fonctionnement du terminal dans l'interface de fenêtre. L'entrée et la sortie de cet appareil sont contrôlées par un émulateur de terminal qui fonctionne dans l'espace utilisateur.
  3. Périphérique série - communique directement avec le matériel. Il n'est généralement pas utilisé directement, mais existe en tant que niveau le plus bas dans l'organisation de l'architecture d'un terminal.

Dans cet article, nous parlerons spécifiquement du deuxième type d'appareils TTY - les pseudo-terminaux.




Discipline de la ligne ATS


Nous commençons à examiner la discipline de la gamme d'appareils TTY.


La première caractéristique importante d'une discipline de ligne est qu'elle est responsable du traitement des E / S. Cela inclut, par exemple, le traitement des caractères de contrôle (voir Caractères de contrôle ) et le formatage de la sortie. Par exemple, vous saisissez n'importe quel texte, mais soudain vous vous rendez compte que vous vous êtes trompé en écrivant quelque chose et que vous voulez l'effacer - c'est là que la discipline de ligne entre en jeu.


Nous analyserons en détail ce qui se passe exactement lorsque nous travaillons dans Bash en cours d'exécution dans le terminal. Par défaut, un appareil TTY fonctionne en mode canonique avec l' écho activé . Un écho est un affichage des caractères que vous avez entrés à l'écran.


Lorsque nous saisissons, par exemple, le caractère a , ce caractère est envoyé au dispositif ATS, mais est intercepté par la discipline de la ligne ATS du dispositif. Elle lit un caractère dans sa mémoire tampon interne, voit que le mode echo est activé et affiche le caractère à l'écran. À l'heure actuelle, rien n'est encore disponible pour la lecture dans le programme auquel le terminal est connecté. Appuyez sur la touche de backspace sur le clavier. Symbole ^? à nouveau intercepté par la discipline de la ligne, et cette dernière, réalisant que l'utilisateur veut effacer le dernier caractère saisi, supprime ce caractère de sa mémoire tampon interne et efface également ce caractère de l'écran. Maintenant, si nous appuyons sur Entrée, la discipline de ligne TTY envoie finalement au tampon de lecture du périphérique terminal tout ce qui a été écrit précédemment dans le tampon interne de la discipline, y compris LF. En même temps, les caractères CR et LF sont affichés à l'écran afin de déplacer le curseur sur une nouvelle ligne - c'est le formatage de la sortie.


Voici comment fonctionne le mode canonique - il transfère tous les caractères saisis vers l'appareil uniquement après avoir appuyé sur Enter , traite les caractères de contrôle et formate la sortie.


Modification de la ligne ATS


TTY Line Editing est le composant qui est responsable du traitement des entrées dans la discipline de ligne. Il faut dire que l' édition de ligne est un concept général et concerne le traitement d'entrée. Par exemple, Bash et Vim ont leur propre édition de ligne.


Nous pouvons contrôler les paramètres de discipline de la ligne de l'appareil TTY actuel à l'aide du programme stty . Essayons un peu.


Ouvrez Bash ou tout autre Shell et tapez:


 stty icanon -echo 

Maintenant, essayez de taper quelque chose et vous ne verrez pas votre saisie (ne vous inquiétez pas, vous pouvez toujours transmettre la saisie au programme). Vous venez de désactiver l'écho, c'est-à-dire l'affichage des caractères saisis à l'écran. Entrez maintenant:


 stty raw echo 

Essayez de taper quelque chose. Vous voyez comment la conclusion est rompue. Mais pour plus d'effet, allons dans Dash - tapez /bin/sh . Essayez maintenant de saisir des caractères spéciaux ( Ctrl + n'importe quel caractère du clavier) ou appuyez simplement sur Enter . Vous êtes perplexe - quels sont ces étranges personnages à l'écran? Le fait est que, étant entré dans le Shell le plus simple, en plus de la modification de ligne de la discipline elle-même, nous avons également désactivé la modification de ligne Bash, et maintenant nous pouvons observer avec puissance et maintenir l'effet de l'inclusion du mode brut de discipline de la ligne. Ce mode ne traite pas du tout l'entrée et ne formate pas la sortie. Pourquoi le mode brut est-il nécessaire? Par exemple, pour Vim : il s'ouvre dans toute la fenêtre du terminal et traite l'entrée elle-même, au moins de sorte que les symboles spéciaux de la discipline de ligne ne se croisent pas avec les symboles spéciaux de Vim lui-même.


Pour encore plus de compréhension, examinons la personnalisation des caractères de contrôle. La commande stty <control-character> <string> nous aidera avec cela.
Entrez dans Bash:


 stty erase 0 

Maintenant, le caractère de contrôle d' erase sera assigné au caractère 0 . Le bouton de backspace compte généralement ^? , mais maintenant ce caractère spécial sera envoyé au tampon de lecture PTS littéralement - essayez-le vous-même. Vous pouvez maintenant effacer des caractères à l'aide du bouton 0 du clavier, car vous avez vous-même demandé à la discipline de ligne tty de reconnaître le caractère entré comme caractère de contrôle d' erase . Vous pouvez retourner le paramètre à l'aide de la commande stty erase ^\? ou simplement fermer le terminal, car nous n'avons affecté que le périphérique tty actuel.


Vous pouvez trouver plus d'informations dans man stty .




Émulateur de terminal et pseudoterminal


Chaque fois que nous ouvrons un nouveau terminal dans le système X Window, le serveur de terminaux GNOME génère un nouveau processus et y lance le programme par défaut. Il s'agit généralement d'une sorte de Shell (par exemple, Bash).


La communication avec le programme en cours se fait via le pseudoterminal (pseudo-terminal, PTY). Le pseudo-terminal lui-même existe dans le noyau, cependant, il reçoit des entrées de l'espace utilisateur - de l'émulateur de terminal.


Le pseudo-terminal se compose des deux appareils TTY virtuels suivants:
1) Maître PTY (PTM) - la partie principale du pseudo-terminal. Utilisé par GNOME Terminal Server pour transférer les entrées du clavier vers un programme s'exécutant à l'intérieur du terminal, ainsi que pour lire la sortie du programme et afficher la sortie. Le serveur de terminaux GNOME, à son tour, communique avec le système X Window via le protocole X.
2) esclave PTY (PTS) - partie esclave du pseudo-terminal. Utilisé par un programme exécuté à l'intérieur du terminal pour lire les entrées du clavier et afficher les sorties à l'écran. Du moins, le programme lui-même le pense (je vais expliquer ce que cela signifie, un peu plus loin).


Toutes les données enregistrées dans le dispositif PTS sont l'entrée du dispositif PTM, c'est-à-dire qu'elles deviennent lisibles sur le dispositif PTM. Et vice versa: toutes les données enregistrées dans le dispositif PTM sont l'entrée du dispositif PTS. C'est ainsi que GNOME Terminal Server et le programme s'exécutant à l'intérieur du terminal communiquent. Chaque appareil PTM est associé à son propre appareil PTS.


Le processus de lancement d'un nouveau terminal ressemble à ceci:
1) GNOME Terminal Server crée des périphériques maîtres et esclaves en appelant la fonction open () sur un périphérique spécial / dev / ptmx . L'appel open () renvoie le descripteur de fichier du périphérique PTM créé - master_fd .
2) GNOME Terminal Server crée un nouveau processus en appelant fork() . Ce processus sera le nouveau terminal.
3) Dans le terminal PTS, l'appareil s'ouvre sur les descripteurs de fichiers 0, 1, 2 (stdin, stdout et stderr, respectivement). Maintenant, les E / S du terminal standard circulent vers cet appareil.
4) Le programme souhaité est lancé dans le terminal en appelant la fonction exec() . Certains Shell démarrent généralement (par exemple, Bash). Tout programme lancé ultérieurement à partir de Bash aura les mêmes descripteurs de fichier que Bash lui-même, c'est-à-dire que les flux de programme seront dirigés vers le périphérique PTS.


Vous pouvez voir par vous-même où les flux de sortie de terminal standard sont dirigés à l'aide de la commande ls -la /proc/self/fd :


Le périphérique PTS est situé sur le chemin / dev / pts / N , et le chemin vers le périphérique PTM ne nous intéresse pas du tout. Le fait est que GNOME Terminal Server possède déjà un descripteur de fichier pour le périphérique PTM ouvert et qu'il n'a pas besoin de chemin d'accès, cependant, dans le processus enfant, nous devons ouvrir le périphérique PTS sur des flux de sortie standard en appelant la fonction open() , qui nécessite le chemin d'accès au fichier.


Rappelez-vous, j'ai dit qu'un programme utilisant un appareil PTS pense seulement qu'il communique directement avec le terminal? Le fait est que le PTS est également un appareil terminal ( appareil TTY), mais la différence entre l'appareil PTS et l'appareil TTY réel est que l'appareil PTS reçoit une entrée non pas du clavier, mais de l'appareil maître, et la sortie ne va pas à l'affichage, mais à appareil maître. C'est pourquoi le pseudo-terminal est nommé ainsi: le pseudo-terminal imite (encore une fois ??) le terminal. La différence entre l'émulateur de terminal et le pseudo-terminal est que l'émulateur de terminal n'est qu'un programme graphique qui vous permet d'exécuter le terminal directement à l'intérieur de l'interface de la fenêtre, mais cette fonctionnalité est implémentée à l'aide du pseudo-terminal.


Le fait que l' appareil PTS soit un appareil TTY est très important. Voici pourquoi:


  1. Le programme auquel le terminal est connecté possède toutes les capacités d'un terminal conventionnel. Par exemple: désactiver l'écho, désactiver / activer la vue canonique.
  2. Le programme, sachant qu'un terminal est attaché à celui-ci (il est dit que le programme a un terminal de contrôle), peut fonctionner de manière interactive et demander à l'utilisateur de le saisir. Par exemple, demandez un nom d'utilisateur et un mot de passe.
  3. Il existe également une discipline de ligne TTY, nous avons donc la possibilité de traiter les caractères de contrôle avant qu'ils n'atteignent le programme, ainsi que de formater la sortie du programme.

Le dispositif PTM est également un dispositif ATS, mais cela ne joue aucun rôle, car il n'est pas utilisé comme terminal de commande. De plus, la discipline de ligne du périphérique PTM est définie sur le mode brut, par conséquent, le traitement n'est pas effectué lors du transfert de données du PTS vers le périphérique PTM. Cependant, les appels à read() et write() partir de l'espace utilisateur sont toujours traités en premier par la discipline de ligne sur les deux appareils. Ce moment jouera un rôle encore plus important, comme nous le verrons plus loin.


Le processus de communication entre GNOME Terminal Server et le programme exécuté à l'intérieur du terminal est le suivant:



Il convient d'examiner plus en détail le rôle que la discipline de ligne joue dans la communication entre les deux parties d'un pseudo-terminal. Ici, la discipline de ligne est responsable du traitement des données passant du PTM au dispositif PTS , ainsi que de la livraison des données d'une partie du pseudo-terminal à une autre. Lorsque nous sommes dans le pilote de périphérique PTS, nous engageons la discipline de ligne du périphérique PTM, et vice versa.




Appareils virtuels


Vous auriez probablement pensé que vous pourriez ouvrir le fichier le long du chemin / dev / pts / N et y écrire ou lire des données, comme à partir d'un fichier texte normal? Oui, tous les appareils sur les systèmes de type Unix sont des fichiers, grâce au principe fondamental d'Unix, qui stipule que tout est un fichier. Cependant, aucun fichier de périphérique spécial (anglais - fichier de périphérique) n'est un fichier texte. Ces périphériques sont appelés périphériques virtuels, c'est-à-dire qu'ils existent exclusivement en mémoire et non sur disque.


N'essayez pas d'ouvrir ces fichiers en tant que fichiers texte standard. Cependant, vous pouvez utiliser ces périphériques via des opérations d' write() et de read() , dont l'appel sera servi par le pilote de périphérique. Essayons de le faire.


Ouvrez deux fenêtres de terminal et entrez tty dans chaque commande. Cette commande montrera quel appareil TTY dessert le terminal actuellement actif. Entrez maintenant l' echo "Hello, World!" > /dev/pts/N echo "Hello, World!" > /dev/pts/N dans la première fenêtre de terminal, où N est l'index PTS du deuxième périphérique de fenêtre, passez à la deuxième fenêtre et vous verrez votre entrée à partir de la première fenêtre. Vous avez maintenant écrit les données sur le périphérique PTS de la deuxième fenêtre comme si elles avaient été effectuées par un programme exécuté sur ce terminal .





Dispositif pseudo-terminal


Nous nous rapprochons de plus en plus de la dernière partie de l'article, mais avant cela, nous jetons un œil «sous le capot» de Linux - considérons le périphérique du pseudo-terminal au niveau du noyau. Il y aura beaucoup de code, mais j'essaierai d'expliquer chaque bloc de code donné avec autant de détails que possible, de réduire les détails sans importance et d'aller en séquence.


Avant de commencer, nous vous présentons le soi-disant "panier de composants". Au fur et à mesure que nous progressons dans le cœur, nous y ajouterons de plus en plus de composants et trouverons une connexion entre eux. J'espère que cela vous aidera à mieux comprendre le dispositif pseudo-terminal. Commençons.


Lorsque Linux démarre, il charge les pilotes de périphériques nécessaires. Notre pseudo-terminal dispose également d'un tel pilote. Son enregistrement commence par un appel à cette fonction:


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

Pour tous les systèmes modernes, la fonction unix98_pty_init() sera appelée:


 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"); 

Ici, nous sommes intéressés par 3 choses:


  1. Appelle tty_set_operatons pour le pilote maître pty et les périphériques esclaves pty.
  2. La fonction ptmx_open , qui est responsable de la création des deux parties du pseudo-terminal lors de l'ouverture du périphérique spécial / dev / ptmx . Important: / dev / ptmx n'est pas un périphérique PTM, mais juste une interface pour créer un nouveau pseudo-terminal.
  3. Enregistrez les pilotes de périphérique PTM et PTS.

Allons dans l'ordre:


1. tty_set_operations


La fonction tty_set_operations () configure simplement une table de fonctions pour le pilote actuel:


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

La structure tty_operations est une table de fonctions utilisée pour accéder aux fonctions du pilote TTY du périphérique.


Je vais souligner la chose la plus importante dans les structures pty_unix98_ops et ptm_unix98_ops , qui sont le tableau des fonctions pour les parties correspondantes du pseudo-terminal:


 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, // ... }; 

Ici, vous pouvez observer la fonction pty_write, pty_write est déjà familière dans l'article sur Sishny printf - nous y reviendrons un peu plus tard.


Ajoutons cette structure à notre panier de composants:


Comme vous pouvez le voir, les principales méthodes des deux pilotes ne sont pas du tout différentes. Soit dit en passant, notez qu'il n'y a pas de fonction pour l'opération read () - il n'y a rien de tel que pty_read() . Le fait est que la lecture sera servie uniquement par discipline de ligne. Ainsi, nous découvrons la deuxième caractéristique importante de la discipline de ligne - la lecture de données à partir d'un appareil TTY.




2. ptmx_open


Passons maintenant à ptmx_open () :


 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 ,   } 

Nous nous intéressons à la fonction tty_init_dev() , où le premier argument est le pilote de périphérique PTM et le second est l'index de périphérique. Ici, nous quittons la zone de responsabilité du pilote PTY et allons dans le fichier, qui n'est responsable que des appareils TTY généraux et ne sait rien de notre pseudo-terminal.


 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; } 

Tout d'abord, nous 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; } 

La seule chose qui nous intéresse ici est la fonction 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; } 

Qui appelle 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; } 

Nous avons donc examiné l'appel à la fonction alloc_tty_struct() , qui crée la structure tty_struct avec la discipline de ligne - la structure tty_ldisc . Les deux structures sont liées l'une à l'autre. Examinons de plus près ces structures.


  • tty_struct est une structure permettant d'accéder au pilote de périphérique TTY et à d'autres champs. Cela ressemble à ceci:

 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 est la structure de la discipline de la ligne TTY de l'appareil. Il se compose de seulement deux champs et se présente comme suit:

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

Cela ne semble rien de compliqué? Ajoutons toutes les structures considérées jusqu'à présent à notre panier et relions-les de la même manière qu'elles sont connectées dans le code:
tty_struct


Mais nous avons créé tty_struct juste pour le périphérique PTM. Et l'appareil PTS? Pour ce faire, nous revenons à la fonction tty_init_dev() et rappelons que l'on est alors censé appeler la fonction 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); } 

Le commentaire nous dit que cette méthode est responsable de la création de diverses structures supplémentaires. Appareil PTS et sera notre structure supplémentaire. J'avoue, c'était extrêmement surprenant pour moi, car c'est l'enfer, tout l'appareil, et pas seulement une sorte de structure supplémentaire! Mais nous comprenons tous que tous les appareils ne sont qu'une sorte de structure, alors passez à autre chose. Ok, qu'est-ce que driver-> ops-> installer ici ? Pour ce faire, consultez à nouveau le tableau des fonctions du pilote PTM:


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

Et nous comprenons que nous sommes intéressés par la fonction pty_unix98_install() :


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

Qui appelle la fonction 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 .




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





Bonjour tout le monde!


. "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 . , ? Peu importe comment. , , — .


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



Conclusion


. :


  1. TTY
  2. ,

, ! - — , !


Les sources


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


All Articles