CoursesLinux essentialsProcesses & /proc

Processes & /proc

ps, top, and what a process really is.

Beginner12 min · lesson 19 of 25

A program sitting on disk does nothing. It is like a recipe printed in a cookbook: instructions on a page, no cooking happening. A process is what you get when someone actually makes the recipe. A real cook stands at a real burner, using real pans, following the steps one at a time. The file /usr/bin/nginx can sit on disk for years, untouched. The nginx process is alive right now, holding memory, using processor time, and it belongs to a specific user.

When you launch a program, the kernel (the core part of the operating system that talks directly to the hardware) brings that recipe to life. It gives the new process a PID (process ID, a plain number you use to refer to it), writes down which user owns it, and remembers which process asked for it. After that, the kernel keeps tabs on everything the process does: how much memory it holds, which files it has open, whether it is running or asleep. Working out which process owns what, what each one is touching, and why one is wedged is a big part of running Linux day to day.

Every Process Carries An ID Card

Four facts pin down any process. Its PID, the number the kernel stamped on it. Its owner, the user it runs as (identified by a UID, a user ID number), which decides everything it is allowed to touch. Its parent, the process that started it, recorded as the PPID (parent process ID). And its state: running, sleeping, or finished but not yet cleaned up. PID 1 is the one to know by name. It is the first process the kernel starts at boot, the init system (the program in charge of launching and supervising everything else, which on modern Linux is systemd). Every other process is a descendant of PID 1, the way every folder on the machine lives somewhere under the top folder. That family tree matters more than it looks, and you will use it to investigate trouble.

ps: The Snapshot

ps (process status) prints a still photograph of what is running the moment you ask. The workhorse form is ps aux: every process on the box, its owner, how hard it is working the processor and memory, and the command behind it.

~/secopslog — bash
$ ps aux | head -4
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.6 168140 12016 ? Ss 09:14 0:02 /sbin/init root 623 0.0 0.4 15420 9128 ? Ss 09:14 0:00 /usr/sbin/sshd -D www-data 980 0.1 1.2 145600 24880 ? S 09:15 0:03 nginx: worker process

Read that left to right. USER is the owner. PID is the number. %CPU and %MEM are how much of the processor (the CPU, or central processing unit, the chip that does the actual computing) and the RAM (random access memory, the fast working memory) each process is using. VSZ and RSS are two ways of measuring that memory. VSZ (virtual size) is the whole address space the process has mapped, most of which is not really sitting in RAM. RSS (resident set size) is the real physical memory it is holding right now, and it is usually the number you care about. STAT is the state: S means sleeping, R means actively running on the processor. (You will also see a second letter, like the trailing s in Ss, which flags extras such as a process being the leader of its session.) The last column is the command that started it. PID 1 running /sbin/init is systemd, the ancestor of everything else.

A flat list hides the family relationships. Add --forest and ps draws the tree with indentation, so you can watch one process give birth to the next.

~/secopslog — bash
$ ps -ef --forest | head
UID PID PPID C STIME TTY TIME CMD root 1 0 0 09:14 ? 00:00:02 /sbin/init root 623 1 0 09:14 ? 00:00:00 /usr/sbin/sshd -D root 1840 623 0 09:20 ? 00:00:00 \_ sshd: alice [priv] alice 1902 1840 0 09:20 ? 00:00:00 \_ sshd: alice@pts/0 alice 1903 1902 0 09:20 pts/0 00:00:00 \_ -bash alice 1975 1903 0 09:22 pts/0 00:00:00 \_ ps -ef --forest

Read it top to bottom. systemd started the SSH service (secure shell, the encrypted way you log in to a remote machine), which accepted alice's login, which started her shell, which ran the ps command she typed. Every process has a parent, and the chain leads all the way back to PID 1.

When you already know the name and only want the PIDs, pgrep (process grep) beats scanning a wall of text. The -a flag prints the full command line beside each PID, not only the number.

~/secopslog — bash
$ pgrep -a sshd
623 /usr/sbin/sshd -D 1840 sshd: alice [priv] 1902 sshd: alice@pts/0

top: The Live View

ps freezes one instant. top keeps a live view that redraws every few seconds, sorted so the hungriest process sits on top. It is the first thing you open when a machine feels slow, because it answers one question fast: what is eating this box right now?

~/secopslog — bash
$ top
top - 09:44:12 up 30 min, 1 user, load average: 1.02, 0.61, 0.28 Tasks: 109 total, 2 running, 107 sleeping, 0 stopped, 0 zombie %Cpu(s): 98.9 us, 0.7 sy, 0.0 ni, 0.2 id, 0.0 wa, 0.0 hi, 0.2 si, 0.0 st MiB Mem : 1987.4 total, 118.9 free, 902.1 used, 966.4 buff/cache MiB Swap: 975.0 total, 973.0 free, 2.0 used. 920.7 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3187 www-data 20 0 924136 43980 3120 R 98.7 2.1 4:12.55 kinsing 980 www-data 20 0 145600 24880 14200 S 0.3 1.2 0:03.42 nginx 1 root 20 0 168140 12016 9004 S 0.0 0.6 0:02.11 systemd

The header lines summarize the whole machine: load, task counts, processor use, memory. Below that is one row per process, busiest first. Press M to sort by memory, P to go back to sorting by processor, and q to quit. If htop is installed it is the friendlier version, with colored bars and arrow-key scrolling. Right now the top row is the whole story: a process named kinsing, owned by www-data, burning a full core. www-data is the low-privilege user the web server runs as, and it has no business running a busy program that came out of nowhere. Time to open it up.

pkill matches more than you mean
pkill and pgrep match the process name as a substring by default, not as an exact word. Running pkill fire to stop firefox will also cut down firewalld and anything else with those letters in its name. Before killing by name, run pgrep -a <name> to see the full list you are about to hit, or kill by exact PID. On a production box, the wrong pkill is its own outage.

/proc: The Process Turned Inside Out

Linux tries to make everything look like a file, and live processes are no exception. The kernel keeps a special directory called /proc, and inside it every running process has a folder named after its PID. These are not real files on any disk. They are live windows the kernel opens onto a process, built the instant you read them, showing its exact state. You open them with the same cat and ls you already use for ordinary files. For our suspicious process at PID 3187, four of these files answer almost everything.

First question: what is actually running? /proc/<pid>/exe is a link that points at the real binary on disk, no matter what name the process gave itself. Reading files under another user's process usually needs root, so these run with sudo (run one command as the superuser).

~/secopslog — bash
$ sudo ls -l /proc/3187/exe
lrwxrwxrwx 1 www-data www-data 0 Jul 17 09:46 /proc/3187/exe -> '/tmp/.cache/kinsing (deleted)'

Two red flags in one line. The binary lives in /tmp, a world-writable scratch folder where nothing legitimate should be running from. And the (deleted) tag means the file was unlinked from disk while the program keeps running from memory, a standard trick for hiding malware from anyone who later goes looking for the file. A normal service points at something like /usr/sbin/nginx, with no (deleted) beside it.

Next: how was it started, and with which arguments? /proc/<pid>/cmdline holds the exact command line.

~/secopslog — bash
$ sudo cat /proc/3187/cmdline | tr '\0' ' '; echo
/tmp/.cache/kinsing -c /tmp/.cache/config.json

That tr in the command is doing real work. The kernel stores the arguments separated by null bytes (invisible zero characters), not spaces, so cat on its own would smash them together into one blob. tr '\0' ' ' swaps each null for a space so a human can read it. Here the program was launched pointing at a config file, also parked in /tmp.

Now: what is it touching? Every open file, pipe, and network connection a process holds is listed under /proc/<pid>/fd as numbered links. fd stands for file descriptor, the small integer handle a program uses for anything it has opened.

~/secopslog — bash
$ sudo ls -l /proc/3187/fd
total 0 lrwx------ 1 www-data www-data 64 Jul 17 09:46 0 -> /dev/null lrwx------ 1 www-data www-data 64 Jul 17 09:46 1 -> 'pipe:[52117]' lrwx------ 1 www-data www-data 64 Jul 17 09:46 2 -> 'pipe:[52117]' lr-x------ 1 www-data www-data 64 Jul 17 09:46 3 -> /tmp/.cache/config.json lrwx------ 1 www-data www-data 64 Jul 17 09:46 4 -> 'socket:[52140]' lrwx------ 1 www-data www-data 64 Jul 17 09:46 5 -> 'socket:[52141]'

Numbers 0, 1, and 2 are always standard input, output, and error. What matters here are the entries pointing at socket:[...]: those are open network connections. A quiet program with live sockets is talking to something across the network, which for a miner is usually a mining pool or a command server telling it what to do. fd 3 is open read-only against that /tmp config file, which is why its link shows lr-x rather than lrwx.

Last: who owns it, and who started it? /proc/<pid>/status is a plain readout of the process's identity, and grep pulls out the lines that matter.

~/secopslog — bash
$ sudo grep -E '^(Name|State|PPid|Uid):' /proc/3187/status
Name: kinsing State: R (running) PPid: 980 Uid: 33 33 33 33

PPid is 980, the parent process ID, which top already showed us is the nginx worker. Uid is 33, printed four times over (the real, effective, saved, and filesystem user IDs), and 33 is www-data on Debian and Ubuntu. So the web server itself spawned this miner. That is the whole break-in in two numbers: someone found a hole in the web application, and the web server, running as www-data, was made to download and run a miner.

Environment variables are not a hiding place
Whatever a process was handed as environment variables (database passwords, API tokens, cloud keys) sits in plain text in /proc/<pid>/environ for as long as it runs. The owner and root can read it live with a single cat. An attacker who lands as www-data will read the environ of every www-data process hoping for a secret that gets them further, and if they reach root, they can read every process on the box. For anything sensitive, prefer a secrets file with tight permissions, or a secrets manager, over an environment variable.

The Owner Column Is A Security Column

A process can do exactly what its owner can do, no more and no less. That one rule is why the USER column is the first thing a defender reads. It tells you the blast radius. A bug in a program running as www-data can wreck what www-data can reach, which is bad but bounded. The same bug in a program running as root can rewrite the entire system, because root can touch everything. This is why services run as their own dedicated low-privilege users instead of as root, and why a web server running as root is treated as a serious mistake, not a shortcut.

Our miner is stuck at www-data for now, which is exactly why the attacker left it wired to the web server. Their next move is to climb from www-data to root, and reading other processes' secrets is one common ladder. Confirming the parent takes one command.

~/secopslog — bash
$ ps -p 980 -o pid=,user=,comm=
980 www-data nginx

That closes the loop: the nginx worker (www-data) is the parent, so the entry point was the web application, not SSH or a stolen login. Killing PID 3187 stops the miner for a minute, but if you do not fix the hole in the web app, the same program comes back under a new PID within the hour. Contain it in the right order: capture the evidence from /proc first, then kill the process, then patch the way in.

Reading an unfamiliar process, in order
1Spot it
top or ps: note %CPU and the USER column
2What is it
/proc/<pid>/exe, flag /tmp and (deleted)
3How it started
/proc/<pid>/cmdline for the full arguments
4What it touches
/proc/<pid>/fd for open files and sockets
5Who launched it
status PPid, then walk up the parent tree
6Contain
save evidence, kill the PID, patch the entry point
Quick check
01You find a busy process and ls -l /proc/<pid>/exe prints -> /usr/bin/python3 (deleted). What is the most likely explanation?
Incorrect — Linux never removes a binary from disk just because it is running.
Correct — (deleted) means the running binary was unlinked while the process keeps living.
Incorrect — /proc/<pid> only exists while the process is alive; a crashed one has no exe link.
Incorrect — PIDs are not 'deleted'; the tag describes the binary file, not the number.
02In ps aux output a process shows VSZ 924136 and RSS 43980. Which number reflects the real physical RAM the process is holding right now?
Incorrect — VSZ is the whole mapped address space, most of which is not resident in RAM.
Incorrect — %MEM is derived from RSS, which is itself the real resident figure.
Correct — RSS (resident set size) is the physical RAM in use now, and it is usually the number you care about.
Incorrect — RSS is already the resident amount; you do not subtract to get it.
03On a production box you want to stop a hung firefox and are about to run pkill fire. Why is that risky, and what is the safer move?
Incorrect — pkill works by name, not PID, so it will match and signal processes.
Correct — pkill and pgrep match substrings by default, so previewing with pgrep -a or targeting the PID avoids collateral kills.
Incorrect — ownership limits apply, but that does not stop it matching several of your own processes by name.
Incorrect — the default signal is SIGTERM, and the real danger here is matching the wrong processes, not the signal type.

The habit that makes all of this fast is a boring one. On a machine you trust, run ps aux and top once while everything is healthy, and actually read them. Learn what normal looks like: which users own what, which programs are always there. The day something is wrong, the odd line, the wrong owner, the binary running out of /tmp, stops being a needle in a haystack and becomes the one row that does not belong.

Try this

Work through “The Owner Column Is A Security Column” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: pkill matches more than you mean. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related