资讯动态

匿名文件:内核自动清理的临时文件机制解析

发布时间:2026/10/15 22:21:27 来源:尧图企业网站定制
你有没有遇到过这种情况程序崩溃后/tmp目录下面留下一堆不知道是谁创建的临时文件不仅占磁盘还得专门写个 cron 去清理。我在维护某个高并发服务时被这种问题折磨了很久直到我深入了解了一种特殊的文件类型——匿名文件。这种文件从诞生起就不需要程序员操心清理它的整个生命周期由内核托管最后一个引用释放时自动消失进程崩溃也不会残留甚至你在文件系统目录树里永远找不到它的名字。这篇文章我就围绕匿名文件展开讲讲它到底怎么实现内核自动清理有哪些常用路径memfd_create和O_TMPFILE适合哪些场景以及我用它落地时踩过的坑和总结的经验。如果你写后端、搞系统组件、做安全模块或者只是对 Linux 文件系统感兴趣这篇都值得花十分钟读完。1. 从删不掉的文件说起临时文件的生命周期困局先别急着上代码。想真正理解匿名文件得先知道传统临时文件方案到底别扭在哪。1.1 传统临时文件的问题到底在哪很多程序需要临时文件无非是三种诉求需要一个不至于撑爆内存的落盘区域比如排序中间结果。需要一块能通过路径让别的进程访问的数据交换区。需要保存运行过程中的配置快照崩溃后可排查。最常见的做法是在/tmp下创建一个带随机名字的文件用完就unlink进程退出前再做一次兜底清理。听起来挺合理但实际生产环境里全是漏洞。第一路径就是安全隐患。文件名一旦生成理论上任何有权限访问该目录的进程都能猜到或遍历到。你把一份带敏感信息的临时数据放在/tmp/xxxx等于把钥匙挂在门口。第二崩溃只在一瞬间兜底清理经常不执行。进程被SIGKILL杀掉atexit不跑清理函数不跑文件就永远躺在那里了。时间一长垃圾文件累积磁盘告警还得人工介入。第三写清理脚本本身就是负担。按时间戳删文件容易误删正在使用的文件按进程名匹配又太脆弱。我印象最深的一次线上事故某个内部服务在启动时生成一个约 2 GB 的中间文件程序处理完本该删除但某次版本升级引入了fork子进程子进程把 fd 继承走了父进程退出时文件还被子进程抱着结果磁盘被写满整个节点直接不可用。所以问题的本质是我们需要的清理时机不是程序想起来的时候而是没有人再用这个文件的时候。这个没有人在用的判断只有内核最清楚因为只有内核掌握着文件描述符、引用计数和 dentry目录项的完整状态。1.2 内核视角下自动清理意味着什么传统文件的生命周期大致是open创建文件 - 目录项把文件名和 inode 关联起来 - 引用计数归零 - 删除数据。但无人使用这件事普通文件系统很难界定。即使进程关闭了 fd只要文件名还挂在目录树上文件就算活着。反过来如果文件名从目录树上消失了但还有进程持有打开的 fd文件数据依然存在——这就是我们常说的删不掉的文件。内核真正能做到自动清理关键在于把文件的可见性与它的使用者绑定。匿名文件的设计思路非常直接根本不让你创建一个能够被路径引用的名字。既然名字都不存在自然就不会有残留文件这个概念当最后一个持有 fd 的进程关闭或退出时内核立刻释放 inode 和数据块整个过程不需要注册任何清理回调。很多不熟悉内核对齐机制的读者可能觉得这只是个小技巧。实际上这是文件系统语义上的一次简化把必须有一个路径名这个传统假设打破让生命周期完全由引用计数接管。你可以理解为匿名文件是一块没有门牌号的内核托管空间门锁一关东西自动回收谁也不用惦记。2. 匿名文件的两条实现路径memfd_create 与 O_TMPFILE标题里写的是匿名文件一种可以实现由内核自动清理的文件但 Linux 里匿名的实现不止一种。目前主流就两套内存型的memfd_create和磁盘型的O_TMPFILE。两者解决的问题有重叠但细节差很多。2.1 memfd_create内存里的无路径文件memfd_create是内核 3.17 引入的系统调用作用是在内核的 tmpfs一种基于内存的临时文件系统中创建一个匿名文件返回一个文件描述符这个 fd 就是后续操作的全部凭据。先看一个最基础的使用示例#include sys/mman.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include string.h #include errno.h int main(void) { int fd memfd_create(hello, 0); if (fd 0) { perror(memfd_create); return 1; } const char *msg anonymous file content; write(fd, msg, strlen(msg)); // 读回来验证 lseek(fd, 0, SEEK_SET); char buf[128] {0}; read(fd, buf, strlen(msg)); printf(read back: %s\n, buf); close(fd); return 0; }注意memfd_create可以直接用但 glibc 在 2.27 之前没有封装需要自己syscall(__NR_memfd_create, ...)。头文件里需要定义MFD_CLOEXEC和MFD_ALLOW_SEALING这两个 flag具体值可以查/usr/include/linux/memfd.h。这个文件从创建那一刻起在文件系统目录树中是不存在的。你在/tmp、/home下都找不到它。如果打开/proc/self/fd/N你只能看到一行类似memfd:hello (deleted)的链接。括号里的 deleted 不是事故而是这类文件的常态——因为它本来就没有文件系统路径。写入的数据首先占用内存由 tmpfs 管理。当内存压力大的时候内核可以把它换到 swap 区但这不是程序能控制的也不应该依赖这一点。2.2 O_TMPFILE磁盘上的匿名临时文件如果说memfd_create是无名且在内存那O_TMPFILE就是无名但落在真实磁盘。它由内核 3.11 引入逻辑是在指定文件系统上创建临时文件但不分配路径名。调用方式也很简单int fd open(/var/tmp, O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR); if (fd 0) { perror(open with O_TMPFILE); return 1; }上面这段代码会在/var/tmp所在的文件系统中创建一个匿名临时文件返回一个可用于读写操作的 fd。这个文件在目录里不占名字对用户不可见。当最后一个 fd 关闭时文件数据自动释放。那么问题来了如果文件不在目录里后续想把它落成一个正式名字怎么办答案是linkat。内核提供了一个特殊操作可以把 O_TMPFILE 创建的匿名文件与一个路径关联起来相当于补办门牌号。char proc_path[64]; snprintf(proc_path, sizeof(proc_path), /proc/self/fd/%d, fd); linkat(AT_FDCWD, proc_path, AT_FDCWD, /var/tmp/final-name, AT_SYMLINK_FOLLOW);这样文件就从一个匿名临时文件变成一个正式文件之后生命周期就和普通文件完全一致了。这个特性特别适合先完整写数据、再原子性发布的场景你可以先在目标文件系统上生成一个不可见的文件把内容全部写好后再用linkat一次性挂到正式路径上对读取方完全无感还不会出现读到一半的空文件。2.3 两条路径的选型对比我把两种方式放在一起对比便于你做技术决策维度memfd_createO_TMPFILE底层存储tmpfs主要占内存可能换出到 swap所在块设备上的真实磁盘文件路径可见性不可见只有/proc/self/fd/N能找到创建期不可见linkat 后变成普通文件生命周期最后一个 fd 关闭即释放最后一个 fd 关闭即释放linkat 后由目录项持有适用场景共享内存、跨进程数据传递、缓存、代码加载大文件临时计算、原子发布、敏感临时落盘是否受文件系统类型限制必须内核支持 tmpfs必须文件系统支持 O_TMPFILEext4、xfs 等支持修改文件类型只能保持匿名可以转正为具名文件选型时我的经验是只要能放内存优先memfd_create。性能快、无磁盘写放大、不污染目录。只有当数据量大到内存扛不住或者后续明确要转正成正式文件时才考虑O_TMPFILE。3. 内核自动清理的背后文件引用计数与 dentry 回收很多资料讲匿名文件都是直接举 API 例子但如果你不去追一层内核到底做了什么遇到并发、fd 泄漏、内存暴涨这类问题会摸不着头脑。所以这一节我们把内核的数据结构拆开看。3.1 一个文件对象的内核生活在 Linux 内核中一个被打开的文件由多个层次组成struct file文件描述符在进程中的实体包含当前读写位置、引用标志等由文件描述符指向。struct dentry目录项负责把路径名和 inode 关联起来。普通文件至少有一个 dentry 挂在目录树上。struct inode文件元数据和数据块的位置记录文件大小、访问时间、权限等。当你open(/tmp/a.txt)时内核会找到或者创建对应 dentry并把它挂到/tmp目录的链表上。这时候如果别人执行ls /tmp/a.txt就能看到它。即使用unlink删除路径只要还有进程持有 fddentry 的引用就不会清零inode 仍然存在文件也仍然可读写等到所有 fd 关闭引用计数归零dentry 被回收inode 被释放。匿名文件的不同在于它从一开始就没有挂在目录树上的 dentry只有一个匿名 dentry 绑定 inode。memfd_create会创建一个 tmpfs 的 inode再创建 dentry但这个 dentry 不加入任何目录队列。O_TMPFILE也是类似逻辑只不过它依托的是目标文件系统而非 tmpfs。所以内核自动清理这个过程听上去神秘其实还是老一套引用计数归零 释放 inode。只是把必须挂在目录树上这一步省掉了生命周期完全由 fd 决定。3.2 为什么进程崩溃也清得干净这是匿名文件最吸引人的点。进程如果突然崩溃或被kill -9操作系统会统一回收它持有的所有 fd。fd 没了struct file引用减一如果这是最后一个引用那么对应 inode 的引用也归零内核立刻释放相关内存/磁盘块。这个过程发生在进程退出路径中和程序自己写没写清理代码完全无关。换句话说即使代码里有资源泄漏、没执行清理函数内核也不会让文件残留。不过这里有唯一的例外场景如果有其他进程通过SCM_RIGHTS之类的机制把 fd 传走了文件的生命周期会被延长到接收方关闭为止。讨论清理问题时必须把这个边界说清楚。它仍然是安全的因为还有人持有引用说明文件还未真正无人使用只是引用主体换了个进程。这不违背设计语义反而恰恰是共享文件 fd 的理想行为。从操作层面看你在容器里频繁看到/proc/*/fd/* (deleted)这样的条目就是某一类匿名或已 unlink 文件的正常表现。如果一个进程一直持有某个已删除文件的 fd磁盘空间可能无法回收这不是内核故障而是确实还有人在用。这块我们后面讲诊断方法时再展开。4. 匿名文件能做什么共享内存、防篡改缓存与敏感数据隔离原理讲完该上价值了。匿名文件不是用来炫技的它解决的是几个非常现实的工程问题。4.1 用 memfd_create 做跨进程共享进程间传数据经典方案是 socket、管道、共享内存、临时文件。管道和 socket 在数据量大时涉及多次拷贝临时文件需要处理路径和生命周期传统共享内存shm_open虽然也是 tmpfs但需要给区域起名字还可能被其他进程恶意打开。memfd_create提供了一个非常干净的方案父进程创建匿名文件把 fd 通过SOCK_SEQPACKET或SCM_RIGHTS传给子进程子进程直接对同一 fd 做读写。因为文件没有路径外部进程想猜到并打开它是不可能的安全性天然好于任何依赖路径的方案。我在某个内部数据管道里实际用过这个方案。主进程从网络收到一批结构化数据直接写入 memfd然后把 fd 发给多个 worker 进程每个 worker 只需要一次性mmap这块区域即可读取。整套流程下来既没有磁盘 IO也没有/tmp文件残留数据量大时性能表现也很稳定。有个细节要注意memfd_create的文件偏移(position)是共享的。多个进程通过同一个 fd 读写时要格外小心它们会相互影响。常用做法是让每个进程先lseek到自己需要的位置或者干脆用mmap映射到各自的虚拟地址空间避免共享偏移量的坑。4.2 seal 机制给缓存加一道锁memfd_create的进阶玩法是搭配 seal密封机制。创建时可以传入MFD_ALLOW_SEALING之后用fcntl给文件加各种 sealF_SEAL_SHRINK禁止缩小文件大小。F_SEAL_GROW禁止扩大文件大小。F_SEAL_WRITE禁止写入。F_SEAL_SEAL禁止再修改 seal 集合。加完 seal 之后的文件从某种意义上变成了只读不可变内存块非常适合做缓存和配置快照。比如某个服务启动时需要读取一份大型配置你用 memfd 加载并加F_SEAL_WRITE和F_SEAL_SEAL然后再把 fd 共享给多个线程或子进程。任何代码试图修改配置都会直接得到写入错误而不会产生静默的脏数据。这种不可变性是普通共享内存很难保证的。F_SEAL_WRITE有一个小陷阱它只影响通过 write 写入不影响 mmap 映射的可写映射。如果要保证绝对不可改写内存映射时也要用MAP_PRIVATE或者接受高权限攻击者修改的可能性。安全边界要考虑清楚不要把 seal 当成强制安全机制来用它能防的更多是程序 bug 造成的意外写。4.3 O_TMPFILE 的敏感数据场景另一类典型场景和处理敏感数据有关。比如你导入一批加密密钥需要先在磁盘上解出一个临时文件处理完再销毁。如果使用普通临时文件文件名可能被其他进程看到甚至日志、崩溃转储、文件系统监控工具都会留下痕迹。用O_TMPFILE创建匿名磁盘文件文件内容直到关闭前都没有路径路径泄露的风险就降到了最低。同样的逻辑也适用于原子发布的临时文件。过去我们经常把临时文件写到目标目录写好后再rename到正式名。但rename之前如果进程被 kill临时文件就会残留。用O_TMPFILE加linkat整个操作要么不出现名字要么一出现就是完整文件中间状态在目录树中不存在天然适合做原子更新。这套逻辑在配置管理、依赖包缓存、离线数据导入工具里都很好用。只要你能接受最终产物必须落名但中间过程不允许有路径痕迹O_TMPFILE就是最稳的姿势。4.4 其他值得注意的使用方式执行代码把 ELF 文件写入 memfd然后通过/proc/self/fd/N执行可以实现从内存直接执行很多打包器和沙箱在绕过路径限制时会用这个技巧但必须注意系统的 exec 权限策略。容器临时分区某些容器初始化时用 memfd 存放环境变量或启动配置进程退出后自动释放避免多个容器互相读错文件。压缩包解压解压工具可以先把文件落在匿名临时文件里校验完成后一次性linkat避免解压一半时目录里出现半个文件。场景本身很多核心思路只有一个凡是临时存在用完即走还不想留名的数据都可以考虑匿名文件。5. 实践细节与踩坑记录fd 继承、内存约束和容量控制这一节全是实操经验是我在自己项目里被真实问题逼出来的。很多人学匿名文件的 API 十分钟就能上手但生产环境里翻车往往都翻在下面这几个地方。5.1 CLOEXEC别把 fd 泄漏给子进程创建 memfd 时如果传入0那么这个 fd 默认不是 close-on-exec 的。也就是说如果程序里创建完 memfd 之后又fork了子进程并执行了外部程序这个 fd 会跟着被子进程继承。后果是什么父进程以为把 fd 关了就完事了但子进程还抱着这个 fd文件的生命周期被无限拉长内存和磁盘空间迟迟不能回收甚至子进程崩溃退出时才会释放。更麻烦的是这类问题很难定位因为从宿主机的视角看文件明明处于已删除状态但就是一直占着空间不消失。最省心的方法是创建时就带上MFD_CLOEXECint fd memfd_create(tmp, MFD_CLOEXEC);除非你确实需要跨界把 fd 传给子进程否则一律建议加这个标志。同理O_TMPFILE 的open可以加O_CLOEXEC。千万不要把继承 fd当成默认行为去依赖这几乎都会变成事故。5.2 内存占用不是免费的memfd_create的数据放在 tmpfstmpfs 的容量受挂载参数限制而且默认情况下它**计入进程内存占用RSS**吗这块理解容易出错。具体来说写入 tmpfs 的页会被计为进程的内存使用当系统内存紧张时页面可能会被回收或 swap 换出。因此如果你拿 memfd 当大容量缓存用必须先确认内存水位是否安全别写了几 GB 进去就把系统内存打爆。O_TMPFILE用的是磁盘空间不会直接吃内存但也不是完全无代价页缓存依然会被占用。读入、写出时依然有页缓存的开销只是压力远小于 memfd。因此选型时可以参考数据量 1 GB优先 memfd性能好。数据量在 1 GB ~ 几十 GB优先 O_TMPFILE避免内存紧张。数据量再大就要考虑是不是该用成熟的存储引擎了。这只是我个人的经验值具体还要看你系统内存总容量。别教条要盯着监控数据。5.3 /proc 下的玄机与调试方法匿名文件的调试和普通文件不太一样。它没有路径遇到问题第一反应往往不是去/tmp里翻而是要借助/proc。ls -l /proc/pid/fd/能看到进程打开了哪些 fd其中带(deleted)标记的一般就是匿名文件或已 unlink 的普通文件。这可以帮助你判断有哪些进程在占用匿名文件。cat /proc/meminfo | grep Shmemtmpfs 整体占用情况只要是 memfd 或共享内存都会体现在这里。如果 Shmem 数值很高但看不到明显用途就要小心某个组件在无限创建匿名文件。du -sh /dev/shm如果你的系统把/dev/shm挂成 tmpfs这也能快速确认占用趋势。我踩过最隐蔽的一个坑是某个服务每次处理请求都创建一个 memfd但忘记 close。由于 fd 数量增长到很大的量级系统报出了 too many open files。而这类 fd 在/proc/pid/fd里看起来全都正常只是数量惊人。排查方式也很简单数一下/proc/pid/fd下有多少条memfd:的软链就能确认是不是 memfd 泄漏。5.4 容量控制别被自己的匿名文件撑死在使用 memfd 比较频繁的系统里一定要检查 tmpfs 挂载大小。很多发行版/dev/shm默认只有内存的一半。如果你的程序创建大量匿名文件且不加以控制很快会写满 tmpfs返回ENOSPC。从工程角度建议给 memfd 建立单文件上限与总内存上限的双重限制一旦超限立即报错或走磁盘落盘降级路径。同理O_TMPFILE 所在文件系统也要预留足够空间否则匿名文件会打满整个分区影响同盘其他业务。如果你想要一个更灵活的控制方案可以考虑把 memfd 文件的ftruncate控制在合理范围尽量复用同一块匿名文件而不是反复创建新 fd。在实时性要求高的场景重复创建和销毁 tmpfs inode 本身也有开销频繁操作会显著增加内核路径消耗。5.5 和 fd 传递配合使用时的注意事项如果你准备把 memfd 或 O_TMPFILE 的 fd 传给另一个进程走 Unix domain socket 时最常用SCM_RIGHTS。此时要记住两点第一接收方拿到的 fd 不是一个新的匿名文件概念它就是同一个文件的内核引用。接收方关闭 fd 后文件引用减一但原文件是否真的释放还得看发发方和其他副本是否都关了。第二发送之前先sendmsg发送之后不要立刻 close。如果你发送完马上就close而内核还没把 fd 真正发送到对端文件可能被提前回收造成对端读到的 fd 无效。虽然这个时序在内核中一般被处理得很好但在高并发、大量 fd 一次传递时我会在发送端保留 fd 直到sendmsg返回成功后再关稳一点总没错。如果对端需要长期持有发送端更不该提前 close等收到对端确认后再清理。6. 我实际使用后的一些感受把匿名文件真正用进生产环境后我最大的体会是它最值钱的不是省了那几行删除代码而是让生命周期这个本来散落在程序员脑海里的隐式规则重新回到了内核手里。以前我们靠各种 lock 文件、pid 文件、定时清理任务去逼近进程崩溃也不残留的目标现在只需要开一个 fd、关一个 fd其余交给引用计数去自然演化。如果你在项目里也遇到临时文件堆成山路径泄露隐患共享内存命名冲突这类问题我建议直接拿 memfd_create 写一个小 Demo 跑一遍再决定要不要引入到核心链路。这里我也建议先把MFD_CLOEXEC当成默认习惯把内存和磁盘容量做上监控再配合/proc/pid/fd的排查套路就不会被常见的坑绊倒。最后分享一个小技巧如果你想验证某个程序是否存在 memfd 泄漏可以连续执行多次请求然后观察cat /proc/meminfo | grep Shmem的变化量。如果它在每次操作后持续增长且不回落那基本就是匿名文件没有正确关闭——这个排查方法比翻代码快得多而且是所有基于 tmpfs 的对象都通用的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑