Linux創建線程時 內存分配的那些事【轉】


轉自:https://blog.csdn.net/Z_Stand/article/details/106444952

文章目錄
問題描述
問題分析
針對問題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;
}

總結
glibc用戶態的調用到最后仍然還是內核態進行實際的物理操作。
至此,關於線程創建時的內存分配追蹤就到這里了。我們會發現操作系統的博大精深和環環相扣,使用一個個工具驗證自己的猜測, 再從原理發掘前人的設計,這樣就會對整個鏈路有了一個更加深刻的理解。

至於更加底層的內核實現,如何將物理內存與用戶進程進行隔離且互不影響,這又是一段龐大復雜的設計鏈路。有趣的事情很多,慢慢來~
————————————————
版權聲明:本文為CSDN博主「z_stand」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/Z_Stand/article/details/106444952


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM