Na pseudoterminal składają się w istocie dwa urządzenia: master i slave. W systemach BSD część master pseudoterminala ma nazwę /dev/ttyXY, gdzie X to [a-z], a Y to [0-9a-f]. Z kolei część slave ma nazwę /dev/ptyXY, gdzie X i Y są takie same, jak w odpowiadjącej jej części master. Linux wspiera taki sposób nazywania pseudoterminali ale umożliwia także sposób alternatywny w stylu Unix98. W tym przypadku proces chcący skorzystać z pseudoterminala otwiera plik /dev/ptmx (pseudo-terminal multiplexer) i jest mu przydzielany pierwszy wolny pseudoterminal. Jego część slave ma nazwę /dev/pts/NUMER.
Brzmi to może nieco skomplikowanie ale niezależnie od stylu nazewnictwa zasada działania pseudoterminali jest taka sama. Dane zapisywane po stronie master są odczytywane po stronie slave i odwrotnie. Części slave pseudoterminala przypisuje się rolę terminala kontrolującego procesu, który ma się komunikować za pośrednictwem gniazd.
Do zarezerwowania pseudoterminala służą dwie funkcje (pty.h) znajdujące się w bibliotece libutil.so:
#include <pty.h>
int openpty (int *master, int *slave, char *name, struct termios *termp,
struct winsize *winp );
openpty (&master, &slave, &name, NULL, NULL);W wyniku działanie tej funkcji otrzymamy deskryptory obu części pseudoterminala, a w name dodatkowo znajdzie się nazwa części slave.
Drugą, chyba znacznie częściej wykorzystywaną funkcją jest:
pid_t forkpty (int *master, char *name, struct termios *termp, struct winsize *winp);Znaczenie poszczególnych parametrów jest analogiczne, jak w openpty(). Funkcja forkpty() działa mniej-więcej tak:
Jeśli ktoś nadal nie ma pojęcia, jak to wszystko poskładać w całość niech dokładnie przeanalizuje przykład, który za chwilę przedstawimy. Na początek warto się przekonać, że niektóre programy nie uruchomią się bez terminala na wejściu/wyjściu. Dodajmy taką linijkę do /etc/inetd.conf:
ipx stream tcp nowait nobody /bin/su suPotem:
# killall -HUP inetd # telnet 0 213 Trying 0.0.0.0... Connected to 0. Escape character is '^]'. standard in must be a tty Connection closed by foreign host. #Rzeczywiście su okazuje się dosyć wybredne. Dlatego spróbujemy pokazać, jak oszukać su "podstawiając" mu pseudoterminal.
#include <stdio.h>
#include <pty.h> /* forkpty() */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
main ()
{
int pid, /* PID potomka */
mast, /* deskryptor części master */
n;
char ptyname[255]; /* nazwa części slave */
char buf[255]; /* bufor pośredniczący */
fd_set rfd; /* zbiór deskryptorów */
pid = forkpty (&mast, ptyname, NULL, NULL);
Wywołaliśmy funkcję forkpty(). W zależności od tego, co zwróciła mamy trzy
różne możliwości:
switch (pid)
{
case -1: /* błąd */
{
perror ("forkpty()");
exit (1);
}
Wywołanie forkpty() zakończylo się błędem. Najczęstszą przyczyną tego jest
brak pamięci do stworzenia nowego procesu albo brak wolnych pseudoterminali.
case 0: /* dziecko */
{
printf ("Dziecko.\n");
fflush (stdout);
execl ("/bin/su", "su", NULL);
}
Jeśli pid==0 to znaczy, że działamy w kontekście potomka. Dzięki funkcji
forkpty() wszystkie trzy standardowe strumienie (stdin/stdout/stderr) potomka
są podłączone do części slave pseudoterminala. Oczywiście to samo będzie
dotyczyło programu uruchomionego przez execl().
Cała reszta kodu będzie wykonywana przez rodzica:
default: /* rodzic */
printf ("Rodzic. Potomek na %s\n", ptyname);
fflush (stdout);
}
Deskryptory rodzica prezentują się tak:
fcntl (0, F_SETFL, O_NONBLOCK);
fcntl (mast, F_SETFL, O_NONBLOCK);
Zarówno standardowe wejście (gniazdo), jak i część pseudoterminala przełączamy
w tryb nieblokujący.
FD_ZERO (&rfd);
FD_SET (0, &rfd);
FD_SET (mast, &rfd);
Oba deskryptory będziemy monitorować pod względem możliwości odczytu
w tej oto pętli:
while (1)
{
FD_SET (0, &rfd);
FD_SET (mast, &rfd);
n = select (mast + 1, &rfd, NULL, NULL, NULL);
Funkcja select() oczekuje na możliwość odczytu nowych danych z któregoś z
dwóch deskryptorów. Zastanówmy się, jakie dane będą odczytywane z tych
deskryptorów. Standardowe wejście (0) jest podłączone do zdalnego klienta -
będą się więc tutaj pojawiały polecenia wydawane przez klienta. Tutaj też
muszą być kierowane odpowiedzi procesu uruchomionego przez potomka. Z kolei
z części master będziemy odczytywali to, co potomek będzie kierował do części
slave pseudoterminala, czyli wszelkie komunikaty wypisywane przez program
/bin/su. Tutaj też muszą trafiać polecenia klienta - "wypłyną" one po stronie
klienta na standardowym wejściu programu /bin/su. Wniosek z tego taki, że
rolą procesu-rodzica jest przekazywanie danych (forwarding) pomiędzy
deskryptorami 0 i mast.
if (n > 0)
{
if (FD_ISSET (0, &rfd)) /* klient wydał jakieś polecenie, trzeba */
/* je przekazać dla procesu-potomka */
/* (czyli dla programu /bin/su) */
{
bzero (buf, sizeof (buf));
read (0, buf, sizeof (buf));
write (mast, buf, strlen (buf));
}
if (FD_ISSET (mast, &rfd)) /* potomek (/bin/su) wyświelił jakiś */
/* komunikat, przekazujemy go zdalnemu*/
/* klientowi */
{
bzero (buf, sizeof (buf));
read (mast, buf, sizeof (buf));
write (0, buf, strlen (buf));
}
}
}
}
Teraz kompilujemy program (z parametrem -lutil) i próbujemy od nowa wstawiając go zamiast /bin/su do /etc/inetd.conf. Su powinno się uruchomić bez problemów. Efekt napewno nie będzie tak dobry jak na prawdziwym terminalu. Można jednak znacznie poprawić rezultaty. Trzeba zmienić charakterystykę pseudoterminala dokładnie w taki sam sposób, jak się to robi z prawdziwymi terminalami. To jest jednak oddzielny temat - man 3 termios.
Jeśli będziemy na serio tworzyć program używający pseudoterminali to należy pamiętać aby zainstalować jeszcze obsługę SIGCHILD. Pozwoli to na uniknięcie procesów zombie i jednocześnie umożliwi zakończenie procesu-rodzica zaraz po tym, jak potomek zakończy pracę. Tego brakuje w przykładowym kodzie: jeśli zakończymy sesję su to połączenie telnetowe nie zostanie automatycznie przerwane ponieważ rodzic znajduje się w pętli bez wyjścia (while(1)) nie zdając sobie sprawy, że spełnił już swoje zadanie.