The /proc and /sys Filesystems

Linux exposes the kernel's internal state as files. Want to know how much memory a process uses? Read a file. Want to check CPU topology? Read a file. Want to toggle a GPIO pin? Write to a file. This chapter explores /proc and /sys -- the "everything is a file" philosophy at its most powerful.

/proc: Process and Kernel Information

/proc is a virtual filesystem. Nothing is stored on disk. Every read generates the content on the fly from kernel data structures.

/proc/
  |- 1/                  <-- init process
  |   |- status          <-- process state
  |   |- maps            <-- memory mappings
  |   |- fd/             <-- open file descriptors
  |   +- cmdline         <-- command line
  |- self/               <-- symlink to current process
  |- cpuinfo             <-- CPU details
  |- meminfo             <-- memory statistics
  |- uptime              <-- seconds since boot
  +- loadavg             <-- load averages

Reading /proc/self/maps

Every process can inspect its own memory layout.

/* proc_maps.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int global_var = 42;

int main(void) {
    int stack_var = 99;
    int *heap_var = malloc(sizeof(int));
    *heap_var = 7;

    printf("Addresses:\n");
    printf("  main()      = %p  (text)\n",  (void *)main);
    printf("  global_var  = %p  (data)\n",  (void *)&global_var);
    printf("  heap_var    = %p  (heap)\n",  (void *)heap_var);
    printf("  stack_var   = %p  (stack)\n", (void *)&stack_var);

    printf("\n--- /proc/self/maps ---\n");

    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) { perror("fopen"); return 1; }

    char line[512];
    while (fgets(line, sizeof(line), f)) {
        /* Show only interesting segments */
        if (strstr(line, "proc_maps") ||   /* our binary */
            strstr(line, "[heap]")    ||
            strstr(line, "[stack]")   ||
            strstr(line, "[vdso]")) {
            printf("  %s", line);
        }
    }

    fclose(f);
    free(heap_var);
    return 0;
}
$ gcc -O0 -o proc_maps proc_maps.c && ./proc_maps
Addresses:
  main()      = 0x5599a3b00189  (text)
  global_var  = 0x5599a3b03010  (data)
  heap_var    = 0x5599a4e482a0  (heap)
  stack_var   = 0x7ffc1a2b3c44  (stack)

--- /proc/self/maps ---
  5599a3b00000-5599a3b01000 r-xp ... proc_maps
  5599a4e48000-5599a4e69000 rw-p ... [heap]
  7ffc1a294000-7ffc1a2b5000 rw-p ... [stack]
  7ffc1a2fd000-7ffc1a301000 r-xp ... [vdso]

The maps file shows: address range, permissions (r/w/x/p), offset, device, inode, and pathname.

Try It: Run the program and find which region contains each address. Can you identify the text, data, heap, and stack regions in the maps output?

Reading /proc/[pid]/status

/* proc_status.c */
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(void) {
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/status", getpid());

    FILE *f = fopen(path, "r");
    if (!f) { perror("fopen"); return 1; }

    char line[256];
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "Name:", 5) == 0 ||
            strncmp(line, "Pid:", 4) == 0 ||
            strncmp(line, "PPid:", 5) == 0 ||
            strncmp(line, "VmSize:", 7) == 0 ||
            strncmp(line, "VmRSS:", 6) == 0 ||
            strncmp(line, "Threads:", 8) == 0) {
            printf("%s", line);
        }
    }

    fclose(f);
    return 0;
}
$ gcc -o proc_status proc_status.c && ./proc_status
Name:   proc_status
Pid:    12345
PPid:   11000
VmSize:     2104 kB
VmRSS:       768 kB
Threads:        1

Key fields:

  • VmSize: Total virtual memory.
  • VmRSS: Resident Set Size -- how much physical memory is actually used.
  • Threads: Number of threads in the process.

Reading /proc/cpuinfo and /proc/meminfo

/* sysinfo.c */
#include <stdio.h>
#include <string.h>

static void print_matching_lines(const char *path, const char *prefix) {
    FILE *f = fopen(path, "r");
    if (!f) { perror(path); return; }

    char line[256];
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, prefix, strlen(prefix)) == 0)
            printf("%s", line);
    }
    fclose(f);
}

int main(void) {
    printf("=== CPU ===\n");
    print_matching_lines("/proc/cpuinfo", "model name");
    print_matching_lines("/proc/cpuinfo", "cpu cores");

    printf("\n=== Memory ===\n");
    print_matching_lines("/proc/meminfo", "MemTotal");
    print_matching_lines("/proc/meminfo", "MemFree");
    print_matching_lines("/proc/meminfo", "MemAvailable");
    print_matching_lines("/proc/meminfo", "SwapTotal");

    return 0;
}
$ gcc -o sysinfo sysinfo.c && ./sysinfo
=== CPU ===
model name      : Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
cpu cores       : 8
=== Memory ===
MemTotal:       16384000 kB
MemFree:         4200000 kB
MemAvailable:   12000000 kB
SwapTotal:       8192000 kB

Rust: Reading /proc

// proc_reader.rs
use std::fs;
use std::io::{self, BufRead};

fn read_proc_field(path: &str, prefix: &str) -> io::Result<Vec<String>> {
    let file = fs::File::open(path)?;
    let reader = io::BufReader::new(file);

    let matches: Vec<String> = reader
        .lines()
        .filter_map(|line| {
            let line = line.ok()?;
            if line.starts_with(prefix) {
                Some(line)
            } else {
                None
            }
        })
        .collect();

    Ok(matches)
}

fn main() -> io::Result<()> {
    // Read our own memory maps
    let pid = std::process::id();
    let maps = fs::read_to_string(format!("/proc/{pid}/maps"))?;
    println!("=== Memory Maps (first 5 lines) ===");
    for line in maps.lines().take(5) {
        println!("  {line}");
    }

    // Read system info
    println!("\n=== CPU ===");
    for line in read_proc_field("/proc/cpuinfo", "model name")?.iter().take(1) {
        println!("  {line}");
    }

    println!("\n=== Memory ===");
    for line in &read_proc_field("/proc/meminfo", "MemTotal")? {
        println!("  {line}");
    }
    for line in &read_proc_field("/proc/meminfo", "MemAvailable")? {
        println!("  {line}");
    }

    Ok(())
}

/sys: The Device Model

/sys exposes the kernel's device model. It's organized by bus, class, and device.

/sys/
  |- class/
  |   |- net/             <-- network interfaces
  |   |   |- eth0/
  |   |   +- lo/
  |   |- block/           <-- block devices
  |   +- tty/             <-- terminals
  |- bus/
  |   |- pci/
  |   |- usb/
  |   +- platform/
  |- devices/             <-- device hierarchy
  +- kernel/              <-- kernel parameters

Reading Network Interface Info via sysfs

/* sysfs_net.c */
#include <stdio.h>
#include <string.h>
#include <dirent.h>

static int read_sysfs_str(const char *path, char *buf, size_t len) {
    FILE *f = fopen(path, "r");
    if (!f) return -1;
    if (!fgets(buf, len, f)) {
        fclose(f);
        return -1;
    }
    /* Remove trailing newline */
    buf[strcspn(buf, "\n")] = '\0';
    fclose(f);
    return 0;
}

int main(void) {
    DIR *d = opendir("/sys/class/net");
    if (!d) { perror("opendir"); return 1; }

    struct dirent *entry;
    while ((entry = readdir(d)) != NULL) {
        if (entry->d_name[0] == '.')
            continue;

        char path[256], buf[64];
        printf("Interface: %s\n", entry->d_name);

        /* Read MTU */
        snprintf(path, sizeof(path),
                 "/sys/class/net/%s/mtu", entry->d_name);
        if (read_sysfs_str(path, buf, sizeof(buf)) == 0)
            printf("  MTU:       %s\n", buf);

        /* Read operstate (up/down) */
        snprintf(path, sizeof(path),
                 "/sys/class/net/%s/operstate", entry->d_name);
        if (read_sysfs_str(path, buf, sizeof(buf)) == 0)
            printf("  State:     %s\n", buf);

        /* Read MAC address */
        snprintf(path, sizeof(path),
                 "/sys/class/net/%s/address", entry->d_name);
        if (read_sysfs_str(path, buf, sizeof(buf)) == 0)
            printf("  MAC:       %s\n", buf);

        /* Read speed (may fail for loopback) */
        snprintf(path, sizeof(path),
                 "/sys/class/net/%s/speed", entry->d_name);
        if (read_sysfs_str(path, buf, sizeof(buf)) == 0)
            printf("  Speed:     %s Mbps\n", buf);

        printf("\n");
    }

    closedir(d);
    return 0;
}
$ gcc -o sysfs_net sysfs_net.c && ./sysfs_net
Interface: eth0
  MTU:       1500
  State:     up
  MAC:       00:11:22:33:44:55
  Speed:     1000 Mbps

Interface: lo
  MTU:       65536
  State:     unknown
  MAC:       00:00:00:00:00:00

Writing to sysfs

Some sysfs attributes are writable. This is how you configure hardware from user space.

/* sysfs_write.c — set network interface MTU */
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <interface> <mtu>\n", argv[0]);
        return 1;
    }

    char path[256];
    snprintf(path, sizeof(path),
             "/sys/class/net/%s/mtu", argv[1]);

    FILE *f = fopen(path, "w");
    if (!f) {
        perror("fopen (need root?)");
        return 1;
    }

    fprintf(f, "%s\n", argv[2]);
    fclose(f);

    printf("Set %s MTU to %s\n", argv[1], argv[2]);
    return 0;
}
$ gcc -o sysfs_write sysfs_write.c
$ sudo ./sysfs_write eth0 9000
Set eth0 MTU to 9000

Caution: Writing to /sys files can change hardware behavior. Setting a wrong MTU, disabling a device, or modifying power settings can cause system instability. Always check what an attribute does before writing.

GPIO via sysfs (Legacy Interface)

The classic sysfs GPIO interface demonstrates read/write device control. Note that modern Linux prefers the libgpiod character device interface, but sysfs remains common in embedded systems.

/* gpio_sysfs.c — toggle a GPIO pin (legacy interface) */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

static int write_file(const char *path, const char *value) {
    int fd = open(path, O_WRONLY);
    if (fd < 0) { perror(path); return -1; }
    write(fd, value, strlen(value));
    close(fd);
    return 0;
}

int main(void) {
    int gpio_num = 17;  /* Example: Raspberry Pi GPIO 17 */

    char buf[64];

    /* Export the GPIO */
    snprintf(buf, sizeof(buf), "%d", gpio_num);
    write_file("/sys/class/gpio/export", buf);

    /* Set direction to output */
    snprintf(buf, sizeof(buf), "/sys/class/gpio/gpio%d/direction", gpio_num);
    write_file(buf, "out");

    /* Toggle the pin */
    snprintf(buf, sizeof(buf), "/sys/class/gpio/gpio%d/value", gpio_num);
    for (int i = 0; i < 10; i++) {
        write_file(buf, (i & 1) ? "1" : "0");
        usleep(500000);  /* 500 ms */
    }

    /* Unexport */
    snprintf(buf, sizeof(buf), "%d", gpio_num);
    write_file("/sys/class/gpio/unexport", buf);

    printf("Done toggling GPIO %d\n", gpio_num);
    return 0;
}

Driver Prep: When you write a kernel driver, you create the sysfs attributes that user-space programs read and write. The device_attribute structure and sysfs_create_file() are the kernel-side API. Everything you're reading here was created by a driver.

Rust: Reading sysfs

// sysfs_reader.rs
use std::fs;
use std::path::Path;

fn read_sysfs(path: &str) -> Option<String> {
    fs::read_to_string(path)
        .ok()
        .map(|s| s.trim().to_string())
}

fn main() {
    let net_dir = Path::new("/sys/class/net");

    let entries = fs::read_dir(net_dir).expect("cannot read /sys/class/net");

    for entry in entries {
        let entry = entry.unwrap();
        let name = entry.file_name();
        let name = name.to_str().unwrap();

        println!("Interface: {name}");

        let base = format!("/sys/class/net/{name}");

        if let Some(mtu) = read_sysfs(&format!("{base}/mtu")) {
            println!("  MTU:   {mtu}");
        }
        if let Some(state) = read_sysfs(&format!("{base}/operstate")) {
            println!("  State: {state}");
        }
        if let Some(mac) = read_sysfs(&format!("{base}/address")) {
            println!("  MAC:   {mac}");
        }

        println!();
    }
}

Udev Rules

Udev runs in user space and reacts to kernel device events. Rules in /etc/udev/rules.d/ can:

  • Set device permissions.
  • Create stable symlinks (/dev/mydevice).
  • Run scripts when devices appear.

Example rule (/etc/udev/rules.d/99-usb-serial.rules):

# When a USB serial adapter is plugged in, create /dev/myserial
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", \
    SYMLINK+="myserial", MODE="0666"

Test rules without reboot:

$ sudo udevadm trigger
$ sudo udevadm test /sys/class/tty/ttyUSB0

"Everything Is a File" in Practice

The /proc and /sys filesystems are the ultimate expression of Unix's "everything is a file" design:

+-------------------+---------------------------+--------------------------+
| What              | File path                 | Operation                |
+-------------------+---------------------------+--------------------------+
| Process memory    | /proc/[pid]/maps          | read                     |
| Kernel version    | /proc/version             | read                     |
| System uptime     | /proc/uptime              | read                     |
| Network MTU       | /sys/class/net/eth0/mtu   | read/write               |
| CPU frequency     | /sys/devices/.../scaling_  | read/write               |
|                   |   cur_freq                |                          |
| Disk scheduler    | /sys/block/sda/queue/     | read/write               |
|                   |   scheduler               |                          |
| LED brightness    | /sys/class/leds/.../       | read/write               |
|                   |   brightness              |                          |
+-------------------+---------------------------+--------------------------+

This means shell scripts, Python, C, Rust -- any language that can read files can control hardware.

Try It: Write a C program that reads /proc/uptime and prints how long the system has been running in hours, minutes, and seconds.

Quick Knowledge Check

  1. What is the difference between /proc and /sys?
  2. Why is VmRSS more useful than VmSize for understanding memory usage?
  3. How does a udev rule differ from directly writing to /sys?

Common Pitfalls

  • Parsing /proc with fixed offsets. Fields can change between kernel versions. Always search for the label.
  • Caching /proc data. It's generated on read. Old data is immediately stale.
  • Writing to /sys without root. Most writable attributes require CAP_SYS_ADMIN or root.
  • Assuming sysfs paths are stable. Hardware topology can change. Use udev rules for stable names.
  • Blocking on /proc reads. Some /proc files (like /proc/kmsg) block. Use non-blocking I/O or poll.
  • String parsing errors. /proc values often have trailing newlines or varying whitespace. Always trim() / strcspn().