Understanding Job Control In Bash


Between opening a number of shells and running a screen multiplexer like screen there are many ways people utilize terminals to run multiple commands at once. Bash provides its own built-in way of handling this: Job Control.

Each process being executed is a child process of the bash shell. Job Control allows you to attach and detach from the child processes. An attached child process is one that is in the foreground. A detached child process is one that is in the background.

A child process that is in the background may not read from the terminal and should not write to the terminal. A child process that is in the foreground behaves as you would expect any process you execute to.

Starting a process in the background will automatically make it a job. Let's take an example where we start vim as a background process to edit our /etc/lighttpd/vhost.conf file.


root@symkat:~# vim /etc/lighttpd/vhost.conf &
[1] 28306
root@symkat:~#

What has happened here is that the process vim has been started, with /etc/lighttpd/vhost.conf as an argument. The & put this process in the background and bash listed the job ID ([1]) and the process ID (28306).

If we run the built-in command jobs we can take a look at the currently running jobs:


root@symkat:~# jobs
[1]+  Stopped                 vim /etc/lighttpd/vhost.conf
root@symkat:~#

We see the job ID is 1, which was shown to us when first launching it. Additionally we see the + sign. This indicates that it is the last job to be associated with the terminal, so running any job related command without specifying a job ID will use this job. We see the current state of the job is Stopped, and the command associated with it.

Let's run an additional command in the background and take a look at how the jobs table changes.


root@symkat:~# vim /etc/lighttpd/lighttpd.conf &
[2] 28899
[1]+  Stopped                 vim /etc/lighttpd/vhost.conf
root@symkat:~# jobs
[1]-  Stopped                 vim /etc/lighttpd/vhost.conf
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
root@symkat:~#

We got the same information again when launching the second job. Namely the Job ID and the Process ID assigned to vim. We also got a listing of the currently running jobs.

Running jobs again is showing us new information. Now our first stopped process has a - sign. This means it was the second to last command to be used. The second process now has a + sign indicating it was the last command to be used and job controls will use it as the default if no job identifier is given.

At this point it may be useful to take a look at the process table:


root@symkat:~# ps af | grep bash -A4
28303 pts/0    S      0:00  _ /bin/bash
28306 pts/0    T      0:00      _ vim /etc/lighttpd/vhost.conf
28899 pts/0    T      0:00      _ vim /etc/lighttpd/lighttpd.conf
29522 pts/0    R+     0:00      _ ps af
29523 pts/0    S+     0:00      _ grep bash -A4
root@symkat:~#

The f switch to ps shows us a tree format, which allows us to see parent/child relationships. Both of our vim commands are children of bash. It may also be useful to explain those capitol letters. The S indicates something is sleeping. This usually means it's waiting for input. The T classification that both of our stopped processes are under is stopped by job control or because the process is being traced. In this case both are stopped due to job control. The R for ps af indicates that it is currently running and the + sign in front of it indicates that it is currently a foreground process. The S+ indicates that the grep statement is sleeping (it's waiting for ps af to complete) and in the foreground.

Now we have two jobs that have been executed. There is a handful of ways to call a process back to the foreground. The command fg provides the mechanism to call a stopped process back to the foreground by it's Job ID.


root@symkat:~# fg %1
vim /etc/lighttpd/vhost.conf
root@symkat:~# jobs
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
root@symkat:~#

Inside of vim I quit with :q. Now we notice the command run was printed to the screen after I exited vim. Looking at jobs, we now only see the second one. Notice that it will remain with Job ID 2, even after Job ID 1 has been reaped.

Let's pick up the second editor and exit it without killing the job.


root@symkat:~# fg %2
vim /etc/lighttpd/lighttpd.conf
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
root@symkat:~#

We did this with the control character ^Z. This will suspend a process into the background as if you typed the command followed by an &. The ^Z control character is the fastest way to put your current foreground process into the background.

For sake of verbosity: control characters denoted by ^X are expressed by holding control and hitting the key given as X. For instance, ^Z is control-z, while the bell character, ^G is control-g. These are sometimes written in documentation as C-x where x is the control key being entered.

It is worth noting that data that is being output to the shell is stopped. The application is effectively freezing in time. Let's look at an example:


root@symkat:~# cat>shell.sh;chmod 750 shell.sh;./shell.sh
i=0
while [ 1 ];
do
    let i=$i+1;
    echo "Looping your ^Z $i times";
    sleep 2;
done
^D
Looping your ^Z 1 times
Looping your ^Z 2 times
Looping your ^Z 3 times
Looping your ^Z 4 times
Looping your ^Z 5 times
^Z
[3]+  Stopped                 ./shell.sh
root@symkat:~# sleep 10
root@symkat:~# fg %3
./shell.sh
Looping your ^Z 6 times
Looping your ^Z 7 times
Looping your ^Z 8 times
Looping your ^Z 9 times
Looping your ^Z 10 times
Looping your ^Z 11 times
^C
root@symkat:~#

Here we wrote a little bash script to print a number that will increment each second. We stopped the application for ten seconds and then picked it up again. When we picked it up, instead of having jumped to 15, it jumped only to 6. Right where we had left off.

If we had put the script in the background however, it would have continued. ^Z suspends an application with a SIGSTOP, while using bg to put something in the background will allow it to continue executing. Let's look at an example of doing that.


root@symkat:~# ./shell.sh
Looping your ^Z 1 times
Looping your ^Z 2 times
Looping your ^Z 3 times
^Z
[3]+  Stopped                 ./shell.sh
root@symkat:~# bg %3
[3]+ ./shell.sh &
root@symkat:~# Looping you ^Z 4 times
Looping your ^Z 5 times
Looping your ^Z 6 times
Looping your ^Z 7 times
Looping your ^Z 8 times
fg %3
./shell.sh
Looping your ^Z 9 times
^C
root@symkat:~#

In this example, our script broke the rules about printing to the terminal when turned into a background process. A process knows it's not supposed to print to the terminal while in the background because it received a SIGTTIN signal from the terminal when attempting to read and a SIGTTOU signal from the terminal when attempting to write.

Often times the SIGTTOU is not sent. The decision to send a SIGTTOU signal to the application is controlled by the terminal. You can modify the decision with the stty command, specifically stty tostop to enable it and stty -tostop to disable it. By default it is typically disabled.

The best shortcut for job control is that if you access a job by the %id alone on the terminal, you do not need to use fg before hand. Some people consider fg to be redundant as a result.

Let's take a look at a typical workflow with Job Control to get an idea of how it can be used in everyday practice.


root@symkat:~# jobs
root@symkat:~# vim /etc/lighttpd/vhost.conf
[1]+  Stopped                 vim /etc/lighttpd/vhost.conf
root@symkat:~# vim /etc/lighttpd/lighttpd.conf
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
root@symkat:~# %1
vim /etc/lighttpd/vhost.conf
[1]+  Stopped                 vim /etc/lighttpd/vhost.conf
root@symkat:~# vim ~/.bashrc
[3]+  Stopped                 vim ~/.bashrc
root@symkat:~# jobs
[1]-  Stopped                 vim /etc/lighttpd/vhost.conf
[2]   Stopped                 vim /etc/lighttpd/lighttpd.conf
[3]+  Stopped                 vim ~/.bashrc
root@symkat:~# %2
vim /etc/lighttpd/lighttpd.conf
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
root@symkat:~# jobs
[1]   Stopped                 vim /etc/lighttpd/vhost.conf
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
[3]-  Stopped                 vim ~/.bashrc
root@symkat:~# fg
vim /etc/lighttpd/lighttpd.conf
[2]+  Stopped                 vim /etc/lighttpd/lighttpd.conf
root@symkat:~# %-
vim ~/.bashrc
[3]+  Stopped                 vim ~/.bashrc
root@symkat:~# %?bash
vim ~/.bashrc
[3]+  Stopped                 vim ~/.bashrc
root@symkat:~# %?vhost
vim /etc/lighttpd/vhost.conf
[1]+  Stopped                 vim /etc/lighttpd/vhost.conf
root@symkat:~# %vim ~
bash: fg: vim: ambiguous job spec
root@symkat:~# nano nano_never_wins_editor_wars.txt
Use "fg" to return to nano.
[4]+  Stopped                 nano nano_never_wins_editor_wars.txt
root@symkat:~# %nano
root@symkat:~# jobs
[1]+  Stopped                 vim /etc/lighttpd/vhost.conf
[2]   Stopped                 vim /etc/lighttpd/lighttpd.conf
[3]   Stopped                 vim ~/.bashrc
root@symkat:~#

We've looked at a handful of methods for recalling jobs.

%id

By recalling a Job by ID we can simply supply % followed by the numerical value of the Job ID. %1 for the first job, %2 for the second job and so on.

%?substring_search

By recalling the Job by a substring search we can supply a unique part of the command we executed. For instance above we used %?vhost to recall the vim /etc/lighttpd/vhost.conf Job. We could also say %?lighttpd. with the period to recall the vim /etc/lighttpd/lighttpd.conf Job. When using a substring search your string MUST be unqiue to only one command in the Job table. Otherwise you will get the following warning:


root@symkat:~# %?lighttpd
bash: fg: lighttpd: ambiguous job spec
%start

%start_string_search

By recalling the Job by a starting-string lookup we can supply the first part of the command we ran. This is useful if you're running a collection of unique programs such as irssi, vim and pine and switching between them. In our example it's not terribly useful as all the commands started with vim. So we launched nano for variety.

Of course, you could always run screen.


Contact Me