- 容器运行时
- 云原生
【免费下载链接】docker-ce
:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:
procfs是 Prometheus 团队维护的纯 Go 库,它把 Linux/proc与/sys伪文件系统中散落的系统、内核与进程指标封装成一组类型安全的 Go API,是 node_exporter、客户端采集器以及容器运行时监控链路的底层基石。本文以 Docker CE 仓库内 components/engine/vendor/github.com/prometheus/procfs 的实际代码为蓝本,系统讲解其设计思想、包组织方式、核心 API 用法与测试工具链,帮助你快速理解并上手这套“无需 cgo、跨平台可编译”的指标采集方案。
procfs 是什么:从裸文件读取到结构化指标
Linux 内核会把运行状态持续暴露在两组伪文件系统里:/proc(进程与系统统计)和/sys(内核参数与设备信息)。直接读取这些文件并用strings.Fields、strconv手写解析,不仅重复劳动巨大,还极易踩到“文件长度报告错误”“返回 EAGAIN 导致读取阻塞”等内核伪文件的特殊坑。
procfs库的职责正是把这些零散文件抽象成 Go 结构体与方法:调用方不再关心行格式、字段顺序与单位换算,拿到的是Stat、Meminfo、CPUInfo这类语义化数据。从源码结构看,整个库由以下部分构成:
- 根包
procfs:系统级指标(CPU、内存、负载、网络、挂载等)与进程级指标(/proc/[pid]/*)的入口; internal/fs:封装挂载点校验与路径拼接,见 internal/fs/fs.go;internal/util:提供绕过os.Stat的安全读取工具与数值解析器,见 internal/util/readfile.go。
值得注意的是,随 Docker CE 引擎一起 vendored 的这份库在代码中标注了Work in Progress警告:其 API 可能在未通知的情况下发生不向后兼容的破坏,生产代码引用时需要锁定版本(Docker 通过 vendor.conf 等清单文件固化版本)。
为什么必须绕过 os.Stat:伪文件系统的读取陷阱
内核伪文件与传统文件有一个关键差异:很多/proc、/sys文件在stat时报告的尺寸要么是 0、要么是固定的 4096,与真实内容长度不符。若先os.Stat再按尺寸读取,要么读到空、要么读到残留脏数据。procfs的 internal/util/readfile.go 直接os.Open后配合io.LimitReader读取,并通过 512 KiB 的maxBufferSize约束单次读取上限,避免内核文件被无限放大。
/sys下部分文件还存在另一类问题:某些驱动(如 hwmon)会返回EAGAIN,导致 Go 标准库ioutil.ReadFile陷入轮询死等。为此库内提供了 sysreadfile.go 这样的专用实现,直接通过syscall.Read一次性读取(单次上限 128 字节),要么拿到数据要么立刻失败,绝不挂起。
快速上手:两条核心初始化路径
procfs的对外入口统一是一个FS类型,它代表某个伪文件系统的挂载根。初始化方式有两种:
路径一:只读 /proc 的系统级指标
大多数 CPU、内存、进程统计来自/proc/stat、/proc/meminfo等文件,使用根包procfs.NewFS("/proc")即可:
fs, err := procfs.NewFS("/proc") if err != nil { log.Fatal(err) } stats, err := fs.Stat() // 解析 /proc/stat这段代码对应文档中的官方示例。NewFS的实现(fs.go)会先对挂载点执行os.Stat校验——目录不存在或不是目录都会直接返回错误。如果不关心挂载点差异,也可以用procfs.NewDefaultFS()直接使用内置常量DefaultMountPoint = "/proc"。
路径二:同时需要 /proc 与 /sys
少数子包如blockdevice(块设备磁盘统计)需要同时访问两个伪文件系统:
fs, err := blockdevice.NewFS("/proc", "/sys") stats, err := fs.ProcDiskstats()这是文档中给出的典型用法。其背后是internal/fs定义的默认挂载点常量:/proc、/sys以及/sys/kernel/config,见 internal/fs/fs.go。
FS 的路径拼接设计
FS底层其实就是一个字符串类型(type FS string),通过Path方法把挂载根与子路径用filepath.Join拼接(internal/fs/fs.go)。这种设计使得/proc、/sys乃至/proc/<pid>都能复用同一套拼接与读取逻辑,也便于在容器场景下把宿主机的/proc挂载到其他位置后再初始化。
包组织:按数据来源与信息类型分层
文档明确指出,项目按两条维度组织包:
- 数据来源:来自
/proc、/sys还是两者; - 信息类型:进程信息、块设备、网络等。
据此形成了清晰的分层:
| 层级 | 包 | 覆盖范围 | 关键文件 |
|---|---|---|---|
| 根包 | procfs | 系统级 CPU/内存/负载/网络统计 +/proc/[pid]进程统计 | stat.go、meminfo.go、proc.go |
| 子包 | blockdevice等 | 需要同时访问 /proc 与 /sys 的块设备信息 | 需联合两个挂载点初始化 |
| 内部工具 | internal/fs、internal/util | 挂载点抽象、安全读取、数值解析 | internal/fs/fs.go、internal/util/readfile.go |
例如,进程级信息(PID、命令行、文件描述符等)集中在根包内;磁盘等块设备信息则下沉到blockdevice子包;挂载点与读取工具属于内部实现细节,不对外暴露。
核心 API 深度解析:系统级指标
根包提供了覆盖面很广的系统级查询方法,下面结合源码逐一拆解。
1. 系统统计fs.Stat()—— /proc/stat
/proc/stat是 CPU 与内核活动的核心统计文件。FS.Stat()(stat.go)逐行解析,返回结构体包含:
| 字段 | 含义 | 来源键 |
|---|---|---|
BootTime | 系统启动时间(Epoch 秒) | btime |
CPUTotal | 全体 CPU 汇总统计 | cpu |
CPU | 按核索引排列的每核统计 | cpuN |
IRQTotal/IRQ | 中断总数与按编号分布 | intr |
ContextSwitches | 上下文切换次数 | ctxt |
ProcessCreated | 已创建进程数 | processes |
ProcessesRunning/ProcessesBlocked | 运行中/阻塞进程数 | procs_running/procs_blocked |
SoftIRQTotal/SoftIRQ | 软中断总数与分类明细 | softirq |
CPU 时间字段(User/Nice/System/Idle/Iowait/IRQ/SoftIRQ/Steal/Guest/GuestNice)在解析时统一除以userHZ常量(值为 100),把内核时钟滴答换算为秒。这个换算系数经过了充分考证:源码注释(proc_stat.go)说明,为避免引入 cgo 与交叉编译问题,USER_HZ在 Go 支持的所有平台上都被硬编码为 100。per-CPU 行通过cpu后缀解析出 CPU ID,首行cpu(无后缀)则汇总到CPUTotal。
2. 内存统计fs.Meminfo()—— /proc/meminfo
/proc/meminfo记录了几乎全部内存信息。FS.Meminfo()(meminfo.go)返回的Meminfo结构体覆盖 50 余个字段,常用者包括:
MemTotal/MemFree/MemAvailable:总内存、空闲内存与可用于启动新应用的估算可用内存;Buffers/Cached/SwapCached:页缓存与交换缓存;Active/Inactive/AnonPages/Shmem:活跃/不活跃内存、匿名页与共享内存;SwapTotal/SwapFree:交换分区总量与剩余;Slab/SReclaimable/SUnreclaim:内核 slab 缓存及其可回收部分;CommitLimit/CommittedAS:按 overcommit 策略计算的承诺上限与已承诺内存;HugePages_Total/HugePages_Free、DirectMap4k/DirectMap2M/DirectMap1G:HugePages 与直接映射明细。
解析器(meminfo.go)按行拆分后取前两个字段,忽略kB单位并以strconv.ParseUint(fields[1], 0, 64)兼容十六进制前缀。
3. CPU 信息fs.CPUInfo()—— /proc/cpuinfo
FS.CPUInfo()(cpuinfo.go)返回[]CPUInfo,每个元素描述一个逻辑 CPU,字段包括Processor、VendorID、ModelName、CPUMHz、CacheSize、PhysicalID、Siblings、CoreID、CPUCores、Flags(特性标志位列表)与Bugs等。解析器以processor行作为新 CPU 条目的起点,其余行按key: value匹配填充。
4. 负载与交换
fs.LoadAvg()(loadavg.go):读取/proc/loadavg前三个字段,返回 1/5/15 分钟平均负载Load1、Load5、Load15;fs.Swaps():解析/proc/swaps,返回交换设备列表;fs.BuddyInfo()(buddyinfo.go):读取/proc/buddyinfo,按内存节点与 zone 返回空闲页块分布数组,块大小由2^n*PAGE_SIZE决定。
5. 网络指标
fs.NetDev()(net_dev.go):解析/proc/net/dev,以接口名为 key 返回NetDev映射,每条NetDevLine含 RX/TX 双方向的字节数、包数、错误、丢弃、FIFO、压缩等 16 个字段;还提供Total()方法对所有接口求和;fs.NetSoftnetStat()(net_softnet.go):读取/proc/net/softnet_stat,返回每个 CPU 的处理包数、丢弃包数与配额挤占次数,可直接用于观察收包软中断是否饱和;- 此外还有 conntrack、sockstat、UDP、UNIX socket 等网络子统计文件,均以
fs.Net*系列方法暴露。
6. 挂载信息
fs.Proc.MountInfo()与顶层GetMounts()/GetProcMounts(pid)(mountinfo.go):解析/proc/self/mountinfo与/proc/<pid>/mountinfo。每条MountInfo包含MountID、ParentID、Root、MountPoint、FSType、Source以及挂载/超级块选项。解析器(mountinfo.go)要求行内至少 11 个字段,并强制校验倒数第 4 位必须是分隔符-,否则判定该行格式非法;p.MountStats():读取进程命名空间内的/proc/[pid]/mountstats,给出挂载点的统计与配置详情。
进程级 API:/proc/[pid] 的全面抽象
根包的进程能力集中在Proc结构体上(proc.go),通过三个便捷函数进入:
p, err := procfs.Self() // 当前进程,等价于 /proc/self p, err := procfs.NewProc(pid) // 指定 PID ps, err := procfs.AllProcs() // 全部进程列表AllProcs()实现(proc.go)会读取/proc根目录,把纯数字目录名当作 PID 组装成Procs切片,天然跳过非数字目录;Procs还实现了sort.Interface,可按 PID 排序。
基础信息
| 方法 | 数据源 | 返回值 |
|---|---|---|
p.CmdLine() | /proc/[pid]/cmdline | 以\0分隔再切片的命令行参数数组 |
p.Comm() | /proc/[pid]/comm | 进程命令名 |
p.Executable() | /proc/[pid]/exe符号链接 | 可执行文件绝对路径 |
p.Cwd() | /proc/[pid]/cwd | 当前工作目录 |
p.RootDir() | /proc/[pid]/root | chroot 后的根目录 |
注意:Executable、Cwd、RootDir在符号链接失效(进程已退出)时返回空串而非错误,这是源码中明确的容错策略。
文件描述符
p.FileDescriptors():返回所有打开的 fd 编号([]uintptr);p.FileDescriptorsLen():fd 总数——这是判断进程文件句柄是否泄漏的最常用指标;p.FileDescriptorTargets():每个 fd 的符号链接目标(socket 等非文件目标返回空串);p.FileDescriptorsInfo()/p.FDInfo(n):进一步解析/proc/[pid]/fdinfo,得到每个 fd 的详细状态。
统计与状态
p.Stat()(proc_stat.go):解析/proc/[pid]/stat,返回ProcStat,核心字段包括Comm、State、PPID、PGRP、UTime/STime/CUTime/CSTime(用户/内核态及子进程时钟滴答)、Priority、Nice、NumThreads、VSize(虚拟内存字节数)、RSS(以页为单位)等。解析器利用/proc/[pid]/stat中(comm)括号定位命令名,从而规避命令名内包含空格导致的字段错位问题。配套便捷方法:CPUTime()=(UTime+STime)/userHZ,返回 CPU 时间秒数;VirtualMemory()=VSize;ResidentMemory()=RSS * os.Getpagesize(),把页数换算为字节;StartTime()=BootTime + Starttime/userHZ,得到进程启动的 Unix 时间戳。
p.NewStatus()(proc_status.go):解析/proc/[pid]/status,返回ProcStatus。字段按Key: Value逐行匹配,内存类字段(VmPeak、VmSize、VmRSS、VmData、VmSwap等)会去掉kB后缀并乘以 1024 统一换算成字节;还包含VoluntaryCtxtSwitches/NonVoluntaryCtxtSwitches,以及TotalCtxtSwitches()求和方法。p.IO()(proc_io.go):解析/proc/[pid]/io,返回rchar/wchar(字符读写)、syscr/syscw(读写系统调用次数)、read_bytes/write_bytes(字节读写)与cancelled_write_bytes。p.Schedstat():返回任务调度统计ProcSchedstat。
命名空间与映射
p.NS():解析/proc/[pid]/ns的命名空间符号链接信息;p.Maps()/p.ProcMaps():解析/proc/[pid]/maps的内存映射布局;p.Environ():读取进程环境变量;p.Limits():解析/proc/[pid]/limits的进程资源限制。
实战范例:进程 CPU 与内存快照
结合 doc.go 中的官方示例,一个完整的最小程序如下:
package main import ( "fmt" "log" "github.com/prometheus/procfs" ) func main() { p, err := procfs.Self() if err != nil { log.Fatalf("could not get process: %s", err) } stat, err := p.Stat() if err != nil { log.Fatalf("could not get process stat: %s", err) } fmt.Printf("command: %s\n", stat.Comm) fmt.Printf("cpu time: %fs\n", stat.CPUTime()) fmt.Printf("vsize: %dB\n", stat.VirtualMemory()) fmt.Printf("rss: %dB\n", stat.ResidentMemory()) }输出示例:
command: myapp cpu time: 12.340000s vsize: 2147483648B rss: 31457280B如需统计所有进程的 CPU 使用率,可先procfs.AllProcs()获取列表,再对每个元素调用p.Stat()并累积CPUTime()差值;如需按 PID 对进程排序,直接使用Procs类型自带的排序能力。
在 Prometheus 客户端中的真实调用链
仓库内 components/engine/vendor/github.com/prometheus/client_golang/prometheus/process_collector_other.go 展示了 procfs 在真实监控场景中的用法:
_, err := procfs.NewDefaultFS() // 探测 /proc 是否可读 p, err := procfs.NewProc(pid) // 获取目标进程 stat, _ := p.Stat() ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime()) ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory())) fds, _ := p.FileDescriptorsLen() // 开放 fd 数 limits, _ := p.Limits() // 资源上限这正是 process collector 的采集路径:CPU 时间、虚拟内存、RSS、启动时间、开放 fd 数与 fd 上限全部来自 procfs。在容器场景下,若宿主机/proc被绑定挂载到非标准路径,只要用NewFS传入正确挂载点,同一套代码即可直接采集容器或宿主机视角的指标。
构建与测试:如何验证与更新 fixtures
procfs定位为应用内嵌库,不产出独立可执行二进制,因此没有分发二进制文件;但其 API 大多数都配有单元测试。文档约定的测试入口是make test。由于仓库 vendored 场景下通常不包含 Makefile 与 fixtures.ttar(测试夹具在 CI/上游源码树中提供),完整流程参考上游约定:
测试夹具机制
库内含有一套模拟/proc、/sys的测试夹具,以单个 ttar 归档(fixtures.ttar)存放,测试运行时自动解包成fixtures目录。由于这些伪文件在真实内核上才能生成,夹具机制让单元测试完全脱离内核环境即可运行。
# 重新解包夹具并运行全部测试 rm -rf fixtures make test更新夹具的完整流程
当/proc、/sys出现新字段或格式变化,需要按以下步骤更新夹具:
- 删除旧
fixtures目录,执行make test或make fixtures/.unpacked重新解包最新归档; - 在
fixtures目录中修改文件,模拟新的内核输出格式; - 运行
make update_fixtures,依据修改后的目录重新生成fixtures.ttar; - 用
git diff fixtures.ttar核对归档变更是否符合预期。
这套流程保证了“固定输入 → 固定输出”的确定性测试:只要夹具与真实内核文件保持一致,解析逻辑的回归风险就能被充分覆盖。在 Docker CE 仓库中,该库作为 vendored 依赖锁定版本(见 vendor.conf),升级时同样需要经过上述夹具验证步骤。
使用注意事项与边界
- API 尚不稳定:README 明确警告该包仍处于开发演进中(Work in Progress),可能出现不兼容破坏,依赖方应锁定版本并关注变更;
- 仅限类 Unix 平台:库依赖
/proc//sys,vm.go等文件带!windows构建标签,Windows 平台不可用; - 读取上限:常规文件单次最多读 512 KiB,超大文件建议改用 scanner 逐行处理;
/sys单值文件则受 128 字节上限约束; - USER_HZ 硬编码:CPU 时间换算固定按 100 tick/秒进行,极端异构系统上可能出现最多两个指标略微偏差,属已知取舍;
- 错误处理:符号链接类读取(
exe、cwd、root)在目标消失时返回空串而非报错,解析 fd 等场景需自行判断数据可用性。
小结
procfs 用一个简洁的FS抽象把 Linux 伪文件系统转化为高可用的 Go API,既覆盖了系统级 CPU、内存、网络、挂载、负载指标,也覆盖了进程级的状态、IO、fd、命名空间与资源限制,是 Prometheus 生态采集体系不可替代的底层基础设施。在 Docker CE 仓库中,它以 vendored 依赖形式存在,并与 client_golang 的 process collector 形成完整的“采集 → 上报”链路。阅读本文后,你可以直接在 procfs 根目录 的*_test.go与doc.go中继续深入每个 API 的实现细节,并在自己的 Go 服务中复刻这套零 cgo、跨平台可编译的指标采集方案。
- 容器运行时
- 云原生
【免费下载链接】docker-ce
:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:
相关推荐
prometheus/procfs 库实战指南:从 /proc 与 /sys 伪文件系统采集 Go 系统指标
prometheus/procfs 库实战指南:从 /proc 与 /sys 伪文件系统采集 Go 系统指标 导读 github.com/prometheus/
CLI开发工具深入解析 prometheus/procfs:从 /proc 与 /sys 伪文件系统采集系统指标的 Go 库
深入解析 prometheus/procfs:从 /proc 与 /sys 伪文件系统采集系统指标的 Go 库 导读 procfs 是 Prometheus 生
后端微服务存储认证鉴权Hyperledger Fabric 中的 prometheus/procfs 库:从 /proc 与 /sys 伪文件系统采集系统指标的 Go 实现解析
Hyperledger Fabric 中的 prometheus/procfs 库:从 /proc 与 /sys 伪文件系统采集系统指标的 Go 实现解析 导读
区块链密码学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考