Table of contents
- A Process Is a “Living Program”
- PIDs and the Process Tree
- ps — Snapshot View
- top, htop — Real-Time Observation
- Signals — Talking to Processes
- kill — The Name Is Harsh, but It Doesn’t Always Kill
- & — Background Execution
- jobs, fg, bg — Job Control
- nohup — Surviving Session Disconnects
- disown — Freeing an Already Launched Process
- Practical Process Commands
- Recap of This Part
A Process Is a “Living Program”
The ls binary file stored on disk is just a bundle of bits. The moment someone executes that file, it gets loaded into memory, the CPU processes instructions, file descriptors are opened, and signals are received. This “running instance of a program” is called a process.
Everything developers deal with daily is ultimately a process. The Spring application launched with java -jar app.jar, the web server running as nginx, the shell you connected through ssh — all processes. The problem is that jumping into operations without knowing “how processes live, how they die, and how they run in the background” will get you stuck in unexpected places. Why does an app ignore the termination signal? Why does a process launched from a CI script die when the session ends? This part looks behind those baffling moments.
PIDs and the Process Tree
Every process receives a unique PID (Process ID). PID 1 belongs to the very first process that starts at system boot. On modern distributions, systemd claims PID 1, while inside Docker containers, the user-specified initial process becomes PID 1.
Every process (except PID 1) has a parent process. Following parent PIDs (PPIDs) upward always leads to PID 1. That’s why Linux processes form a giant tree structure.
flowchart TB
INIT["PID 1<br/>systemd"]
INIT --> SSHD["sshd<br/>PID 900"]
INIT --> NGINX["nginx (master)<br/>PID 1200"]
INIT --> DOCKERD["dockerd<br/>PID 1500"]
SSHD --> BASH["bash (my shell)<br/>PID 3100"]
NGINX --> NW1["nginx (worker)<br/>PID 1201"]
NGINX --> NW2["nginx (worker)<br/>PID 1202"]
BASH --> VIM["vim<br/>PID 4200"]
BASH --> NODE["node app.js<br/>PID 4300"]
To view this tree directly, use pstree.
pstree -p
# systemd(1)─┬─sshd(900)───bash(3100)─┬─vim(4200)
# │ └─node(4300)
# ├─nginx(1200)─┬─nginx(1201)
# │ └─nginx(1202)
# └─dockerd(1500)─...
Your shell’s PID is stored in $$, and the parent PID is in $PPID.
echo "Current shell PID: $$"
echo "Parent PID: $PPID"
The parent process plays an important role. When a child finishes, the parent must collect the exit status. A child whose status hasn’t been collected becomes a zombie, and a child whose parent died first becomes an orphan that PID 1 adopts. This is exactly why Docker uses init processes like tini — if the app that becomes PID 1 in a container doesn’t have zombie-reaping capabilities, leaks will occur.
ps — Snapshot View
The tool for sweeping through currently running processes at a glance is ps (Process Status). Its options are a bit confusing because two families (BSD and System V) got mixed together for historical reasons. A few combinations are enough for practice.
# Processes running in my shell (BSD style, shortest)
ps
# All system processes, verbose (BSD)
ps aux
# All system processes + parent-child relationships (System V)
ps -ef
# Search by name
ps aux | grep nginx
Decoding the columns of ps aux beforehand is helpful.
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
alice 4300 1.2 0.5 512M 32M pts/0 S+ 10:30 0:03 node app.js
USER: ownerPID: process ID%CPU,%MEM: CPU/memory usage percentageVSZ: virtual memory size (reserved via malloc but may not actually be used)RSS: actual physical memory (Resident Set Size). This is the memory truly being usedTTY: the terminal this process is attached to.?means it runs without a terminal, like a daemonSTAT: state.R(running),S(sleep),D(uninterruptible sleep),Z(zombie),T(stopped), etc.TIME: cumulative CPU timeCOMMAND: the executed command
top, htop — Real-Time Observation
If ps is a snapshot, top is a live camera. It refreshes every second, showing the top CPU and memory consuming processes.
top
# q to quit, k to send a signal to a specific PID, M for memory sort, P for CPU sort
top comes pre-installed on all distributions, but its UI is crude. It’s common to install htop, which supports colors and mouse interaction.
# Debian/Ubuntu
sudo apt install htop
# RHEL/CentOS
sudo dnf install htop
# macOS
brew install htop
htop
# F3 search, F9 send signal, F5 tree view, F6 sort
The first thing you open on a production server when asking “what’s eating the CPU?” is top or htop. They provide “live” information on an entirely different level from what developers see running locally.
Signals — Talking to Processes
The messages Linux sends to processes are called signals. There are over 30 types, but only about half a dozen come up frequently in practice.
flowchart LR
SENDER["Sender<br/>(user, kernel, another process)"]
SENDER -->|SIGTERM / SIGKILL / SIGHUP ...| RECEIVER["Receiving process"]
RECEIVER --> REACT{"How does it react?"}
REACT --> DEFAULT["Default action<br/>(usually terminate)"]
REACT --> HANDLER["Handler<br/>(executes if defined)"]
REACT --> IGNORE["Ignore<br/>(except SIGKILL/SIGSTOP)"]
Frequently Used Signals
| Name | Number | Meaning | Can be handled? |
|---|---|---|---|
SIGHUP | 1 | Terminal connection lost. Daemons repurpose it as a “reload config” signal | Yes |
SIGINT | 2 | Interrupt. Ctrl+C in terminal | Yes |
SIGQUIT | 3 | Quit + core dump. Ctrl+\ | Yes |
SIGKILL | 9 | Immediate forced termination. Cannot be ignored or caught | No |
SIGTERM | 15 | ”Please shut down gracefully.” Default for kill | Yes |
SIGSTOP | 19 | Forced pause. Cannot be ignored | No |
SIGTSTP | 20 | Ctrl+Z in terminal | Yes |
SIGCONT | 18 | Resume a stopped process | Yes |
SIGTERM vs SIGKILL. Anyone working in DevOps must understand this difference.
SIGTERM(15): “I’ll give you time to clean up, so come down on your own.” The app can close open DB connections, finish in-flight requests, clean up temporary files, then exitSIGKILL(9): “Die immediately.” The app gets no opportunity at all. The kernel reclaims the process’s memory and that’s it. Since cleanup code doesn’t have time to run, DB locks can remain or files can become corrupted
Kubernetes follows this flow when taking down a Pod too. It first sends SIGTERM, waits for terminationGracePeriodSeconds (default 30 seconds), and if the process is still alive, finishes with SIGKILL. That’s why apps that don’t support graceful shutdown leave connection errors every time they’re deployed in a Kubernetes environment.
kill — The Name Is Harsh, but It Doesn’t Always Kill
The name of the kill command is a historical artifact. In early Unix, most signals that could be sent to a process meant “terminate,” hence the name. In reality, it’s a tool that can send any signal.
# Default is SIGTERM (15)
kill 4300
# Explicitly specify a signal (by name or number)
kill -TERM 4300
kill -15 4300
# Forced termination — only when truly necessary
kill -KILL 4300
kill -9 4300
# Reload configuration (supported by some services)
kill -HUP 4300
# List all available signals
kill -l
To kill by name all at once, use pkill or killall. Be careful since these use name matching.
# SIGTERM to all processes with "node" in the name
pkill node
# Only those with the exact name "node"
killall node
# Only a specific user's processes
pkill -u alice
Accidentally typing pkill -9 java in production can kill multiple applications at once — a catastrophe. Build the habit of verifying the targets with pgrep -a node before sending any signal.
pgrep -a node
# 4300 node app.js
# 4450 node worker.js
& — Background Execution
Appending & to a command sends it to the background. The shell doesn’t wait for the process to finish and immediately returns the prompt.
# Foreground — the shell is blocked until the command finishes
./long-task.sh
# Background — prompt returns immediately
./long-task.sh &
# [1] 4600
# ^ ^
# │ └─ PID
# └───── job number
jobs, fg, bg — Job Control
The built-in command set jobs/fg/bg manages background processes launched from the same shell.
# Run
sleep 300 &
# [1] 4700
sleep 500 &
# [2] 4750
# List background jobs in the current shell
jobs
# [1]- Running sleep 300 &
# [2]+ Running sleep 500 &
# Bring job 1 to the foreground
fg %1
# Press Ctrl+Z in the foreground to stop with SIGTSTP
# To resume a stopped job in the background
bg %1
Job numbers are referenced with the %n syntax. You can also send signals by job number, like kill %1.
However, this world is only valid within the shell session. When the shell exits, the job list is gone. That’s why long-running tasks need a different approach, as described next.
nohup — Surviving Session Disconnects
If you SSH in, throw a job to the background with ./deploy.sh &, and then close the SSH connection, you might think the job will keep running — but it usually dies. Why?
The culprit is SIGHUP. When an SSH connection drops, the kernel sends SIGHUP to child processes connected to that terminal. Since the default action is “terminate,” the job dies along with the session.
The solution is nohup (NO HangUP). Wrapping a command with it configures the process to ignore SIGHUP.
nohup ./deploy.sh > deploy.log 2>&1 &
# nohup: ignoring input and appending output to 'nohup.out'
If you don’t redirect output, nohup automatically writes to a nohup.out file. To specify a log file, redirect explicitly as shown above with > log.txt 2>&1. Here, 2>&1 means “send stderr (2) to the same place as stdout (1)” — detailed redirection is covered in Part 4.
However, nohup is the old-fashioned way. Modern practice more commonly uses tools that detach the session itself.
Better Alternatives: tmux, screen, systemd
- tmux / screen: Terminal multiplexers. Create a session, run your task there, then
detach— even if SSH disconnects, the session stays alive. Reconnect andattachto pick up right where you left off
# Create a tmux session
tmux new -s deploy
# Run ./deploy.sh inside the session
# Press Ctrl+b then d — detach
# SSH can disconnect and it won't die
# Reattach
tmux attach -t deploy
- systemd service: For truly long-running services, register them as a
systemdunit. It handles auto-start on reboot, restart on crash, and integrated logging. If “it needs to stay up as a service,” the answer issystemd, notnohup
disown — Freeing an Already Launched Process
You forgot to use nohup and just launched with &. If you disconnect SSH now, it’ll die. Is there anything you can do?
Yes. disown removes the process from the shell’s job table and prevents it from receiving SIGHUP.
./deploy.sh &
# [1] 4800
jobs
# [1]+ Running ./deploy.sh &
disown %1
# Or all current jobs: disown -a
jobs
# (empty — detached)
# Now SSH can disconnect and PID 4800 will survive
Practical Process Commands
Let’s collect frequently used combinations.
# Who is using a specific port
sudo lsof -i :8080
sudo ss -ltnp | grep :8080 # More modern
# Files opened by a specific process
sudo lsof -p 4300
# Detailed info for a specific PID
cat /proc/4300/status | head
cat /proc/4300/cmdline | tr '\0' ' ' # Full command line at launch
ls -l /proc/4300/cwd # Working directory
ls -l /proc/4300/exe # Executable path
# Check if killed by OOM due to excessive memory usage
dmesg | grep -i "out of memory"
# Or recent logs
journalctl -k | grep -i oom
The files under /proc/<PID>/ are virtual files dynamically generated by the kernel, as mentioned in Part 1. A significant portion of what monitoring tools do is reading values from here and presenting them nicely.
Recap of This Part
The ground we’ve covered.
- Processes are linked by PID and parent PID into a single tree
- Use
ps aux,top,htopto observe what’s currently running - Signals are messages sent to processes.
SIGTERMmeans “clean up and come down,”SIGKILLmeans “terminate immediately” &for background,jobs/fg/bgfor job control. To survive across sessions, usenohuportmux; for real services, usesystemd- To check port usage or open files, use
lsof,ss, and/proc
In the next part, we’ll cover the core philosophy Linux has maintained for 50 years: text processing and pipes. We’ll look at the power that emerges when grep, sed, awk, and | are combined.

Loading comments...