Linux 記憶體中的 Cache 真的能被回收麼?
在 Linux 系統中,我們經常用 free 命令來檢視系統記憶體的使用狀態。在一個 RHEL6 的系統上,free 命令的顯示內容大概是這樣一個狀態:
[root@tencent64 ~]# free
total used free shared buffers cached
Mem: 132256952 72571772 59685180 0 1762632 53034704
-/+ buffers/cache: 17774436 114482516
Swap: 2101192 508 2100684
這裡的預設顯示單位是 kb,我的伺服器是 128G 記憶體,所以數字顯得比較大。這個命令幾乎是每一個使用過 Linux 的人必會的命令,但越是這樣的命令,似乎真正明白的人越少(我是說比例越少)。一般情況下,對此命令輸出的理解可以分這幾個層次:
- 不瞭解。這樣的人的第一反應是:天啊,記憶體用了好多,70個多 G,可是我幾乎沒有執行什麼大程式啊?為什麼會這樣? Linux 好佔記憶體!
- 自以為很瞭解。這樣的人一般評估過會說:嗯,根據我專業的眼光看的出來,記憶體才用了 17G 左右,還有很多剩餘記憶體可用。buffers/cache 佔用的較多,說明系統中有程序曾經讀寫過檔案,但是不要緊,這部分記憶體是當空閒來用的。
- 真的很瞭解。這種人的反應反而讓人感覺最不懂 Linux,他們的反應是:free 顯示的是這樣,好吧我知道了。神馬?你問我這些記憶體夠不夠,我當然不知道啦!我特麼怎麼知道你程式怎麼寫的?
根據目前網路上技術文件的內容,我相信絕大多數了解一點 Linux 的人應該處在第二種層次。大家普遍認為,buffers 和 cached 所佔用的記憶體空間是可以在記憶體壓力較大的時候被釋放當做空閒空間用的。但真的是這樣麼?在論證這個題目之前,我們先簡要介紹一下 buffers 和 cached 是什麼意思:
什麼是 buffer/cache?
buffer 和 cache 是兩個在計算機技術中被用濫的名詞,放在不同語境下會有不同的意義。在 Linux 的記憶體管理中,這裡的buffer 指 Linux 記憶體的:Buffer cache。這裡的 cache 指 Linux 記憶體中的:Page cache。翻譯成中文可以叫做緩衝區快取和頁面快取。在歷史上,它們一個(buffer)被用來當成對 io 裝置寫的快取,而另一個(cache)被用來當作對 io 裝置的讀快取,這裡的 io 裝置,主要指的是塊裝置檔案和檔案系統上的普通檔案。但是現在,它們的意義已經不一樣了。在當前的核心中,page cache 顧名思義就是針對記憶體頁的快取,說白了就是,如果有記憶體是以 page 進行分配管理的,都可以使用 page cache 作為其快取來管理使用。當然,不是所有的記憶體都是以頁進行管理的,也有很多是針對塊進行管理的,這部分記憶體使用如果要用到 cache 功能,則都集中到 buffer cache 中來使用。(從這個角度出發,是不是 buffer cache 改名叫做 block cache 更好?)然而,也不是所有塊都有固定長度,系統上塊的長度主要是根據所使用的塊裝置決定的,而頁長度在 X86 上無論是32位還是64位都是 4k。
明白了這兩套快取系統的區別,就可以理解它們究竟都可以用來做什麼了。
什麼是 page cache
Page cache 主要用來作為檔案系統上的檔案資料的快取來用,尤其是針對當程序對檔案有 read/write 操作的時候。如果你仔細想想的話,作為可以對映檔案到記憶體的系統呼叫:mmap 是不是很自然的也應該用到 page cache?在當前的系統實現裡, page cache 也被作為其它檔案型別的快取裝置來用,所以事實上 page cache 也負責了大部分的塊裝置檔案的快取工作。
什麼是 buffer cache
Buffer cache 則主要是設計用來在系統對塊裝置進行讀寫的時候,對塊進行資料快取的系統來使用。這意味著某些對塊的操作會使用 buffer cache 進行快取,比如我們在格式化檔案系統的時候。一般情況下兩個快取系統是一起配合使用的,比如當我們對一個檔案進行寫操作的時候,page cache 的內容會被改變,而 buffer cache 則可以用來將 page 標記為不同的緩衝區,並記錄是哪一個緩衝區被修改了。這樣,核心在後續執行髒資料的回寫時,就不用將整個 page 寫回,而只需要寫回修改的部分即可。
如何回收 cache?
Linux 核心會在記憶體將要耗盡的時候,觸發記憶體回收的工作,以便釋放出記憶體給急需記憶體的程序使用。一般情況下,這個操作中主要的記憶體釋放都來自於對 buffer/cache 的釋放。尤其是被使用更多的 cache 空間。既然它主要用來做快取,只是在記憶體夠用的時候加快程序對檔案的讀寫速度,那麼在記憶體壓力較大的情況下,當然有必要清空釋放 cache,作為 free 空間分給相關程序使用。所以一般情況下,我們認為 buffer/cache 空間可以被釋放,這個理解是正確的。
但是這種清快取的工作也並不是沒有成本。理解 cache 是幹什麼的就可以明白清快取必須保證 cache 中的資料跟對應檔案中的資料一致,才能對 cache 進行釋放。所以伴隨著 cache 清除的行為的,一般都是系統 IO 飆高。因為核心要對比 cache 中的資料和對應硬碟檔案上的資料是否一致,如果不一致需要寫回,之後才能回收。
在系統中除了記憶體將被耗盡的時候可以清快取以外,我們還可以使用下面這個檔案來人工觸發快取清除的操作:
[root@tencent64 ~]# cat /proc/sys/vm/drop_caches
1
方法是:
echo 1 > /proc/sys/vm/drop_caches
當然,這個檔案可以設定的值分別為1、2、3。它們所表示的含義為:
echo 1 > /proc/sys/vm/drop_caches:表示清除 page cache。
echo 2 > /proc/sys/vm/drop_caches:表示清除回收 slab 分配器中的物件(包括目錄項快取和 inode 快取)。slab 分配器是核心中管理記憶體的一種機制,其中很多快取資料實現都是用的 page cache。
echo 3 > /proc/sys/vm/drop_caches:表示清除 page cache 和 slab 分配器中的快取物件。
cache都能被回收麼?
我們分析了 cache 能被回收的情況,那麼有沒有不能被回收的 cache 呢?當然有。我們先來看第一種情況:
tmpfs
大家知道 Linux 提供一種“臨時”檔案系統叫做 tmpfs,它可以將記憶體的一部分空間拿來當做檔案系統使用,使記憶體空間可以當做目錄檔案來用。現在絕大多數 Linux 系統都有一個叫做 /dev/shm 的 tmpfs 目錄,就是這樣一種存在。當然,我們也可以手工建立一個自己的 tmpfs,方法如下:
[root@tencent64 ~]# mkdir /tmp/tmpfs
[root@tencent64 ~]# mount -t tmpfs -o size=20G none /tmp/tmpfs/
[root@tencent64 ~]# df
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda1 10325000 3529604 6270916 37% /
/dev/sda3 20646064 9595940 10001360 49% /usr/local
/dev/mapper/vg-data 103212320 26244284 71725156 27% /data
tmpfs 66128476 14709004 51419472 23% /dev/shm
none 20971520 0 20971520 0% /tmp/tmpfs
於是我們就建立了一個新的 tmpfs,空間是 20G,我們可以在 /tmp/tmpfs 中建立一個 20G 以內的檔案。如果我們建立的檔案實際佔用的空間是記憶體的話,那麼這些資料應該佔用記憶體空間的什麼部分呢?根據 page cache 的實現功能可以理解,既然是某種檔案系統,那麼自然該使用 page cache 的空間來管理。我們試試是不是這樣?
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 36 89 0 1 19
-/+ buffers/cache: 15 111
Swap: 2 0 2
[root@tencent64 ~]# dd if=/dev/zero of=/tmp/tmpfs/testfile bs=1G count=13
13+0 records in
13+0 records out
13958643712 bytes (14 GB) copied, 9.49858 s, 1.5 GB/s
[root@tencent64 ~]#
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 49 76 0 1 32
-/+ buffers/cache: 15 110
Swap: 2 0 2
我們在 tmpfs 目錄下建立了一個 13G 的檔案,並透過前後 free 命令的對比發現,cached 增長了 13G,說明這個檔案確實放在了記憶體裡並且核心使用的是 cache 作為儲存。再看看我們關心的指標: -/+ buffers/cache 那一行。我們發現,在這種情況下 free 命令仍然提示我們有 110G 記憶體可用,但是真的有這麼多麼?我們可以人工觸發記憶體回收看看現在到底能回收多少記憶體:
[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 43 82 0 0 29
-/+ buffers/cache: 14 111
Swap: 2 0 2
可以看到,cached 佔用的空間並沒有像我們想象的那樣完全被釋放,其中 13G 的空間仍然被 /tmp/tmpfs 中的檔案佔用的。當然,我的系統中還有其他不可釋放的 cache 佔用著其餘16G記憶體空間。那麼 tmpfs 佔用的 cache 空間什麼時候會被釋放呢?是在其檔案被刪除的時候。如果不刪除檔案,無論記憶體耗盡到什麼程度,核心都不會自動幫你把 tmpfs 中的檔案刪除來釋放cache空間。
[root@tencent64 ~]# rm /tmp/tmpfs/testfile
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 30 95 0 0 16
-/+ buffers/cache: 14 111
Swap: 2 0 2
這是我們分析的第一種 cache 不能被回收的情況。還有其他情況,比如:
共享記憶體
共享記憶體是系統提供給我們的一種常用的程序間通訊(IPC)方式,但是這種通訊方式不能在 shell 中申請和使用,所以我們需要一個簡單的測試程式,程式碼如下:
[root@tencent64 ~]# cat shm.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define MEMSIZE 2048*1024*1023
int
main()
{
int shmid;
char *ptr;
pid_t pid;
struct shmid_ds buf;
int ret;
shmid = shmget(IPC_PRIVATE, MEMSIZE, 0600);
if (shmid<0) {
perror("shmget()");
exit(1);
}
ret = shmctl(shmid, IPC_STAT, &buf);
if (ret < 0) {
perror("shmctl()");
exit(1);
}
printf("shmid: %d\n", shmid);
printf("shmsize: %d\n", buf.shm_segsz);
buf.shm_segsz *= 2;
ret = shmctl(shmid, IPC_SET, &buf);
if (ret < 0) {
perror("shmctl()");
exit(1);
}
ret = shmctl(shmid, IPC_SET, &buf);
if (ret < 0) {
perror("shmctl()");
exit(1);
}
printf("shmid: %d\n", shmid);
printf("shmsize: %d\n", buf.shm_segsz);
pid = fork();
if (pid<0) {
perror("fork()");
exit(1);
}
if (pid==0) {
ptr = shmat(shmid, NULL, 0);
if (ptr==(void*)-1) {
perror("shmat()");
exit(1);
}
bzero(ptr, MEMSIZE);
strcpy(ptr, "Hello!");
exit(0);
} else {
wait(NULL);
ptr = shmat(shmid, NULL, 0);
if (ptr==(void*)-1) {
perror("shmat()");
exit(1);
}
puts(ptr);
exit(0);
}
}
程式功能很簡單,就是申請一段不到 2G 共享記憶體,然後開啟一個子程序對這段共享記憶體做一個初始化操作,父程序等子程序初始化完之後輸出一下共享記憶體的內容,然後退出。但是退出之前並沒有刪除這段共享記憶體。我們來看看這個程式執行前後的記憶體使用:
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 30 95 0 0 16
-/+ buffers/cache: 14 111
Swap: 2 0 2
[root@tencent64 ~]# ./shm
shmid: 294918
shmsize: 2145386496
shmid: 294918
shmsize: -4194304
Hello!
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 32 93 0 0 18
-/+ buffers/cache: 14 111
Swap: 2 0 2
cached 空間由 16G 漲到了 18G。那麼這段 cache 能被回收麼?繼續測試:
[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 32 93 0 0 18
-/+ buffers/cache: 14 111
Swap: 2 0 2
結果是仍然不可回收。大家可以觀察到,這段共享記憶體即使沒人使用,仍然會長期存放在 cache 中,直到其被刪除。刪除方法有兩種,一種是程式中使用 shmctl() 去 IPC_RMID,另一種是使用 ipcrm 命令。我們來刪除試試:
[root@tencent64 ~]# ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 4
0x00005fe7 32769 root 666 524288 2
0x00005fe8 65538 root 666 2097152 2
0x00038c0e 131075 root 777 2072 1
0x00038c14 163844 root 777 5603392 0
0x00038c09 196613 root 777 221248 0
0x00000000 294918 root 600 2145386496 0
[root@tencent64 ~]# ipcrm -m 294918
[root@tencent64 ~]# ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 4
0x00005fe7 32769 root 666 524288 2
0x00005fe8 65538 root 666 2097152 2
0x00038c0e 131075 root 777 2072 1
0x00038c14 163844 root 777 5603392 0
0x00038c09 196613 root 777 221248 0
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 30 95 0 0 16
-/+ buffers/cache: 14 111
Swap: 2 0 2
刪除共享記憶體後,cache 被正常釋放了。這個行為與 tmpfs 的邏輯類似。核心底層在實現共享記憶體(shm)、訊息佇列(msg)和訊號量陣列(sem)這些 POSIX:XSI 的 IPC 機制的記憶體儲存時,使用的都是 tmpfs。這也是為什麼共享記憶體的操作邏輯與 tmpfs 類似的原因。當然,一般情況下是 shm 佔用的記憶體更多,所以我們在此重點強調共享記憶體的使用。說到共享記憶體,Linux 還給我們提供了另外一種共享記憶體的方法,就是:
mmap
mmap() 是一個非常重要的系統呼叫,這僅從 mmap 本身的功能描述上是看不出來的。從字面上看,mmap 就是將一個檔案對映進程序的虛擬記憶體地址,之後就可以透過操作記憶體的方式對檔案的內容進行操作。但是實際上這個呼叫的用途是很廣泛的。當 malloc 申請記憶體時,小段記憶體核心使用 sbrk 處理,而大段記憶體就會使用 mmap。當系統呼叫 exec 族函式執行時,因為其本質上是將一個可執行檔案載入到記憶體執行,所以核心很自然的就可以使用 mmap 方式進行處理。我們在此僅僅考慮一種情況,就是使用 mmap 進行共享記憶體的申請時,會不會跟 shmget() 一樣也使用 cache?
同樣,我們也需要一個簡單的測試程式:
[root@tencent64 ~]# cat mmap.c
#include <stdlib.h>
#include <stdio.h>
#include <strings.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#define MEMSIZE 1024*1024*1023*2
#define MPFILE "./mmapfile"
int main()
{
void *ptr;
int fd;
fd = open(MPFILE, O_RDWR);
if (fd < 0) {
perror("open()");
exit(1);
}
ptr = mmap(NULL, MEMSIZE, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, fd, 0);
if (ptr == NULL) {
perror("malloc()");
exit(1);
}
printf("%p\n", ptr);
bzero(ptr, MEMSIZE);
sleep(100);
munmap(ptr, MEMSIZE);
close(fd);
exit(1);
}
這次我們乾脆不用什麼父子程序的方式了,就一個程序,申請一段 2G 的 mmap 共享記憶體,然後初始化這段空間之後等待 100 秒,再解除影射所以我們需要在它 sleep 這 100 秒內檢查我們的系統記憶體使用,看看它用的是什麼空間?當然在這之前要先建立一個 2G 的檔案 ./mmapfile。結果如下:
[root@tencent64 ~]# dd if=/dev/zero of=mmapfile bs=1G count=2
[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 30 95 0 0 16
-/+ buffers/cache: 14 111
Swap: 2 0 2
然後執行測試程式:
[root@tencent64 ~]# ./mmap &
[1] 19157
0x7f1ae3635000
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 32 93 0 0 18
-/+ buffers/cache: 14 111
Swap: 2 0 2
[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 32 93 0 0 18
-/+ buffers/cache: 14 111
Swap: 2 0 2
我們可以看到,在程式執行期間,cached 一直為 18G,比之前漲了 2G,並且此時這段 cache 仍然無法被回收。然後我們等待100秒之後程式結束。
[root@tencent64 ~]#
[1]+ Exit 1 ./mmap
[root@tencent64 ~]#
[root@tencent64 ~]# free -g
total used free shared buffers cached
Mem: 126 30 95 0 0 16
-/+ buffers/cache: 14 111
Swap: 2 0 2
程式退出之後,cached 佔用的空間被釋放。這樣我們可以看到,使用 mmap 申請標誌狀態為 MAP_SHARED 的記憶體,核心也是使用的 cache 進行儲存的。在程序對相關記憶體沒有釋放之前,這段 cache 也是不能被正常釋放的。實際上,mmap 的 MAP_SHARED 方式申請的記憶體,在核心中也是由 tmpfs 實現的。由此我們也可以推測,由於共享庫的只讀部分在記憶體中都是以 mmap 的 MAP_SHARED 方式進行管理,實際上它們也都是要佔用 cache 且無法被釋放的。
最後
我們透過三個測試例子,發現 Linux 系統記憶體中的 cache 並不是在所有情況下都能被釋放當做空閒空間用的。並且也也明確了,即使可以釋放 cache,也並不是對系統來說沒有成本的。總結一下要點,我們應該記得這樣幾點:
- 當 cache 作為檔案快取被釋放的時候會引發 IO 變高,這是 cache 加快檔案訪問速度所要付出的成本。
- tmpfs 中儲存的檔案會佔用 cache 空間,除非檔案刪除否則這個 cache 不會被自動釋放。
- 使用 shmget 方式申請的共享記憶體會佔用 cache 空間,除非共享記憶體被 ipcrm 或者使用 shmctl 去 IPC_RMID,否則相關的 cache 空間都不會被自動釋放。
- 使用 mmap 方法申請的 MAP_SHARED 標誌的記憶體會佔用 cache 空間,除非程序將這段記憶體 munmap,否則相關的 cache 空間都不會被自動釋放。
- 實際上 shmget、mmap 的共享記憶體,在核心層都是透過 tmpfs 實現的,tmpfs 實現的儲存用的都是 cache。
當理解了這些的時候,希望大家對 free 命令的理解可以達到我們說的第三個層次。我們應該明白,記憶體的使用並不是簡單的概念,cache 也並不是真的可以當成空閒空間用的。如果我們要真正深刻理解你的系統上的記憶體到底使用的是否合理,是需要理解清楚很多更細節知識,並且對相關業務的實現做更細節判斷的。我們當前實驗場景是 Centos 6 的環境,不同版本的 Linux 的 free 現實的狀態可能不一樣,大家可以自己去找出不同的原因。
當然,本文所述的也不是所有的 cache 不能被釋放的情形。那麼,在你的應用場景下,還有那些 cache 不能被釋放的場景呢?
相關文章
- Linux記憶體、Swap、Cache、BufferLinux記憶體
- linux記憶體管理(十)- 頁面回收(二)Linux記憶體
- SoftReference 到底在什麼時候被回收 ? 如何量化記憶體不足 ?記憶體
- Javascrip高程中的垃圾記憶體回收制(6)Java記憶體
- JavaScript中的垃圾回收和記憶體洩漏JavaScript記憶體
- 記憶體回收介紹記憶體
- js記憶體回收機制JS記憶體
- 探索JVM的垃圾回收(堆記憶體)JVM記憶體
- JVM記憶體回收機制——哪些記憶體需要被回收(JVM學習系列2)JVM記憶體
- redis的記憶體滿了之後,redis如何回收記憶體嗎Redis記憶體
- Linux記憶體是怎麼工作的?Linux記憶體
- Linux 中的“大記憶體頁”(hugepage)是個什麼?Linux記憶體
- JVM垃圾回收器、記憶體分配與回收策略JVM記憶體
- 什麼是Java記憶體模型(JMM)中的主記憶體和本地記憶體?Java記憶體模型
- JavaScript 記憶體管理及垃圾回收JavaScript記憶體
- JVM記憶體管理和垃圾回收JVM記憶體
- Node - 記憶體管理和垃圾回收記憶體
- Node記憶體限制和垃圾回收記憶體
- Java記憶體管理 -JVM 垃圾回收Java記憶體JVM
- Java堆外直接記憶體回收Java記憶體
- 效能測試必備知識(11)- 怎麼理解記憶體中的Buffer和Cache?記憶體
- 記憶體能被魯大師識別,但是不能被系統使用記憶體
- jvm:記憶體模型、記憶體分配及GC垃圾回收機制JVM記憶體模型GC
- golang 垃圾回收器如何標記記憶體?Golang記憶體
- Redis的記憶體回收機制和記憶體過期淘汰策略詳解Redis記憶體
- Linux記憶體被吃掉了,它去哪裡了?Linux記憶體
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- Java記憶體模型,垃圾回收機制,常用記憶體命令及工具Java記憶體模型
- javascript的垃圾回收機制和記憶體管理JavaScript記憶體
- JVM 之 記憶體分配與回收策略JVM記憶體
- JVM垃圾回收和記憶體分配策略JVM記憶體
- PHP 垃圾回收與記憶體管理指引PHP記憶體
- C# 垃圾回收釋放記憶體C#記憶體
- 在Linux中,什麼是虛擬記憶體?它是如何工作的?Linux記憶體
- [轉帖]深入JVM - Code Cache記憶體池JVM記憶體
- 在Linux中,記憶體怎麼看?磁碟狀態怎麼看?Linux記憶體
- JS中的棧記憶體、堆記憶體JS記憶體
- Linux共享記憶體的管理Linux記憶體
- [Linux]共享記憶體Linux記憶體