Linux 磁盘缓存实验与趣玩

希望你现在已经相信 Linux 并没有吃掉你的内存。下面是一些有趣的实验,帮助你了解磁盘缓存的工作原理。

注意:以下示例基于 2009 年左右的硬件环境,没有 SSD。你可能需要把文中的数字乘以 10 才能看到明显的效果。

磁盘缓存对应用程序内存分配的影响

既然我已经承诺过磁盘缓存不会阻止应用程序获取它们想要的内存,那就从这个实验开始吧。下面是一个 C 程序(munch.c),它会尽可能多地吞掉内存,或者吞到指定的上限为止:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv) {
    int max = -1;
    int mb = 0;
    char* buffer;

    if (argc > 1)
        max = atoi(argv[1]);

    while ((buffer = malloc(1024 * 1024)) != NULL && mb != max) {
        memset(buffer, 0, 1024 * 1024);
        mb++;
        printf("Allocated %d MB\n", mb);
    }

    return 0;
}

内存耗尽可不是什么好玩的事,不过 OOM killer 应该只会干掉这个进程,其他进程大概率不受影响。做这个实验之前,我们得先把 swap 关掉,不然程序连 swap 也会一起吞掉。

$ sudo swapoff -a

$ free -m

(注意,你的 free 输出可能不一样,可能显示的是 ‘available’ 列而不是 ‘-/+’ 行)

              total        used        free      shared  buff/cache   available
Mem:           1504         636          13           0         855      792
Swap:             0           0           0
$ gcc munch.c -o munch

$ ./munch
Allocated 1 MB
Allocated 2 MB
(...)
Allocated 877 MB
Allocated 878 MB
Allocated 879 MB
Killed

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         581         922           0           1      922
Swap:             0           0           0

虽然 “free” 显示只剩 14MB,但这并没有阻止应用程序吞掉 879MB 内存。之后缓存几乎被清空了2,但随着文件的读写,它会逐渐重新填满。不妨亲自试一试。

磁盘缓存对 swap 的影响

我还说过磁盘缓存不会导致应用程序去用 swap。我们同样用上一个实验里的 munch 程序来验证。这次我们打开 swap,并限制它只吞几百 MB:

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         636          13           0         855      792
Swap:          2047           6        2041

$ ./munch 400
Allocated 1 MB
Allocated 2 MB
(...)
Allocated 399 MB
Allocated 400 MB

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         636         413           0         455      792
Swap:          2047           6        2041

munch 吞掉了 400MB 内存,这些内存是从磁盘缓存中拿过来的,完全没动 swap。同样地,我们重新把磁盘缓存填满,它也不会去碰 swap。如果你在一个终端里跑 watch free -m,另一个终端里跑 find . -type f -exec cat {} + > /dev/null,就能看到 “cached” 一栏在涨、“free” 一栏在降。过一会儿涨幅会趋于平缓,但 swap 自始至终纹丝不动1

清空磁盘缓存

做实验的时候,能随时清空磁盘缓存会方便很多。我们可以用特殊文件 /proc/sys/vm/drop_caches 来实现。写入 3 就能清掉大部分磁盘缓存:

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         636          13           0         855      792
Swap:          2047           6        2041

$ echo 3 | sudo tee /proc/sys/vm/drop_caches
3

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         636         734           0         134      792
Swap:          2047           6        2041

可以看到 “buffers” 和 “cached” 降了,free 内存涨了,而 free+buffers/cache 保持不变。

磁盘缓存对程序加载速度的影响

我们来写两个测试程序,一个用 Python,一个用 Java。Python 和 Java 都有相当大的运行时库,运行程序之前必须先加载它们。这正是磁盘缓存大显身手的绝佳场景。

$ cat hello.py
print "Hello World! Love, Python"

$ cat Hello.java
class Hello {
    public static void main(String[] args) throws Exception {
        System.out.println("Hello World! Regards, Java");
    }
}

$ javac Hello.java

$ python hello.py
Hello World! Love, Python

$ java Hello
Hello World! Regards, Java

我们的 hello world 程序都能正常运行。现在清空磁盘缓存,看看要花多久才能跑起来。

$ echo 3 | sudo tee /proc/sys/vm/drop_caches
3

$ time python hello.py
Hello World! Love, Python

real    0m1.026s
user    0m0.020s
sys     0m0.020s

$ time java Hello
Hello World! Regards, Java

real    0m2.174s
user    0m0.100s
sys     0m0.056s

哇哦。Python 花了 1 秒,Java 花了 2 秒?就为了打个招呼,这也太慢了吧。不过现在运行它们所需的所有文件都已经在磁盘缓存里了,可以直接从内存读取。再来一次:

$ time python hello.py
Hello World! Love, Python

real    0m0.022s
user    0m0.016s
sys     0m0.008s

$ time java Hello
Hello World! Regards, Java

real    0m0.139s
user    0m0.060s
sys     0m0.028s

漂亮!Python 现在只要 22 毫秒,Java 用了 139 毫秒。分别快了 45 倍和 15 倍!你所有的应用都能自动享受到这种加速!

磁盘缓存对文件读取的影响

我们来创建一个超大文件,看看磁盘缓存对读取速度有什么影响。我这儿创建一个 200MB 的文件,如果你空闲内存比较少可以调小一点。

$ echo 3 | sudo tee /proc/sys/vm/drop_caches
3

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         465         954           0          85      960
Swap:          2047           6        2041

$ dd if=/dev/zero of=bigfile bs=1M count=200
200+0 records in
200+0 records out
209715200 bytes (210 MB) copied, 6.66191 s, 31.5 MB/s

$ ls -lh bigfile
-rw-r--r-- 1 vidar vidar 200M 2009-04-25 12:30 bigfile

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           1504         465         754           0          285      960
Swap:          2047           6        2041

由于文件刚刚写入,它会进入磁盘缓存。这个 200MB 的文件让 “cached” 涨了 200MB。现在来读它,然后清空缓存,再读一次,看看速度差多少:

$ time cat bigfile > /dev/null

real    0m0.139s
user    0m0.008s
sys     0m0.128s

$ echo 3 | sudo tee /proc/sys/vm/drop_caches
3

$ time cat bigfile > /dev/null

real    0m8.688s
user    0m0.020s
sys     0m0.336s

快了五十多倍!

总结

Linux 磁盘缓存非常低调。它利用闲置内存大幅提升磁盘访问速度,却从不抢占应用程序的内存。Linux 上内存被用满,是硬件被高效利用的体现,绝不是危险信号。

LinuxAteMyRam.com 由 VidarHolen.net 呈现。本网站已在 GitHub 上开源,欢迎提出建议或提交 PR。


本页内容做了一些简化:

  1. 虽然新申请的内存总归会从磁盘缓存中拿而不会动 swap(不过见第 2 点),但 Linux 可以配置为在后台抢先 swap 掉其他不用的应用程序,以腾出内存给缓存。这可以通过 /proc/sys/vm/swappiness 中的 swappiness 参数来调整。

    服务器可能倾向于 swap 掉不用的程序来加速正在运行程序的磁盘访问(让系统更快),而桌面系统可能更希望把程序留在内存里,避免用户切回来时卡顿(让系统响应更灵敏)。这个问题至今争议不断。

  2. 缓存中有一些部分是无法释放的,即便要给新应用程序腾地方也不行。这包括被某些应用程序 mlock 住的 mmap 页面、尚未写入存储的脏页,以及存储在 tmpfs 中的数据(包括用于共享内存的 /dev/shm)。mmap 且被 mlock 的页面会一直留在 page cache 中。脏页大部分情况下会很快被写出。tmpfs 中的数据如果可能的话会被 swap 出去。