How terminals work

18 October 2009

Via @toros on Identi.ca, I stumbled across this awesome explanation by Scott James Remnant on how terminals (real terminals, virtual terminals, and pseudo terminals) in Unix work.

Because it is pasted from an IRC conversation, it is a little hard to follow, so I present it to you reformatted to be more readable. (I have tried to remain as faithful as possible to the original.)

How Terminals Work

By Scott James Remnant

In Linux, we have consoles, but really we mean Virtual Terminals (VTs), TTYs, and Pseudo-Terminals (PTYs). It’s all a bit of the kind of jumble sale you get after 40 years of different solutions to different problems. We can lump them together under the description “terminals” for the next bit.

We also have processes. Now, processes have a lot of odd little details: they have a parent, and they have a session. A session has a foreground process group and a background process group. (Stevens devotes an entire chapter for this and nobody but me apparently understands it. And hopefully the guy who maintains the kernel side. ;) )

So, each process is part of a session — a process may begin a new session by calling setsid(). Every process that init creates is in its own session. The process is also then the leader of the foreground process group of that session. New processes are also in that session, and in that process group, unless otherwise placed into a new process group (setpgrp()). Any new process group is a background process group.

So now, you have a bunch of sessions. Each session has a bunch of process groups, one of which is the foreground process group. Each process group has a bunch of processes, one of which is the leader.

So this all has to do, fundamentally, with terminals, and who gets the signals.

When the leader of the foreground process group of a session opens a terminal device (without O_NOCTTY) that becomes the controlling terminal of that process group and session. The terminal and the session become bound to each other.

You can fake this another way by opening a terminal device without O_NOCTTY (or having one passed to you) and then calling the TIOCSCTTY ioctl().

Okay, so: terminals, controlling terminals and processes — here’s where this gets fun.

If the controlling terminal is hung up, SIGHUP is sent to the foreground process group. If ^C is pressed on the controlling terminal, SIGINT is sent to the foreground process group. If ^Z is pressed on the controlling terminal, SIGTSTP is sent to the foreground process group. And so on and so forth.

So this is how the relationship between magic key presses and signals gets established. Shells care about this a lot (and yes, when you use command & that becomes a background process group, and when you use command | command they are all in the same process group).

Now, this controlling terminal business applies to all terminals, whether they be true terminals (which Linux doesn’t have), virtual terminals, or pseudo terminals. So this is as true for your SSH login as your VT1.

You can always access your controlling terminal using /dev/tty — (it’s a badly named device node); it may also be called /dev/ttyS0 or /dev/pts/4, etc.

So, Linux has a bunch of virtual terminals. These are the things we think of when we say “console” but we’re using that wrongly. Virtual terminals behave just like ordinary terminals: they can be the controlling terminal for a process group, but unfortunately, stacked on top is the linux VT API — they didn’t think to make it separate.

So the stuff to set fonts — to place it in raw or graphics mode, create new VTs, switch VTs, etc. — is all loaded into the TTY API, so in order for X to function, it needs a VT. X needs access to that VT in interesting and familiar ways to place it into raw and graphics mode, and so on.

X also needs to know if the current VT is switched, so it opens the VT device it wants (/dev/tty7), and that becomes its controlling terminal. So if you were to delete VT7, X would get SIGHUP. :)

Now, on VT1-6 you have getty, and on VT8 you have usplash, and so on. This is all fine and dandy, except there’s this last mystical piece: /dev/console. /dev/console is, like /dev/tty, a fake device: it points at the currently active VT, whatever that is. But it behaves like a terminal in its own right (whereas /dev/tty just behaves as a proxy for the underlying terminal).

Now, Upstart has a few knobs to customise the standard input, output and error file descriptors. Normally it just starts all jobs with them as /dev/null, but for emulation of sysvinit, it has two other options:

  1. Set them to /dev/console
  2. Set them to /dev/console and issue the TIOCSCTTY ioctl() (console output, console owner)

Now, if your current VT is 7 (X), and you start a job that has console owner in it, the new process will take the terminal away from X! X gets SIGHUP, and either hits 100% CPU or crashes. Solving this problem for good requires jobs that need user input to be rewritten.

So this is clearly bad. The problem really is that things need a “console” at all — jobs that require interaction should do it themselves: they should open a VT, switch to it, and ask there. Or they should use usplash to do it — or they should use X.

Link to original source