暗色模式

Linux 内存文件系统 tmpfs 与 ramfs:容量上限、内存账本与常见坑

技术教程
2026-10-01
11
0
本文要点
  • tmpfs 是拿内存当文件系统:df 显示的容量就是它的上限,size= 是硬墙。实测往 size=16M 的 tmpfs 里写 32M,dd 报 No space left on device、退出码 1,只写进去 16M,df 显示 100%
  • tmpfs 占的内存记在 free 的 shared 列(实测写 16M 后 shared 从 0 变成 16),也记在 /proc/meminfo 的 Shmem 里。Shmem 虽然算在 Cached 那堆里,但清缓存清不掉它:实测 drop_caches 前后 Shmem 都是 66536 kB 一分未动,而 Cached 从 1.5G 掉到 190M
  • tmpfs 可以不停机扩容:mount -o remount,size=48M 不需要卸载,文件还在(扩前扩后 md5sum 一致);反过来缩到比已用量还小会直接失败(实测返回码 32),文件系统保持原样
  • ramfs 是「没有任何账本」的兄弟:findmnt 和 df 都报容量 0,写进 8M 之后还是报 0;它占的内存落在 buff/cache 而不是 shared——和磁盘页缓存混在一起,从账面上看不出是它,而且完全没有上限
  • 机器上本来就有好几处 tmpfs(/dev/shm 默认是内存的一半、/run、/run/lock)。内存账本怎么看、available 为什么不能只看 free,见 Linux 内存排查:从 free 到 /proc/meminfo 与 OOM Killer 定位

Linux 内存文件系统 tmpfs 与 ramfs:容量上限、内存账本与常见坑

/tmp 挂 tmpfs、容器里的 /dev/shm、编译时的中间目录、放密钥的临时空间——这些做法的共同点是:文件不落盘,直接住在内存里。好处很直接(快、重启即失),代价却不那么直观:

  • df -h 显示 /dev/shm 有 989M,这个数字是从哪来的?
  • 往 tmpfs 里写文件,占用记在 free 的哪一列?为什么 buff/cache 涨了、shared 却没动?
  • 磁盘写满了会报 ENOSPC,tmpfs 写满了报什么?会不会把整台机器拖死?
  • tmpfs 和 ramfs 差在哪,为什么 df 对 ramfs 报的容量是 0?

本文在一台 1 核 1.9G、没有 swap 的 Ubuntu 22.04 演示机(内核 5.15.0-30-generic)上,把这几件事逐个实测一遍。所有操作都在 /tmp 下用临时挂载点做,演示完即卸载,磁盘和内存都不留残留。

机器上本来就有好几处 tmpfs

不用自己创建,先看看系统已经在用的:

findmnt -t tmpfs -o TARGET,SIZE,USED,AVAIL,OPTIONS
echo "--- 物理内存与 swap ---"
free -m | head -2
cat /proc/swaps
echo "--- /dev/shm 的默认大小 ---"
df -h /dev/shm | tail -1

findmnt 列出的 tmpfs 挂载点:/dev/shm 988.8M、/run 197.8M、/run/lock 5M,以及 free -m 显示 total 1977MB、shared 为 0,/proc/swaps 只有表头(没有 swap)

这张图里有四个值得注意的地方:

① /dev/shm 的 988.8M 是内存的一半。 这台机器 free -m 显示 total 1977MB,而 /dev/shm 的容量正好是它的一半左右。这是 tmpfs 的默认规则:不写 size= 时,容量取物理内存的 50%。所以同一份 fstab 配置,在 2G 的机器和 64G 的机器上行为完全不同——这也是「小机器上往 /dev/shm 写大文件突然失败」的根源。

② /run、/run/lock、/run/user/0 都是 tmpfs。 系统自己就在用:/run/lock 只有 5M(锁文件本来就该很小),/run/user/0 是每个登录用户的运行时目录。注意它的挂载选项里有 nr_inodes=50627——tmpfs 除了限制空间,还能限制 inode 数量,空间没满但 inode 用尽时同样会报 ENOSPC。

③ 这些 tmpfs 目前一个字节都没占(USED 全是 0 或 4K),free 里的 shared 也是 0。 tmpfs 是惰性的:挂载本身不预留任何内存,只有真的写进文件才扣内存。所以「机器上挂了一堆 tmpfs」本身不需要紧张,紧张的是往里写了多少。

④ /proc/swaps 只有一行表头——这台机器没有 swap。 这一点很关键:tmpfs 的内容属于 shmem,在有 swap 的机器上还能被换出到磁盘(代价是之后访问它要再读回来,表现为变慢);而在这台机器上无处可换,写满 tmpfs 就是直接吃掉物理内存,压力全部落在物理内存和 OOM Killer 上。演示机这种 1.9G 无 swap 的配置,/dev/shm 里放一个 900M 的文件就足以把系统送走。判断方法就一行:cat /proc/swaps 有没有内容。

size= 是硬墙:写超了会怎样

自己建一个 16M 的 tmpfs,然后故意往里写 32M:

mkdir -p /tmp/tpdemo
mount -t tmpfs -o size=16M tmpfs /tmp/tpdemo
findmnt -no TARGET,SIZE,OPTIONS /tmp/tpdemo
echo "--- 往 16M 的盘里写 32M ---"
dd if=/dev/zero of=/tmp/tpdemo/a.bin bs=1M count=32 2>&1; echo "dd 退出码=$?"
df -h /tmp/tpdemo | tail -1
echo "--- 这 16M 在内存账本上记在哪 ---"
free -m | head -2
echo "--- 在线扩容到 48M,文件还在吗 ---"
md5sum /tmp/tpdemo/a.bin
mount -o remount,size=48M /tmp/tpdemo && echo "扩容返回码=0"
df -h /tmp/tpdemo | tail -1
md5sum /tmp/tpdemo/a.bin
umount /tmp/tpdemo && rmdir /tmp/tpdemo && echo "演示挂载点已清理"

16M tmpfs 写 32M 实测:dd 报 No space left on device、退出码 1,只写进 16M,df 显示 100%;free 的 shared 列从 0 变成 16;在线扩容到 48M 后 md5sum 不变

输出里的几个细节:

dd: error writing '/tmp/tpdemo/a.bin': No space left on device
17+0 records in
16+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 0.0166534 s, 1.0 GB/s
dd 退出码=1
tmpfs            16M   16M     0 100% /tmp/tpdemo
  • size=16M 就是硬墙,和磁盘写满一样报 No space left on device(ENOSPC)。tmpfs 不会「超卖」:它知道自己的额度,写超的部分直接拒绝;
  • 16+0 records out 说明 dd 写了一半就被打断,留下一个 16M 的残缺文件。所以脚本里往 tmpfs 写东西,必须检查退出码——dd 退出码=1 才是判断依据,光看日志很容易漏;
  • df 显示 100%,USED 是 16M、AVAIL 是 0。这个 USED 不是磁盘占用,而是已经吃掉的内存;
  • free -m 的 shared 列从 0 变成了 16——这就是下一节要说的账本。

顺带一提速度:tmpfs 的强项确实是快,但差距没有想象中那么夸张。同一份 64MiB 数据,都带 conv=fsync 强制落盘:

mkdir -p /tmp/spd /tmp/disk && mount -t tmpfs -o size=64M tmpfs /tmp/spd
( time dd if=/dev/zero of=/tmp/spd/f bs=1M count=64 conv=fsync ) 2>&1 | tail -3
( time dd if=/dev/zero of=/tmp/disk/f bs=1M count=64 conv=fsync ) 2>&1 | tail -3

实测 tmpfs 是 real 0m0.102s(其中 sys 0m0.101s,全是内存拷贝),磁盘目录是 real 0m0.262s(sys 0m0.149s)——大约 2.6 倍。换算成吞吐是 640MB/s 对 250MB/s,对绝大多数应用来说,这点差距远不如「读写模式」重要。tmpfs 真正的价值不在快,而在「文件即内存」带来的语义:重启即失、天然不落盘、进程间通过文件共享内存。

这 16M 记在谁头上:shared 与 Shmem

上一节的 free -m 里,shared 从 0 变成 16——tmpfs 占的内存在 free 里是记在 shared 列的,不会记到 used(那是进程自己的 RSS),也不会记到 buff/cache(那是磁盘页缓存)。这个区分很有用:

  • used 涨 → 找进程(ps aux --sort=-rss);
  • shared 涨 → 找 tmpfs / 共享内存(df -h | grep tmpfs、ipcs -m);
  • buff/cache 涨 → 多半是文件读写留下的页缓存,属于「可以回收」的那部分。

更细的账本在 /proc/meminfo 的 Shmem 里。把 tmpfs 里放一个 64M 文件,然后清一次页缓存看看会发生什么:

grep -E "^(MemFree|MemAvailable|Cached|Shmem|Buffers):" /proc/meminfo
sync; echo 3 > /proc/sys/vm/drop_caches
grep -E "^(MemFree|MemAvailable|Cached|Shmem|Buffers):" /proc/meminfo
md5sum /tmp/spd/f

实测结果:

# drop_caches 之前
MemFree:           85124 kB
MemAvailable:    1601240 kB
Buffers:           93392 kB
Cached:          1520604 kB
Shmem:             66536 kB

# sync + drop_caches 之后
MemFree:         1633076 kB
MemAvailable:    1636788 kB
Buffers:            1344 kB
Cached:           194648 kB
Shmem:             66536 kB

Cached 从 1.5G 掉到 190M(页缓存确实被放掉了),Shmem 却一动不动——还是 66536 kB,而且 tmpfs 里那个文件的 md5sum 照样能算出来(数据完好)。

原因很简单:Shmem 属于 Cached 这个大类,但它的背后是真实的、有主的内存页(tmpfs 文件、共享内存段),不是「磁盘内容的副本」。清缓存能丢掉「副本」(丢了再读一次磁盘就行),丢不掉「原件」——丢掉原件就等于丢数据。所以:

  • drop_caches 不是「内存不够时的救命稻草」,它只回收磁盘页缓存,对 tmpfs / 共享内存完全无效;
  • 反过来也一样:看到 Cached 很高、以为「随时能回收」是有条件的——里面有多少是 Shmem,就是多少收不回来的部分;
  • 磁盘页缓存与 drop_caches 的完整实测(以及 available 为什么纹丝不动)见 Linux 内存排查:从 free 到 /proc/meminfo 与 OOM Killer 定位,那篇讲的是「能回收的那部分」,本文这一节讲的是收不回来的那部分。

在线扩容:不用卸载、数据不丢

tmpfs 的 size 是挂载参数,但不需要卸载再挂就能改——mount -o remount 是原子的:

mount -o remount,size=48M /tmp/tpdemo && echo "扩容返回码=0"
df -h /tmp/tpdemo | tail -1
md5sum /tmp/tpdemo/a.bin
扩容返回码=0
tmpfs            48M   16M    32M  34% /tmp/tpdemo
2c7ab85a893283e98c931e9511add182  /tmp/tpdemo/a.bin

三点结论:

  • 扩容不需要卸载:容器、/dev/shm、编译目录里的进程全程感知不到,业务不中断;
  • 数据不丢:扩前扩后 md5sum 完全一致(2c7ab85a…),因为改的只是额度,页面本来就在内存里;
  • 扩容是「额度」变化,不是「重新分配」:df 里 USED 仍是 16M,只是 AVAIL 从 0 变成了 32M。

但反过来缩小是另一回事。 对一个已经 100% 占满的 tmpfs 执行 mount -o remount,size=8M,实测直接失败:

mount: /tmp/spd: mount point not mounted or bad option.
缩小 remount 返回码=32
tmpfs            64M   64M     0 100% /tmp/spd

返回码 32,文件系统保持原样(还是 64M、还是 100%)——内核不会为了满足新额度去丢数据。这条规则很有用:/dev/shm 撑大之后想改回来,得先确认里面的文件已经被删干净,否则改不动。目标额度的报错信息是 mount point not mounted or bad option,看起来像「挂载点不存在」,实际原因是「缩不下去」,别被这句话带偏。

ramfs:连账本都没有的兄弟

ramfs 比 tmpfs 更老、更简单:它就没有「容量」这个概念,也不参与任何内存统计。同样做一遍挂载、写入、再看账面:

mkdir -p /tmp/rfdemo
mount -t ramfs ramfs /tmp/rfdemo
echo "--- ramfs 报出来的容量 ---"
findmnt -no TARGET,FSTYPE,SIZE,USED /tmp/rfdemo
df -h /tmp/rfdemo | tail -1
free -m | head -2
echo "--- 往里面写 8M ---"
dd if=/dev/zero of=/tmp/rfdemo/r.bin bs=1M count=8 2>&1 | tail -1
ls -l /tmp/rfdemo/r.bin
echo "--- 再报一次容量,以及内存的变化 ---"
findmnt -no TARGET,FSTYPE,SIZE,USED /tmp/rfdemo
free -m | head -2
umount /tmp/rfdemo && rmdir /tmp/rfdemo && echo "ramfs 演示挂载点已清理"

ramfs 实测:findmnt 报 SIZE 0 / USED 0,df 报 0 0 0;写进 8M 文件后再查仍然报 0,而 free 的 buff/cache 从 1608 涨到 1616、shared 保持 0

对比着看输出:

/tmp/rfdemo ramfs     0    0
none               0     0     0    - /tmp/rfdemo
-rw-r--r-- 1 root root 8388608 Sep 30 21:12 /tmp/rfdemo/r.bin
  • findmnt 报 SIZE 0、df 报 0 0 0:不是「没有空间」,而是「不统计」。文件照样写得进去(8388608 字节,8M);
  • 写完之后再查,还是 0。所以 ramfs 上没有任何容量配额:它能一直吃内存,直到把机器吃垮(在没有 swap 的机器上就是 OOM Killer 登场);
  • 它占的内存落在 buff/cache 而不是 shared:写 8M 前后对比,free 的 shared 保持 0,而 buff/cache 从 1608 涨到 1616、free 从 171 掉到 165。ramfs 的页面直接进了页缓存账本,从 free 的视角看,它和「读了几个磁盘文件」长得一模一样——这是排查时最容易漏掉的一类内存占用。

两者的取舍很清楚:

tmpfsramfs
容量上限有(size=,默认内存的 50%)没有
df / findmnt 报容量报报 0
内存记在哪shared / Shmembuff/cache(和磁盘缓存混在一起)
写满之后ENOSPC,安全失败继续吃内存,直到 OOM
内存吃紧时有 swap 时可换出到磁盘(表现为变慢)不能换出,只能删文件释放
典型用途/dev/shm、/run、/tmp内核 initramfs(早期启动阶段)

日常使用选 tmpfs:它至少知道自己的边界,写超了只是那一次写失败,而不是把整台机器拖下水。ramfs 基本只出现在内核自己的场景里(initramfs 阶段用它做临时根文件系统),用户态几乎没有任何理由主动挂 ramfs——「没有上限」听起来自由,实际是「没有刹车」。

什么时候该用、什么时候会出事

适合用 tmpfs 的场景:

  • 进程间共享数据(/dev/shm):比管道灵活、比临时文件快,进程退出即释放;
  • 敏感中间产物:私钥、解密后的数据放在 tmpfs,机器一重启就没了,不用记得擦;
  • 高频临时读写:编译中间产物、socket 文件、会话缓存;
  • 容器//tmp:很多发行版默认把 /tmp 挂成 tmpfs,好处是重启自动清空。

必须警惕的坑:

  1. 没有 swap 的机器上,tmpfs 写满就是 OOM。先看 cat /proc/swaps 确认有没有 swap,再看 df -h 数一数所有 tmpfs 的 size 之和——那才是这台机器「最坏情况下会被 tmpfs 吃掉多少内存」;
  2. /dev/shm 默认是内存的 50%,容器里通常被压到 64M。做数据处理、机器学习、多进程共享数组时,写 /dev/shm 之前先 df -h /dev/shm 看一眼额度,否则会拿到一个莫名其妙的 ENOSPC;
  3. 监控要盯 shared / Shmem,而不是磁盘。tmpfs 的占用不会体现在任何磁盘分区的 df 上(那个 df 显示的 USED 是内存),磁盘监控全绿、内存却悄悄被吃光,是很典型的场景;
  4. du 看不见「已删除但仍被打开」的文件占的空间。tmpfs 上删了文件但如果还有进程开着它,内存不会立刻还回来——排查时要看 lsof +L1,而不是只看 du(用法见 lsof 打开文件排查);
  5. ramfs 不要用在用户态。理由如上:没有上限、没有账本、写爆了才发现。

小结

把 tmpfs 和 ramfs 放回内存子系统的坐标系里,三句话就能记住:

  1. tmpfs 是一个「知道自己有多大」的内存文件系统:size=(默认内存 50%)是硬额度,超出报 ENOSPC 并留下残缺文件;可以 remount 在线扩容(数据不丢),但缩不到已用量以下;
  2. 它占的内存记在 shared / Shmem:Shmem 属于 Cached 大类却清不掉(drop_caches 前后实测一分未动),所以「Cached 高就等于可回收」这个直觉对 tmpfs 不成立;
  3. ramfs 是它的无账本版本:df 报 0、没有上限、内存混进 buff/cache,用户态应该避开。

排查顺序也很固定:先 free -m 看哪一列在涨(used → 进程,shared → tmpfs,buff/cache → 页缓存),再 df -h | grep tmpfs 找是哪个挂载点,最后 du -sh 定位到具体文件。 内存整体排查(available 怎么算、OOM Killer 怎么挑人)见 Linux 内存排查:从 free 到 /proc/meminfo 与 OOM Killer 定位;进程间共享内存的用法(SysV 共享内存段与 /dev/shm)见 Linux 进程间通信:FIFO、共享内存与信号量;如果内存真的紧张到要压缩换出,那就是 Linux zram:压缩内存当 swap 用 的地盘了。

本文全部演示在一台 1 核 1.9G、无 swap 的 Ubuntu 22.04(内核 5.15.0-30-generic)上完成,零安装(findmnt、free、dd、md5sum 全是系统自带);演示挂载点已全部卸载并删除,/tmp 下无残留,机器上原有的 tmpfs 未做任何改动。

发表评论

暂无评论,快来抢沙发吧!