/proc 里那些文件是谁写的

/proc 里那些文件是谁写的
隔壁老康难度 ★★★☆☆ · 前置:进程状态:R、S、D、Z 各自在等什么 | 一句话结论:
/proc不是磁盘上的文件,是内核按需现编的字符串。所以大小全是 0(cp/tar会拿到空文件)、self指向打开它的那个进程、有些文件读一下就会卡住。
df -h 报出来一个我到现在都记得的输出,那天在查磁盘满,/run 挂着一个 1.6G 的 tmpfs,只用了 2.1M,而 /dev/sda1 明明写着 98G 却已经吃掉 95%,一个几乎空一个几乎满并排放着,我盯着看了十几秒才意识到自己没看懂的不是容量,是 tmpfs 这三个字母,那是我入职第一周,之前只见过 Windows 的 C 盘 D 盘,脑子里并没有一个东西可以既占着挂载点又不占磁盘(tmpfs 是内核拿内存当磁盘用,/run 里放的是开机后才生成的 pid 文件和 socket,重启就没了,所以它小得离谱是正常的,这点我是后来才查的,当时只把它划进看不懂就先放着那一类,反正那天要查的那个 95% 才是正事)。
1 | df -h |
1 | Filesystem Size Used Avail Use% Mounted on |
看完磁盘我顺手 ls -l /proc,想看看这个目录到底占了多少空间。
1 | dr-xr-xr-x 2 root root 0 Nov 13 20:00 1 |
大小全是 0,时间全是刚才那一秒。
再往下走一步。
1 | ls -l /proc/self/status |
第一条报 0 字节,第二条报 1300 多字节。一个 0 字节的文件读出来 1300 字节,我当时的第一反应是 ls 坏了,第二反应是 wc 坏了,第三反应才轮到正主,这些东西压根不是文件,它一个字节都没存过,那些字节是读的时候现算出来的。
伪文件系统
/proc 是个伪文件系统,挂在 VFS 上,但背后没有块设备,没有扇区、没有 inode 表、也不占硬盘空间,它靠的是一套叫 procfs 的接口,内核对每一次打开 /proc/... 的操作都调一个自定义回调,当场算一份内容给你,算完就扔,下一次打开再算一遍,所以你隔一秒 cat 两次同一个文件,看到的数字可能就不一样,cat /proc/uptime 连小数部分都在跳,这不是缓存没刷新,是它真的每次都重新算了一遍。(副作用是这些文件没法被 mmap 之后反复读,每次读都得重新走一遍内核,这也是为什么有些监控程序读 /proc 读得特别凶。)
graph LR
A["cat /proc/meminfo"] --> B["VFS open()"]
B --> C["procfs 的 inode_operations"]
C --> D{"这个文件<br/>谁注册的?"}
D -->|"/proc/meminfo"| E["fs/proc/meminfo.c 等<br/>生成的 seq_file"]
D -->|"/proc/PID/status"| F["fs/proc/array.c<br/>task 转文本"]
D -->|"/proc/sys/net/..."| G["sysctl 表项"]
E --> H["返回文本字节"]
F --> H
G --> H
按这个划分,/proc 下的东西是三类。
| 路径 | 内容 | 举例 |
|---|---|---|
/proc/PID/ |
每个进程一个目录,名字就是 PID | /proc/1/status |
/proc/<静态名> |
全局信息,一堆固定名字 | /proc/meminfo、/proc/cpuinfo、/proc/loadavg |
/proc/sys/ |
可读可写的内核参数 | /proc/sys/net/ipv4/ip_forward |
/sys 是它的亲戚,叫 sysfs,设计规整得多,一个设备一个目录,一个属性一个文件,读写的语义都有明确约定,/proc 的思路正好相反,先能用就行,于是二十几年里各种风格的接口混在一起,有的文件结尾有换行有的没有,有的返回一张表有的只返回一个数字,同一个目录下 meminfo 是几百行的表格而 uptime 只有两个浮点数,谁也说不清为什么,这就是历史,我后来看内核源码才发现这两样东西甚至不在同一个文件里实现,一个在 fs/proc/meminfo.c,另一个在 fs/proc/uptime.c,各写各的,格式自然对不齐。
self 指向的是打开它的那个进程
self 和 thread-self 这两个名字不是 PID,但永远能打开。
1 | ls -l /proc/self | head -1 |
1 | lrwxrwxrwx ... /proc/self -> 12345 |
报出来的链接指向 12345,那是打开它的那个进程。
重点就在打开它的这半句上,我在 shell 里敲 ls /proc/self,链接指向的是 ls 自己,不是我的 shell,因为真正调用 open("/proc/self") 的是那个 ls 进程,它一晃就退了,链接也就作废了,要拿 shell 的得用 $$,shell 会把它替换成自己的 PID,路径才不会跑偏。这个坑我在写 COW 那个验证程序时栽过一次,用 system("grep VmRSS /proc/self/status") 想读自己的 RSS,读出来的永远是 sh 的,当时还以为 system 会复用当前进程的上下文,后来才明白它 fork 出来的是个全新的 sh,self 忠实地指向了它,那一下我才算真的理解 self 这个名字里的自指到底指给谁。
1 | echo "shell 的 PID: $$" |
进程目录里该看什么
/proc/PID/ 下几十个文件,常用的就这些。
| 文件 | 看什么 |
|---|---|
status |
人类可读的全景:状态、内存、uid/gid、信号掩码 |
stat |
一行,机器可解析,ps 就是从这读的 |
cmdline |
启动参数,\0 分隔 |
environ |
环境变量,\0 分隔 |
cwd |
符号链接,指向当前工作目录 |
exe |
符号链接,指向可执行文件 |
root |
符号链接,指向进程看到的 /(容器里很有用) |
fd/ |
每个打开的 fd 一个符号链接 |
limits |
所有 rlimit,比 ulimit -a 全 |
maps |
内存映射表 |
smaps |
maps 的加强版,带每块的 RSS |
io |
读写字节数(要权限) |
wchan |
等在内核哪个函数 |
stack |
内核栈(要 root) |
mountinfo |
挂载信息,比 mounts 全 |
cmdline 和 environ 是 \0 分隔的,直接 cat 会把一整排变量挤成一行,看着像坏了,其实数据是好的,只是分隔符不是换行,先 tr 一下再读就正常了,这一点跟别的文件都不一样。
1 | tr '\0' ' ' < /proc/self/cmdline; echo |
为什么大小是 0
因为 stat 报的 st_size 是内核随手填的,填 0 最省事,它也不知道该填多少,内容是读的时候才算出来的,算之前谁都不知道有多长。cat 不在乎这个,它一路 read 到返回 0 为止,所以大小 0 但有内容完全合法。
但有些工具在乎,这个差别会咬人。
| 工具 | 会出什么问题 |
|---|---|
cp |
有的版本先看 st_size,复制出空文件 |
tar |
打包出来是空的 |
rsync |
可能跳过 |
scp |
同上 |
所以在 /proc 下想保存内容必须重定向,不能复制文件,前者是一路 read 到底,后者上来先 stat 一下,看大小是 0 就以为没什么可读的。我是被 tar 坑过一次才知道的,备份脚本跑完不报错,解开一看是个空目录,当时以为是打包参数写错,来回改了三遍才发现问题不在参数,在那堆文件本身就没有大小。
1 | cat /proc/self/status > /tmp/status.txt # 对 |
为什么读某些文件会卡
因为那真的是个回调,回调要拿锁、要遍历、要等别人放手,读到一半被调度出去也是常事,文件大小是 0 不代表读取的代价是 0,这一点跟普通文件的直觉正好反过来,普通文件读得越多越慢,/proc 里有些文件读一次就等于让内核做一遍全表扫描。/proc/PID/stack 要读目标的内核栈,目标正卡在 D 状态的时候读它会卡一会儿,而且经常直接 Permission denied,它要 root 加上内核编译时开了 CONFIG_STACKTRACE,少一样都不行,还有个更隐蔽的情况是目标进程正在迁走,栈指针刚取到就已经失效,回头内核只能补一条不完整的调用链。
/proc/PID/smaps 要遍历每一块 VMA 再算 RSS,一个用了 20GB 的进程,读一遍能花几秒,我一开始以为是磁盘慢,iostat 看了半天一点动静都没有,才想起来这东西压根不碰磁盘。/proc/sysrq-trigger 是另一个极端,往里写一个字母就触发系统级动作,别碰。
最阴的是 /proc/kcore,它的大小不是物理内存,而是 ELF 头加上内核虚拟地址映射的总跨度,get_kcore_size() 用 kc_vaddr_to_offset() 一段段累加,x86_64 上很容易报到上百 TB,ls -l /proc 会把这个天量数字原样报出来,看着像磁盘要炸了(我第一次见的时候真去 df -h 核对了一遍,当然对不上,它压根不是容量)。它是个能当内存镜像读的 ELF 文件,绝对不能 cp,也别 cat,那东西会一路读到你 OOM。
/proc/sys 和 sysctl 是同一份数据
路径里的 / 换成 .,就是 sysctl 的名字,两套接口指向的是同一份数据,怎么写都落到同一个内核变量上。
/proc/sys/ 路径 |
sysctl 名字 |
|---|---|
/proc/sys/net/ipv4/ip_forward |
net.ipv4.ip_forward |
/proc/sys/vm/swappiness |
vm.swappiness |
/proc/sys/kernel/pid_max |
kernel.pid_max |
/proc/sys/fs/file-max |
fs.file-max |
改哪个都行,都要 root,而且 ip_forward 是能改变主机网络行为的敏感开关,改完记得改回去。
1 | sudo sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward' |
sysctl -w 底下就是写那个文件,只是多做了一点错误检查。
但两者都是临时的,重启回到默认值,因为 /proc/sys 本身就是内存里的一块数据结构,写进去就是改内存,中间没有任何落盘动作。要持久化得写 /etc/sysctl.d/*.conf,开机时 systemd-sysctl 会把那个目录下所有 .conf 按文件名字典序读一遍,再写进 /proc/sys。
1 | echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forward.conf |
sysctl --system 会把所有配置文件重读一遍,包括你刚写的那个,而 sysctl -p 只读 /etc/sysctl.conf,读不到 .d 目录,这是老教程里最常见的过时做法,我照着敲过一次,改完不生效,来回查了半小时才在手册页里看到还有另一个参数。
写之前先确认两件事
有些参数改错了会让系统直接不可用,动手前先看当前值和允许范围。
1 | cat /proc/sys/vm/swappiness |
大部分是 0 到 n 的整数,少数是字符串,比如 net.ipv4.tcp_congestion_control 的值是 cubic 或者 bbr。写只读文件报 Permission denied,写不存在的键报 No such file or directory,写进去不合法值的时候,有的直接 EINVAL,有的默默接受然后在别处崩。vm.min_free_kbytes 设太大能把机器 OOM 掉,而且你完全看不出来是谁干的,我是被这个参数坑掉一台测试机之后才养成先算清楚再写的习惯。
ps 的数字从哪来
ps 和 /proc/PID/stat 的关系,是 ps 不是魔法最直接的证据。
1 | sleep 300 & |
1 | 12345 (sleep) S 12300 12345 12345 0 -1 4194560 158 0 0 0 0 0 0 0 20 0 1 0 88213 4151296 123 18446744073709551615 ... |
一堆数字,位置是固定的。
| 字段位置 | 内容 | 对应 ps 的哪一列 |
|---|---|---|
| 1 | PID | PID |
| 2 | 括号里的 comm |
COMMAND |
| 3 | 状态字母 | STAT |
| 4 | 父 PID | PPID |
| 5 | pgrp(进程组) | |
| 14 | utime(用户态 jiffies) | TIME 的一部分 |
| 15 | stime(内核态 jiffies) | |
| 22 | starttime | |
| 24 | RSS(单位是页) | RSS |
第 2 个字段带括号,而且括号里可能有空格,进程名可以叫 my app,按空格切当场散架,得找到第一个 ( 和最后一个 ),把那截原样取出来,ps 自己也是这么干的。
再看 fd 目录,这是前面讲文件描述符那篇说的三层跳转的现场。
1 | ls -l /proc/$PID/fd |
sleep 只有三个,0、1、2,都指向 /dev/pts/0,这就是进程继承了 shell 终端的全部证据,想让它在后台活着又不往终端写,就得在启动的时候把这几个 fd 换掉。
然后看 environ,顺便再确认一次它也是假文件。
1 | tr '\0' '\n' < /proc/$PID/environ | head -8 |
再看那些大小是 0 但能读的。
1 | stat -c '%s %n' /proc/self/status /proc/self/cmdline /proc/self/maps 2>/dev/null |
前面全 0,后面有几百上千字节。
1 | cat /proc/sys/kernel/pid_max |
不用改,看这几个就够了,pid_max 尤其值得知道,它决定了僵尸能撑多久。
第一次读 /proc 会卡住的几处
有人说 /proc 是硬盘上的目录,其实它是内核现编的伪文件系统,一个字节都不占磁盘。有人说大小 0 的文件没内容,其实 st_size 是内核随手填的,内容在 read 的时候才生成。
有人说 cp /proc/xxx 能备份,其实看 st_size 的工具会拿到空文件。有人说 /proc/self 指向我的进程,其实它指向打开它的那个进程,你在 shell 里看到的永远是 ls 的那个。
有人说 sysctl -p 会读 .d 目录,其实它只读 /etc/sysctl.conf。有人说改 /proc/sys 是永久的,其实重启就失效。
有人说 /proc/<数字> 只有进程,其实还有线程的 TID 目录,和进程目录平级。有人说 /proc 可以随便 find,其实扫过去要遍历整个进程表,慢还报一堆错。
现编出来的文件
/proc 是伪文件系统,每个文件都是内核现算的回调输出,st_size 不可信。
/proc/PID/ 下是进程的全部内脏,status 给人看,stat 给 ps 看。
/proc/self 指向打开它的进程,不是你的 shell。
/proc/sys 和 sysctl 是同一份数据的两套接口,都是临时生效。
进程目录翻完,下一步是进程之间怎么打招呼。kill -9 里那个 9 是什么,SIGTERM 和 SIGKILL 的区别为什么不是强弱而是两套机制,以及一个进程要怎么退出才不丢请求、不断连接、不留脏数据,这些问题的答案都藏在信号那套编号里。











