Linux /proc Filesystem — The Kernel’s Window

 

 

 

 

Linux /proc Filesystem — The Kernel’s Window
Understand how Linux exposes live process and system data through a fake filesystem — no disk involved
Part 1
of 2
/proc
Virtual FS
C
Code Examples

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.

Topics Covered in This Post
/proc filesystem /proc/PID structure /proc/PID/status /proc/PID/fd thread task directory system-wide /proc files reading /proc in C pid_max example /proc/sys/kernel volatile /proc/PID

Section 1 — The /proc Directory: A Live View Into the Kernel

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 Directory Layout (Conceptual View)
/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.

Section 2 — Reading /proc/PID/status: The Process Report Card

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:

Key Fields in /proc/PID/status
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
⚠️ Parse by field name, not by line number

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.

Section 3 — Other Files Inside /proc/PID You Should Know

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.

Example: File Descriptors of a Running Process (ls -la /proc/PID/fd)
lrwxrwxrwx 0 -> /dev/null (stdin, redirected to /dev/null) lrwxrwxrwx 1 -> /var/log/app.log (stdout, writing to a log file) lrwxrwxrwx 2 -> /var/log/app.log (stderr, same log) lrwxrwxrwx 3 -> /etc/config.yaml (a config file it opened) lrwxrwxrwx 4 -> socket:[98231] (a network socket) lrwxrwxrwx 5 -> /tmp/lockfile.pid (a lock file)

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)

Section 4 — Threads in /proc: The task/ Subdirectory

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 with 3 Threads — How /proc Represents It
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

Section 5 — System-Wide Files in /proc

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/.

Important System-Wide /proc Files and Directories
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

Section 6 — Reading and Writing /proc Files From a C Program

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:

/proc Access Rules Summary
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_RDONLY if just reading, O_RDWR if 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 and write() 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.

Important: Handle Volatile /proc/PID Directories

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.

Section 7 — Quick Practical Commands You Can Use Today

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 ENOENT when scanning /proc/PID — processes can exit mid-scan
  • /proc/self is a convenient shortcut to your own process directory

Continue to Part 2 →
Learn the uname() system call — how to get kernel version, architecture and hostname from a C program

Leave a Reply

Your email address will not be published. Required fields are marked *