路径都长得像文件,操作含义却很不同:tmpfs 保存应用数据,procfs 生成状态,sysfs 的写入可能控制设备。阅读这些接口,先确认职责、寿命和资源边界。
| 要解释的现象 | 阅读位置 |
| 读出的文本是否是一致快照? | 第 1~2 节:分类与 seq_file |
| 为什么不能用编辑器保存 sysfs 属性? | 第 3~4 节:控制与追踪接口 |
| 临时文件为何也可能触及 swap? | 第 5 节:tmpfs 与 memfd |
| 设备节点、早期根与实际挂载有何区别? | 第 6~7 节:启动与定位 |
1. 先按职责分类
| 文件系统 | 常见职责 | 关键边界 |
| procfs | 进程与系统信息、部分控制接口 | 受 PID namespace、权限和挂载选项影响 |
| sysfs | 设备模型及内核对象属性 | 属性接口需遵守 ABI,不是随意导出所有内部结构 |
| debugfs | 开发调试接口 | 通常不承诺稳定用户 ABI,也可能完全未启用 |
| tracefs | 追踪配置与数据 | 读不同文件可能是快照、阻塞流或消费记录 |
| tmpfs | 临时文件、共享内存相关存储 | 占用内存,可按配置换出;有空间与 inode 限额 |
| ramfs | 简单内存文件存储 | 不可按普通文件方式写回释放,也没有 tmpfs 式大小上限 |
| devtmpfs | 内核设备节点 | 节点存在不保证设备当前可用或有权限访问 |
/proc/filesystems 中的 nodev 不是“所有伪文件系统”的精确标签:NFS 等无需块设备挂载来源的文件系统也可列为 nodev。它更不等于挂载选项 nodev,后者限制把设备特殊文件当设备使用。
2. procfs:文本输出背后有对象寿命
/proc/<pid>/status、maps、fd 分别涉及任务状态、虚拟内存映射和文件描述符;这些对象可以在读取期间变化或退出。PID 还能复用,因此多次按同一数字路径读取,不必然操作的是同一个长期存活对象。需要稳定进程引用时,应了解 pidfd 等接口,而不是把目录名当永久身份。
/proc/self 按访问者解析。容器看到的进程集合取决于 proc 挂载关联的 PID namespace;权限、hidepid 等也影响可见性。并不是所有 /proc 系统统计都自动成为容器专属统计。/proc/<pid>/mem 受严格访问检查,不能概括成绕过 ptrace 权限的替代通道。见 procfs 文档。
seq_file 解决输出组织,不替你冻结系统
seq_file 管理分段输出、缓冲和定位,适合把可遍历对象生成文本。start/next/stop/show 等回调仍要遵守自身对象的同步和引用规则。缓冲不足或 seek 等情况可能导致重新遍历,show 不宜带不可重复的业务副作用。
单次 cat 也可能由多次 read 组成;输出的多个字段是否来自同一一致快照,取决于具体实现,不能仅凭“使用 seq_file”推导全局原子观测。用户态解析应按键识别、容忍新增字段,不宜依赖第几行永远是什么。见 seq_file 接口。
下面是一个只输出常量的独立内核模块示例,使用 proc_create_single() 复用 seq_file。它说明注册、失败返回和移除配对,不读取可变驱动对象:
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
static struct proc_dir_entry *entry;
static int kb_show(struct seq_file *seq, void *unused)
{
seq_puts(seq, "format_version 1\n");
seq_puts(seq, "example_value 42\n");
return 0;
}
static int __init kb_init(void)
{
entry = proc_create_single("kb_seq_example", 0444, NULL, kb_show);
return entry ? 0 : -ENOMEM;
}
static void __exit kb_exit(void)
{
proc_remove(entry);
}
module_init(kb_init);
module_exit(kb_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Constant seq_file teaching example");
对应 Kbuild 行为 obj-m += kb_seq_example.o。教学检查可以只编译对象文件,不需要加载模块。若把常量改成动态对象,仍必须处理移除时的并发回调、后台任务和私有数据寿命;复制这个框架不自动解决那些引用关系。
3. sysfs 与 kernfs:对象属性不是普通可编辑文件
sysfs 将内核对象以目录、属性和链接呈现;/sys/devices 表示设备层次,bus/class 等视图常通过链接组织。一个 kobject 通常对应目录,属性才对应文件,不能说每个 kobject 直接等于一个属性文件。
| 操作 | 属性接口实际做什么 |
| show | 生成当前值 |
| store | 验证输入并执行相应变更 |
| 返回成功 | 报告接口处理结果,未必表示任意设备操作都已完全结束 |
文本属性通常使用简洁明确的值;二进制属性有自己的契约。大小、seek 与重复读取也要按接口解释。见 sysfs 设计。
kernfs 为 sysfs、cgroupfs 等提供树和文件操作的基础设施,早于 6.x 就存在。它还需协调节点删除与正在执行的操作,不能只理解为“一个带 RCU 的目录树”。其链接设施主要是符号链接,不能泛称具有普通磁盘文件系统的任意硬链接语义。框架提供的同步也不代替驱动私有状态的保护。
用户态写控制属性时,应使用文档规定的整条输入格式,不要用编辑器的临时文件加 rename 保存模式。要理解某一属性的稳定性与单位,应优先找 Documentation/ABI/ 对应条目,而不是从文件名猜测。
4. debugfs 与 tracefs:观测也可能有开销和状态变化
debugfs 方便开发者导出数据,但接口可能随版本和配置变化。生产排查可以按权限和流程使用它;“不稳定 ABI”不等于生产环境绝对不能读。创建 debugfs 节点的驱动也要考虑用户持有打开引用时的设备移除和数据释放。
tracefs 为追踪设施提供独立挂载。trace 与 trace_pipe 的读取方式不同:后者可等待新事件并消费数据,因此 cat 不一定很快退出;修改事件开关、buffer 大小或 tracing_on 则会改变系统追踪状态。是否暴露某节点还受配置、挂载和权限限制。见 debugfs 与 ftrace 文档。
5. tmpfs:临时不等于不占空间,也不等于绝不触盘
tmpfs 使用 shmem 机制保存用户数据。它没有普通文件系统的数据块后端,但在允许 swap 的配置下可以把内容换出;所以“数据只在 RAM,绝不出现在磁盘”不是通用保证。卸载或重启后不能指望它像持久文件系统那样恢复,即使曾写入 swap 或调用 fsync。
四笔数量:限额、逻辑长度、驻留与换出
| 数量 | 意义 |
| size=、nr_inodes= | 容量和 inode 数量上限;不是预留全部内存 |
| 文件逻辑大小 | 稀疏扩展后可以很大,未必分配全部内容页 |
| 已分配页与驻留量 | 内容已分配多少、当前有多少在 RAM |
| 换出量 | 部分内容可能由 swap 后备承接 |
挂载限额、cgroup 和系统压力共同作用,挂载仍有余量不代表内存申请必定成功。见 tmpfs 文档。
/dev/shm 常挂载 tmpfs 以承载 POSIX 共享内存对象;/run 常存放运行状态;/tmp 则可能是 tmpfs,也可能是磁盘目录,应查看实际 mountinfo。System V 共享内存、匿名共享映射等也使用相关 shmem 机制,但不要求都对应 /dev/shm 中一个可见名字。
在下面先改变文件长度,再只写其中几页。把页面换出后,对照逻辑长度、已分配量、RAM 和 swap:释放 RAM 不会同时取消文件的已分配内容账。
正在呈现知识画面
文件有八页长,为什么不占八页 RAM?稀疏长度、文件系统分配额度、RAM 驻留和 swap 后备是四笔不同的账。换出降低驻留,却不释放 tmpfs 的内容分配额度。
{"id":"tmpfs-four-quantities","title":"文件有八页长,为什么不占八页 RAM?","summary":"稀疏长度、文件系统分配额度、RAM 驻留和 swap 后备是四笔不同的账。换出降低驻留,却不释放 tmpfs 的内容分配额度。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"length\" data-i18n=\"length\"></label><output for=\"length\" id=\"length-value\"></output><input id=\"length\" type=\"range\" min=\"0\" max=\"8\" value=\"8\" step=\"1\"></div><p class=\"caption\" data-i18n=\"pages\"></p><div class=\"choices\"><button data-page=\"0\">0<span id=\"p0\"></span></button><button data-page=\"1\">1<span id=\"p1\"></span></button><button data-page=\"2\">2<span id=\"p2\"></span></button><button data-page=\"3\">3<span id=\"p3\"></span></button><button data-page=\"4\">4<span id=\"p4\"></span></button><button data-page=\"5\">5<span id=\"p5\"></span></button><button data-page=\"6\">6<span id=\"p6\"></span></button><button data-page=\"7\">7<span id=\"p7\"></span></button></div><div class=\"actions\"><button id=\"write\" data-i18n=\"write\"></button><button id=\"read\" data-i18n=\"read\"></button><button id=\"swap\" data-i18n=\"swap\"></button></div><div class=\"metrics\"><div><span data-i18n=\"logical\"></span><strong id=\"logical\"></strong></div><div><span data-i18n=\"allocated\"></span><strong id=\"allocated\"></strong></div><div><span data-i18n=\"resident\"></span><strong id=\"resident\"></strong></div><div><span data-i18n=\"swapped\"></span><strong id=\"swapped\"></strong></div></div><div class=\"band\"><i id=\"ram-band\"></i><i class=\"second\" id=\"swap-band\"></i></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"full\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.choices button{min-height:7.5em}.choices span{display:block;font-size:11px;min-height:4.8em;margin-top:5px}.choices button.outside{opacity:.5}.choices button.swapped{border-style:dashed;border-color:var(--second)}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let chosen=0,pages=Array(8).fill(null),length=8,message='note';function draw(){const allocated=pages.filter(Boolean).length,ram=pages.filter(x=>x==='ram').length,swap=allocated-ram;set('length-value',length+' P');for(let i=0;i<8;i++){const e=document.querySelector('[data-page=\"'+i+'\"]');e.setAttribute('aria-current',String(i===chosen));e.classList.toggle('outside',i>=length);e.classList.toggle('swapped',pages[i]==='swap');set('p'+i,t(i>=length?'outside':pages[i]==='swap'?'swapState':pages[i]??'hole'));}set('logical',length*4);set('allocated',allocated+' / 4');set('resident',ram);set('swapped',swap);$('#ram-band').style.width=ram/4*100+'%';$('#swap-band').style.width=swap/4*100+'%';$('#read').disabled=$('#write').disabled=chosen>=length;$('#swap').disabled=chosen>=length||pages[chosen]!=='ram';set('explanation',t(message));}$('#length').oninput=()=>{length=+$('#length').value;for(let i=length;i<8;i++)pages[i]=null;message='note';draw();};document.querySelectorAll('[data-page]').forEach(e=>e.onclick=()=>{chosen=+e.dataset.page;draw();});$('#write').onclick=()=>{if(pages[chosen]===null&&pages.filter(Boolean).length>=4)message='full';else{pages[chosen]='ram';message='note';}draw();};$('#read').onclick=()=>{if(pages[chosen]==='swap')pages[chosen]='ram';message='note';draw();};$('#swap').onclick=()=>{pages[chosen]='swap';message='note';draw();};$('#reset').onclick=()=>{pages=Array(8).fill(null);length=8;$('#length').value=8;message='note';draw();};draw();","audio":false,"strings":{"allocated":"占用额度 / 4 页","full":"四页额度已满,不能为这个空洞分配新内容。先缩短文件释放已分配页;换出不能腾出此额度。","heading":"文件有八页长,为什么不占八页 RAM?","hole":"空洞","intro":"点击页选择位置,再写入、读取或换出;改变逻辑长度。观察已分配量与驻留量为何不一起变化。","length":"逻辑长度 / 页","logical":"逻辑长度 / KiB","note":"缩短文件会移除末尾内容;重新拉长只恢复空洞。换出保留内容与分配额度,读取换出页会让它回到 RAM。","outside":"文件末尾之外","pages":"页的位置与内容状态","ram":"驻留","read":"读取选中页","reset":"重置","resident":"RAM 驻留 / 页","scope":"八页地址范围、最多分配四页、每页 4 KiB。未写空洞读取零且模型不计持久内容分配;真实读取也可能有临时内存开销。允许 swap 且容量充足,省略 inode/cgroup 限额和元数据。只模拟,不申请真实压力内存。","scopeLabel":"模型范围与约定","swap":"换出选中页","swapState":"换出","swapped":"换出 / 页","write":"写入选中页"}}
memfd:通过 fd 管理的内存文件
普通 memfd_create() 提供匿名的内存文件,可 mmap、传递 fd,并可使用 seals 限制后续修改;MFD_HUGETLB 属于不同后备模式,不能与普通 shmem 路径混为一谈。seals 是对象级约束,不只是把当前 fd 改成只读;已有可写共享映射等也会影响某些 seal 能否加入。
import errno
import fcntl
import os
fd = os.memfd_create("kb-memfd", os.MFD_CLOEXEC | os.MFD_ALLOW_SEALING)
try:
assert os.write(fd, b"hello") == 5
seals = (fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW |
fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL)
fcntl.fcntl(fd, fcntl.F_ADD_SEALS, seals)
assert fcntl.fcntl(fd, fcntl.F_GET_SEALS) & seals == seals
assert os.pread(fd, 5, 0) == b"hello"
try:
os.pwrite(fd, b"X", 0)
except OSError as error:
assert error.errno == errno.EPERM
else:
raise AssertionError("sealed file unexpectedly writable")
print("memfd 内容可读,seal 阻止改写:通过")
finally:
os.close(fd)
此例不挂载文件系统,不创建永久路径;它也不演示持久化或秘密数据永不换出的保证。接口细节见 memfd_create(2)。
6. ramfs、rootfs、initramfs 与 devtmpfs
| 名称 | 在启动或存储中的角色 |
| ramfs | 内存文件存储,无普通写回后端及 tmpfs 式容量限制 |
| rootfs | 启动所需特殊根挂载,按配置使用 ramfs 或 tmpfs |
| initramfs | 解包到早期根的归档内容和启动环境 |
持续写 ramfs 可以消耗系统内存;initramfs 则不是与 ramfs/tmpfs 并列的磁盘文件系统格式。见 rootfs 与 initramfs。
设备节点:创建节点与启动硬件分开看
devtmpfs 由内核设备模型管理基本设备节点,udev 等用户态设施在事件之上应用权限、命名链接和其他规则。节点的基本创建不应全归功于 udev,也不能保证所有启动路径都由内核自动挂载好 /dev;内核配置与 initramfs 用户态会影响挂载过程。
设备节点是类型与设备号等元数据,打开后进入相应驱动;删除节点不等于停止硬件,重新创建节点也不保证驱动已存在。nodev 挂载限制、权限与设备访问控制仍参与授权判断。
7. 把观测限定在需要的范围
| 问题 | 优先查看 |
/tmp 到底是否在内存里 | findmnt -T /tmp 与实际挂载选项 |
| tmpfs 写满但系统还有内存 | 挂载容量、inode 限额与逻辑/实际占用 |
/proc/<pid> 看不到预期任务 | PID namespace、进程寿命、权限与 hidepid |
| sysfs 属性不存在 | 设备实例、驱动绑定、内核配置与 ABI 文档 |
| tracefs 读取不退出 | 是否读取阻塞/消费型流,是否还有事件产生 |
findmnt、df -t tmpfs 和定点读取属性通常足以建立第一轮事实。整棵递归遍历 /sys/devices 既昂贵又混杂;对动态接口也不能因扩展名像普通文件就批量读写。源码可沿 fs/proc/、fs/sysfs/、fs/kernfs/、mm/shmem.c 和 drivers/base/devtmpfs.c 逐层定位。