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 init belongs to root: 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.js process with 12.4 in %CPU is 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. VSZ is all the memory the process could potentially use, including what it never touches; RSS is the physical memory it is really occupying right now. RSS is 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 like dockerd; pts/0 means it is running in your current terminal.
  • STAT: the process's state, the column you are going to look at the most to diagnose. S is interruptible sleep (waiting on something, like keyboard input); R is actively running; Z is a zombie process — it already finished, but its parent has not yet read its exit code; T is 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; l indicates it uses several internal threads; s marks 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:

SignalNumber (typical on Linux x86)What it meansWho triggers it
SIGINT2Interrupt from the keyboardCtrl+C
SIGTSTP20Stop, but you can resume laterCtrl+Z
SIGTERM15Request to terminate, with a chance to clean upkill with no flags (it is the default)
SIGKILL9Immediate termination, no possible exceptionkill -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+Z sends SIGTSTP to the foreground process: it pauses it entirely (it stops consuming CPU) and hands you back control of the terminal immediately.
  • jobs lists the paused or backgrounded jobs belonging to your current shell session, with a job number for each one (%1, %2, …).
  • bg %n resumes job number n so it keeps running, but in the background — without blocking your terminal.
  • fg %n brings job number n back 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
  1. Ctrl+Z — pauses the process and hands back terminal control immediately. At this point the training is stopped, not running.
  2. jobs — confirms the process shows up as "Stopped" and finds its job number (for example, %1).
  3. 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
  1. lsof -i :5000 — to identify exactly which process is listening on that port, along with its PID and owning user.
  2. With the PID identified, kill PID (no flags) to ask it to terminate respectfully.
  3. Verify by running lsof -i :5000 again: if it no longer prints any line, the port is free.
  4. Only if the process is still showing up after waiting a few seconds, escalate to kill -9 PID.
  5. 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 aux and identify, unassisted, any listed process's PID, %CPU, and state (STAT);
  • explain why plain kill with no flags should always be your first attempt, and in what specific situation escalating to kill -9 is 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 (or ss -tulpn on 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