Why should we need to know about terminal I/O with all these nifty graphical user interfaces out there? Terminals are thing of the past, right? Sorry, in Unix, the terminal device driver controls a lot more than just terminals. modems, printers, direct connections to other computers, and other special devices that rely on streams of characters.
Terminal devices can be put into different states. The default state of the terminal when running in a shell is canonical mode, also known as cooked mode. In this mode, the terminal driver returns one line of data at a time from the terminal device. Any special characters are processed as they come into the device (^C, ^Z, etc.).
The second state the terminal can be in is noncanonical mode, or raw mode. In this state, the terminal device driver returns one character at a time without assembling lines of data. Also, special characters are not processed in this mode. Programs such as vi, pine, and elm use this mode for data input and output. This allows complete control of input and output characters.
A third state, one which Posix.1 defines, is the cbreak mode. This mode is similar to raw mode, except that the processing of special characters still takes place and the corresponding signals are raised for the special characters.
Things to remember about the Input and Output Queues:
All of the attributes that can be controlled in the terminal device
are contained in the termios structure. This structure is
defined as:
struct termios {
tcflag_t c_iflag; /* input flags */
tcflag_t c_oflag; /* output flags */
tcflag_t c_cflag; /* control flags */
tcflag_t c_lflag; /* local flags */
cc_t c_cc[NCCS]; /* control characters */
};
The c_iglag attribute is what controls any input
characteristics of the terminal (map CR to NL, ring bell on input
queue full, etc.). The c_oflag attribute is what you set to
control any output processing of the terminal (expand tabs to spaces,
map lowercase to uppercase on output, etc.). Most of the
c_oflag settings are not Posix compliant. The
c_cflag attribute is for setting the serial line attributes
(enable parity, set flow control, etc.). The c_lflag
attribute is for the settigns of the interface between the user and
the device driver (local echo, enable signals generated byt the
terminal, etc.).
This structure is used with two different functions,
tcgetattr() and tcsetattr(). The prototypes are as
follows:
#include <termios.h> int tcgetattr(int filedes, struct termios *termptr); int tcsetattr(int filedes, int opt, const struct termios *termptr); Both return: 0 if OK, -1 on error |
As the names suggest, tcgetattr() gets the current state of the terminal that the open file descriptor filedes points to, and tcsetattr() sets attributes for the terminal that filedes is associated with. These functions will return an error if the filedes argument is not associated with a terminal device.
The argument opt in tcsetattr() is for
specifying when the changes are to take place. This is defined by the
following macros:
TCSANOW | Make the changes now. |
TCSADRAIN | Make the changes after all output has been transmitted from the buffer. This should be used when setting output attributes. |
TCSAFLUSH | Make the changes after all output has been transmitted, and flush the input queue of any unprocessed data. |
Sometimes you may find that you need to change the speed of the terminal dsession to match that of the device it is connected to. This is done with four functions in combination with the tcgetattr() and tcsetattr() functions.
#include <termios.h> speed_t cfgetispeed(const struct termios *termptr); speed_t cfgetospeed(const struct termios *termptr); Both return: baud rate value speed_t cfsetispeed(struct termios *termptr, speed_t speed); speed_t cfsetospeed(struct termios *termptr, speed_t speed); Both return: 0 if OK, -1 on error |
The first thing that must be done here in order to change the baud rate
of the terminal is use tcgetattr() so that you can pass the termios
struct to the cfset functions. You then pass the struct to the cfset functions
to set the correct baud rate in the termios struct. This does not actually
set the terminal speed, however. You still need to make a call to
tcsetattr() with termios struct that has the changed baud rate.
The line control for the terminal is important if you want to prevent overflowing the buffer for the device when there is no hardware flow control implemented. Also, you can flush the input and/or output of a device discarding any data that has not already been sent or read from the buffer.
#include <termios.h> int tcdrain(int filedes); int tcflow(int filedes, int action); int tcflush(int filedes, int queue); int tcsendbreak(int filedes, int duration); All four return: 0 if OK, -1 on error |
The tcdrain() function suspends the process until all of the data in the ouput buffer has been transmitted. The tcflow() function gives control over input and output flow control. The action argument to tcflow() can be any of the following macros:
TCOOFF | Suspend Output |
TCOON | Restart output |
TCIOFF | Suspend input |
TCION | Restart input |
The tcflush() function lets us discard input or output buffer data. Data in the input buffer is data that has been received but not read yet. Data in the output buffer is data that has been written but not transmitted yet. The queue argument can have the follow macro values:
TCIFLUSH | Flush the input buffer |
TCOFLUSH | Flush the output buffer |
TCIOFLUSH | Flush both the input and output buffers |
The tcsendbreak() function transmits a continous stream of zero bits. If the duration attribute is set to 0, then the duration of the transmition is between 0.25 and 0.5 seconds. If the duration is nonzero, it is implementation specific. Under Linux, if the duration is nonzero, the length of transmission is duration*N seconds where N is between 0.25 and 0.5.
At some point in your career, you may want to find out what terminal device your process is attached to. In the old days, you could just open "/dev/tty" and that was the correct terminal for your process all of the time. Now, their is a POSIX.1 call that you can make to guarantee that you have the right name of your terminal device.
#include <stdio.h> char *ctermid(char *ptr); returns some stuff |
If ptr is not null, it must be an array of char's that is as large as or larger than the macro L_ctermid and the name of the controlling terminal is stored in this array. If ptr is null, then the name of the controlling terminal is stored in a static array. In both cases, a pointer to the first element of the array storing the name of the controlling terminal is returned.
Two really cool and usful functions are isatty() and ttyname().
#include <unistd.h> int isatty(int filedes); Returns: 1 if terminal device, 0 otherwise char *ttyname(int filedes); Returns: pointer to pathname of terminal, NULL on error |
These functions are useful in finding out if filedes is associated with a terminal device or not. Under the hood, the function ttyname(), searches through the terminal device files in /dev/ looking for a matching special file with the same device number and i-node number as filedes. If this statement made no since to you, just ignore it for now. :)
So, what is this cooked terminal mode anyway? When you read from the terminal, if the terminal returns a line at a time instead of each character as it is received, then you are in cooked (canonical) mode. There are a number of reasons that can make the read return:
Cooked mode is the default state of your terminal for almost all shells. At least when you execute another program with the shell, the terminal is put into cooked mode before it makes a call to an exec function.
Raw, or noncanonical for those that don't like raw, is a bit harder to explain. It doesn't necessarily return one byte at a time. You can also set a time limit for your read to return if the number of characters you want to get have not been received from the device. The first step in going into raw mode, no matter what form you want, is to turn off the flag ICANON for your terminal device. This makes it so the input is not put into lines before it is returned. It also makes so some of the special characters are not processed: ERASE, KILL, EOF, NL, EOL, EOL2, CR, REPRINT, STATUS, and WERASE.
So, how do we specify how long to wait for input and how many bytes to read before we return? There are two variables in the c_cc array in the termios structure that must be set: MIN and TIME. These elements are indexed by the macro defines VMIN and VTIME. MIN is the minimum number of bytes that are read in before returning (read blocks until MIN number of bytes have been read). TIME is the amount of time in tenths of a second to wait for data to arrive. So, here is the breakdown of the different cases:
This can be a little confusing at first, just read it a couple of times if you don't understand. Then let it mull over in your brain. Try writing a toy program for each case discussed above. Here is an example of the use of the c_cc variable:
struct termios trm; tcgetattr(STDIN_FILENO, &trm); /* get the current settings */ trm.c_cc[VMIN] = 1; /* return after one byte read */ trm.c_cc[VTIME] = 0; /* block forever until 1 byte is read */ . . /* set some other stuff */ . tcsetattr(STDIN_FILENO, TCSANOW, &trm); /* set the terminal with the new settings */
Some source code for raw mode taken from the Stevens book:
#include
Ever wonder how some terminal applications redraw the screen when you changed the size of your xterm? There is a structure that the kernel maintains for every terminal and pseudo terminal. This is the winsize struct:
struct winsize { unsigned short ws_row; /* rows in characters */ unsigned short ws_col; /* columns in characters */ unsigned short ws_xpixel; /* horizontal size in pixels (not used) */ unsigned short ws_ypixel; /* vertical size in pixels (not used) */ }
The signal SIGWINCH is sent to the forground process any time there is a change made to this strucure in the kernel. We can get the current value of this structure by making a call to ioctl with TIOCGWINSZ request:
struct winsize size; ioctl(STDIN_FILENO, TIOCGWINSZ, (char *) &size);To change the structure in kernel memory, a call to ioctl is made with the TIOCSWINSZ request:
struct winsize size; ioctl(STDIN_FILENO, TIOCSWINSZ, (char *) &size);
When you set the size of the structure in the kernel, if the size is different than it was previously, SIGWINCH is sent to the foreground process.