Olá Mundo! Imersão profunda em terminais


Fui inspirado a escrever este artigo por um artigo sobre a análise de Sishny printf . No entanto, houve um momento em que os dados seguem depois que entram no dispositivo terminal. Neste artigo, desejo corrigir esse defeito e analisar o caminho dos dados no terminal. Também descobriremos como o Terminal difere do Shell, o que é o Pseudoterminal, como os emuladores de terminal funcionam e muito mais.


O básico


Vamos primeiro entender o que é Terminal, Shell, Console, como o Emulador de Terminal difere do Terminal comum e por que esse nome é chamado. Muitas informações já foram escritas sobre isso, então você não ouvirá nada de novo aqui. Quase todas as informações aqui foram retiradas da Internet, fornecerei links no final do artigo. Quem já sabe o que todas essas coisas significam, pode pular esta seção com segurança.




Terminal


Um terminal é uma combinação de uma tela e um teclado, ou seja, um dispositivo físico. Antes de os terminais se tornarem essa combinação específica, eles eram um tipo de dispositivo chamado tele-impressora (teletipo, teletipo ou TTY), ou seja, uma combinação de impressora e teclado. Normalmente, vários terminais estão conectados ao mesmo computador. Assim, foi possível trabalhar para vários usuários no mesmo computador, e cada um teve sua própria sessão, independente dos outros. O terminal recebeu esse nome porque estava localizado na extremidade do cabo do terminal.


Este é o Teletype :


Teletype

E este é o Terminal :


Terminal



Console


Console (console) - um terminal conectado diretamente ao computador. O fato é que a maioria dos terminais estava conectada implicitamente, mas pelo menos um estava conectado diretamente ao computador. O console teve permissão para usar um círculo estritamente definido de pessoas, pois permitiu configurar o computador.




Shell


Se os dois anteriores forem dispositivos físicos, essa definição se refere exclusivamente ao software.


Shell é um intérprete de linha de comando. O principal objetivo é executar outros programas. Há um grande número de conchas diferentes. O mais comum é o Bash (do inglês Bourne Again SHell, que, como sugere a Wikipedia , é um trocadilho com o Shell "Born again", ou seja, um Shell "revivido"). Outros exemplos: Dash (um Shell leve, disponível se você executar o binário em / bin / sh), Zsh.




Obviamente, tanto os terminais quanto os consoles não conseguiram deixar de refletir seus tempos modernos. Portanto, consideraremos ainda mais o Emulador de Terminal e o Console Virtual .


Emulador de terminal


Emulador de terminal - um emulador do bom e velho terminal. É necessário um emulador de terminal para programas que não podem interagir diretamente com o X Window System - Bash, Vim e outros.


Vamos primeiro estabelecer as responsabilidades do terminal:


  1. Transferir entrada do usuário para um computador
  2. Entrega da saída do computador para a tela

Portanto, nosso Emulador de Terminal faz exatamente a mesma coisa: entrega a entrada do usuário ao programa em execução e também exibe a saída do programa no visor. De qualquer forma, o significado permanece - entre o usuário e o programa em execução, existe algum tipo de camada responsável pela entrada / saída. Exemplos de emulador de terminal: gnome-terminal, xterm, konsole.


Por favor, não confunda Shell e Emulador de Terminal!
O Emulador de Terminal é um aplicativo GUI, ou seja, uma janela no Sistema X Window. O Shell é um interpretador de linha de comando, ou seja, apenas um executor de comando, não possui um shell gráfico. Falando corretamente, você não inicia o Bash , executa o Emulador de Terminal, que inicia o Bash dentro de si . Terminal Emulator e Bash são absolutamente 2 programas diferentes. O primeiro é o único responsável pela entrada / saída, o segundo - pelo processamento de comandos.


Mais adiante neste artigo, todas as referências ao terminal se referirão ao emulador de terminal.




Console Virtual (Terminal Virtual)


Pressione Ctrl + Alt + FN, onde N geralmente possui valores de 1 a 6. O que você acabou de ver se chama Console Virtual (console virtual) ou Terminal Virtual (terminal virtual). Lembra do que eu disse anteriormente sobre terminais? Muitos terminais foram conectados a um computador e cada terminal foi uma sessão separada, independente dos outros. O Console virtual repete essa idéia: pode haver várias sessões independentes dentro do seu computador (no entanto, os recursos do computador ainda estão obviamente compartilhados).


Você pode nomear essa entidade como Console virtual e Terminal virtual, porque, por definição, um console é um terminal conectado diretamente a um computador, mas todos os terminais virtuais são, de certo modo, conectados diretamente a um computador.




Dispositivos TTY


Cada terminal possui seu próprio dispositivo TTY (dispositivo de terminal), que fornece o console. Embora você não encontre teletipos, mas a redução no TTY sobreviveu até hoje.


Um dispositivo TTY consiste em dois componentes fundamentais:


  1. Driver de dispositivo Ele é responsável por fornecer a entrada do teclado para o programa e exibir a saída do programa na tela.
  2. Disciplina de linha TTY (disciplina de linha russa). A disciplina de linha é a interface de acesso ao driver, que, no entanto, traz muita lógica ao dispositivo TTY. Podemos dizer que os proxies de disciplina de linha chamam o driver. Qual é a área de responsabilidade desse componente, descobriremos durante o artigo.

Crie um dispositivo TTY:



Existem 3 tipos de dispositivos TTY:


  1. Dispositivo do console - fornece operação do console virtual. A entrada e saída deste dispositivo é totalmente controlada pelo kernel.
  2. Dispositivo PTY (pseudo-terminal) - fornece operação do terminal na interface da janela. A entrada e saída deste dispositivo é controlada por um emulador de terminal que opera no espaço do usuário.
  3. Dispositivo serial - comunica-se diretamente com o hardware. Geralmente não é usado diretamente, mas existe como o nível mais baixo na organização da arquitetura de um dispositivo terminal.

Neste artigo, falaremos especificamente sobre o segundo tipo de dispositivos TTY - pseudo-terminais.




Disciplina de Linha TTY


Começamos a examinar a disciplina da linha de dispositivos TTY.


A primeira característica importante de uma disciplina de linha é que ela é responsável pelo processamento de E / S. Isso inclui, por exemplo, o processamento de caracteres de controle (consulte Caracteres de controle ) e a formatação da saída. Por exemplo, você digita qualquer texto, mas de repente percebe que estava errado ao escrever algo e deseja apagá-lo - é aqui que a disciplina de linha entra em cena.


Analisaremos em detalhes o que exatamente acontece quando trabalhamos no Bash em execução no terminal. Por padrão, um dispositivo TTY opera no modo canônico com o eco ativado . Um eco é uma exibição dos caracteres que você inseriu na tela.


Quando inserimos, por exemplo, o caractere a , esse caractere é enviado ao dispositivo TTY, mas é interceptado pela disciplina da linha TTY do dispositivo. Ela lê um personagem em seu buffer interno, vê que o modo de echo está ativado e exibe o personagem na tela. No momento, nada ainda está disponível para leitura no programa ao qual o dispositivo terminal está conectado. Vamos pressionar backspace no teclado. Symbol ^? novamente interceptado pela disciplina de linha, e o último, percebendo que o usuário deseja apagar o último caractere inserido, remove esse caractere do buffer interno e apaga esse caractere também da tela. Agora, se pressionarmos Enter, a Disciplina de Linha TTY finalmente enviará para o buffer de leitura do dispositivo terminal tudo o que foi escrito anteriormente para o buffer interno da disciplina, incluindo LF. Ao mesmo tempo, os caracteres CR e LF são exibidos na tela para mover o cursor para uma nova linha - esta é a formatação da saída.


É assim que o modo canônico funciona - ele transfere todos os caracteres inseridos no dispositivo somente depois de pressionar Enter , processa os caracteres de controle e formata a saída.


Edição de linha TTY


A edição de linha TTY é o componente responsável pelo processamento de entrada na disciplina de linha. Deve-se dizer que a edição de linha é um conceito geral e se refere ao processamento de entrada. Por exemplo, Bash e Vim têm sua própria edição de linha.


Podemos controlar as configurações de disciplina da linha do dispositivo TTY atual usando o programa stty . Vamos experimentar um pouco.


Abra o Bash ou qualquer outro Shell e digite:


 stty icanon -echo 

Agora, tente digitar alguma coisa e você não verá a sua entrada (não se preocupe, você ainda pode passar a entrada para o programa). Você acabou de desativar o eco - ou seja, a exibição dos caracteres digitados na tela. Agora digite:


 stty raw echo 

Tente digitar alguma coisa. Você vê como a conclusão é quebrada. Mas, para mais efeito, vamos para o Dash - digite /bin/sh . Agora tente inserir caracteres especiais ( Ctrl + qualquer caractere no teclado) ou apenas pressione Enter . Você está perplexo - quais são esses caracteres estranhos na tela? O fato é que, ao entrar no Shell mais simples, além da edição de linha da própria disciplina, também desabilitamos o Bash de edição de linhas e agora podemos observar com poder e principal o efeito da inclusão do modo bruto de disciplina da linha. Este modo não processa a entrada e não formata a saída. Por que o modo bruto é necessário? Por exemplo, para o Vim : ele se abre em toda a janela do terminal e processa a entrada em si, pelo menos para que símbolos especiais da disciplina de linha não se cruzem com símbolos especiais do próprio Vim.


Para um entendimento ainda maior, vejamos a personalização dos caracteres de controle. O stty <control-character> <string> nos ajudará com isso.
Entre no Bash:


 stty erase 0 

Agora, o caractere de controle de erase será atribuído ao caractere 0 . O botão backspace geralmente importa ^? , mas agora esse caractere especial será enviado para o buffer de leitura do PTS literalmente - tente você mesmo. Agora você pode apagar caracteres usando o botão 0 no teclado, porque você mesmo solicitou a disciplina de linha tty para reconhecer o caractere inserido como um caractere de controle de exclusão. Você pode retornar a configuração usando o comando stty erase ^\? ou apenas fechando o terminal, porque afetamos apenas o dispositivo tty atual.


Você pode encontrar mais informações no man stty .




Emulador de terminal e pseudoterminal


Cada vez que abrimos um novo terminal no Sistema X Window, o GNOME Terminal Server gera um novo processo e inicia o programa padrão nele. Geralmente, esse é algum tipo de shell (por exemplo, Bash).


A comunicação com o programa em execução ocorre através do chamado Pseudoterminal (pseudo-terminal, PTY). O próprio pseudo-terminal existe no kernel, no entanto, recebe entrada do espaço do usuário - do emulador de terminal.


O pseudo-terminal consiste nos seguintes dois dispositivos TTY virtuais :
1) Mestre PTY (PTM) - a parte principal do pseudo-terminal. Usado pelo GNOME Terminal Server para transferir a entrada do teclado para um programa em execução dentro do terminal, bem como para ler a saída do programa e exibir a saída. O GNOME Terminal Server, por sua vez, se comunica com o Sistema X Window através do protocolo X.
2) PTY escravo (PTS) - parte escrava do pseudo-terminal. Usado por um programa em execução dentro do terminal para ler a entrada do teclado e exibir a saída na tela. Pelo menos, o próprio programa pensa assim (vou explicar o que isso significa, um pouco mais).


Qualquer dado gravado no dispositivo PTS é a entrada do dispositivo PTM, ou seja, fica legível no dispositivo PTM. E vice-versa: qualquer dado gravado no dispositivo PTM é a entrada do dispositivo PTS. É assim que o GNOME Terminal Server e o programa em execução no terminal se comunicam. Cada dispositivo PTM está associado ao seu próprio dispositivo PTS.


O processo de lançamento de um novo terminal é mais ou menos assim:
1) O GNOME Terminal Server cria dispositivos mestre e escravo chamando a função open () em um dispositivo especial / dev / ptmx . A chamada open () retorna o descritor de arquivo do dispositivo PTM criado - master_fd .
2) O GNOME Terminal Server cria um novo processo chamando fork() . Este processo será o novo terminal.
3) No terminal PTS, o dispositivo abre nos descritores de arquivo 0, 1, 2 (stdin, stdout e stderr, respectivamente). Agora, a E / S do terminal padrão flui para este dispositivo.
4) O programa desejado é iniciado no terminal chamando a função exec() . Alguns Shell geralmente iniciam (por exemplo, Bash). Qualquer programa lançado posteriormente a partir do Bash terá os mesmos descritores de arquivo que o próprio Bash, ou seja, os fluxos do programa serão direcionados ao dispositivo PTS.


Você pode ver por si mesmo onde os fluxos de saída do terminal padrão são direcionados usando o ls -la /proc/self/fd :


O dispositivo PTS está localizado no caminho / dev / pts / N , e o caminho para o dispositivo PTM não nos interessa. O fato é que o GNOME Terminal Server já possui um descritor de arquivo para o dispositivo PTM aberto e não precisa de um caminho para ele, no entanto, no processo filho, precisamos abrir o dispositivo PTS nos fluxos de saída padrão chamando a função open() , que requer o caminho para o arquivo.


Lembre-se, eu disse que um programa que usa um dispositivo PTS pensa apenas que se comunica diretamente com o terminal? O fato é que o PTS também é um dispositivo terminal ( dispositivo TTY), mas a diferença entre o dispositivo PTS e o dispositivo TTY real é que o dispositivo PTS recebe entrada não do teclado, mas do dispositivo mestre, e a saída não vai para a tela, mas para dispositivo mestre. É por isso que o pseudo-terminal é nomeado assim - o pseudo-terminal apenas imita (de novo?) O terminal. A diferença entre o emulador de terminal e o pseudo-terminal é que o emulador de terminal é apenas um programa gráfico que permite executar o terminal diretamente dentro da interface da janela, mas esse recurso é implementado usando o pseudo-terminal.


O fato de o dispositivo PTS ser um dispositivo TTY é muito importante. Aqui está o porquê:


  1. O programa ao qual o dispositivo terminal está conectado possui todos os recursos de um terminal convencional. Por exemplo: desativar eco, desativar / ativar a exibição canônica.
  2. O programa, sabendo que um dispositivo terminal está conectado a ele (diz-se que o programa possui um terminal de controle), pode trabalhar interativamente e pedir entrada ao usuário. Por exemplo, solicite um nome de usuário e senha.
  3. Há também uma Disciplina de Linha TTY, portanto, temos a capacidade de processar caracteres de controle antes que eles cheguem ao programa, além de formatar a saída do programa.

O dispositivo PTM também é um dispositivo TTY, mas não desempenha nenhum papel, pois não é usado como um terminal de controle. Além disso, a disciplina de linha do dispositivo PTM é configurada para o modo bruto, portanto, o processamento não é realizado ao transferir dados do PTS para o dispositivo PTM. No entanto, as chamadas para read() e write() do espaço do usuário ainda são atendidas pela disciplina de linha nos dois dispositivos. Este momento terá um papel ainda maior, como veremos mais adiante.


O processo de comunicação entre o GNOME Terminal Server e o programa em execução no terminal é o seguinte:



Vale a pena examinar com mais detalhes o papel que a disciplina de linha desempenha na comunicação entre as duas partes de um pseudo-terminal. Aqui, a disciplina de linha é responsável pelo processamento de dados que passam do PTM para o dispositivo PTS , bem como pela entrega de dados de uma parte do pseudo-terminal para outra. Quando estamos no driver de dispositivo PTS, adotamos a disciplina de linha do dispositivo PTM e vice-versa.




Dispositivos virtuais


Você provavelmente pensaria que poderia abrir o arquivo ao longo do caminho / dev / pts / N e escrever ou ler dados dele, como em um arquivo de texto comum? Sim, todos os dispositivos em sistemas semelhantes ao Unix são arquivos, graças ao princípio fundamental do Unix, que afirma que tudo é um arquivo. No entanto, nenhum arquivo de dispositivo especial (inglês - arquivo de dispositivo) é um arquivo de texto. Esses dispositivos são chamados de dispositivos virtuais - ou seja, eles existem exclusivamente na memória, não no disco.


Não tente abrir esses arquivos como arquivos de texto regulares. No entanto, você pode usar esses dispositivos através das operações write() e read() , cuja chamada será atendida pelo driver do dispositivo. Vamos tentar fazer isso.


Abra duas janelas de terminal e insira tty em cada comando. Este comando mostrará qual dispositivo TTY está servindo o terminal atualmente ativo. Agora digite echo "Hello, World!" > /dev/pts/N echo "Hello, World!" > /dev/pts/N na primeira janela do terminal, onde N é o índice PTS do dispositivo da segunda janela, alterne para a segunda janela e você verá a entrada da primeira janela. Agora você gravou os dados no dispositivo PTS da segunda janela como se tivessem sido executados por um programa em execução naquele terminal .





Dispositivo pseudo-terminal


Estamos nos aproximando cada vez mais da parte final do artigo, mas antes disso examinamos "por baixo do capô" do Linux - considere o dispositivo do pseudo-terminal no nível do kernel. Haverá muito código, mas tentarei explicar cada bloco de código com o máximo de detalhes possível, reduzir detalhes sem importância e seguir em seqüência.


Antes de começar, apresentamos a chamada "cesta de componentes". À medida que avançamos no núcleo, adicionaremos mais e mais componentes a ele e encontraremos uma conexão entre eles. Espero que isso ajude você a entender melhor o dispositivo pseudo-terminal. Vamos começar.


Quando o Linux inicia, ele carrega os drivers de dispositivo necessários. Nosso pseudo-terminal também possui esse driver. Seu registro começa com uma chamada para esta função:


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

Para todos os sistemas modernos, a função unix98_pty_init() será chamada:


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

Aqui estamos interessados ​​em 3 coisas:


  1. Chama tty_set_operatons para o driver mestre pty e dispositivos escravo pty.
  2. A função ptmx_open , responsável por criar as duas partes do pseudo-terminal ao abrir o dispositivo especial / dev / ptmx . Importante: / dev / ptmx não é um dispositivo PTM, mas apenas uma interface para criar um novo pseudo-terminal.
  3. Registre os drivers de dispositivo PTM e PTS.

Vamos em ordem:


1. tty_set_operations


A função tty_set_operations () apenas configura uma tabela de funções para o driver atual:


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

A estrutura tty_operations é uma tabela de funções usada para acessar as funções do driver TTY do dispositivo.


Vou destacar o mais importante nas estruturas pty_unix98_ops e ptm_unix98_ops , que são a tabela de funções para as partes correspondentes do 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, // ... }; 

Aqui você pode observar a função pty_write, pty_write já é familiar no artigo sobre Sishny printf - retornaremos a ela um pouco mais tarde.


Vamos adicionar essa estrutura à nossa cesta de componentes:


Como você pode ver, os principais métodos de ambos os drivers não são de todo diferentes. A propósito, observe que não há função para a operação read () - não há nada como pty_read() . O fato é que a leitura será servida apenas pela disciplina de linha. Assim, aprendemos sobre o segundo recurso importante da disciplina de linha - leitura de dados de um dispositivo TTY.




2. ptmx_open


Agora vamos para 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 ,   } 

Estamos interessados ​​na função tty_init_dev() , onde o primeiro argumento é o driver de dispositivo PTM e o segundo é o índice do dispositivo. Aqui deixamos a zona de responsabilidade do driver PTY e vamos para o arquivo, que é responsável apenas pelos dispositivos TTY gerais e não sabe nada sobre o nosso 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; } 

Primeiro, alloc_tty_struct() função 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; } 

A única coisa que nos interessa aqui é a função 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; } 

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

Então, examinamos a chamada para a função alloc_tty_struct() , que cria a estrutura tty_struct junto com a disciplina de linha - a estrutura tty_ldisc . Ambas as estruturas têm links entre si. Vamos dar uma olhada nessas estruturas.


  • tty_struct é uma estrutura para acessar o driver de dispositivo TTY e alguns outros campos. É assim:

 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 é a estrutura para a disciplina da linha TTY do dispositivo. Consiste em apenas dois campos e tem a seguinte aparência:

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

Parece não ser nada complicado? Vamos adicionar todas as estruturas consideradas até esse momento à nossa cesta e vinculá-las da mesma maneira que estão conectadas no código:
Crie tty_struct


Mas criamos tty_struct apenas para o dispositivo PTM. E o dispositivo PTS? Para fazer isso, retornamos à função tty_init_dev() e lembramos que devemos chamar a função 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); } 

O comentário nos diz que esse método é responsável pela criação de várias estruturas adicionais. Dispositivo PTS e será nossa estrutura adicional. Eu admito, foi extremamente surpreendente para mim, porque é, diabos, todo o dispositivo, e não apenas algum tipo de estrutura adicional! Mas todos entendemos que todos os dispositivos são apenas algum tipo de estrutura, então siga em frente. Ok, o que é driver-> ops-> instalar aqui ? Para fazer isso, consulte a tabela de funções do driver PTM novamente:


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

E entendemos que estamos interessados ​​na função pty_unix98_install() :


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

Que chama a função 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 .




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





Olá Mundo!


. "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 . , ? Não importa como. , , — .


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



Conclusão


. :


  1. TTY
  2. ,

, ! - — , !


Fontes


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


All Articles