Unix job control and its interactions with TTYs (and shells)
For a user, Unix's job control gives you more or less two things. First, you can stop a program you're running with Ctrl-Z (usually), and often then have it run in the background for a while if you want (or you may want it to just stop, for example so it stops using up all of your CPU). Second, it lets you multiplex your terminal between multiple programs that all want to interact with you. You can Ctrl-Z the current foreground program, bring another one to the foreground, interact with it for a while, switch back, and so on.
The actual implementation of this is somewhat tangled. One way to put it is that Unix doesn't trust programs to cooperate with job control (which also means that programs didn't have to all be updated when BSD introduced the idea of job control). Instead, job control is managed by a combination of your Unix shell and the Unix TTY driver, primarily through the mechanism of the foreground (terminal) process group.
A job control shell puts every command
or pipeline you run into a different process group. Then Unix TTYs
(pseudo-ttys these days, but once upon a time real serial terminal
connections) have one and only one foreground process group; normally,
only processes in this process group are allowed to interact freely
with the terminal. If a process in another process group attempts to
either read from or write to the terminal, it will usually get hit with
(If you ignore
SIGTTIN, you generally get an
EIO error instead
when you read from the TTY and you're not in the foreground process
Plain ordinary Unix programs do nothing special with the TTY and
let this basic handling take care of everything. Some Unix programs
need to do more; for example, if they turn off the standard TTY
handling, Ctrl-Z stops being special and they need to handle it
themselves. Generally they do this by performing any particular
clean-up they need to do then sending themselves (or their entire
process group) a
To make all of this work, job control shells change the TTY's current foreground process group to match whatever they think should currently be in the foreground. They also watch for processes (and groups of them) that have become suspended, which is a sign of things like the user pressing Ctrl-Z. One consequence of this is that when the shell regains control of the terminal because the current foreground process has either been suspended or has finished, the first thing the shell has to do is reset the current foreground process group back to itself. Otherwise, its own attempts to do IO to the TTY will fail (when the shell wants to print its prompt, read input, and perhaps tell you that something has just been suspended).
PS: If there are multiple processes in the current foreground process group, they can freely write a jumble of TTY output or all try to read from the TTY at the same time. Job control only protects you from different command lines (ie, different process groups) interfering with each other. Generally this isn't an issue because if there's more than one process in a process group, they're in a pipeline (or a shell script).