di Alessandro Rubini
Riprodotto con il permesso di Linux Magazine, Edizioni Master. In queste pagine si spiega cosa sono gli pseudo-terminali (pty) e si mostrano le interfaccie di programmazione esportate dal kernel.
Gli pseudo-terminali, o pty (pseudo-tele-type, in memoria delle vecchie telescriventi), sono un componente estremamente importante dei sistemi Unix e compatibili. Lo pseudo-terminale è il meccanismo alla base di xterm, kterm e tutti gli altri emulatori di terminale (appunto); come pure di sshd, rshd, telnetd e qualunque altro sistema di login remoto; vengono anche usati da emacs in sh-mode e in altre situazioni. Il codice presentato è stato provato su Linux-2.6.5, ma in questo ambito non ci sono cambiamenti rispetto al 2.4.
In ambiente Unix un «terminale», o tty, è un dispositivo attraverso cui l'utente interattivo può dialogare con un interprete di comandi o altri programmi. Il termine viene usato indifferentemente per indicare il dispositivo fisico composto da tastiera e monitor (si pensi ai vecchi terminali seriali VT100 o VT320) o per indicare il file speciale che viene usato dalle applicazioni per comunicare con la periferica. Poiché i VT100 sono da tempo estinti i VT320 sono una specie a rischio, oggigiorno quando si parla di «terminale» ci si riferisce quasi sempre al concetto astratto di terminale o ad un programma che svolge le stesse funzioni, che è perciò detto «emulatore di terminale».
Un esempio di terminale è la porta seriale (come /dev/ttyS0 o /dev/ttyUSB0), un altro esempio è il "terminale virtuale" della console di testo (/dev/tty1, /dev/tty2 eccetera), un altro ancora è la finestra di xterm o di un programma equivalente (/dev/ttyp0 o /dev/pts/0).
In tutti questi casi, i file speciali in /dev offrono ai processi che vi accedono le funzionalità specifiche di un terminale: i parametri di termios (terminal input/output settings), ovvero tutti quei noiosi ma importanti particolari relativi alla velocità di trasmissione, parità, convenzioni sulle andate a capo e quant'altro, ma anche l'assegnamento di alcune funzionalità speciali ad alcuni caratteri. Si può simulare un fine-file tramite <ctrl-D> o uccidere un processo tramite <ctrl-C> solo perché questi caratteri raggiungono un file speciale associato ad un terminale. Ogni utente può scegliere quali siano i caratteri speciali e nessuno di questi ha un significato speciale al di fuori del contesto di terminale. <!-- a differenza di quanto accade in un noto sistemi operativo obsoleto -->
Per leggere o modificare la configurazione termios di un terminale, si possono usare le funzioni di libreria tcgetattr e tcsetattr (terminal control get/set attributes) o il comando stty (set tty), ricordando che il comando agisce sul suo standard input:
In entrambi i casi la configurazione viene letta o scritta dal
kernel tramite la chiamata di sistema ioctl, con i comandi
burla% stty
speed 38400 baud; line = 0; erase = ^H; -brkint -imaxbel
burla% stty < /dev/ttyS0
speed 9600 baud; line = 0; -brkint -imaxbel
burla% stty < /etc/passwd
stty: standard input: Inappropriate ioctl for device
TCGETA
, TCSETA
o altri della stessa famiglia,
la cui implementazione è nel file drivers/char/tty_io.c.
Mentre un terminale come /dev/ttyS0 è chiaramente associato ad una periferica (la porta seriale) cui può essere collegato un terminale fisico, il dispositivo associato alla finestra di xterm non permette di controllare alcuna periferica hardware e i dati scambiati tra l'interprete di comandi e il file speciale in /dev vengono gestiti da un altro processo sulla stessa macchina, xterm appunto.
In tutte le situazioni in cui occorre eseguire un processo all'interno dell'astrazione "terminale" senza far uso di una vera interfaccia hardware, ci si appoggia al meccanismo degli pseudo-terminali, meccanismo secondo il quale ad ogni pty è associato un altro file speciale che si comporta come se fosse l'altra estremità del cavo seriale. I due, insieme, si chiamano "coppia di pseudo-terminali" o più semplicemente "coppia di terminali", tty pair.
I due componenti della coppia si comportano come una pipe bidirezionale e vengono definiti master e slave. Il comportamento dei file speciali associati non è però completamente simmetrico come succede per i descrittori di file associati a pipe e socket: il terminale slave è un vero e proprio terminale, ma può essere aperto solo dopo il master associato; il terminale master invece può essere aperto una volta sola e non si comporta esattamente come un terminale (per esempio, non può essere aperto più di una volta).
Riquadro 1 - Apertura di una coppia di terminali
|
Storicamente, gli pseudo terminali, master e slave, esistevano nella
directory /dev, dove si trovano ancora oggi, almeno in alcune
distribuzioni. Qualora mancassero, si possono creare invocando
il comando "/dev/MAKEDEV pty
". I terminali slave hanno
major numnber 3 e i loro nomi sono per esempio /dev/ttyp0; i terminali
master hanno major number 2 e i loro nomi sono come /dev/ptyp0,
dove tutte le p
indicano "pseudo".
Il codice per gestire queste periferiche è opzionale nel kernel, e
viene abilitato dalla voce CONFIGLEGACYPTYS
.
I nomi dei file speciali associati a ciascuna coppia di terminali
differiscono negli ultimi due caratteri, ciascuno dei quali può
assumere uno di 16 valori, per un totale di 256 coppie. Il semplice
programma legacy.c, nel riquadro 1, mostra la classica procedura di
apertura di una coppia di terminali, cercando il primo master
disponibile e poi aprendo lo slave associato. Se un master è già in
uso, open ritorna EIO
e il ciclo continua; se il supporto non è
abilitato nel kernel, la prima open ritornerà ENODEV
; se non
esistono i file speciali in /dev la prima open ritornerà
ENOENT
e in entrambi i casi il ciclo termina.
Il comportamento del programma può essere osservato con strace.
Un programma che usa i terminali per svolgere qualche compito, come xterm o sshd dovrà naturalmente fare altre operazioni, come cambiare il proprietario e i permessi di accesso al file speciale perché rispecchi l'utente che ne ha preso il controllo e le sue preferenze (si veda "man mesg", per esempio).
I meccanismo con coppie di file appena descritto ha però alcuni problemi no trascurabili: il processo che apre una sessione deve essere privilegiato (per cambiare il proprietario del terminale), l'assegnazione del terminale non è atomica e dà luogo a corse critiche, la scansione dei dispositivi per trovarne uno libero può introdurre ritardi indesiderati.
Infine, 512 file speciali in /dev sono spesso di impiccio, e questo è normalmente un problema con le macchine embedded. Per esempio, il sistema di sviluppo del processore Etrax viene distribuito con la directory /dev su un dispositivo in sola lettura, dove sono state create soltanto tre coppie di terminali, per non sprecare il limitato spazio a disposizione; di conseguenza il server telnet non può accettare più di tre utenti contemporaneamente e non è possibile usare la macchina per fare esercitazione a più di tre studenti per volta, a meno di non riprogrammare la memoria flash con una versione personalizzata del sistema.
Alcuni problemi relativi ai terminali virtuali sono stati risolti semplicemente osservando che il master viene aperto una sola volta; è cioè possibile implementare un solo file speciale per gestire tutti i master, facendo sì che il kernel, una volta aperto il file, lo associ ad uno specifico terminale master; un po' come /dev/tty che ha un significato diverso in base al contesto in cui viene aperto. Il processo che ha aperto il terminale master potrà poi chiedere il nome del terminale slave associato. Non è invece possibile unificare tutti i terminali slave in un solo file, perché altri processi devono poter aprire i terminali slave in uso. È così che funzionano i programmi della famiglia di talk, che ritengo ancora preferibile a IRC in alcuni contesti, e altri servizi asincroni per l'utente testuale.
Questo approccio è stato ratificato nello standard ``Unix98'', che ha definito una serie di funzioni per aprire e configurare i terminali. L'uso di tali funzioni nasconde i dettagli di ogni singola implementazione, come i nomi dei file speciali da usare o il meccanismo usato per cambiare il proprietario del terminale slave (spesso tale meccanismo è un processo setuid apposito).
Nel sistema GNU/Linux è stato implementata questa infrastruttura ma si
è andati oltre: invece di usare file speciali statici in /dev per i
terminali slave, si è creato un filesystem apposito in modo che sia il
kernel stesso a rendere visibili i terminali slave in risposta
all'accesso al terminale master, evitando che l'integratore di sistema
debba scegliere tra occupare prezioso spazio su disco o limitare
arbitrariamente il numero di sessioni. L'implementazione del
filesystem, tra codice e dati, è meno di 10kB e viene abilitata
dall'opzione CONFIG_UNIX98_PTY
.
|
L'apertura e la configurazione di una coppia di terminali secondo lo standard Unix98 vengono effettuate attraverso le funzioni di libreria getpt, grantpt, unlockpt, ptsname, di cui è interessante leggere le pagine del manuale. Il programma open.c, nel riquadro 2, mostra invece un'approccio di più basso livello che sfrutta direttamente i meccanismi di Linux (il kernel) a discapito della portabilità.
In questo caso, il terminale master è chiamato /dev/ptmx
(pseudo-tty multiplexer) e i terminali slave risiedono nella directory
/dev/pts, dove viene montato il filesystem
devpts. I due comandi ioctl utilizzati nel programma
di esempio sono usati per chiedere al
sistema il numero dello slave da aprire (TIOCGPTN
, Terminal IOCtl
Get PTy Number) e per sbloccarlo, autorizzando quindi
l'accesso allo slave (TIOCSPTLCK
, Terminal IOCtl Slave PTy LoCK).
Il terminale slave scompare automaticamente dal filesystem
quando si termina di usarlo; se eseguite open sotto strace vedrete che
il programma apre un terminale slave che non vedrete più nel
filesystem una volta finita l'esecuzione.
Il filesystem devpts era già disponibile in Linux-2.2 ed è praticamente immutato nella versione 2.6, se non per l'aggiunta degli attributi estesi, una funzionalità disponibile nei principali filesystem ma ad di fuori dei nostri interessi in questo numero. Normalmente il filesystem devpts viene montato durante la procedura di avvio della distribuzione, anche se non presente in /etc/fstab. È possibile smontare /dev/pts solo dopo aver chiuso tutti gli pseudo-terminali in uso; il sistema continuerà a funzionare con il meccanismo precedente (a patto di avere i file speciali in /dev e il supporto relativo nel kernel). È sempre possibile rimontare /dev/pts, facendo convivere i due sistemi:
burla% who
rubini ttyp1 Apr 6 09:43 (ostro.i.gnudd.com)
rubini ttyp2 Apr 6 09:43 (ostro.i.gnudd.com)
rubini pts/16 Apr 6 09:43 (ostro.i.gnudd.com)
|
Il programma openpty.c, nel riquadro 3, apre una coppia di terminali ed esegue un interprete di comandi all'interno dello slave. Per semplificare e accorciare il codice sono state usate le funzioni openpty e login_tty. Queste funzioni fanno parte di libutil: non fanno rigorosamente parte di libc ma sono state rese disponibili per evitare che ogni applicativo debba reimplementarsele. Il Makefile che ho usato, perciò, è composto solo di queste due righe:
Una volta aperta la coppia di terminali, il programma crea un processo
figlio al quale viene assegnato il nuovo terminale slave come
terminale di controllo, prima di eseguire sh. Il processo padre,
invece, si occupa di copiare il suo stdin verso il terminale master
e tutto quello che esce dal terminale master sul suo stdout.
CFLAGS = -ggdb -Wall
LDFLAGS = -lutil
Figura 1
openpty e la shell figlia
La figura è anche disponibile
in PostScript
La situazione risultante è quella rappresentata in figura 1, in cui le frecce entranti e uscenti dai processi rappresentano i file standard di input e output mentre la riga blu uscente da openpty rappresenta il file aperto verso il terminale master. Poiché però standard input e output di openpty staranno probabilmente girando all'interno di un altro terminale (la console, xterm o, come nel mio caso, rshd -- a sua volta controllato da rsh dentro ad un xterm), occorre configurare il terminale ospite per permettere l'uso interattivo (un carattere alla volta) dell'interprete di comandi invocato nel nuovo terminale slave; a questo fine è stato usato il comando stty presentato precedentemente.
openpty funziona sia con i terminali legacy sia con devpts, in quanto la funzione di libreria usa il codice mostrato in legacy.c se fallisce con il metodo standard di Unix98:
burla% tty
/dev/ttyp0
burla% ./openpty
sh-2.05a$ tty
/dev/ttyp1
sh-2.05a$ exit
exit
burla% sudo mount -t devpts none /dev/pts
burla% ./openpty
sh-2.05a$ tty
/dev/pts/7
sh-2.05a$ exit
burla% tty
/dev/ttyp0
Gli pseudo-terminali si prestano anche ad usi non convenzionali, sfruttando la loro completa equivalenza, a livello software, con una porta seriale, ovvero con un modem.
Il protocollo PPP (ma anche SLIP) è implementato da una "disciplina di linea", un modulo software che può essere usato su qualsiasi tipo di terminale. La disciplina di linea serve appunto a disciplinare il comportamento del sistema in risposta ai dati che raggiungono il kernel tramite quel terminale, oltre a permettere l'invio di dati verso il terminale. Una discussione approfondita della discplina di linea si può trovare nell'articolo disponibile in rete come www.linux.it/kerneldocs/serial/ .
Ma se PPP può lavorare su qualsiasi terminale, allora è possibile fare un collegamento IP punto-punto tra due macchine remote, a patto di poter creare una coppia di terminali in ognuna di esse. In figura 2 è rappresentato il modo per realizzare tale collegamento instradando il protocollo dentro un canale ssh, invece che su una porta seriale come normalmente si fa con PPP. Ognuno dei due pppd viene fatto comunicare con un terminale slave, che per il software è indistinguibile da un modem, e i due processi ssh, client e server, si occupano di fare da ponte tra il terminale master e il canale cifrato su protocollo IP.
Figura 2
Situazione con ppptunnel
La figura è anche disponibile
in PostScript
Il codice per realizzare tale struttura di processi è riportato nel riquadro 4, questa volta scritto in linguaggio ettcl (una versione di Tcl modificata per poter fare da motore del sistema embedded EtLinux). Su internet si trova una versione di pppptunnel scritta in Perl ed è immediato riscrivere lo strumento in qualsiasi linguaggio sia in grado di aprire una coppia di terminali; la scelta del linguaggio non influsice minimamente sulle prestazioni perché il programma deve solo collegare i file descriptor e passare il controllo ai due pppd, in un caso tramite ssh.
Riquadro 4 - Un tunnel PPP scritto in EtTcl
|
Il compito di ppptunnel è quello di aprire una coppia di terminali
(nel "local tty subsystem" in figura) e chiamare fork. Il processo
figlio chiude il terminale master ed esegue pppd sul terminale
slave; il processo padre chiude il terminale slave ed esegue ssh,
specificando in linea di comando di eseguire pppd sulla macchina
remota dopo aver aperto un terminale di controllo (cioè un'altra
coppia di pseudo-terminali, specificando -t
).
Per l'esecuzione del comando nella forma in cui è riportato conviene che l'utente locale sia autorizzato dall'host remoto e che sudo possa funzionare senza password su entrambe le macchine, ma è possibile digitare le password relative ad ssh e al sudo locale sul terminale in cui viene invocato ppptunnel; è invece necessario che il sudo remoto non chieda una password. A livello kernel, questo collegamento si appoggia sui normali moduli di PPP: ppp_generic, ppp_async e i moduli di compressione.