首页 游戏 软件 资讯 排行榜 专题
首页
业界动态
/proc 文件系统实战:原来 top、htop 都是靠读文件实现的

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

热心网友
79
转载
2026-04-22

一、/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
免责声明: 游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

最新APP

宝宝过生日
宝宝过生日
应用辅助 04-07
台球世界
台球世界
体育竞技 04-07
解绳子
解绳子
休闲益智 04-07
骑兵冲突
骑兵冲突
棋牌策略 04-07
三国真龙传
三国真龙传
角色扮演 04-07

热门推荐

iPhone16之间如何快速传输App?详细步骤解析
iphone
iPhone16之间如何快速传输App?详细步骤解析

通过AirDrop功能,可在iPhone16之间快速传输已安装的App,无需重新下载。 省去重新下载的等待,直接在两部iPhone 16之间“搬运”已经安装好的App——这个用AirDrop传App的功能,确实方便。不过,想顺利操作,有几个关键前提得先摆正。 准备工作与条件确认 开始之前,最好花一分

热心网友
04.22
iPhone17设备名称怎么修改?详细步骤教程
iphone
iPhone17设备名称怎么修改?详细步骤教程

修改iPhone17设备名称的核心步骤 想给你的iPhone17换个独具特色的名字吗?其实很简单,整个操作的核心路径就在「设置」>「通用」>「关于本机」>「名称」里,几步就能完成自定义。 为什么要修改iPhone17的设备名称? 给iPhone17改个名,可不仅仅是图个新鲜。它在蓝牙配对、使用Air

热心网友
04.22
iPhone14隐藏ID怎么解除?详细步骤与注意事项
iphone
iPhone14隐藏ID怎么解除?详细步骤与注意事项

解除iPhone14隐藏ID的核心方法是联系原机主或提供购买凭证,通过官方渠道重置Apple ID 手里突然多出一台被锁的iPhone 14,用起来处处受限,这事儿确实头疼。好消息是,只要遵循官方路径,问题基本都能解决。关键在于,你得有耐心走完正规流程。 什么是iPhone隐藏ID? 简单来说,iP

热心网友
04.22
怎么查找我的iPhone17位置?
iphone
怎么查找我的iPhone17位置?

通过“查找”应用或iCloud网站,登录Apple ID即可实时定位iPhone 17,即使设备离线也能显示最后已知位置。 使用“查找”应用定位iPhone 17 如果你手边还有别的苹果设备,比如iPad或者Mac,最省事的方法就是直接用上面的“查找”应用。打开应用,登录和iPhone 17同一个

热心网友
04.22
iPhone 16通知权限设置与微信提示音修复指南
iphone
iPhone 16通知权限设置与微信提示音修复指南

iPhone 16通知权限设置与微信提示音修复指南 微信消息突然“静音”了?先别急着怀疑手机坏了。在iPhone 16上,通知体系和声音管理比以往更精细,有时只是某个开关没到位。接下来,咱们就把系统通知中心、应用权限、勿扰模式这几个关键环节捋清楚,帮你快速找回失联的提示音,避免错过重要信息。 iPh

热心网友
04.22