Right, let’s talk about what happens when you get a little too trigger-happy with Enter and your terminal session starts to resemble a war zone. You’ve launched vim, then top, then a ping that just won’t quit, and now you’re staring at a frozen prompt, wondering how to get back to your editor without killing everything. This is where shell job control comes in. It’s the built-in process manager you didn’t know you had, and it’s about to become your best friend.

Think of your shell as a grumpy but brilliant stage manager for a play. Every command you start by just typing its name (vim, python3 script.py, etc.) is an actor shoved onto the main stage (your foreground). The stage manager can’t do anything else—like talk to you, the director—until that actor leaves the stage. Hitting Ctrl+C is the equivalent of firing a cannon at the actor to get them off. Effective, but messy.

Job control is how you politely ask the actor to step into the wings (the background) so the stage manager can give you back your prompt. Or how you bring an actor back from the wings to the main stage.

The Magic Keystroke: Ctrl+Z

This is your emergency eject button. It doesn’t kill the process; it suspends it. It freezes the program in its tracks and kicks you back to your shell prompt. The program is still in memory, exactly as you left it, but it’s not executing. It’s in a state of cryogenic sleep.

$ ping example.com
PING example.com (93.184.216.34): 56 data bytes
64 bytes from 93.184.216.34: icmp_seq=0 ttl=54 time=123.456 ms
# ...it's going to keep doing this forever...
^Z
[1]  +  suspended  ping example.com
$

See that [1]? That’s the job number your shell assigned it. The + sign means it’s the most recent job you’ve interacted with. Now you have your prompt back! You can compile something, check a log, or just breathe.

The jobs Command: Taking Attendance

Now that you’ve got things running and suspended, how do you keep track of it all? You ask the stage manager for the program.

$ jobs
[1]  + suspended  ping example.com
[2]  - suspended  top
[3]   running     python3 long_script.py &

The jobs command lists all the jobs your current shell session is managing. The + marks the current job (the one fg and bg will act on by default), and the - marks the previous job. This is crucial when you’re juggling multiple suspended tasks.

bg: Send it to the Background

So you’ve suspended ping with Ctrl+Z. It’s frozen, doing nothing. But what if you want it to keep running, just not in your face? You tell it to run in the background.

$ bg %1
[1]  + continued  ping example.com
$

You can now see your prompt, and ping is happily running away, sending packets and printing its output to the screen whenever it feels like it. This can get noisy, which is why you’ll often want to redirect output (ping example.com > ping_log.txt &) if you’re serious about backgrounding. The %1 tells bg to work on job number 1. If you just type bg with no argument, it assumes you mean the current job (the one with the +).

fg: Bring it to the Foreground

This is how you bring a job back to the main stage. Let’s say you suspended vim and now you want to get back to editing.

$ fg %1
# You are now back in your vim session

Again, fg with no arguments brings the current job (+) to the foreground.

The Ampersand (&): Starting in the Background

You can skip the whole suspend-resume dance by launching a process directly into the background by appending an ampersand (&) to the command.

$ python3 long_running_script.py &
[1] 12345

The shell gives you the job number [1] and the Process ID (PID) 12345. The script runs in the background, and you get your prompt immediately. Its output will still vomit onto your terminal, which is a classic pitfall. Best practice? Always redirect both stdout and stderr if you use & for anything non-trivial: python3 script.py > script.log 2>&1 &.

The Gritty Details and Pitfalls

Here’s where the designers made some… interesting choices.

  1. Terminal Input and Background Jobs: A background job that tries to read from stdin (like an fgets() call in a C program) will immediately be suspended again. It can’t read from your keyboard because the shell is in control of it. This is why you can’t just bg a interactive script that expects input; it’s a one-way ticket to Suspensionville.
  2. SIGHUP Will Kill Your Children: This is the big one. If you log out of your shell (close the terminal, exit SSH), your shell sends a SIGHUP (hangup) signal to all its child processes—including your background jobs. They all die. This is why you thought bg was useless. The solution is to either use nohup (nohup python3 script.py &) or, better yet, a terminal multiplexer like tmux or screen. These tools create a persistent session that survives disconnection, making shell-level job control look a bit quaint by comparison.
  3. Output Chaos: As mentioned, background jobs will happily print to your terminal whenever they want, potentially interrupting your workflow. It’s functional, but it’s not clean.
  4. Job Control is Shell-Local: Jobs are managed per shell instance. You can’t see the jobs from your bash session in your zsh session. They are separate stage managers for separate stages.

Despite its rough edges, understanding jobs, fg, and bg is non-negotiable. It’s the fundamental layer of process management on the command line. It’s what you use when you just need to quickly pause something, check a man page, and get right back to it. For anything more permanent, you graduate to the big leagues: tmux.