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
/sysfiles 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_attributestructure andsysfs_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/uptimeand prints how long the system has been running in hours, minutes, and seconds.
Quick Knowledge Check
- What is the difference between
/procand/sys? - Why is VmRSS more useful than VmSize for understanding memory usage?
- 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_ADMINor root. - Assuming sysfs paths are stable. Hardware topology can change. Use udev rules for stable names.
- Blocking on /proc reads. Some
/procfiles (like/proc/kmsg) block. Use non-blocking I/O or poll. - String parsing errors.
/procvalues often have trailing newlines or varying whitespace. Alwaystrim()/strcspn().