Linux内核分析之文件系统-04
37.1 /proc 文件系统 —— 进程与内核信息
procfs 把"每进程的内核账本"(/proc/<pid>/*)与"系统杂项"(/proc/sys、/proc/meminfo…)暴露为文件。它的独特基础设施是按需合成:目录内容随进程生灭动态枚举,文件内容在 read 时由回调生成。本节拆解其节点模型与 proc_ops。
37.1.1 proc_dir_entry:合成节点
// fs/proc/internal.h:31(节点结构, 节选要点)
struct proc_dir_entry {
unsigned int low_ino; /* 合成 inode 号 */
umode_t mode;
nlink_t nlink;
...
const struct proc_ops *proc_ops; /* 操作表 (非 file_operations!) */
proc_confuser *confuser;
...
union {
const char *name; /* 名字 (read 时按需生成内容) */
...
};
...
struct completion *pde_unload_completion;
struct list_head pde_openers; /* 跟踪 open 持有者 */
} __randomize_layout;
// include/linux/proc_fs.h:35(专用操作表)
struct proc_ops {
unsigned int proc_flags;
int (*proc_open)(struct inode *, struct file *);
ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *);
...
__poll_t (*proc_poll)(struct file *, struct poll_table_struct *);
long (*proc_ioctl)(struct file *, unsigned int, unsigned long);
...
};
proc_ops 与 file_operations 的分离是 procfs 的头号工程决策:proc 文件的 file->f_op 是一个通用包装(固定 proc_reg_read 等),内部按 pde->proc_ops 分发——这样 struct file_operations 的字段增删不再破坏全部 proc 驱动代码(历史上 read_proxy 插槽变更曾波及数百个调用点)。注册入口 __proc_create()(fs/proc/generic.c:418)与 proc_create(:489)/proc_mkdir(:529-536)建立节点树。
37.1.2 进程目录:动态枚举
/proc/<pid>/ 的按需合成:
pid 目录不是静态创建的:
查 /proc 目录 → proc_root_readdir 遍历
pid_hash 表的当前活进程 → 临时合成 dentry/inode
进程退出 → 节点随 pid 哈希消失 (无残留)
/proc/<pid>/ 的经典账本 (24/33 章多处引用过):
status 进程状态/RSS/uid (人类可读)
stat 机器可读全字段 (ps 的数据源)
maps VMA 列表 (22.2 节分区的用户态视图)
smaps VMA + 每段 RSS/PSS/Shared/Private
fd/* 打开文件 (符号链接到 f_path, 33.1.4 节)
stack 内核栈回溯 (锁死诊断)
/proc/self 始终指向读者自己 (进程内自省的巧门)
/proc/sys ← sysctl 的文件面 (调参入口, 全书多处)
/proc/meminfo, vmstat, interrupts, locks (33.5!)...
读取的语义陷阱:/proc/<pid>/stat 的多字段解析要求按"最后一个右括号"切分(进程名可含空格/括号)——无数解析器在这栽过。procfs 输出无原子性:读 4KB 的 status 跨两次 read 回调之间进程可能变化,seq_file 接口提供"逐条目重放"缓解(proc_ops 内的 proc_seq_start/next/stop)。
小结
procfs 以 proc_dir_entry + 专用 proc_ops 实现"函数即文件",与 file_operations 的隔离解除了 ABI 演进负担;进程目录按 pid 哈希动态合成、随进程生灭,seq_file 缓解多字段输出的原子性。它是全书反复出现的观测面(meminfo/vmstat/smaps/locks)的内核产地。下一节看 sysfs 如何用 kernfs 把 kobject 属性合成文件。
37.2 /sys (sysfs) —— 设备与驱动模型导出
sysfs 的规则极简:每个 kobject(39 章设备模型的对象)一个目录、每个 attribute 一个文件。它的实现底座是 kernfs——一个"合成文件系统框架",procfs 的演进教训(结构污染、锁层级)都在这里被吸收。本节拆解 kernfs 的属性机制与驱动模型的导出面。
37.2.1 kernfs:合成文件框架
// include/linux/kernfs.h:277(操作表, 节选要点)
struct kernfs_ops {
int (*open)(struct kernfs_open_file *of);
void (*release)(struct kernfs_open_file *of);
ssize_t (*read)(struct kernfs_open_file *of,
char *buf, size_t bytes, loff_t off);
...
size_t (*atomic_write_len)(...);
ssize_t (*write)(struct kernfs_open_file *of,
char *buf, size_t len, loff_t off);
...
__poll_t (*poll)(struct kernfs_open_file *of,
struct poll_table_struct *pt);
...
};
kernfs 的 open/read/write 回调拿到的是整页缓冲(读时内核先调 show 填满、写时先收满再调 store)——"一次交互一整值"的属性模型,天然规避 procfs 逐段 read 的解析陷阱。poll 钩子让属性可等(设备的 uevent/cgroup 控制文件的唤醒依赖此)。sysfs 是 kernfs 的第一个消费者(cgroupfs 是第二个,12 章——两者的目录树/属性机制完全共享)。
37.2.2 kobject 属性:一驱动一文件
// fs/sysfs/file.c:364
int sysfs_create_file_ns(struct kobject *kobj, const struct attribute *attr,
const void *ns);
驱动视角的属性定义 (include/linux/sysfs.h 的宏族):
static ssize_t my_show(struct device *dev,
struct device_attribute *attr, char *buf)
{ return sysfs_emit(buf, "%d\n", my_value); }
static ssize_t my_store(..., const char *buf, size_t count)
{ sscanf(buf, ...); return count; }
static DEVICE_ATTR_RW(my); /* 生成 struct device_attribute */
设备注册时: device_create_file(dev, &dev_attr_my)
→ /sys/devices/.../my 出现
cat /sys/.../my → my_show
echo 1 > /sys/.../my → my_store
sysfs_emit: 强制"单值+换行"格式化 (防内核堆格式化
注入面 — 老式 snprintf 的 %s 嵌套曾被滥用)
目录树即设备树:/sys/devices/(物理拓扑)→ /sys/bus/(总线驱动绑定,39.4 节 probe)→ /sys/class/(功能视角 net/block/tty…)是同一批 kobject 的三重投影——kobject 在树中的位置由"挂到哪个 parent"决定,多处挂靠靠 symlink 实现。uevent 文件与 netlink 广播(48 章)是 udev 感知设备热插拔的通道。
37.2.3 与 procfs 的分工纪律
文档化约定 (Documentation/filesystems/sysfs.rst):
一属性一值: echo/cat 全值读写, 不做多字段表
可预测: 值/单位/语义在 Documentation/ABI/ 声明
二进制勿放: sysfs 只放 ASCII 属性; 二进制导出用
bin_attribute (仅固件 dump 类)
proc/sys/debug 三者分工:
proc 进程相关 + 历史杂项 (不再新增系统级)
sysfs 结构化对象状态 (设备/驱动/cgroup)
sysctl(/proc/sys) 内核可调参数
小结
sysfs 以 kernfs 为底座实现"一 kobject 一目录、一 attribute 一文件":整页缓冲的 show/store 模型、可 poll 的属性、sysfs_emit 的格式化安全;目录树的三重投影(devices/bus/class)让同一设备拓扑以不同视角可查,uevent 通道供 udev 消费。proc 管"进程与历史杂项"、sysfs 管"结构化状态"、sysctl 管参数——分工纪律是内核 ABI 文档化的样板。下一节看没有稳定 ABI 之虑的调试出口 debugfs/tracefs。
37.3 debugfs 与 tracefs
debugfs 是"想导出就导出"的调试出口——无 ABI 承诺、仅调试配置可用;tracefs 承载 ftrace 的事件与跟踪控制面。两者合起来是内核开发与性能分析的第一工作台(本书记录的 blk-mq-debugfs、writeback 统计等都以此为窗)。
37.3.1 debugfs:无门槛导出
debugfs 的 API 族 (fs/debugfs/inode.c + file.c):
debugfs_create_file(name, mode, parent, data, fops)
debugfs_create_u32/x64/bool/atomic_t/size_t(...) 单值速成
debugfs_create_dir / symlink / blob / regset32
debugfs_create_devm_seqfile (设备驱动随手导出)
语义:
- 挂载点 /sys/kernel/debug (debugfs, 通常开机挂)
- 数据/回调随意, 格式随意, 随时改名删除
- CONFIG_DEBUG_FS 关闭时全部 API 变空操作 —
驱动代码无条件调用, 生产内核零痕迹
全书出现过的实例:
/sys/kernel/debug/blk/*/ blk-mq 逐请求状态 (35.3.3)
/sys/kernel/debug/kvm/*/ VM/VCPU 统计 (23.4 节排查序)
/sys/kernel/debug/ext4/*/ 焦点组/mballoc 视图 (34.4.3)
/sys/kernel/debug/tracing → tracefs 的符号链接
纪律由"无承诺"实现:文档明确警告"debugfs 的文件没有兼容义务"——正是这份自由让开发者敢于导出内部结构;它也因此不能进生产依赖(有 CVE 把 debugfs 当攻击面——生产建议不挂载或限权)。
37.3.2 tracefs:ftrace 的控制面
/sys/kernel/tracing 的关键文件 (ftrace 框架):
available_events 全部 tracepoint (本书各章大量引用:
mm_page_alloc, kmem_cache_alloc,
sched_switch, writeback_*)
events/<sub>/<event>/enable|filter|format
事件开关/过滤/格式自省
set_graph_function / set_ftrace_filter
函数跟踪的范围控制
tracer_on/off / buffer_size_kb / trace_pipe
缓冲控制与流式读取
per_cpu/cpu*/trace 每核缓冲 (抢占延迟分析)
tracefs 与 debugfs 的关系: tracefs 独立挂载
(ftrace 控制面), debugfs 下留符号链接向后兼容;
BPF (perf/ftrace-bpf) 经同一 tracepoint 接口挂接
tracepoint 是两者的桥梁:子系统的 TRACE_EVENT 宏(本章各卷引用过的 mm/page_alloc、kmem、sched、writeback 事件)经 tracefs 暴露开关与格式,经 perf/BPF 提供采样与编程消费——"内核自带的观测仪器架"。42 章 softirq、11 章调度延迟的分析工具全部立于此。
小结
debugfs 以"无 ABI 承诺 + DEBUG_FS 关闭即空操作"换取导出自由,是全书内核内部状态观测的实物产地;tracefs 以 tracepoint 事件架 + ftrace 控制面成为内核自观测的仪器台,perf/BPF 共享同一事件接口。两者的共同哲学:观测能力默认随内核分发,成本只在打开时支付。下一节看四个伪文件系统中唯一"存真数据"的 tmpfs。
37.4 tmpfs 与 ramfs —— 内存文件系统
tmpfs 用页缓存当存储:文件内容是普通的匿名/页缓存页,"落盘"是幻觉(掉电即失,swap 可能兜底)。它是 /dev/shm(30.1 节)、SysV shm 真身(28.4 节)、匿名 MAP_SHARED(22.3.3 节)与 initramfs 的共同底座;ramfs 是它的无限额无 swap 祖先。本节拆解 shmem 的数据结构与内存供给。
37.4.1 tmpfs 的定位:页缓存当存储
tmpfs = "没有后备设备的文件系统":
普通 ext4 文件: 页缓存 ←→ 磁盘块 (36 章回写双写)
tmpfs 文件: 页缓存就是最终存储, 无回写路径
dirty 页永远不会写盘 (没有 a_ops->writepage 目标)
内存压力下的出路: swap (匿名页语义, 24 章)
→ tmpfs 兼具 页缓存身份 与 匿名页命运
两个祖先:
ramfs: 最简 (无大小限制!) — initramfs 用它
写满 = 内存耗尽, 仅受 cgroup 约束
tmpfs/shmem: ramfs + 大小限额 + swap 支持 +
XArray/扩展属性 — 通用内存文件系统
37.4.2 shmem 的数据结构与供给
// mm/shmem.c:2749(缺页供货, 与 25 章矩阵的对接点)
static vm_fault_t shmem_fault(struct vm_fault *vmf)
// mm/shmem.c:4172(symlink: 目标串存在页里)
static const char *shmem_get_link(struct dentry *dentry,
struct inode *inode, ...)
shmem 的三件内政:
[1] 页来源二分:
页缓存池 (a_ops 正常供货) / 大页 (THP/hugetlb 变体)
swap 预留: SHM_NORESERVE 的记账点 (28.4.2 节引用过)
[2] shmem_inode_info (inode->i_private):
交换记账、XArray 内 swap 槽索引、
稀疏空段处理 (洞页不占内存)
[3] 缺页路径 shmem_fault (:2749):
XArray 有页? → 映射
在 swap? → swap-in (22.4.5 节同款)
都无 → 分配清零页入缓存 (首次读=零页, 25.2.1)
挂载形态:
/dev/shm POSIX shm (30.1 节)
内部 shm_mnt SysV shm 真身 (mm/shmem.c:46, 28.4)
匿名 MAP_SHARED 不可见的 tmpfs 文件 (22.3.3)
initramfs 根文件系统的第一形态 (3.10 节关联)
"匿名 MAP_SHARED 不可见"的机制:内核在内部 tmpfs 上 shmem_file_setup 建文件,VMA 挂它的 i_mmap 树——30.1 节"三个 API 一个真相"的最后一环。shmem_get_link(:4172)则展示 tmpfs 的细节洁癖:连 symlink 的目标串都存在页里(不占额外内存类型),读链接就是读页。
37.4.3 限额与运维
限额体系:
mount -o size=4G 文件系统总量上限 (默认 RAM/2)
RLIMIT_AS/cgroup 进程/cgroup 视角
swap 交互: tmpfs 页可换出 → size 可超物理内存
(swap 空间变成 tmpfs 的"后备")
运维事故对照 (28.3.4/30.1 节呼应):
/dev/shm 塞满 → shm_open 写失败 (ENOSPC)
tmpfs 无限写 → 内存耗尽 (ramfs 祖先的教训)
df /dev/shm 常规巡检项
小结
tmpfs 以页缓存为最终存储、swap 为溢出通道,兼具页缓存身份与匿名页命运;ramfs 是去掉限额与 swap 的极简祖先(initramfs 专用)。shmem_fault 把供货逻辑并进 25 章缺页矩阵,/dev/shm、内部 shm_mnt、匿名 MAP_SHARED、initramfs 四种挂载形态共享同一实现——它是本书第五、六部分多条线索(共享内存、页缓存、缺页)的物理交汇点。第六部分还剩最后一章:OverlayFS——容器镜像的联合挂载。