What Problem Does /proc Solve?
Imagine you are a mechanic and you want to check what is happening inside a running car engine — but the engine has no dashboard, no gauges, nothing. You would have to crack it open every time just to look inside. That was exactly the situation with older UNIX systems. If you wanted to know which processes were running, what files they had open, or how memory was being used — you had to dig directly into kernel memory. That required root privileges, detailed knowledge of internal kernel data structures, and it broke every time the kernel was updated.
Linux fixed this with the /proc virtual filesystem. Think of it as a live dashboard that the kernel keeps updated for you. You can read it just like a normal file. No special tools, no kernel hacking needed. Just cat /proc/something and you get the answer.
The word virtual is key here. These files do not exist on your hard drive. They are not stored anywhere. When you open a /proc file, the kernel generates the content on the spot — right at that moment. When you close it, the content is gone. It is like asking someone a question and they make up a correct answer on the spot based on what is currently happening.
Open your Linux terminal right now and type ls /proc. You will see a bunch of numbered directories like 1, 423, 8901, and some named ones like meminfo, cpuinfo, net, etc. Each numbered directory corresponds to a running process — the number is the Process ID (PID). The named ones are system-wide information files.
| /proc/ | Root of virtual filesystem — generated by kernel at runtime |
| ├── 1/ | Directory for process with PID 1 (init / systemd) |
| ├── status | Memory, state, UIDs, signals for this process |
| ├── cmdline | The exact command that started this process |
| ├── fd/ | All open file descriptors (as symlinks) |
| ├── maps | Memory map — what is loaded where in virtual memory |
| └── task/ | Subdirectory for each thread in this process |
| ├── 4231/ | Another running process |
| ├── meminfo | System-wide RAM and swap usage |
| ├── cpuinfo | CPU model, cores, flags, cache sizes |
| ├── uptime | How long the system has been running |
| └── sys/kernel/ | Writable kernel settings like pid_max, hostname |
A very useful shortcut: any process can look at its own /proc directory without knowing its own PID. The kernel provides a symlink /proc/self that always points to the current process’s /proc/PID directory. So if your program does cat /proc/self/status, it gets its own status. Simple.
The status file inside any /proc/PID directory is like a report card for that process. Run this in your terminal to see your shell’s report:
cat /proc/self/status
You will see output like this (values differ on your system):
Name: bash
State: S (sleeping)
Tgid: 4512
Pid: 4512
PPid: 4490
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
VmPeak: 12340 kB
VmSize: 11200 kB
VmRSS: 3100 kB
Threads: 1
Let me break down the fields that actually matter for you as an embedded/Linux programmer:
| Field | What It Means | Practical Use |
|---|---|---|
| Name | Executable name (first 15 chars) | Check which program a PID belongs to |
| State | R=running, S=sleeping, Z=zombie, D=disk wait | Debug stuck or zombie processes |
| Pid / Tgid | Thread ID vs Thread Group ID (traditional PID) | Distinguish threads from processes |
| PPid | Parent Process ID | Build process trees, find who launched a process |
| Uid / Gid | Real, Effective, Saved, FS user/group IDs | Check privilege level of a process |
| VmRSS | Resident Set Size — RAM currently used | Monitor memory consumption of a process |
| VmPeak | Highest RAM usage ever seen for this process | Detect memory spikes in long-running daemons |
| Threads | Number of threads in this process | Find thread-heavy processes |
| SigBlk / SigIgn | Signal bitmasks: blocked and ignored signals | Debug why a process is not responding to signals |
The status file format has changed across kernel versions — fields have been added and removed. Never assume “line 4 is PPid”. Instead, scan for the line that starts with PPid: and read from there. This is a real gotcha in production code.
Beyond status, every /proc/PID directory has several other very useful files. Let’s go through them with actual terminal examples you can run right now.
3.1 cmdline — How Was This Process Started?
This file holds the exact command-line arguments used to launch the process, separated by null bytes (\0). It is how tools like ps aux know what to display in the CMD column.
# Replace 1234 with any real PID on your system
cat /proc/1234/cmdline | tr '\0' ' '
# Output example: /usr/sbin/sshd -D
3.2 environ — What Environment Variables Does It Have?
Shows all environment variables of the process, again null-byte separated. Useful for debugging — imagine you want to check if your daemon picked up the right PATH or LD_LIBRARY_PATH.
cat /proc/self/environ | tr '\0' '\n'
# Lists all env vars of the current shell
3.3 fd/ — What Files Does This Process Have Open?
The fd subdirectory contains one symbolic link for every file descriptor the process has open. The symlink name is the descriptor number. So /proc/1234/fd/0 is standard input, /proc/1234/fd/1 is standard output, /proc/1234/fd/2 is standard error, and so on.
This is incredibly useful. If a process is running fine but you suspect it is leaking file descriptors, just check how many entries are in /proc/PID/fd/ over time. If the count keeps growing, you have a leak.
# Count how many files process 4512 currently has open
ls /proc/4512/fd | wc -l
3.4 exe and cwd — The Binary and Working Directory
exe is a symlink to the actual binary being run. cwd is a symlink to the current working directory. Super handy when a rogue process is running and you want to find where it came from.
ls -la /proc/4512/exe
# Output: /proc/4512/exe -> /usr/bin/python3
ls -la /proc/4512/cwd
# Output: /proc/4512/cwd -> /home/ravi/projects/myapp
3.5 maps — Memory Layout of the Process
The maps file shows every memory region the process has — the code segment, stack, heap, and all shared libraries. Each line shows a virtual address range, permissions, and what is mapped there.
cat /proc/self/maps
# Example output lines:
# 55b8a1200000-55b8a1234000 r-xp ... /bin/bash (code, read+execute)
# 7f9c45600000-7f9c45800000 r--p ... /lib/libc.so (libc, read-only)
# 7fff8a200000-7fff8a221000 rw-p ... [stack] (stack, read+write)
When a process spawns multiple threads (using pthreads, for example), those threads share most of the process’s resources — same address space, same open files, same PID as seen by the OS from outside. But they do have some things that are unique per thread: their own stack, their own signal mask, their own CPU state.
Linux represents this through the task/ subdirectory inside /proc/PID/. For each thread in that process, there is a subdirectory named after the Thread ID (TID):
| Process PID 5000 (e.g., a web server) | |
| /proc/5000/status | Shows: Threads = 3, Tgid = 5000 |
| /proc/5000/task/5000/ | Main thread — has its own status, SigPnd, State |
| /proc/5000/task/5001/ | Worker thread 1 — may be in different State (R vs S) |
| /proc/5000/task/5002/ | Worker thread 2 — its own signal mask, CPU affinity |
A concrete example: suppose your BLE daemon is stuck and you suspect one of its threads is in a D state (waiting for disk I/O forever). You can check cat /proc/PID/task/TID/status for each thread to find the stuck one. This saves hours of debugging.
# List all thread IDs in process 5000
ls /proc/5000/task/
# Check state of each thread
grep "^State" /proc/5000/task/*/status
Not everything under /proc is about a specific process. There are directories and files that expose the overall health and configuration of the system. These are not inside any /proc/PID folder — they live at the top level or under /proc/sys/.
| Path | What It Contains | Writable? |
|---|---|---|
| /proc/meminfo | RAM total, free, buffers, swap usage | No (read-only) |
| /proc/cpuinfo | CPU model, features, frequency per core | No |
| /proc/uptime | System uptime in seconds + idle time | No |
| /proc/loadavg | 1, 5, 15 minute CPU load averages | No |
| /proc/net/tcp | All active TCP connections (hex addresses) | No |
| /proc/sys/kernel/pid_max | Maximum PID value allowed on this system | Yes (root only) |
| /proc/sys/kernel/hostname | Current hostname of the machine | Yes (root only) |
| /proc/sys/vm/overcommit_memory | Controls how kernel handles memory overcommit | Yes (root only) |
| /proc/sys/fs/file-max | Max number of open files system-wide | Yes (root only) |
| /proc/version | Kernel version string + compiler info | No |
Try these in your terminal right now to get a feel:
cat /proc/uptime # e.g: 342819.23 681247.40 (uptime, idle time in seconds)
cat /proc/loadavg # e.g: 0.45 0.32 0.28 2/412 9834
cat /proc/meminfo | head -10
cat /proc/version
You can interact with /proc files using the same system calls you use for normal files — open(), read(), write(), close(). There is nothing special about them from a syscall perspective. A few access rules apply though:
| Rule | Example |
|---|---|
| Most /proc/PID files are read-only | You can read /proc/1234/status but not write to it |
| /proc/PID files are owned by the process owner | Only root or the process owner can read sensitive files like /proc/PID/environ |
| /proc/sys/ files can be written by root | echo 65536 > /proc/sys/kernel/pid_max (as root) |
| /proc/PID directories are volatile | If the process dies between your opendir() and open() calls, the directory vanishes — handle ENOENT |
Let me write a clean C example that reads the current value of /proc/sys/kernel/pid_max and optionally updates it. This is similar to how real system utilities work.
/*
* read_pid_max.c
* Reads /proc/sys/kernel/pid_max
* If a new value is passed as argv[1], updates it (needs root)
*
* Compile: gcc -o read_pid_max read_pid_max.c
* Run: ./read_pid_max
* sudo ./read_pid_max 65536
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define PROC_FILE "/proc/sys/kernel/pid_max"
#define BUF_SIZE 64
int main(int argc, char *argv[])
{
int fd;
char buf[BUF_SIZE];
ssize_t n;
/* Open read-write only if we plan to write a new value */
int flags = (argc > 1) ? O_RDWR : O_RDONLY;
fd = open(PROC_FILE, flags);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
/* Read current value */
n = read(fd, buf, BUF_SIZE - 1);
if (n == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
buf[n] = '\0';
if (argc > 1)
printf("Current pid_max: %s", buf);
else
printf("pid_max = %s", buf);
/* Write new value if provided */
if (argc > 1) {
/* lseek back to beginning before writing */
if (lseek(fd, 0, SEEK_SET) == -1) {
perror("lseek");
close(fd);
exit(EXIT_FAILURE);
}
size_t len = strlen(argv[1]);
if (write(fd, argv[1], len) != (ssize_t)len) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
/* Verify the change took effect */
if (lseek(fd, 0, SEEK_SET) == -1) { perror("lseek"); }
n = read(fd, buf, BUF_SIZE - 1);
if (n > 0) {
buf[n] = '\0';
printf("New pid_max: %s", buf);
}
}
close(fd);
return 0;
}
How it works, step by step:
- We open the /proc file with
O_RDONLYif just reading,O_RDWRif updating - We
read()the current value into a buffer — it comes back as a string like"32768\n" - If a new value was passed on the command line, we
lseek()back to position 0 andwrite()the new string - We verify by seeking to 0 and reading again
Now let me write a second example — a small utility that reads the Name and PPid of a given PID by scanning its /proc/PID/status file:
/*
* proc_info.c
* Reads Name and PPid from /proc/PID/status for a given PID
*
* Compile: gcc -o proc_info proc_info.c
* Run: ./proc_info 1
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
exit(EXIT_FAILURE);
}
/* Build the path: /proc/PID/status */
char path[64];
snprintf(path, sizeof(path), "/proc/%s/status", argv[1]);
FILE *fp = fopen(path, "r");
if (!fp) {
/* Process may have exited between our check and open */
perror("fopen");
exit(EXIT_FAILURE);
}
char line[256];
char name[64] = "(not found)";
char ppid[32] = "(not found)";
char state[64] = "(not found)";
while (fgets(line, sizeof(line), fp)) {
/* Search by field label — not by line number */
if (strncmp(line, "Name:", 5) == 0)
sscanf(line, "Name:\t%63s", name);
else if (strncmp(line, "PPid:", 5) == 0)
sscanf(line, "PPid:\t%31s", ppid);
else if (strncmp(line, "State:", 6) == 0)
sscanf(line, "State:\t%63[^\n]", state);
}
fclose(fp);
printf("PID : %s\n", argv[1]);
printf("Name : %s\n", name);
printf("State : %s\n", state);
printf("Parent : %s\n", ppid);
return 0;
}
Run it as ./proc_info 1 and you will see information about systemd (or init). Try ./proc_info $$ in bash to query your own shell’s process. The $$ expands to the current shell’s PID.
A process can exit at any moment. Between the time your code confirms that /proc/4512/ exists and the time you actually call fopen("/proc/4512/status"), the process might have died and its directory vanished. This will give you ENOENT (No such file or directory). Always check for this error and handle it gracefully — it is not a bug, it is expected behaviour. This is especially important when scanning all PIDs in a loop.
Here is a small cheat sheet of /proc-based commands that are actually useful in day-to-day Linux work:
# Find memory usage of all running processes (RSS in kB)
for pid in /proc/[0-9]*/status; do
awk '/^Name/{n=$2} /^VmRSS/{r=$2" "$3} END{printf "%s: %s\n", n, r}' "$pid"
done 2>/dev/null | sort -t: -k2 -n | tail -10
# Find which process has a specific file open
# (useful: who is holding /var/log/syslog open?)
for pid in /proc/[0-9]*/fd; do
ls -la "$pid" 2>/dev/null | grep "/var/log/syslog" && echo "PID: ${pid%/fd}"
done
# Check if a process is multithreaded
cat /proc/$(pgrep bluetoothd)/status | grep Threads
# Raise system file descriptor limit temporarily (root required)
echo 1000000 > /proc/sys/fs/file-max
cat /proc/sys/fs/file-max # verify
# See all kernel tunable parameters
ls /proc/sys/kernel/
Quick Recap — What You Learned in Part 1
- /proc is a virtual filesystem — no disk, all content generated live by the kernel
- /proc/PID/ exists for every running process and disappears when it exits
- /proc/PID/status is the process report card — parse by field name, not line number
- /proc/PID/fd/ shows all open file descriptors as symlinks
- /proc/PID/task/ exposes per-thread information
- /proc/sys/ contains writable kernel tunables (root required)
- Use standard
open()/read()/write()to access /proc files from C - Always handle
ENOENTwhen scanning /proc/PID — processes can exit mid-scan - /proc/self is a convenient shortcut to your own process directory
