In part two, I discussed Linux devices in some detail, and discussed terminal devices briefly. In this post I will go into terminal devices in a bit more detail.
In the previous part, I pointed out how terminal devices fit into the abstraction of Unix-like systems. When you type things into a virtual terminal, a process that is reading from a file descriptor to that virtual terminal receives the characters you type in, and likewise, when a process writes to a file descriptor to a virtual terminal, characters appear on that terminal’s virtual screen. But this is not by any means the entire story. It sounds as though a terminal is just like a Unix domain stream socket that happens to have a physical terminal on the other side. (Like stream sockets, Linux ttys are full-duplex, and can be read and written at the same time.) But terminals are much richer than that. For example, they support colour and other text formatting. If you are like me, you might be clueless about how that works. (Not that I’m clueless—I wouldn’t be writing this if I were—but I used to be, before I looked it up.)
First, though, let’s get a trivial question out of the way, that I alluded to earlier. We know that bash running in interactive mode on tty1 has file descriptors 0, 1, and 2 all referring to
/dev/tty1, for example. How does bash come to have open file descriptors to virtual terminal 1, or any terminal, really? I suppose people know what happens when they execute scripts, for example,
bash foo.sh will pass
foo.sh as an argument to
bash, which will then open up the filename in that argument and start executing. But what about the tty?
The answer is not too hard. When a process has files open, either it opened those files itself, or it already had those files open before the (possibly
exec(2) that resulted in its existence; open files are preserved across both system calls. The following is what happens on my system:
init(8) process is created on system startup.
init(8) reads its configuration files and discovers that it is supposed to start a process called
getty(8) on some of the virtual terminals available on the system, typically
- The process
getty(8) is started with, say,
tty1as an argument. It inherits stdin, stdout, and stderr from
init(8). On my system, these all point to
/dev/null. It closes file descriptors 0, 1, and 2, and opens
/dev/tty1as file descriptors 0, 1, and 2. It prints out the name of the system and a prompt for a username. It reads a username I type in, say,
root, and then
/bin/login -- rootwithout
- The process
login(1) prompts for a password, reads in the password, checks it, and then
bash(1), again without
fork(2)ing. Note that bash inherits the stdin, stdout, and stderr that were initially opened by getty(8).
- When I end the session by typing
logoutor Ctrl+D at the bash prompt,
init(8) receives a notification via
wait(2) or a related function telling it that its child has died. It then respawns
getty(8) on that terminal so the next person can log in. (goto 3)
This suggests an answer to another question, which I alluded to in part one. My system supports 63 numbered virtual terminals,
tty63. Even though at most six are enabled by default, you can enable any of the terminals numbered 8 through 63 by starting
getty(8) on them. You can additionally edit the
init(8) configuration files if you want them to be enabled automatically on each startup. Note that you can activate (foreground) any numbered terminal using
chvt(1), as explained in part one, but you can’t type anything in until after you’ve started
getty(8) on it. This suggests that
getty(8) is doing something behind the scenes that makes the terminal usable, besides just opening it. Not so—the actual rule is that you can type in to any terminal as long as at least one process has it open. You can test this by starting
cat /dev/tty8 (for example); once you do this, you will be able to type on
tty8 (and it will echo your characters).
Now back to the main story. Terminals have all sorts of features that sockets simply don’t have. These features are implemented by the terminal driver, and all programs that interact with terminals can take advantage of them without having to implement them themselves. I’m mostly repeating myself from Quora here:
- A program running on a terminal can produce colour-coded output1. Obviously, a program writing to a regular file, pipe, or socket cannot colour the outgoing byte stream.
- Printing the character
'\a'(ASCII 0×07) to a terminal will result in a beep (audible bell) or flash (visible bell), but printing the same character to a regular file, pipe, or socket will not.
- Pressing Ctrl+C at a terminal will send the SIGINT signal to all processes in the foreground (more on this later), but a process does not receive SIGINT when it reads a character with ASCII code 0×03 from a regular file, pipe, or socket. Furthermore, because this signal originates from the terminal driver, it bypasses permission checks. That’s why you can interrupt a process like
login(1) even though it runs with root privileges, but you wouldn’t be able to
kill(2) that process without being root. This is one aspect of job control, to which I will devote an entire part of this tutorial.
- I mentioned in part two that you can’t seek on a terminal. However, you can move the cursor. This is important, because programs like
vim(1) depend on it. It wouldn’t make much sense to pipe the output of
less(1) to another process, or redirect it to a file, would it?
These are not by any means the only features of terminals, but they do serve as a representative sample to illustrate the following point: terminal devices were engineered to facilitate human-computer interaction; after all, that is their sole purpose. Therefore, all the cool features of terminals that do not exist in regular files, pipes, and sockets are basically useful only because terminals have human beings on the other end. None of these features would be useful at all for two processes talking to each other through a socket pair. Regrettably, the diversity of terminal features has resulted in a complicated interface, which I hope to describe in detail.
There are fundamentally two kinds of terminal features.
First, terminal devices support a number of
ioctl(2) calls. As I mentioned briefly in part two, a device driver may choose to implement
ioctl on a device file it introduces, to represent operations on that device that are not reading, writing, or seeking; these operations can then be carried out by any process that has an open file descriptor to that device, when it calls
ioctl(2). For example,
ioctl(fd, CDROMEJECT) ejects the CD-ROM drive referred to by
fd. (Such a device is usually named
/dev/cdrom or something similar.)
Curiously, POSIX specifies nothing whatsoever about the behaviour of
ioctl(2) on a terminal! Instead, POSIX provides a set of library functions that, in Linux at least, ultimately map onto
ioctl(2) calls, that form a portable interface to these features. These functions’ names start with
tc. (I’m not sure what this stands for, but my guess is “terminal control”.) There are some
ioctl(2) calls, such as the
VT_ACTIVATE call I mentioned in part one, that cannot be performed via the POSIX interface because they are Linux-specific. However, there are other Linux-specific things that you can do using the POSIX interface. Full documentation should exist in the manual pages
termios(3) (for the POSIX interface),
tty_ioctl(4) (for Linux-specific ioctls), and
console_ioctl(4) (for Linux-specific ioctls that only work on the console and numbered virtual terminals).
In what follows, when I say “terminal ioctl”, I do not necessary mean that you should use
ioctl(2) to access the feature; indeed, you should use the POSIX interface when you can. I only mean that it falls into this category.
Second, there are some things that you can do to terminals simply by writing certain character sequences to them. The terminal bell I mentioned previously is one example. Moving the cursor is done using an escape sequence, and so is outputting coloured text. (On the other hand, the Ctrl+C interrupt does not occur as a result of writing to terminals; it only occurs when you press Ctrl+C. You can actually input a
'\003' at the terminal by pressing Ctrl+V Ctrl+C, which does not send an interrupt signal. This signalling can be configured and disabled using terminal ioctls.) These features of terminals are known as terminal capabilities, and documentation may be found in the
terminfo(5) manual page; however, the console and numbered virtual terminals support additional escape sequences that are not found in terminfo, so this is not the full documentation.
This dichotomy might seem strange, but there is a logical reason for it. Terminal ioctls and terminal capabilities deal with different aspects of terminals. Terminal capabilities lie closer to the physical terminal, so to speak; nearly all of them deal with the appearance of output, so that the human will notice their absence, but the process will not. You’ll notice that this is true of colour-coding, terminal bells, and moving the cursor, the three examples I gave above. A process can output escape codes that set the terminal colour to a monochrome terminal. All the process notices is that the
write(2) has succeeded (or, more likely,
printf(3)). The terminal itself might display the escape sequence itself since it does not understand its meaning, or it might simply ignore it because it does not support colour. The user will see that it didn’t work, but the process will be blissfully unaware. On the other hand, terminal features affected by terminal ioctls do affect processes, and, furthermore, if a process tries to perform an unsupported ioctl, it will be notified with a return code of -1. The Ctrl+C interrupt is one example of a feature that is configurable with
ioctl(2), even though
ioctl(2) is not actually executed when this feature is used; the
VT_ACTIVATE ioctl is an example of a feature that is performed using
ioctl(2), and it affects nearly all non-daemon processes on the system, because it decides who gets keyboard input and who doesn’t!
By the way, the program
stty(1) can be used to call
ioctl(2) on the terminal on which it runs; it bears the same relationship to
ioctl(2) as, say
mv(1) bears to
rename(2). It is commonly used by n00bs to disable echoing using an abomination like
system("stty -echo"). (This is acceptable in a shell script, though.) Some terminal capabilities can be set using the program
setterm(1), mostly the ones that have something to do with conceptually “setting” something on the terminal; for example,
setterm -blength 0 disables the audible bell by setting its length to zero milliseconds.
stty(1) is specified in POSIX, whereas
setterm(1) is not.
The dichotomy is even more pronounced in pseudoterminals, that is, terminal-like devices with a master end and a slave end, where the slave end behaves like a tty and the master end behaves like the keyboard and monitor. These support all the ioctls that ttys support (except the ones in
console_ioctl(4)) and the corresponding features such as the Ctrl+C interrupt, but none of the capabilities; escape sequences simply pass through them verbatim. It is up to whatever process has the master end, usually a terminal emulator, to support these capabilities. But that’s a story for another part.
Part four will flesh out terminal ioctls and the terminal interface, describing some of the most important ioctls; then it will discuss the terminfo interface and describe some of the most important capabilities.