Lec 13: The Terminal and Job Control
Table of Contents
1 The Terminal
We have reached the point where we have a decent understanding of process, how they are created, how they die, and how they get scheduled by the kernel. In this lesson, we will look at how our interaction with the terminal and the shell, and it's affect on process state.
The terminal, itself, is of interest here. We like to think of the terminal as a device, but it's not like some of the other physical devices, like the disc or network controller. Instead, you can view the terminal as the mechanism that provides the standard file descriptors: standard input, standard output, and standard error.
1.1 tty
and virtual tty's
In the literature of Unix, a terminal device is also described as a
tty
, which stands for TeleTYpewriter. This is a anachronism
referring to the time when computers were mostly centralized main
frames, and you used a telephone connected typewrite to
communicate with it.
Figure 1: Teletype Model 33 ASR teleprinter (wikipedia)
Of course, today, we do not have teletypewriters but the terminology of this long ago day is still used to describe the computer terminal interaction. At first, this might seem a little crazy, but the principles behind the teletypewriter and the modern terminal are not so different. There is still a standard input mechanism where data and command are read, and a standard output mechanisms where data and results are written. It just happens to be on paper for a teletypwriter versus on screen for modern computers.
On modern Unix computing systems, the tty
devices still exist, and in
general, when your computer boots, typically there are 7 tty
's
launched. The first 6 are command line only, and the 7'th is used
for your X-session, which is the nice graphical interface we like to
use when interacting with computers. You can switch between
different terminals using the Ctrl-Alt-F#
sequence. For example,
to switch to tty
1, type Ctrl-Alt-F1
, to switch to tty
3, type
Ctrl-Alt-F3
. To get back to the graphical interface, Ctrl-Alt-F7
.
There are also a number of virtual tty which simulate the core
tty
. These are used when you open a X-terminal, like
gnome-terminal. All the terminals, virtual and otherwise, can be
seen in the /dev
directory, where the kernel provides user-level
inspection of open devices"
#> ls /dev/tty[0-9]* /dev/tty0 /dev/tty16 /dev/tty23 /dev/tty30 /dev/tty38 /dev/tty45 /dev/tty52 /dev/tty6 /dev/tty1 /dev/tty17 /dev/tty24 /dev/tty31 /dev/tty39 /dev/tty46 /dev/tty53 /dev/tty60 /dev/tty10 /dev/tty18 /dev/tty25 /dev/tty32 /dev/tty4 /dev/tty47 /dev/tty54 /dev/tty61 /dev/tty11 /dev/tty19 /dev/tty26 /dev/tty33 /dev/tty40 /dev/tty48 /dev/tty55 /dev/tty62 /dev/tty12 /dev/tty2 /dev/tty27 /dev/tty34 /dev/tty41 /dev/tty49 /dev/tty56 /dev/tty63 /dev/tty13 /dev/tty20 /dev/tty28 /dev/tty35 /dev/tty42 /dev/tty5 /dev/tty57 /dev/tty7 /dev/tty14 /dev/tty21 /dev/tty29 /dev/tty36 /dev/tty43 /dev/tty50 /dev/tty58 /dev/tty8 /dev/tty15 /dev/tty22 /dev/tty3 /dev/tty37 /dev/tty44 /dev/tty51 /dev/tty59 /dev/tty9
1.2 Login from tty to shell
In the beginning, there was init
, the first process, not the
terminal and not the shell, but we know that when we first interact
with Unix systems, we usually do so through the shell. To get to that
point, the O.S. must first establish the tty
and perform a log in.
Based on the number of tty
's available, the init
process will
fork that many times. Each new child process will execute getty
,
which is the program that initializes the tty
line and the
standard file describes. getty
is also the program that reads your
username for log in, but not your password, login
does that. We
can see the running getty
's using the pstree
output:
#>pstree -ac root init | : . : ├─getty -8 38400 tty4 ├─getty -8 38400 tty5 ├─getty -8 38400 tty2 ├─getty -8 38400 tty3 ├─getty -8 38400 tty6 ├─getty -8 38400 tty1
There are 6 available, the 7th is running the X session. At this
point, if you were to switch to a tty
, say tty1
using
Ctrl-Alt-F1
, you'd see the prompt for the user name. Once we enter
our user name, getty
will execute login
:
#> pstree -ac init | : . : ├─getty -8 38400 tty4 ├─getty -8 38400 tty5 ├─getty -8 38400 tty2 ├─getty -8 38400 tty3 ├─getty -8 38400 tty6 : . : |-login
The login
program's task is to read the password from the user and
authenticate. If that is successful, login
will execute the login
shell
, which is typically bash
for most users.
We can also see this apear in the pstree
. Note, that after
login
, the shell is run by the newly logged in user, and not by
the root
user. This done by a setuid()
, which is a discussion
for a later lesson.
#> pstree -acu init | : . : ├─getty -8 38400 tty4 ├─getty -8 38400 tty5 ├─getty -8 38400 tty2 ├─getty -8 38400 tty3 ├─getty -8 38400 tty6 : . : ├─login -- │ └─bash,aviv
At this point, the user is logged in, and there is a shell running
as that user. The tty
associated with that shell is managed by a
terminal device driver within the kernel, which opened the
standard file descriptors when getty
executed and was inherited by
each successive executed program. When you read/write from that the
terminal, it's the terminal device driver that is ensuring that this
action can be taken.
2 Job Control and Sharing a Terminal
Great, now we have a terminal all to ourselves with a shell running: what could be better? What's the problem? There's just one terminal, but this is the multitasking generation. We want to do multiple things at the same time, but with a single terminal and a single shell, this may seem like this is it. No multitasking. :(
Unix user's, from the beginning, have been challenged by the single terminal problem, and a very elegant solution has been developed. The idea is that there exists multiple states of a process running within a shell. A process (or group of process) can be established as a foreground process group or a background process group. The shell then provides a mechanism to easily swap process between the foreground and background (called job control), and the kernel provides system calls to indicate which set of process are in the foreground and thus have direct access to the terminal standard file descriptors. The kernel then can also ensure that process that do not have access to the terminal cannot interfere with the process that do.
It's the same story of abstraction and isolation: the O.S. views the terminal as a resource and is providing an abstract interface to interact with it, the terminal device driver, and via this interface, the O.S. can also provides isolation, processes running in the foreground and background. Let us explore the interface and the commands that provide this service, and not only will it illuminate some of the interactions with the O.S., it will also help you become more effective Unix users. Skills associate with Job Control is vitally important to productivity in the shell.
2.1 Terminating and Stopping a Job
When discussing the exeuction of a task in a terminal, we use the term job to refer to all process that are executed at the same time. For example, the execution of this pipeline actually causes each command to be forked and executed as its own process, but all the process are grouped together as a process group and described as a single job. (We'll come back to this notion of process grouping and pipelines in later lessons.)
When managing jobs in the shell, one of the first things that young
Unix users learn is how to terminate a job that's gotten out of hand,
like an infinite loop. This is the Ctrl-C
key, but this action is
actually invoking the terminal driver to deliver the job a special
signal, called a SIGTERM
, which essentially says, "Hey, you,
terminate!" The job then follows that direction and dies.
#> cat | grep "MD" > /dev/null ^C #>
The above sequence, cat
at the start of pipeline, will not proceed
without input, so we can terminate that job with Ctrl-C
, and then
the shell comes back with another prompt. The child has terminated
and the wait()
in the shell is no longer blocking.
Another option a user has besides terminating a job is to stop it
with the Ctrl-z
key. When you type Ctrl-z
you are asking the
terminal device driver to deliver a SIGSTOP
signal says, "Hey, you,
stop what you're doing and await further instructions." That job, and
all its associated process, will enter a blocking state where it is
awaiting a message to continue before proceedings.
#> cat | grep MD > /dev/null ^Z [1]+ Stopped cat | grep --color=auto MD > /dev/null #>
After a job being stopped, the shell will prompt for more input, but the stopped job hasn't disappeared. It still exists, it's just not running, and we can start more jobs and stop those as well.
#> cat | grep MD > /dev/null ^Z [1]+ Stopped cat | grep --color=auto MD > /dev/null #> sleep 1200 ^Z [2]+ Stopped sleep 1200 #>
We now have two stopped jobs: what to do with them? We could choose to bring those jobs back to a running state, either in the background or the foreground.
2.2 Continue a Job in the foreground and background
A job running in the foreground is one where the shell is explicitely waiting for it to complete. A job running in the background is one where the shell is not waiting for the job to complete before prompting the user for more commands to run. The act of managing jobs running state in the foreground and background is called job control and is a feature provided by the shell.
Here are the set of commands most used in job control:
cmd &
: execute a job in the backgroundCtrl-Z
: stop a currently running jobbg
: continue the last stopped command in the backgorundbg %i
: continue job numberi
in the background,fg
: bring the last job from the background to the foregroundfg %i
: brint job numberi
from the background to the foregroundjobs
: list the current jobs, stoped and running
A few examples should give you a feeling for how to use these
commands, and the best way to do that is within the terminal,
only. No graphical interfaces, so switch over to the first tty,
tty1
, and log in.
2.2.1 Job Control Demo 1: Editing a file
Let's start with a fairly common situation. You are remotely logged
into one of the lab machines and editing a file. You type emacs
for
your editor, and it comes up within the terminal. Now you need to
compile your program, so you need the shell. You can use job control
to switch between those two states. Do your editing, then issue a
ctlr-z
to stop the editor, work in the terminal, then type fg
to
bring the editor back to the foreground. Below, is a walk through of
such a situation.
- Running emacs
#> emacs helloworld.c
- Edit the program
File Edit Options Buffers Tools C Help #include <stdio.h> #include <stdlib.h> int main(){ printf("Hello World\n:); return 0; } -UU-:----F1 helloworld.c All L1 (C/l Abbrev)----------------------------
- Type
Ctrl-z
to move it to background and compile,#> emacs helloworld.c [1]+ Stopped emacs helloworld.c #> clang helloworld.c -o helloworld helloworld.c:5:12: warning: missing terminating '"' character printf("Hello World\n:); ^ helloworld.c:5:12: error: expected expression 1 warning and 1 error generated.
- Oops, type
fg
top bring it to the foreground to fixFile Edit Options Buffers Tools C Help #include <stdio.h> #include <stdlib.h> int main(){ printf("Hello World\n"); return 0; } -UU-:----F1 helloworld.c All L1 (C/l Abbrev)----------------------------
- Ctrl-z, compile and run
#> fg emacs helloworld.c [1]+ Stopped emacs helloworld.c #> clang helloworld.c -o helloworld #> ./helloworld Hello World #>
2.2.2 Job Control Demo 2: Continuing jobs in the background
Another scenario you might find yourself in is that you've just
opened a file to edit, using emacs
in graphical mode, but you
forgot to use an &
to have it run in the background. You'll find
your terminal stuck like this:
You can move the emacs to the background by first stopping it with
Ctrl-z
and then issue the bg
to continue the last job in the
background.
Now, you have emacs
running in the background and the shell
available, as if you started emacs
with the &
option in the
background originally.
2.2.3 Job Control Demo 3: Selecting Specific Jobs with jobs and job id
Suppose now that we've started a number of jobs, and some of them
are running or stopped in the background. We want to bring one of
those to the foreground or continue one in the background, but it is
not the most recent. We need a way to select some specific job to do
this action on, and we can do so using the jobs
command, which
will output all the available jobs and an identifier.
We just started a bunch of jobs:
#> emacs helloworld.c& [1] 3629 #> cat & [2] 3630 [1]+ Stopped emacs helloworld.c #> sleep 200 & [3] 3632 [2]+ Stopped cat #> sleep 300 & [4] 3633 #> jobs [1]- Stopped emacs helloworld.c [2]+ Stopped cat [3] Running sleep 200 & [4] Running sleep 300 & #>
If you look at the jobs output, for the stopped jobs, some have a
"+" and some have a "-". If I were to type fg
, the job with the
"+" will come to the foreground.
#> fg cat ^Z [2]+ Stopped cat #> jobs [1]- Stopped emacs helloworld.c [2]+ Stopped cat [3] Running sleep 200 & [4] Running sleep 300 & #>
Instead, if I want to bring the emacs
to the foreground, I have to
specify the job id directly.
#> fg %1 emacs helloworld.c ^Z [1]+ Stopped emacs helloworld.c #> jobs [1]+ Stopped emacs helloworld.c [2]- Stopped cat [3] Running sleep 200 & [4] Running sleep 300 & #>
And now emacs
has the "+" and is the most recent job. We could
also use fg
for the other jobs, like say we wanted to kill the
sleeps.
#> fg %3 ^C #> fg %4 sleep 300 ^C #> jobs [1]+ Stopped emacs helloworld.c [2] Stopped cat
And we can also bring cat
to the foreground and kill that.
#> fg %2 cat ^C #> jobs [1]+ Stopped emacs helloworld.c
Finally, if we want to kill something in the background, we can
kill
with the job id as well:
#> kill %1 #> jobs [1]+ Terminated emacs helloworld.c #> jobs #>
2.3 Terminal Device Driver and Job Control
Job contorl, while implemented by the shell, is coordinated by the
terminal device driver. The action of delivering terminal signals,
like when the keys Ctr-c
and Ctrl-z
are pressed, are the
responsible of the temrinal device driver and should only be
delivered to the foreground process group. Think of the alternative,
you're running an inifinite loop and you type Ctrl-C
, and instead
of your program exiting, your program and your shell exit because
they both received the Ctlr-c
message. That would be very
annoying, and to ensure this does not happen, the shell uses a
special system call tcsetpgrp()
to indicate which job is currently
in the foreground and should receive the terminal singals, like
Ctrl-c
. We will look at this in more detail in lab, but it's
important to recongize that there is a number of things going on here.
For example, another service provided by the terminal device driver
is that ensures that only the foreground process can read from
stdin at a time. Try the following, running cat
in the background:
#> cat & [1] 22851 #> jobs [1]+ Stopped cat #>
It's blocked, but I said to run in the background … how could this
be? When a background process tries to read from stdin
this causes
a signal from the terminal devic driver that says, "Hey, you
shouldn't be doing that, stop," and the process does just that. It
doesn't have to, thought, and this request can be ignored. For
example, the shell usually ignores such request.