天天看點

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

您真的了解Linux的free指令麼?

在Linux系統中,我們經常用free指令來檢視系統記憶體的使用狀态。在一個RHEL6的系統上,free指令的顯示内容大概是這樣一個狀态:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

這裡的預設顯示機關是kb,我的伺服器是128G記憶體,是以數字顯得比較大。這個指令幾乎是每一個使用過Linux的人必會的指令,但越是這樣的指令,似乎真正明白的人越少(我是說比例越少)。

一般情況下,對此指令輸出的了解可以分這幾個層次:

  1. 不了解。這樣的人的第一反應是:天啊,記憶體用了好多,70個多G,可是我幾乎沒有運作什麼大程式啊?為什麼會這樣?Linux好占記憶體!
  2. 自以為很了解。這樣的人一般自習評估過會說:嗯,根據我專業的眼光看出來,記憶體才用了17G左右,還有很多剩餘記憶體可用。buffers/cache占用的較多,說明系統中有程序曾經讀寫過檔案,但是不要緊,這部分記憶體是當空閑來用的。
  3. 真的很了解。這種人的反應反而讓人感覺最不懂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作為其緩存來管理使用。

當然,不是所有的記憶體都是以頁(page)進行管理的,也有很多是針對塊(block)進行管理的,這部分記憶體使用如果要用到cache功能,則都集中到buffer cache中來使用。

(從這個角度出發,是不是buffer cache改名叫做block cache更好?)然而,也不是所有塊(block)都有固定長度,系統上塊的長度主要是根據所使用的塊裝置決定的,而頁長度在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标記為不同的緩沖區,并記錄是哪一個緩沖區被修改了。

這樣,核心在後續執行髒資料的回寫(writeback)時,就不用将整個page寫回,而隻需要寫回修改的部分即可。

如何回收cache?

Linux核心會在記憶體将要耗盡的時候,觸發記憶體回收的工作,以便釋放出記憶體給急需記憶體的程序使用。

一般情況下,這個操作中主要的記憶體釋放都來自于對buffer/cache的釋放。尤其是被使用更多的cache空間。既然它主要用來做緩存,隻是在記憶體夠用的時候加快程序對檔案的讀寫速度,那麼在記憶體壓力較大的情況下,當然有必要清空釋放cache,作為free空間分給相關程序使用。

是以一般情況下,我們認為buffer/cache空間可以被釋放,這個了解是正确的。

是以伴随着cache清除的行為的,一般都是系統IO飙高。因為核心要對比cache中的資料和對應硬碟檔案上的資料是否一緻,如果不一緻需要寫回,之後才能回收。

在系統中除了記憶體将被耗盡的時候可以清緩存以外,我們還可以使用下面這個檔案來人工觸發緩存清除的操作:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

方法是:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

當然,這個檔案可以設定的值分别為1、2、3。它們所表示的含義為:

echo 1 > /proc/sys/vm/drop_caches:表示清除pagecache。

echo 2 > /proc/sys/vm/drop_caches:表示清除回收slab配置設定器中的對象(包括目錄項緩存和inode緩存)。slab配置設定器是核心中管理記憶體的一種機制,其中很多緩存資料實作都是用的pagecache。

echo 3 > /proc/sys/vm/drop_caches:表示清除page cache和slab配置設定器中的緩存對象。

cache都能被回收麼?

我們分析了cache能被回收的情況,那麼有沒有不能被回收的cache呢?當然有。我們先來看第一種情況:

tmpfs

大家知道Linux提供一種“臨時”檔案系統叫做tmpfs,它可以将記憶體的一部分空間拿來當做檔案系統使用,使記憶體空間可以當做目錄檔案來用。

現在絕大多數Linux系統都有一個叫做/dev/shm的tmpfs目錄,就是這樣一種存在。當然,我們也可以手工建立一個自己的tmpfs,方法如下:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

于是我們就建立了一個新的tmpfs,空間是20G,我們可以在/tmp/tmpfs中建立一個20G以内的檔案。

如果我們建立的檔案實際占用的空間是記憶體的話,那麼這些資料應該占用記憶體空間的什麼部分呢?

根據pagecache的實作功能可以了解,既然是某種檔案系統,那麼自然該使用pagecache的空間來管理。我們試試是不是這樣?

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

我們在tmpfs目錄下建立了一個13G的檔案,并通過前後free指令的對比發現,cached增長了13G,說明這個檔案确實放在了記憶體裡并且核心使用的是cache作為存儲。

再看看我們關心的名額:    -/+ buffers/cache那一行。

我們發現,在這種情況下free指令仍然提示我們有110G記憶體可用,但是真的有這麼多麼?我們可以人工觸發記憶體回收看看現在到底能回收多少記憶體:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

可以看到,cached占用的空間并沒有像我們想象的那樣完全被釋放,其中13G的空間仍然被/tmp/tmpfs中的檔案占用的。當然,我的系統中還有其他不可釋放的cache占用着其餘16G記憶體空間。

那麼tmpfs占用的cache空間什麼時候會被釋放呢?是在其檔案被删除的時候,如果不删除檔案,無論記憶體耗盡到什麼程度,核心都不會自動幫你把tmpfs中的檔案删除來釋放cache空間。

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

這是我們分析的第一種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共享記憶體,然後打開一個子程序對這段共享記憶體做一個初始化操作,父程序等子程序初始化完之後輸出一下共享記憶體的内容,然後退出。但是退出之前并沒有删除這段共享記憶體。

我們來看看這個程式執行前後的記憶體使用:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

cached空間由16G漲到了18G。那麼這段cache能被回收麼?繼續測試:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

結果是仍然不可回收。大家可以觀察到,這段共享記憶體即使沒人使用,仍然會長期存放在cache中,直到其被删除。删除方法有兩種:

  1. 程式中使用shmctl()去IPC_RMID
  2. 使用ipcrm指令

我們來删除試試:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

删除共享記憶體後,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。結果如下:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

然後執行測試程式:

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

我們可以看到,在程式執行期間,cached一直為18G,比之前漲了2G,并且此時這段cache仍然無法被回收。然後我們等待100秒之後程式結束。

我就是認真:Linux 記憶體中的Cache,真的能被回收麼?

程式退出之後,cached占用的空間被釋放。

這樣我們可以看到,使用mmap申請标志狀态為MAP_SHARED的記憶體,核心也是使用的cache進行存儲的。在程序對相關記憶體沒有釋放之前,這段cache也是不能被正常釋放的。

實際上,mmap的MAP_SHARED方式申請的記憶體,在核心中也是由tmpfs實作的。由此我們也可以推測,由于共享庫的隻讀部分在記憶體中都是以mmap的MAP_SHARED方式進行管理,實際上它們也都是要占用cache且無法被釋放的。

最後

我們通過三個測試例子,發現Linux系統記憶體中的cache并不是在所有情況下都能被釋放當做空閑空間用的。并且也明确了,即使可以釋放cache,也并不是對系統來說沒有成本的。總結一下要點,我們應該記得這樣幾點:

  1. 當cache作為檔案緩存被釋放的時候會引發IO變高,這是cache加快檔案通路速度所要付出的成本。
  2. tmpfs中存儲的檔案會占用cache空間,除非檔案删除否則這個cache不會被自動釋放。
  3. 使用shmget方式申請的共享記憶體會占用cache空間,除非共享記憶體被ipcrm或者使用shmctl去IPC_RMID,否則相關的cache空間都不會被自動釋放。
  4. 使用mmap方法申請的MAP_SHARED标志的記憶體會占用cache空間,除非程序将這段記憶體munmap,否則相關的cache空間都不會被自動釋放。
  5. 實際上shmget、mmap的共享記憶體,在核心層都是通過tmpfs實作的,tmpfs實作的存儲用的都是cache。

當了解了這些的時候,希望大家對free指令的了解可以達到我們說的第三個層次。

記憶體的使用并不是簡單的概念,cache也并不是真的可以當成空閑空間用的。

如果我們要真正深刻了解你的系統上的記憶體到底使用的是否合理,是需要了解清楚很多更細節知識,并且對相關業務的實作做更細節判斷的。