Module 4: Permissions, Processes, and Environment
5. Processes, jobs, and signals: ps, top, kill, and jobs
Description
By the end of this lesson you will be able to list any process running on your system and read its PID, its CPU and memory usage, and its parent-child relationship; send it the right signal based on what you actually want to accomplish — not always the most aggressive one; suspend a process, send it to the background, and bring it back to the foreground without losing its work; and, the most concrete case of all, find exactly which process is holding a port and end it with judgment.
This is exactly what happens the next time you run npm run dev, python manage.py runserver, or any local server and the system responds with something like Error: listen EADDRINUSE: address already in use :::3000. Almost every junior's instinctive reaction is to
restart the computer — thirty seconds of waiting to "fix" something that actually gets solved
in five seconds with a command. What left the port busy is almost always a process from an
earlier session that never ended properly: a terminal you closed abruptly, a server you
launched two hours ago and forgot about, a process that got stuck. Diagnosing instead of
restarting is exactly the difference this module promises.
Connection to the module: the previous lesson taught you who can touch which file. This
lesson changes the question: it is no longer "who can," but "what is running right now, and
how does it relate to everything else." You are going to use sudo in some examples when the
process belongs to another user or to the system — you already understand that from the
previous lesson — but today's topic is different: processes, signals, and jobs. You are not
going to touch PATH or environment variables yet; that is exactly what comes next, when you
solve the module's third classic error, command not found.
A process is a badged worker, not a row in a list
Think of your operating system as an office that hires temporary workers for every task. Every time you run a program, the system hires a new worker and hands them a badge with a number nobody else is going to have while that worker stays active. That number is the PID (process ID). No worker appears out of nowhere: someone else hired them — another worker who was already active — and that someone is their parent, identified by the PPID (parent PID). When you type a command in your terminal and press Enter, it is your own shell — which is, in turn, a process with its own PID — that hires the new worker to run exactly that task. If you close the shell, in most cases the workers it hired leave too: that is why sometimes closing a terminal instantly kills everything that was running inside it.
Formally: a process is a running instance of a program. It is not the program itself — the
node file on disk is a single program, but you can have ten node processes running at the
same time, each with its own PID, its own memory, and its own state. Every process has, at
minimum: a unique PID, a PPID pointing to whoever created it, an owning user (the identity you
saw in this module's lesson 2), a state, and a share of CPU and memory it is using right now.
Worked example
The most direct tool for seeing every active worker is ps (process status). With the aux
options — no dash, it is a historical BSD convention — you see the full process list from
every user, not just yours:
ps aux
What to expect (trimmed output; on your machine dozens more lines are going to show up):
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 16800 9312 ? Ss 09:03 0:01 /sbin/init
dev 842 0.2 0.5 715200 42112 ? Sl 09:04 0:03 /usr/bin/dockerd
dev 2210 0.0 0.0 9028 5120 pts/0 Ss 09:14 0:00 -bash
dev 3391 12.4 2.1 981200 168420 pts/0 Sl+ 10:02 1:47 node server.js
dev 3402 0.0 0.0 9028 1560 pts/0 S+ 10:02 0:00 grep node
Read it column by column, left to right:
- USER: the process's owner — the same identity model you saw in lesson 2. Notice
initbelongs toroot: it is the first process that starts when the machine boots, and the direct or indirect parent of every other one. - PID: that worker's unique badge. It is the number you are going to need when you want to send it a signal later in this lesson.
- %CPU and %MEM: how much processor and how much memory that process is consuming
right now, as a percentage. The
node server.jsprocess with12.4in%CPUis the clearest suspect if your fan suddenly kicked in. - VSZ (virtual size) and RSS (resident set size): two different ways of measuring
memory, both in kibibytes.
VSZis all the memory the process could potentially use, including what it never touches;RSSis the physical memory it is really occupying right now.RSSis almost always the number you care about when you suspect a memory leak. - TTY: the terminal the process is tied to. A question mark (
?) means it has no associated terminal — the normal case for a system service likedockerd;pts/0means it is running in your current terminal. - STAT: the process's state, the column you are going to look at the most to diagnose.
Sis interruptible sleep (waiting on something, like keyboard input);Ris actively running;Zis a zombie process — it already finished, but its parent has not yet read its exit code;Tis stopped by a job-control signal, exactly this lesson's later section's topic. The+symbol next to the state means the process is running in its terminal's foreground;lindicates it uses several internal threads;smarks the session leader. - START and TIME: when the process started, and how much accumulated CPU time it has used — not how long it has been alive, but how much processor time it actually used.
- COMMAND: the exact command it was launched with, arguments included.
Notice the last line: grep node shows up in the output itself, with %CPU at zero. It is
not a coincidence or a bug: you were searching for "node" with a grep, and grep's own
process has the word "node" in its command line, so it finds itself. It is the same kind of
surprise you already saw with grep in module 2, applied this time to processes instead of
lines of text.
To avoid that noise and not have to read a full list, pgrep searches directly by process
name and hands you back only the matching PIDs, with no grep of itself in the way:
pgrep -l node
What to expect:
3391 node
-l adds the process's name next to the PID, so you do not have to guess what each number
corresponds to. If you need to search for something that is not the program's name but a
fragment of its arguments — for example, the exact script's name — -f widens the search to
the full command line:
pgrep -fl "server.js"
What to expect:
3391 node server.js
Watching usage live: top and htop
ps aux is a snapshot: the exact state of every process at the instant you ran it, and
nothing more. If you want to see how CPU and memory usage change second by second — for
example, to confirm whether a process really is consuming more and more resources or it was a
momentary spike — you need something that refreshes on its own. That is what top exists for:
top
What to expect: a screen that refreshes on its own, every few seconds, with a system
summary at the top (average load, total and used memory) and, below, the same kind of columns
you already know from ps aux — PID, USER, %CPU, %MEM, COMMAND — but sorted by CPU usage
largest to smallest, so the process demanding the most from the machine always stays on top,
in view. You quit with q. The internal shortcuts for reordering, filtering, or killing a
process from right there vary a bit between the macOS and Linux versions — the most reliable
way to learn them is pressing ? or h inside the program itself, exactly the habit of
reading the help you already practiced in module 1.
htop is an alternative with a colored interface, per-core CPU usage bars, and navigation
with the mouse or arrow keys, which most people find more comfortable to read than top. It
does not come preinstalled on every distribution or on macOS — it gets installed with your
system's package manager, a topic outside this guide's scope. If you already have it
available, it is worth trying; if not, top does exactly the same job with a plainer
interface.
Signals: asking a process for something, not hitting it
Before touching kill, it is worth correcting the idea almost everyone shows up with:
kill does not directly kill a process. It sends a signal — a short message,
identified by a name and a number, that the operating system delivers to the process. What
happens next depends on which signal it is and how that process is written: most signals can
be caught, ignored, or handled with custom code before terminating; only one cannot.
The ones you are going to use the most:
| Signal | Number (typical on Linux x86) | What it means | Who triggers it |
|---|---|---|---|
SIGINT | 2 | Interrupt from the keyboard | Ctrl+C |
SIGTSTP | 20 | Stop, but you can resume later | Ctrl+Z |
SIGTERM | 15 | Request to terminate, with a chance to clean up | kill with no flags (it is the default) |
SIGKILL | 9 | Immediate termination, no possible exception | kill -9 |
The exact numbers can vary depending on the architecture or operating system — that is why in
practice almost nobody memorizes them and almost everyone uses the name or, at most, the
number for SIGTERM and SIGKILL, which are stable on the vast majority of systems.
Ctrl+C sends SIGINT to the process you currently have in the foreground. By default,
SIGINT terminates the process — but the process can decide to catch that signal and do
something else: save its state, ask "are you sure you want to quit?", or flat-out ignore it.
That is why sometimes you press Ctrl+C and the process stays alive: the shortcut did not
fail, the process decided not to obey that particular signal.
kill PID sends SIGTERM by default: a polite request to terminate, which gives the process
the chance to close open files, release locks, save its state, or cleanly disconnect from a
database before leaving. kill -9 PID (equivalent to kill -SIGKILL PID) sends a signal
neither the process nor the kernel itself can postpone: the process ends immediately, at the
exact point it was at, with no chance to clean up anything.
Worked example
You go back to the node server.js process with PID 3391 you saw earlier with ps aux. You
ask it to terminate, as respectfully as possible:
kill 3391
What to expect: nothing on screen. If you run ps aux | grep node again, that PID no
longer shows up — the process received SIGTERM, had the chance to close its connection and
its files, and ended on its own.
Now imagine a different process, one that really got stuck — waiting on a network operation
that is never going to complete, for example — and does not even respond to SIGTERM after
several seconds of waiting. Only then do you escalate:
kill -9 4820
What to expect: the process disappears immediately, with no possible exception. And that
is exactly why kill -9 is the last resort and not the first: if that process had a file half
written or a database transaction half committed, SIGKILL gave it no chance to close it
carefully — it simply stopped where it was.
Job control: pausing, backgrounding, and bringing back
Your terminal is a single desk: in the foreground, at any given moment, there can only be one process receiving what you type. But that does not mean you have to choose between "wait for it to finish" or "close the terminal and lose everything." You can pause a job, park it, switch to another, or send one off to keep running on its own in the background while you use that same desk for something else.
Four pieces make this possible:
Ctrl+ZsendsSIGTSTPto the foreground process: it pauses it entirely (it stops consuming CPU) and hands you back control of the terminal immediately.jobslists the paused or backgrounded jobs belonging to your current shell session, with a job number for each one (%1,%2, …).bg %nresumes job numbernso it keeps running, but in the background — without blocking your terminal.fg %nbrings job numbernback to the foreground, exactly where you left it.&at the end of a command launches it directly in the background from the start, with it never occupying your terminal.
Worked example
You launch a development server and, as normal, the terminal stays busy showing its output without returning the prompt:
$ npm run dev
> Server listening on port 3000
You need to keep working in that same terminal without shutting the server down. You press
Ctrl+Z:
$ npm run dev
> Server listening on port 3000
^Z
[1]+ Stopped npm run dev
The server got paused, not running in the background yet — note the difference: right now
it is not handling any requests. You confirm with jobs:
$ jobs
[1]+ Stopped npm run dev
You send it to actually run in the background:
$ bg %1
[1]+ npm run dev &
Now it is really running, handling requests, and your prompt is free for whatever else you
need. One thing to keep in mind: if that process keeps printing its own output, those lines
are still going to show up in your terminal even though the job is in the background — a
background job does not stop writing to screen just because you got your prompt back. If that
bothers you, you already know the fix from module 3: next time you launch it, redirect its
output to a file with npm run dev > server.log 2>&1 &, all in one line, straight to the
background from the start.
If later you need to interact with it directly again — for example, to press Ctrl+C and
carefully shut it down — you bring it back to the foreground:
fg %1
What to expect: the server takes over your terminal again exactly as if you had never paused it, showing its live output once more.
Finding and freeing a busy port
A port behaves like a phone line: only one process can be "answering" on a given port at a time. If you try to spin up a second server on the same port, the operating system rejects it immediately — not because the new command is wrong, but because the line is already busy with someone else.
npm run dev
What to expect:
Error: listen EADDRINUSE: address already in use :::3000
The message tells you exactly what the problem is (the address is already in use), but not
who is using it. To find out, lsof (list open files — on Unix, a network socket also
counts as an open file) with -i and the port:
lsof -i :3000
What to expect:
COMMAND PID USER FD TYPE NODE NAME
node 4820 dev 22u IPv6 TCP *:3000 (LISTEN)
There it is: the node process with PID 4820, owned by your own user, listening on port
3000. It is almost certainly a server from an earlier session you never closed. On Linux
systems (WSL included) you also have ss, the modern replacement for the old netstat, which
you are going to see in depth when module 5 gets into network diagnosis — here all you care
about is that it also answers the same question:
sudo ss -tulpn | grep :3000
What to expect:
tcp LISTEN 0 511 0.0.0.0:3000 0.0.0.0:* users:(("node",pid=4820,fd=22))
The PID shows up the same, 4820, this time inside the final parentheses. (If the process
belonged to another user or a system service, both lsof and ss would need the sudo you
already know from the previous lesson to show you its full information.)
With the PID in hand, you apply exactly the previous section's criterion: first ask politely, and only escalate if it does not respond.
kill 4820
What to expect: nothing on screen. You verify the port is free by running lsof -i :3000
again — if it prints no line, the port is free and you can launch your server with no error.
And if you are in a hurry and trust that a direct SIGTERM is going to be enough, you can
chain it all into a single line reusing xargs, which you already know from module 3: -t
asks lsof to print only the PID, with no other columns, ready for xargs to turn it into
kill's argument.
lsof -ti :3000 | xargs kill
What to expect: the same result as before, in a single line: the process holding port
3000 receives SIGTERM and ends, leaving the port free for your next attempt.
Common mistakes
1. Believing kill -9 is the "strongest" option and therefore always the most convenient to
use (conceptual). What happens: the student types kill -9 as a reflex against any
process that does not respond immediately, even on their first attempt. Why it happens:
kill with no flags sounds "weaker" than kill -9, and intuition associates "stronger" with
"better." But SIGTERM (the default) gives the process a chance to close files, release
locks, and save its state before leaving; SIGKILL ends the process in the kernel, at the
exact point it was at, with no chance to clean anything up. How to spot it: if your first
command against any problematic process includes -9, with no wait or attempt at a plain
kill first. How to fix it: always kill PID first, wait a few seconds, and check with
ps whether the process is still alive; only if it is still there, then kill -9. This is
particularly important with any process writing data — a database, a process editing a file —
where an abrupt termination can leave something half-written.
2. Confusing the PID with the job number when using fg or bg. What happens: the
student sees a process's PID (for example with ps aux) and types fg 4820, expecting to
bring it to the foreground, and gets an error along the lines of "no such job." Why it
happens: fg and bg work with job numbers (%1, %2, …), which your shell assigns and
numbers for its own session, not with PIDs, which the kernel assigns globally across the whole
system — they are two completely different numbering systems, even though both are small
numbers that look similar. How to spot it: an error message mentioning "job" despite you
having confirmed with ps that the process exists and is still alive. How to fix it: run
jobs to see the correct job number and prepend the percent sign (fg %1), or simply type
fg with no argument to bring back the most recent job.
3. A background job that needs to read from the keyboard stops on its own. What happens:
you send a process to the background with bg or &, and at some point it stops on its own
with no visible error; running jobs, it shows up with the "Stopped" state instead of
"Running." Why it happens: a background process cannot read directly from your terminal's
keyboard — if at some point it tries to ask for a confirmation or an interactive password, the
system automatically stops it instead of leaving it waiting for input that is never going to
arrive. How to spot it: jobs shows the job stopped (not running) after you sent it to the
background with nothing done on your part. How to fix it: bring it to the foreground with
fg to give it the input it is waiting for, or avoid the problem at the root by launching it
with the flags that skip interactive confirmations before sending it to the background.
Exercises
1. This is ps aux's output on your machine. One process is consuming way more CPU than
normal. Identify it and write the command to end it, always starting with the most respectful
attempt.
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
dev 501 0.1 0.3 45200 12100 ? Ss 08:00 0:02 /usr/lib/systemd/systemd
dev 3120 97.8 4.2 512300 210400 pts/1 R+ 09:45 4:12 python train_model.py
dev 3405 0.0 0.1 10200 3200 pts/0 S+ 09:50 0:00 bash
See solution
The problematic process is python train_model.py, with PID 3120 and 97.8 in the %CPU
column. The right command to start with is the polite request, no flags:
kill 3120
Only if after a few seconds the process is still showing up in ps aux — confirming it
ignored SIGTERM — would you escalate to kill -9 3120.
Why it works: %CPU is the column designed exactly for this — comparing every process's
relative usage at a glance — and starting with SIGTERM gives the process the chance to close
cleanly before forcing its termination with SIGKILL.
2. You are running python train_model.py in the foreground and need the terminal's
control back without stopping the training. Describe the exact sequence of commands you would
use, in order.
See solution
Ctrl+Z— pauses the process and hands back terminal control immediately. At this point the training is stopped, not running.jobs— confirms the process shows up as "Stopped" and finds its job number (for example,%1).bg %1— resumes the process, but in the background, so the training keeps running while you use the terminal for something else.
Why it works: Ctrl+Z and bg are two deliberately separate steps — pausing and resuming
in the background are not the same action — and separating them gives you the chance to
confirm with jobs that you are about to resume the right job before doing it.
3. You try to spin up a server with npm run dev and see Error: listen EADDRINUSE: address already in use :::5000. Describe, step by step, how you would diagnose and solve this
using lsof.
See solution
lsof -i :5000— to identify exactly which process is listening on that port, along with its PID and owning user.- With the PID identified,
kill PID(no flags) to ask it to terminate respectfully. - Verify by running
lsof -i :5000again: if it no longer prints any line, the port is free. - Only if the process is still showing up after waiting a few seconds, escalate to
kill -9 PID. - Relaunch
npm run dev.
Why it works: EADDRINUSE tells you there is a conflict, but never tells you who is
causing it — lsof -i is the tool designed specifically to translate "a port" into "a
concrete PID," which is the piece of data you need in order to act.
4. A rushed teammate runs kill -9 against a database process that at that moment was
writing a transaction to disk. Why is this riskier than having used plain kill with no
flags, and what could have gone wrong?
See solution
kill with no flags sends SIGTERM, a signal the process can catch to close cleanly: finish
the transaction in progress, release its file locks, close its network connections in an
orderly way. kill -9 sends SIGKILL, which neither the process nor the kernel can postpone —
the process stops immediately, at exactly the point it was at, with no chance to finish what
it had halfway done. If the transaction was being written to disk at that instant, the
possible result is a corrupt or inconsistent data file, something a SIGTERM well handled by
the database would have avoided.
Why it works: SIGKILL is intentionally impossible to intercept — that is exactly what
makes it useful as a last resort against a process that truly does not respond — but that same
trait is what makes it dangerous against any process with work half-finished at that moment.
Summary and next step
You can now read your system's full process list with ps aux, understand what each column
means — especially PID, %CPU, %MEM, and STAT — and find a process by name with pgrep with no
confusion from its own search command. You know top shows you that same usage live, and that
kill does not kill a process: it sends it a signal, and SIGTERM (the default) gives it the
chance to close carefully that SIGKILL (-9) never gives — the reason one is the first
attempt and the other is the last resort. You know how to pause a job with Ctrl+Z, send it to
the background with bg, bring it back with fg, and launch something directly in the
background with &. And you know how to solve, with evidence and not superstition, one of the
three errors that give this module its name: finding with lsof -i or ss -tulpn exactly
which process holds a port, and ending it with the same signal judgment you already know.
Before moving on you should be able to:
- run
ps auxand identify, unassisted, any listed process's PID, %CPU, and state (STAT); - explain why plain
killwith no flags should always be your first attempt, and in what specific situation escalating tokill -9is justified; - pause a foreground process, send it to the background, and bring it back to the foreground without losing its work;
- diagnose and free up a busy port using
lsof -i :PORT(orss -tulpnon Linux) to find the responsible PID.
Today you assumed, in every example, that the command existed and started with no problem —
the only obstacle was a busy port or an unresponsive process. The next lesson solves the case
where you do not even get that far: the system tells you command not found even though you
swear the program is installed. There you are going to meet the PATH, the list of folders
your shell searches for every command you type, and why that single concept explains almost
every command not found you are going to see in your career.
Resources
- ps(1) — Linux manual page — full reference
for
ps, including the STAT column's codes and the exact definition of VSZ and RSS. - signal(7) — Linux manual page — the canonical list of standard signals, their numbers, and their default action, including the note on variation between architectures.
- kill(1) — Linux manual page — full
syntax for
kill, including the difference between passing a PID and passing a job number. - Job Control Basics — GNU Bash Reference Manual —
the official explanation of
jobs,fg,bg, and job identifiers (%n). - lsof(8) — Linux manual page — full
documentation for
lsof, including the-ioption for filtering by address or port. - ss(8) — Linux manual page — reference for
ss, the modern replacement fornetstat, with detail on the-t,-u,-l,-p, and-noptions.