天天看點

Redis記憶體管理的基石zmallc.c源碼解讀(二)開胃菜大餐

        上一篇博文中,我介紹了zmalloc.c檔案中幾個常用的函數,接下來給大家介紹一下該檔案中的其他函數,其實本文中的很多函數要比上一篇文章中的函數要更有趣的,并且涉及到很多作業系統的知識。前面幾個函數比較簡單,一筆帶過,後面幾個是學習的重點。

開胃菜

zmalloc_enable_thread_safeness

void zmalloc_enable_thread_safeness(void) {
    zmalloc_thread_safe = 1;
}
           

        zmalloc_thread_safe是一個全局靜态變量(static int)。它是操作是否是線程安全的辨別。1 表示線程安全,0 表示非線程安全。

zmalloc_used_memory

size_t zmalloc_used_memory(void) {
    size_t um;

    if (zmalloc_thread_safe) {
#if defined(__ATOMIC_RELAXED) || defined(HAVE_ATOMIC)
        um = update_zmalloc_stat_add(0);
#else
        pthread_mutex_lock(&used_memory_mutex);
        um = used_memory;
        pthread_mutex_unlock(&used_memory_mutex);
#endif
    }
    else {
        um = used_memory;
    }

    return um;
}
           

        該函數要完成的操作就是傳回變量used_memory(已用記憶體)的值,是以它的功能是查詢系統目前為Redis配置設定的記憶體大小。本身代碼量不大,但是涉及到了線程安全模式下的查詢操作。實作線程同步用到了互斥鎖(mutex)。關于互斥鎖的内容在上一篇文章中已經簡要介紹過了。總之要記住的是加鎖(pthread_mutex_lock)和解鎖(pthread_mutex_unlock)。在加了互斥鎖之後,就能保證之後的代碼同時隻能被一個線程所執行。

zmalloc_set_oom_handler

void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
    zmalloc_oom_handler = oom_handler;
}
           

        該函數的功能是給zmalloc_oom_handler指派。zmalloc_oom_handler是一個函數指針,表示在記憶體不足(out of memory,縮寫oom)的時候所采取的操作,它的類型是void (*) (size_t)。是以zmalloc_set_oom_handler函數的參數也是void (*) (size_t)類型,調用的時候就是傳遞一個該類型的函數名就可以了。         不過zmalloc_oom_handler在聲明的時候初始化了預設值——zmalloc_default_oom()。同樣在上一篇博文中也有過介紹。

zmalloc_size

#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr) {
    void *realptr = (char*)ptr-PREFIX_SIZE;
    size_t size = *((size_t*)realptr);
    /* Assume at least that all the allocations are padded at sizeof(long) by
     * the underlying allocator. */
    if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
    return size+PREFIX_SIZE;
}
#endif
           

        這段代碼和我在上一篇博文中介紹的zfree()函數中的内容頗為相似。大家可以去閱讀那一篇 博文。這裡再概括一下,zmalloc(size)在配置設定記憶體的時候會多申請sizeof(size_t)個位元組大小的記憶體【64位系統中是8位元組】,即調用malloc(size+8),是以一共 申請配置設定size+8個位元組,zmalloc(size)會在已配置設定記憶體的首位址開始的8位元組中存儲size的值,實際上因為記憶體對齊,malloc(size+8)配置設定的記憶體可能會比size+8要多一些,目的是湊成8的倍數,是以實際配置設定的記憶體大小是size+8+X【(size+8+X)%8==0 (0<=X<=7)】。然後記憶體指針會向右偏移8個位元組的長度。zfree()就是zmalloc()的一個逆操作,而zmalloc_size()的目的就是計算出size+8+X的總大小。 --------------------------------------------------------------------------------------------------------------------------------------------------------------

        這個函數是一個條件編譯的函數,通過閱讀zmalloc.h檔案,我們可以得知zmalloc_size()依據不同的平台,具有不同的宏定義,因為在某些平台上提供查詢已配置設定記憶體實際大小的函數,可以直接 #define zmalloc_size(p):

  1. tc_malloc_size(p)               【tcmalloc】
  2. je_malloc_usable_size(p)【jemalloc】 
  3. malloc_size(p)                 【Mac系統】

當這三個平台都不存在的時候,就自定義,也就是上面的源碼。 --------------------------------------------------------------------------------------------------------------------------------------------------------------

大餐

zmalloc_get_rss

        擷取RSS的大小,這個RSS可不是我們在網絡上常常看到的RSS,而是指的Resident Set Size,表示目前程序實際所駐留在記憶體中的空間大小,即不包括被交換(swap)出去的空間。         了解一點作業系統的知識,就會知道我們所申請的記憶體空間不會全部常駐記憶體,系統會把其中一部分暫時不用的部分從記憶體中置換到swap區(裝Linux系統的時候我們都知道有一個交換空間)。         該函數大緻的操作就是在目前程序的 /proc/<pid>/stat 【<pid>表示目前程序id】檔案中進行檢索。該檔案的第24個字段是RSS的資訊,它的機關是pages(記憶體頁的數目)

size_t zmalloc_get_rss(void) {
    int page = sysconf(_SC_PAGESIZE);
    size_t rss;
    char buf[4096];
    char filename[256];
    int fd, count;
    char *p, *x;

    snprintf(filename,256,"/proc/%d/stat",getpid());
    if ((fd = open(filename,O_RDONLY)) == -1) return 0;
    if (read(fd,buf,4096) <= 0) {
        close(fd);
        return 0;
    }
    close(fd);

    p = buf;
    count = 23; /* RSS is the 24th field in /proc/<pid>/stat */
    while(p && count--) {
        p = strchr(p,' ');
        if (p) p++;
    }
    if (!p) return 0;
    x = strchr(p,' ');
    if (!x) return 0;
    *x = '\0';

    rss = strtoll(p,NULL,10);
    rss *= page;
    return rss;
}
           

         函數開頭:

int page = sysconf(_SC_PAGESIZE);
           

        通過調用庫函數sysconf()【大家可以man sysconf檢視詳細内容】來查詢記憶體頁的大小。

接下來:

snprintf(filename,256,"/proc/%d/stat",getpid());
           

        getpid()就是獲得目前程序的id,是以這個snprintf()的功能就是将目前程序所對應的stat檔案的絕對路徑名儲存到字元數組filename中。【不得不稱贊一下類Unix系統中“萬物皆檔案”的概念】

if ((fd = open(filename,O_RDONLY)) == -1) return 0;
    if (read(fd,buf,4096) <= 0) {
        close(fd);
        return 0;
    }
           

        以隻讀模式打開 /proc/<pid>/stat 檔案。然後從中讀入4096個字元到字元數組buf中。如果失敗就關閉檔案描述符fd,并退出(個人感覺因錯誤退出,還是傳回-1比較好吧)。

p = buf;
    count = 23; /* RSS is the 24th field in /proc/<pid>/stat */
    while(p && count--) {
        p = strchr(p,' ');
        if (p) p++;
    }
           

        RSS在stat檔案中的第24個字段位置,是以就是在第23個空格的後面。觀察while循環,循環體中用到了字元串函數strchr(),這個函數在字元串p中查詢空格字元,如果找到就把空格所在位置的字元指針傳回并指派給p,找不到會傳回NULL指針。p++原因是因為,p目前指向的是空格,在執行自增操作之後就指向下一個字段的首位址了。如此循環23次,最終p就指向第24個字段的首位址了。

if (!p) return 0;
    x = strchr(p,' ');
    if (!x) return 0;
    *x = '\0';
           

        因為循環結束也可能是p變成了空指針,是以判斷一下p是不是空指針。接下來的的幾部操作很好了解,就是将第24個字段之後的空格設定為'\0',這樣p就指向一個一般的C風格字元串了。

rss = strtoll(p,NULL,10);
    rss *= page;
    return rss;
           

        這段代碼又用到了一個字元串函數——strtoll():顧名思義就是string to long long的意思啦。它有三個參數,前面兩個參數表示要轉換的字元串的起始和終止位置(字元指針類型),NULL和'\0'是等價的。最後一個參數表示的是“進制”,這裡就是10進制了。         後面用rss和page相乘并傳回,因為rss獲得的實際上是記憶體頁的頁數,page儲存的是每個記憶體頁的大小(機關位元組),相乘之後就表示RSS實際的記憶體大小了。

zmalloc_get_fragmentation_ratio

/* Fragmentation = RSS / allocated-bytes */
float zmalloc_get_fragmentation_ratio(size_t rss) {
    return (float)rss/zmalloc_used_memory();
}
           

        這個函數是查詢記憶體碎片率(fragmentation ratio),即RSS和所配置設定總記憶體空間的比值。需要用zmalloc_get_rss()獲得RSS的值,再以RSS的值作為參數傳遞進來。 ------------------------------------------------------------------------------------------------------------------------- -------------------------------------

記憶體碎片分為:内部碎片和外部碎片

  • 内部碎片:是已經被配置設定出去(能明确指出屬于哪個程序)卻不能被利用的記憶體空間,直到程序釋放掉,才能被系統利用;
  • 外部碎片:是還沒有被配置設定出去(不屬于任何程序),但由于太小了無法配置設定給申請記憶體空間的新程序的記憶體空閑區域。

------------------------------------------------------------------------------------------------------------------------- -------------------------------------

zmalloc_get_fragmentation_ratio()要獲得的顯然是内部碎片率。

zmalloc_get_smap_bytes_by_field

#if defined(HAVE_PROC_SMAPS)
size_t zmalloc_get_smap_bytes_by_field(char *field) {
    char line[1024];
    size_t bytes = 0;
    FILE *fp = fopen("/proc/self/smaps","r");
    int flen = strlen(field);

    if (!fp) return 0;
    while(fgets(line,sizeof(line),fp) != NULL) {
        if (strncmp(line,field,flen) == 0) {
            char *p = strchr(line,'k');
            if (p) {
                *p = '\0';
                bytes += strtol(line+flen,NULL,10) * 1024;
            }
        }
    }
    fclose(fp);
    return bytes;
}
#else
size_t zmalloc_get_smap_bytes_by_field(char *field) {
    ((void) field);
    return 0;
}
#endif
           

一個條件編譯的函數,我們當然要聚焦到#if defined的部分。

FILE *fp = fopen("/proc/self/smaps","r");
           

        用标準C的fopen()以隻讀方式打開/proc/self/smaps檔案。簡單介紹一下該檔案,前面我們已經說過/proc目錄下有許多以程序id命名的目錄,裡面儲存着每個程序的狀态資訊,而/proc/self目錄的内容和它們是一樣的,self/ 表示的是目前程序的狀态目錄。而smaps檔案中記錄着該程序的詳細映像資訊,該檔案内部由多個結構相同的 塊組成,看一下其中 某一塊的内容:

00400000-004ef000 r-xp 00000000 08:08 1305603                            /bin/bash
Size:                956 kB
Rss:                 728 kB
Pss:                 364 kB
Shared_Clean:        728 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:          728 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd ex mr mw me dw sd 
           

除去開頭和結尾兩行,其他的每一行都有一個字段和該字段的值(機關kb)組成【每個字段的具體含義,各位自行百度】。注意這隻是smaps檔案的一小部分。

while(fgets(line,sizeof(line),fp) != NULL) {
        if (strncmp(line,field,flen) == 0) {
            char *p = strchr(line,'k');
            if (p) {
                *p = '\0';
                bytes += strtol(line+flen,NULL,10) * 1024;
            }
        }
    }
           
  • 利用fgets()逐行讀取/proc/self/smaps檔案内容
  • 然後strchr()将p指針定義到字元k的位置
  • 然後将p置為'\0',截斷形成普通的C風格字元串
  • line指向的該行的首字元,line+flen(要查詢的字段的長度)所指向的位置就是字段名後面的空格處了,不必清除空格,strtol()無視空格可以将字元串轉換成int類型
  • strol()轉換的結果再乘以1024,這是因為smaps裡面的大小是kB表示的,我們要傳回的是B(位元組byte)表示

------------------------------------------------------------------------------------------------------------------------- -------------------------------------

實際上 /proc/self目錄是一個符号連結,指向/proc/目錄下以目前id命名的目錄。我們可以進入該目錄下敲幾個指令測試一下。

[email protected]:/proc/self# pwd -P
/proc/4152
[email protected]:/proc/self# ps aux|grep [4]152
root      4152  0.0  0.0  25444  2176 pts/0    S    09:06   0:00 bash
           

------------------------------------------------------------------------------------------------------------------------- -------------------------------------

zmalloc_get_private_dirty

size_t zmalloc_get_private_dirty(void) {
    return zmalloc_get_smap_bytes_by_field("Private_Dirty:");
}
           

        源代碼很簡單,該函數的本質就是在調用 zmalloc_get_smap_bytes_by_field("Private_Dirty:");其完成的操作就是掃描 /proc/self/smaps檔案,統計其中所有 Private_Dirty字段的和。那麼這個Private_Dirty是個什麼意思呢?         大家繼續觀察一下,我在上面貼出的 /proc/self/smaps檔案的結構,它有很多結構相同的部分組成。其中有幾個字段有如下的關系:

Rss=Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty         其中:

  • Shared_Clean:多程序共享的記憶體,且其内容未被任意程序修改 
  • Shared_Dirty:多程序共享的記憶體,但其内容被某個程序修改 
  • Private_Clean:某個程序獨享的記憶體,且其内容沒有修改 
  • Private_Dirty:某個程序獨享的記憶體,但其内容被該程序修改

    其實所謂的共享的記憶體,一般指的就是Unix系統中的共享庫(.so檔案)的使用,共享庫又叫動态庫(含義同Windows下的.dll檔案),它隻有在程式運作時才被裝入記憶體。這時共享庫中的代碼和資料可能會被多個程序所調用,于是就會産生共享(Shared)與私有(Private)、幹淨(Clean)與髒(Dirty)的差別了。此外該處所說的共享的記憶體除了包括共享庫以外,還包括System V的IPC機制之一的共享記憶體段(shared memory) ------------------------------------------------------------------------------------------------------------------------- -------------------------------------

關于smaps檔案中Shared_Clean、Shared_Dirty、Private_Clean、Private_Dirty這幾個字段含義的詳細讨論,有位網友進行了深入地探究,并形成了博文,推薦閱讀:

  1. 《Linux /proc/$pid/smaps的含義》
  2. 《/proc/$pid/smaps各字段值的計算測試》

------------------------------------------------------------------------------------------------------------------------- -------------------------------------

繼續閱讀