文章目錄
- 問題描述
- 問題分析
- 針對問題1 的猜測:
- 針對問題2 的猜測:
- 原理追蹤
- 總結
問題描述
事情開始于一段記憶體問題,通過gperf工具抓取程序運作過程中的記憶體占用情況。
分析結果時發現一個有趣的事情,top看到的實際實體記憶體隻有幾兆,但是pprof統計的記憶體資訊卻達到了幾個G(其實這個問題用gperf heap profiler的選項也能很好的驗證想法,但是還是想探索一番)。
很明顯是建立線程時産生的記憶體配置設定,且最終的配置設定函數是
__pthread_create_2_1
,這是目前版本glibc建立線程時的實作函數,且在該函數内進行線程空間的配置設定。
檢視程序代碼,發現确實有大量的線程建立,我們知道線程是有自己獨立的棧空間,top的 RES統計的是目前程序占用實體記憶體的情況,也就是當使用者程序想要申請實體記憶體的時候會發出缺頁異常,程序切換到核心态,由核心調用對應的系統調用取一部分實體記憶體加入頁表交給使用者态程序。這個時候,使用的實體記憶體的大小才會被計算到RES之中。
回到top資料和pprof抓取的記憶體資料對不上的問題,難道單獨線程的建立并不會占用實體記憶體?
到現在為止可以梳理出以下幾個問題:
- 線程的建立消耗的記憶體在哪裡? (猜測可能在棧上,因為top的VIRT确實很大)
- 消耗的記憶體大小 是如何判斷的?(目前還不太清楚,不過以上程序代碼是建立了800個線程,算下來平均每個線程的大小是10M了)
問題分析
- 為了單獨聚焦線程建立時的記憶體配置設定問題,編寫如下的簡單測試代碼,建立800個線程:
#include <cstdio>
#include <cstdlib>
#include <thread>
void f(long id) {
fprintf(stdout, "create thread %ld\n",id);
sleep(10000);
}
int main()
{
long thread_num = 800; // client thread num
std::vector<std::thread> v;
for (long id = 0;id < thread_num; ++id ) {
std::thread t(f,id);
t.detach();
fprintf(stdout, "exit ...\n");
}
printf("\n");
sleep(4000);
return 0;
}
單純的建立線程,并不做其他的記憶體配置設定操作。
- 為了抓取該程序的記憶體配置設定過程,我們加入gperf工具來運作檢視。
#目前shell的環境變量中加入tcmalloc動态庫的路徑
#如果沒有tcmalloc,則yum install gperftools即可
env LD_PRELOAD="/usr/lib/libtcmalloc.so"
#編譯加傳入連結接tcmalloc的選項
g++ -std=c++11 test.cpp -pthread -ltcmalloc
#使用會生成heap profile的方式啟動程序
#開啟隻監控mmap,mremap,sbrk的系統調用配置設定記憶體的方式,并且ctrl+c停止運作時生成heap檔案
HEAPPROFILESIGNAL=2 HEAP_PROFILE_ONLY_MMAP=true HEAP_PROFILE_INUSE_INTERVAL=1024 HEAPPROFILE=./thread ./a.out
-
程序運作的過程中我們使用pmap檢視程序記憶體空間的配置設定情況
輸出資訊如下pmap -X PID
-
其中:
address為程序的虛拟位址
size為目前字段配置設定的虛拟記憶體的大小,機關是KB
Rss為占用的實體記憶體的大小
Mapping為記憶體所處的區域
統計了一下size:10240KB 的區域剛好是800個,顯然該區域為線程空間。所處的程序記憶體區域也不在heap上,占用的實體記憶體大小大小也就是一個指針的大小,8B
使用
pmap PID
再次檢視發現線程的空間都分布在anno區域上,即使用的匿名頁的方式
匿名頁的描述資訊如下:
The amount of anonymous memory is reported for each mapping. Anonymous memory shared with other address spaces is not included, unless the -a option is specified.
Anonymous memory is reported for the process heap, stack, for ‘copy on write’ pages with mappings mapped with MAP_PRIVATE.
即匿名頁是使用mmap方式配置設定的,且會将使用的記憶體葉标記為MAP_PRIVATE,即僅為程序使用者空間獨立使用。
針對問題1 的猜測:
到現在為止我們通過工具發現了線程的記憶體配置設定貌似是通過mmap,使用匿名頁的方式配置設定出來的,因為匿名頁能夠和其他程序共享記憶體空間,是以不會被計入目前程序的實體記憶體區域。
關于程序的記憶體分布可以參考程序記憶體分布,匿名頁是在堆區域和棧區域之間的一部分記憶體區域,pmap的輸出我們也能看出來mmapping的那一列。
針對問題2 的猜測:
那為什麼會占用10M的虛拟記憶體呢(size那一列),顯然也很好了解了。因為線程是獨享自己的棧空間的,是以需要為每個線程開辟屬于自己的函數棧空間來儲存函數棧幀和局部變量。
ulimit -a
能夠看到stack size 那一行是屬于目前系統預設的程序棧空間的大小。
這裡可以通過
ulimit -s 2048
将系統的預設配置設定的棧的大小設定為2M,再次運作程式會發現線程的虛拟記憶體占用變為了2M
是不是很有趣。
到了這裡,我們僅僅是使用工具進行了線程記憶體的占用分析,但問題并沒有追到底層。
原理追蹤
我們上面使用了gperf的heap proflie運作了程式,此時我們ctrl+c終端程序之後會在目前目錄下生成很多個.heap檔案,使用pprof 的svg選項将檔案内容導出
pprof --svg a.out thread.0001.heap > thread.svg
将導出的thread.svg放入浏覽器中可以看到線程記憶體占用的一個calltrace,如下(如果程式中鍊入了glibc以及核心的靜态庫,估計calltrace會龐大很多):
也就是線程建立時的棧空間的配置設定最終是由函數
__pthread_create_2_1
配置設定的。
PS:這裡的calltrace 僅僅包括mmap,mremap,sbrk的配置設定,因為我們在程序運作的時候指定了HEAP_PROFILE_ONLY_MMAP=true 選項,如果各位僅僅想要确認malloc,calloc,realloc等在堆上配置設定的記憶體大小可以去掉該選項來運作程序。
輸出svg的時候增加pprof的
--ignore
選項來忽略mmap,sbrk的配置設定記憶體,這樣的calltrace就沒有他們的記憶體占用了,僅包括堆上的記憶體占用
pprof --ignore='DoAllocWithArena|SbrkSysAllocator::Alloc|MmapSysAllocator::Alloc' --svg a.out thread.0001.heap > thread.svg
檢視glibc的線程建立源碼
pthread_create.c
函數__pthread_create_2_1 調用ALLOCATE_STACK為線程的資料結構pd配置設定記憶體空間。
versioned_symbol (libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1)
int
__pthread_create_2_1 (newthread, attr, start_routine, arg)
pthread_t *newthread;
const pthread_attr_t *attr;
void *(*start_routine) (void *);
void *arg;
{
......
struct pthread *pd = NULL;
int err = ALLOCATE_STACK (iattr, &pd);
if (__builtin_expect (err != 0, 0)
......
}
ALLOCATE_STACK函數實作入下
allocatestack.c
:
配置設定的空間大小會優先從使用者設定的pthread_attr屬性
attr.stacksize
中擷取,如果使用者程序沒有設定stacksize,就會擷取系統預設的stacksize的大小。
接下來會調用get_cached_stack函數來擷取棧上面可以獲得的空間大小size以及所處的虛拟記憶體空間的位址mem。
最後通過mmap将目前線程所需要的記憶體葉标記為MAP_PRIVATE和MAP_ANONYMOUS表示目前記憶體區域僅屬于使用者程序且被使用者程序共享。
詳細實作如下:
static int
allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
ALLOCATE_STACK_PARMS)
{
......
/* Get the stack size from the attribute if it is set. Otherwise we
use the default we determined at start time. */
size = attr->stacksize ?: __default_stacksize;
......
void *mem;
......
/* Try to get a stack from the cache. */
reqsize = size;
pd = get_cached_stack (&size, &mem);
if (pd == NULL)
{
/* To avoid aliasing effects on a larger scale than pages we
adjust the allocated stack size if necessary. This way
allocations directly following each other will not have
aliasing problems. */
#if MULTI_PAGE_ALIASING != 0
if ((size % MULTI_PAGE_ALIASING) == 0)
size += pagesize_m1 + 1;
#endif
/*mmap配置設定實體記憶體,并進行記憶體區域的标記*/
mem = mmap (NULL, size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (__builtin_expect (mem == MAP_FAILED, 0))
{
if (errno == ENOMEM)
__set_errno (EAGAIN);
return errno;
}