The /proc directory looks familiar enough that it is easy to form the wrong mental picture: start a program, and Linux appears to create a folder of files for it. What you are really seeing is procfs—a kernel interface that renders process state through file operations. Once that clicks, entries such as exe, fd, maps, and status stop feeling mysterious.

Build a process that waits without burning a CPU core

proc-demo.cc
#include <stdio.h>
#include <unistd.h>
 
int main(int argc, char *argv[])
{
    printf("PID: %ld, argument: %s\n", (long)getpid(),
           argc > 1 ? argv[1] : "none");
    fflush(stdout);
 
    for (;;)
        pause();
}

Why this process is friendlier than while(1)

  • getpid() returns the caller’s process ID in its current PID namespace.

  • argc and argv preserve the command-line arguments that process startup supplied to main.

  • fflush(stdout) makes the identifying line visible before the process blocks.

  • pause() sleeps until a signal is delivered instead of continuously consuming a CPU.

  • The default action for SIGTERM ends this program, which gives the cleanup step a conventional path.

Project directorybash
gcc -std=c17 -Wall -Wextra -Wpedantic proc-demo.c -o proc-demo
./proc-demo "hello procfs" &
demo_pid=$!
printf 'shell captured PID: %s\n' "$demo_pid"
ps -p "$demo_pid" -o pid,ppid,state,comm,args
PID: 24173, argument: hello procfs
shell captured PID: 24173
    PID    PPID S COMMAND   COMMAND
  24173   ...  S proc-demo ./proc-demo hello procfs

Capture identity at process creation

  • & starts the command asynchronously in this shell.

  • $! expands to the PID of the shell’s most recent asynchronous pipeline, avoiding an unreliable ps | grep search.

  • Quoting $demo_pid prevents accidental word splitting and makes every later target explicit.

  • The S state commonly means interruptible sleep, which is expected while pause() waits.

  • Keep this terminal open because the shell variable is local to it.

/proc is a mounted kernel interface

Same shellbash
findmnt -T /proc/$demo_pid
stat -f -c 'filesystem: %T' /proc/$demo_pid
ls -ld /proc/$demo_pid
TARGET SOURCE FSTYPE OPTIONS
/proc  proc   proc   ...
filesystem: proc

What these checks establish

  • findmnt -T finds the filesystem containing the target path.

  • The filesystem type is proc, not the disk filesystem that contains the executable.

  • Directory ownership and mode expose some access rules, but individual proc entries can enforce additional ptrace-style checks.

  • Containers may mount their own procfs view, so identical numeric PIDs can mean different processes across PID namespaces.

Read the command line correctly

Same shellbash
tr '\0' ' ' < /proc/$demo_pid/cmdline
printf '\n'
./proc-demo hello procfs

Why cat can produce a misleading display

  • cmdline stores argument strings separated by NUL bytes rather than newline-delimited text.

  • tr makes those separators visible as spaces for this demonstration.

  • A process can modify its argument strings, so the file is best understood as the command line the process currently presents.

  • A zombie has an empty cmdline, and access may be denied for another user’s process.

  • Command-line arguments are observable and should not carry secrets such as passwords or tokens.

Same shellbash
readlink -v /proc/$demo_pid/exe
readlink -v /proc/$demo_pid/cwd
readlink -v /proc/$demo_pid/root
/home/user/demo/proc-demo
/home/user/demo
/

Three paths describe different process context

  • exe refers to the executed file; it may show a (deleted) suffix if the pathname was unlinked while the process kept running.

  • cwd is the process working directory used to resolve relative paths.

  • root is the process filesystem root and can differ because of chroot, mount namespaces, or containers.

  • Reading these links for another process is permission-controlled and can race with process exit or directory changes.

Inspect open file descriptors

Same shellbash
ls -l /proc/$demo_pid/fd
for fd in 0 1 2; do
  printf 'fd %s -> ' "$fd"
  readlink "/proc/$demo_pid/fd/$fd"
done
0 -> /dev/pts/3
1 -> /dev/pts/3
2 -> /dev/pts/3

Descriptors are capabilities held by the process

  • Entries are symlinks named by descriptor number, not copies of file contents.

  • Descriptors 0, 1, and 2 conventionally represent standard input, output, and error.

  • Targets can be regular files, terminals, pipes, sockets, anonymous inodes, or deleted files.

  • The set can change between listing and reading it, so monitoring software must tolerate races.

  • Opening some /proc/<pid>/fd links can duplicate access to the underlying object when permissions allow.

Connect virtual memory mappings to files

Same shellbash
head -20 /proc/$demo_pid/maps
... r--p ... /home/user/demo/proc-demo
... r-xp ... /home/user/demo/proc-demo
... r-xp ... /usr/lib/.../libc.so.6
... rw-p ... [heap]
... rw-p ... [stack]

How to read one maps row

  • The address range belongs to the process virtual address space, not a physical RAM address.

  • Permissions use r, w, and x, followed by private copy-on-write p or shared s.

  • Offset, device, and inode can correlate a file-backed mapping with its source file.

  • Anonymous regions may be labelled [heap], [stack], or have no pathname.

  • ASLR means addresses normally change between executions; do not hardcode them.

Use status and stat for structured process facts

Same shellbash
grep -E '^(Name|State|Pid|PPid|Threads|VmRSS|voluntary_ctxt_switches):' /proc/$demo_pid/status
cat /proc/$demo_pid/comm
Name:   proc-demo
State:  S (sleeping)
Pid:    24173
PPid:   ...
Threads: 1
VmRSS:  ... kB
proc-demo

Pick an interface that matches the job

  • status is human-readable and exposes identifiers, credentials, memory counters, capabilities, signals, and context-switch information.

  • stat is compact and machine-oriented, but its field parsing has traps such as a parenthesized command name that can contain spaces.

  • comm is a short process name and is not necessarily the executable basename or full command line.

  • Memory counters are snapshots with kernel-defined accounting semantics, not immutable billing totals.

Namespaces change what “the PID” means

Same shellbash
ls -l /proc/$demo_pid/ns
grep '^NSpid:' /proc/$demo_pid/status 2>/dev/null || true
pid -> pid:[4026531836]
mnt -> mnt:[4026531841]
...
NSpid: 24173

The process view depends on the observer

  • Namespace links identify the process membership for PID, mount, network, user, and other isolated resources.

  • NSpid can show nested PID values from the procfs mount’s namespace toward the process namespace.

  • A container’s /proc can hide host processes and present PID 1 for a process that has another host PID.

  • Do not combine a PID observed in one namespace with /proc mounted for another and assume the identity matches.

Stop the demonstration and verify cleanup

Same shellbash
kill -TERM "$demo_pid"
wait "$demo_pid"
exit_status=$?
printf 'wait status: %d\n' "$exit_status"
test ! -e "/proc/$demo_pid" && echo 'proc entry is gone'
wait status: 143
proc entry is gone

Cleanup closes the observation window

  • kill -TERM requests termination of the exact captured PID; it is not a recursive or system-wide command.

  • wait lets the parent shell reap its child and returns signal-derived status on common shells.

  • Once the task exits, its procfs directory disappears.

  • The kernel can later reuse the number, so PID alone is not a permanent identity.

  • Status 143 conventionally means 128 plus signal 15 in the shell, but portable programs should reason about wait-status APIs rather than assume shell encoding.

Troubleshoot surprising observations

  • No `/proc/<pid>` directory: the process already exited, the PID was copied incorrectly, or it is outside your PID namespace.

  • Permission denied: ownership, ptrace access checks, capabilities, Yama, or procfs hidepid policy restrict the entry.

  • `cmdline` has no spaces: arguments are NUL-separated; render them deliberately.

  • `exe` says deleted: the running image still exists through kernel references even though its pathname was unlinked.

  • `maps` changes while reading: mappings are live state and other threads can call mmap, munmap, or load libraries.

  • PID points to the wrong program later: the original exited and its numeric identifier was reused.

Primary Linux references