Site menu Terminais UNIX

Terminais UNIX

Copyright © 2001 Conectiva S/A, (c) 2004 EPx

1. O driver de terminal

No mundo Unix, o driver de terminal é a interface para controle do console, das portas seriais e dos pseudo-terminais. Em tese, as portas seriais não precisariam de uma interface tão rica — ou tão complexa — para seu controle. Mas lembremos que, num passado não muito distante, a função mais proeminente das portas seriais era justamente a conexão com terminais "burros".

Muitos programadores não gostam do driver de terminal Unix; acham-no complexo demais. Nossa opinião é que a "birra" com o driver deriva de problemas de portabilidade entre Unixes. Inicialmente, todo o controle do terminal era feita usando-se a função ioctl(). Hoje, os sistemas compatíveis com POSIX, como o Linux, usam funções próprias para a tarefa. Quem precisa escrever programas portáveis tem de prever as duas possibilidades no código.

Hoje em dia, via de regra, quem confeccionar novos programas para modo texto usará uma bibioteca como ncurses. Apenas quem for fazer programas de nível mais baixo (e.g. emuladores de terminais, aplicativos que lidem diretamente com portas seriais) terão de conhecer e lidar com o driver de terminal.

Mesmo assim, é importante a todo programador conhecê-lo pelo menos superficialmente, pois esse conhecimento será bastante esclarecedor sobre como funciona o ambiente Unix. (Isso é particularmente válido para quem vem de ambientes Microsoft.)

É importante entender que o driver de terminal intermedia a comunicação entre o programa e o dispositivo. Ele não controla o dispositivo em si. Isto será tarefa de algum outro driver de nível mais baixo.

Observe o seguinte diagrama, que ilustra a situação 'clássica' de uso do driver de terminal:

Programa ----> Driver de ----> Driver da    ----> Porta  ----> Terminal burro
         <----  terminal <---- porta serial <---- serial <---- remoto 

Nos dias de hoje, o uso de terminais 'burros' é pequeno e decrescente. O que ainda se usa em muitas instalações é o emulador de terminal rodando em um PC comum - mas isso não altera o modus operandi do diagrama acima.

Outra configuração possível é a comunicação serial pura entre dois computadores. Como dissemos, a comunicação serial em Unix também é controlada pelo driver de terminal:

(computador 1)
Programa ---> Driver de ---> Driver da    ---> Porta  -------+
         <---  terminal <--- porta serial <--- serial <---+  |
                                                          |  |
                                                          |  |
                                                          |  |
Programa ---> Driver de ---> Driver da    ---> Porta  ----+  |
         <---  terminal <--- porta serial <--- serial <------+
(computador 2)

Lembrar que a ponta remota, seja um terminal, seja um outro computador, pode estar distante milhares de quilômetros, e a conexão serial pode ser algo tão efêmero quanto um link via modem telefônico. Ainda, pode haver um "delay" considerável na transmissão dos dados.

Agora, se o "terminal" for o console, então o Linux terá de arcar adicionalmente com a emulação de terminal. Pelo menos nesse caso o "delay" de transmissão será negligenciável:

Programa ---> Driver de ---> Driver do ---> Driver do vídeo -----> Vídeo
         <---  terminal <--- console <--+
                                        |
                                        |
                                        |
                                        +--- Driver do teclado <--- Teclado

Nada desta complexidade adicional transparecerá ao programa; ele vê apenas o driver de terminal.

Uma outra combinação possível é a do pseudo-terminal, que será abordada mais adiante em seção própria.

2. Conceitos básicos

2.1. Modos canônico e 'cru'

Os terminais Unix podem trabalhar em dois modos:

MODO CANÔNICO OU PADRÃO: Modo em que o programa recebe os dados apenas quando o usuário pressiona ENTER. Nesse modo, o usuário pode digitar, usar BackSpace para corrigir o que estiver errado, e quando estiver satisfeito, envia a linha para o programa.

Nesse modo, o driver de terminal controla por ele mesmo o vídeo e o teclado. (Lembrar que ele é parte do kernel.) O programa fica 'dormindo' até que receba a seqüência de caracteres, quando o usuário pressiona ENTER.

Outro ponto importante: mesmo nesse modo, cada caractere digitado viaja do teclado até o kernel da máquina onde o programa roda - mesmo que o terminal em questão esteja muito distante da máquina, eventualmente conectado via modem etc. (Falaremos mais sobre isso quando tratarmos sobre eco local e remoto.)

Muito embora a interface de terminal defina algumas poucas teclas para tornar a experiência do usuário menos árida, é de se esperar que esse modo seja bastante primitivo, para os padrões atuais - e é mesmo. Esse modo é do tempo dos terminais com rolo de papel ao invés de vídeo, e só se aplica bem a programas que trabalhem desse modo i.e. com as diversas linhas 'rolando' pelo vídeo.

Não obstante, esse modo é eficiente e 'leve' para a máquina, portanto deve ser usado onde se presta - e de fato é usado em todos os shells e programas simples de modo texto.

MODO 'CRU' OU DIRETO: Nesse modo, o driver de terminal não tenta ser inteligente; simplesmente repassa os caracteres que recebe ao programa, que terá o encargo de oferecer, se quiser, amenidades tais como Backspace e outras.

A única opção que o programa tem é receber os caracteres um a um ou em blocos de "n" caracteres. Nesse último caso, ainda é possível definir um tempo máximo de espera, depois do qual o driver de terminal liberará quaisquer caracteres do buffer, mesmo que contem menos de "n" unidades.

Programas construídos com bibliotecas de modo texto de alto nível e.g. ncurses vão quase sempre trabalhar nesse modo. Também será este o modo em que, via de regra, trabalharão os programas de comunicação serial.

2.2. Eco local e remoto

Quando você digita um caractere em um terminal, o mesmo é (quase) imediatamente impresso na tela. Esse feedback, ou "eco" pode ser de responsabilidade do próprio terminal (eco local), ou do driver de terminal (eco remoto).

Se o terminal e o driver forem configurados para eco local, então o próprio terminal fará o eco do que o usuário digitar; não é necessário que o driver de terminal retransmita cada caractere recebido.

O eco local é particularmente desejável em terminais ligados via modem ou via Internet, pois elimina o "delay" entre o pressionamento da tecla e a aparição do caractere na tela. A experiência do usuário será muito mais agradável e produtiva. Além disso, economiza-se a retransmissão de cada byte.

Porém, há um problema: não existe a certeza de que o driver de terminal, lá do outro lado da linha, recebeu mesmo o caractere. Portanto, para linhas ruidosas, ou onde de alguma forma a entrega do caractere não seja garantida, o eco remoto proporciona mais segurança do que está acontecendo.

É necessário configurar tanto o terminal quanto o driver para eco local ou remoto, sob pena de ocorrer eco duplo (quando tanto o computador remoto quanto o terminal ecoam o que foi digitado), ou a ausência de eco (quando nenhuma das duas pontas faz eco).

Note ainda que configurar o driver NÃO implica na configuração automática do terminal remoto ! Este terá de ser configurado manualmente, ou receber uma cadeia de caracteres especiais com esse fim - e isso é responsabilidade do programa, ou de alguma biblioteca.

Por último, programas de comunicação serial vão querer desligar incondicionalmente o eco local.

2.3. Terminal controlador e programa em "foreground"

Todo programa em modo texto deve (ou deveria) ter um terminal controlador. Por exemplo, quando o usuário pressionar uma combinação de teclas que gere sinais (por exemplo, Control-C gera um sinal SIGINT para o programa, e isso normalmente faz o mesmo encerrar), esse sinal será enviado para o programa que tenha eleito aquele terminal como controlador.

Algumas poucos Controls são definidos como geradores de sinais. E sim, o driver de terminal permite redefinir ou mesmo desligar essas teclas especiais. Com certeza a primeira coisa que um programa de comunicação serial faz é desligar essas teclas antes de começar a usar a porta.

Mas o sinal não deve ser enviado para todos os programas controlados por aquele terminal - mas apenas para o(s) programa(s) em "foreground". Para isso existe uma função (tcsetpgrp) que permite a um programa definir-se a si mesmo como "foreground".

Esse controle de processos em "foreground" ou "background" envolve noções de controle de processos, e está fora do escopo deste artigo. Aqui, basta dizer que tal controle é normalmente feito pelo shell, de modo que não precisamos cuidar disso em nossos próprios programas.

Agora, se você pretende fazer programas que lançam subprocessos, que por sua vez precisam lidar com o terminal (talvez você esteja escrevendo um novo shell, sei lá :) você precisará buscar esse conhecimento adicional. Recomendo o "GNU C Library Reference", que chega ao ponto de dissecar cada parte de um mini-shell para ilustrar o assunto.

2.4. Características de hardware do terminal

Algumas características da linha serial podem ser configuradas via driver de terminal: paridade, handshake, número de bits etc.

A velocidade é um caso especial: não existe interface padrão para configurar velocidades superiores a 38400 bps. (E alguma interface serial usa taxas inferiores à essa hoje em dia ?) Dizendo de outra forma: cada Unix o fará de uma forma diferente. Consulte o código-fonte de programas portáveis e imite suas técnicas.

2.5. Arquivos de dispositivo

Quase tudo em Unix pode ser controlado como se fosse um arquivo comum, e os dispositivos controlados via driver de terminal não fazem exceção. Tecnicamente, esses arquivos são "dispositivos de caractere", visto que o hardware correspondente transfere dados byte a byte.

Segue algumas famílias de arquivos de terminal. Note que, apesar da interface ser uniforme a todos, existe uma separação pelo tipo de hardware subjacente:

/dev/ttyN (N > 0): Os diversos consoles do Linux.
/dev/ttySN (N >= 0): As portas seriais padrão IBM PC
/dev/ttyXY (X >= 'p', Y um dígito hexadecimal): Pseudo-terminais

Ainda, cada driver de placas multi-seriais terão sua própria faixa de arquivos em /dev.

O programa que vai lidar com terminais precisa trocar informações com o driver de terminal. Para essa troca de informações, a única metáfora possível é a chamada ioctl(), pois as chamadas read() e write() estão reservados para os dados em si.

Para quem já usou ioctl(), sabe que ele é uma espécie de "vala comum" para tudo que não possa ser metaforizado em alguma outra chamada de sistema. Pela sua abrangência, o compilador C não pode checar se os parâmetros passados para ioctl() são válidos.

Para resolver esse problema, o padrão POSIX definiu um conjunto de funções específicas para controle do driver de terminal. Essas funções são via de regra traduzidas para chamadas ioctl() ainda em tempo de compilação, mas pelo menos o compilador pode checar se os parâmetros foram passados corretamente.

O Linux e a maioria dos Unixes modernos implementam as funções POSIX, mas versões mais antigas e/ou que não seguem o padrão POSIX lhe deixarão apenas com os ioctls(). Se você confecciona programas que devem ser portáveis até mesmo para tais 'sabores' do Unix, V. terá de prever as duas situações em seu código-fonte.

3. Interface C para o driver de pseudo-terminal

3.1. A saída padrão é um terminal ?

Programas bem-feitos modificam seu comportamento, conforme a entrada e saída padrões sejam terminais, pipes ou arquivos regulares. Por exemplo, o programa "ls" gera códigos especiais para colorir os nomes de arquivos; mas apenas se a saída for um terminal. Se a saída for um arquivo comum, apenas texto puro será gravado nele.

A função isatty() permite determinar se um manipulador de arquivo é um terminal. O programa a seguir determina se a saída padrão é um terminal:

int main()
{
	printf("A saída padrão %s é terminal", isatty(1): "", "não");
}

Esse comando também serve para, por exemplo, um programa de comunicação serial fiscalizar se está se comunicando mesmo com uma porta serial.

3.2. O nome do terminal corrente

Se o programa precisar reabrir seu terminal controlador, não é necessário determinar o nome exato do arquivo de dispositivo. O arquivo /dev/tty sempre corresponde ao terminal controlador.

Se ainda assim V. quiser saber o nome do terminal, dado um manipulador de arquivo, use a função

char *ttyname(int filedes);

O ponteiro retornado por ttyname() é estático e será sobrescrito na próxima chamada, portanto copie o resultado para um lugar mais seguro :)

3.3. A estrutura termios

NOTA: O livro "Beggining Linux Programming", da Wrox Press, foi usado extensivamente para a composição desta seção.

#include <termios.h>

struct termios {
	tcflag_t c_iflag;
	tcflag_t c_oflag;
	tcflag_t c_cflag;
	tcflag_t c_lflag;
	cc_t c_cc[NCCS];
}

Praticamente todo o controle que o programa pode exercer sobre o driver de terminal é feito através da passagem da estrutura acima. A título de curiosidade, o símbolo tcflag_t costuma ser um inteiro de 32 bits, e cada um de seus bits tem certo significado.

Para obter a estrutura de controle do driver, deve-se a usar a função

tcgetattr(int fildes, struct termios *termios_p);

onde "fildes" é o manipulador de arquivo (sim, o arquivo de terminal já deve estar aberto, e normalmente será a entrada ou saída padrão), e "termios_p" é o ponteiro para uma estrutura, onde a função possa gravar os dados fornecidos pelo driver de terminal.

Para mandar essa mesma estrutura de volta ao driver, usa-se analogamente tcseattr(int fildes, int actions, const struct *termios_p);

O novo parâmetro ("actions") pode levar um dos seguintes valores:

TCSANOW mude o comportamento do terminal imediatamente
TCSADRAIN mude apenas quando não houver mais nada no buffer de saída
TCSAFLUSH idem TCSADRAIN, porém descarte qualquer dado porventura presente no buffer de entrada

IMPORTANTE: se o seu programa muda a configuração do terminal, é sua responsabilidade restaurar a configuração original antes de fechar o programa. Programas bem-feitos tal como o shell do Unix costumam fazer isso por você, mas é boa prática não contar com isso.

Para mudar a configuração do terminal, o procedimento padrão é obter a estrutura termios, alterar apenas o que for necessário, e mandá-la de volta ao driver de terminal. Dificilmente um programa cria essa estrutura do zero. O livro "GNU C Library Reference" desaconselha usar estruturas zeradas, visto que, no futuro, novos bits de controle poderão ser criados, e talvez passá-los como zero não seja uma boa idéia...

Vejamos agora o que significa cada bit de cada pedaço da estrutura termios. Os valores, especificados por símbolos, são potências de 2 que devem ser adicionados ou retirados usando-se operações AND e OR:

// para adicionar um bit
tc.c_iflags |= BRKINT;
// para retirar um bit
tc.c_iflags &= ~BRKINT;

3.3.1. Bits de entrada

Esses bits referem-se ao tratamento de caracteres recebidos do lado remoto.

BRKINTGerar uma interrupção quando um sinal de "break" é detectado na linha. (Para quem não sabe, o "break" é um sinal que pode ser mandado via porta serial, independentemente dos dados);
IGNBRK Ignora quaisquer sinais de "break";
ICRNL Converter CR recebidos em LF;
IGNCR Descartar CR recebidos;
INLCR Converter LF recebidos em CR;
IGNPAR Descartar LF recebidos;
INPCK Fazer checagem de paridade nos caracteres recebidos;
PARMRK Marcar erros de paridade;
ISTRIP Zerar o oitavo bit dos caracteres recebidos;
IXOFF Desligar o controle de fluxo por software;
IXON Ligar o controle de fluxo por software.

Para um terminal normal, é provável que seu programa vá deixar todos esses bits como estavam antes. Já para programas de comunicação serial, todos eles deverão ser desligados, exceto talvez pelo IGNBRK, que normalmente será ligado.

3.3.2. Bits de tratamento de saída

OPOST Habilitar o tratamento da saída;
ONLCR Converter LFs enviados pelo programa para o par CR+LF;
OCRNL Converter todo CR para LF;
ONOCR Desabilitar CRs se a coluna atual do cursor for 0;
ONLRET O terminal converte, por si só, LF para CR+LF.

Outros flags existem, mas referem-se basicamente à inserção de caracteres de "enchimento" (nulos) para terminais burros antigos e lentos. Salvo V. vá lidar com esse tipo de hardware, jamais terá de lidar com esses flags.

Desligar o flag OPOST tem o efeito de desabilitar qualquer tipo de interferência na transmissão de caracteres, é portanto um flag utilizável por programas de comunicação serial.

3.3.3. Bits de controle

CLOCAL Ignorar quaisquer sinais de status de modem. Importante quando a comunicação é feita por cabo serial com apenas 3 fios, como é comum em instalações de terminais "burros";
CREAD Habilitar a recepção de caracteres;
CSx (5 <= x <= 8) Usar x bits em transmissão e recepção de caracteres;
CSTOPB Usar dois "stop bits" em cada caractere, ao invés de um;
HUPCL Fazer o "hang up" do modem quando o arquivo for fechado;
PARENB Habilitar geração de bit de paridade, e a respectiva detecção;
PARODD Usar paridade ímpar ao invés de par (par é a padrão).
CRTSCTS Habilita handshaking por hardware.

3.3.4. Bits de modo local

ECHO Habilita o eco dos caracteres recebidos;
ECHOE Executa Backspace+Espaço+Backspace ao receber o caractere 'erase' (isto é: o caractere é efetivamente "limpo" no vídeo, enquanto o padrão seria apenas voltar o cursor uma coluna);
ECHOK Apaga a linha inteira ao receber o caractere 'kill';
ECHONL Ecoar caracteres LF;
ICANON Habilita o modo canônico;
IEXTEN Habilita funções específicas da implementação Unix;
ISIG Habilita sinais;
NOFLSH Normalmente, os sinais INTR, QUIT e SUSP limparão os buffers de entrada e saída. Habilitar esse bit desliga essa "feature";
TOSTOP Mandar sinal para os processos em background mediante tentativas de escrita no dispositivo de terminal.

Em programas de comunicação serial, via de regra todos esses bits serão desligados.

3.3.5. Tabela de caracteres especiais

Os caracteres especiais podem ser livremente especificados. Por exemplo, o caractere VINTR, que gera o sinal SIGINT e normalmente é igual ao Control-C (0x03), pode ser consultado/alterado da seguinte forma:

termios.c_cc[VINTR]

Segue a lista de caracteres configuráveis. Cada um é segido pelo dígito 'c' se ele é válido para o modo canônico; e 'n' se válido para o modo não canônico.

VEOF(c)		Caractere EOF (padrão=Control-D)
VEOL(c)		Caractere EOL (padrão=Control-L)
VERASE(c)	Caractere ERASE (padrão=Control-H)
VINTR(cn)	Caractere INTR (padrão=Control-C)
VKILL(c)	Caractere KILL, que deleta a linha inteira
VQUIT(cn)	Caractere QUIT
VSUSP(cn)	Caractere SUSP
VSTART(cn)	Caractere START (padrão=Control-Q)
VSTOP(cn)	Caractere STOP (padrão=Control-S)

Adicionalmente, há dois valores configuráveis para o modo não canônico:

VMIN(n) Número mínimo de caracteres que o buffer deve acumular para então enviá-los ao programa;
VTIME(n) Tempo máximo que o driver vai esperar até enviar o buffer de entrada ao programa, mesmo que a contagem de caracteres não tenha atingido VMIN. VTIME=0 significa que o driver vai esperar "ad infinitum" até coletar VMIN caracteres.

Os valores mais comumente utilizados para VMIN e VTIME são 1 e 0, respectivamente. Ou seja, o programa sempre vai receber imediatamente qualquer byte que for recebido. Porém a função read() não retorna até obter pelo menos um caractere do dispositivo...

3.4. Outras funções

3.4.1. Configuração de velocidade

Essas funções são limitadas a velocidades baixas, até 38400bps. Acima disto, deve-se procurar chamadas de sistema específicas de cada 'sabor' de Unix. Essa é a má notícia. A boa notícia é que o Linux permite o uso de velocidades mais altas por essas mesmas funções.

speed_t cfgetispeed(const struct termios *);
speed_t cfgetospeed(const struct termios *);
int cfsetispeed(struct termios *, speed_t speed);
int cfsetospeed(struct termios *, speed_t speed);

Os símbolos mais importantes para 'speed' são:

B0	   Colocar o terminal no gancho ("hang up");
B1200	   1200 baud
B2400	   2400 baud
B9600	   9600 baud
B19200	   19200 baud
B38400	   38400 baud

Daqui para frente, a portabilidade não é garantida:

B57600	   57600 baud
B115200	   115200 baud

O Linux apresenta símbolos até B4000000. A lista completa de símbolos pode ser encontrada no arquivo /usr/include/asm/termbits.h.

3.4.2. Controle do buffer

int tcdrain(int fildes);

Faz o programa aguardar até que todo o buffer de saída tenha sido transmitido.

int tcflow(int fildes, int flowtype);

O parâmetro flowtype pode assumir um dos seguintes valores:

TCOOFF	   Suspende a transmissão;
TCOON	   Reinicia a transmissão;
TCIOFF     Suspende a recepção;
TCION	   Reinicia a recepção.
int tcflush(int fildes, int in_out_selector);

Pode ser usado para limpar o buffer de entrada, de saída, ou ambos.

TCIFLUSH	Limpa o buffer de entrada;
TCOFLUSH	Limpa o buffer de saída;
TCIOFLUSH	Limpa ambos os buffers.

3.5. Se as funções POSIX não estiverm presentes

V. pode consultar o arquivo /usr/include/term.h, presente em qualquer distribuição Linux, para verificar qual o ioctl() equivalente a cada função POSIX. Segue um pequeno trecho, que prova que o mapeamento de funções POSIX para chamadas ioctl() é muito simples.

#ifndef TCSANOW
#define TCSANOW TCSETA
#endif
#ifndef TCSADRAIN
#define TCSADRAIN TCSETAW
#endif
#ifndef TCSAFLUSH
#define TCSAFLUSH TCSETAF
#endif
#ifndef tcsetattr
#define tcsetattr(fd, cmd, arg) ioctl(fd, cmd, arg)
#endif
#ifndef tcgetattr
#define tcgetattr(fd, arg) ioctl(fd, TCGETA, arg)
#endif
#ifndef cfgetospeed
#define cfgetospeed(t) ((t)->c_cflag & CBAUD)
#endif

4. Pseudo-terminais

Imagine a seguinte situação: você quer controlar o funcionamento de um programa através de outro programa. A princípio, isso seria fácil - basta passar, como entrada e saída padrões, um pipe ou um soquete TCP/IP ao programa-escravo.

Porém, boa parte dos programas exige que a entrada e saída padrões sejam terminais - caso em que a estratégia sugerida acima não funcionaria. É, por exemplo, o caso do shell.

Para cobrir essa lacuna, existem os pseudo-terminais, onde o "dispositivo" no outro lado do diagrama será outro programa:

Programa- ---> Driver de ---> Driver de  ---> Programa-
escravo   <--- terminal  <--- pseudo-tty <--- mestre

O programa-mestre pode mandar e receber dados, como se fosse um "teclado" e um "vídeo". Por outro lado, o programa-escravo trabalha na ilusão de que, por trás do driver de terminal, existe um terminal real.

Isso tudo pode estar parecendo esotérico e meio inútil, então segue a descrição de algumas situações práticas onde os pseudo-terminais são usados:

- Quando abre-se um shell dentro do ambiente gráfico. O emulador de terminal (por exemplo, o xterm) cria um pseudo-terminal e invoca o shell. O shell trabalha na ilusão de estar falando com um terminal verdadeiro; por outro lado, o emulador lê o teclado, recebe as mensagens do shell e imprime-as na janela gráfica.

- Numa conexão telnet ou ssh, o shell da máquina remota não terá como conversar diretamente com o terminal da máquina-cliente. Então, um programa intermediário (in.telnetd ou sshd) cria um pseudo-terminal e invoca um shell para ele. O processo intermediário precisa existir para repassar os dados entre o soquete TCP/IP e o pseudo-terminal.

- O pppd só aceita fazer conexões PPP através de dispositivos controlados pelo driver de terminal. Um programa externo pode oferecer um pseudo-terminal ao pppd e tunelar a conexão PPP via TCP/IP ou outro meio qualquer. Veja o VPN mini-HOWTO para mais detalhes.

- E a situação "clássica", onde precisamos controlar um programa através de outro, emulando teclado e vídeo. Não é tarefa comum, mas eventualmente pode ser preciso fazê-lo.

Pelo diagrama, note que denominamos o programa que usa o terminal, como "escravo". Isso porque, de certa forma, ele é controlado pelo programa que criou o pseudo-terminal, cognominado "mestre" no mesmo diagrama. Por associação, os respectivos arquivos de dispositivo também são chamados, de forma um pouco infeliz, de "terminal mestre" e "terminal escravo".

O pseudo-terminal precisa ser CRIADO pelo programa-mestre antes de ser utilizado pelo programa-escravo. Esse processo de criação varia um pouco em cada 'sabor' de Unix:

4.1. BSD

Mesmo os sistemas que oferecem formas mais civilizadas (e proprietárias) de criação de pseudo-terminais, oferecem o padrão BSD em paralelo, por ser o mais portável.

Observe o seguinte fragmento de código:

char PTY10[] = "pqrstuvwxyz";
char PTY01[] = "0123456789abcdef";
char devPTY[] = "/dev/ptyxx";
char devTTY[] = "/dev/ttyxx";

int buscatty()
	char* p10;
	char* p01;
	for (p10 = PTY10; *p10 != '\0'; p10++) {
		devPTY[8] = *p10;
		devTTY[8] = *p10;
		for(p01 = PTY01; *p01 != '\0'; p01++) {
			devPTY[9] = *p01;
			devTTY[9] = *p01;
			fd = open(devPTY, O_RDWR);
			if (fd >= 0) {
				fcntl(fd, F_SETFL, O_NDELAY);
				return fd;
 			}           
		}
	}
	return -1;
}

Na função buscatty(), o programa tentará abrir todos os dispositivos /dev/ptyXY, até que tenha sucesso. Quando isso acontecer, o pseudo-terminal estará habilitado. (Isso mesmo, é uma busca por tentativa-e-erro !)

Por convenção, os caracteres X e Y estarão nas faixas especificadas logo no início do exemplo. O lado "escravo" do terminal terá o nome /dev/ttyXY.

Um problema desse esquema do BSD é que deverá haver um par de arquivos /dev/{tty,pty}XX para cada pseudo-terminal disponível. Se há 256 pseudo- terminais disponíveis, então haverá 512 arquivos colaborando para tornar o /dev ainda mais lotado e ilegível.

4.2. Padrão Unix 98 (seguido pela maioria das distribuições Linux)

A partir do kernel 2.2, o Linux passou a oferecer um esquema de pseudo-terminais sugerido pelo padrão Unix 98. É preferível ao BSD por ser muito mais "limpo" e por não entupir o /dev de dispositivos ociosos. Porém, é menos portável:

char *devTTY;

int buscatty()
{
	int pty_fd = open("/dev/ptmx", O_RDWR);
	if (pty_fd >= 0) {
		devTTY = strdup(ttyname(pty_fd));
	}
	return pty_fd;
}

Se a abertura do arquivo /dev/ptmx for bem-sucedida, automaticamente o terminal-escravo será criado, e o nome do respectivo dispositivo será /dev/pts/N (N é um número inteiro não negativo).

Normalmente, o diretório /dev/pts é montado como um sistema de arquivos especial (devpts), que cria e destrói os dispositivos /dev/pts/N conforme a necessidade.

4.3. AIX

A única diferença entre o AIX e o padrão Unix 98 é que o dispositivo "fornecedor" de pseudo-terminais é /dev/ptc, e não o /dev/ptmx.

5. Bibliografia e fontes de consulta

Como bibliografia, recomendamos o excelente "Beggining Linux Programming", da Wrox Press, e o GNU C Library Reference Manual, da Free Software Foundation.

Nenhum dos dois cobre de forma suficiente os pseudo-terminais, caso em que a consulta do código-fonte dos seguintes programas pode ser de grande valia:

Outro aspecto interessante acerca dos programas citados é que eles têm grande preocupação com portabilidade.