Processes from the Terminal

Table of Contents

  1. Every Command Is a Process
  2. Seeing What's Running: ps
  3. A Live View: top and htop
  4. Stopping a Process: Ctrl + C and kill
  5. Foreground and Background
  6. Exit Codes: Did It Work?
  7. "Port 3000 Is Already in Use"
  8. 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 dev starts 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:

ColumnMeaning
USERWho owns the process
PIDIts process ID
%CPU / %MEMHow much CPU and memory it's using right now
COMMANDThe 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 ps is a simpler version that doesn't understand aux and 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.

SignalSent byMeaning
SIGINTCtrl + C"Please stop" (from the keyboard)
SIGTERMkill PID"Please shut down"
SIGKILLkill -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:

CommandWhat it does
command &Start in the background
Ctrl + ZPause the foreground process and put it in the background, stopped
bgResume a paused job, running in the background
fgBring a background job back to the foreground
kill %1Stop 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, run jobs.

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:

  • 0 means 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. Use netstat -ano | grep :3000 to find the PID (the last column), then stop it with taskkill //PID 19132 //F. The double slashes are a Git Bash quirk.

8. Try It Yourself

Start each exercise from ~/cli-practice.

  1. Run ps, then ps aux | grep zsh (or bash). Can you find your own shell's PID?
  2. Open top, watch it for a few seconds, and quit with q. Which process is using the most CPU?
  3. Start sleep 300 & twice. Use jobs to see both, then stop one with kill %1 and the other by finding its PID with ps.
  4. Run sleep 300, pause it with Ctrl + Z, resume it in the background with bg, check jobs, and finally stop it with fg and Ctrl + C.
  5. Run ls notes and ls nothere, checking echo $? after each.
  6. Recreate the "port already in use" problem with python3 -m http.server 3000 &. Find the process with lsof -i :3000, stop it with kill, 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!