Linux内核分析之文件系统-04

This language version is unavailable; showing the other language.

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——容器镜像的联合挂载。