IC221: Systems Programming (SP16)


Home Policy Calendar Resources

Lec. 16: 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.

ASR-33_1.jpg

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.

init-to-login.png

Figure 2: From init to login

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.

login-to-shell.png

Figure 3: Starting the shell

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 SIGINT, which essentially says, "Hey, you, I'm interrupting you!" The default action might be to terminate in those cases, and 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 background
  • Ctrl-Z : stop a currently running job
  • bg : continue the last stopped command in the backgorund
  • bg %i : continue job number i in the background,
  • fg : bring the last job from the background to the foreground
  • fg %i : brint job number i from the background to the foreground
  • jobs : 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 fix

    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)----------------------------
    
  • 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:

emacs_foreground.png

Figure 4: Emacs in the foreground of the terminal

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.

emacs_background.png

Figure 5: Emacs in the foreground of the terminal

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.