Bash processes aren’t just commands you run; they’re living entities with states, lifecycles, and interactions you can orchestrate.

Let’s watch a process do its thing. We’ll start a simple loop that prints a number every second.

$ for i in {1..5}; do echo $i; sleep 1; done
1
2
3
4
5

Now, imagine that loop was something more substantial, a background service or a long-running computation. You’d want to manage it.

The core of process management in Bash revolves around understanding how processes are launched, how they communicate (or don’t), and how you can signal them to change their behavior. When you type a command like ls or grep, Bash doesn’t just execute it; it forks a new process, executes the command within that new process, and then often waits for it to finish.

Let’s see this in action. We’ll launch that loop again, but this time, we’ll put it in the background using the & operator.

$ for i in {1..5}; do echo $i; sleep 1; done &
[1] 12345

See that [1] 12345? [1] is the job ID within your current Bash session, and 12345 is the actual Process ID (PID) on the system. Your shell immediately returned the prompt. The loop is now running independently. You can verify this with ps aux | grep 'sleep'.

$ ps aux | grep 'sleep'
user      12345  0.0  0.0   1234   567 pts/0    S+   10:30   0:00 /bin/sleep 1
user      12346  0.0  0.0   7890  1234 pts/1    S+   10:31   0:00 grep --color=auto sleep

Notice the S+ state for the sleep process, indicating it’s sleeping. The + means it’s in the foreground job of its terminal. If we’d backgrounded a command that also tries to read from the terminal, it would likely stop and require fg to bring it back.

To bring a backgrounded job back into the foreground, you use fg.

$ fg %1
for i in {1..5}; do echo $i; sleep 1; done

Now, the shell is waiting for that command to finish again. If you want to stop a running process, you can send signals. The most common signal is SIGINT (interrupt), which is what you send when you press Ctrl+C. You can send other signals using the kill command.

To stop the backgrounded loop, we first need its PID. Let’s re-run it and note the PID:

$ for i in {1..5}; do echo $i; sleep 1; done &
[2] 12347

Okay, PID is 12347. To send the interrupt signal:

$ kill -SIGINT 12347

Or, more commonly:

$ kill -2 12347

The process will terminate. If you try to send SIGKILL (-9), which is a forceful termination that the process cannot ignore:

$ kill -9 12347

This is useful when a process is stuck and doesn’t respond to SIGINT. However, SIGKILL doesn’t allow the process to clean up, so it’s a last resort.

You can also monitor processes. ps is your primary tool. ps aux gives a comprehensive snapshot of all running processes. top and htop provide a dynamic, real-time view, showing CPU and memory usage, and allowing you to interactively kill or renice processes.

Let’s say you have a service that’s hogging CPU. You’d run top, find the PID, and then maybe renice it to give it lower priority.

$ top
# ... output ...
PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+ COMMAND
12348 user      20   0 123456 7890 4567 R  99.9  0.1   0:15.30 my_cpu_hog
# ... other processes ...

To reduce its priority (making it "nicer" to other processes), you’d use renice. Lower NI (nice value) means higher priority. We want to increase the nice value to decrease priority. Let’s give it a nice value of 10.

$ renice +10 12348
12348: old priority 0, new priority 10

Now, my_cpu_hog will consume less CPU.

The relationship between jobs, fg, bg, kill, pkill, pgrep, ps, top, and renice forms the bedrock of Bash process control. You start something with & for backgrounding, monitor it with ps or top, bring it to the foreground with fg if needed, and terminate it with kill or pkill when necessary, often after adjusting its priority with renice.

A subtle point many overlook is that SIGINT (Ctrl+C or kill -2) is a request to terminate. A well-behaved program will catch this signal, perform cleanup (like saving state or closing files), and then exit. However, a program can choose to ignore SIGINT entirely, making SIGKILL (kill -9) the only way to stop it, but at the cost of that cleanup.

The next logical step is understanding how to group processes and manage them as a unit, often through shell scripting or more advanced tools like screen or tmux.

Want structured learning?

Take the full Bash course →