next up previous contents
Next: Funkcje pomocnicze Up: Zaawansowane programowanie gniazd Previous: Współpraca z Inetd   Contents

Pseudoterminale

Wiele programów interaktywnych odmówi działania bądź też będzie działać nieprawidłowo jeśli na standardowym wejściu/wyjściu nie będzie znajdował się terminal. Programy te można zmusić do działania oferując im tzw. pseudoterminale. Urządzenia takie emulują zachowanie prawdziwego terminala przy użyciu prawie dowolnych strumieni danych, w szczególności pary połączonych gniazd.

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 );
  
Przeznaczeniem funkcji jest znalezienie wolnego pseudoterminala. Najprostszym sposobem wywołania jest:
  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:
1.
znajduje wolny pseudoterminal
2.
tworzy nowy proces
3.
terminalem kontrolującym tego procesu czyni część slave pseudoterminala
4.
potomek zamyka część master
5.
rodzic zamyka część slave
6.
rodzic otrzymuje deskryptor części master
Jak widać jest to swoiste wash&go. Pierwszy etap działania tej funkcji dokładnie odpowiada temu, do czego służy samo openpty(). Znaczenie wartości zwracanej przez omawianą funkcję jest takie samo jak dla fork().

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 su
  
Potem:
  # 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.


next up previous contents
Next: Funkcje pomocnicze Up: Zaawansowane programowanie gniazd Previous: Współpraca z Inetd   Contents
Paweł Niewiadomski
2000-10-17