Logo  

CS471/571 - Operating Systems

Virtual Terminals

Some programs require a terminal in order to function (such as an interactive shell, the passwd program, etc.) We may want to "pipe" data to and from such programs, but we cannot use a simple pipe that replacing stdin/out/err for such programs. To control the input and output of such programs we must use a pseudo-terminal which acts very much like a bi-directional pipe but with a virtual TTY for the program(s) on the other end.

The Pseudo-terminal interface:

  • A following steps are required to open a pseudo terminal:

    1. Get/save our preferred TTY line discipline and window size.

    2. Open the pty (pseudo-terminal) master device using posix_openpt(3).

    3. Call grantpt(3) to get a "slave" device.

    4. Call unlockpt(3) to unlock the slave device.

    5. Use ptsname(3) to learn the slave device name (ex: "/dev/pts/2")

    6. Fork a new process:

      A) In the child:

      a) Open the slave TTY using open()
      b) Make the opened slave the stdin/out/err of the new process.
      c) Apply the saved TTY disciplines/window size from step 0.
      d) Execve() the new program.

      B) In the parent:

      a) Read and write on the master device descriptor to communicate with the child process, alasocketpair()`.

The posix_openpt(3) function call:

  #define _XOPEN_SOURCE 600
  #include <stdlib.h>
  #include <fcntl.h>

  int posix_openpt(int flags);
  • Opens an unused pseudo-terminal master device (/dev/ptmx), returning a valid file descriptor on success or -1 on failure.
    flags:
    O_RDWR Open device for reading and writing
    O_NOCTTY Don't make the new pty the controlling terminal for this process.

Example:

int pty = posix_openpt(O_RDWR | O_NOCTTY);

The grantpt(3) function call:

  #define _XOPEN_SOURCE 500
  #include <stdlib.h>

  int grantpt(int fd);
  • Changes the mode (0620) and owner of the slave device to that of the current processes uid. Returns 0 on success, -1 on failure.

Example:

grantpt(pty);

The unlockpt(3) function call:

  #define _XOPEN_SOURCE 500
  #include <stdlib.h>

  int unlockpt(int fd);
  • "Unlocks" the slave side of the pseudo-terminal. Returns 0 on success, -1 on failure. I'm not sure why this function exists, seems to make it possible to open the slave device for writing to in the child process.

Example:

unlock(pty);

The ptsname(3) function call:

  #define _XOPEN_SOURCE 500
  #include <stdlib.h>

  char *ptsname(int fd);
  • Returns the name (such as "/dev/pts/2") of the slave pty device that corresponds to the master pty. Use the returned name to open the pty in the child and attach it to stdin/out/err.

  • Returns a string or NULL on error.

Example:

char *slave_name = ptsname(pty);

The tcgetattr(3) and tcsetattr(3) function calls:

  #include <termios.h>
  #include <unistd.h>

  int tcgetattr(int fd, struct termios *termios_p);

  int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
  • Get or set terminal line disciplines.

  • optional_actions can be one of:

    TCSANOW Change things immediately.
    TCSADRAIN Change after everything has been written.
    TCSAFLUSH Change after everything is written and anything not read is flushed.
  • Returns 0 on success, -1 on error.

Example:

    struct termios save;

    tcgetattr(STDIN_FILENO, &save);
    ...
    tcsetattr(slave_pty, TCSANOW, &save);

Getting the "window" size of the terminal (width/height):

The ioctl(2) system call:

  #include <sys/ioctl.h>

  int ioctl(int fd, unsigned long request, ...);
  • Makes some request w/ respect to the given file descriptor. There are many ioctls. Returns 0 on success, or -1 on error.

  • Additional parameters after the request may be required.

    Requests:
    TIOCGWINSZ Get the window size (stored in a struct winsize pointer)
    TIOCSWINSZ Set the window size
   struct winsize {
     unsigned short ws_row;
     unsigned short ws_col;
     unsigned short ws_xpixel;   /* unused */
     unsigned short ws_ypixel;   /* unused */
   };
  • A SIGWINCH signal will be sent to the process when the window size changes.

Example:

     struct winsize ws;
     ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
     ...
     ioctl(slave_pty, TIOCSWINSZ, &ws);

pty.c

#define _XOPEN_SOURCE 600

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <errno.h>

#define K   1024

// man pty

int main(int argc, char *argv[])
{
  struct termios tio;
  struct winsize ws;
  pid_t pid;
  int pty, tty, r;
  char *pts, buf[K];

  // Add a signal handler to handle things like SIGCHLD, maybe SIGWINCH

  // Get line attributes from our TTY:
  if (tcgetattr(STDIN_FILENO, &tio) < 0) {
    perror("tcgetattr");
    exit(1);
  }
  // Get the window size from our TTY:
  if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) {
    fprintf(stderr,"Getting winsize\n");
    exit(1);
  }
  // Open master end of the PTY:
  if ((pty = posix_openpt(O_RDWR | O_NOCTTY)) < 0) {
    perror("openpt");
    exit(1);
  }
  // The remaining can probably be done in the child process.
  // Allocate a slave terminal to us:
  if (grantpt(pty) < 0) {
    perror("grantpt");
    exit(1);
  }
  // Unlock and set permissions on slave terminal side:
  if (unlockpt(pty) < 0) {
    perror("unlockpt");
    exit(1);
  }
  // Find out what the device name of the slave side pseudo terminal is:
  if ((pts = ptsname(pty)) == NULL) {
    perror("ptsname");
    exit(1);
  }

  pid = fork();
  if (pid < 0) {
    perror("fork");
    exit(1);
  }
  if (pid == 0) {
    // Open slave side PTY:
    if ((tty = open(pts, O_RDWR)) < 0) {
      perror("slave open");
      exit(1);
    }
    dup2(tty, STDIN_FILENO);
    dup2(tty, STDOUT_FILENO);
    dup2(tty, STDERR_FILENO);
    close(tty);
    // Set line attributes (immediately):
    if (tcsetattr(STDIN_FILENO, TCSANOW, &tio) < 0) {
      perror("tcsetattr");
    }
    // Set the window size:
    if (ioctl(STDIN_FILENO, TIOCSWINSZ, &ws) < 0) {
      fprintf(stderr,"winsize\n");
    }
    execlp("arogue", "arogue", NULL);
    perror("execlp");
    exit(1);
  }
  // In the parent process:

  // Add support for async I/O on keyboard and from the master pty:
  while((r = read(pty, buf, K)) > 0) {
    write(STDOUT_FILENO, buf, r);
  }
  exit(0);
}

Enabling/disabling keyboard "echo" functions:

/**
 * Termios are documented in man 3 tcgetattr
 */
void noecho()
{
  struct termios t;

  tcgetattr(STDIN_FILENO,&t);
  // Turn off:
  // ICANNON    - canonical mode (line buffered/processed input)
  // ECHO       - echoing characters as we type them
  // ISIG       - Send signals when special keys are typed (Ctrl-C, Ctrl-Z, etc)
  // IEXTEN     - Enable line editing
  t.c_lflag &= ~(ICANON|ECHO|ISIG|IEXTEN);
  // Set MIN minimum number of characters to block read on and TIME maximum
  // amount of time to wait for input.
  t.c_cc[VMIN] = 1;
  t.c_cc[VTIME] = 0;
  tcsetattr(STDIN_FILENO,TCSANOW,&t);
}

void echo()
{
  struct termios t;

  tcgetattr(STDIN_FILENO, &t);
  t.c_lflag |= (ICANON|ECHO|ISIG|IEXTEN);
  tcsetattr(STDIN_FILENO, TCSANOW, &t);
}