Linux创建线程时 内存分配的那些事
文章目錄
- 問(wèn)題描述
- 問(wèn)題分析
- 針對(duì)問(wèn)題1 的猜測(cè):
- 針對(duì)問(wèn)題2 的猜測(cè):
- 原理追蹤
- 總結(jié)
問(wèn)題描述
事情開(kāi)始于一段內(nèi)存問(wèn)題,通過(guò)gperf工具抓取進(jìn)程運(yùn)行過(guò)程中的內(nèi)存占用情況。
分析結(jié)果時(shí)發(fā)現(xiàn)一個(gè)有趣的事情,top看到的實(shí)際物理內(nèi)存只有幾兆,但是pprof統(tǒng)計(jì)的內(nèi)存信息卻達(dá)到了幾個(gè)G(其實(shí)這個(gè)問(wèn)題用gperf heap profiler的選項(xiàng)也能很好的驗(yàn)證想法,但是還是想探索一番)。
很明顯是創(chuàng)建線程時(shí)產(chǎn)生的內(nèi)存分配,且最終的分配函數(shù)是__pthread_create_2_1,這是當(dāng)前版本glibc創(chuàng)建線程時(shí)的實(shí)現(xiàn)函數(shù),且在該函數(shù)內(nèi)進(jìn)行線程空間的分配。
查看進(jìn)程代碼,發(fā)現(xiàn)確實(shí)有大量的線程創(chuàng)建,我們知道線程是有自己獨(dú)立的棧空間,top的 RES統(tǒng)計(jì)的是當(dāng)前進(jìn)程占用物理內(nèi)存的情況,也就是當(dāng)用戶(hù)進(jìn)程想要申請(qǐng)物理內(nèi)存的時(shí)候會(huì)發(fā)出缺頁(yè)異常,進(jìn)程切換到內(nèi)核態(tài),由內(nèi)核調(diào)用對(duì)應(yīng)的系統(tǒng)調(diào)用取一部分物理內(nèi)存加入頁(yè)表交給用戶(hù)態(tài)進(jìn)程。這個(gè)時(shí)候,使用的物理內(nèi)存的大小才會(huì)被計(jì)算到RES之中。
回到top數(shù)據(jù)和pprof抓取的內(nèi)存數(shù)據(jù)對(duì)不上的問(wèn)題,難道單獨(dú)線程的創(chuàng)建并不會(huì)占用物理內(nèi)存?
到現(xiàn)在為止可以梳理出以下幾個(gè)問(wèn)題:
- 線程的創(chuàng)建消耗的內(nèi)存在哪里? (猜測(cè)可能在棧上,因?yàn)閠op的VIRT確實(shí)很大)
- 消耗的內(nèi)存大小 是如何判斷的?(目前還不太清楚,不過(guò)以上進(jìn)程代碼是創(chuàng)建了800個(gè)線程,算下來(lái)平均每個(gè)線程的大小是10M了)
問(wèn)題分析
-
為了單獨(dú)聚焦線程創(chuàng)建時(shí)的內(nèi)存分配問(wèn)題,編寫(xiě)如下的簡(jiǎn)單測(cè)試代碼,創(chuàng)建800個(gè)線程:
#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 numstd::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; }單純的創(chuàng)建線程,并不做其他的內(nèi)存分配操作。
-
為了抓取該進(jìn)程的內(nèi)存分配過(guò)程,我們加入gperf工具來(lái)運(yùn)行查看。
#當(dāng)前shell的環(huán)境變量中加入tcmalloc動(dòng)態(tài)庫(kù)的路徑 #如果沒(méi)有tcmalloc,則yum install gperftools即可 env LD_PRELOAD="/usr/lib/libtcmalloc.so"#編譯加入鏈接tcmalloc的選項(xiàng) g++ -std=c++11 test.cpp -pthread -ltcmalloc#使用會(huì)生成heap profile的方式啟動(dòng)進(jìn)程 #開(kāi)啟只監(jiān)控mmap,mremap,sbrk的系統(tǒng)調(diào)用分配內(nèi)存的方式,并且ctrl+c停止運(yùn)行時(shí)生成heap文件 HEAPPROFILESIGNAL=2 HEAP_PROFILE_ONLY_MMAP=true HEAP_PROFILE_INUSE_INTERVAL=1024 HEAPPROFILE=./thread ./a.out -
進(jìn)程運(yùn)行的過(guò)程中我們使用pmap查看進(jìn)程內(nèi)存空間的分配情況
pmap -X PID
輸出信息如下
其中:
address為進(jìn)程的虛擬地址
size為當(dāng)前字段分配的虛擬內(nèi)存的大小,單位是KB
Rss為占用的物理內(nèi)存的大小
Mapping為內(nèi)存所處的區(qū)域
統(tǒng)計(jì)了一下size:10240KB 的區(qū)域剛好是800個(gè),顯然該區(qū)域?yàn)榫€程空間。所處的進(jìn)程內(nèi)存區(qū)域也不在heap上,占用的物理內(nèi)存大小大小也就是一個(gè)指針的大小,8B
使用pmap PID再次查看發(fā)現(xiàn)線程的空間都分布在anno區(qū)域上,即使用的匿名頁(yè)的方式
匿名頁(yè)的描述信息如下:
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.
即匿名頁(yè)是使用mmap方式分配的,且會(huì)將使用的內(nèi)存葉標(biāo)記為MAP_PRIVATE,即僅為進(jìn)程用戶(hù)空間獨(dú)立使用。
針對(duì)問(wèn)題1 的猜測(cè):
到現(xiàn)在為止我們通過(guò)工具發(fā)現(xiàn)了線程的內(nèi)存分配貌似是通過(guò)mmap,使用匿名頁(yè)的方式分配出來(lái)的,因?yàn)槟涿?yè)能夠和其他進(jìn)程共享內(nèi)存空間,所以不會(huì)被計(jì)入當(dāng)前進(jìn)程的物理內(nèi)存區(qū)域。
關(guān)于進(jìn)程的內(nèi)存分布可以參考進(jìn)程內(nèi)存分布,匿名頁(yè)是在堆區(qū)域和棧區(qū)域之間的一部分內(nèi)存區(qū)域,pmap的輸出我們也能看出來(lái)mmapping的那一列。
針對(duì)問(wèn)題2 的猜測(cè):
那為什么會(huì)占用10M的虛擬內(nèi)存呢(size那一列),顯然也很好理解了。因?yàn)榫€程是獨(dú)享自己的棧空間的,所以需要為每個(gè)線程開(kāi)辟屬于自己的函數(shù)棧空間來(lái)保存函數(shù)棧幀和局部變量。
ulimit -a能夠看到stack size 那一行是屬于當(dāng)前系統(tǒng)默認(rèn)的進(jìn)程棧空間的大小。
這里可以通過(guò)ulimit -s 2048 將系統(tǒng)的默認(rèn)分配的棧的大小設(shè)置為2M,再次運(yùn)行程序會(huì)發(fā)現(xiàn)線程的虛擬內(nèi)存占用變?yōu)榱?M
是不是很有趣。
到了這里,我們僅僅是使用工具進(jìn)行了線程內(nèi)存的占用分析,但問(wèn)題并沒(méi)有追到底層。
原理追蹤
我們上面使用了gperf的heap proflie運(yùn)行了程序,此時(shí)我們ctrl+c終端進(jìn)程之后會(huì)在當(dāng)前目錄下生成很多個(gè).heap文件,使用pprof 的svg選項(xiàng)將文件內(nèi)容導(dǎo)出
pprof --svg a.out thread.0001.heap > thread.svg
將導(dǎo)出的thread.svg放入瀏覽器中可以看到線程內(nèi)存占用的一個(gè)calltrace,如下(如果程序中鏈入了glibc以及內(nèi)核的靜態(tài)庫(kù),估計(jì)calltrace會(huì)龐大很多):
也就是線程創(chuàng)建時(shí)的棧空間的分配最終是由函數(shù)__pthread_create_2_1分配的。
PS:這里的calltrace 僅僅包括mmap,mremap,sbrk的分配,因?yàn)槲覀冊(cè)谶M(jìn)程運(yùn)行的時(shí)候指定了HEAP_PROFILE_ONLY_MMAP=true 選項(xiàng),如果各位僅僅想要確認(rèn)malloc,calloc,realloc等在堆上分配的內(nèi)存大小可以去掉該選項(xiàng)來(lái)運(yùn)行進(jìn)程。
輸出svg的時(shí)候增加pprof的--ignore選項(xiàng)來(lái)忽略mmap,sbrk的分配內(nèi)存,這樣的calltrace就沒(méi)有他們的內(nèi)存占用了,僅包括堆上的內(nèi)存占用
pprof --ignore='DoAllocWithArena|SbrkSysAllocator::Alloc|MmapSysAllocator::Alloc' --svg a.out thread.0001.heap > thread.svg
查看glibc的線程創(chuàng)建源碼pthread_create.c
函數(shù)__pthread_create_2_1 調(diào)用ALLOCATE_STACK為線程的數(shù)據(jù)結(jié)構(gòu)pd分配內(nèi)存空間。
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函數(shù)實(shí)現(xiàn)入下allocatestack.c:
分配的空間大小會(huì)優(yōu)先從用戶(hù)設(shè)置的pthread_attr屬性 attr.stacksize中獲取,如果用戶(hù)進(jìn)程沒(méi)有設(shè)置stacksize,就會(huì)獲取系統(tǒng)默認(rèn)的stacksize的大小。
接下來(lái)會(huì)調(diào)用get_cached_stack函數(shù)來(lái)獲取棧上面可以獲得的空間大小size以及所處的虛擬內(nèi)存空間的地址mem。
最后通過(guò)mmap將當(dāng)前線程所需要的內(nèi)存葉標(biāo)記為MAP_PRIVATE和MAP_ANONYMOUS表示當(dāng)前內(nèi)存區(qū)域僅屬于用戶(hù)進(jìn)程且被用戶(hù)進(jìn)程共享。
詳細(xì)實(shí)現(xiàn)如下:
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 weuse 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 weadjust the allocated stack size if necessary. This wayallocations directly following each other will not havealiasing problems. */#if MULTI_PAGE_ALIASING != 0if ((size % MULTI_PAGE_ALIASING) == 0)size += pagesize_m1 + 1;#endif/*mmap分配物理內(nèi)存,并進(jìn)行內(nèi)存區(qū)域的標(biāo)記*/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;}
總結(jié)
glibc用戶(hù)態(tài)的調(diào)用到最后仍然還是內(nèi)核態(tài)進(jìn)行實(shí)際的物理操作。
至此,關(guān)于線程創(chuàng)建時(shí)的內(nèi)存分配追蹤就到這里了。我們會(huì)發(fā)現(xiàn)操作系統(tǒng)的博大精深和環(huán)環(huán)相扣,使用一個(gè)個(gè)工具驗(yàn)證自己的猜測(cè), 再?gòu)脑戆l(fā)掘前人的設(shè)計(jì),這樣就會(huì)對(duì)整個(gè)鏈路有了一個(gè)更加深刻的理解。
至于更加底層的內(nèi)核實(shí)現(xiàn),如何將物理內(nèi)存與用戶(hù)進(jìn)程進(jìn)行隔離且互不影響,這又是一段龐大復(fù)雜的設(shè)計(jì)鏈路。有趣的事情很多,慢慢來(lái)~
總結(jié)
以上是生活随笔為你收集整理的Linux创建线程时 内存分配的那些事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 求助!!!我父亲通过中介到国外打工,已经
- 下一篇: skiplist跳表的 实现