淺談Linux 內存中的Cache: buffers 與 cached

Linux 內存中的Cache,真的能被回收麼?


您真的瞭解Linux的free命令麼?


       在Linux系統中,我們經常用free命令來查看系統內存的使用狀態。在一個RHEL6的系統上,free命令的顯示內容大概是這樣一個狀態:

       這裏的默認顯示單位是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是幹什麼的就可以明白清緩存必須保證cache中的數據跟對應文件中的數據一致,才能對cache進行釋放。


所以伴隨着cache清除的行爲的,一般都是系統IO飆高。因爲內核要對比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,方法如下:

 

於是我們就創建了一個新的tmpfs,空間是20G,我們可以在/tmp/tmpfs中創建一個20G以內的文件。

如果我們創建的文件實際佔用的空間是內存的話,那麼這些數據應該佔用內存空間的什麼部分呢?

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

 

我們在tmpfs目錄下創建了一個13G的文件,並通過前後free命令的對比發現,cached增長了13G,說明這個文件確實放在了內存裏並且內核使用的是cache作爲存儲。

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

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

 

可以看到,cached佔用的空間並沒有像我們想象的那樣完全被釋放,其中13G的空間仍然被/tmp/tmpfs中的文件佔用的。當然,我的系統中還有其他不可釋放的cache佔用着其餘16G內存空間。

那麼tmpfs佔用的cache空間什麼時候會被釋放呢?是在其文件被刪除的時候,如果不刪除文件,無論內存耗盡到什麼程度,內核都不會自動幫你把tmpfs中的文件刪除來釋放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共享內存,然後打開一個子進程對這段共享內存做一個初始化操作,父進程等子進程初始化完之後輸出一下共享內存的內容,然後退出。但是退出之前並沒有刪除這段共享內存。

我們來看看這個程序執行前後的內存使用:

 

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

 

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

  1. 程序中使用shmctl()去IPC_RMID

  2. 使用ipcrm命令

我們來刪除試試:

 

刪除共享內存後,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。結果如下:

 

然後執行測試程序:

 

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

 

程序退出之後,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也並不是真的可以當成空閒空間用的。

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

我們當前實驗場景是Centos 6的環境,不同版本的Linux的free現實的狀態可能不一樣,大家可以自己去找出不同的原因。

當然,本文所述的也不是所有的cache不能被釋放的情形。那麼,在你的應用場景下,還有那些cache不能被釋放的場景呢?


引用:

http://mp.weixin.qq.com/s?__biz=MzA4Nzg5Nzc5OA==&mid=2651659553&idx=1&sn=6a14ef45ef79dffbdd302f882eb0d096&scene=2&srcid=0505jCalIMY3VQjhEIZDL0kZ&from=timeline&isappinstalled=0#wechat_redirect


後注: 偶爾翻看到,甚解我惑,僅以獻給同道中人

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章