Por dentro da console em sistemas Linux
- por Sergio Prado
Você sabe que o Linux é um sistema operacional multi-usuário e multi-tarefa, e por esse motivo vários usuários podem se conectar ao mesmo tempo.
E existem várias formas dos usuários se conectarem ao sistema. A mais comum, principalmente em sistemas desktop, é através da camada de interface gráfica X11, que provê mecanismos de login de múltiplos usuários no sistema. É comum também o uso de protocolos de rede como telnet e ssh para conexões remotas em sistemas Linux.
Já em hardwares dedicados com Linux embarcado, muitas vezes bastante limitados com relação às interfaces disponíveis, possuem sempre uma interface RS232 que pode ser usada como console para conexão e execução de comandos do sistema operacional.
Além disso, esta console possibilita o acesso às mensagens de boot do kernel desde praticamente as primeiras rotinas de inicialização do Linux, e isso pode nos ajudar quando estamos desenvolvendo alguma atividade de customização ou debugging do kernel.
É realmente uma ferramenta muito importante para o desenvolvedor de Linux embarcado, e por isso é importante conhecermos sua arquitetura.
Se você já tem certa experiência com Linux, sabe que os dispositivos seriais no Linux são representados com o arquivo de dispositivo “/dev/ttyX”. No kit mini2440, por exemplo, existe o device “/dev/ttySAC0” para acesso à porta serial. Mas o fato de ter um arquivo de dispositivo representando a porta serial não a transforma em uma console (ainda).
NOTIFICANDO O KERNEL
O kernel precisa saber qual dispositivo é a console do sistema. Você deve passar esta informação à ele através da linha de comando do kernel, que normalmente depende do bootloader que você esta usando. No kit mini2440 com o U-Boot, esta é a linha de comando passada para o kernel:
root=/dev/mtdblock3 rootfstype=jffs2 console=ttySAC0,115200 |
Na inicialização do kernel, cada plataforma implementa uma rotina chamada “setup_early_printk()” para iniciar a console. Por exemplo, para a arquitetura ARM, esta função esta implementada em “arch/arm/kernel/early_printk.c”:
1 2 3 4 5 |
static int __init setup_early_printk(char *buf) { register_console(&early_console); return 0; } |
A função “register_console()”, implementada no arquivo “kernel/printk.c”, é responsável pela inicialização da console. A partir deste ponto, toda e qualquer chamada a printk() terá como consequência o envio das mensagens do kernel à porta serial.
AGORA O LOGIN
Até então, o que fizemos foi configurar a console no kernel, mas ainda não é possível logar e ter acesso à linha de comando pela porta serial. Precisamos de uma aplicação para abrir a porta serial, realizar o procedimento de login e estabelecer uma seção do usuário com o sistema operacional. Esta aplicação é o famoso “getty”.
No kit mini2440, temos a linha abaixo no “/etc/inittab” que realiza este trabalho:
ttySAC0::respawn:/sbin/getty -L ttySAC0 115200 vt100 # GENERIC_SERIAL |
Na verdade, o “getty” é apenas um wrapper para abrir a porta tty e chamar o programa “login”. Este sim realiza o processo de login e então passa o controle para o interpretador de comando disponível no seu rootfs (bash, sh, ash, ksh, etc). Desta forma, cada byte recebido pela serial chegará ao interpretador de comando, e a saída será enviada de volta para a porta serial.
Bom, na verdade, não é bem assim. Existem algumas camadas extras no kernel para o tratamento dos dados enviados e recebidos pela console.
A propósito, você já parou pra pensar porque os dispositivos seriais são nomeados com TTY?
COISA DE MUSEU
TTY vem de TeleTYpe, um dispositivo que hoje é item de museu. Teletypes eram basicamente máquinas de escrever eletro-mecânicas com uma impressora como dispositivo de saída, e usadas principalmente para comunicação ponto-a-ponto através de um canal de comunicação, que podia ser cabeado ou sem fio. Foram usadas durante algumas décadas como sistema de comunicação enquanto os computadores não existiam ou não eram algo acessível. Por exemplo, a Telex foi uma rede mundial de teletypes conectados para troca de mensagens (telegramas) entre diversos países.
Você pode ver os teletypes como os antecessores de um thinclient ou terminal burro: um dispositivo de entrada (teclado eletro-mecânico), um dispositivo de saída (impressora) e um meio de comunicação para envio e recebimento dos dados. E nada de processamento.
Mas o que isso tem a ver com o Linux e o subsistema TTY? Tudo. Na década de 70, os computadores abandonaram o sistema de processamento em batch e passaram a usar linha de comando para interação com o sistema operacional. E eles precisaram de um dispositivo de entrada e saída para essa interação. É aí que entram os teletypes.
Como nessa época também crescia a popularidade do sistema operacional UNIX, foi desenvolvida uma camada de software no kernel do UNIX para conversar com os teletypes. Essa camada é o início do que conhecemos hoje como o sub-sistema TTY no Linux.
É claro que os teletypes, como dispositivos físicos, não existem mais. Mas sua herança em forma de módulos do kernel do Linux ainda estarão presentes por um bom tempo em nossas vidas. Como é então sua arquitetura dentro do kernel?
A ARQUITETURA TTY
A arquitetura do sub-sistema TTY é basicamente esta:
Veja que existem 3 camadas no kernel do Linux responsáveis pelo tratamento de um dispositivo TTY:
-
UART/Low-Level Driver: esse é o device driver da porta serial. É ele que conversa com o meio físico, configura as portas de I/O do chip da UART, baudrate, paridade, bits de stop, etc. É ele também que trata as interrupções da serial e envia os dados recebidos para a camada mais acima. Para efeito de curiosidade, segue abaixo a rotina de tratamento de interrupção do famoso chip 8250, implementada no kernel do Linux em “drivers/tty/serial/8250.c”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
/* * This is the serial driver's interrupt routine. * * Arjan thinks the old way was overly complex, so it got simplified. * Alan disagrees, saying that need the complexity to handle the weird * nature of ISA shared interrupts. (This is a special exception.) * * In order to handle ISA shared interrupts properly, we need to check * that all ports have been serviced, and therefore the ISA interrupt * line has been de-asserted. * * This means we need to loop through all ports. checking that they * don't have an interrupt pending. */ static irqreturn_t serial8250_interrupt(int irq, void *dev_id) { struct irq_info *i = dev_id; struct list_head *l, *end = NULL; int pass_counter = 0, handled = 0; DEBUG_INTR("serial8250_interrupt(%d)...", irq); spin_lock(&i->lock); l = i->head; do { struct uart_8250_port *up; unsigned int iir; up = list_entry(l, struct uart_8250_port, list); iir = serial_in(up, UART_IIR); if (!(iir & UART_IIR_NO_INT)) { serial8250_handle_port(up); handled = 1; end = NULL; } else if ((up->port.iotype == UPIO_DWAPB || up->port.iotype == UPIO_DWAPB32) && (iir & UART_IIR_BUSY) == UART_IIR_BUSY) { /* The DesignWare APB UART has an Busy Detect (0x07) * interrupt meaning an LCR write attempt occured while the * UART was busy. The interrupt must be cleared by reading * the UART status register (USR) and the LCR re-written. */ unsigned int status; status = *(volatile u32 *)up->port.private_data; serial_out(up, UART_LCR, up->lcr); handled = 1; end = NULL; } else if (end == NULL) end = l; l = l->next; if (l == i->head && pass_counter++ > PASS_LIMIT) { /* If we hit this, we're dead. */ printk_ratelimited(KERN_ERR "serial8250: too much work for irq%d\n", irq); break; } } while (l != end); spin_unlock(&i->lock); DEBUG_INTR("end.\n"); return IRQ_RETVAL(handled); }
-
TTY Driver: Esta camada abstrai a chamada às funções de baixo nível do driver da UART, padronizando a interface de comunicação com a camada mais acima, a “Line Discipline”. Sua implementação encontra-se em
“drivers/char/tty_io.c”.
- Line Discipline: Em uma conexão pela console, alguns bytes podem ter um tratamento especial. Por exemplo, os usuários podem usar a tecla BACKSPACE para corrigir um erro de digitação. Se você conectasse diretamente o driver da UART com a sua aplicação, este controle ficaria todo com esta aplicação. Mas segundo a filosofia UNIX, as aplicações devem ser o mais simples possível. Então a camada “Line Discipline” tem exatamente o objetivo de fazer um tratamento especial dos dados recebidos do driver TTY e depois disponibilizá-los para as aplicações user space. A implementação padrão desta camada encontra-se no kernel em “drivers/char/n_tty.c”.
Estas três camadas em conjunto formam o sub-sistema TTY no kernel do Linux. E podemos perceber algumas vantagens nesta modularização:
- O sub-sistema TTY é independente de hardware, pois você pode substituir a UART por uma conexão SPI ou uma porta paralela, e ter uma console usando outras interfaces além da RS232 substituindo apenas o driver da camada de acesso ao hardware.
- O tratamento dos bytes é abstraído através da camada Line Discipline. Por exemplo, uma conexão PPP usa o mesmo sub-sistema TTY para comunicação, mas a camada Line Discipline é substituída para tratar as especificidades do protocolo PPP.
Vimos aqui apenas o básico da arquitetura TTY, mas espero ter despertado em vocês o desejo de saber mais e se aventurar pelas linhas de código do kernel do Linux.
Um abraço,
Sergio Prado