標籤:rss 記憶體頁 procselfsmaps private_dirty reids
上一篇博文中,我介紹了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_SIZEsize_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):
- tc_malloc_size(p) 【tcmalloc】
- je_malloc_usable_size(p)【jemalloc】
- 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變成了null 指標,所以判斷一下p是不是null 指標。接下來的的幾部操作很好理解,就是將第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;}#elsesize_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/bashSize: 956 kBRss: 728 kBPss: 364 kBShared_Clean: 728 kBShared_Dirty: 0 kBPrivate_Clean: 0 kBPrivate_Dirty: 0 kBReferenced: 728 kBAnonymous: 0 kBAnonHugePages: 0 kBSwap: 0 kBKernelPageSize: 4 kBMMUPageSize: 4 kBLocked: 0 kBVmFlags: 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]152root 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這幾個欄位含義的詳細討論,有位網友進行了深入地探究,並形成了博文,推薦閱讀:
《Linux /proc/$pid/smaps的含義》
《/proc/$pid/smaps各欄位值的計算測試》
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Redis記憶體管理的基石zmallc.c源碼解讀(二)