Processes from the Terminal
Table of Contents
- Every Command Is a Process
- Seeing What's Running:
ps - A Live View:
topandhtop - Stopping a Process:
Ctrl + Candkill - Foreground and Background
- Exit Codes: Did It Work?
- "Port 3000 Is Already in Use"
- Try It Yourself
1. Every Command Is a Process
In Processes you learned that a process is a running program, with its own PID (process ID), its own memory, and a parent that started it. You watched them in Task Manager or Activity Monitor.
Every command you've typed in this series has been a process. When you
run ls, the shell starts a child process, ls does its job in a few
milliseconds, and exits. You never noticed, because it was over almost
instantly.
But many of the programs you'll run as a developer don't finish by themselves:
npm run devstarts a development server that runs until you stop it.tail -f(chapter 5) watches a file forever.- A database or a Docker container runs for days.
For these, you need to know how to see what's running, stop it, and run
it without blocking your terminal. To have something to practise on,
we'll use sleep, which does nothing at all for the number of seconds
you give it. As always, work from your practice folder:
cd ~/cli-practice
sleep 5
For five seconds the prompt doesn't come back. The shell is waiting for
the sleep process to finish.
2. Seeing What's Running: ps
ps (process status) lists processes. On its own, it shows only the
ones started from this terminal:
ps
PID TTY TIME CMD
1934 ttys001 0:00.66 -zsh
That's your shell itself. To see every process on the computer, from
every user, add aux:
ps aux
That's hundreds of lines. The columns you care about:
| Column | Meaning |
|---|---|
USER | Who owns the process |
PID | Its process ID |
%CPU / %MEM | How much CPU and memory it's using right now |
COMMAND | The command that started it |
You'll almost never read ps aux in full. Instead, pipe it into grep
(chapter 6) to find the process you're looking for:
ps aux | grep node
The results often include one extra line — the grep node command
itself, which is also a process with node in its name. Ignore it.
Git Bash: its
psis a simpler version that doesn't understandauxand only shows Git Bash's own processes. Use Task Manager for the full picture, or use WSL.
3. A Live View: top and htop
ps is a snapshot. top is the terminal version of Task Manager: a
live view that refreshes every few seconds, with the busiest processes
at the top:
top
Press q to quit — the same key as less and man.
Many developers prefer htop, a friendlier version with colours,
CPU bars, and mouse support. It's not always installed, but it's easy to
add:
sudo apt install htop # Ubuntu and WSL
brew install htop # macOS, if you use Homebrew
(sudo and installing software come up in chapter 10.)
4. Stopping a Process: Ctrl + C and kill
Ctrl + C
You've been using it since chapter 2. Start something long:
sleep 100
Press Ctrl + C. The prompt comes back immediately.
What actually happens is that the terminal sends the process a
signal — a small message from the operating system — called
SIGINT ("interrupt"). Most programs react by cleaning up and
exiting. That's how you stop npm run dev, tail -f, and almost
anything else running in the foreground.
kill
What if the process isn't in front of you — it's running in another
terminal, or in the background, or it's frozen? Find its PID, then use
kill:
ps aux | grep sleep
kill 12345 # use the real PID from the line above
Despite the name, kill just sends a signal. By default it sends
SIGTERM ("terminate"), which politely asks the process to shut down.
Well-behaved programs save their work, close their files, and exit.
If a process is truly stuck and ignores that request, you can send
SIGKILL with -9:
kill -9 12345
SIGKILL can't be caught or ignored: the operating system simply removes
the process. That's also why it's a last resort — the program gets
no chance to clean up, so files it was writing can be left half-written.
Always try a plain kill first.
| Signal | Sent by | Meaning |
|---|---|---|
| SIGINT | Ctrl + C | "Please stop" (from the keyboard) |
| SIGTERM | kill PID | "Please shut down" |
| SIGKILL | kill -9 PID | "You're gone" — can't be refused |
You can also stop processes by name instead of PID, with pkill:
pkill sleep
Be careful: it stops every process with that name that you own.
5. Foreground and Background
Normally a command runs in the foreground: it owns your terminal
until it finishes, and you can't type another command. Add & at the
end to run it in the background instead:
sleep 300 &
[1] 23456
The prompt comes straight back. The shell prints a job number ([1])
and the PID. The process keeps running while you carry on working.
jobs lists the background jobs started from this terminal:
jobs
[1] + running sleep 300
You can move jobs back and forth:
| Command | What it does |
|---|---|
command & | Start in the background |
Ctrl + Z | Pause the foreground process and put it in the background, stopped |
bg | Resume a paused job, running in the background |
fg | Bring a background job back to the foreground |
kill %1 | Stop job number 1 (note the %) |
Try it:
sleep 300
Press Ctrl + Z — the prompt returns and the shell says the job is
suspended. Run jobs to see it, fg to bring it back, then
Ctrl + C to stop it for good.
Watch out for
Ctrl + Z: beginners sometimes press it thinking it means "undo" or "quit". It only pauses the program, which keeps running in memory — and if it was a server, it keeps holding its port (see section 7). If a command "disappeared" and things are acting strangely, runjobs.
When you close the terminal window, the processes started from it are usually stopped too. That's why servers and long-running tools are normally run by a process manager, or inside Docker, rather than from someone's open terminal.
6. Exit Codes: Did It Work?
When a process finishes, it hands back a number to its parent, called the exit code (or exit status). The rule is simple:
0means success.- Anything else means something went wrong. Different numbers can mean different problems.
The shell keeps the exit code of the last command in a special variable,
$?:
ls notes
echo $?
0
ls nothere
echo $?
1
(Linux's ls returns 2 here instead — any non-zero number means
failure.)
You won't check $? by hand very often, but exit codes are how
everything decides whether a step worked: shell scripts (chapter
11), Git hooks, CI pipelines that run your tests on GitHub, and Docker
health checks. When a test runner "fails the build", it does it by
exiting with a non-zero code.
You can also use them to chain commands. && runs the second command
only if the first one succeeded:
mkdir -p projects/api && cd projects/api
If mkdir fails, the cd never runs. You'll see && constantly in
install instructions, like npm install && npm run dev.
7. "Port 3000 Is Already in Use"
Here's a problem every web developer hits, usually in the first week. You start your app and get:
Error: listen EADDRINUSE: address already in use :::3000
A port is a numbered "door" a program opens on your computer to
receive network connections — your dev server listens on port 3000, a
database might use 5432. Only one process can listen on a port at a
time. The error means some other process already has port 3000 —
very often, an old copy of your own server that you forgot in another
terminal, or paused with Ctrl + Z.
To see the problem for yourself, start a tiny web server on port 3000
in the background (Python is included in Ubuntu and WSL; on macOS, the
first run of python3 may offer to install Apple's developer tools —
accept it, you'll need them for Git anyway):
python3 -m http.server 3000 &
Now try to start a second one:
python3 -m http.server 3000
OSError: [Errno 48] Address already in use
(Linux says Errno 98; the meaning is the same.) To find out who
has the port, use lsof (list open files — on Unix, network
connections count as files):
lsof -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 19132 dara 5u IPv6 0x... 0t0 TCP *:3000 (LISTEN)
There's the PID. Now you know what to do:
kill 19132 # use your real PID
Run lsof -i :3000 again — it prints nothing, so the port is free.
If lsof isn't installed on Ubuntu or WSL, add it with
sudo apt install lsof.
Git Bash: there's no
lsof. Usenetstat -ano | grep :3000to find the PID (the last column), then stop it withtaskkill //PID 19132 //F. The double slashes are a Git Bash quirk.
8. Try It Yourself
Start each exercise from ~/cli-practice.
- Run
ps, thenps aux | grep zsh(orbash). Can you find your own shell's PID? - Open
top, watch it for a few seconds, and quit withq. Which process is using the most CPU? - Start
sleep 300 &twice. Usejobsto see both, then stop one withkill %1and the other by finding its PID withps. - Run
sleep 300, pause it withCtrl + Z, resume it in the background withbg, checkjobs, and finally stop it withfgandCtrl + C. - Run
ls notesandls nothere, checkingecho $?after each. - Recreate the "port already in use" problem with
python3 -m http.server 3000 &. Find the process withlsof -i :3000, stop it withkill, and confirm the port is free.
You can now see, stop, and juggle the programs you run. Next, let's
solve the mystery behind command not found: environment variables and
PATH.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!