游乐游手机版
首页/业界动态/文章详情

/proc 文件系统实战:原来 top、htop 都是靠读文件实现的

时间:2026-04-22 18:51
一、 proc是什么:假装是文件系统的内核接口 乍一看, proc 就是个普通目录,对吧?但真相是,它压根不在硬盘上。它是一个由内核在内存中实时维护的虚拟文件系统(procfs)。每次你读取 proc 下的一个文件,内核都会现场“生成”对应的数据返回给你,数据是活的。 $ mount | grep

一、/proc是什么:假装是文件系统的内核接口

乍一看,/proc 就是个普通目录,对吧?但真相是,它压根不在硬盘上。它是一个由内核在内存中实时维护的虚拟文件系统(procfs)。每次你读取 /proc 下的一个文件,内核都会现场“生成”对应的数据返回给你,数据是活的。

$ mount | grep proc
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
$ ls /proc
1    42   1234  ...   # 数字目录 = 正在运行的进程(PID)
cpuinfo   meminfo   net   sys   ...  # 系统全局信息

先随手试几个命令,感受一下它的脉搏:

# 当前进程的 PID
echo $$   # 假设是 12345
# 查看进程状态
cat /proc/12345/status
# 查看内存信息
cat /proc/meminfo | head -5
# 查看 CPU 信息
cat /proc/cpuinfo | grep "model name" | head -1
# 查看系统负载(1/5/15 分钟平均负载)
cat /proc/loada vg

二、/proc/[PID]/里有什么宝藏?

每个正在运行的进程,在 /proc 下都有一个以自己 PID 命名的专属目录。这里面藏着的,正是 top、htop 这些监控工具的原始数据源。

光说不练假把式,我们直接动手看看真实数据长什么样:

# 找到 nginx 的 PID
pidof nginx   # 假设是 1234
# CPU 时间(第14、15字段)
cat /proc/1234/stat | awk '{print "utime="$14, "stime="$15}'
# 物理内存(单位 kB)
grep VmRSS /proc/1234/status
# 打开的文件数量
ls /proc/1234/fd | wc -l
# 实际磁盘 IO
cat /proc/1234/io

三、CPU 使用率的计算原理:不是直接读出来的

这里有个最常见的理解误区:/proc/[PID]/stat 里并没有直接存储着“CPU 使用率 3.2%”这样的现成数字。

它存储的是进程自启动以来累计消耗的 CPU 时间,单位是 jiffies(1 jiffie 可能是 10ms 或 4ms,取决于内核配置)。那么,top 是怎么算出那个百分比的?答案是:两次采样做差

这下就明白了,为什么 top 默认每 3 秒刷新一次。它每隔 3 秒做一次快照,然后用进程在这 3 秒内消耗的 CPU 时间增量,除以系统在这 3 秒内的总 CPU 时间增量,从而得到这 3 秒内的平均 CPU 使用率。系统总 CPU 时间从 /proc/stat 读取,进程 CPU 时间则来自 /proc/[PID]/stat

四、动手实现:迷你进程监控器

纸上得来终觉浅,现在我们把上面的知识串起来,写一个真正能跑的迷你进程监控工具,核心逻辑不到100行。

#include 
#include 
#include 
#include 

typedef struct {
    long utime, stime;      // 进程 CPU 时间
    long total_cpu;         // 系统总 CPU 时间
    long vm_rss;            // 物理内存 KB
    long rchar, wchar;      // IO 读写字节
} ProcInfo;

// 读进程 CPU 时间(/proc/PID/stat 第14、15字段)
void read_proc_stat(int pid, ProcInfo *info) {
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/stat", pid);
    FILE *f = fopen(path, "r");
    if (!f) return;
    // 跳过前13个字段,读第14(utime)和第15(stime)
    long dummy; char name[256]; char state;
    fscanf(f, "%ld %s %c %ld %ld %ld %ld %ld %ld %ld %ld %ld %ld %ld %ld",
           &dummy, name, &state,
           &dummy, &dummy, &dummy, &dummy, &dummy,
           &dummy, &dummy, &dummy, &dummy, &dummy,
           &info->utime, &info->stime);
    fclose(f);
}

// 读系统总 CPU 时间(/proc/stat 第1行)
long read_total_cpu() {
    FILE *f = fopen("/proc/stat", "r");
    long user, nice, system, idle, iowait, irq, softirq;
    fscanf(f, "cpu %ld %ld %ld %ld %ld %ld %ld",
           &user, &nice, &system, &idle, &iowait, &irq, &softirq);
    fclose(f);
    return user + nice + system + idle + iowait + irq + softirq;
}

// 读物理内存(/proc/PID/status 中的 VmRSS)
void read_memory(int pid, ProcInfo *info) {
    char path[64], line[128];
    snprintf(path, sizeof(path), "/proc/%d/status", pid);
    FILE *f = fopen(path, "r");
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "VmRSS:", 6) == 0) {
            sscanf(line, "VmRSS: %ld", &info->vm_rss);
            break;
        }
    }
    fclose(f);
}

// 读 IO 字节数(/proc/PID/io)
void read_io(int pid, ProcInfo *info) {
    char path[64], line[128];
    snprintf(path, sizeof(path), "/proc/%d/io", pid);
    FILE *f = fopen(path, "r");
    if (!f) return;   // 需要 root 才能读其他用户的 io
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "rchar:", 6) == 0) sscanf(line, "rchar: %ld", &info->rchar);
        if (strncmp(line, "wchar:", 6) == 0) sscanf(line, "wchar: %ld", &info->wchar);
    }
    fclose(f);
}

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("用法: %s \n", argv[0]); return 1; }
    int pid = atoi(argv[1]);
    ProcInfo prev = {}, curr = {};
    while (1) {
        read_proc_stat(pid, &prev);
        prev.total_cpu = read_total_cpu();
        read_memory(pid, &prev);
        read_io(pid, &prev);

        sleep(1);  // 采样间隔 1 秒

        read_proc_stat(pid, &curr);
        curr.total_cpu = read_total_cpu();
        read_memory(pid, &curr);
        read_io(pid, &curr);

        // 计算 CPU 使用率
        long proc_delta  = (curr.utime + curr.stime) - (prev.utime + prev.stime);
        long total_delta = curr.total_cpu - prev.total_cpu;
        double cpu_pct   = total_delta > 0 ? (double)proc_delta / total_delta * 100.0 : 0.0;

        // 计算 IO 速率(字节/秒)
        long read_rate  = curr.rchar - prev.rchar;
        long write_rate = curr.wchar - prev.wchar;

        printf("\033[2J\033[H");   // 清屏
        printf("PID: %d\n", pid);
        printf("CPU:    %.1f%%\n", cpu_pct);
        printf("内存:   %ld KB (%.1f MB)\n", curr.vm_rss, curr.vm_rss / 1024.0);
        printf("读 IO:  %ld B/s\n", read_rate);
        printf("写 IO:  %ld B/s\n", write_rate);
        printf("FD 数量: ");
        fflush(stdout);
        // 统计打开的文件描述符数量
        char fd_path[64];
        snprintf(fd_path, sizeof(fd_path), "ls /proc/%d/fd 2>/dev/null | wc -l", pid);
        system(fd_path);

        prev = curr;
    }
    return 0;
}

编译并运行它:

gcc -O2 -o minimon minimon.c
./minimon 1234    # 监控 PID 为 1234 的进程

你会看到类似这样的实时输出:

PID: 1234
CPU:    12.3%
内存:   45312 KB (44.3 MB)
读 IO:  8192 B/s
写 IO:  4096 B/s
FD 数量: 23

五、/proc的其他实用技巧

快速查看进程完整命令行:

cat /proc/1234/cmdline | tr '\0' ' '
# 输出示例:/usr/sbin/nginx -g daemon off;

查看进程打开了哪些文件/连接:

ls -la /proc/1234/fd
# 输出示例:
# lrwxrwxrwx  0 -> /dev/null
# lrwxrwxrwx  1 -> pipe:[12345]
# lrwxrwxrwx  3 -> socket:[67890]
# lrwxrwxrwx  4 -> /var/log/nginx/access.log

读取/修改内核参数(无需重启):

# 查看最大文件描述符数
cat /proc/sys/fs/file-max
# 开启 IP 转发(Docker/K8s 必须开)
echo 1 > /proc/sys/net/ipv4/ip_forward
# 等价于 sysctl -w net.ipv4.ip_forward=1

查看系统整体内存使用:

cat /proc/meminfo
# MemTotal:       16384000 kB
# MemFree:         2048000 kB
# Cached:          4096000 kB  ← Page Cache
# Buffers:          512000 kB
# ...

查看所有进程的内存总使用量(比 free 命令更精确):

# 把所有进程的 VmRSS 加起来
grep VmRSS /proc/*/status 2>/dev/null | awk '{sum+=$2} END {print sum/1024 " MB"}'

六、高频面试题精析

Q:/proc目录里的文件真的存在磁盘上吗?

不存在。/proc 是 procfs 虚拟文件系统,挂载在内存里。读取一个 /proc 文件时,内核会实时生成对应数据返回;写入某些文件(如 /proc/sys/ 下的文件)则会直接修改内核参数。这些文件没有 inode 对应的磁盘块,用 ls -l 看大小显示为 0,但内容是实时、真实的。

Q:top显示的 CPU 使用率是怎么算出来的?

top 定期读取 /proc/[PID]/stat(获取进程 CPU 时间)和 /proc/stat(获取系统总 CPU 时间),进行两次采样。然后用进程 CPU 时间的增量,除以系统总 CPU 时间的增量,得到百分比。这就是为什么刚启动 top 时,通常需要等待一个刷新周期(比如3秒)后,数据才会变得准确。

Q:VmRSS和VmSize有什么区别?

VmSize 是进程的虚拟内存大小——它包括了所有映射的地址空间,但其中大部分可能并没有对应的物理内存页。VmRSS(Resident Set Size)则是实际占用的物理内存——即已经加载到 RAM 中的页面。监控内存使用率时应该看 VmRSS,VmSize 通常会因为包含大量共享库和映射空间而显得虚高。

Q:为什么有些进程的/proc/[PID]/io读不了?

读取其他用户进程的 io 文件需要 root 权限(或 CAP_SYS_PTRACE 能力)。读取自己进程的 io 则无需特殊权限。这是内核出于安全考虑的设计——IO 数据能够揭示进程的行为模式,不应随意暴露给非特权用户。

七、结语

/proc 堪称 Linux 系统最透明的一扇窗。内核把自己几乎所有的运行时状态,都以文件的形式陈列于此,对任何有权限的读者开放。

我们日常使用的 top、htop、ps、lsof、netstat 等工具,本质上都是 /proc 的“读者”和“翻译官”。现在,你不仅知道了它们的数据从何而来,也掌握了亲手打造专属监控工具的能力。下次再看到进程指标跳动时,你看到的将不再是一个神秘的数字,而是一段可以直接触摸和计算的数据流。

来源:https://www.51cto.com/article/839500.html
上一篇Vibe Coding正在杀死开源软件,让软件供应链风险悄然升级 下一篇踩坑!MySQL这个参数让应用直接崩了,90%的DBA都忽略了!
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
长安汽车明年一季度发布首款车载人形机器人小安
业界动态 · 2026-06-29

长安汽车明年一季度发布首款车载人形机器人小安

长安汽车公布机器人战略,采用“1+N+X”布局,联合头部伙伴攻克大脑、能源、驱动技术。人形机器人“小安”身高169cm,体重69kg,移动速度0 8m s,具备40个自由度,续航超2小时。预计明年一季度发布首款车载组件机器人,已在广州车展展示。

中国信科刷新光通信世界纪录 每秒可下载1.4万部4K电影
业界动态 · 2026-06-29

中国信科刷新光通信世界纪录 每秒可下载1.4万部4K电影

3月25日,光通信领域迎来又一个里程碑:中国信科集团光通信技术和网络全国重点实验室联合鹏城实验室、烽火藤仓光纤科技有限公司,成功实现了2 5Pb s 24芯光纤超大容量实时光传输,再次刷新了世界纪录。 这一研究成果不仅入选国际顶级光通信会议OFC(2026)并荣获“高分论文”称号,还受国际权威SCI

美国调查18万辆特斯拉Model3车门应急释放装置易找性
业界动态 · 2026-06-29

美国调查18万辆特斯拉Model3车门应急释放装置易找性

美国国家公路交通安全管理局对约17 9万辆2024款特斯拉Model3启动缺陷调查,焦点在于车门应急释放装置是否不易找到且标识不清。该调查源于一份缺陷请愿,不意味着立即召回,但可能引发后续监管措施。

doc个人图书馆停服 创始人称无偿转让失败
业界动态 · 2026-06-29

doc个人图书馆停服 创始人称无偿转让失败

运营长达20年,累计服务8000万用户的360doc个人图书馆,最终还是迎来了谢幕时刻。2026年5月1日,这个承载着无数用户收藏记忆的知名平台将正式停止服务——关停原因并非用户流失,而是始终未能寻得一位能够安全接管的合适人选。 创始人蔡智在告别信中坦言,近两个月来,他一直在尝试将360doc无偿转

年Q1随身WiFi实测安全靠谱高性价比机型推荐
业界动态 · 2026-06-29

年Q1随身WiFi实测安全靠谱高性价比机型推荐

2025年10月,艾瑞咨询正式授予飞猫“AI WiFi品类开创者”认证,紧接着CIC也将其认定为“多网融合自由切换技术服务首创者”。这些权威认证背后,折射出一个清晰的市场趋势:移动办公、户外出行、宿舍上网等场景的需求正在快速增长,随身WiFi几乎已成为不少用户的刚需装备。但问题也随之而来——网络卡顿