CSE 271 Lab 11: Terminals

0. Announcements

1. Overview

We saw how devices were similar to files, but that we could control other characteristics of them. Today we will focus on better handling of input to your program by manipulating terminal characteristics.

2. Re-visitting controlling the echo to the screen.

As it turns out, I made two mistakes in class on Monday. The first was trying this on tcsh instead of bash. tcsh has the ability to mark a subset of tty settings that cannot be changed. And echo was on that list. For tcsh, you first have to use setty to mark echo as changeable. Once you do that, then stty -echo will turn off echo in the shell, and stty echo will turn it back on (both in tcsh and in bash).

Secondly, I believe I tried running the little program without arguments, which, if you look at the code for Setecho.c (slide 13/16), you'll see that the program silently quits. If, instead, you use a single argument beginning with y, you'll see that echoing is turned off; anything not beginning with y will turn off echoing. Works just fine if you are running bash (or figure out the problem described above with tcsh).

3. User Programs

Most of the UNIX utilities we have seen so far just read and write to stdin and stdout respectively (or to optional files). Examples include who, ls, grep, etc. You've shown that you are able to string them together, sending the output of one as the input into another.

There are many programs, however, that are intended to be used by interactive users, not other programs. These include editors like emacs and vi, email clients like pine and elm, web browsers such as lynx, and pagers like more and less. Such programs work only with a user at a keyboard and screen.

Such user programs are concerned with

Today we will consider how to address each of these concerns.

Compile and run the following simple program.

// rotate.c : map a->b, b->, ... z->a

#include <stdio.h>
#include <ctype.h>

int main() {
  int c;
  while ( (c=getchar() ) != EOF) {
    if (c=='z')
      c = 'a';
    else if (islower(c))
      c++;
    putchar(c);
  }

  return 0;
}

It is intended to read a single character at a time, change it, and output it. Notice how you can go back and remove or change input characters before the program processes them -- that in fact, it needs to wait until you press return before processing. Also, that if you press ctrl-c after typing a few characters, the program will quit before processing the characters already typed. Finally, characters appear on the screen as you type them. All this, as we discussed on Monday, is handled by the terminal driver.

Exercise 1. Try using stty to control the terminal and run rotate again. icanon is the flag that determines editing (canonical input). min 1 says that we want to read at least one byte at a time (the default is often higher, such as four). -echo disables the automatic display of typed letters. For example:
   $ stty -icanon min 1; ./rotate
and
   $ stty -icanon min 1 -echo; ./rotate
Note that after running the second one, you may need to type stty echo to turn echoing back on in your shell! This last one has most of the behaviors we want in a more realistic program (except for handling the ctrl-c character properly).

4. Editing and Buffering

So lets revise rotate.c to eliminate editing and buffering.

Exercise 2. Revise the rotate.c to include:
  #include <termios.h>
and just after int c; at the beginning of main() add:
  struct termios original_mode;
  struct termios ttystate;

  // record initial terminal settings for later use
  tcgetattr(0, &original_mode);

  // change settings
  tcgetattr( 0, &ttystate);               /* read curr. setting   */
  ttystate.c_lflag        &= ~ICANON;     /* no buffering         */
  ttystate.c_cc[VMIN]     =  1;           /* get 1 char at a time */
  tcsetattr( 0 , TCSANOW, &ttystate);     /* install settings     */
Finally, before returning at the end, add
  // restore initial terminal settings
  tcsetattr(0, TCSANOW, &original_mode); 
Now compile and run this modified version -- it has turned off editing of the buffer, and as set the buffer size to be just one character. The program is now processing each character as it is typed.

5. Echoing

The previous version of rotate.c still showed the keys as they were pressed.

Exercise 3. Add the appropriate line to rotate.c to disable echoing as well. Examine the man page for termio to find the right flagname.

Note that if your program quits without restoring the terminal settings, your terminal may be in a bad state and you'll need to type stty echo again or possibly stty sane.

6. Non-blocking input

Our current version of rotate.c works well. However, it will wait forever until it receives input. Sometimes, the absence of input is a form of input as well. Fortunately, the terminal driver has the ability to stop waiting after a certain amount of time (specified in units of .1 seconds).

Exercise 4. Compile and run the following version of rotate.c:
// rotate.c : map a->b, b->, ... z->a

#include <stdio.h>
#include <ctype.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
  int c;
  struct termios original_mode;
  struct termios ttystate;

  // record initial terminal settings for later use
  tcgetattr(0, &original_mode);

  // change settings
  tcgetattr( 0, &ttystate);               /* read curr. setting   */
  ttystate.c_lflag        &= ~ICANON;     /* no buffering         */
  ttystate.c_lflag        &= ~ECHO;       /* no echo              */
  ttystate.c_cc[VMIN]     =  0;           /* no minimum # of chars*/
  ttystate.c_cc[VTIME]     =  10;         /* wait up to 1 second  */
  tcsetattr( 0 , TCSANOW, &ttystate);     /* install settings     */

  while ( 1 ) {
    c=getchar();
    if (c==EOF)
      putchar('.');
    else {
      if (c=='Q')
        break;
      else if (c=='z')
        c = 'a';
      else if (islower(c))
        c++;
      putchar(c);
    }
  }

  // restore initial terminal settings
  tcsetattr(0, TCSANOW, &original_mode); 

  return 0;
}
Note the change to VMIN, the addition of VTIME, and the special character 'Q' to exit the program (since EOF will now be returned after a timeout).

7. Handling interruptions (signals)

The final thing we wish to handle is an interruption -- when a user types ctrl-c (break) in a typical program, the process just halts without performing any cleanup. We wish to be able to clean up before quitting -- restore the terminal, close relevant files, etc.

What is a signal? It's a one-word message that is delivered by the OS to the program. Some signals, like the KILL signal, are actually processed by the OS, and so the application cannot receive them. See man signal.h or kill -l for a list of available signals. CTRL-C is usually mapped to the interrupt signal (SIGINT). Other signals include floating point exceptions (e.g., if you divide by zero) and segmentation faults. You can also send user-defined signals to processes.

A process can tell the kernal how it wants to respond to signals using signal(3c). It can

  1. accept the default (usually process death)
  2. ignore the signal -- call signal(SIGINT, SIG_IGN);
  3. ask that a function be run -- call signal(SIGINT, func_name);
In general, signal() tells the OS what to do for a particular signal.

Exercise 5. Modify your most recent rotate.c to add a signal handler to catch the INT signal (usually sent via CTRL-C), print the string "Exiting...\n" and do clean-up activities before exit()ing.

If you finish early, you might want to work on the current homework and project.


Last revised: 2 April 2013, Prof. Davison.